mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-28 16:32:06 +00:00
7b5730910ea2ee5146f7fb121dded10cf61c8f4e
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e2e9aa851e |
Fix what three platforms found: two shipped bugs, four unportable tests
The platform workflow's first run produced seventeen failures on Windows and macOS, where the JavaScript suite had never executed. Two were real defects in shipped code. A CONTAINMENT CHECK THAT REFUSED EVERYTHING ON WINDOWS `resolveWithinProfile` compared `target.startsWith(root + "/")` with a hardcoded forward slash. On Windows `resolve()` returns `C:\profile\file.txt` and the comparison asked for `C:\profile/`, so it matched nothing: document export and import refused every path, including legitimate ones. It failed CLOSED, so nothing leaked — the feature simply did not work. Now uses `relative()`, which needs no separator of our choosing. A COMMAND PARSER THAT ATE WINDOWS PATHS `parseCommandSpec` treated backslash as a POSIX escape unconditionally, so `--exec "C:\Program Files\tool.exe"` was spawned as the command `C:Program` with an argument `Filestool.exe`. Every Windows path handed to --exec or --stdin-to was mangled before the spawn. This is the SECOND instance of this exact bug today — the Python TUI palette had it too — which is what a whole class looks like rather than a one-off. `windows` is an explicit parameter, not a `process.platform` read, so both branches are tested from either platform. That parameter is the actual fix for the class: the reason these survived is that the Windows branch could only ever run on Windows. THE REST WERE TESTS, NOT CODE - `/bin/true` does not exist on macOS (it is /usr/bin/true), so a spawn failed with ENOENT and the refusal looked like the daemon's crash-safety fix having regressed. Now uses process.execPath, which exists everywhere by definition. - The oversized-request test treated its own EPIPE as fatal. The daemon caps the line, answers and hangs up; on macOS that hangup lands while the client is still pushing the remaining megabyte. Correct server behaviour was failing the test on one platform and passing on the other. - Three sink tests wrote `#!/bin/sh` files. Shebangs and /bin/sh do not exist on Windows. Now Node scripts run by the interpreter already running the tests. - Eight `0600` assertions and two clipboard tests are guarded by documented constants in test/helpers/platform.ts. Windows has no POSIX mode bits and the runner has no clipboard. The skips state a gap rather than closing it, and TODO.md now records that the Windows permissions question grew from two Python tests to ten across both languages — including the vault index and the semantic index, which hold secrets. With TypeScript becoming the only implementation that is a shipping question, not a test-suite one. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN |
||
|
|
75e370d098 |
feat(vault): detect shared BIP-85 derivation coordinates; bind Python imports too (L-1, L-8)
L-1 — three entry kinds derive from BIP-85 app 32 at the entry's index:
ssh@N -> 32 bytes of m/83696968'/32'/N'
pgp@N -> 32 bytes of m/83696968'/32'/N' (the SAME 32 bytes)
password@N -> 64 bytes of the same, then PBKDF2
So ssh@N and pgp@N are byte-for-byte the same Ed25519 private key, and
password@N takes that key as its entropy prefix. The password stays safe (256
unknown bits remain, PBKDF2 is one-way) but this is not key separation, and
SSH keys are exactly what gets exported to a server or uploaded to a forge.
Deliberately NOT fixing the derivation itself yet. Domain-separating the three
uses is a versioned protocol change in both implementations, i.e. a new
derivation path -- and the project's own note is that "a derivation bug does
not throw, it quietly produces a secret that cannot be recovered". Adding one
immediately before the first outside review inverts the point of the review.
The v2 design is written up in TODO.md instead, ready to implement after it.
What ships is the audit's short-term recommendation, which needs no derivation
change: notice the state and say so. findDerivationCollisions /
find_derivation_collisions report cross-kind clashes at one app-32 index,
worst first, in both implementations with identical message text -- a user
moving between the two must not be told different things about one vault.
Wired into every path the state can arrive by: vault import (CLI and TUI, both
implementations), nostr restore's merge, `vault import --inspect` before
anything is written, and a new `util check-derivation` for auditing an
existing vault (exit 1 when it finds something, for scripts).
Neither implementation can create the state -- index == vault id, ids unique,
watermark prevents reuse -- so this is about imported, foreign, and merged
data, which is also why it must never be the thing that fails an import: it
reports, never refuses, and tolerates malformed input.
The lead test in each implementation does not exercise the detector at all.
It derives the keys and compares the bytes, proving the property the detector
reports. Without that, the rest only checks that a written-down rule is
applied consistently, and a derivation change could falsify the rule with
everything still green.
L-8 (Python) — while wiring the above it turned out Python's import_backup had
the same unbound-plaintext-import gap the audit found in TypeScript: the
wrapper carries a `fingerprint` and nothing compared it. Now fixed there too,
with a new ProfileMismatchError so the TUI can offer the override instead of
reporting a generic failure.
The comparison is against the fingerprint DERIVED FROM THE SEED, not the
profile directory's name. In production they are the same string, but the
question worth asking is "was this backup made from my seed?", and only the
seed answers it -- comparing directory names refused a legitimate restore of
the same seed into a freshly named profile, which test_account_roundtrip_
restore caught immediately. It is enforced only when the wrapper records
something that is actually a fingerprint: a backup carrying some other label
offers no evidence about its source seed, and refusing on no evidence blocks
restores without making anyone safer.
All guards mutation-verified on both sides.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
|
||
|
|
4b01ac62e0 |
fix(cli): real rolling backups, and imports bound to their profile (L-4, L-8)
Two findings from the 2026-08-18 audit, entangled in the same files.
L-4 — the additional backup location was inert. The TUI accepted a path and
answered "Additional backups will be written to X"; nothing ever wrote one.
That is worse than an absent setting: it is false assurance in the feature a
user leans on precisely when everything else has gone wrong. The cause was
larger than the setting — Python's BackupManager is not ported at all, so
`backup_interval` was dead for the same reason.
New backups.ts ports the write side: after every committed mutation, the
encrypted index is copied to <profile>/backups/entries_db_backup_<ts>.json.enc
at 0600, and mirrored to `additional_backup_path` as <fingerprint>_<name>.
Filenames match Python's byte for byte so the two implementations share one
backup directory. Hooked after the save in both mutation funnels, matching
Python's create_backup()-after-_save_index() order, so a snapshot is the
post-mutation state.
Two deliberate deviations, both documented in the source:
- Throttling reads the newest snapshot on disk rather than Python's
in-memory _last_backup_time. Every CLI command is its own process, so an
in-memory counter would make backup_interval inert for CLI use — the same
bug in a new place.
- A failed mirror is reported rather than swallowed. Best-effort must not
fail a committed vault write, but silence would restore the false
assurance this change exists to remove.
Quick Unlock stays in the menu for item-order parity but now says it is not
implemented instead of storing a preference nothing reads. In Python the flag
never changed unlocking either — it writes an audit entry and raises a
security finding.
L-8 — plaintext backup import was not bound to a profile. A seed-only backup
is implicitly seed-bound: the wrong seed cannot decrypt it. A plaintext one
imports into any profile and is then re-derived from the TARGET seed, so every
password, SSH key, PGP key and seed silently comes out different from the one
the backup was taken to preserve. Nothing errors anywhere along the way.
Both the CLI and the TUI now compare the wrapper's fingerprint to the target
profile and require an explicit override (--allow-fingerprint-mismatch, or a
confirmation in the TUI). `vault import --inspect` reports the backup's
fingerprint and encryption mode so the mismatch is visible before committing.
The CLI also stops deciding encryption mode by regex over the whole file: new
parseBackupWrapper reads the envelope, which is the structural question being
asked.
Tests: backups.test.ts (9 cases: filename parity, 0600, verbatim ciphertext,
mirror naming, mirror-failure reporting, interval throttling, CLI end-to-end)
plus foreign-import cases in cli.test.ts and tuiMenus.test.ts using a genuine
second profile rather than a doctored file. All mutation-verified: removing
each hook or guard turns the matching tests red.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
|
||
|
|
cf433e3a73 |
fix(cli): vault import with --vault no longer demands a default profile
`vault import` resolved the active fingerprint unconditionally, even though an explicit --vault target never uses it. On a machine with no ~/.seedpass that made the command fail with "no profile selected" -- and it is why the ts-parity workflow has been red for 33 straight runs while every local run passed: the developer machine has a real ~/.seedpass, so the environment coupling was invisible exactly where it mattered. The profile lookup is now lazy, used only for the default-vault path. The deeper fix is in the test harness: cli.test.ts now sets a hermetic SEEDPASS_APP_DIR, so any code path that touches the default profile directory fails locally the way it fails in CI instead of borrowing the developer's real vault state. Verified as a negative control: with the hermetic env and the old code, the import test reproduces CI's exact "Unexpected end of JSON input" locally; with the fix, all 158 CLI tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tkj4rzKBA3uhfgk7kqjsb8 |
||
|
|
0482bc3dfd |
feat(cli): .seedpass backup naming (phase 1 -- convention only, zero format change)
A portable backup is an encrypted, checksummed, cross-impl-verified file; its only sin was landing on disk as an anonymous .json. Exports now self-identify: point `vault export` (or the TUI database export) at a directory and the file is named seedpass-<fingerprint>-<date>.seedpass -- tied to the profile it restores and sortable by date. The TUI also appends .seedpass to an extensionless destination, since interactive input has no script-compatibility concerns. The zero-risk boundary, kept deliberately: an explicit file path is used exactly as typed -- a script that exports to backup.json still gets backup.json -- and imports sniff content, never extensions, so any old export restores unchanged. No byte of the format moved, which is what makes this safe to ship ahead of the review gate while format_version 2 (magic header) and the OS icon association stay queued behind it. Tests pin all three behaviours in both surfaces: directory target gets the generated name, extensionless TUI input gets the suffix, explicit paths pass through untouched. 158 CLI tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tkj4rzKBA3uhfgk7kqjsb8 |
||
|
|
e434b0abc1 |
feat(schema): carry unknown entry kinds through instead of refusing the vault
Spec section 8.2: a record whose kind this build does not understand -- one written by a newer build, or by another application sharing the vault, such as a future BitLogin namespaced kind -- must be carried through untouched, never dropped, and must not brick the whole index. TS previously failed the entire parse on the first unrecognized kind, stranding any shared vault on older SeedPass builds. The entry schema is now a union: known kinds validate strictly against the discriminated union exactly as before, and anything else parses as an opaque loose record. The refine on the opaque branch is the load-bearing detail -- a malformed record of a KNOWN kind must still fail its own schema, never fall through and escape validation. Records with no kind or type at all remain rejected as garbage. Statically the entries map stays typed to the known kinds (exhaustive switches keep their meaning); the module comment states the runtime widening plainly. At the CLI surface, foreign records list with unrecognized fields redacted to has_* flags -- another application's data is not ours to hand out -- and reveal refuses with "unsupported entry kind" instead of guessing. Allocation counts foreign ids, so neighbours never collide with them. Python's natural tolerance is now pinned by test, which surfaced a real nuance: Python backfills base-field defaults (tags, links, date_modified) onto foreign records on load, where TS carries them verbatim. No data is lost, so both conform to the preservation rule as now worded (no loss, no reinterpretation; base-field normalization permitted) -- but the two produce different canonical bytes for the same foreign record, which matters once foreign kinds sync. Recorded in the spec's gap ledger to align before any application ships them. Verified: 4 of the 6 new TS tests fail against the previous schema; 230 core + 152 CLI tests and the cross-impl suite pass with the change. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tkj4rzKBA3uhfgk7kqjsb8 |
||
|
|
1eab3bf137 |
feat: interactive mode, so the bare command opens the vault
Python's `seedpass` launches its TUI when invoked with no subcommand. The port printed a help dump instead, which is why running the bare command looked broken: nothing was wrong with it, there was simply no way in. This adds the interactive mode that was missing. Scope is the daily path -- browse, search, copy, reveal, add a password / TOTP / key-value entry, archive, switch profile -- not a port of Python's v2 and v3 TUIs, which are around 20k lines of Textual between them. Piped or redirected, the bare command still prints the help: there is no terminal to drive. No new runtime dependencies. Node's tty and ANSI directly, rather than a TUI framework, because everything bundled here shares a process with unlocked seed phrases and the CLI's runtime surface is currently commander plus the noble/scure crypto libraries. The agent-blind rule from plan section 9.3 carries over intact, and matters more here than in the CLI because this screen renders the whole vault at once: list and detail views show metadata only, `c` copies without ever drawing the value, and `r` is the single explicit reveal -- on screen until the next keypress, then gone from the following frame. Everything is drawn on the alternate screen buffer, so a revealed secret cannot be recovered by scrolling back after quitting, and never lands in a terminal log that captures the main buffer. The key-value field is masked while being typed for the same reason. terminal.ts owes the user one thing above all: their shell back. Raw mode with a hidden cursor is an unusable terminal, so restoration is idempotent and wired to exit, SIGINT, SIGTERM, SIGHUP, and the uncaught-exception paths -- verified through a pty for each. Holding raw mode across the whole unlock sequence rather than toggling it per prompt also fixes a narrow leak: echo came back on between password attempts, so a password retyped after a typo was echoed in clear text. Tests drive the real screens through an injected terminal, including the redaction properties above. Two harness bugs worth recording, because both produced confident nonsense: sampling the frame count after dispatching a key made a synchronous redraw look like no redraw at all, and waiting only for "the screen stopped changing" returned during the second-long PBKDF2 a mutation performs, so the next keystroke landed on the previous screen and typed "q" into a form field instead of quitting. 122 CLI tests, 220 core tests, 36/36 cross-impl checks. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
0dfeca8a50 |
fix: a bare seedpass-js printed help and then reported a bogus error
exitOverride() makes commander throw for everything it would otherwise exit on -- displaying help and its own usage errors included. The handler only recognised two of its codes, so running `seedpass-js` with no arguments printed the help and followed it with `error: (outputHelp)`, and unknown commands had their message printed twice. Any commander.* code is now commander's business: honour its exit code and let it own the message. SeedPass's own errors still print as before. Found by running the command. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
1864343084 |
cutover: close gates 5, 6 and 7
Gate 5 (feature parity) -- document import/export ported: `entry import-document` reads a text file (label defaults to the file stem, type from the extension) and `entry export-document` writes one back, reproducing Python's filename sanitization, directory-vs-file destination handling and overwrite refusal. That was the last concrete item; the three remaining exclusions (PGP RSA, semantic, api) are now documented decisions rather than open work, and none of them blocks cutover. Gate 6 (packaging) -- js/packages/cli/build.mjs bundles the CLI into a single self-contained ESM file with no runtime dependencies beyond Node 22, writes a .sha256 beside it, and is wired up as the package `bin` and `prepack` so npm install/pack produce a working command. --version now reports a real version stamped at build time. CI builds the bundle and smoke-tests it from a directory with no node_modules -- version, capabilities, profile creation, entry creation, reveal -- then uploads it. Verified locally that the bundle runs standalone outside the workspace. Gate 7 (migration and rollback) -- docs/typescript_migration_guide.md: the migration is a no-op (same profiles), so the guide covers what to back up, how to rehearse against a copy with SEEDPASS_APP_DIR, the two behavioral differences users will notice, the known exclusions, and seed-only recovery. Rollback is proven rather than asserted: new cross-impl phase K drives a profile through the TS CLI -- several entry kinds created, one edited, one archived -- then checks Python reopens it, sees every change, derives the same secrets, and can still write to it afterwards. Also adds js/packages/cli/README.md documenting the reference-first output model and the scoped-token workflow. 193 core + 49 CLI tests green; 36/36 cross-implementation checks green. All eight cutover gates are now green. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
dcb8fc9219 |
fix: four real bugs found by end-to-end manual testing
Ran the full CLI surface against a freshly generated throwaway seed (profile -> agent unlock -> all 8 entry kinds -> reveal/sinks -> tokens -> audit -> backup -> relay sync -> seed-only recovery). Four defects that no unit test had caught: - clipboard sink hung the CLI forever. xclip/wl-copy fork a resident process to own the X/Wayland selection; it inherited our stdio, so any reader of the CLI's stdout blocked indefinitely, and waiting for process exit never returned. Now detached with ignored stdio, success declared once stdin is flushed. - 'vault import' read the mnemonic straight from the env, so it failed whenever the vault was unlocked through the agent -- the normal path. - 'vault import' never wrote anything; it only printed a summary. It now restores into the active vault, refuses to overwrite a non-empty vault without --yes, and keeps the old behavior behind --inspect. - A corrupt/foreign vault file surfaced the raw codec error "invalid token encoding". It now names the actual likely causes. Also documented the variadic-option limitation that a trailing flag is lost (commander stops collecting at the next dash-prefixed token): a quoted command spec now splits on whitespace, no shell involved. Regression tests for all four. 151 core + 44 CLI tests green. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
4f2b8a79bf |
TS port slice 7: entry modify/archive/links + util generate-password
- core entryMod: modify_entry port with the per-kind allowed-field matrix, policy merging, blacklisted-field cleanup and timestamp touching; archive/restore; typed links (add/remove/get with Python's normalization, dedupe on target/relation/note, self-link and missing-target refusal). - Parity fixture entry_mods.json: the real Python EntryManager applies a 10-op modification sequence at a later pinned clock; TS replays the same ops from the creation state and matches byte-for-byte. - CLI: entry modify/archive/unarchive/links/link-add/link-remove, entry totp-codes (explicit egress, honors --at and archived state), util generate-password (Python parity: index 0 + gen v1 defaults, full policy flags — v1/v2 outputs verified against password fixtures). - capabilities: egress command list updated. - 148 core parity tests (Node + jsdom) + 22 CLI tests green. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
e91d5d0be2 |
TS port slice 5: agent-blind provisioning, entry-secret parity, nostr app fix
- core entryOps: add* functions for all creatable kinds rebuild the Python EntryManager fixture index byte-for-byte under a pinned clock — field shapes, id allocation, independent TOTP derivation-index allocation, and Python's ISO timestamp format all verified. - CLI 'entry add password/totp/key-value/document/seed/managed-account/ nostr': provision-blind — returns the reference and metadata, never the secret the entry represents; tests assert the derived password / imported secret / child mnemonic appear nowhere in provisioning output. - Parity bug found and fixed by the new entry_secrets fixtures: nostr ENTRY keys derive via the default BIP-85 app 39 path (EntryManager), not app 1237 (sync client identity); the CLI had used 1237. Reveal values for password/TOTP/nostr/seed/managed-account now match Python retrieval. - 129 core parity tests (Node + jsdom) + 16 CLI tests green. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
ab569fe900 |
TS port slice 4: portable backup, CI, and the agent-blind CLI MVP
- Portable backup (format_version 1): TS imports Python exports, re-exports
byte-identically under pinned nonce/timestamp, verifies the canonical
checksum, rejects tampering and unknown versions.
- .github/workflows/ts-parity.yml: typecheck + Node/jsdom parity suite on
js/ changes, plus a fixture-drift job that regenerates fixtures from
Python and fails if the committed ones don't match (manifest commit field
excluded by construction).
- packages/cli — seedpass-js MVP implementing plan section 9.3:
* reference-first output: entry list/get/search emit sp://entry/<id> refs
and metadata; secret-bearing fields become has_* flags
* 'entry reveal' is the only plaintext egress command
* 'use --clipboard|--exec|--stdin-to' delivers secrets to sinks; --exec
injects SEEDPASS_SECRET into the child env, never argv or stdout
* vault export/import over portable backups; capabilities --format json
* mnemonic comes from SEEDPASS_MNEMONIC env, never the command line
- CLI tests prove the blindness properties: derived password matches the v2
fixture, TOTP at pinned time, secrets absent from all default output and
from sink-delivery confirmations.
- 125 core parity tests (Node + jsdom) + 11 CLI tests green.
Co-Authored-By: Claude Fable 5 <[email protected]>
|