Initial commit
This commit is contained in:
commit
2793e49ea9
36
SamAuthenticator/AddKeyDialog.py
Normal file
36
SamAuthenticator/AddKeyDialog.py
Normal file
@ -0,0 +1,36 @@
|
||||
from PyQt5.QtWidgets import QDialog, QMainWindow, QWidget, QTableView, QGridLayout, QLineEdit, QAction, qApp, QInputDialog, \
|
||||
QMessageBox, QPushButton
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
|
||||
|
||||
class AddKeyDialog(QDialog):
|
||||
new_key_to_add_signal = pyqtSignal(str, str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.main_layout = QGridLayout()
|
||||
|
||||
self.name_field = QLineEdit()
|
||||
self.name_field.setPlaceholderText("Name")
|
||||
self.key_field = QLineEdit()
|
||||
self.key_field.setPlaceholderText("Secret Key")
|
||||
self.ok_button = QPushButton("OK")
|
||||
|
||||
self.main_layout.addWidget(self.name_field, 0, 0, 1, 1)
|
||||
self.main_layout.addWidget(self.key_field, 1, 0, 1, 1)
|
||||
self.main_layout.addWidget(self.ok_button, 2, 0, 1, 1)
|
||||
|
||||
self.setLayout(self.main_layout)
|
||||
|
||||
self.ok_button.clicked.connect(self.ok_clicked)
|
||||
|
||||
def get_new_key(self):
|
||||
self.setModal(True)
|
||||
self.name_field.setFocus()
|
||||
self.name_field.setText("")
|
||||
self.key_field.setText("")
|
||||
self.open()
|
||||
|
||||
def ok_clicked(self):
|
||||
self.new_key_to_add_signal.emit(self.name_field.text(), self.key_field.text())
|
||||
self.close()
|
79
SamAuthenticator/Authenticator.py
Normal file
79
SamAuthenticator/Authenticator.py
Normal file
@ -0,0 +1,79 @@
|
||||
import onetimepass as otp
|
||||
import json
|
||||
import cryptography.fernet
|
||||
import argon2
|
||||
import base64
|
||||
import os
|
||||
import copy
|
||||
|
||||
_salt = "V5RlhpuwACffXuUNLex7Al9ulPy4SRHbyaAxWigjX9Z01OVaCO"
|
||||
|
||||
|
||||
def GetDefaultSalt():
|
||||
return copy.deepcopy(_salt)
|
||||
|
||||
def encrypt_data(data_bytes, password, salt):
|
||||
password_hash = argon2.argon2_hash(password=password, salt=salt)
|
||||
encoded_hash = base64.urlsafe_b64encode(password_hash[:32])
|
||||
encryptor = cryptography.fernet.Fernet(encoded_hash)
|
||||
return encryptor.encrypt(data_bytes)
|
||||
|
||||
|
||||
def decrypt_data(cipher_bytes, password, salt):
|
||||
password_hash = argon2.argon2_hash(password=password, salt=salt)
|
||||
encoded_hash = base64.urlsafe_b64encode(password_hash[:32])
|
||||
decryptor = cryptography.fernet.Fernet(encoded_hash)
|
||||
return decryptor.decrypt(cipher_bytes)
|
||||
|
||||
|
||||
def write_keys_to_file(auth_keys, password, file_name="data.dat"):
|
||||
backup_file = file_name + ".bak"
|
||||
if os.path.exists(file_name):
|
||||
os.rename(file_name, backup_file)
|
||||
with open(file_name, 'wb') as f:
|
||||
f.write(encrypt_data(auth_keys.dump_data().encode(), password, _salt))
|
||||
if os.path.exists(backup_file):
|
||||
os.remove(backup_file)
|
||||
|
||||
|
||||
def read_keys_from_file(password, file_name="data.dat"):
|
||||
with open(file_name, 'rb') as f:
|
||||
ciphered_data = f.read()
|
||||
readable_data = decrypt_data(ciphered_data, password, _salt)
|
||||
keys_object = AuthenticatorKeys()
|
||||
keys_object.read_dump(readable_data.decode())
|
||||
return keys_object
|
||||
|
||||
|
||||
class AuthenticatorKeys:
|
||||
def __init__(self):
|
||||
self.data = {'secrets': {},
|
||||
'version': '1.0'}
|
||||
|
||||
def set_secret(self, name, secret):
|
||||
self.data['secrets'][name] = {'secret': secret}
|
||||
|
||||
def get_secret(self, name):
|
||||
return self.data['secrets'][name]['secret']
|
||||
|
||||
def get_token(self, name):
|
||||
return otp.get_totp(self.get_secret(name))
|
||||
|
||||
def remove_secret(self, name):
|
||||
del self.data['secrets'][name]
|
||||
|
||||
def get_names(self):
|
||||
return self.data['secrets'].keys()
|
||||
|
||||
def get_size(self):
|
||||
return len(self.data['secrets'])
|
||||
|
||||
def dump_data(self):
|
||||
return json.dumps(self.data)
|
||||
|
||||
def read_dump(self, dump_data):
|
||||
self.data = json.loads(dump_data)
|
||||
|
||||
@staticmethod
|
||||
def test_secret_validity(secret):
|
||||
otp.get_totp(secret)
|
13
SamAuthenticator/AuthenticatorGUIApp.py
Normal file
13
SamAuthenticator/AuthenticatorGUIApp.py
Normal file
@ -0,0 +1,13 @@
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
import SamAuthenticator.AuthenticatorWindow as MainWindow
|
||||
import sys
|
||||
|
||||
|
||||
def start():
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
w = MainWindow.AuthenticatorGUI()
|
||||
|
||||
w.show()
|
||||
|
||||
return app.exec_()
|
231
SamAuthenticator/AuthenticatorWindow.py
Normal file
231
SamAuthenticator/AuthenticatorWindow.py
Normal file
@ -0,0 +1,231 @@
|
||||
from PyQt5.QtWidgets import QMainWindow, QWidget, QGridLayout, QLineEdit, QAction, qApp, QInputDialog, \
|
||||
QMessageBox, QFileDialog
|
||||
from PyQt5.QtCore import QSortFilterProxyModel, pyqtSlot, QSettings
|
||||
from PyQt5.Qt import Qt, QIcon, QSizePolicy
|
||||
import SamAuthenticator.KeysDataModel as model
|
||||
import SamAuthenticator.KeyDataView as dataview
|
||||
import SamAuthenticator.Authenticator as auth
|
||||
import SamAuthenticator.TrayIcon as tray
|
||||
import functools
|
||||
import os
|
||||
|
||||
|
||||
class AuthenticatorGUI(QMainWindow):
|
||||
new_key_to_add_slot = pyqtSlot(str, str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.data_file_name = "data.dat"
|
||||
|
||||
self.load_geometry()
|
||||
|
||||
current_path = os.path.dirname(os.path.abspath(__file__))
|
||||
main_icon = QIcon(os.path.join(current_path, "images/key.png"))
|
||||
# self.setWindowIcon(main_icon)
|
||||
|
||||
self.tray_icon = tray.SamAuthenticatorTrayIcon(self, main_icon)
|
||||
self.tray_icon.show()
|
||||
|
||||
self.mainWidget = QWidget()
|
||||
self.setCentralWidget(self.mainWidget)
|
||||
|
||||
self.main_layout = QGridLayout()
|
||||
self.filter_line_edit = QLineEdit()
|
||||
self.filter_line_edit.setPlaceholderText("Enter filter (Ctrl+F)")
|
||||
self.keys_table_view = dataview.KeyDataView()
|
||||
|
||||
self.keys_data_model = None
|
||||
self.keys_data_model_proxy = None
|
||||
self.setup_data_model(auth.AuthenticatorKeys())
|
||||
|
||||
self.main_layout.addWidget(self.filter_line_edit, 1, 0, 1, 2)
|
||||
self.main_layout.addWidget(self.keys_table_view, 2, 0, 1, 2)
|
||||
|
||||
self.mainWidget.setLayout(self.main_layout)
|
||||
|
||||
self.filter_line_edit.textChanged.connect(self.set_filter_string)
|
||||
|
||||
self.add_menus()
|
||||
self.add_toolbar()
|
||||
|
||||
self.setWindowTitle('Sam Authenticator')
|
||||
self.show()
|
||||
self.load_data_from_default_path()
|
||||
|
||||
def set_filter_string(self, filter_str):
|
||||
if self.keys_data_model_proxy is not None:
|
||||
self.keys_data_model_proxy.setFilterWildcard(filter_str)
|
||||
|
||||
def set_data_file_name(self, file_name):
|
||||
self.data_file_name = file_name
|
||||
|
||||
def import_data(self):
|
||||
file_name = QFileDialog.getOpenFileName(self, "Import data from...", "", "Encrypted data (*.dat, *.*)")
|
||||
# if a file is chosen
|
||||
if file_name[1]:
|
||||
if os.path.exists(file_name[0]):
|
||||
self.load_data_from(file_name[0])
|
||||
else:
|
||||
QMessageBox.warning(self, "File not found", "The path you chose doesn't contain a file.")
|
||||
|
||||
def load_data_from_default_path(self):
|
||||
if not os.path.exists(self.data_file_name):
|
||||
return
|
||||
|
||||
self.load_data_from(self.data_file_name)
|
||||
self.filter_line_edit.clear()
|
||||
|
||||
def decrypt_data_file(self):
|
||||
source_file = QFileDialog.getOpenFileName(self, "Choose the file to decrypt...", "", "Encrypted data (*.dat, *.*)")
|
||||
# if a file is chosen
|
||||
if not source_file[1]:
|
||||
return
|
||||
|
||||
if not os.path.exists(source_file[0]):
|
||||
QMessageBox.warning(self, "File not found", "The path you chose doesn't contain a file.")
|
||||
return
|
||||
|
||||
dest_file = QFileDialog.getSaveFileName(self, "Save decrypted file as...", "", "JSON file (.json)")
|
||||
# if a file is chosen
|
||||
if not dest_file[1]:
|
||||
return
|
||||
|
||||
password_from_dialog = QInputDialog.getText(self, "Input encryption password",
|
||||
"Encryption password:",
|
||||
QLineEdit.Password, "")
|
||||
ok_pressed = password_from_dialog[1]
|
||||
if not ok_pressed:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(source_file[0], 'rb') as f_load:
|
||||
ciphered_data = f_load.read()
|
||||
readable_data = auth.decrypt_data(ciphered_data, password_from_dialog[0], auth.GetDefaultSalt())
|
||||
|
||||
with open(dest_file[0], 'wb') as f_save:
|
||||
f_save.write(readable_data)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Error", "Decryption failed. " + str(e))
|
||||
|
||||
def load_data_from(self, data_file_path):
|
||||
password_from_dialog = QInputDialog.getText(self, "Input encryption password",
|
||||
"Encryption password:",
|
||||
QLineEdit.Password, "")
|
||||
ok_pressed = password_from_dialog[1]
|
||||
if not ok_pressed:
|
||||
return
|
||||
|
||||
try:
|
||||
keys = auth.read_keys_from_file(password_from_dialog[0], data_file_path)
|
||||
self.setup_data_model(keys)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Unable to read data", "Unable to read data. " + str(e))
|
||||
|
||||
def save_data(self, data_file):
|
||||
pass_from_dialog = QInputDialog.getText(self, "Input encryption password",
|
||||
"New encryption password:",
|
||||
QLineEdit.Password, "")
|
||||
ok_pressed = pass_from_dialog[1]
|
||||
if not ok_pressed:
|
||||
return
|
||||
|
||||
password = pass_from_dialog[0]
|
||||
|
||||
if self.keys_data_model is not None:
|
||||
auth.write_keys_to_file(self.keys_data_model.getKeysObject(), password, data_file)
|
||||
else:
|
||||
QMessageBox.warning(self, "No data loaded", "Data should be loaded before attempting to save it")
|
||||
|
||||
def save_data_as(self):
|
||||
file_name = QFileDialog.getSaveFileName(self, "Save data as...", "", "Encrypted data (*.dat)")
|
||||
# if a file is chosen
|
||||
if file_name[1]:
|
||||
self.save_data(file_name[0])
|
||||
|
||||
def add_menus(self):
|
||||
exit_act = QAction('&Exit', self)
|
||||
exit_act.setShortcut('Ctrl+Q')
|
||||
exit_act.setStatusTip('Exit application')
|
||||
exit_act.triggered.connect(qApp.quit)
|
||||
|
||||
reload_data_act = QAction('&Reload data from default location (' + self.data_file_name + ')' , self)
|
||||
reload_data_act.setStatusTip('Reload data from file')
|
||||
reload_data_act.setShortcut('Ctrl+R')
|
||||
reload_data_act.triggered.connect(self.load_data_from_default_path)
|
||||
|
||||
save_data_act = QAction('&Save data to default location (' + self.data_file_name + ')', self)
|
||||
save_data_act.setShortcut('Ctrl+S')
|
||||
save_data_act.setStatusTip('Save data to the default path')
|
||||
save_data_act.triggered.connect(functools.partial(self.save_data, self.data_file_name))
|
||||
|
||||
save_data_as_act = QAction('&Save data as...', self)
|
||||
save_data_as_act.setShortcut('Ctrl+Shift+S')
|
||||
save_data_as_act.setStatusTip('Save data to...')
|
||||
save_data_as_act.triggered.connect(self.save_data_as)
|
||||
|
||||
import_data_act = QAction('&Import data from...' , self)
|
||||
import_data_act.setStatusTip('Import data from a file...')
|
||||
import_data_act.triggered.connect(self.import_data)
|
||||
|
||||
decrypt_data_file_act = QAction('&Decrypt a data file', self)
|
||||
decrypt_data_file_act.setStatusTip('Decrypt a data file to raw text (unsafe)')
|
||||
decrypt_data_file_act.triggered.connect(self.decrypt_data_file)
|
||||
|
||||
QAction("File")
|
||||
file_menu = self.menuBar().addMenu('&File')
|
||||
file_menu.addAction(reload_data_act)
|
||||
file_menu.addAction(save_data_act)
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction(import_data_act)
|
||||
file_menu.addAction(save_data_as_act)
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction(decrypt_data_file_act)
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction(exit_act)
|
||||
|
||||
def add_toolbar(self):
|
||||
toolbar = self.addToolBar("File")
|
||||
|
||||
left_spacer = QWidget()
|
||||
left_spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
right_spacer = QWidget()
|
||||
right_spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
current_path = os.path.dirname(os.path.abspath(__file__))
|
||||
toolbar.addWidget(left_spacer)
|
||||
add_act = QAction(QIcon(os.path.join(current_path, "images/add.png")), "Add new key", self)
|
||||
add_act.setShortcut('Ctrl+A')
|
||||
toolbar.addAction(add_act)
|
||||
remove_act = QAction(QIcon(os.path.join(current_path, "images/delete.png")), "Remove selected key", self)
|
||||
remove_act.setShortcut('Ctrl+D')
|
||||
toolbar.addAction(remove_act)
|
||||
toolbar.addWidget(right_spacer)
|
||||
|
||||
add_act.triggered.connect(self.keys_table_view.add_new_key_from_dialog)
|
||||
remove_act.triggered.connect(self.keys_table_view.remove_row)
|
||||
|
||||
def setup_data_model(self, keys):
|
||||
self.keys_data_model_proxy = QSortFilterProxyModel()
|
||||
self.keys_data_model_proxy.setFilterCaseSensitivity(Qt.CaseInsensitive)
|
||||
self.keys_data_model = model.AuthenticatorKeysDataModel(keys)
|
||||
self.keys_data_model_proxy.setSourceModel(self.keys_data_model)
|
||||
self.keys_table_view.setModel(self.keys_data_model_proxy)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == Qt.Key_C and event.modifiers() & Qt.ControlModifier:
|
||||
self.keys_table_view.copy_selected()
|
||||
if event.key() == Qt.Key_F and event.modifiers() & Qt.ControlModifier:
|
||||
self.filter_line_edit.setFocus()
|
||||
self.filter_line_edit.selectAll()
|
||||
if event.key() == Qt.Key_Escape:
|
||||
self.filter_line_edit.clear()
|
||||
|
||||
def load_geometry(self):
|
||||
settings = QSettings("SamApps", "SamAuthenticator")
|
||||
self.restoreGeometry(settings.value("geometry"))
|
||||
|
||||
def closeEvent(self, event):
|
||||
settings = QSettings("SamApps", "SamAuthenticator")
|
||||
settings.setValue("geometry", self.saveGeometry())
|
||||
super().closeEvent(event)
|
70
SamAuthenticator/KeyDataView.py
Normal file
70
SamAuthenticator/KeyDataView.py
Normal file
@ -0,0 +1,70 @@
|
||||
from PyQt5.QtWidgets import QTableView, QAction, QMenu, QMessageBox, qApp
|
||||
from PyQt5.Qt import Qt, QHeaderView, QCursor
|
||||
from SamAuthenticator.AddKeyDialog import AddKeyDialog
|
||||
import SamAuthenticator.KeysDataModel as model
|
||||
|
||||
|
||||
class KeyDataView(QTableView):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.add_key_dialog = AddKeyDialog()
|
||||
self.add_key_dialog.new_key_to_add_signal.connect(self.add_new_key)
|
||||
|
||||
self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
self.setSelectionBehavior(QTableView.SelectRows)
|
||||
self.setSelectionMode(QTableView.SingleSelection)
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.contextMenuEvent)
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
table_context_menu = QMenu(self)
|
||||
menu = QMenu(self)
|
||||
copy_action = QAction('Copy', self)
|
||||
copy_action.triggered.connect(self.copy_selected)
|
||||
add_key_action = QAction('Add new key', self)
|
||||
add_key_action.triggered.connect(self.add_new_key_from_dialog)
|
||||
remove_key_action = QAction('Remove key', self)
|
||||
remove_key_action.triggered.connect(self.remove_row)
|
||||
|
||||
menu.addAction(copy_action)
|
||||
menu.addSeparator()
|
||||
menu.addAction(add_key_action)
|
||||
if len(self.selectedIndexes()) > 0:
|
||||
menu.addAction(remove_key_action)
|
||||
|
||||
menu.popup(QCursor.pos())
|
||||
|
||||
def copy_selected(self):
|
||||
cells = self.selectedIndexes()
|
||||
if len(cells) == 0:
|
||||
return
|
||||
if len(cells) != self.model().columnCount():
|
||||
return
|
||||
for el in cells:
|
||||
if el.column() == model.AuthenticatorKeysDataModel.TOKEN_COL:
|
||||
qApp.clipboard().setText(str(el.data()))
|
||||
|
||||
def remove_row(self):
|
||||
cells = self.selectedIndexes()
|
||||
if len(cells) == 0:
|
||||
return
|
||||
if len(cells) != self.model().columnCount():
|
||||
return
|
||||
for el in cells:
|
||||
row = el.row()
|
||||
self.model().removeRow(row)
|
||||
return
|
||||
|
||||
def add_new_key_from_dialog(self):
|
||||
self.add_key_dialog.get_new_key()
|
||||
|
||||
def add_new_key(self, name, secret):
|
||||
try:
|
||||
self.model().getKeysObject().test_secret_validity(secret)
|
||||
self.model().getKeysObject().set_secret(name, secret)
|
||||
self.model().refreshAll()
|
||||
except Exception as e:
|
||||
self.add_key_dialog.close()
|
||||
QMessageBox.warning(self, "Error", "Testing the secret you entered failed. " + str(e))
|
||||
return
|
106
SamAuthenticator/KeysDataModel.py
Normal file
106
SamAuthenticator/KeysDataModel.py
Normal file
@ -0,0 +1,106 @@
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5.Qt import Qt, pyqtSignal
|
||||
import SamAuthenticator.Authenticator as auth
|
||||
|
||||
|
||||
class AuthenticatorKeysDataModel(QtCore.QAbstractTableModel):
|
||||
tokensUpdatedSignal = pyqtSignal()
|
||||
|
||||
NAME_COL = 0
|
||||
TOKEN_COL = 1
|
||||
|
||||
UpdateTimerPeriod = 2000
|
||||
|
||||
def __init__(self, authenticator_keys: auth.AuthenticatorKeys, parent=None):
|
||||
QtCore.QAbstractTableModel.__init__(self, parent)
|
||||
self._keys = authenticator_keys
|
||||
self.updateTimer = QtCore.QTimer()
|
||||
self.updateTimer.start(self.UpdateTimerPeriod)
|
||||
self.updateTimer.timeout.connect(self.updateTokens)
|
||||
|
||||
def getKeysObject(self):
|
||||
return self._keys
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
return self._keys.get_size()
|
||||
|
||||
def columnCount(self, parent=None):
|
||||
return 2
|
||||
|
||||
def data(self, index, role=QtCore.Qt.DisplayRole):
|
||||
if index.isValid():
|
||||
if role == QtCore.Qt.EditRole:
|
||||
self.updateTimer.stop()
|
||||
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
|
||||
all_names = sorted(list(self._keys.get_names()))
|
||||
if index.column() == self.NAME_COL:
|
||||
return all_names[index.row()]
|
||||
if index.column() == self.TOKEN_COL:
|
||||
try:
|
||||
return str(self._keys.get_token(all_names[index.row()])).zfill(6)
|
||||
except Exception as e:
|
||||
return "<error: " + str(e) + ">"
|
||||
if role == Qt.TextAlignmentRole:
|
||||
return Qt.AlignCenter
|
||||
return None
|
||||
|
||||
def setData(self, index, value, role=QtCore.Qt.DisplayRole):
|
||||
try:
|
||||
if index.column() == self.NAME_COL:
|
||||
if index.data() != value:
|
||||
old_name = str(index.data())
|
||||
new_name = str(value)
|
||||
self._keys.set_secret(new_name, self._keys.get_secret(old_name))
|
||||
self._keys.remove_secret(old_name)
|
||||
self.dataChanged.emit(index, index)
|
||||
return True
|
||||
finally:
|
||||
self.updateTimer.start(self.UpdateTimerPeriod)
|
||||
return False
|
||||
|
||||
def headerData(self, rowcol, orientation, role=QtCore.Qt.DisplayRole):
|
||||
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
|
||||
if rowcol == self.NAME_COL:
|
||||
return "Name"
|
||||
if rowcol == self.TOKEN_COL:
|
||||
return "Token"
|
||||
if role == Qt.TextAlignmentRole:
|
||||
return Qt.AlignCenter
|
||||
return None
|
||||
|
||||
def flags(self, index):
|
||||
flags = super(self.__class__, self).flags(index)
|
||||
if index.isValid():
|
||||
l = sorted(list(self._keys.get_names()))
|
||||
if index.column() == self.NAME_COL:
|
||||
flags |= QtCore.Qt.ItemIsEditable
|
||||
flags |= QtCore.Qt.ItemIsSelectable
|
||||
flags |= QtCore.Qt.ItemIsEnabled
|
||||
return flags
|
||||
if index.column() == self.TOKEN_COL:
|
||||
return flags
|
||||
|
||||
# flags |= QtCore.Qt.ItemIsEditable
|
||||
# flags |= QtCore.Qt.ItemIsSelectable
|
||||
# flags |= QtCore.Qt.ItemIsEnabled
|
||||
# flags |= QtCore.Qt.ItemIsDragEnabled
|
||||
# flags |= QtCore.Qt.ItemIsDropEnabled
|
||||
return flags
|
||||
|
||||
def updateTokens(self):
|
||||
self.dataChanged.emit(self.index(1, 0), self.index(1, self._keys.get_size() - 1))
|
||||
self.tokensUpdatedSignal.emit()
|
||||
|
||||
def refreshAll(self):
|
||||
self.beginResetModel()
|
||||
self.endResetModel()
|
||||
|
||||
def removeRows(self, start_row, count, parent=None, *args, **kwargs):
|
||||
self.beginRemoveRows(QtCore.QModelIndex(), start_row, start_row + count - 1)
|
||||
to_remove = []
|
||||
for i in range(start_row, start_row + count):
|
||||
to_remove.append(self.data(self.index(i, self.NAME_COL), role=QtCore.Qt.DisplayRole))
|
||||
for name in to_remove:
|
||||
self._keys.remove_secret(name)
|
||||
self.endRemoveRows()
|
||||
return True
|
22
SamAuthenticator/TrayIcon.py
Normal file
22
SamAuthenticator/TrayIcon.py
Normal file
@ -0,0 +1,22 @@
|
||||
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, qApp
|
||||
from PyQt5.Qt import QIcon
|
||||
import os
|
||||
|
||||
|
||||
class SamAuthenticatorTrayIcon(QSystemTrayIcon):
|
||||
IconTooltip_normal = "Sam Authenticator"
|
||||
|
||||
def __init__(self, main_win, icon, parent=None):
|
||||
QSystemTrayIcon.__init__(self, parent)
|
||||
|
||||
self.setIcon(icon)
|
||||
|
||||
self.main_win = main_win
|
||||
self.menu = QMenu(parent)
|
||||
self.show_action = self.menu.addAction("Show")
|
||||
self.menu.addSeparator()
|
||||
self.exit_action = self.menu.addAction("Exit")
|
||||
self.setContextMenu(self.menu)
|
||||
self.exit_action.triggered.connect(qApp.quit)
|
||||
self.show_action.triggered.connect(self.main_win.raise_)
|
||||
self.setToolTip(self.IconTooltip_normal)
|
2
SamAuthenticator/__init__.py
Normal file
2
SamAuthenticator/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from SamAuthenticator.Authenticator import *
|
||||
from SamAuthenticator.AuthenticatorGUIApp import *
|
BIN
SamAuthenticator/images/add.png
Normal file
BIN
SamAuthenticator/images/add.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 341 B |
BIN
SamAuthenticator/images/delete.png
Normal file
BIN
SamAuthenticator/images/delete.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 463 B |
BIN
SamAuthenticator/images/key.png
Normal file
BIN
SamAuthenticator/images/key.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 137 KiB |
Loading…
Reference in New Issue
Block a user