From fe12bd922b0410090d25f40b74738e3cb76b840c Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 16 Aug 2026 03:44:43 +0000 Subject: [PATCH] feat(settings): add encrypted key download to PrivateKeyManager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The settings page could import a key backup file, back up to the server, and restore from the server, but there was no way to download an encrypted copy of your keys to disk. privateKeyManager already implemented exportPrivateKeys(), generateExportFilename() and downloadExportedKeys() — nothing in the UI ever called the download path. This wires them to a "Download Keys to File" card that sits above the existing import card, with password + confirmation (min 6 chars) and a note that the password is unrecoverable. Co-Authored-By: Claude Opus 5 (1M context) --- .../components/settings/PrivateKeyManager.jsx | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/lib/components/settings/PrivateKeyManager.jsx b/src/lib/components/settings/PrivateKeyManager.jsx index 9a9e3b45..e4953f87 100644 --- a/src/lib/components/settings/PrivateKeyManager.jsx +++ b/src/lib/components/settings/PrivateKeyManager.jsx @@ -25,6 +25,10 @@ export default function PrivateKeyManager() { const [importPassword, setImportPassword] = useState(''); const [importLoading, setImportLoading] = useState(false); const [importAndBackup, setImportAndBackup] = useState(false); + const [exportPassword, setExportPassword] = useState(''); + const [confirmExportPassword, setConfirmExportPassword] = useState(''); + const [showExportPassword, setShowExportPassword] = useState(false); + const [exportLoading, setExportLoading] = useState(false); useEffect(() => { if (user) { checkKeys(); checkPin(); checkBackup(); } @@ -109,6 +113,20 @@ export default function PrivateKeyManager() { } finally { setImportLoading(false); } } + async function downloadKeys() { + if (exportPassword.length < 6) { setError('Export password must be at least 6 characters'); return; } + if (exportPassword !== confirmExportPassword) { setError('Export passwords do not match'); return; } + setExportLoading(true); setError(''); setSuccess(''); + try { + const encrypted = await privateKeyManager.exportPrivateKeys(exportPassword); + const filename = privateKeyManager.generateExportFilename(); + privateKeyManager.downloadExportedKeys(encrypted, filename); + setExportPassword(''); setConfirmExportPassword(''); + setSuccess(`Downloaded ${filename} — store it somewhere safe. Without this password the file cannot be recovered.`); + } catch (err) { setError(err.message || 'Export failed'); } + finally { setExportLoading(false); } + } + async function setNewPin() { if (!pin || pin !== confirmPin || !/^\d{6,12}$/.test(pin)) { setError('PIN must be 6–12 digits and match'); return; } setLoading(true); setError(''); setSuccess(''); @@ -189,6 +207,48 @@ export default function PrivateKeyManager() { )} +
+

Download Keys to File

+ {!hasKeys ? ( +

+ No keys on this device yet — generate or restore keys before exporting. +

+ ) : ( + <> +

+ Save an encrypted copy of your private keys as a JSON file. The file is encrypted with the + password you choose here — nobody, including us, can recover it if you lose that password. +

+
+ setExportPassword(e.target.value)} + placeholder="Password to encrypt the file (min 6 characters)" + className="pkm-input" + /> + setConfirmExportPassword(e.target.value)} + placeholder="Confirm password" + className="pkm-input" + /> + + +
+ + )} +
+

Import Keys from File