feat: Legacy Encryption — dual-key AES-256-GCM seed phrase backup for Bitcoin inheritance#938
feat: Legacy Encryption — dual-key AES-256-GCM seed phrase backup for Bitcoin inheritance#938ericscalibur wants to merge 1 commit into
Conversation
Adds a new "Legacy Encryption" entry to the Tools menu that lets a benefactor and a beneficiary each set a password. The seed phrase is encrypted with both keys (PBKDF2-SHA256, 600 000 iterations + AES-256-GCM) and displayed as a QR code the user photographs and stores publicly. Decryption requires both keys — neither alone reveals the seed. The encrypted QR is fully cross-compatible with Legacy-offline.html (the browser-side companion at https://legacyencryption.com) so the seed can be recovered with a browser, no hardware required. New files --------- src/seedsigner/helpers/legacy_encryption.py - Pure-Python AES-256-GCM + PBKDF2 crypto module; no extra deps beyond `cryptography` (already in the Buildroot image for PSBT signing). - Produces byte-identical output to the JavaScript reference implementation. src/seedsigner/views/legacy_views.py - Full encrypt + decrypt UI flow: scan/enter seed → enter both keys → show encrypted QR → scan encrypted QR → enter both keys → show words. - _RawQRDecoder: custom QR decoder for the encrypted base64 blob (standard DecodeQR rejects it as INVALID); uses a daemon thread so a libzbar hang never freezes the UI. - _make_key_entry_screen: thin subclass of SeedAddPassphraseScreen with a custom TopNav title for "Benefactor Key" / "Beneficiary Key". - All navigation via View/Destination/BackStackView; follows existing patterns. Changed files ------------- src/seedsigner/views/tools_views.py - Adds "Legacy Encryption" entry to ToolsMenuView (last item). src/seedsigner/hardware/camera.py - stream parking: stop_video_stream_mode() now parks rather than destroys the PiVideoStream. On Pi Zero, calling PiCamera() a second time while MMAL is still releasing from the first session blocks indefinitely. Parking keeps the background capture thread alive at 1 fps; the next start_video_stream_mode() reattaches and flushes stale frames (500 ms) before handing control back to ScanScreen. - 1 fps throttle during PBKDF2: 12 DMA interrupts/sec on a single-core Pi Zero starves the main thread during PBKDF2 causing UI freezes on every subsequent screen. 1 fps drops interrupt load to negligible. - 5 s first-frame watchdog: raises CameraConnectionError (upstream class) if capture_continuous silently stalls after PiCamera() opens. - _force_close_stream(): gracefully signals then hard-closes a stalled PiVideoStream; used by both stop paths and the watchdog. - start_single_frame_mode() now drains any parked stream before opening its own PiCamera instance — two open instances deadlock on MMAL. src/seedsigner/hardware/pivideostream.py - Adds last_capture_time (float) updated on every captured frame; used by Camera's watchdog to detect a stalled stream. Performance note: PBKDF2 at 600 000 iterations takes 15–30 s on Pi Zero 1.3. A loading screen is shown. This is intentional — the high iteration count is what makes brute-force attacks on the backup infeasible. Tested on Pi Zero 1.3 + Waveshare 1.3" 240×240 LCD HAT + OV5647 camera. Encrypt and decrypt verified end-to-end; cross-compat confirmed with browser version (Legacy-offline.html). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
I was looking for a site but following your github now. Neat stuff! |
|
@jdlcdl My main page https://ericscalibur.github.io/ If you have other questions hmu! @ericscalibur on all socials |
|
Concept NACK. There's rarely (never?) a reason to add new cryptography to help secure a bitcoin setup, even when complex inheritance planning is involved. We already have enough tools that are either enforced onchain or have been through rigorous review and testing:
How a key / passphrase / etc is managed in the real world is up to the user or third party service or whomever. Some standardized, documented, vetted processes can have a place in SeedSigner (e.g. Shamir, SeedXOR, bip85). Our one novel approach -- SeedQR -- was taken with great hesitation but has since been adopted more broadly (admittedly, if it were to be introduced today, I'd want to see it go through a BIP process first). But this PR is too focused on a specific third party service's offering. If there was a BIP that had been discussed and merged and implemented in at least one major project elsewhere, then it would a stronger consideration. But we're already behind on building support for more well-established bitcoin advances (e.g. Silent Payments, BIP-353, miniscript). Aside from the above, I had Claude analyze the PR. Its critiques on the actual approach and code changes are included here for your review. It may be wrong but I would be very cautious about going forward with any plans to actually build out this service. You have the freedom to keep all of these proposed changes and put out a custom forked release for your users. But the cryptography questions are concerning. Also, Krux already has encrypted SeedQR support. If that's your goal, you're better off using an existing implementation. Claude's AI review: Review: PR #938 — "Legacy Encryption"Recommendation: Close / do not mergeThis should be declined on scope grounds first, and even setting scope aside, the implementation has cryptographic and architectural problems that would make it a hard no on its own. 1. Scope — this is the deciding issueThe PR adds a bespoke off-chain secret-encryption scheme to SeedSigner. Bitcoin already provides the right primitives for shared custody, inheritance, and recovery. On-chain enforced, auditable, and don't depend on SeedSigner inventing or maintaining a key-management scheme. This feature is the opposite of that: a custom AES-GCM blob whose security lives entirely off-chain, in how two passwords are stored and shared. That's exactly the secret-management surface that belongs outside the device's domain. Adopting it would commit the project to maintaining and vouching for an encryption product it doesn't own. A specific red flag reinforcing the scope concern: the feature's value proposition is byte-identical cross-compatibility with a third-party website ( 2. The "dual-key" design doesn't deliver what it claimsEven if encryption were in scope, the core security claim is misleading: combined_key = benefactor_key + beneficiary_key # legacy_encryption.py
enc = encrypt_data(seed_phrase, combined_key)"Dual-key" is just string concatenation of two passwords into one PBKDF2 password. This is not threshold cryptography, not secret sharing, nothing like Shamir or a 2-of-2 multisig. Consequences:
3. Other crypto/correctness problems
4. Architectural / code-quality blockers
|
|
Hey thanks for the feedback! Big fan of Seedsigner.
- Eric
…On Sun, Jun 21, 2026, 7:04 AM kdmukai ***@***.***> wrote:
*kdmukai* left a comment (SeedSigner/seedsigner#938)
<#938 (comment)>
Concept NACK.
There's rarely (never?) a reason to add new cryptography to help secure a
bitcoin setup, even when complex inheritance planning is involved.
We already have enough tools that are either enforced onchain or have been
through rigorous review and testing:
- Single sig + bip39 passphrase
- 2-of-2, 2-of-3, 3-of-5, etc key distribution strategies
- miniscript multi-multisigs and/or degrading multisigs
- Newer musig2, FROST composite keys
How a key / passphrase / etc is managed in the real world is up to the
user or third party service or whomever. Some standardized, documented,
vetted processes can have a place in SeedSigner (e.g. Shamir, SeedXOR,
bip85). Our one novel approach -- SeedQR -- was taken with great hesitation
but has since been adopted more broadly (admittedly, if it were to be
introduced today, I'd want to see it go through a BIP process first).
But this PR is too focused on a specific third party service's offering.
If there was a BIP that had been discussed and merged and implemented in at
least one major project elsewhere, then it would a stronger consideration.
But we're already behind on building support for more well-established
bitcoin advances (e.g. Silent Payments, BIP-353, miniscript).
------------------------------
Aside from the above, I had Claude analyze the PR. Its critiques on the
actual approach and code changes are included here for your review. It may
be wrong but I would be very cautious about going forward with any plans to
actually build out this service.
You have the freedom to keep all of these proposed changes and put out a
custom forked release for your users. But the cryptography questions are
concerning.
Also, Krux already has encrypted SeedQR support. If that's your goal,
you're better off using an existing implementation.
Claude's AI review:
------------------------------
Review: PR #938 <#938> —
"Legacy Encryption" Recommendation: Close / do not merge
This should be declined on *scope* grounds first, and even setting scope
aside, the implementation has cryptographic and architectural problems that
would make it a hard no on its own.
------------------------------
1. Scope — this is the deciding issue
The PR adds a bespoke off-chain secret-encryption scheme to SeedSigner.
Bitcoin already provides the right primitives for shared custody,
inheritance, and recovery. On-chain enforced, auditable, and don't depend
on SeedSigner inventing or maintaining a key-management scheme.
This feature is the opposite of that: a custom AES-GCM blob whose security
lives entirely off-chain, in how two passwords are stored and shared.
That's exactly the secret-management surface that belongs outside the
device's domain. Adopting it would commit the project to maintaining and
vouching for an encryption product it doesn't own.
A specific red flag reinforcing the scope concern: the feature's value
proposition is *byte-identical cross-compatibility with a third-party
website* (legacyencryption.com → now
ericscalibur.github.io/Legacy_Encryption). That couples SeedSigner's
behavior to an external, single-maintainer spec the project doesn't
control, and effectively makes SeedSigner a client of someone's personal
product (the PR description even links the author's socials). Merging this
is an implicit, indefinite endorsement.
------------------------------
2. The "dual-key" design doesn't deliver what it claims
Even if encryption were in scope, the core security claim is misleading:
combined_key = benefactor_key + beneficiary_key # legacy_encryption.pyenc = encrypt_data(seed_phrase, combined_key)
"Dual-key" is just *string concatenation of two passwords into one PBKDF2
password*. This is not threshold cryptography, not secret sharing,
nothing like Shamir or a 2-of-2 multisig. Consequences:
- "Neither key alone reveals the seed" is trivially true of *any*
password and provides none of the guarantees the framing implies.
- Concatenation is ambiguous: "ab" + "c" derives the same key as "a" +
"bc". There's no separator or length binding between the two halves.
- It's strictly weaker than the multisig/passphrase options Bitcoin
already gives you, while *looking* like it offers more.
3. Other crypto/correctness problems
- *Padding is pointless and fragile.* It appends 0–4 random
chr(randbelow(256)) characters, but chr(128–255) encodes to *two*
UTF-8 bytes while paddingLength counts characters; decrypt then strips
characters from an errors="replace"-decoded string. GCM needs no
padding at all — this only adds a corruption/ambiguity vector.
- *validate_seed_phrase does not validate the BIP-39 checksum* — it
only checks word membership and count (12/24). Yet on failure the UI says "Bad
Checksum" / "doesn't match the checksum." The error message is simply
false, and invalid-checksum seeds pass validation.
- Reimplements wordlist loading (_load_wordlist with path-guessing +
embedded fallback) and seed validation instead of reusing Seed /
embit, which are already in the project.
4. Architectural / code-quality blockers
- *Invasive camera rewrite for all flows.* camera.py gains stream
"parking," busy-wait watchdogs, _force_close_stream, framerate
throttling, and time.sleep flush loops — ~130 lines of fragile MMAL
workarounds added to a singleton used by *every* scan flow, purely to
compensate for PBKDF2 starving the Pi Zero's single core. High regression
risk to core hardware code, in service of one out-of-scope feature.
- *Concurrency hacks in the view layer:* per-frame daemon decode
threads that are explicitly allowed to "hang forever," SIGALRM handlers,
DecodeQR subclasses that bypass type detection, manual Renderer frame
pushes (_sync_loading_frame), and dynamic SeedAddPassphraseScreen
subclassing that mutates self.components by index.
- *Secret handling regresses the project's discipline.* The seed
phrase and both keys are threaded as plain view_args strings across
many View instances and remain in the back-stack history; the self.seed_phrase
= "" "clear" is cosmetic since copies persist upstream. This sidesteps
the careful seed-in-controller.storage handling SeedSigner uses
elsewhere.
- *No tests.* Zero. SeedSigner maintains a high-coverage pytest suite
as a merge requirement; this is a standalone hard blocker.
- *No i18n.* Every string is a hardcoded English literal, not _()-wrapped
— right after the translations submodule was updated. Breaks the
localization convention project-wide.
- Debug _log.info tracing and scattered import gc; gc.collect() left
throughout.
—
Reply to this email directly, view it on GitHub
<#938?email_source=notifications&email_token=AA654NMBGFSFGU5C3S2XCTT5A7MPPA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTINZWGIYDMNBYGM2KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-4762064834>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AA654NOVVCIQCPYPURXJM5L5A7MPPAVCNFSNUABFKJSXA33TNF2G64TZHMZTEMJSGA3DINZUHNEXG43VMU5TINRTGU3DOOBTHEZKC5QC>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/AA654NKVDH6Z6LHIRL3YMCD5A7MPPA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTINZWGIYDMNBYGM2KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJKTGN5XXIZLSL5UW64Y>
and Android
<https://github.com/notifications/mobile/android/AA654NIZN2GI7GCWYHEXLQT5A7MPPA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTINZWGIYDMNBYGM2KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLTGN5XXIZLSL5QW4ZDSN5UWI>.
Download it today!
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
What this is
Legacy Encryption is a Bitcoin inheritance tool built on top of SeedSigner's existing hardware. A benefactor and a beneficiary each choose a password. The seed phrase is encrypted with both (PBKDF2-SHA256 · 600 000 iterations + AES-256-GCM) and stored as a QR code photographed by the user — no backend, no cloud.
Decryption requires both keys. Neither key alone reveals the seed. The encrypted QR can live in a drawer, a safety deposit box, or a family document — it is safe to store publicly.
Cross-compatibility: the Python implementation produces byte-identical output to a companion browser app (Legacy-offline.html), so the seed can always be recovered with a browser, without any SeedSigner hardware.
User flow
Encrypt
Decrypt
Files changed
New files
src/seedsigner/helpers/legacy_encryption.pycryptographyis already in the Buildroot image for PSBT signing. Produces byte-identical output to the JS reference.src/seedsigner/views/legacy_views.pyModified files
src/seedsigner/views/tools_views.py— adds "Legacy Encryption" as the last entry in ToolsMenuView.src/seedsigner/hardware/camera.py— Pi Zero–specific fixes required to keep the UI responsive during PBKDF2:stop_video_stream_mode()now parks thePiVideoStreambackground thread (sets_video_stream = NonesoLivePreviewThreadexits, but leaves the underlyingPiCameraopen at 1 fps). On Pi Zero, callingPiCamera()a second time while MMAL is releasing from the first session blocks the constructor indefinitely in the main thread. Parking avoids a secondPiCamera()call entirely.CameraConnectionError(existing upstream exception) ifcapture_continuoussilently stalls afterPiCamera()opens._force_close_stream(): gracefully signals then hard-closes a stalledPiVideoStream; used by both stop paths and the watchdog.start_single_frame_mode()drains any parked stream before opening its ownPiCamerainstance — two open MMAL instances deadlock.src/seedsigner/hardware/pivideostream.py— addslast_capture_time(updated every captured frame) used by Camera's stale-stream watchdog.Performance
PBKDF2 at 600 000 iterations takes 15–30 seconds on Pi Zero 1.3. A loading screen is shown. This is intentional — the iteration count is what makes brute-force attacks on a publicly-stored encrypted QR infeasible.
Testing
Tested end-to-end on Pi Zero 1.3 + Waveshare 1.3" 240×240 LCD HAT + OV5647 camera:
Security model
cryptographylibrary🤖 Generated with Claude Code