Skip to content

[Feature] Import legacy AES-256 encrypted seed backups via QR code#882

Open
berlinxray wants to merge 1 commit intoSeedSigner:devfrom
berlinxray:feat/encrypted-seed-qr-import
Open

[Feature] Import legacy AES-256 encrypted seed backups via QR code#882
berlinxray wants to merge 1 commit intoSeedSigner:devfrom
berlinxray:feat/encrypted-seed-qr-import

Conversation

@berlinxray
Copy link
Copy Markdown

@berlinxray berlinxray commented Feb 21, 2026

Summary

Scan a QR code containing an OpenSSL-compatible AES-256-CBC encrypted seed backup, decrypt it with a user-provided passphrase, validate as BIP-39 or Electrum mnemonic, and import as a seed.

Note: This PR is focused exclusively on encrypted seed QR import. The Seed XOR rebuild flow and Electrum XOR support are in a separate PR (#884).

Workflow

  1. Scan QR code (auto-detects U2FsdGVkX1 / Salted__ prefix)
  2. Prompt for decryption passphrase
  3. Decrypt, validate as BIP-39 or Electrum mnemonic
  4. Import as a seed

Activated via Settings > Advanced > Encrypted seeds (SETTING__ENCRYPTED_SEEDS, disabled by default).

Changes (10 files)

Core (src/seedsigner/helpers/aes_decrypt.py) — New

  • Pure-Python AES-256-CBC decryptor (no pyaes dependency)
  • Precomputed InvMixColumns lookup tables for Pi Zero performance
  • PBKDF2 key derivation with auto-detection of 10,000 and 100,000 iterations
  • decrypt_openssl_aes256cbc() and DecryptionError exception

QR Detection (src/seedsigner/models/decode_qr.py, qr_type.py)

  • Add QRType.SEED__ENCRYPTED constant
  • Add EncryptedSeedQrDecoder for base64-encoded encrypted seed QRs
  • Auto-detect encrypted QRs by U2FsdGVkX1 prefix

Settings (src/seedsigner/models/settings_definition.py)

  • Add SETTING__ENCRYPTED_SEEDS toggle (advanced, disabled by default)

Views (src/seedsigner/views/seed_views.py)

  • EncryptedSeedPassphraseView — Passphrase entry screen
  • EncryptedSeedDecryptView — Decryption + BIP-39/Electrum validation
  • EncryptedSeedDecryptionFailedView — Retry/discard on failure

Scan (src/seedsigner/views/scan_views.py)

  • Route encrypted QR scans to decryption flow
  • Check SETTING__ENCRYPTED_SEEDS toggle
  • Add back button support for scan screen

GUI (src/seedsigner/gui/screens/seed_screens.py)

  • Fix SeedAddPassphraseScreen to preserve custom title ("Decryption Passphrase")

Tests

  • tests/test_aes_decrypt.py — AES-256-CBC unit tests
  • tests/test_encrypted_seed_qr.py — Encrypted QR integration tests
  • tests/conftest.py — Shared test fixtures

Legacy Workflow (reference)

# Encrypt
openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -in seed.txt -out seed.enc
qrencode -r seed.enc -o seedenc.png

# Decrypt (verify)
cat seed.enc | openssl aes-256-cbc -a -d -pbkdf2

Checklist

  • pytest passes
  • Tested on Raspberry Pi OS (manual build)
  • Tested on SeedSigner OS (Pi0)

Test plan

  • All 11 tests pass (AES unit + encrypted QR integration)
  • Manual test: Scan encrypted seed QR → enter passphrase → verify seed imports correctly
  • Manual test: Wrong passphrase → "Decryption Failed" → retry works
  • Manual test: Feature disabled → OptionDisabledView shown

@berlinxray berlinxray changed the title feat: add support for importing AES-256-CBC encrypted seed backups vi… feat: Import AES-256-CBC encrypted seed backups via QR code Feb 21, 2026
@berlinxray berlinxray changed the title feat: Import AES-256-CBC encrypted seed backups via QR code [NEW FEATURE] Import AES-256-CBC encrypted seed backups via QR code Feb 21, 2026
@berlinxray berlinxray changed the title [NEW FEATURE] Import AES-256-CBC encrypted seed backups via QR code [Feature] Import AES-256-CBC encrypted seed backups via QR code Feb 21, 2026
@berlinxray berlinxray force-pushed the feat/encrypted-seed-qr-import branch 2 times, most recently from cfb08ad to 739037d Compare February 21, 2026 06:00
@berlinxray berlinxray marked this pull request as draft February 21, 2026 20:29
@berlinxray berlinxray marked this pull request as ready for review February 22, 2026 05:03
@berlinxray berlinxray changed the title [Feature] Import AES-256-CBC encrypted seed backups via QR code [Feature] Import AES-256 encrypted seed backups via QR code Feb 22, 2026
@berlinxray berlinxray changed the title [Feature] Import AES-256 encrypted seed backups via QR code [Feature] Import legacy AES-256 encrypted seed backups via QR code Feb 22, 2026
…a QR

Scan a QR code containing an OpenSSL-compatible AES-256-CBC encrypted
seed (base64-encoded, starting with U2FsdGVkX1 / Salted__), decrypt
with a user-provided passphrase, validate as BIP-39 or Electrum
mnemonic, and import as a seed.

- Add pure-Python AES-256-CBC decryptor (no pyaes dependency) with
  precomputed InvMixColumns lookup tables for Pi Zero performance
- Add EncryptedSeedQrDecoder and QRType.SEED__ENCRYPTED for auto-
  detecting encrypted seed QRs
- Add SETTING__ENCRYPTED_SEEDS toggle (advanced, disabled by default)
- Add EncryptedSeedPassphraseView, EncryptedSeedDecryptView, and
  EncryptedSeedDecryptionFailedView for the decrypt/retry UI flow
- Support both 10,000 and 100,000 PBKDF2 iterations (auto-detect)
- Fix SeedAddPassphraseScreen to preserve custom title for decrypt flow
- Add back button support for scan screen
- Add 11 tests (AES unit + encrypted QR integration)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@berlinxray berlinxray force-pushed the feat/encrypted-seed-qr-import branch from 412bf5a to 8b8809b Compare March 2, 2026 03:40
@none34829
Copy link
Copy Markdown

none34829 commented Apr 9, 2026

Checked out this branch and ran the tests locally. Went through the AES implementation- verified _inv_cipher_block against the NIST FIPS 197 Appendix C.3 known-answer test (key 000102...1f, ciphertext 8ea2b7ca..., plaintext 00112233...) and it produces the correct output, so the core cipher looks solid.

One thing I noticed with the tests though: every success-path test in test_aes_decrypt.py generates its expected vectors at runtime by shelling out to openssl. If openssl isn't on the machine, those all get pytest.skip'd and the AES never actually gets verified against a correct decryption. The error-path tests are hardcoded but they only cover failure modes.

For a hand-rolled AES on a signing device I think there should be at least one hardcoded vector that works without any external tools. Something like:

def test_decrypt_hardcoded_vector():
    """Pre-computed OpenSSL vector- no openssl binary needed at test time."""
    encrypted = "U2FsdGVkX1+5ER21G7dz8fbBzXbp4p3LiMqJuwXOJN29f+1djh0nhJovkaPluB7Vcm9yr9g5Ss65pFvt+UQTsiYiEwJSiL9D08dn5AXmI5UmPR2WJuBQD14tEMrX/eXXVNLtG3Gy7qbiJkidJepX0w=="
    result = decrypt_openssl_aes256cbc(encrypted, "TestVector2026")
    assert result == "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"

I generated that with:

echo -n "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" | openssl enc -aes-256-cbc -pbkdf2 -iter 10000 -base64 -A -pass pass:TestVector2026

Ran it against the branch and it passes. Just means the happy path is always covered regardless of the test environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants