fix(python): TOTP codes must honor the entry's period and digits

TotpManager built pyotp.TOTP(secret) with library defaults (6 digits, 30s)
and ignored what the entry recorded, so any imported or derived entry using
other values produced codes the issuing service rejects. The service was
configured from the entry's values -- those govern the code.

- current_code_from_secret and current_code take period/digits
- EntryManager.get_totp_code passes the entry's values for both the
  derived and imported paths
- display_service, menu_handler (totp_list now carries digits) and the GUI
  updated at every call site
- regression tests for imported and deterministic entries with custom
  parameters, asserting against pyotp with the matching interval/digits,
  and confirming default entries are byte-identical to before

Found by scripts/cross_impl_check.py, which flagged it as a divergence
against the TypeScript port. That allowance is now removed: the suite
asserts TOTP equality and reports 30/30 with zero divergences.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Fable 5 committed 2026-08-14 16:41:04 -04:00
1 parent b4f20d417a
commit 5dcd779a6f
9 files changed
+132 -62

No files matched your search

+9 -11
View File
@@ -87,24 +87,22 @@ nostr, ssh.
## Known intentional divergences
1. **TOTP codes for entries with a non-default period/digits.** Python's
`TotpManager.current_code_from_secret` constructs `pyotp.TOTP(secret)`
with library defaults (6 digits, 30s) and ignores the entry's stored
`period`/`digits`; TS honors them. For an imported 8-digit/45s secret the
two produce different codes — and Python's would be rejected by the
issuing service. **TS behavior is believed correct; awaiting a product
decision on whether Python should be fixed to match.** Recorded by the
cross-impl suite as a `[DIVERGE]` line so it can never become silent.
2. **`kind` backfill on legacy entries** (see the migrations row): TS fills
1. **`kind` backfill on legacy entries** (see the migrations row): TS fills
`kind` from `type` at parse time for pre-v2 entries; Python leaves them
`type`-only and falls back on read. The filled shape matches what Python
writes for new entries, and Python reads it unchanged.
3. **Index/config file envelope**: Python wraps ciphertext in a kdf/ct JSON
2. **Index/config file envelope**: Python wraps ciphertext in a kdf/ct JSON
envelope; TS writes bare ciphertext. Both implementations read both forms
(Python via its legacy fallback, TS via `parseEncryptedFile`), verified in
cross-impl phases A/B/F/J.
**Resolved:** TOTP codes for entries with a non-default period/digits used
to differ — Python built `pyotp.TOTP(secret)` with library defaults and
ignored the entry's recorded values, so an imported 8-digit/45s secret
produced codes the issuing service would reject. Python was fixed to honor
the entry (`src/seedpass/core/totp.py` and every call site); the cross-impl
suite now asserts equality with no divergence allowance.
Any future divergence must carry a compatibility version, a
migration/fallback path, release notes, and tests (plan §6).