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