Add ability to save the key to avoid being prompted for it
This commit is contained in:
parent
4f773e1c1b
commit
43bbe971eb
7 changed files with 98 additions and 46 deletions
|
|
@ -9,7 +9,7 @@ It uses [SQLCipher bindings](https://pypi.org/project/sqlcipher3-wheels) as a dr
|
||||||
for SQLite3. This means that the underlying database for the notebook is encrypted at rest.
|
for SQLite3. This means that the underlying database for the notebook is encrypted at rest.
|
||||||
|
|
||||||
To increase security, the SQLCipher key is requested when the app is opened, and is not written
|
To increase security, the SQLCipher key is requested when the app is opened, and is not written
|
||||||
to disk.
|
to disk unless the user configures it to be in the settings.
|
||||||
|
|
||||||
There is deliberately no network connectivity or syncing intended.
|
There is deliberately no network connectivity or syncing intended.
|
||||||
|
|
||||||
|
|
@ -35,6 +35,8 @@ There is deliberately no network connectivity or syncing intended.
|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
||||||
|
Make sure you have `libxcb-cursor0` installed (it may be called something else on non-Debian distributions).
|
||||||
|
|
||||||
### From source
|
### From source
|
||||||
|
|
||||||
* Clone this repo or download the tarball from the releases page
|
* Clone this repo or download the tarball from the releases page
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
QBrush,
|
|
||||||
QColor,
|
QColor,
|
||||||
QDesktopServices,
|
QDesktopServices,
|
||||||
QFont,
|
QFont,
|
||||||
|
|
@ -14,7 +13,6 @@ from PySide6.QtGui import (
|
||||||
from PySide6.QtCore import Qt, QUrl, Signal, Slot, QRegularExpression
|
from PySide6.QtCore import Qt, QUrl, Signal, Slot, QRegularExpression
|
||||||
from PySide6.QtWidgets import QTextEdit
|
from PySide6.QtWidgets import QTextEdit
|
||||||
|
|
||||||
from .url_highlighter import UrlHighlighter
|
|
||||||
|
|
||||||
class Editor(QTextEdit):
|
class Editor(QTextEdit):
|
||||||
linkActivated = Signal(str)
|
linkActivated = Signal(str)
|
||||||
|
|
@ -55,13 +53,13 @@ class Editor(QTextEdit):
|
||||||
while it.hasNext():
|
while it.hasNext():
|
||||||
m = it.next()
|
m = it.next()
|
||||||
start = block.position() + m.capturedStart()
|
start = block.position() + m.capturedStart()
|
||||||
end = start + m.capturedLength()
|
end = start + m.capturedLength()
|
||||||
|
|
||||||
cur.setPosition(start)
|
cur.setPosition(start)
|
||||||
cur.setPosition(end, QTextCursor.KeepAnchor)
|
cur.setPosition(end, QTextCursor.KeepAnchor)
|
||||||
|
|
||||||
fmt = cur.charFormat()
|
fmt = cur.charFormat()
|
||||||
if fmt.isAnchor(): # already linkified; skip
|
if fmt.isAnchor(): # already linkified; skip
|
||||||
continue
|
continue
|
||||||
|
|
||||||
href = m.captured(0)
|
href = m.captured(0)
|
||||||
|
|
@ -110,7 +108,7 @@ class Editor(QTextEdit):
|
||||||
|
|
||||||
# When pressing Enter/return key, insert first, then neutralise the empty block’s inline format
|
# When pressing Enter/return key, insert first, then neutralise the empty block’s inline format
|
||||||
if key in (Qt.Key_Return, Qt.Key_Enter):
|
if key in (Qt.Key_Return, Qt.Key_Enter):
|
||||||
super().keyPressEvent(e) # create the new (possibly empty) paragraph
|
super().keyPressEvent(e) # create the new (possibly empty) paragraph
|
||||||
|
|
||||||
# If we're on an empty block, clear the insertion char format so the
|
# If we're on an empty block, clear the insertion char format so the
|
||||||
# *next* Enter will create another new line (not consume the press to reset formatting).
|
# *next* Enter will create another new line (not consume the press to reset formatting).
|
||||||
|
|
@ -156,7 +154,11 @@ class Editor(QTextEdit):
|
||||||
def apply_weight(self):
|
def apply_weight(self):
|
||||||
cur = self.currentCharFormat()
|
cur = self.currentCharFormat()
|
||||||
fmt = QTextCharFormat()
|
fmt = QTextCharFormat()
|
||||||
weight = QFont.Weight.Normal if cur.fontWeight() == QFont.Weight.Bold else QFont.Weight.Bold
|
weight = (
|
||||||
|
QFont.Weight.Normal
|
||||||
|
if cur.fontWeight() == QFont.Weight.Bold
|
||||||
|
else QFont.Weight.Bold
|
||||||
|
)
|
||||||
fmt.setFontWeight(weight)
|
fmt.setFontWeight(weight)
|
||||||
self.merge_on_sel(fmt)
|
self.merge_on_sel(fmt)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ from .db import DBManager
|
||||||
from .editor import Editor
|
from .editor import Editor
|
||||||
from .key_prompt import KeyPrompt
|
from .key_prompt import KeyPrompt
|
||||||
from .search import Search
|
from .search import Search
|
||||||
from .settings import APP_NAME, load_db_config, save_db_config
|
from .settings import APP_ORG, APP_NAME, load_db_config, save_db_config
|
||||||
from .settings_dialog import SettingsDialog
|
from .settings_dialog import SettingsDialog
|
||||||
from .toolbar import ToolBar
|
from .toolbar import ToolBar
|
||||||
|
|
||||||
|
|
@ -44,9 +44,12 @@ class MainWindow(QMainWindow):
|
||||||
else:
|
else:
|
||||||
first_time = False
|
first_time = False
|
||||||
|
|
||||||
# Always prompt for the key (we never store it)
|
# Prompt for the key unless it is found in config
|
||||||
if not self._prompt_for_key_until_valid(first_time):
|
if not self.cfg.key:
|
||||||
sys.exit(1)
|
if not self._prompt_for_key_until_valid(first_time):
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
self._try_connect()
|
||||||
|
|
||||||
# ---- UI: Left fixed panel (calendar) + right editor -----------------
|
# ---- UI: Left fixed panel (calendar) + right editor -----------------
|
||||||
self.calendar = QCalendarWidget()
|
self.calendar = QCalendarWidget()
|
||||||
|
|
@ -149,7 +152,7 @@ class MainWindow(QMainWindow):
|
||||||
self._refresh_calendar_marks()
|
self._refresh_calendar_marks()
|
||||||
|
|
||||||
# Restore window position from settings
|
# Restore window position from settings
|
||||||
self.settings = QSettings(APP_NAME)
|
self.settings = QSettings(APP_ORG, APP_NAME)
|
||||||
self._restore_window_position()
|
self._restore_window_position()
|
||||||
|
|
||||||
def _try_connect(self) -> bool:
|
def _try_connect(self) -> bool:
|
||||||
|
|
@ -328,12 +331,13 @@ class MainWindow(QMainWindow):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _move_to_cursor_screen_center(self):
|
def _move_to_cursor_screen_center(self):
|
||||||
screen = QGuiApplication.screenAt(QCursor.pos()) or QGuiApplication.primaryScreen()
|
screen = (
|
||||||
|
QGuiApplication.screenAt(QCursor.pos()) or QGuiApplication.primaryScreen()
|
||||||
|
)
|
||||||
r = screen.availableGeometry()
|
r = screen.availableGeometry()
|
||||||
# Center the window in that screen’s available area
|
# Center the window in that screen’s available area
|
||||||
self.move(r.center() - self.rect().center())
|
self.move(r.center() - self.rect().center())
|
||||||
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
try:
|
try:
|
||||||
# Save window position
|
# Save window position
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,11 @@ def get_settings() -> QSettings:
|
||||||
def load_db_config() -> DBConfig:
|
def load_db_config() -> DBConfig:
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
path = Path(s.value("db/path", str(default_db_path())))
|
path = Path(s.value("db/path", str(default_db_path())))
|
||||||
return DBConfig(path=path, key="")
|
key = s.value("db/key", "")
|
||||||
|
return DBConfig(path=path, key=key)
|
||||||
|
|
||||||
|
|
||||||
def save_db_config(cfg: DBConfig) -> None:
|
def save_db_config(cfg: DBConfig) -> None:
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
s.setValue("db/path", str(cfg.path))
|
s.setValue("db/path", str(cfg.path))
|
||||||
|
s.setValue("db/key", str(cfg.key))
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,12 @@ from __future__ import annotations
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
|
QCheckBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
QFormLayout,
|
QFormLayout,
|
||||||
|
QFrame,
|
||||||
|
QGroupBox,
|
||||||
|
QLabel,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
|
|
@ -15,9 +19,12 @@ from PySide6.QtWidgets import (
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
)
|
)
|
||||||
|
from PySide6.QtCore import Qt, Slot
|
||||||
|
from PySide6.QtGui import QPalette
|
||||||
|
|
||||||
|
|
||||||
from .db import DBConfig, DBManager
|
from .db import DBConfig, DBManager
|
||||||
from .settings import save_db_config
|
from .settings import load_db_config, save_db_config
|
||||||
from .key_prompt import KeyPrompt
|
from .key_prompt import KeyPrompt
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -27,10 +34,11 @@ class SettingsDialog(QDialog):
|
||||||
self.setWindowTitle("Settings")
|
self.setWindowTitle("Settings")
|
||||||
self._cfg = DBConfig(path=cfg.path, key="")
|
self._cfg = DBConfig(path=cfg.path, key="")
|
||||||
self._db = db
|
self._db = db
|
||||||
|
self.key = ""
|
||||||
|
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
|
form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
|
||||||
self.setMinimumWidth(520)
|
self.setMinimumWidth(560)
|
||||||
self.setSizeGripEnabled(True)
|
self.setSizeGripEnabled(True)
|
||||||
|
|
||||||
self.path_edit = QLineEdit(str(self._cfg.path))
|
self.path_edit = QLineEdit(str(self._cfg.path))
|
||||||
|
|
@ -47,18 +55,65 @@ class SettingsDialog(QDialog):
|
||||||
h.setStretch(1, 0)
|
h.setStretch(1, 0)
|
||||||
form.addRow("Database path", path_row)
|
form.addRow("Database path", path_row)
|
||||||
|
|
||||||
|
# Encryption settings
|
||||||
|
enc_group = QGroupBox("Encryption")
|
||||||
|
enc = QVBoxLayout(enc_group)
|
||||||
|
enc.setContentsMargins(12, 8, 12, 12)
|
||||||
|
enc.setSpacing(6)
|
||||||
|
|
||||||
|
# Checkbox to remember key
|
||||||
|
self.save_key_btn = QCheckBox("Remember key")
|
||||||
|
current_settings = load_db_config()
|
||||||
|
if current_settings.key:
|
||||||
|
self.save_key_btn.setChecked(True)
|
||||||
|
else:
|
||||||
|
self.save_key_btn.setChecked(False)
|
||||||
|
self.save_key_btn.setCursor(Qt.PointingHandCursor)
|
||||||
|
self.save_key_btn.toggled.connect(self.save_key_btn_clicked)
|
||||||
|
enc.addWidget(self.save_key_btn, 0, Qt.AlignLeft)
|
||||||
|
|
||||||
|
# Explanation for remembering key
|
||||||
|
self.save_key_label = QLabel(
|
||||||
|
"If you don't want to be prompted for your encryption key, check this to remember it. "
|
||||||
|
"WARNING: the key is saved to disk and could be recoverable if your disk is compromised."
|
||||||
|
)
|
||||||
|
self.save_key_label.setWordWrap(True)
|
||||||
|
self.save_key_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||||
|
# make it look secondary
|
||||||
|
pal = self.save_key_label.palette()
|
||||||
|
pal.setColor(self.save_key_label.foregroundRole(), pal.color(QPalette.Mid))
|
||||||
|
self.save_key_label.setPalette(pal)
|
||||||
|
|
||||||
|
exp_row = QHBoxLayout()
|
||||||
|
exp_row.setContentsMargins(24, 0, 0, 0) # indent to line up under the checkbox
|
||||||
|
exp_row.addWidget(self.save_key_label)
|
||||||
|
enc.addLayout(exp_row)
|
||||||
|
|
||||||
|
line = QFrame()
|
||||||
|
line.setFrameShape(QFrame.HLine)
|
||||||
|
line.setFrameShadow(QFrame.Sunken)
|
||||||
|
enc.addWidget(line)
|
||||||
|
|
||||||
# Change key button
|
# Change key button
|
||||||
self.rekey_btn = QPushButton("Change key")
|
self.rekey_btn = QPushButton("Change key")
|
||||||
|
self.rekey_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||||
self.rekey_btn.clicked.connect(self._change_key)
|
self.rekey_btn.clicked.connect(self._change_key)
|
||||||
|
enc.addWidget(self.rekey_btn, 0, Qt.AlignLeft)
|
||||||
|
|
||||||
|
# Put the group into the form so it spans the full width nicely
|
||||||
|
form.addRow(enc_group)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||||
bb.accepted.connect(self._save)
|
bb.accepted.connect(self._save)
|
||||||
bb.rejected.connect(self.reject)
|
bb.rejected.connect(self.reject)
|
||||||
|
|
||||||
|
# Root layout (adjust margins/spacing a bit)
|
||||||
v = QVBoxLayout(self)
|
v = QVBoxLayout(self)
|
||||||
|
v.setContentsMargins(12, 12, 12, 12)
|
||||||
|
v.setSpacing(10)
|
||||||
v.addLayout(form)
|
v.addLayout(form)
|
||||||
v.addWidget(self.rekey_btn)
|
v.addWidget(bb, 0, Qt.AlignRight)
|
||||||
v.addWidget(bb)
|
|
||||||
|
|
||||||
def _browse(self):
|
def _browse(self):
|
||||||
p, _ = QFileDialog.getSaveFileName(
|
p, _ = QFileDialog.getSaveFileName(
|
||||||
|
|
@ -71,7 +126,7 @@ class SettingsDialog(QDialog):
|
||||||
self.path_edit.setText(p)
|
self.path_edit.setText(p)
|
||||||
|
|
||||||
def _save(self):
|
def _save(self):
|
||||||
self._cfg = DBConfig(path=Path(self.path_edit.text()), key="")
|
self._cfg = DBConfig(path=Path(self.path_edit.text()), key=self.key)
|
||||||
save_db_config(self._cfg)
|
save_db_config(self._cfg)
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
|
@ -97,6 +152,18 @@ class SettingsDialog(QDialog):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Error", f"Could not change key:\n{e}")
|
QMessageBox.critical(self, "Error", f"Could not change key:\n{e}")
|
||||||
|
|
||||||
|
@Slot(bool)
|
||||||
|
def save_key_btn_clicked(self, checked: bool):
|
||||||
|
if checked:
|
||||||
|
p1 = KeyPrompt(
|
||||||
|
self, title="Enter your key", message="Enter the encryption key"
|
||||||
|
)
|
||||||
|
if p1.exec() != QDialog.Accepted:
|
||||||
|
return
|
||||||
|
self.key = p1.key()
|
||||||
|
self._cfg = DBConfig(path=Path(self.path_edit.text()), key=self.key)
|
||||||
|
save_db_config(self._cfg)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self) -> DBConfig:
|
def config(self) -> DBConfig:
|
||||||
return self._cfg
|
return self._cfg
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PySide6.QtCore import Signal, Qt
|
from PySide6.QtCore import Signal, Qt
|
||||||
from PySide6.QtGui import QFont, QAction
|
from PySide6.QtGui import QAction
|
||||||
from PySide6.QtWidgets import QToolBar
|
from PySide6.QtWidgets import QToolBar
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
from PySide6.QtGui import QSyntaxHighlighter, QTextCharFormat
|
|
||||||
from PySide6.QtCore import Qt, QRegularExpression
|
|
||||||
|
|
||||||
class UrlHighlighter(QSyntaxHighlighter):
|
|
||||||
def __init__(self, doc):
|
|
||||||
super().__init__(doc)
|
|
||||||
self.rx = QRegularExpression(r"(https?://[^\s<>\"]+|www\.[^\s<>\"]+)")
|
|
||||||
|
|
||||||
def highlightBlock(self, text: str):
|
|
||||||
it = self.rx.globalMatch(text)
|
|
||||||
while it.hasNext():
|
|
||||||
m = it.next()
|
|
||||||
href = m.captured(0)
|
|
||||||
if href.startswith("www."):
|
|
||||||
href = "https://" + href
|
|
||||||
|
|
||||||
fmt = QTextCharFormat()
|
|
||||||
fmt.setAnchor(True)
|
|
||||||
fmt.setAnchorHref(href)
|
|
||||||
fmt.setFontUnderline(True)
|
|
||||||
fmt.setForeground(Qt.blue)
|
|
||||||
self.setFormat(m.capturedStart(0), m.capturedLength(0), fmt)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue