first commit
This commit is contained in:
105
docs/security-stack/crypto/crypto-bin/README.md
Normal file
105
docs/security-stack/crypto/crypto-bin/README.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# @suaveplan/crypto-bin - Suaveplan crypto bins: suaveplan-hash-password (Argon2id / blake2b password hash + verify CLI). Lives in its own package so the universal @suaveplan/crypto does not pull in @suaveplan/cli runtime deps.
|
||||
|
||||
## Summary
|
||||
|
||||
Suaveplan password-hashing CLI. Ships the `suaveplan-hash-password`
|
||||
binary — Argon2id (default) and BLAKE2b password hashing + verification
|
||||
with PHC/MCF-formatted output strings, distinct exit codes per failure
|
||||
class, and an env-injected output adapter so the core is testable
|
||||
without subprocess spawn.
|
||||
|
||||
This package lives separately from `@suaveplan/crypto` so the
|
||||
universal crypto façade does not pull CLI runtime deps into
|
||||
browser/server consumers that just want primitives.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [bin](./src/bin/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [hash-password](./src/bin/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [hash-password](./src/bin/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/bin/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/bin/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/bin/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_ - [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
# Hash a password (Argon2id by default)
|
||||
$ suaveplan-hash-password --password "correct horse battery staple"
|
||||
$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>
|
||||
|
||||
# Verify (exit 0 = match, 1 = mismatch)
|
||||
$ suaveplan-hash-password --password "correct horse battery staple" \
|
||||
--verify '$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>'
|
||||
$ echo $?
|
||||
0
|
||||
```
|
||||
|
||||
## Algorithms
|
||||
|
||||
- **`--algo argon2id`** (default) — OWASP 2023-recommended password hash.
|
||||
Output prefix `$argon2id$v=19$...`.
|
||||
- **`--algo blake2b`** — opt-in alternative for environments that need
|
||||
it. Output prefix `$blake2b$v=1$...`. Cost factor is configurable via
|
||||
`--cost-factor <n>` (1–31).
|
||||
|
||||
The verify mode auto-detects the algorithm from the stored hash's prefix,
|
||||
so callers do not have to track which algorithm was used at hash time.
|
||||
|
||||
## Exit codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|------------------------------------------------------------|
|
||||
| 0 | Success / verify matched |
|
||||
| 1 | Verify mismatch (wrong password) |
|
||||
| 2 | Bad arguments (missing/malformed flag value) |
|
||||
| 3 | Unknown algorithm (unrecognised `--algo` or stored prefix) |
|
||||
| 4 | Verify failed (stored hash is corrupt or unparseable) |
|
||||
|
||||
The `0` / `1` split matches the de-facto contract for shell pipelines —
|
||||
`if suaveplan-hash-password --password ... --verify ...; then ...; fi`
|
||||
works as expected, with `3+` reserved for operator errors so callers can
|
||||
distinguish "wrong password" from "stored data is broken".
|
||||
|
||||
## Programmatic use
|
||||
|
||||
```ts
|
||||
import { main } from "@suaveplan/crypto-bin";
|
||||
|
||||
const out: string[] = [];
|
||||
const err: string[] = [];
|
||||
const code = main(
|
||||
["--password", "pw", "--algo", "blake2b", "--cost-factor", "10"],
|
||||
{
|
||||
write: (s) => out.push(s),
|
||||
writeError: (s) => err.push(s),
|
||||
},
|
||||
);
|
||||
|
||||
if (code === 0) {
|
||||
storeHash(out.join("").trim());
|
||||
}
|
||||
```
|
||||
|
||||
The `main()` function never throws — every error path is captured and
|
||||
surfaced via `env.writeError(...)` and a non-zero return value. See
|
||||
`src/hash-password/hash-password.md` for the full contract.
|
||||
|
||||
## Public surface
|
||||
|
||||
- `main` — testable CLI core.
|
||||
- `parseArgs` — pure argv parser (throws `HashPasswordCliError`).
|
||||
- `detectAlgo` — algorithm sniffer for stored hashes.
|
||||
- `HashPasswordCliEnv` — output adapter interface.
|
||||
- `HashPasswordCliError` — error class wrapping the stable code registry.
|
||||
- `HASH_PASSWORD_CLI_CODES` — `BAD_ARGS` / `UNKNOWN_ALGO` /
|
||||
`VERIFY_FAILED`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `@suaveplan/crypto-hash` — Argon2id + BLAKE2b primitives.
|
||||
- `@suaveplan/error` — `SuaveplanError` base + stable code registry.
|
||||
- `@suaveplan/types` — shared cross-package contracts.
|
||||
|
||||
No dependency on `@suaveplan/cli` — argv parsing is hand-rolled (~40
|
||||
lines) so the bin stays small.
|
||||
|
||||
107
docs/security-stack/crypto/crypto-bin/src/bin/hash-password.md
Normal file
107
docs/security-stack/crypto/crypto-bin/src/bin/hash-password.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# `suaveplan-hash-password` bin entry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
This file is the platform-runtime entry for the `suaveplan-hash-password`
|
||||
binary shipped by `@suaveplan/crypto-bin`. It is intentionally a minimal
|
||||
shim: it constructs an `HashPasswordCliEnv` adapter from `process.stdout`
|
||||
and `process.stderr`, then delegates to the testable `main()` function in
|
||||
`../hash-password/hash-password.ts`. All argument parsing, algorithm
|
||||
dispatch, and exit-code logic live in that pure core so they can be unit
|
||||
tested by direct invocation rather than by spawning a subprocess.
|
||||
|
||||
The shim exists so the package can publish a Node-style `bin` entry under
|
||||
`./dist/bin/hash-password.js` (with the `#!/usr/bin/env node` shebang
|
||||
preserved by Vite) without coupling the testable core to `process.*`
|
||||
globals. Anything that touches `process.argv` / `process.exit` /
|
||||
`process.stdout` is confined to this module.
|
||||
|
||||
## Features
|
||||
|
||||
- Forwards `process.argv.slice(2)` into `main()` and exits with the
|
||||
returned numeric code.
|
||||
- Wires `process.stdout.write` / `process.stderr.write` into the
|
||||
`HashPasswordCliEnv` adapter — the core never references `process`
|
||||
directly.
|
||||
- Auto-exec guard: only runs `main()` when invoked as the top-level
|
||||
module (`process.argv[1]` ends with `hash-password.js`). Importing this
|
||||
module from a test or another script does not trigger an exit.
|
||||
- Coverage-excluded by design — this module is exercised end-to-end via
|
||||
the bin install path; the logic it wraps is fully covered by
|
||||
`../hash-password/hash-password.test.ts`.
|
||||
- Shebang line emitted verbatim into the dist bundle so the file is
|
||||
directly executable once symlinked by `bun install` / `npm install`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```bash
|
||||
# Once installed, the bin is on PATH via the package.json `bin` map
|
||||
$ suaveplan-hash-password --password "correct horse battery staple"
|
||||
$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>
|
||||
$ echo $?
|
||||
0
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```bash
|
||||
# Pipe-friendly verify (exit 0 = match, 1 = mismatch, 3+ = error)
|
||||
$ STORED='$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>'
|
||||
$ if suaveplan-hash-password --password "$PW" --verify "$STORED"; then
|
||||
> echo "login ok"
|
||||
> else
|
||||
> case $? in
|
||||
> 1) echo "wrong password" ;;
|
||||
> 3) echo "stored hash has unrecognised algorithm" ;;
|
||||
> 4) echo "stored hash is corrupt" ;;
|
||||
> *) echo "argument error" ;;
|
||||
> esac
|
||||
> fi
|
||||
```
|
||||
|
||||
For programmatic invocation that does not spawn a process, import the
|
||||
testable core directly instead of this shim:
|
||||
|
||||
```ts
|
||||
import { main } from "@suaveplan/crypto-bin";
|
||||
const code = main(["--password", "pw"], {
|
||||
write: (s) => process.stdout.write(s),
|
||||
writeError: (s) => process.stderr.write(s),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
This module has no public exports — it is a side-effect entry point.
|
||||
Behaviour:
|
||||
|
||||
- On import, registers the auto-exec guard and (if invoked as the
|
||||
top-level script) calls `main(argv, env)` and exits with its return
|
||||
value.
|
||||
- `argv`: `process.argv.slice(2)` — same shape `main()` accepts.
|
||||
- `env.write(s)`: forwards `s` to `process.stdout.write` synchronously.
|
||||
- `env.writeError(s)`: forwards `s` to `process.stderr.write`
|
||||
synchronously.
|
||||
|
||||
For the contract of `main()` itself (exit codes, accepted flags, error
|
||||
classes) see `../hash-password/hash-password.md`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The auto-exec guard checks `process.argv[1]?.endsWith("hash-password.js")`
|
||||
rather than `import.meta.url === ...` so the same module survives Vite's
|
||||
ES-module bundling without ESM-vs-CJS surprises in older Node hosts. The
|
||||
guard string matches the bundled filename (`hash-password.js`) deliberately
|
||||
— the source is `.ts` and is never executed directly in production.
|
||||
|
||||
`/* istanbul ignore */` markers wrap the guard and the `process.exit`
|
||||
call because the test suite invokes `main()` directly with a captured env
|
||||
adapter (see `../hash-password/hash-password.test.ts`). Spawning a
|
||||
subprocess to cover this two-line wrapper would add filesystem and Node
|
||||
runtime coupling to the test suite without exercising any logic that is
|
||||
not already covered. The `vitest.config.ts` coverage `exclude` list
|
||||
documents this decision in a sibling comment so future readers do not
|
||||
mistake the exclusion for an oversight.
|
||||
@@ -0,0 +1,63 @@
|
||||
# suaveplan-hash-password CLI
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`hash-password.ts` is the testable core of the `suaveplan-hash-password` binary in `@suaveplan/crypto-bin`. It accepts an argv-style list of flags, an env adapter (so tests can capture stdout/stderr without process side effects), and returns an exit code. Two modes: hash mode (no `--verify`) prints a stored hash; verify mode (`--verify <stored>`) checks a candidate password against a stored hash and returns 0/1 by match outcome.
|
||||
|
||||
The bin lives in its own package (`@suaveplan/crypto-bin`) rather than alongside the universal `@suaveplan/crypto` because CLI surfaces would force the entire `@suaveplan/cli` runtime tree into universal consumers; keeping the bin in a separate package preserves universal purity.
|
||||
|
||||
## Features
|
||||
|
||||
- Two algorithms: `argon2id` (default, OWASP 2023 recommended) and `blake2b` (opt-in via `--algo blake2b`).
|
||||
- Hash mode: prints `$argon2id$v=19$...` or `$blake2b$v=1$...` to stdout, exits 0.
|
||||
- Verify mode: detects algorithm from the stored hash's prefix and runs the matching `verifyPassword*` function. Exits 0 on match, 1 on mismatch, 3+ on argument / format errors.
|
||||
- Strict argument validation: missing `--password`, unknown `--algo`, malformed `--cost-factor` all surface as `HashPasswordCliError` with one of three stable error codes (`HASH_PASSWORD_CLI_BAD_ARGS` / `HASH_PASSWORD_CLI_UNKNOWN_ALGO` / `HASH_PASSWORD_CLI_VERIFY_FAILED`).
|
||||
- Env-injected stdout/stderr — never touches `process.stdout` / `process.stderr` directly, so tests can capture output without spawning a subprocess.
|
||||
- Per-spec exit-code contract: 0 = success / verify-match, 1 = verify-mismatch, 2 = bad args, 3 = unknown algo, 4 = verify failed (corrupt stored hash).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```bash
|
||||
# Hash a password (Argon2id by default)
|
||||
$ suaveplan-hash-password --password "correct horse battery staple"
|
||||
$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>
|
||||
|
||||
# Verify
|
||||
$ suaveplan-hash-password --password "correct horse battery staple" --verify '$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>'
|
||||
$ echo $?
|
||||
0
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// Programmatic invocation (e.g. from a test harness or a higher-level CLI)
|
||||
import { main } from "@suaveplan/crypto-bin/hash-password/hash-password";
|
||||
|
||||
const out: string[] = [];
|
||||
const err: string[] = [];
|
||||
const code = main(["--password", "pw", "--algo", "blake2b", "--cost-factor", "10"], {
|
||||
write: (s) => out.push(s),
|
||||
writeError: (s) => err.push(s),
|
||||
});
|
||||
|
||||
if (code === 0) {
|
||||
storeHash(out.join("").trim());
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `main(argv: readonly string[], env: HashPasswordCliEnv): number` — testable entry point.
|
||||
- `parseArgs(argv: readonly string[]): HashPasswordCliArgs` — pure argv parser; throws `HashPasswordCliError` on bad input.
|
||||
- `detectAlgo(storedHash: string): Algo` — algorithm sniffer; throws on unknown prefix.
|
||||
- `HashPasswordCliEnv { write(s), writeError(s) }` — output adapter interface.
|
||||
- `HashPasswordCliError extends SuaveplanError` — error class wrapping the three stable codes.
|
||||
- `HASH_PASSWORD_CLI_CODES` — exported registry: `HASH_PASSWORD_CLI_BAD_ARGS`, `HASH_PASSWORD_CLI_UNKNOWN_ALGO`, `HASH_PASSWORD_CLI_VERIFY_FAILED`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The CLI deliberately does not depend on `@suaveplan/cli` to avoid pulling the readline / TTY-prompt machinery into a tiny one-shot binary. argv parsing is hand-rolled (≈40 lines) because the option set is small and stable. Verify-mode exit codes are spec-pinned: 0 = match, 1 = mismatch — those two values are the contract for shell pipelines (`if suaveplan-hash-password --password ... --verify ...; then login; fi`). Errors from the verify path (corrupt stored hash) intentionally exit with `4` rather than `1` so a caller can distinguish "wrong password" from "your stored hash is corrupt"; the spec's `verify mismatch exit 1` requirement is preserved for the wrong-password case.
|
||||
Reference in New Issue
Block a user