Add ability to save the key to avoid being prompted for it

This commit is contained in:
Miguel Jacq 2025-11-02 11:44:22 +11:00
parent 4f773e1c1b
commit 43bbe971eb
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
7 changed files with 98 additions and 46 deletions

View file

@ -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.
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.
@ -35,6 +35,8 @@ There is deliberately no network connectivity or syncing intended.
## How to install
Make sure you have `libxcb-cursor0` installed (it may be called something else on non-Debian distributions).
### From source
* Clone this repo or download the tarball from the releases page

View file

@ -1,7 +1,6 @@
from __future__ import annotations
from PySide6.QtGui import (
QBrush,
QColor,
QDesktopServices,
QFont,
@ -14,7 +13,6 @@ from PySide6.QtGui import (
from PySide6.QtCore import Qt, QUrl, Signal, Slot, QRegularExpression
from PySide6.QtWidgets import QTextEdit
from .url_highlighter import UrlHighlighter
class Editor(QTextEdit):
linkActivated = Signal(str)
@ -55,13 +53,13 @@ class Editor(QTextEdit):
while it.hasNext():
m = it.next()
start = block.position() + m.capturedStart()
end = start + m.capturedLength()
end = start + m.capturedLength()
cur.setPosition(start)
cur.setPosition(end, QTextCursor.KeepAnchor)
fmt = cur.charFormat()
if fmt.isAnchor(): # already linkified; skip
if fmt.isAnchor(): # already linkified; skip
continue
href = m.captured(0)
@ -110,7 +108,7 @@ class Editor(QTextEdit):
# When pressing Enter/return key, insert first, then neutralise the empty blocks inline format
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
# *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):
cur = self.currentCharFormat()
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)
self.merge_on_sel(fmt)

View file

@ -26,7 +26,7 @@ from .db import DBManager
from .editor import Editor
from .key_prompt import KeyPrompt
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 .toolbar import ToolBar
@ -44,9 +44,12 @@ class MainWindow(QMainWindow):
else:
first_time = False
# Always prompt for the key (we never store it)
if not self._prompt_for_key_until_valid(first_time):
sys.exit(1)
# Prompt for the key unless it is found in config
if not self.cfg.key:
if not self._prompt_for_key_until_valid(first_time):
sys.exit(1)
else:
self._try_connect()
# ---- UI: Left fixed panel (calendar) + right editor -----------------
self.calendar = QCalendarWidget()
@ -149,7 +152,7 @@ class MainWindow(QMainWindow):
self._refresh_calendar_marks()
# Restore window position from settings
self.settings = QSettings(APP_NAME)
self.settings = QSettings(APP_ORG, APP_NAME)
self._restore_window_position()
def _try_connect(self) -> bool:
@ -328,12 +331,13 @@ class MainWindow(QMainWindow):
return False
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()
# Center the window in that screens available area
self.move(r.center() - self.rect().center())
def closeEvent(self, event):
try:
# Save window position

View file

@ -21,9 +21,11 @@ def get_settings() -> QSettings:
def load_db_config() -> DBConfig:
s = get_settings()
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:
s = get_settings()
s.setValue("db/path", str(cfg.path))
s.setValue("db/key", str(cfg.key))

View file

@ -3,8 +3,12 @@ from __future__ import annotations
from pathlib import Path
from PySide6.QtWidgets import (
QCheckBox,
QDialog,
QFormLayout,
QFrame,
QGroupBox,
QLabel,
QHBoxLayout,
QVBoxLayout,
QWidget,
@ -15,9 +19,12 @@ from PySide6.QtWidgets import (
QSizePolicy,
QMessageBox,
)
from PySide6.QtCore import Qt, Slot
from PySide6.QtGui import QPalette
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
@ -27,10 +34,11 @@ class SettingsDialog(QDialog):
self.setWindowTitle("Settings")
self._cfg = DBConfig(path=cfg.path, key="")
self._db = db
self.key = ""
form = QFormLayout()
form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
self.setMinimumWidth(520)
self.setMinimumWidth(560)
self.setSizeGripEnabled(True)
self.path_edit = QLineEdit(str(self._cfg.path))
@ -47,18 +55,65 @@ class SettingsDialog(QDialog):
h.setStretch(1, 0)
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
self.rekey_btn = QPushButton("Change key")
self.rekey_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
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.accepted.connect(self._save)
bb.rejected.connect(self.reject)
# Root layout (adjust margins/spacing a bit)
v = QVBoxLayout(self)
v.setContentsMargins(12, 12, 12, 12)
v.setSpacing(10)
v.addLayout(form)
v.addWidget(self.rekey_btn)
v.addWidget(bb)
v.addWidget(bb, 0, Qt.AlignRight)
def _browse(self):
p, _ = QFileDialog.getSaveFileName(
@ -71,7 +126,7 @@ class SettingsDialog(QDialog):
self.path_edit.setText(p)
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)
self.accept()
@ -97,6 +152,18 @@ class SettingsDialog(QDialog):
except Exception as 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
def config(self) -> DBConfig:
return self._cfg

View file

@ -1,7 +1,7 @@
from __future__ import annotations
from PySide6.QtCore import Signal, Qt
from PySide6.QtGui import QFont, QAction
from PySide6.QtGui import QAction
from PySide6.QtWidgets import QToolBar

View file

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