mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-03 19:34:45 +00:00
7b5730910ea2ee5146f7fb121dded10cf61c8f4e
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
73ac93a9f1 |
Deliver the oversize refusal before hanging up, and pin a POSIX-only test
Two left from the platform run, one real.
THE DAEMON HUNG UP WITHOUT SAYING WHY, ON macOS ONLY
The line-length cap did `socket.write(refusal)` and then `socket.destroy()`.
destroy() tears the socket down immediately and discards whatever is still
buffered — on Linux the write usually reaches the kernel first and the peer
sees "request too large", on macOS it does not and the peer sees an
unexplained close. The refusal itself worked on both; only the reason was
lost, and only on one platform, which is why nothing noticed until this suite
ran there.
Now `end(payload, cb)` and destroy from the callback, so the bytes are
flushed before teardown, plus a `refused` flag so a peer still pushing the
rest of its megabyte is ignored rather than re-buffered.
The test that caught it was itself half the problem: it treated its own EPIPE
as fatal, when a client's broken pipe is the expected consequence of a server
correctly hanging up mid-write. Fixed in the previous commit; this one fixes
what that then exposed.
AND ONE TEST ASSERTING POSIX BEHAVIOUR WITHOUT SAYING SO
`honours backslash escapes outside single quotes` called parseCommandSpec with
no platform flag, so it read process.platform and asserted POSIX semantics on
Windows — where backslash is a path separator and deliberately is not an
escape. It now passes `{ windows: false }`, because that is the branch it is
about.
Windows went 15 failures to 1 to 0; macOS 2 to 1 to 0.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
|
||
|
|
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 |
||
|
|
e1ea9c2b48 |
Teach the mutation harness two blind spots it had, and close what they found
The harness only mutated block-form guards (`if (cond) {`), so every guard
written as a one-liner was invisible to it — which turned out to be most of
the load-bearing ones. src/highRisk.ts generated ZERO mutants and printed as
a clean sweep. Three changes to the tool:
- Mutate single-statement guards too (`if (cond) return ...;`). This alone
surfaced 27 previously unreachable mutants, including the entire token
validation chain: revoked, expired, exhausted, scope-not-granted.
- Report a target that generates no mutants as NOT MEASURED. Zero killed and
zero survived prints exactly like a clean sweep, and it is the one result
that must never be read as a pass.
- Add `kind`, `mnemonic` and `seed` to the security hints. A token's `kinds`
list is half of what it may reach, and leaving the word out hid the shape
check that stops a string `kinds` becoming a substring match.
Also adds `--kind` so a new mutation rule can be swept across every target
without re-judging the mutants already settled.
What the new mutants found, all of it previously unasserted:
- A token's `kinds` restriction was never tested. This file's own header
claimed kinds and labels were both covered; only labels were. Removing the
check let a token scoped to one kind read every other kind's secrets.
- `token-issue` would mint a token for a profile the agent is not holding.
The seed being resident is the only proof the caller had authority over
that vault.
- `token-issue`'s array shape checks for `scopes` and `kinds`. A string
`kinds` turns tokenMaySee's exact match into String.includes.
- An empty high-risk factor. Refused in both implementations, tested in
neither; it would wrap the partition key under nothing while
`factorConfigured` still answered true.
- POST /api/v1/fingerprint/select accepting a profile that does not exist —
the caller's string becomes ctx.fingerprint, which later requests join onto
a path. AppDir repeats the check, so the guard's real value is answering
404 instead of an unhandled 500.
- The API's requiresPassword flag is redundant for every route today, so
disabling it changed nothing. Its purpose is the route that does not exist
yet, whose author sets the flag and forgets the call — so the test now
registers exactly that route and confirms the server layer stops it before
the handler runs.
- sinks.ts drops SEEDPASS_-prefixed variables from a child's environment.
Nothing on the allowlist starts with SEEDPASS_, so the backstop cannot fire
against the real list. sinkEnv now takes the allowlist as a defaulted
parameter purely so that future-edit case can be exercised.
All six targets now sweep to zero survivors.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
|
||
|
|
cf18ab8091 |
test: close the gaps mutation testing found in approvals, sinks, and a flaky test
Approvals scored 58%. Five survivors, all genuine:
- `ttlSeconds` and `uses` validation was unguarded by any test. An approval
issued with ttl 0 is expired the instant it exists; one with uses 0 can
never be consumed. Either would look like a granted authorization to
whoever asked for it and silently authorize nothing — the confusing
failure rather than the safe one.
- The expiry boundary (`<=` vs `<`) was unpinned. It now asserts that an
approval expiring exactly now is refused, and that one millisecond earlier
it still works, so the boundary is where it claims to be rather than
approximately there. Python uses `<=`, so this is parity as well as the
safer direction.
Approvals now 100%.
Sinks scored 67%. The survivor turned `&&` into `||` in the command-spec
parser, which makes a multi-element spec whose FIRST element contains
whitespace get tokenized — silently discarding every element after it. Losing
an argument changes which command runs, and dropping a `--dry-run` or a target
path is exactly the kind of silent change that matters for a sink being handed
a secret. Sinks now 100%.
Separately, the full suite caught a flaky test of my own: the recovery-split
tamper check rewrote the payload's tail and, depending on the payload's
padding, sometimes reproduced the original string exactly — asserting nothing.
It only failed when a randomized share happened to end the wrong way. Now it
flips a character and asserts the result actually differs before expecting the
throw, plus a second case that tampers at four positions rather than trusting
one. Five consecutive runs are clean.
The agent's mapping was also wrong: agent.ts holds the high-risk session, whose
tests live in highRisk.test.ts, so leaving that suite out scored the agent at
38% and blamed the code for a gap that was really a gap in the harness.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
|
||
|
|
135a6268ce |
feat(tui): actually clear the clipboard after the configured delay
clipboard_clear_delay has been in the default config (45s), settable, and read by nothing: clipboardSink copied and returned. That made TUI Secret Mode -- whose entire purpose is to route secrets off the screen -- strictly less safe than Python's, because it parked them on a session-wide clipboard indefinitely. clipboardSink now takes an optional clearAfterSeconds. When set, it schedules a wipe after the delay, but only if the clipboard still holds the value it put there, so a value the user copied in the meantime is left alone -- the same guard Python's copy_to_clipboard(text, timeout) applies via a read-back. Where the clipboard cannot be read (no paste tool), it clears unconditionally, because leaving a secret parked is the worse outcome. The timer is unref'd: it fires while the long-running TUI is up but never holds a process open, so the one-shot CLI and the agent are unchanged (they pass no delay). Both TUI clipboard paths -- Secret Mode reveals and the entry 'C' action -- now route through the profile's clipboard_clear_delay. Tests use a fake wl-copy/wl-paste pair on PATH: the secret is wiped after the delay, and a value copied over it in the meantime survives. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tkj4rzKBA3uhfgk7kqjsb8 |
||
|
|
b0e2a309a4 |
feat: generate a master seed, and fix three bugs found by using the CLI
Driving the CLI from an empty app directory turned up a gap that no test
covered: there was no way to create a vault. Every path assumed you already
had a seed and told you to export SEEDPASS_MNEMONIC, so a new user could not
get started at all. Python has generate_bip85_seed; the port did not.
fingerprint create fills that in with the same construction Python uses --
32 bytes of OS entropy through BIP-85 app 39 index 0. The entropy call sits
outside any try/catch on purpose, carried over from Python's entropy audit:
nothing may catch a CSPRNG failure and substitute a value.
Where the phrase goes is a deliberate choice rather than a default. This is
the one secret in SeedPass with no other copy, so:
- the destination is validated BEFORE generating, and delivery happens
BEFORE the profile is created -- a failure at either point leaves
nothing behind rather than a vault whose seed nobody has;
- --out writes 0600 with O_EXCL, so an existing file is never clobbered;
- with no destination and a non-TTY stdout it refuses outright, instead of
writing the only copy of a seed into a pipe -- a log, a transcript, an
agent's context.
Also fixed, all found the same way:
- use --exec always exited 0, so chaining a deploy command after it with
&& reported success even when the deploy failed. The sink child's exit
status is now the CLI's own.
- the sink command spec split on whitespace and ignored quotes, so
--exec 'sh -c "..."' -- the most common form there is -- handed sh the
fragments of the script and died on an unterminated string.
- --tags work,dev silently stored a single tag named "work,dev" that no
search would ever match. Commas and spaces both separate now.
Two normal states reported raw errno: a stopped agent said
'connect ENOENT /.../agent.sock', and an occupied --out path said EEXIST.
The capability map claimed ssh/pgp were unported. Both work and are at
parity with Python; automation branches on that list, so it now describes
this build.
Verified against Python on a TS-generated seed: same fingerprint, same
password, same TOTP code. 320 JS tests and 36/36 cross-impl checks pass.
Co-Authored-By: Claude Fable 5 <[email protected]>
|