first commit
This commit is contained in:
207
docs/security-stack/security/otp/README.md
Normal file
207
docs/security-stack/security/otp/README.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# @suaveplan/otp - RFC 4226 (HOTP) + RFC 6238 (TOTP) + RFC 6287 (OCRA) + Google Authenticator Key URI Format + Base32 (RFC 4648 §6) + recovery codes + clock-drift detector — zero browser/Node deps
|
||||
|
||||
## Summary
|
||||
|
||||
> **RFC 4226 (HOTP) + RFC 6238 (TOTP) + RFC 6287 (OCRA) + Google Authenticator Key URI Format + Base32 (RFC 4648 §6) + recovery codes + clock-drift detector** — zero browser/Node deps.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [base32](./src/base32/base32.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [base32](./src/base32/base32.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [drift](./src/drift/detect.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [detect](./src/drift/detect.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [codes](./src/errors/codes.md) — Stable, registered error codes for `@suaveplan/otp` plus the typed `OtpError` base class. Every failure that crosses th…
|
||||
- [zod-map](./src/errors/zod-map.md) — Translate Zod boundary failures into the most-specific `OtpErrorCode`. When a caller passes options that fail strict-mo…
|
||||
- [hotp](./src/hotp/hotp.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
- [encode-counter](./src/hotp/encode-counter.md) — Encode an HOTP counter as the 8-byte big-endian block required by RFC 4226 §5.1. `bigint` is the canonical input type b…
|
||||
- [hmac](./src/hotp/hmac.md) — Algorithm-routing HMAC façade for the OTP family. SHA-256 and SHA-512 route through `@suaveplan/crypto-hash` (the canon…
|
||||
- [hotp](./src/hotp/hotp.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
- [hotp.schema](./src/hotp/hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [ocra](./src/ocra/ocra.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - ocra.schema — Zod boundary schema for `OcraParams`……
|
||||
- [encode-challenge](./src/ocra/encode-challenge.md) — Encode the OCRA challenge into the 128-byte block required by RFC 6287 §5.1. Three challenge formats are supported: `QN…
|
||||
- [mutual-auth](./src/ocra/mutual-auth.md) — RFC 6287 §7.1 mutual authentication helpers. Both sides compute OCRA over the concatenation of the client and server ch…
|
||||
- [ocra](./src/ocra/ocra.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - ocra.schema — Zod boundary schema for `OcraParams`……
|
||||
- [ocra.schema](./src/ocra/ocra.schema.md) — Zod boundary schema for `OcraParams` — the runtime parameter bag passed alongside a suite string and a key. Optional fi…
|
||||
- [suite](./src/ocra/suite.md) — Parse an OCRA suite string into a typed `OcraSuite` AST per RFC 6287 §6.1. Exposed separately so callers can validate s…
|
||||
- [verify-ocra](./src/ocra/verify-ocra.md) — Constant-time verify of an OCRA response. Recomputes the response under the supplied suite, key, and params, then compa…
|
||||
- [generate](./src/recovery/generate.md) — Generate Crockford-base32 recovery codes — the printable single-use fallback the user prints / writes down when first e…
|
||||
- [hash](./src/recovery/hash.md) — Hash recovery codes with Argon2id for at-rest storage. Recovery codes are long-lived shared secrets — they must NOT be…
|
||||
- [verify](./src/recovery/verify.md) — Verify a user-supplied recovery code against a list of stored hashes. Constant-time over the full set — every entry is…
|
||||
- [totp](./src/totp/totp.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - totp.schema — Zod boundary schemas for `TotpOptions…
|
||||
- [totp](./src/totp/totp.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - totp.schema — Zod boundary schemas for `TotpOptions…
|
||||
- [totp.schema](./src/totp/totp.schema.md) — Zod boundary schemas for `TotpOptions` and `TotpVerifyOptions`. The TOTP schema covers algorithm / digits / period / t0…
|
||||
- [verify-many](./src/totp/verify-many.md) — Backup-aware TOTP verify. Used during rotation windows where an old and a new TOTP secret coexist — the user's authenti…
|
||||
- [verify](./src/totp/verify.md) — RFC 6238 §5.2 verify with drift window. Checks the supplied code against the current step and `±window` neighbouring st…
|
||||
- [format-uri](./src/uri/format-uri.md) — Build a canonical `otpauth://` URI from an `OtpUri` value object. Round-trips with `parseUri` for every valid input. Pe…
|
||||
- [parse-uri](./src/uri/parse-uri.md) — Parse the `otpauth://` Key URI Format used by Google Authenticator and every compatible authenticator app. Supports bot…
|
||||
- [uri.schema](./src/uri/uri.schema.md) — Zod boundary schemas for the `OtpUri` discriminated union. `OtpTotpUriSchema` and `OtpHotpUriSchema` cover the per-vari…
|
||||
<!-- end:auto-docs --> - [base32](./src/base32/base32.md) — <!-- begin:auto-docs --> _(no submodules)_- [drift](./src/drift/detect.md) — <!-- begin:auto-docs --> _(no submodules)_- [detect](./src/drift/detect.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — Stable, registered error codes for `@suaveplan/otp` plus the typed `OtpError` base class. Every failure that crosses th…
|
||||
- [zod-map](./src/errors/zod-map.md) — Translate Zod boundary failures into the most-specific `OtpErrorCode`. When a caller passes options that fail strict-mo…
|
||||
- [hotp](./src/hotp/hotp.md) — <!-- begin:auto-docs --> _(no submodules)_- hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
- [encode-counter](./src/hotp/encode-counter.md) — Encode an HOTP counter as the 8-byte big-endian block required by RFC 4226 §5.1. `bigint` is the canonical input type b…
|
||||
- [hmac](./src/hotp/hmac.md) — Algorithm-routing HMAC façade for the OTP family. SHA-256 and SHA-512 route through `@suaveplan/crypto-hash` (the canon…
|
||||
- [hotp](./src/hotp/hotp.md) — <!-- begin:auto-docs --> _(no submodules)_- hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
- [hotp.schema](./src/hotp/hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [ocra](./src/ocra/ocra.md) — <!-- begin:auto-docs --> _(no submodules)_- ocra.schema — Zod boundary schema for `OcraParams`……
|
||||
- [encode-challenge](./src/ocra/encode-challenge.md) — Encode the OCRA challenge into the 128-byte block required by RFC 6287 §5.1. Three challenge formats are supported: `QN…
|
||||
- [mutual-auth](./src/ocra/mutual-auth.md) — RFC 6287 §7.1 mutual authentication helpers. Both sides compute OCRA over the concatenation of the client and server ch…
|
||||
- [ocra](./src/ocra/ocra.md) — <!-- begin:auto-docs --> _(no submodules)_- ocra.schema — Zod boundary schema for `OcraParams`……
|
||||
- [ocra.schema](./src/ocra/ocra.schema.md) — Zod boundary schema for `OcraParams` — the runtime parameter bag passed alongside a suite string and a key. Optional fi…
|
||||
- [suite](./src/ocra/suite.md) — Parse an OCRA suite string into a typed `OcraSuite` AST per RFC 6287 §6.1. Exposed separately so callers can validate s…
|
||||
- [verify-ocra](./src/ocra/verify-ocra.md) — Constant-time verify of an OCRA response. Recomputes the response under the supplied suite, key, and params, then compa…
|
||||
- [generate](./src/recovery/generate.md) — Generate Crockford-base32 recovery codes — the printable single-use fallback the user prints / writes down when first e…
|
||||
- [hash](./src/recovery/hash.md) — Hash recovery codes with Argon2id for at-rest storage. Recovery codes are long-lived shared secrets — they must NOT be…
|
||||
- [verify](./src/recovery/verify.md) — Verify a user-supplied recovery code against a list of stored hashes. Constant-time over the full set — every entry is…
|
||||
- [totp](./src/totp/totp.md) — <!-- begin:auto-docs --> _(no submodules)_- totp.schema — Zod boundary schemas for `TotpOptions…
|
||||
- [totp](./src/totp/totp.md) — <!-- begin:auto-docs --> _(no submodules)_- totp.schema — Zod boundary schemas for `TotpOptions…
|
||||
- [totp.schema](./src/totp/totp.schema.md) — Zod boundary schemas for `TotpOptions` and `TotpVerifyOptions`. The TOTP schema covers algorithm / digits / period / t0…
|
||||
- [verify-many](./src/totp/verify-many.md) — Backup-aware TOTP verify. Used during rotation windows where an old and a new TOTP secret coexist — the user's authenti…
|
||||
- [verify](./src/totp/verify.md) — RFC 6238 §5.2 verify with drift window. Checks the supplied code against the current step and `±window` neighbouring st…
|
||||
- [format-uri](./src/uri/format-uri.md) — Build a canonical `otpauth://` URI from an `OtpUri` value object. Round-trips with `parseUri` for every valid input. Pe…
|
||||
- [parse-uri](./src/uri/parse-uri.md) — Parse the `otpauth://` Key URI Format used by Google Authenticator and every compatible authenticator app. Supports bot…
|
||||
- [uri.schema](./src/uri/uri.schema.md) — Zod boundary schemas for the `OtpUri` discriminated union. `OtpTotpUriSchema` and `OtpHotpUriSchema` cover the per-vari…
|
||||
- [base32](./src/base32/base32.md) — <!-- begin:auto-docs --> _(no submodules)_- [drift](./src/drift/detect.md) — <!-- begin:auto-docs --> _(no submodules)_- [detect](./src/drift/detect.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — Stable, registered error codes for `@suaveplan/otp` plus the typed `OtpError` base class. Every failure that crosses th…
|
||||
- [zod-map](./src/errors/zod-map.md) — Translate Zod boundary failures into the most-specific `OtpErrorCode`. When a caller passes options that fail strict-mo…
|
||||
- [hotp](./src/hotp/hotp.md) — <!-- begin:auto-docs --> _(no submodules)_- hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
- [encode-counter](./src/hotp/encode-counter.md) — Encode an HOTP counter as the 8-byte big-endian block required by RFC 4226 §5.1. `bigint` is the canonical input type b…
|
||||
- [hmac](./src/hotp/hmac.md) — Algorithm-routing HMAC façade for the OTP family. SHA-256 and SHA-512 route through `@suaveplan/crypto-hash` (the canon…
|
||||
- [hotp](./src/hotp/hotp.md) — <!-- begin:auto-docs --> _(no submodules)_- hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
- [hotp.schema](./src/hotp/hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [ocra](./src/ocra/ocra.md) — <!-- begin:auto-docs --> _(no submodules)_- ocra.schema — Zod boundary schema for `OcraParams`……
|
||||
- [encode-challenge](./src/ocra/encode-challenge.md) — Encode the OCRA challenge into the 128-byte block required by RFC 6287 §5.1. Three challenge formats are supported: `QN…
|
||||
- [mutual-auth](./src/ocra/mutual-auth.md) — RFC 6287 §7.1 mutual authentication helpers. Both sides compute OCRA over the concatenation of the client and server ch…
|
||||
- [ocra](./src/ocra/ocra.md) — <!-- begin:auto-docs --> _(no submodules)_- ocra.schema — Zod boundary schema for `OcraParams`……
|
||||
- [ocra.schema](./src/ocra/ocra.schema.md) — Zod boundary schema for `OcraParams` — the runtime parameter bag passed alongside a suite string and a key. Optional fi…
|
||||
- [suite](./src/ocra/suite.md) — Parse an OCRA suite string into a typed `OcraSuite` AST per RFC 6287 §6.1. Exposed separately so callers can validate s…
|
||||
- [verify-ocra](./src/ocra/verify-ocra.md) — Constant-time verify of an OCRA response. Recomputes the response under the supplied suite, key, and params, then compa…
|
||||
- [generate](./src/recovery/generate.md) — Generate Crockford-base32 recovery codes — the printable single-use fallback the user prints / writes down when first e…
|
||||
- [hash](./src/recovery/hash.md) — Hash recovery codes with Argon2id for at-rest storage. Recovery codes are long-lived shared secrets — they must NOT be…
|
||||
- [verify](./src/recovery/verify.md) — Verify a user-supplied recovery code against a list of stored hashes. Constant-time over the full set — every entry is…
|
||||
- [totp](./src/totp/totp.md) — <!-- begin:auto-docs --> _(no submodules)_- totp.schema — Zod boundary schemas for `TotpOptions…
|
||||
- [totp](./src/totp/totp.md) — <!-- begin:auto-docs --> _(no submodules)_- totp.schema — Zod boundary schemas for `TotpOptions…
|
||||
- [totp.schema](./src/totp/totp.schema.md) — Zod boundary schemas for `TotpOptions` and `TotpVerifyOptions`. The TOTP schema covers algorithm / digits / period / t0…
|
||||
- [verify-many](./src/totp/verify-many.md) — Backup-aware TOTP verify. Used during rotation windows where an old and a new TOTP secret coexist — the user's authenti…
|
||||
- [verify](./src/totp/verify.md) — RFC 6238 §5.2 verify with drift window. Checks the supplied code against the current step and `±window` neighbouring st…
|
||||
- [format-uri](./src/uri/format-uri.md) — Build a canonical `otpauth://` URI from an `OtpUri` value object. Round-trips with `parseUri` for every valid input. Pe…
|
||||
- [parse-uri](./src/uri/parse-uri.md) — Parse the `otpauth://` Key URI Format used by Google Authenticator and every compatible authenticator app. Supports bot…
|
||||
- [uri.schema](./src/uri/uri.schema.md) — Zod boundary schemas for the `OtpUri` discriminated union. `OtpTotpUriSchema` and `OtpHotpUriSchema` cover the per-vari…
|
||||
- [base32](./src/base32/base32.md) — <!-- begin:auto-docs --> _(no submodules)_- [drift](./src/drift/detect.md) — <!-- begin:auto-docs --> _(no submodules)_- [detect](./src/drift/detect.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — Stable, registered error codes for `@suaveplan/otp` plus the typed `OtpError` base class. Every failure that crosses th…
|
||||
- [zod-map](./src/errors/zod-map.md) — Translate Zod boundary failures into the most-specific `OtpErrorCode`. When a caller passes options that fail strict-mo…
|
||||
- [hotp](./src/hotp/hotp.md) — <!-- begin:auto-docs --> _(no submodules)_- hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
- [encode-counter](./src/hotp/encode-counter.md) — Encode an HOTP counter as the 8-byte big-endian block required by RFC 4226 §5.1. `bigint` is the canonical input type b…
|
||||
- [hmac](./src/hotp/hmac.md) — Algorithm-routing HMAC façade for the OTP family. SHA-256 and SHA-512 route through `@suaveplan/crypto-hash` (the canon…
|
||||
- [hotp](./src/hotp/hotp.md) — <!-- begin:auto-docs --> _(no submodules)_- hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
- [hotp.schema](./src/hotp/hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [ocra](./src/ocra/ocra.md) — <!-- begin:auto-docs --> _(no submodules)_- ocra.schema — Zod boundary schema for `OcraParams`…
|
||||
- [encode-challenge](./src/ocra/encode-challenge.md) — Encode the OCRA challenge into the 128-byte block required by RFC 6287 §5.1. Three challenge formats are supported: `QN…
|
||||
- [mutual-auth](./src/ocra/mutual-auth.md) — RFC 6287 §7.1 mutual authentication helpers. Both sides compute OCRA over the concatenation of the client and server ch…
|
||||
- [ocra](./src/ocra/ocra.md) — <!-- begin:auto-docs --> _(no submodules)_- ocra.schema — Zod boundary schema for `OcraParams`…
|
||||
- [ocra.schema](./src/ocra/ocra.schema.md) — Zod boundary schema for `OcraParams` — the runtime parameter bag passed alongside a suite string and a key. Optional fi…
|
||||
- [suite](./src/ocra/suite.md) — Parse an OCRA suite string into a typed `OcraSuite` AST per RFC 6287 §6.1. Exposed separately so callers can validate s…
|
||||
- [verify-ocra](./src/ocra/verify-ocra.md) — Constant-time verify of an OCRA response. Recomputes the response under the supplied suite, key, and params, then compa…
|
||||
- [generate](./src/recovery/generate.md) — Generate Crockford-base32 recovery codes — the printable single-use fallback the user prints / writes down when first e…
|
||||
- [hash](./src/recovery/hash.md) — Hash recovery codes with Argon2id for at-rest storage. Recovery codes are long-lived shared secrets — they must NOT be…
|
||||
- [verify](./src/recovery/verify.md) — Verify a user-supplied recovery code against a list of stored hashes. Constant-time over the full set — every entry is…
|
||||
- [totp](./src/totp/totp.md) — <!-- begin:auto-docs --> _(no submodules)_- totp.schema — Zod boundary schemas for `TotpOptions…
|
||||
- [totp](./src/totp/totp.md) — <!-- begin:auto-docs --> _(no submodules)_- totp.schema — Zod boundary schemas for `TotpOptions…
|
||||
- [totp.schema](./src/totp/totp.schema.md) — Zod boundary schemas for `TotpOptions` and `TotpVerifyOptions`. The TOTP schema covers algorithm / digits / period / t0…
|
||||
- [verify-many](./src/totp/verify-many.md) — Backup-aware TOTP verify. Used during rotation windows where an old and a new TOTP secret coexist — the user's authenti…
|
||||
- [verify](./src/totp/verify.md) — RFC 6238 §5.2 verify with drift window. Checks the supplied code against the current step and `±window` neighbouring st…
|
||||
- [format-uri](./src/uri/format-uri.md) — Build a canonical `otpauth://` URI from an `OtpUri` value object. Round-trips with `parseUri` for every valid input. Pe…
|
||||
- [parse-uri](./src/uri/parse-uri.md) — Parse the `otpauth://` Key URI Format used by Google Authenticator and every compatible authenticator app. Supports bot…
|
||||
- [uri.schema](./src/uri/uri.schema.md) — Zod boundary schemas for the `OtpUri` discriminated union. `OtpTotpUriSchema` and `OtpHotpUriSchema` cover the per-vari…
|
||||
- [base32](./src/base32/base32.md) — <!-- begin:auto-docs --> _(no submodules)_- [drift](./src/drift/detect.md) — <!-- begin:auto-docs --> _(no submodules)_ - [detect](./src/drift/detect.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — Stable, registered error codes for `@suaveplan/otp` plus the typed `OtpError` base class. Every failure that crosses th…
|
||||
- [zod-map](./src/errors/zod-map.md) — Translate Zod boundary failures into the most-specific `OtpErrorCode`. When a caller passes options that fail strict-mo…
|
||||
- [hotp](./src/hotp/hotp.md) — <!-- begin:auto-docs --> _(no submodules)_ - [encode-counter](./src/hotp/encode-counter.md) — Encode an HOTP counter as the 8-byte big-endian block required by RFC 4226 §5.1. `bigint` is the canonical input type b…
|
||||
- [hmac](./src/hotp/hmac.md) — Algorithm-routing HMAC façade for the OTP family. SHA-256 and SHA-512 route through `@suaveplan/crypto-hash` (the canon…
|
||||
- [hotp](./src/hotp/hotp.md) — <!-- begin:auto-docs --> _(no submodules)_ - [hotp.schema](./src/hotp/hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ - [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [ocra](./src/ocra/ocra.md) — <!-- begin:auto-docs --> _(no submodules)_ - [encode-challenge](./src/ocra/encode-challenge.md) — Encode the OCRA challenge into the 128-byte block required by RFC 6287 §5.1. Three challenge formats are supported: `QN…
|
||||
- [mutual-auth](./src/ocra/mutual-auth.md) — RFC 6287 §7.1 mutual authentication helpers. Both sides compute OCRA over the concatenation of the client and server ch…
|
||||
- [ocra](./src/ocra/ocra.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ocra.schema](./src/ocra/ocra.schema.md) — Zod boundary schema for `OcraParams` — the runtime parameter bag passed alongside a suite string and a key. Optional fi…
|
||||
- [suite](./src/ocra/suite.md) — Parse an OCRA suite string into a typed `OcraSuite` AST per RFC 6287 §6.1. Exposed separately so callers can validate s…
|
||||
- [verify-ocra](./src/ocra/verify-ocra.md) — Constant-time verify of an OCRA response. Recomputes the response under the supplied suite, key, and params, then compa…
|
||||
- [generate](./src/recovery/generate.md) — Generate Crockford-base32 recovery codes — the printable single-use fallback the user prints / writes down when first e…
|
||||
- [hash](./src/recovery/hash.md) — Hash recovery codes with Argon2id for at-rest storage. Recovery codes are long-lived shared secrets — they must NOT be…
|
||||
- [verify](./src/recovery/verify.md) — Verify a user-supplied recovery code against a list of stored hashes. Constant-time over the full set — every entry is…
|
||||
- [totp](./src/totp/totp.md) — <!-- begin:auto-docs --> _(no submodules)_ - [totp](./src/totp/totp.md) — <!-- begin:auto-docs --> _(no submodules)_ - [totp.schema](./src/totp/totp.schema.md) — Zod boundary schemas for `TotpOptions` and `TotpVerifyOptions`. The TOTP schema covers algorithm / digits / period / t0…
|
||||
- [verify-many](./src/totp/verify-many.md) — Backup-aware TOTP verify. Used during rotation windows where an old and a new TOTP secret coexist — the user's authenti…
|
||||
- [verify](./src/totp/verify.md) — RFC 6238 §5.2 verify with drift window. Checks the supplied code against the current step and `±window` neighbouring st…
|
||||
- [format-uri](./src/uri/format-uri.md) — Build a canonical `otpauth://` URI from an `OtpUri` value object. Round-trips with `parseUri` for every valid input. Pe…
|
||||
- [parse-uri](./src/uri/parse-uri.md) — Parse the `otpauth://` Key URI Format used by Google Authenticator and every compatible authenticator app. Supports bot…
|
||||
- [uri.schema](./src/uri/uri.schema.md) — Zod boundary schemas for the `OtpUri` discriminated union. `OtpTotpUriSchema` and `OtpHotpUriSchema` cover the per-vari…
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/otp
|
||||
```
|
||||
|
||||
## Subpath exports
|
||||
|
||||
| Subpath | Surface |
|
||||
|---|---|
|
||||
| `@suaveplan/otp` | HOTP / TOTP / verify / verifyMany / detectDrift / recovery codes |
|
||||
| `@suaveplan/otp/uri` | `parseUri`, `formatUri`, `OtpUri`, `OtpUriSchema` |
|
||||
| `@suaveplan/otp/base32` | `base32Encode`, `base32Decode` |
|
||||
| `@suaveplan/otp/ocra` | `ocra`, `parseOcraSuite`, `verifyOcra`, mutual-auth helpers |
|
||||
| `@suaveplan/otp/errors` | `OTP_CODES`, `OtpError`, `OtpErrorCode` |
|
||||
| `@suaveplan/otp/observability` | OTel instrument factories + recorders |
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { totp, verify } from "@suaveplan/otp";
|
||||
import { base32Decode } from "@suaveplan/otp/base32";
|
||||
import { parseUri } from "@suaveplan/otp/uri";
|
||||
|
||||
const u = parseUri("otpauth://totp/alice?secret=JBSWY3DPEHPK3PXP&issuer=Example");
|
||||
const secret = base32Decode(u.secret);
|
||||
|
||||
const code = totp(secret);
|
||||
const r = verify(code, secret);
|
||||
// r === { valid: true, delta: 0 }
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`base32/base32.md`](src/base32/base32.md) — RFC 4648 §6 base32.
|
||||
- [`hotp/hotp.md`](src/hotp/hotp.md) — RFC 4226 HOTP.
|
||||
- [`hotp/encode-counter.md`](src/hotp/encode-counter.md) — 8-byte big-endian.
|
||||
- [`hotp/hmac.md`](src/hotp/hmac.md) — algorithm-routing HMAC.
|
||||
- [`hotp/hotp.schema.md`](src/hotp/hotp.schema.md) — Zod boundary.
|
||||
- [`totp/totp.md`](src/totp/totp.md) — RFC 6238 TOTP.
|
||||
- [`totp/verify.md`](src/totp/verify.md) — drift-windowed verify.
|
||||
- [`totp/verify-many.md`](src/totp/verify-many.md) — backup-aware verify.
|
||||
- [`totp/totp.schema.md`](src/totp/totp.schema.md) — Zod boundary.
|
||||
- [`uri/parse-uri.md`](src/uri/parse-uri.md) — `otpauth://` parser.
|
||||
- [`uri/format-uri.md`](src/uri/format-uri.md) — `otpauth://` builder.
|
||||
- [`uri/uri.schema.md`](src/uri/uri.schema.md) — Zod boundary.
|
||||
- [`ocra/ocra.md`](src/ocra/ocra.md) — RFC 6287 OCRA.
|
||||
- [`ocra/suite.md`](src/ocra/suite.md) — suite-string parser.
|
||||
- [`ocra/encode-challenge.md`](src/ocra/encode-challenge.md) — challenge block.
|
||||
- [`ocra/verify-ocra.md`](src/ocra/verify-ocra.md) — constant-time verify.
|
||||
- [`ocra/mutual-auth.md`](src/ocra/mutual-auth.md) — RFC §7.1 mutual auth.
|
||||
- [`ocra/ocra.schema.md`](src/ocra/ocra.schema.md) — Zod boundary.
|
||||
- [`recovery/generate.md`](src/recovery/generate.md) — Crockford codes.
|
||||
- [`recovery/hash.md`](src/recovery/hash.md) — Argon2id-hashed at-rest form.
|
||||
- [`recovery/verify.md`](src/recovery/verify.md) — constant-time verify.
|
||||
- [`drift/detect.md`](src/drift/detect.md) — clock-drift detector.
|
||||
- [`observability/metrics.md`](src/observability/metrics.md) — OTel.
|
||||
- [`errors/codes.md`](src/errors/codes.md) — `OTP_CODES` / `OtpError`.
|
||||
- [`errors/zod-map.md`](src/errors/zod-map.md) — Zod → code mapper.
|
||||
|
||||
## Standards
|
||||
|
||||
- RFC 4226 — HOTP (counter-based).
|
||||
- RFC 6238 — TOTP (time-based).
|
||||
- RFC 6287 — OCRA (challenge-response).
|
||||
- RFC 4648 §6 — Base32.
|
||||
- Google Authenticator Key URI Format — `otpauth://` scheme.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
58
docs/security-stack/security/otp/src/base32/base32.md
Normal file
58
docs/security-stack/security/otp/src/base32/base32.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# `base32/base32`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
RFC 4648 §6 base32 encode + decode. The OTP profile defaults to **unpadded**
|
||||
output to match the Google Authenticator Key URI Format (`secret=...` is
|
||||
unpadded in the wild); spec-strict consumers can opt in via `padding: true`.
|
||||
Decode is case-insensitive and tolerates whitespace between groups so QR-code
|
||||
URLs that wrap long secrets round-trip cleanly.
|
||||
|
||||
## Features
|
||||
|
||||
- `base32Encode(bytes, opts?)` — bit-packed five-at-a-time encode.
|
||||
- `base32Decode(input)` — single-pass reverse-table decode.
|
||||
- 32-character alphabet `A–Z2–7` per RFC 4648 §6.
|
||||
- Lower-case alias only for letters (digits 2–7 are case-invariant).
|
||||
- `OTP.BASE32_INVALID` raised on non-alphabet character.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { base32Decode, base32Encode } from "@suaveplan/otp/base32";
|
||||
|
||||
const bytes = base32Decode("JBSWY3DPEHPK3PXP");
|
||||
const back = base32Encode(bytes);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { base32Encode } from "@suaveplan/otp/base32";
|
||||
|
||||
// Spec-strict consumer that needs `=` padding.
|
||||
const padded = base32Encode(new Uint8Array([0x66, 0x6f, 0x6f]), {
|
||||
padding: true,
|
||||
});
|
||||
// padded === "MZXW6==="
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `base32Encode(bytes: Uint8Array, opts?: { padding?: boolean }): string`
|
||||
— returns the base32 form. `padding` defaults to `false`.
|
||||
- `base32Decode(input: string): Uint8Array` — decodes padded or unpadded
|
||||
input; whitespace is stripped; case-insensitive; non-alphabet characters
|
||||
raise `OtpError(OTP.BASE32_INVALID)` with the offending position.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The decode table is a 128-entry `Int8Array` indexed by char-code. Letters are
|
||||
aliased to their lower-case slots; digits are not aliased because '2'+32 ='R'
|
||||
which would otherwise overwrite the letter slot for `R`. The encoder emits
|
||||
unpadded output by default to match the prevailing Google Authenticator wire
|
||||
format; `padding: true` makes the output multiple-of-eight characters and
|
||||
RFC-4648-compliant for spec-strict consumers.
|
||||
63
docs/security-stack/security/otp/src/drift/detect.md
Normal file
63
docs/security-stack/security/otp/src/drift/detect.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# `drift/detect`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Search outside the regular verify window for clock drift. Callers use this
|
||||
to render UI hints like _"your device clock is 47 seconds fast"_ — the
|
||||
verify path with `window=1` would simply reject such a code, leaving the
|
||||
user with no idea why. `detectDrift` sweeps a wider range and returns both
|
||||
the signed step delta and the inferred clock offset in milliseconds.
|
||||
|
||||
## Features
|
||||
|
||||
- Default `±60` steps (= ±30 minutes at the default `period=30`).
|
||||
- Returns `{ delta, suggestedClockOffsetMs }` discriminated union.
|
||||
- Constant-time across the sweep range — same defence as `verify`.
|
||||
- Honours the same TOTP option bag as `verify` (algorithm / digits / period
|
||||
/ t0 / clock).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { detectDrift } from "@suaveplan/otp";
|
||||
|
||||
const r = detectDrift(code, secret);
|
||||
if (r.delta !== null && Math.abs(r.delta) > 1) {
|
||||
show("Your device clock is " + r.suggestedClockOffsetMs + " ms off.");
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { detectDrift } from "@suaveplan/otp";
|
||||
|
||||
const r = detectDrift(code, secret, {
|
||||
maxSteps: 120, // sweep ±60 minutes at period=30
|
||||
clock: () => Date.now(),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `detectDrift(code, secret, opts?): DriftResult` — both `delta` and
|
||||
`suggestedClockOffsetMs` are `null` when no candidate matched.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The negative-step guard is critical near time origin (t < `period * maxSteps`)
|
||||
because BigInt arithmetic does not raise on underflow; the guard lets the
|
||||
sweep skip unreachable negative steps without affecting wall-clock timing
|
||||
of the loop. The inferred `suggestedClockOffsetMs` is `delta * period * 1000`
|
||||
and is signed so the UI can render direction.
|
||||
|
||||
This module is intentionally separate from `verify` because the two have
|
||||
different threat models: `verify` is on the hot path of every login and
|
||||
must be tightly bounded, while `detectDrift` runs only when `verify`
|
||||
already failed, sweeps a wider range, and exists to render a helpful
|
||||
error message rather than to authenticate. Callers should never accept a
|
||||
code on the basis of `detectDrift` alone — its purpose is diagnostic, not
|
||||
gating.
|
||||
61
docs/security-stack/security/otp/src/errors/codes.md
Normal file
61
docs/security-stack/security/otp/src/errors/codes.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# `errors/codes`
|
||||
|
||||
## Purpose
|
||||
|
||||
Stable, registered error codes for `@suaveplan/otp` plus the typed `OtpError`
|
||||
base class. Every failure that crosses the public surface — invalid options,
|
||||
empty secret, out-of-range window, malformed Key URI, base32 alphabet
|
||||
violation, OCRA suite-string parse failure, recovery-code shape mismatch —
|
||||
funnels through one of the twelve codes here so callers can branch on
|
||||
`.code` rather than parse messages.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen registry of twelve codes via `defineCodes("OTP", [...])`.
|
||||
- Literal-typed `OtpErrorCode` union derived from the registry.
|
||||
- `OtpError extends SuaveplanError<OtpErrorCode>` with `cause` chain support.
|
||||
- `otpError(code, message, opts?)` helper that prefixes the message with the
|
||||
stable code so message-matching consumers (`expect(...).toThrow(/CODE/)`,
|
||||
log scrapers) get a stable substring without inspecting `.code`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OTP_CODES, otpError } from "@suaveplan/otp/errors";
|
||||
|
||||
throw otpError(OTP_CODES.INVALID_DIGITS, "digits must be 6, 7, or 8");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OtpError, OTP_CODES } from "@suaveplan/otp/errors";
|
||||
|
||||
try {
|
||||
// … OTP operation …
|
||||
} catch (e) {
|
||||
if (e instanceof OtpError && e.code === OTP_CODES.WINDOW_OUT_OF_RANGE) {
|
||||
// route the user to a "shorten verify window" UX.
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `OTP_CODES` — the frozen `Record<key, "OTP.<key>">` registry.
|
||||
- `OtpErrorCode` — literal-union type of every registered value.
|
||||
- `OtpError(code, message, options?)` — concrete error class. `options.cause`
|
||||
is preserved on the cause chain when an `Error` instance is supplied.
|
||||
- `otpError(code, message, options?)` — helper returning an `OtpError` whose
|
||||
message has been prefixed with the stable code; non-`Error` causes are
|
||||
silently dropped to avoid leaking arbitrary objects through the chain.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `otpError` helper deliberately drops non-`Error` causes rather than
|
||||
forwarding them, because the cause chain is consumed by `@suaveplan/error`'s
|
||||
serialiser which assumes `Error`-shaped values. Stripping unknown causes at
|
||||
the boundary keeps wire-format invariants intact. The twelve-code registry is
|
||||
the public contract — adding a new code requires an OpenSpec change because
|
||||
removing or renaming one is a breaking change for downstream callers.
|
||||
56
docs/security-stack/security/otp/src/errors/zod-map.md
Normal file
56
docs/security-stack/security/otp/src/errors/zod-map.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# `errors/zod-map`
|
||||
|
||||
## Purpose
|
||||
|
||||
Translate Zod boundary failures into the most-specific `OtpErrorCode`. When a
|
||||
caller passes options that fail strict-mode parsing the first failed-field's
|
||||
path is mapped to a dedicated code (`INVALID_DIGITS`, `WINDOW_OUT_OF_RANGE`,
|
||||
`INVALID_PERIOD`) instead of a generic `INVALID_OPTIONS`. This keeps error
|
||||
codes meaningful at the boundary without duplicating range checks inside
|
||||
each entry-point body.
|
||||
|
||||
## Features
|
||||
|
||||
- `mapZodToOtp(err, fallback?)` — pure mapping over `ZodError.issues[0].path`.
|
||||
- `rethrowAsOtp(err, message, fallback?)` — convenience that throws the
|
||||
mapped error with an attached cause.
|
||||
- Stable mapping table covering every code that has a Zod-level rule:
|
||||
`digits → INVALID_DIGITS`, `window → WINDOW_OUT_OF_RANGE`,
|
||||
`period | t0 → INVALID_PERIOD`. Any other path falls back.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { TotpVerifyOptionsSchema } from "@suaveplan/otp";
|
||||
import { rethrowAsOtp } from "@suaveplan/otp/errors";
|
||||
|
||||
try {
|
||||
TotpVerifyOptionsSchema.parse(opts);
|
||||
} catch (e) {
|
||||
rethrowAsOtp(e, "invalid verify options");
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { mapZodToOtp, OTP_CODES } from "@suaveplan/otp/errors";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
const code = mapZodToOtp(zodErr as ZodError, OTP_CODES.URI_MALFORMED);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `mapZodToOtp(err, fallback?)` — returns the mapped `OtpErrorCode`. If the
|
||||
issues list is empty the `fallback` (default `INVALID_OPTIONS`) is used.
|
||||
- `rethrowAsOtp(err, message, fallback?)` — never returns; throws an
|
||||
`OtpError`. Plain `Error` causes are preserved on the chain; non-`Error`
|
||||
values are dropped to keep the chain serialisable.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The mapper inspects only `issues[0]` for simplicity. Zod typically reports
|
||||
multiple issues but the first one is the most actionable; the rest are
|
||||
chained as the underlying cause and remain available to the caller. Adding a
|
||||
new mapped path is a one-line change here plus a code in the registry.
|
||||
58
docs/security-stack/security/otp/src/hotp/encode-counter.md
Normal file
58
docs/security-stack/security/otp/src/hotp/encode-counter.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# `hotp/encode-counter`
|
||||
|
||||
## Purpose
|
||||
|
||||
Encode an HOTP counter as the 8-byte big-endian block required by RFC 4226
|
||||
§5.1. `bigint` is the canonical input type because `Number` loses precision
|
||||
past `2^53` and the RFC permits counters up to `2^64 − 1`. Shared by HOTP
|
||||
itself, by OCRA's optional `C` directive, and by OCRA's `T` timestamp block
|
||||
(which is also written as an 8-byte big-endian integer per RFC 6287 §5.1).
|
||||
|
||||
## Features
|
||||
|
||||
- BigInt input — no precision loss across the full 0..2^64-1 range.
|
||||
- Range guard: negative counters and counters past `2^64-1` raise
|
||||
`OtpError(OTP.INVALID_OPTIONS)`.
|
||||
- Pure: returns a freshly-allocated `Uint8Array(8)` per call.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encodeCounter } from "@suaveplan/otp";
|
||||
|
||||
const block = encodeCounter(1n);
|
||||
// block === Uint8Array([0, 0, 0, 0, 0, 0, 0, 1])
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encodeCounter } from "@suaveplan/otp";
|
||||
|
||||
// Counter past Number.MAX_SAFE_INTEGER round-trips losslessly.
|
||||
const block = encodeCounter((1n << 53n) + 1n);
|
||||
// `block` is exactly 8 bytes; the high bytes carry the counter's high bits.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `encodeCounter(counter: bigint): Uint8Array` — returns an 8-byte
|
||||
big-endian block. Negative input raises `OtpError(OTP.INVALID_OPTIONS)`;
|
||||
values >= 2^64 raise the same code.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function loops from byte index 7 back to 0 shifting `counter` right by 8
|
||||
bits each step. The mask `0xffn` keeps the operation inside the `bigint`
|
||||
domain so no implicit `Number` conversion can lose precision. The function
|
||||
is the single source of truth for big-endian counter framing — both HOTP and
|
||||
OCRA route through it so any future endianness change propagates atomically.
|
||||
|
||||
The upper-bound check `counter > MAX_U64` catches the niche case where a
|
||||
caller passes a programmatically-generated counter past the RFC 4226
|
||||
limit. Without the check the encoder would silently truncate the high
|
||||
bits and produce two distinct counters that hash to the same byte block,
|
||||
which would silently break the per-counter uniqueness invariant HOTP
|
||||
relies on. The lower-bound check on negative counters covers the
|
||||
symmetric case for callers who derive counters via `BigInt()` arithmetic
|
||||
that may go negative under bug conditions.
|
||||
56
docs/security-stack/security/otp/src/hotp/hmac.md
Normal file
56
docs/security-stack/security/otp/src/hotp/hmac.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# `hotp/hmac`
|
||||
|
||||
## Purpose
|
||||
|
||||
Algorithm-routing HMAC façade for the OTP family. SHA-256 and SHA-512 route
|
||||
through `@suaveplan/crypto-hash` (the canonical platform HMAC); SHA-1 routes
|
||||
through `@noble/hashes/sha1` directly because crypto-hash deliberately does
|
||||
not expose SHA-1 — it is mandated only by legacy OTP RFCs and must not be
|
||||
presented as a general-purpose option elsewhere in the platform.
|
||||
|
||||
## Features
|
||||
|
||||
- Three algorithms: SHA-1, SHA-256, SHA-512.
|
||||
- SHA-1 isolated to the OTP family — no leakage into the wider crypto API.
|
||||
- Returns a freshly-allocated `Uint8Array` of the algorithm's digest size.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { hmacFor } from "@suaveplan/otp";
|
||||
|
||||
const tag = hmacFor("SHA1", key, message);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hmacFor } from "@suaveplan/otp";
|
||||
|
||||
// Build the OCRA HMAC input directly — useful for KAT-driven test fixtures.
|
||||
const tag = hmacFor("SHA256", key, suiteAsciiPlusZeroPlusDataInput);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `hmacFor(algorithm: HotpAlgorithm, key: Uint8Array, data: Uint8Array): Uint8Array`
|
||||
— returns the HMAC tag.
|
||||
- `HotpAlgorithm = "SHA1" | "SHA256" | "SHA512"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Routing inside this module rather than at the call site keeps the SHA-1
|
||||
exception local to OTP. The crypto-hash boundary uses dashed names
|
||||
(`"SHA-256"`); this module accepts the OTP-RFC-style undashed names
|
||||
(`"SHA256"`) and translates internally. Adding a new algorithm here is the
|
||||
only change required — `hotp.ts`, `totp.ts`, and `ocra.ts` all consume this
|
||||
single dispatch point.
|
||||
|
||||
The SHA-1 routing decision is intentional: `@suaveplan/crypto-hash` is the
|
||||
canonical platform façade for hashing across the project, and exposing
|
||||
SHA-1 there would invite consumers to use it for non-OTP purposes. By
|
||||
isolating the SHA-1 dependency to the OTP module, the rest of the platform
|
||||
keeps a no-SHA-1 posture without breaking RFC 4226 / 6238 / 6287
|
||||
compliance. The noble HMAC primitive used here is the same implementation
|
||||
crypto-hash routes through under the hood for SHA-256 / SHA-512, so the
|
||||
two paths behave identically apart from the chosen hash.
|
||||
79
docs/security-stack/security/otp/src/hotp/hotp.md
Normal file
79
docs/security-stack/security/otp/src/hotp/hotp.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# `hotp/hotp`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs --> - hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
- [hotp.schema](./hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
- hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
|
||||
- [hotp.schema](./hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
|
||||
- hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
|
||||
- [hotp.schema](./hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
|
||||
- hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
|
||||
- [hotp.schema](./hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
- [hotp.schema](./hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
|
||||
## Purpose
|
||||
|
||||
RFC 4226 HOTP — HMAC-based one-time password. Computes
|
||||
`value = DT(HMAC-H(K, C)) mod 10^digits` and returns a `digits`-length
|
||||
zero-padded decimal string. Counter is `bigint` (RFC 4226 §5.1 mandates an
|
||||
8-byte big-endian counter; `Number` loses precision past `2^53`).
|
||||
|
||||
## Features
|
||||
|
||||
- RFC 4226 dynamic truncation per §5.3 step 2.
|
||||
- SHA-1 default; SHA-256 / SHA-512 widening per RFC 6238 §1.2.
|
||||
- 6 / 7 / 8-digit output.
|
||||
- Zod-validated options at the boundary; `INVALID_DIGITS` mapped via the
|
||||
zod-map for fields that fail enum/literal-union checks.
|
||||
- Empty-secret rejection with `OTP.INVALID_SECRET`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { hotp } from "@suaveplan/otp";
|
||||
|
||||
const secret = new TextEncoder().encode("12345678901234567890");
|
||||
const code = hotp(secret, 0n);
|
||||
// code === "755224" (RFC 4226 Appendix D vector)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hotp } from "@suaveplan/otp";
|
||||
|
||||
const code = hotp(secret, (1n << 53n) + 1n, {
|
||||
algorithm: "SHA512",
|
||||
digits: 8,
|
||||
});
|
||||
// 8-digit SHA-512 HOTP at a counter past Number.MAX_SAFE_INTEGER
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `hotp(secret: Uint8Array, counter: bigint, opts?: HotpOptions): string`
|
||||
— returns the `digits`-length decimal code.
|
||||
- `HotpOptions` — `{ algorithm?: "SHA1" | "SHA256" | "SHA512"; digits?: 6 | 7 | 8 }`.
|
||||
- Throws `OtpError(OTP.INVALID_OPTIONS | OTP.INVALID_DIGITS)` on Zod
|
||||
failures and `OtpError(OTP.INVALID_SECRET)` on empty `secret`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Counter encoding lives in `encode-counter.ts` and is shared with OCRA's
|
||||
`C` directive. Dynamic truncation extracts a 31-bit value from the HMAC by
|
||||
masking the high bit to avoid sign-extension surprises across HMAC
|
||||
implementations. Modular reduction uses a precomputed `POW10` table indexed
|
||||
by `digits` rather than `Math.pow` so the modulus is an exact integer.
|
||||
|
||||
The Zod schema rejects unknown keys at the strict-mode boundary so callers
|
||||
who construct options from JSON or external configuration cannot smuggle
|
||||
typo'd fields past the API. Field-level Zod failures flow through
|
||||
`mapZodToOtp` so a bad `digits` value surfaces as `OTP.INVALID_DIGITS`
|
||||
rather than the generic `OTP.INVALID_OPTIONS`. The empty-secret guard is
|
||||
runtime-only because Zod's bytes type does not constrain length.
|
||||
61
docs/security-stack/security/otp/src/hotp/hotp.schema.md
Normal file
61
docs/security-stack/security/otp/src/hotp/hotp.schema.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# `hotp/hotp.schema`
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown
|
||||
keys (so silent typos in caller configuration surface immediately) and
|
||||
narrows `algorithm` and `digits` to their literal-union values. Mismatches
|
||||
flow through `mapZodToOtp` so the boundary error carries a specific code
|
||||
(`INVALID_DIGITS` for the `digits` field, `INVALID_OPTIONS` otherwise).
|
||||
|
||||
## Features
|
||||
|
||||
- `algorithm` enum: `"SHA1" | "SHA256" | "SHA512"`.
|
||||
- `digits` literal union: `6 | 7 | 8`.
|
||||
- Strict-mode object — unknown keys are rejected.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { HotpOptionsSchema } from "@suaveplan/otp";
|
||||
|
||||
HotpOptionsSchema.parse({ algorithm: "SHA1", digits: 6 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { HotpOptionsSchema } from "@suaveplan/otp";
|
||||
|
||||
// Programmatic option builder — feed parsed result directly into hotp().
|
||||
const opts = HotpOptionsSchema.parse(JSON.parse(userJson));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `HotpOptionsSchema` — Zod object schema with `.strict()`.
|
||||
- `HotpOptionsInput` — type alias for the parsed shape.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The strict-mode check is the second-line defence behind TypeScript's compile
|
||||
checks: callers using `as never` or constructing options from JSON can still
|
||||
slip values past the type system. Pinning the schema here keeps the runtime
|
||||
contract aligned with the type-level contract.
|
||||
|
||||
The schema is intentionally narrow — it covers only the surface that
|
||||
HOTP itself needs. Callers building their own option bags around HOTP
|
||||
(e.g. a vault item type that wraps HOTP with extra metadata) should
|
||||
extend this schema using Zod's `.extend()` or compose it via a discriminated
|
||||
union, rather than relaxing the strict-mode setting here. Keeping the base
|
||||
schema tight prevents accidental option-shape drift across the OTP family
|
||||
and means every caller benefits from the same validation guarantees.
|
||||
|
||||
The `algorithm` enum covers exactly the three hashes RFC 4226 / 6238 §1.2
|
||||
allow for OTP. SHA-3 family hashes are not part of any OTP RFC and would
|
||||
be rejected at this boundary even though `@noble/hashes` provides them.
|
||||
|
||||
The `digits` literal union mirrors RFC 4226 §5.3 (`6`) plus the RFC 6238
|
||||
§1.2 widening (`7`, `8`). RFC 6287 OCRA additionally permits digits=4,
|
||||
which is handled in the OCRA suite parser rather than here because the
|
||||
HOTP primitive does not need to expose it directly.
|
||||
@@ -0,0 +1,70 @@
|
||||
# `observability/metrics`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
OpenTelemetry instrument factory + recorders for the OTP family. Emits two
|
||||
duration histograms (`generate` / `verify`), one outcome counter, and one
|
||||
error counter. All instruments are lazily resolved via `metrics.getMeter`
|
||||
so a host that swaps the global `MeterProvider` after import (e.g. test
|
||||
fixtures) still observes emissions against the intended provider.
|
||||
|
||||
## Features
|
||||
|
||||
- Histograms: `otp.generate_duration_ms{type,algorithm}`,
|
||||
`otp.verify_duration_ms{type,algorithm}`.
|
||||
- Counter: `otp.verify_outcome_total{outcome,delta_bucket}` — `outcome` ∈
|
||||
`valid | invalid`, `delta_bucket` ∈ `0 | +-1 | +-2..N | n/a` (low-cardinality).
|
||||
- Counter: `otp.errors_total{code}` — bucketed by stable error code.
|
||||
- `resetOtpMetricsForTesting()` clears the instrument cache.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { recordVerify } from "@suaveplan/otp/observability";
|
||||
|
||||
const start = performance.now();
|
||||
const r = verify(code, secret);
|
||||
recordVerify("totp", "SHA1", performance.now() - start);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
recordGenerate,
|
||||
recordOutcome,
|
||||
resetOtpMetricsForTesting,
|
||||
} from "@suaveplan/otp/observability";
|
||||
|
||||
beforeAll(() => {
|
||||
metrics.setGlobalMeterProvider(testProvider);
|
||||
resetOtpMetricsForTesting();
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `recordGenerate(kind, algorithm, ms)` — records duration histogram.
|
||||
- `recordVerify(kind, algorithm, ms)` — same for verify.
|
||||
- `recordOutcome(outcome, delta)` — increments outcome counter; delta is
|
||||
bucketed via `deltaBucket`.
|
||||
- `recordOtpError(code)` — increments error counter.
|
||||
- `deltaBucket(delta)` — pure mapping helper.
|
||||
- `resetOtpMetricsForTesting()` — clears the instrument cache.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `delta_bucket` enum has cardinality 4 to keep dashboards cheap;
|
||||
emitting raw `delta` values would explode metric storage. The cache is
|
||||
module-level and lazy so the first call after an `import` resolves the
|
||||
`Meter` against whatever `MeterProvider` is current at that moment.
|
||||
|
||||
The four instruments are intentionally decoupled from the OTP code paths
|
||||
so that suppressing observability (e.g. in a security-restricted
|
||||
environment) is a one-line change at the call site rather than a deep
|
||||
refactor. The recorder helpers do nothing measurable when the global
|
||||
`MeterProvider` is the OpenTelemetry no-op default, which keeps the cost
|
||||
near zero in environments that do not opt in to metrics.
|
||||
@@ -0,0 +1,56 @@
|
||||
# `ocra/encode-challenge`
|
||||
|
||||
## Purpose
|
||||
|
||||
Encode the OCRA challenge into the 128-byte block required by RFC 6287 §5.1.
|
||||
Three challenge formats are supported: `QN` (decimal → hex bytes),
|
||||
`QA` (printable ASCII), `QH` (hex). The block is always 128 bytes, with the
|
||||
challenge left-justified and the remainder zero-padded.
|
||||
|
||||
## Features
|
||||
|
||||
- `QN`: decimal → BigInt → hex (right-padded to even length) → byte stream.
|
||||
- `QA`: ASCII bytes copied directly.
|
||||
- `QH`: hex pairs decoded directly.
|
||||
- Length validated against the suite's declared maximum.
|
||||
- Boundary errors raise `OtpError(OTP.OCRA_PARAMS_INVALID)`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encodeChallenge } from "@suaveplan/otp/ocra";
|
||||
|
||||
const block = encodeChallenge({ format: "N", length: 8 }, "00000000");
|
||||
// Always 128 bytes; first byte is 0x00.
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encodeChallenge } from "@suaveplan/otp/ocra";
|
||||
|
||||
// Hex challenge — useful for KAT-driven test fixtures that mirror RFC 6287
|
||||
// Appendix C tables.
|
||||
const block = encodeChallenge({ format: "H", length: 8 }, "deadbeef");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `encodeChallenge(descriptor, value): Uint8Array` — returns the 128-byte
|
||||
block. Raises `OtpError(OTP.OCRA_PARAMS_INVALID)` for length mismatch,
|
||||
non-decimal input under `QN`, or odd-length / non-hex input under `QH`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`QN` decodes via BigInt and re-emits hex with right-padding so an odd-length
|
||||
hex representation (e.g. `"1"` → `"10"`) keeps the high nibble in the high
|
||||
position of the first byte. This matches the RFC 6287 reference
|
||||
implementation — getting the padding direction wrong shifts every byte by
|
||||
4 bits and silently breaks every Appendix C vector.
|
||||
|
||||
The 128-byte fixed width is mandated by the RFC and is independent of the
|
||||
declared `Q` length — the declared length only constrains the maximum
|
||||
input the caller may pass. The remaining bytes are always zero-padded so
|
||||
two different valid challenges of different lengths produce different
|
||||
HMAC inputs (no length-extension ambiguity). Callers MUST NOT trim the
|
||||
returned block before HMAC; doing so would silently break the protocol.
|
||||
71
docs/security-stack/security/otp/src/ocra/mutual-auth.md
Normal file
71
docs/security-stack/security/otp/src/ocra/mutual-auth.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# `ocra/mutual-auth`
|
||||
|
||||
## Purpose
|
||||
|
||||
RFC 6287 §7.1 mutual authentication helpers. Both sides compute OCRA over
|
||||
the concatenation of the client and server challenges, with the order
|
||||
flipped between the two directions (`Qc || Qs` for the server's proof,
|
||||
`Qs || Qc` for the client's). The module exposes pure functions plus
|
||||
stateful `OcraMutualAuthClient` / `OcraMutualAuthServer` wrappers so
|
||||
consumers don't reinvent the concatenation order.
|
||||
|
||||
## Features
|
||||
|
||||
- `serverProof` / `verifyServerProof` — the server's response and the
|
||||
client-side check.
|
||||
- `clientProof` / `verifyClientProof` — the client's response and the
|
||||
server-side check.
|
||||
- `OcraMutualAuthClient` / `OcraMutualAuthServer` — stateful wrappers that
|
||||
hold the suites + key + own challenge and expose narrow methods.
|
||||
- Optional `extra` parameter for suites that include `C` (counter), `T`
|
||||
(timestamp), `Snnn` (session info), or `PSHA{...}` (password hash).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OcraMutualAuthClient, OcraMutualAuthServer } from "@suaveplan/otp/ocra";
|
||||
|
||||
const server = new OcraMutualAuthServer(suites, key, "Qs");
|
||||
const client = new OcraMutualAuthClient(suites, key, "Qc");
|
||||
|
||||
const Rs = server.respond("Qc");
|
||||
const Rc = client.respond("Qs", Rs);
|
||||
const ok = server.verify("Qc", Rc);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { clientProof, serverProof } from "@suaveplan/otp/ocra";
|
||||
|
||||
// Counter-bound mutual auth for a session that needs replay protection.
|
||||
const Rs = serverProof({
|
||||
suites,
|
||||
key,
|
||||
clientChallenge: "Qc",
|
||||
serverChallenge: "Qs",
|
||||
extra: { counter: 7n },
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `serverProof(input): string`
|
||||
- `verifyServerProof(input): boolean`
|
||||
- `clientProof(input): string` — verifies server proof first; throws
|
||||
`OtpError(OTP.VERIFY_FAILED)` on mismatch.
|
||||
- `verifyClientProof(input): boolean`
|
||||
- `OcraMutualAuthClient` / `OcraMutualAuthServer` classes.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The challenge concatenation order is the most error-prone part of RFC 6287
|
||||
§7.1 — `Rs` MUST be over `Qc || Qs` and `Rc` MUST be over `Qs || Qc`.
|
||||
Encoding the order in named functions and class methods keeps callers from
|
||||
having to reread the RFC every time they wire a new flow.
|
||||
|
||||
The class wrappers add a small amount of state (own challenge, own suite,
|
||||
own key) so consumers can keep the per-session context outside the call
|
||||
graph. The `extra` parameter lets the same flow work with counter-bound,
|
||||
timestamp-bound, password-hash-bound, or session-info-bound suites — the
|
||||
mutual-auth shape is the same, only the underlying OCRA inputs differ.
|
||||
81
docs/security-stack/security/otp/src/ocra/ocra.md
Normal file
81
docs/security-stack/security/otp/src/ocra/ocra.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# `ocra/ocra`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs --> - ocra.schema — Zod boundary schema for `OcraParams`……
|
||||
- [ocra.schema](./ocra.schema.md) — Zod boundary schema for `OcraParams` — the runtime parameter bag passed alongside a suite string and a key. Optional fi…
|
||||
- [suite](./suite.md) — Parse an OCRA suite string into a typed `OcraSuite` AST per RFC 6287 §6.1. Exposed separately so callers can validate s…
|
||||
- [verify-ocra](./verify-ocra.md) — Constant-time verify of an OCRA response. Recomputes the response under the supplied suite, key, and params, then compa…
|
||||
- ocra.schema — Zod boundary schema for `OcraParams`……
|
||||
|
||||
- [ocra.schema](./ocra.schema.md) — Zod boundary schema for `OcraParams` — the runtime parameter bag passed alongside a suite string and a key. Optional fi…
|
||||
- [suite](./suite.md) — Parse an OCRA suite string into a typed `OcraSuite` AST per RFC 6287 §6.1. Exposed separately so callers can validate s…
|
||||
- [verify-ocra](./verify-ocra.md) — Constant-time verify of an OCRA response. Recomputes the response under the supplied suite, key, and params, then compa…
|
||||
|
||||
- ocra.schema — Zod boundary schema for `OcraParams`……
|
||||
|
||||
- [ocra.schema](./ocra.schema.md) — Zod boundary schema for `OcraParams` — the runtime parameter bag passed alongside a suite string and a key. Optional fi…
|
||||
- [suite](./suite.md) — Parse an OCRA suite string into a typed `OcraSuite` AST per RFC 6287 §6.1. Exposed separately so callers can validate s…
|
||||
- [verify-ocra](./verify-ocra.md) — Constant-time verify of an OCRA response. Recomputes the response under the supplied suite, key, and params, then compa…
|
||||
|
||||
- ocra.schema — Zod boundary schema for `OcraParams`…
|
||||
|
||||
- [ocra.schema](./ocra.schema.md) — Zod boundary schema for `OcraParams` — the runtime parameter bag passed alongside a suite string and a key. Optional fi…
|
||||
- [suite](./suite.md) — Parse an OCRA suite string into a typed `OcraSuite` AST per RFC 6287 §6.1. Exposed separately so callers can validate s…
|
||||
- [verify-ocra](./verify-ocra.md) — Constant-time verify of an OCRA response. Recomputes the response under the supplied suite, key, and params, then compa…
|
||||
- [ocra.schema](./ocra.schema.md) — Zod boundary schema for `OcraParams` — the runtime parameter bag passed alongside a suite string and a key. Optional fi…
|
||||
- [suite](./suite.md) — Parse an OCRA suite string into a typed `OcraSuite` AST per RFC 6287 §6.1. Exposed separately so callers can validate s…
|
||||
- [verify-ocra](./verify-ocra.md) — Constant-time verify of an OCRA response. Recomputes the response under the supplied suite, key, and params, then compa…
|
||||
|
||||
## Purpose
|
||||
|
||||
RFC 6287 OCRA — OATH Challenge-Response Algorithm. Supports the full
|
||||
`OCRA-1:HOTP-{H}-{N}:[C-]Q{A|N|H}NN[-PSHA{1,256,512}][-Snnn][-T{n}{S|M|H}]`
|
||||
suite-string grammar. Validated against RFC 6287 Appendix C.1 reference
|
||||
vectors so call-site implementations have a known-good baseline.
|
||||
|
||||
## Features
|
||||
|
||||
- All hash algorithms: SHA-1 / SHA-256 / SHA-512.
|
||||
- Digits 4 / 6 / 7 / 8 (RFC 6287 §6.2 widens HOTP's 6/7/8 to also include 4).
|
||||
- Optional counter (`C` directive), password hash (`PSHA{1,256,512}`),
|
||||
session info (`Snnn`), and timestamp (`T{n}{S|M|H}`) blocks.
|
||||
- Three challenge formats: `QN` (decimal), `QA` (printable ASCII), `QH` (hex).
|
||||
- Constant-time verify via `verifyOcra`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ocra } from "@suaveplan/otp/ocra";
|
||||
|
||||
const r = ocra("OCRA-1:HOTP-SHA1-6:QN08", key, { challenge: "12345678" });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ocra } from "@suaveplan/otp/ocra";
|
||||
|
||||
// Counter + password-hashed plus timestamp-bound suite.
|
||||
const r = ocra("OCRA-1:HOTP-SHA256-8:C-QN08-PSHA256-T1M", key, {
|
||||
counter: 17n,
|
||||
challenge: "12345678",
|
||||
passwordHash: sha256(passwordBytes),
|
||||
timestampMs: Date.now(),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ocra(suite, key, params): string` — primary computation API.
|
||||
- `buildOcraMessage(parsed, params): Uint8Array` — exposed for benchmarks
|
||||
and KAT harnesses that want to inspect the pre-HMAC byte stream.
|
||||
- `OcraDigits` — alias for the digit literal-union shared with HOTP.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The HMAC input layout follows RFC 6287 §5.1 exactly: suite ASCII, NUL
|
||||
separator, then the directive blocks in fixed order (counter, challenge,
|
||||
password hash, session info, timestamp). Each block is omitted if the suite
|
||||
does not declare it; callers supplying an unexpected block are silently
|
||||
ignored — the suite is the source of truth.
|
||||
57
docs/security-stack/security/otp/src/ocra/ocra.schema.md
Normal file
57
docs/security-stack/security/otp/src/ocra/ocra.schema.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# `ocra/ocra.schema`
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod boundary schema for `OcraParams` — the runtime parameter bag passed
|
||||
alongside a suite string and a key. Optional fields (`counter`, `passwordHash`,
|
||||
`sessionInfo`, `timestampMs`) are validated when present so callers cannot
|
||||
silently pass through malformed values; the actual presence/absence rule is
|
||||
enforced by `buildOcraMessage` against the parsed suite directives.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict-mode object — unknown keys rejected.
|
||||
- `counter` typed as `bigint` non-negative.
|
||||
- `passwordHash` and `sessionInfo` typed as `Uint8Array`.
|
||||
- `timestampMs` typed as non-negative integer.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OcraParamsSchema } from "@suaveplan/otp/ocra";
|
||||
|
||||
OcraParamsSchema.parse({ challenge: "12345678", counter: 1n });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OcraParamsSchema } from "@suaveplan/otp/ocra";
|
||||
|
||||
// Validate before passing through to ocra() — useful at API boundaries.
|
||||
const safe = OcraParamsSchema.safeParse(req.body);
|
||||
if (!safe.success) return res.status(400).json(safe.error);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `OcraParamsSchema` — strict Zod object covering all five `OcraParams`
|
||||
fields.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema is intentionally permissive about WHICH optional fields are
|
||||
present — that's the suite parser's responsibility. Validating the shape
|
||||
of values that ARE present catches cases like `counter: -1n` or
|
||||
`timestampMs: -5` that would otherwise produce nonsensical OCRA outputs
|
||||
without raising.
|
||||
|
||||
The schema lives in its own module so consumers that only need validation
|
||||
(e.g. an API gateway that records OCRA-bound transactions) can import it
|
||||
without pulling in the OCRA codec. The `Uint8Array` instance check uses
|
||||
Zod's `z.instanceof` so tagged byte arrays round-trip cleanly. Callers
|
||||
passing `ArrayBuffer` instead must convert at the boundary; the schema
|
||||
deliberately does not accept arbitrary buffer shapes to avoid silent
|
||||
slice / endianness surprises. The `counter` and `timestampMs` lower-bounds
|
||||
are non-negative because OCRA inherits the unsigned counter rules from
|
||||
RFC 4226 §5.1.
|
||||
55
docs/security-stack/security/otp/src/ocra/suite.md
Normal file
55
docs/security-stack/security/otp/src/ocra/suite.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# `ocra/suite`
|
||||
|
||||
## Purpose
|
||||
|
||||
Parse an OCRA suite string into a typed `OcraSuite` AST per RFC 6287 §6.1.
|
||||
Exposed separately so callers can validate suite strings before using them
|
||||
(e.g. on enrollment) without paying the cost of an HMAC computation.
|
||||
|
||||
## Features
|
||||
|
||||
- Three colon-separated segments: `OCRA-1`, `CryptoFunction`, `DataInput`.
|
||||
- CryptoFunction: `HOTP-{SHA1|SHA256|SHA512}-{4|6|7|8}`.
|
||||
- DataInput directives: `C`, `Q{A|N|H}NN`, `PSHA{1,256,512}`, `Snnn`,
|
||||
`T{n}{S|M|H}`.
|
||||
- Strict grammar — every malformed segment raises
|
||||
`OtpError(OTP.OCRA_SUITE_INVALID)` with a descriptive message.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseOcraSuite } from "@suaveplan/otp/ocra";
|
||||
|
||||
const s = parseOcraSuite("OCRA-1:HOTP-SHA1-6:QN08");
|
||||
// s.cryptoFunction.hash === "SHA1"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseOcraSuite } from "@suaveplan/otp/ocra";
|
||||
|
||||
const s = parseOcraSuite("OCRA-1:HOTP-SHA256-8:C-QN10-PSHA256-S128-T30S");
|
||||
// every directive surfaces as a typed property on s.dataInput.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `parseOcraSuite(suite: string): OcraSuite` — returns the parsed AST.
|
||||
Throws `OtpError(OTP.OCRA_SUITE_INVALID)` on any grammar error.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `Q{A|N|H}NN` directive constrains length to `4..64` per RFC 6287 §6.1.
|
||||
The `Snnn` directive constrains length to `1..512`. Both bounds are
|
||||
enforced here at parse time so downstream code paths can trust the
|
||||
constraints. The suite parser is the single source of truth for grammar —
|
||||
adding a new directive requires updating only this module.
|
||||
|
||||
Because suite strings are configuration data — they end up in databases,
|
||||
provisioning URIs, and KAT vector files — parsing must be tolerant of
|
||||
spaces between directives is NOT allowed (the RFC grammar uses `-` as
|
||||
the directive separator and a literal space would silently produce a
|
||||
different suite). The parser treats any unrecognised directive as an
|
||||
error rather than silently ignoring it, which is the right default when
|
||||
the suite is treated as a security parameter.
|
||||
60
docs/security-stack/security/otp/src/ocra/verify-ocra.md
Normal file
60
docs/security-stack/security/otp/src/ocra/verify-ocra.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# `ocra/verify-ocra`
|
||||
|
||||
## Purpose
|
||||
|
||||
Constant-time verify of an OCRA response. Recomputes the response under the
|
||||
supplied suite, key, and params, then compares to the candidate using the
|
||||
same constant-time string comparator HOTP / TOTP verify uses. Returns a
|
||||
plain boolean — failure modes (bad params, malformed suite) propagate as
|
||||
the underlying `OtpError`.
|
||||
|
||||
## Features
|
||||
|
||||
- Constant-time compare via UTF-8 byte equality.
|
||||
- Routes through `ocra` so any suite-string failure flows naturally as
|
||||
`OCRA_SUITE_INVALID`.
|
||||
- Single-step verification — the wider mutual-auth flow lives in the
|
||||
`mutual-auth` module which composes this primitive.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyOcra } from "@suaveplan/otp/ocra";
|
||||
|
||||
const ok = verifyOcra(response, suite, key, { challenge });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { verifyOcra } from "@suaveplan/otp/ocra";
|
||||
|
||||
// Server-side challenge / response verification for an inbound request.
|
||||
function handle(req) {
|
||||
const params = { challenge: req.body.challenge, counter: BigInt(req.body.counter) };
|
||||
if (!verifyOcra(req.body.response, suite, key, params)) {
|
||||
return reject();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyOcra(response, suite, key, params): boolean` — true on match.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function intentionally returns `boolean` rather than the more elaborate
|
||||
discriminated union used by TOTP verify because OCRA does not have a drift
|
||||
window — there is exactly one expected value per (suite, key, params)
|
||||
triple. Callers needing telemetry around the outcome can wrap the return
|
||||
value with their own `recordOutcome` call.
|
||||
|
||||
The constant-time string compare is the same one used by TOTP `verify` —
|
||||
it operates on UTF-8 byte arrays, applies an explicit length check, and
|
||||
then runs `constantTimeEqual` from `@suaveplan/crypto-utils`. The compare
|
||||
runs to completion regardless of where in the candidate string the first
|
||||
mismatch occurs, so attackers cannot distinguish "first character wrong"
|
||||
from "fifth character wrong" via response timing. This is the standard
|
||||
defence for any string-equality check that gates an authentication
|
||||
decision.
|
||||
64
docs/security-stack/security/otp/src/recovery/generate.md
Normal file
64
docs/security-stack/security/otp/src/recovery/generate.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# `recovery/generate`
|
||||
|
||||
## Purpose
|
||||
|
||||
Generate Crockford-base32 recovery codes — the printable single-use fallback
|
||||
the user prints / writes down when first enrolling MFA. Default form is the
|
||||
conventional `XXXX-XXXX-XXXX-XXXX` quad-grouped 16-character shape. RNG is
|
||||
injectable so deterministic test fixtures can produce stable byte strings;
|
||||
production callers leave it default and pull from the platform CSPRNG.
|
||||
|
||||
## Features
|
||||
|
||||
- Crockford alphabet (excludes `I`, `L`, `O`, `U` to avoid hand-copy
|
||||
collisions with `1` / `0`).
|
||||
- Configurable length / group size / separator.
|
||||
- Injectable `Rng` (`@suaveplan/types`) — `nextBytes` preferred, falls back
|
||||
to `nextUint32` assembly.
|
||||
- Default RNG draws from `randomBytes` in `@suaveplan/crypto-utils`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generateRecoveryCodes } from "@suaveplan/otp";
|
||||
|
||||
const codes = generateRecoveryCodes(10);
|
||||
// ten codes shaped like "XXXX-XXXX-XXXX-XXXX".
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { generateRecoveryCodes } from "@suaveplan/otp";
|
||||
|
||||
// Custom form for a UI that accepts longer codes.
|
||||
const codes = generateRecoveryCodes(5, {
|
||||
length: 20,
|
||||
groupSize: 5,
|
||||
separator: " ",
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `generateRecoveryCodes(count, opts?): string[]` — primary API.
|
||||
- `bytesFromRng(rng, n): Uint8Array` — exported helper used by the
|
||||
recovery-hash module to draw salt bytes from the same RNG.
|
||||
- `RecoveryCodeOptions` — `{ length?: number; groupSize?: number;
|
||||
separator?: string; rng?: Rng }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The 32-character alphabet is power-of-two sized so taking `byte & 0x1f`
|
||||
introduces no modulo bias. When the supplied `Rng` only exposes
|
||||
`nextUint32`, the helper assembles bytes from 32-bit words and uses guarded
|
||||
indexing so a request for `n < 4` writes only the bytes the caller asked
|
||||
for. The default RNG is the platform CSPRNG via `@suaveplan/crypto-utils` —
|
||||
bring your own only for deterministic tests.
|
||||
|
||||
Crockford-base32's character exclusions (`I`, `L`, `O`, `U`) are the
|
||||
practical reason this module does not reuse the standard RFC 4648 alphabet
|
||||
exposed by the `base32` module: hand-copied recovery codes get transcribed
|
||||
under stress, and visually-similar characters cause silent enrollment
|
||||
errors. The two alphabets are distinct on purpose and must not be confused
|
||||
when implementing import / export between password managers.
|
||||
60
docs/security-stack/security/otp/src/recovery/hash.md
Normal file
60
docs/security-stack/security/otp/src/recovery/hash.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# `recovery/hash`
|
||||
|
||||
## Purpose
|
||||
|
||||
Hash recovery codes with Argon2id for at-rest storage. Recovery codes are
|
||||
long-lived shared secrets — they must NOT be stored in cleartext. Argon2id
|
||||
is the OWASP-recommended password-hashing primitive; this module emits a
|
||||
self-describing `recovery$argon2id$v1$<m,t,p,len,saltLen>$<salt>$<derived>`
|
||||
string suitable for a single column in any datastore.
|
||||
|
||||
## Features
|
||||
|
||||
- OWASP 2023 default parameters (`memoryKB ≥ 64 MiB`, `iterations ≥ 3`,
|
||||
`parallelism = 1`, output length 32).
|
||||
- Self-describing PHC-like format with base64url salt + derived bytes.
|
||||
- Round-trip: `decodeRecoveryHash(encodeRecoveryHash(...))` is the identity.
|
||||
- Tolerant input normalisation — separators stripped, upper-cased before
|
||||
hashing so `xx-xx` matches stored `XXXX`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { hashRecoveryCode } from "@suaveplan/otp";
|
||||
|
||||
const stored = await hashRecoveryCode("ABCD-1234");
|
||||
db.update({ recoveryHash: stored });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hashRecoveryCode } from "@suaveplan/otp";
|
||||
|
||||
// Tighter parameters for a high-throughput admin tool seeding many users.
|
||||
const stored = await hashRecoveryCode(code, {
|
||||
params: { memoryKB: 65536, iterations: 4, parallelism: 1, saltBytes: 32, length: 32 },
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `hashRecoveryCode(code, opts?): Promise<string>` — primary API.
|
||||
- `decodeRecoveryHash(s)` — inverse of `encodeRecoveryHash`.
|
||||
- `normaliseRecoveryCode(input)` — strips separators / upper-cases.
|
||||
- `RECOVERY_DEFAULT_ARGON2ID_PARAMS` — exported defaults.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The PHC-like format is deliberately bespoke rather than the official PHC
|
||||
spec because the official format does not include `saltBytes` separately
|
||||
from the encoded salt — re-deriving with a different salt length would
|
||||
silently produce garbage. Encoding the param block explicitly makes the
|
||||
hash self-describing and avoids silent migration hazards.
|
||||
|
||||
OWASP's 2023 Argon2id recommendations are enforced as a floor, not a
|
||||
ceiling: callers can pass tighter parameters when their threat model
|
||||
demands it. The default `memoryKB = 65536` (64 MiB) is the OWASP minimum
|
||||
for Argon2id at `iterations = 3` and `parallelism = 1`. Lower memory or
|
||||
fewer iterations are accepted by the Argon2id primitive but should only
|
||||
be used in test fixtures where speed matters more than security.
|
||||
63
docs/security-stack/security/otp/src/recovery/verify.md
Normal file
63
docs/security-stack/security/otp/src/recovery/verify.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# `recovery/verify`
|
||||
|
||||
## Purpose
|
||||
|
||||
Verify a user-supplied recovery code against a list of stored hashes.
|
||||
Constant-time over the full set — every entry is evaluated even after a
|
||||
match is found so loop-position timing does not leak which code matched.
|
||||
On success the returned `codeIndex` lets the caller mark that hash as used
|
||||
(recovery codes are single-use).
|
||||
|
||||
## Features
|
||||
|
||||
- Constant-time across the full hash list.
|
||||
- Argon2id re-derivation using the per-hash stored parameters.
|
||||
- Returns `{ valid, codeIndex }` discriminated union.
|
||||
- Input normalisation — separators stripped, upper-cased — so `aaaa-bbbb`
|
||||
matches the original `AAAABBBB` form regardless of how the user types it.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyRecoveryCode } from "@suaveplan/otp";
|
||||
|
||||
const r = await verifyRecoveryCode("aaaa-bbbb", user.recoveryHashes);
|
||||
if (r.valid) markUsed(user.recoveryHashes[r.codeIndex]);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { verifyRecoveryCode } from "@suaveplan/otp";
|
||||
|
||||
// Drop the matched hash from the list atomically; subsequent attempts will
|
||||
// miss even if the same code is replayed.
|
||||
const r = await verifyRecoveryCode(input, hashes);
|
||||
if (r.valid) {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.removeHash(user.id, hashes[r.codeIndex]);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyRecoveryCode(input, hashedCodes): Promise<RecoveryVerifyResult>` —
|
||||
returns `{ valid: true; codeIndex }` on match, `{ valid: false; codeIndex: null }`
|
||||
otherwise.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Argon2id is intentionally slow — verifying a list of N hashes runs N derivations.
|
||||
The constant-time loop completes regardless of where the match is so timing
|
||||
does not reveal list position. Production callers typically cap the list at
|
||||
10 codes; below that the wall-clock cost is bounded and acceptable for an
|
||||
interactive recovery flow.
|
||||
|
||||
Each list entry carries its own Argon2id parameters in the encoded form so
|
||||
parameter rotation works without re-hashing the entire user base: as the
|
||||
threat model evolves, new codes can be hashed with stronger parameters
|
||||
while older codes verify against their original parameters. The hash
|
||||
format is self-describing precisely so this rotation is not a breaking
|
||||
change. Callers should still re-hash on next-use to migrate stragglers
|
||||
forward.
|
||||
93
docs/security-stack/security/otp/src/totp/totp.md
Normal file
93
docs/security-stack/security/otp/src/totp/totp.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# `totp/totp`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs --> - totp.schema — Zod boundary schemas for `TotpOptions…
|
||||
- [totp.schema](./totp.schema.md) — Zod boundary schemas for `TotpOptions` and `TotpVerifyOptions`. The TOTP schema covers algorithm / digits / period / t0…
|
||||
- [verify-many](./verify-many.md) — Backup-aware TOTP verify. Used during rotation windows where an old and a new TOTP secret coexist — the user's authenti…
|
||||
- [verify](./verify.md) — RFC 6238 §5.2 verify with drift window. Checks the supplied code against the current step and `±window` neighbouring st…
|
||||
- totp.schema — Zod boundary schemas for `TotpOptions…
|
||||
|
||||
- [totp.schema](./totp.schema.md) — Zod boundary schemas for `TotpOptions` and `TotpVerifyOptions`. The TOTP schema covers algorithm / digits / period / t0…
|
||||
- [verify-many](./verify-many.md) — Backup-aware TOTP verify. Used during rotation windows where an old and a new TOTP secret coexist — the user's authenti…
|
||||
- [verify](./verify.md) — RFC 6238 §5.2 verify with drift window. Checks the supplied code against the current step and `±window` neighbouring st…
|
||||
|
||||
- totp.schema — Zod boundary schemas for `TotpOptions…
|
||||
|
||||
- [totp.schema](./totp.schema.md) — Zod boundary schemas for `TotpOptions` and `TotpVerifyOptions`. The TOTP schema covers algorithm / digits / period / t0…
|
||||
- [verify-many](./verify-many.md) — Backup-aware TOTP verify. Used during rotation windows where an old and a new TOTP secret coexist — the user's authenti…
|
||||
- [verify](./verify.md) — RFC 6238 §5.2 verify with drift window. Checks the supplied code against the current step and `±window` neighbouring st…
|
||||
|
||||
- totp.schema — Zod boundary schemas for `TotpOptions…
|
||||
|
||||
- [totp.schema](./totp.schema.md) — Zod boundary schemas for `TotpOptions` and `TotpVerifyOptions`. The TOTP schema covers algorithm / digits / period / t0…
|
||||
- [verify-many](./verify-many.md) — Backup-aware TOTP verify. Used during rotation windows where an old and a new TOTP secret coexist — the user's authenti…
|
||||
- [verify](./verify.md) — RFC 6238 §5.2 verify with drift window. Checks the supplied code against the current step and `±window` neighbouring st…
|
||||
- [totp.schema](./totp.schema.md) — Zod boundary schemas for `TotpOptions` and `TotpVerifyOptions`. The TOTP schema covers algorithm / digits / period / t0…
|
||||
- [verify-many](./verify-many.md) — Backup-aware TOTP verify. Used during rotation windows where an old and a new TOTP secret coexist — the user's authenti…
|
||||
- [verify](./verify.md) — RFC 6238 §5.2 verify with drift window. Checks the supplied code against the current step and `±window` neighbouring st…
|
||||
|
||||
## Purpose
|
||||
|
||||
RFC 6238 TOTP — time-based one-time password. Computes
|
||||
`T = floor((now − T0) / period)` per §4 and delegates to HOTP. Time is read
|
||||
via the injected `Clock` exclusively — the package never reads `Date.now()`
|
||||
at module scope, so deterministic tests and replay-style test fixtures can
|
||||
swap in a frozen clock without monkey-patching globals.
|
||||
|
||||
## Features
|
||||
|
||||
- Defaults: SHA-1, digits=6, period=30, T0=0.
|
||||
- Algorithm widening per RFC 6238 §1.2 (SHA-256 / SHA-512).
|
||||
- Injectable `Clock` (`@suaveplan/types`).
|
||||
- Strict-mode Zod boundary; mismatched fields are mapped to specific codes.
|
||||
- Period validated `> 0` (Zod `.positive()`); failure surfaces as
|
||||
`OTP.INVALID_PERIOD`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { totp } from "@suaveplan/otp";
|
||||
|
||||
const code = totp(secret); // current step from system clock, 6 digits, SHA-1.
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { totp } from "@suaveplan/otp";
|
||||
|
||||
const fixed: () => number = () => 1_111_111_109_000;
|
||||
const code = totp(secret, {
|
||||
algorithm: "SHA256",
|
||||
digits: 8,
|
||||
period: 30,
|
||||
clock: fixed,
|
||||
});
|
||||
// Reproduces RFC 6238 Appendix B vector at T=1111111109.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `totp(secret: Uint8Array, opts?: TotpOptions): string` — primary API.
|
||||
- `computeTimeStep(clock, period, t0): bigint` — exposed so verify and the
|
||||
drift detector share the same time-step formula.
|
||||
- `resolveTotpOptions(opts?): TotpDefaults` — fills defaults; useful when
|
||||
forwarding options through wrapper packages.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`resolveTotpOptions` extracts only the known `TotpOptions` subset before
|
||||
calling Zod so callers can forward extended option bags (e.g. verify with
|
||||
`window`, drift with `maxSteps`) without strict-mode rejection. The
|
||||
top-level `totp()` parses with the strict schema so unknown keys surface as
|
||||
`INVALID_OPTIONS` at the public boundary.
|
||||
|
||||
Time is converted to seconds before integer division (`floor(now/1000)`)
|
||||
so a clock that returns sub-millisecond resolution still produces a stable
|
||||
step within each `period`-second window. The BigInt step value is
|
||||
threaded through to HOTP without truncation so step values past
|
||||
`Number.MAX_SAFE_INTEGER` (which would happen far in the future, around
|
||||
year 285,427,621 at `period=30`) round-trip cleanly. Callers who need
|
||||
fixed-clock determinism in tests should pass a clock that returns whole
|
||||
milliseconds.
|
||||
60
docs/security-stack/security/otp/src/totp/totp.schema.md
Normal file
60
docs/security-stack/security/otp/src/totp/totp.schema.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# `totp/totp.schema`
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod boundary schemas for `TotpOptions` and `TotpVerifyOptions`. The TOTP
|
||||
schema covers algorithm / digits / period / t0 / clock; the verify schema
|
||||
extends it with `window` capped at `[0, 10]`. Both are strict-mode so unknown
|
||||
keys surface immediately, and field-level rules feed into `mapZodToOtp` so
|
||||
boundary failures carry meaningful codes (`INVALID_PERIOD`,
|
||||
`WINDOW_OUT_OF_RANGE`, etc.).
|
||||
|
||||
## Features
|
||||
|
||||
- `TotpOptionsSchema` — strict object with optional fields.
|
||||
- `TotpVerifyOptionsSchema` — extends the above with `window`.
|
||||
- `Clock` validated as a structural function via `z.custom`.
|
||||
- Period / T0 enforced as non-negative integers.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { TotpOptionsSchema } from "@suaveplan/otp";
|
||||
|
||||
TotpOptionsSchema.parse({ algorithm: "SHA1", period: 30 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { TotpVerifyOptionsSchema } from "@suaveplan/otp";
|
||||
|
||||
const opts = TotpVerifyOptionsSchema.parse({
|
||||
algorithm: "SHA256",
|
||||
digits: 8,
|
||||
period: 30,
|
||||
window: 2,
|
||||
clock: () => Date.now(),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `TotpOptionsSchema` — strict Zod object.
|
||||
- `TotpVerifyOptionsSchema` — extends `TotpOptionsSchema` with `window`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`Clock` is intentionally validated with `z.custom` rather than the
|
||||
`z.function()` builder because Zod's function validators changed shape
|
||||
between major versions; a structural custom predicate stays portable.
|
||||
Period uses `.positive()` which rejects `0` and negatives — both flow to
|
||||
`OTP.INVALID_PERIOD` via the zod-map.
|
||||
|
||||
The `window` cap of 10 is enforced here at the schema rather than at the
|
||||
verify call site because Zod validation produces a typed error message
|
||||
the host application can render directly. Lifting the bound check to the
|
||||
schema also means the verify call site can trust the type: a `parsed.window`
|
||||
value is guaranteed to be in the legal range, so no runtime guard is
|
||||
duplicated downstream. This is the boundary-vs-deep-defence trade-off
|
||||
discussed in the AGENTS.md zod-mapping section.
|
||||
62
docs/security-stack/security/otp/src/totp/verify-many.md
Normal file
62
docs/security-stack/security/otp/src/totp/verify-many.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# `totp/verify-many`
|
||||
|
||||
## Purpose
|
||||
|
||||
Backup-aware TOTP verify. Used during rotation windows where an old and a
|
||||
new TOTP secret coexist — the user's authenticator may still be holding the
|
||||
old secret while the server has moved to the new one. Returns the matching
|
||||
secret index and signed step delta so the caller can mark the rotation
|
||||
complete after the user has successfully authenticated against the new
|
||||
secret.
|
||||
|
||||
## Features
|
||||
|
||||
- Multi-secret loop with constant-time evaluation across the entire list.
|
||||
- Same `±window` semantics as `verify`.
|
||||
- Returns `{ valid, delta, secretIndex }` so the caller can record which
|
||||
secret was used.
|
||||
- Window cap and Zod-boundary semantics shared with `verify`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyMany } from "@suaveplan/otp";
|
||||
|
||||
const r = verifyMany(code, [oldSecret, newSecret]);
|
||||
if (r.valid) markUsed(r.secretIndex);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { verifyMany } from "@suaveplan/otp";
|
||||
|
||||
// During rotation grace period — accept either secret but log when the old
|
||||
// one is used so the rollout dashboard tracks lag.
|
||||
const r = verifyMany(code, [oldSecret, newSecret], {
|
||||
window: 1,
|
||||
clock: () => Date.now(),
|
||||
});
|
||||
if (r.valid && r.secretIndex === 0) metric.inc("totp.rotation.legacy");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyMany(code, secrets, opts?): TotpVerifyManyResult` —
|
||||
`{ valid: true; delta; secretIndex }` on match, all-`null` triple on miss.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Both loops (over secrets and over windowed steps) run to completion even
|
||||
after the first match so the wall-time of `verifyMany` is independent of
|
||||
which secret / which step matched. The match flag uses `matchedDelta === null`
|
||||
to record the first hit without affecting subsequent iterations. This is
|
||||
the same constant-time pattern as `verify`, extended to cover the secret
|
||||
dimension as well.
|
||||
|
||||
When the secret rotation completes (every authenticated user has
|
||||
re-enrolled against the new secret), the caller drops the old secret from
|
||||
the list and `verifyMany` collapses back to single-secret behaviour. There
|
||||
is no separate "single-secret" code path inside `verifyMany` — a
|
||||
single-element list runs the same way as a multi-element one. This keeps
|
||||
the constant-time guarantees uniform across rotation states.
|
||||
61
docs/security-stack/security/otp/src/totp/verify.md
Normal file
61
docs/security-stack/security/otp/src/totp/verify.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# `totp/verify`
|
||||
|
||||
## Purpose
|
||||
|
||||
RFC 6238 §5.2 verify with drift window. Checks the supplied code against the
|
||||
current step and `±window` neighbouring steps; on match returns the signed
|
||||
step offset so callers can detect creeping clock drift inside the window.
|
||||
Constant-time across the entire window — every step is evaluated even after
|
||||
a match is found, so loop-position timing does not leak which step matched.
|
||||
|
||||
## Features
|
||||
|
||||
- Drift window `[0, 10]` hard-capped (Zod boundary).
|
||||
- Returns `{ valid, delta }` discriminated union.
|
||||
- Constant-time string compare via `@suaveplan/crypto-utils`.
|
||||
- Signed `delta` exposes drift direction (`+1` / `-1`) for UI hints.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verify } from "@suaveplan/otp";
|
||||
|
||||
const r = verify("123456", secret);
|
||||
if (r.valid) acceptLogin(r.delta); else rejectLogin();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { verify } from "@suaveplan/otp";
|
||||
|
||||
const r = verify(code, secret, { window: 2, clock: () => Date.now() });
|
||||
if (r.valid && r.delta !== 0) {
|
||||
log.warn("user clock drifted by %d steps", r.delta);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verify(code, secret, opts?): TotpVerifyResult` — returns
|
||||
`{ valid: true; delta }` on match, `{ valid: false; delta: null }` otherwise.
|
||||
- `constantTimeStringEqual(a, b)` — exposed UTF-8-aware constant-time
|
||||
comparator used by recovery and OCRA.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The loop iterates `[-window, +window]` in fixed order — any candidate step
|
||||
that resolves to `< 0n` after the BigInt addition (near time origin) is
|
||||
skipped without an early return. The first matching delta is recorded but
|
||||
the loop continues so the wall-time of `verify` is independent of which
|
||||
step matched. This is a defence against attacks that distinguish "matched
|
||||
on step 0" from "matched on step +1" via timing.
|
||||
|
||||
`window` defaults to `1` per RFC 6238 §5.2's recommendation. Callers
|
||||
willing to trade off wider clock-drift tolerance for tighter security
|
||||
(`window: 0`) can opt in; callers who need to support legacy hardware
|
||||
authenticators with notoriously poor clocks can opt up to the cap of 10.
|
||||
Anything above 10 is rejected at the schema boundary because the wider
|
||||
the window the more codes the attacker has to brute-force per attempt,
|
||||
which weakens the per-second rate-limit budget the host application
|
||||
maintains around this call.
|
||||
66
docs/security-stack/security/otp/src/uri/format-uri.md
Normal file
66
docs/security-stack/security/otp/src/uri/format-uri.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# `uri/format-uri`
|
||||
|
||||
## Purpose
|
||||
|
||||
Build a canonical `otpauth://` URI from an `OtpUri` value object. Round-trips
|
||||
with `parseUri` for every valid input. Percent-encodes the label so colons
|
||||
and spaces survive transport; rejects literal `:` in the label (the colon is
|
||||
reserved for the issuer-prefix convention and an unencoded `:` would produce
|
||||
an ambiguous URI).
|
||||
|
||||
## Features
|
||||
|
||||
- TOTP and HOTP variants.
|
||||
- Label and issuer percent-encoded.
|
||||
- Query parameters emitted in a stable order (`secret, issuer, algorithm,
|
||||
digits, period | counter`).
|
||||
- Strict-mode Zod boundary at the entry point.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { formatUri } from "@suaveplan/otp/uri";
|
||||
|
||||
const url = formatUri({
|
||||
type: "totp",
|
||||
label: "alice@example.com",
|
||||
secret: "JBSWY3DPEHPK3PXP",
|
||||
issuer: "Example",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { formatUri } from "@suaveplan/otp/uri";
|
||||
|
||||
const url = formatUri({
|
||||
type: "hotp",
|
||||
label: "alice",
|
||||
secret: "JBSWY3DPEHPK3PXP",
|
||||
counter: 17n,
|
||||
algorithm: "SHA256",
|
||||
digits: 8,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `formatUri(opts: OtpUri): string` — returns the URI. Raises
|
||||
`OtpError(OTP.URI_MALFORMED)` on Zod failure or on labels containing `:`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The output is a stable canonical form so two calls with identically-typed
|
||||
input produce byte-identical URLs. This matters when the URI is hashed for
|
||||
QR-code de-duplication or audited via diff tooling. Field order is preserved
|
||||
via a `Map` rather than a plain object so insertion order is stable across
|
||||
JS engines.
|
||||
|
||||
Optional fields are omitted when they match the per-spec default — e.g.
|
||||
TOTP URIs without a `period` parameter elide it rather than emitting
|
||||
`period=30`. This keeps the output minimal and matches the wire format
|
||||
emitted by reference Google Authenticator implementations. Round-tripping
|
||||
through `parseUri` recovers the canonical option bag because both ends
|
||||
agree on the defaults; consumers that need an explicit `period=30` should
|
||||
set it on the input.
|
||||
62
docs/security-stack/security/otp/src/uri/parse-uri.md
Normal file
62
docs/security-stack/security/otp/src/uri/parse-uri.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# `uri/parse-uri`
|
||||
|
||||
## Purpose
|
||||
|
||||
Parse the `otpauth://` Key URI Format used by Google Authenticator and every
|
||||
compatible authenticator app. Supports both `totp` and `hotp` variants and
|
||||
returns a discriminated union narrowed by the `type` field. Malformed input
|
||||
raises `OTP.URI_MALFORMED`; an unrecognised type segment raises
|
||||
`OTP.URI_UNSUPPORTED_TYPE`.
|
||||
|
||||
## Features
|
||||
|
||||
- `otpauth://totp/<label>?secret=...&issuer=...&algorithm=...&digits=...&period=...`
|
||||
- `otpauth://hotp/<label>?secret=...&counter=...` (counter is `bigint`).
|
||||
- Algorithm normalisation: `sha1` / `Sha1` / `SHA1` all canonicalise.
|
||||
- Digits gated to `6 / 7 / 8`.
|
||||
- Optional issuer round-tripped via `issuer=` query parameter.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseUri } from "@suaveplan/otp/uri";
|
||||
|
||||
const u = parseUri(
|
||||
"otpauth://totp/alice?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
);
|
||||
if (u.type === "totp") /* … */;
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseUri } from "@suaveplan/otp/uri";
|
||||
|
||||
const u = parseUri(
|
||||
"otpauth://hotp/alice?secret=JBSWY3DPEHPK3PXP&counter=42&algorithm=SHA512&digits=8",
|
||||
);
|
||||
if (u.type === "hotp") {
|
||||
saveCounter(u.counter); // bigint past 2^53 round-trips losslessly.
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `parseUri(uri: string): OtpUri` — returns the discriminated union.
|
||||
- Throws `OtpError(OTP.URI_MALFORMED)` for shape failures and
|
||||
`OtpError(OTP.URI_UNSUPPORTED_TYPE)` for unrecognised type segments.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The label is decoded via `decodeURIComponent` so percent-encoded `:` and
|
||||
spaces survive. Empty labels are rejected. The query parser tolerates bare
|
||||
flags (`?secret=...&flag&period=30`) and ignores empty keys. Counter values
|
||||
are required to match `^\d+$` (decimal-only) before BigInt conversion so
|
||||
hex / scientific notation are rejected at the boundary.
|
||||
|
||||
The `secret` query parameter is returned as a plain string so callers
|
||||
choose how to decode it. Most callers will route the secret through
|
||||
`base32Decode` from `@suaveplan/otp/base32`. The parser deliberately does
|
||||
NOT auto-decode because some authenticators emit hex-encoded secrets in
|
||||
QR codes that mis-set the `secret=` parameter — leaving the decode at the
|
||||
caller's boundary keeps that recovery path open.
|
||||
60
docs/security-stack/security/otp/src/uri/uri.schema.md
Normal file
60
docs/security-stack/security/otp/src/uri/uri.schema.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# `uri/uri.schema`
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod boundary schemas for the `OtpUri` discriminated union. `OtpTotpUriSchema`
|
||||
and `OtpHotpUriSchema` cover the per-variant shapes; `OtpUriSchema` is the
|
||||
discriminated union over the `type` field. Used by `formatUri` and by
|
||||
downstream consumers that want to validate user-supplied URI configuration
|
||||
before passing it through.
|
||||
|
||||
## Features
|
||||
|
||||
- Discriminated union by `type: "totp" | "hotp"`.
|
||||
- Algorithm and digits literal-typed to the same enums HOTP uses.
|
||||
- HOTP counter as `bigint` (Zod's native bigint validator).
|
||||
- Strict-mode object — unknown keys rejected.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OtpUriSchema } from "@suaveplan/otp/uri";
|
||||
|
||||
const u = OtpUriSchema.parse({
|
||||
type: "totp",
|
||||
label: "alice",
|
||||
secret: "JBSWY3DPEHPK3PXP",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { OtpHotpUriSchema } from "@suaveplan/otp/uri";
|
||||
|
||||
const safe = OtpHotpUriSchema.safeParse(payload);
|
||||
if (!safe.success) renderErrors(safe.error.issues);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `OtpTotpUriSchema` — strict object covering the TOTP shape.
|
||||
- `OtpHotpUriSchema` — strict object covering the HOTP shape.
|
||||
- `OtpUriSchema` — discriminated-union over `type`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schemas live in their own module so callers that only need validation
|
||||
(e.g. a config loader) can import them without pulling in the codec. The
|
||||
HOTP `counter` field intentionally uses `z.bigint().nonnegative()` — Zod's
|
||||
JSON parser does not handle BigInt natively, so callers parsing JSON must
|
||||
pre-convert decimal strings to BigInt before feeding the schema.
|
||||
|
||||
Discriminated unions in Zod produce richer error messages than plain
|
||||
`z.union` because Zod can pick the right variant based on the
|
||||
discriminator and report only that variant's failures. The discriminator
|
||||
here is `type` and matches the runtime field carried on the parsed URI,
|
||||
so callers who narrow on `u.type` after parsing get exhaustive type
|
||||
coverage with no extra runtime checks. Adding a new URI variant requires
|
||||
extending both `OtpUriSchema` and the `OtpUri` discriminated-union type
|
||||
in `uri.types.ts` in lockstep.
|
||||
Reference in New Issue
Block a user