Browse Source

Allow using per-domain settings

master
Johann Schmitz 3 years ago
parent
commit
04b3aec596
Signed by: ercpe GPG Key ID: A084064277C501ED
  1. 38
      esgp/config.py
  2. 181
      esgp/settings.py
  3. 77
      esgp/ui.py

38
esgp/config.py

@ -23,25 +23,49 @@ PATH = os.path.abspath(os.path.expanduser("~/.esgp.cfg"))
class Configuration(object):
def __init__(self):
self.algorithm = 'md5'
self.algorithm = 'MD5'
self.length = 10
self.domain_settings = []
def read(self):
parser = ConfigParser()
parser.read(PATH)
if parser.has_section('esgp'):
self.algorithm = parser.get('esgp', 'algorithm', fallback=self.algorithm)
self.length = int(parser.get('esgp', 'length', fallback=self.length))
if parser.has_section('defaults'):
self.algorithm = parser.get('defaults', 'algorithm', fallback=self.algorithm)
self.length = int(parser.get('defaults', 'length', fallback=self.length))
for section in parser.sections():
if section == "defaults":
continue
self.domain_settings.append({
'domain': section,
'algorithm': parser.get(section, 'algorithm', fallback=self.algorithm),
'length': parser.get(section, 'length', fallback=self.length)
})
def write(self):
parser = ConfigParser()
parser.add_section('esgp')
parser.set('esgp', 'algorithm', self.algorithm)
parser.set('esgp', 'length', str(self.length))
parser.add_section('defaults')
parser.set('defaults', 'algorithm', self.algorithm)
parser.set('defaults', 'length', str(self.length))
for settings in self.domain_settings:
domain = settings['domain']
if not domain:
continue
parser.add_section(domain)
parser.set(domain, 'algorithm', settings['algorithm'])
parser.set(domain, 'length', str(settings['length']))
with open(PATH, 'w') as o:
parser.write(o)
def get_domain_settings(self, domain):
for d in self.domain_settings:
if d.get('domain', '') == domain:
return d

181
esgp/settings.py

@ -0,0 +1,181 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Johann Schmitz
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QModelIndex, QRegExp
from PyQt5.QtGui import QIntValidator, QRegExpValidator
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFrame, QLabel, QTableView, QLineEdit, \
QStyledItemDelegate, QPushButton, QComboBox, QHBoxLayout, QTableWidget, QAbstractItemView, QHeaderView
class DomainSettingsTableModel(QAbstractTableModel):
properties = ['domain', 'length', 'algorithm']
def __init__(self, config, parent):
self._data = config.domain_settings
self.default_length = config.length
self.default_algorithm = config.algorithm
super(DomainSettingsTableModel, self).__init__(parent)
def rowCount(self, parent=None, *args, **kwargs):
return len(self._data)
def columnCount(self, parent=None, *args, **kwargs):
return 3
def data(self, index, role=None):
if not index.isValid() or role not in (Qt.DisplayRole, Qt.EditRole):
return QVariant()
value = self._data[index.row()][self.properties[index.column()]]
if index.column() == 2 and role == Qt.EditRole:
value = value.upper()
return value
def headerData(self, col, orientation, role=None):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
if col == 0:
return "Domain"
if col == 1:
return "Length"
if col == 2:
return "Algorithm"
return ""
def setData(self, index, value, role=None):
if index.isValid() and 0 <= index.row() <= self.rowCount():
self._data[index.row()][self.properties[index.column()]] = value
return True
return False
def flags(self, index):
defaultFlags = super(DomainSettingsTableModel, self).flags(index)
if index.isValid():
return defaultFlags | Qt.ItemIsEditable
return defaultFlags
def insertRow(self, p_int, parent=None, *args, **kwargs):
self.beginInsertRows(QModelIndex(), p_int, p_int)
self._data.insert(p_int, {
'domain': '',
'length': self.default_length,
'algorithm': self.default_algorithm
})
self.endInsertRows()
def removeRow(self, p_int, parent=None, *args, **kwargs):
self.beginRemoveRows(QModelIndex(), p_int, p_int)
self._data.remove(self._data[p_int])
self.endRemoveRows()
class SettingsItemDelegate(QStyledItemDelegate):
def createEditor(self, widget, option, index):
if not index.isValid():
return None
if index.column() == 0:
editor = QLineEdit(widget)
editor.setValidator(QRegExpValidator(QRegExp('.+'), self))
editor.setText(index.model().data(index, Qt.EditRole))
return editor
if index.column() == 1:
editor = QLineEdit(widget)
editor.setValidator(QIntValidator(0, 99, self))
editor.setText(str(index.model().data(index, Qt.EditRole)))
return editor
if index.column() == 2:
editor = QComboBox(parent=widget)
editor.addItem("MD5")
editor.addItem("SHA")
editor.setCurrentIndex(editor.findText(index.model().data(index, Qt.EditRole)))
return editor
return super(SettingsItemDelegate, self).createEditor(widget, option, index)
class SettingsDialog(QDialog):
def __init__(self, config, *args, **kwargs):
self.config = config
super(SettingsDialog, self).__init__(*args, **kwargs)
self.domain_settings_model = None
self.del_button = None
self.build_ui()
def build_ui(self):
self.setWindowTitle("Advanced settings")
self.setFixedWidth(400)
layout = QVBoxLayout()
self.setLayout(layout)
layout.addWidget(self.build_domain_settings_ui())
save_button = QPushButton('Save')
save_button.clicked.connect(self.save_and_close)
layout.addWidget(save_button)
def build_domain_settings_ui(self):
# per-domain settings
domain_settings_layout = QVBoxLayout()
domain_settings_layout.addWidget(QLabel("Per-domain settings"))
self.domain_settings_model = DomainSettingsTableModel(self.config, self)
self.domain_settings_table = QTableView()
self.domain_settings_table.setItemDelegate(SettingsItemDelegate())
self.domain_settings_table.setModel(self.domain_settings_model)
self.domain_settings_table.setSelectionMode(QAbstractItemView.SingleSelection)
self.domain_settings_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.domain_settings_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
self.domain_settings_table.clicked.connect(self.selected)
domain_settings_layout.addWidget(self.domain_settings_table)
add_button = QPushButton('+')
add_button.clicked.connect(self.add_domain_settings_row)
self.del_button = QPushButton('-')
self.del_button.clicked.connect(self.delete_domain_settings_row)
self.del_button.setEnabled(False)
hbox = QHBoxLayout()
hbox.addWidget(add_button)
hbox.addWidget(self.del_button)
domain_settings_layout.addLayout(hbox)
domain_settings_frame = QFrame(self)
domain_settings_frame.setLayout(domain_settings_layout)
return domain_settings_frame
def add_domain_settings_row(self):
self.domain_settings_model.insertRow(self.domain_settings_model.rowCount())
def delete_domain_settings_row(self, *args):
selected = self.domain_settings_table.selectedIndexes()
if selected:
rows = set([i.row() for i in selected])
for row in rows:
self.domain_settings_model.removeRow(row)
def save_and_close(self):
self.config.write()
self.close()
def selected(self, index):
self.del_button.setEnabled(index.isValid())

77
esgp/ui.py

@ -15,18 +15,21 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import hashlib
import logging
from urllib.parse import urlparse
import pydenticon
import supergenpass
from PyQt5.QtCore import QEvent, Qt
from PyQt5.QtGui import QPixmap, QIntValidator, QIcon
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QHBoxLayout, QRadioButton, QLabel, QPushButton, \
QMainWindow, QFrame
from PyQt5.QtWidgets import QVBoxLayout, QLineEdit, QHBoxLayout, QRadioButton, QLabel, QPushButton, \
QFrame, QDialog, QApplication
from esgp.settings import SettingsDialog
logger = logging.getLogger(__name__)
class MainWindow(QWidget):
class MainWindow(QDialog):
def __init__(self, config, cmdargs, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
@ -43,13 +46,13 @@ class MainWindow(QWidget):
self.radio_sha = None
self.build_ui()
self.set_default_settings()
self.show()
def build_ui(self):
self.setWindowTitle('eSGP')
# self.setMinimumSize(300, 300)
self.setFixedWidth(250)
vbox = QVBoxLayout()
self.setLayout(vbox)
@ -60,6 +63,7 @@ class MainWindow(QWidget):
self.master_password = QLineEdit('', self)
self.master_password.setPlaceholderText("Master password")
self.master_password.setEchoMode(QLineEdit.Password)
self.master_password.textChanged.connect(self.options_changed)
self.master_password.installEventFilter(self)
self.master_password.setFocus()
master_pwd_layout.addWidget(self.master_password)
@ -74,11 +78,14 @@ class MainWindow(QWidget):
self.secret_password = QLineEdit('', self)
self.secret_password.setPlaceholderText("Secret password")
self.secret_password.setEchoMode(QLineEdit.Password)
self.secret_password.textChanged.connect(self.options_changed)
self.secret_password.installEventFilter(self)
vbox.addWidget(self.secret_password)
self.domain = QLineEdit(self.initial_domain, self)
self.domain.setPlaceholderText("Domain")
self.domain.textChanged.connect(self.options_changed)
self.domain.textChanged.connect(self.domain_changed)
self.domain.installEventFilter(self)
vbox.addWidget(self.domain)
@ -92,50 +99,50 @@ class MainWindow(QWidget):
self.generated_password.setReadOnly(True)
self.generated_password.setVisible(False)
vbox.addWidget(self.generated_password)
self.master_password.textChanged.connect(self.options_changed)
self.secret_password.textChanged.connect(self.options_changed)
self.domain.textChanged.connect(self.options_changed)
self.radio_md5.toggled.connect(self.options_changed)
self.radio_sha.toggled.connect(self.options_changed)
self.chars.textChanged.connect(self.options_changed)
def build_settings_ui(self, parent):
settings = QHBoxLayout()
self.chars = QLineEdit(str(self.config.length), self)
self.chars = QLineEdit('', self)
self.chars.setValidator(QIntValidator(0, 99, self))
self.chars.textChanged.connect(self.options_changed)
self.chars.installEventFilter(self)
settings.addWidget(self.chars)
algo_layout = QHBoxLayout()
self.radio_md5 = QRadioButton("&MD5")
self.radio_md5.setChecked(self.config.algorithm == 'md5')
self.radio_md5.toggled.connect(self.options_changed)
algo_layout.addWidget(self.radio_md5)
self.radio_sha = QRadioButton("&SHA")
self.radio_sha.setChecked(self.config.algorithm == 'sha')
self.radio_sha.toggled.connect(self.options_changed)
algo_layout.addWidget(self.radio_sha)
settings.addLayout(algo_layout)
# see https://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
save_settings_button = QPushButton('')
save_settings_button.setIcon(QIcon.fromTheme('document-save'))
save_settings_button.clicked.connect(self.save_settings)
save_settings_button.setToolTip("Save current settings as defaults")
settings.addWidget(save_settings_button)
advanced_settings_button = QPushButton('')
advanced_settings_button.setIcon(QIcon.fromTheme('applications-other'))
advanced_settings_button.clicked.connect(self.advanced_settings)
advanced_settings_button.setToolTip("Show advanced settings")
settings.addWidget(advanced_settings_button)
parent.addLayout(settings)
hr = QFrame()
hr.setFrameShape(QFrame.HLine)
parent.addWidget(hr)
def eventFilter(self, source, event):
if event.type() == QEvent.KeyPress and \
(source is self.master_password or source is self.secret_password or source is self.domain) and \
event.text() == '\r':
if event.type() == QEvent.KeyPress and event.text() == '\r'\
and source in (self.master_password, self.secret_password, self.domain, self.chars):
self.generate_password()
return super(MainWindow, self).eventFilter(source, event)
@ -144,8 +151,30 @@ class MainWindow(QWidget):
self.generate_button.setEnabled(bool(self.master_password.text() or "") and bool(self.domain.text() or ""))
self.generated_password.setVisible(False)
self.config.algorithm = 'md5' if self.radio_md5.isChecked() else 'sha'
self.config.length = int(self.chars.text())
def domain_changed(self, text):
if QApplication.clipboard().text() == text and text:
self.domain_pasted(text)
domain_settings = self.config.get_domain_settings(self.domain.text())
if domain_settings:
self.chars.setText(str(domain_settings['length']))
self.radio_md5.setChecked(domain_settings['algorithm'] == 'MD5')
self.radio_sha.setChecked(domain_settings['algorithm'] == 'SHA')
else:
self.set_default_settings()
def set_default_settings(self):
self.chars.setText(str(self.config.length))
self.radio_md5.setChecked(self.config.algorithm == 'MD5')
self.radio_sha.setChecked(self.config.algorithm == 'SHA')
def domain_pasted(self, text):
try:
chunks = urlparse(text)
if chunks.netloc != text:
self.domain.setText(chunks.netloc)
except:
pass
def get_pwd(self):
return "%s%s" % (self.master_password.text() or "", self.secret_password.text() or "")
@ -194,4 +223,10 @@ class MainWindow(QWidget):
self.generated_password.setFocus()
def save_settings(self):
self.config.algorithm = 'MD5' if self.radio_md5.isChecked() else 'SHA'
self.config.length = int(self.chars.text())
self.config.write()
def advanced_settings(self):
settings_dialog = SettingsDialog(config=self.config)
settings_dialog.exec_()