Initial commit
This commit is contained in:
		
							
								
								
									
										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  | 
		Reference in New Issue
	
	Block a user