From f778afd268cd3a3f2e677375f5939ef0b78a7381 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 31 Oct 2025 16:46:42 +1100 Subject: [PATCH] Add ability to change the key --- README.md | 1 - bouquin/db.py | 19 +++++++++++++++++++ bouquin/main_window.py | 2 +- bouquin/settings_dialog.py | 32 ++++++++++++++++++++++++++++++-- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8b20e14..b874668 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,6 @@ There is deliberately no network connectivity or syncing intended. * Search * Taxonomy/tagging - * Ability to change the SQLCipher key * Export to other formats (plaintext, json, sql etc) diff --git a/bouquin/db.py b/bouquin/db.py index 1ea60fa..15cc4c9 100644 --- a/bouquin/db.py +++ b/bouquin/db.py @@ -64,6 +64,25 @@ class DBManager: cur.execute("PRAGMA user_version = 1;") self.conn.commit() + def rekey(self, new_key: str) -> None: + """ + Change the SQLCipher passphrase in-place, then reopen the connection + with the new key to verify. + """ + if self.conn is None: + raise RuntimeError("Database is not connected") + cur = self.conn.cursor() + # Change the encryption key of the currently open database + cur.execute(f"PRAGMA rekey = '{new_key}';") + self.conn.commit() + + # Close and reopen with the new key to verify and restore PRAGMAs + self.conn.close() + self.conn = None + self.cfg.key = new_key + if not self.connect(): + raise sqlite.Error("Re-open failed after rekey") + def get_entry(self, date_iso: str) -> str: cur = self.conn.cursor() cur.execute("SELECT content FROM entries WHERE date = ?;", (date_iso,)) diff --git a/bouquin/main_window.py b/bouquin/main_window.py index 5b8bc97..309ef28 100644 --- a/bouquin/main_window.py +++ b/bouquin/main_window.py @@ -231,7 +231,7 @@ class MainWindow(QMainWindow): self._save_date(self._current_date_iso(), explicit) def _open_settings(self): - dlg = SettingsDialog(self.cfg, self) + dlg = SettingsDialog(self.cfg, self.db, self) if dlg.exec() == QDialog.Accepted: new_cfg = dlg.config if new_cfg.path != self.cfg.path: diff --git a/bouquin/settings_dialog.py b/bouquin/settings_dialog.py index 790c4e0..ca2514c 100644 --- a/bouquin/settings_dialog.py +++ b/bouquin/settings_dialog.py @@ -13,17 +13,20 @@ from PySide6.QtWidgets import ( QFileDialog, QDialogButtonBox, QSizePolicy, + QMessageBox, ) -from .db import DBConfig +from .db import DBConfig, DBManager from .settings import save_db_config +from .key_prompt import KeyPrompt class SettingsDialog(QDialog): - def __init__(self, cfg: DBConfig, parent=None): + def __init__(self, cfg: DBConfig, db: DBManager, parent=None): super().__init__(parent) self.setWindowTitle("Settings") self._cfg = DBConfig(path=cfg.path, key="") + self._db = db form = QFormLayout() form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) @@ -44,12 +47,17 @@ class SettingsDialog(QDialog): h.setStretch(1, 0) form.addRow("Database path", path_row) + # Change key button + self.rekey_btn = QPushButton("Change key") + self.rekey_btn.clicked.connect(self._change_key) + bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel) bb.accepted.connect(self._save) bb.rejected.connect(self.reject) v = QVBoxLayout(self) v.addLayout(form) + v.addWidget(self.rekey_btn) v.addWidget(bb) def _browse(self): @@ -67,6 +75,26 @@ class SettingsDialog(QDialog): save_db_config(self._cfg) self.accept() + def _change_key(self): + p1 = KeyPrompt(self, title="Change key", message="Enter new key") + if p1.exec() != QDialog.Accepted: + return + new_key = p1.key() + p2 = KeyPrompt(self, title="Change key", message="Re-enter new key") + if p2.exec() != QDialog.Accepted: + return + if new_key != p2.key(): + QMessageBox.warning(self, "Key mismatch", "The two entries did not match.") + return + if not new_key: + QMessageBox.warning(self, "Empty key", "Key cannot be empty.") + return + try: + self._db.rekey(new_key) + QMessageBox.information(self, "Key changed", "The database key was updated.") + except Exception as e: + QMessageBox.critical(self, "Error", f"Could not change key:\n{e}") + @property def config(self) -> DBConfig: return self._cfg