Initial commit

This commit is contained in:
Samer Afach 2018-03-27 22:16:42 +02:00
commit 2793e49ea9
13 changed files with 563 additions and 0 deletions

0
README.md Normal file
View File

View 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()

View 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)

View 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_()

View 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)

View 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

View 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

View 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)

View File

@ -0,0 +1,2 @@
from SamAuthenticator.Authenticator import *
from SamAuthenticator.AuthenticatorGUIApp import *

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

4
main.py Normal file
View File

@ -0,0 +1,4 @@
import SamAuthenticator as auth
auth.start()