docker_backup: encryption settings passphrase stored double-encoded — UI passphrase ≠ effective key #55

Closed
opened 2026-08-19 22:07:25 +00:00 by Mark · 0 comments
Owner

Source: kanban card t_c34f4bfb (findings from encryption-at-rest end-to-end verification, parent t_8112dd39). Empirically confirmed on the live system 2026-08-19.

Problem

PUT /api/settings stores each settings value via JSON.stringify(value) per key; the encryption config reader getBackupEncryptionConfig() reads the stored encryptionKey with String(lookup.get('encryptionKey') ?? '') and does not JSON.parse it. Net effect: the effective passphrase is the user's typed value wrapped in literal double quotes.

Reproduction steps

  1. Set a passphrase P via PUT /api/settings (as a user would type it in the UI, e.g. probe-passphrase-2026-08-16).
  2. Read back the stored value: Setting.encryptionKey in /data/docker-backup.db contains the value with literal double quotes wrapped around it (e.g. "\"probe-passphrase-2026-08-16\"").
  3. Attempt external/tooling decryption of an archive encrypted with that passphrase (AES-256-GCM / PBKDF2 100k, DOCKER_BACKUP_ENC magic), using the unquoted passphrase the user actually typed.
  4. Observe the decryption fails with Unsupported state or unable to authenticate data (AES-GCM auth-tag failure).

Empirical proof (live archive /backups/full/cmswbp5f20006aq7x0k2kqhxv.tar.enc on zsus):

  • Decrypt using the QUOTED form "probe-passphrase-2026-08-16" → succeeds, yields a valid POSIX tar (hello.txt, sub/world.txt, sub/data.json).
  • Decrypt using the UNQUOTED form probe-passphrase-2026-08-16 (what a user types in the UI) → fails with AES-GCM auth-tag error.

Actual behavior

  • The passphrase a user enters in the UI ≠ the passphrase actually used (stored double-encoded / quoted).
  • External/tooling decryption with the user's typed passphrase FAILS.
  • Key rotation is dangerous: archives encrypted under the old quoted form are unrecoverable unless the quoted form is preserved.

Expected behavior

  • The passphrase a user enters in the UI should be the effective passphrase.
  • After storing passphrase P via PUT /api/settings, the DB value should equal P (no extra quotes).
  • Tooling-side decryption of a fresh archive with P should succeed.

Suggested fix

The settings store should JSON.parse the stored value on read (or store the string raw) so the UI passphrase is the effective passphrase. The crypto itself (magic DOCKER_BACKUP_ENC / header / AES-256-GCM / PBKDF2 100k) is verified sound — this is purely a read-path quoting bug.

Location

The #20 encryption settings wiring in the docker-backup-app settings handling (repo Mark/docker_backup): backend settings route (PUT /api/settings = JSON.stringify per key) and the backup config reader (reads encryptionKey with String() without a parse).

Verification

After fix: set passphrase P via PUT /api/settings; DB value should equal P (no extra quotes); tooling-side decrypt of a fresh archive with P should succeed.

Follow-up to the #20 encryption-at-rest verification (all acceptance criteria PASS there; this is a separate wiring defect).

**Source:** kanban card `t_c34f4bfb` (findings from encryption-at-rest end-to-end verification, parent `t_8112dd39`). Empirically confirmed on the live system 2026-08-19. ## Problem `PUT /api/settings` stores each settings value via `JSON.stringify(value)` per key; the encryption config reader `getBackupEncryptionConfig()` reads the stored `encryptionKey` with `String(lookup.get('encryptionKey') ?? '')` and does **not** `JSON.parse` it. Net effect: the effective passphrase is the user's typed value wrapped in literal double quotes. ## Reproduction steps 1. Set a passphrase `P` via `PUT /api/settings` (as a user would type it in the UI, e.g. `probe-passphrase-2026-08-16`). 2. Read back the stored value: `Setting.encryptionKey` in `/data/docker-backup.db` contains the value **with** literal double quotes wrapped around it (e.g. `"\"probe-passphrase-2026-08-16\""`). 3. Attempt external/tooling decryption of an archive encrypted with that passphrase (AES-256-GCM / PBKDF2 100k, `DOCKER_BACKUP_ENC` magic), using the **unquoted** passphrase the user actually typed. 4. Observe the decryption fails with `Unsupported state or unable to authenticate data` (AES-GCM auth-tag failure). Empirical proof (live archive `/backups/full/cmswbp5f20006aq7x0k2kqhxv.tar.enc` on zsus): - Decrypt using the QUOTED form `"probe-passphrase-2026-08-16"` → succeeds, yields a valid POSIX tar (`hello.txt`, `sub/world.txt`, `sub/data.json`). - Decrypt using the UNQUOTED form `probe-passphrase-2026-08-16` (what a user types in the UI) → fails with AES-GCM auth-tag error. ## Actual behavior - The passphrase a user enters in the UI ≠ the passphrase actually used (stored double-encoded / quoted). - External/tooling decryption with the user's typed passphrase FAILS. - Key rotation is dangerous: archives encrypted under the old quoted form are unrecoverable unless the quoted form is preserved. ## Expected behavior - The passphrase a user enters in the UI should be the effective passphrase. - After storing passphrase `P` via `PUT /api/settings`, the DB value should equal `P` (no extra quotes). - Tooling-side decryption of a fresh archive with `P` should succeed. ## Suggested fix The settings store should `JSON.parse` the stored value on read (or store the string raw) so the UI passphrase is the effective passphrase. The crypto itself (magic `DOCKER_BACKUP_ENC` / header / AES-256-GCM / PBKDF2 100k) is verified sound — this is purely a read-path quoting bug. ## Location The #20 encryption settings wiring in the docker-backup-app settings handling (repo `Mark/docker_backup`): backend settings route (`PUT /api/settings` = `JSON.stringify` per key) and the backup config reader (reads `encryptionKey` with `String()` without a parse). ## Verification After fix: set passphrase `P` via `PUT /api/settings`; DB value should equal `P` (no extra quotes); tooling-side decrypt of a fresh archive with `P` should succeed. Follow-up to the #20 encryption-at-rest verification (all acceptance criteria PASS there; this is a separate wiring defect).
Mark closed this issue 2026-08-19 22:07:25 +00:00
Sign in to join this conversation.
No description provided.