@nacre.work/core 0.17.5 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ -- 0030 — a password somebody can recover, without a `psql` session.
2
+ --
3
+ -- `POST /v1/users/{id}/password` has existed since there were users, and it is
4
+ -- an *administrator* setting somebody else's. The person who forgot theirs at
5
+ -- the weekend had no route at all, and on a single-administrator installation
6
+ -- — which the open core mostly is — the administrator who forgets theirs had
7
+ -- no route that did not go through the database.
8
+ --
9
+ -- The specification is docs/api.md, "Recovering a password".
10
+
11
+ -- ─────────── the token ───────────
12
+ --
13
+ -- One row per issued link, spent once.
14
+ --
15
+ -- **No cross-tenant read, and that is why the token carries its organization.**
16
+ -- Resolving a credential outside `withOrg` has exactly two mechanisms here and
17
+ -- both are narrow on purpose — `0008`'s own words are that `users` gets neither
18
+ -- "as a decision rather than an omission". A token shaped `<org_id>.<secret>`
19
+ -- means redemption knows the organization before it reads anything, so this
20
+ -- table is read through `withOrg` like every other, and nothing here opens a
21
+ -- second door into a table that a stranger can reach unauthenticated.
22
+ --
23
+ -- The organization id is not a secret from the person holding the link: it is
24
+ -- in their own `/v1/me`. What is secret is the half beside it.
25
+ CREATE TABLE password_reset_tokens (
26
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
27
+ org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
28
+ user_id uuid NOT NULL,
29
+
30
+ -- SHA-256 of the whole token. Fast on purpose: the secret half is 32 bytes
31
+ -- from the CSPRNG, so there is no dictionary to slow down — the same
32
+ -- reasoning the recovery codes in 0029 carry, and the opposite of a password.
33
+ token_hash text NOT NULL,
34
+
35
+ expires_at timestamptz NOT NULL,
36
+ -- Spent by the UPDATE that finds it, so two requests cannot spend one.
37
+ used_at timestamptz,
38
+ created_at timestamptz NOT NULL DEFAULT now(),
39
+
40
+ FOREIGN KEY (user_id, org_id) REFERENCES users (id, org_id) ON DELETE CASCADE,
41
+
42
+ -- Unique across the installation, so redemption is one statement with no read
43
+ -- in front of it that another request could race.
44
+ UNIQUE (token_hash)
45
+ );
46
+
47
+ -- The sweep reads by expiry; redemption reads by hash, which the constraint
48
+ -- above already indexes.
49
+ CREATE INDEX ON password_reset_tokens (expires_at);
50
+
51
+ ALTER TABLE password_reset_tokens ENABLE ROW LEVEL SECURITY;
52
+ ALTER TABLE password_reset_tokens FORCE ROW LEVEL SECURITY;
53
+ CREATE POLICY org_isolation ON password_reset_tokens
54
+ USING (org_id = current_setting('app.current_org')::uuid);
55
+
56
+ -- The application issues, spends and prunes them. Not granted to
57
+ -- `nacre_worker`: the worker has no question about who is signing in, and a
58
+ -- privilege nothing reads is the shape this repository keeps removing.
59
+ GRANT SELECT, INSERT, UPDATE, DELETE ON password_reset_tokens TO nacre_app;
@@ -0,0 +1,123 @@
1
+ -- 0031 — a second factor you cannot be phished out of.
2
+ --
3
+ -- TOTP bounds a stolen password and does not bound a convincing page: six
4
+ -- digits typed into `nacre-work.com` work exactly as well there as here, and
5
+ -- the person who typed them has no way to know. WebAuthn is the factor whose
6
+ -- signature covers the origin, so an assertion produced for the wrong site does
7
+ -- not verify at the right one — that is the whole reason to carry a second kind
8
+ -- rather than more of the first.
9
+ --
10
+ -- Everything docs/authz.md says about a second factor is unchanged and applies
11
+ -- to this one: it decides whether a session starts, it grants nothing, it is
12
+ -- enrolled per person and never on their behalf, and a service account and a
13
+ -- delegation are refused. Nothing in packages/core/authz reads any of it.
14
+
15
+ -- ─────────── the kind ───────────
16
+ --
17
+ -- 0029 wrote `CHECK (kind IN ('totp'))` and said in its own comment that
18
+ -- WebAuthn would widen it "in its own migration rather than being written here
19
+ -- on speculation". This is that migration.
20
+ ALTER TABLE user_second_factors DROP CONSTRAINT user_second_factors_kind_check;
21
+ ALTER TABLE user_second_factors ADD CONSTRAINT user_second_factors_kind_check
22
+ CHECK (kind IN ('totp', 'webauthn'));
23
+
24
+ -- ─────────── what a WebAuthn factor is made of ───────────
25
+ --
26
+ -- A TOTP factor is a sealed shared secret; a WebAuthn factor is a public key
27
+ -- and has no secret at all. That asymmetry is the feature — a database dump
28
+ -- hands over nothing that can produce an assertion — and it is why `secret`
29
+ -- becomes nullable below rather than these columns being squeezed into it.
30
+ ALTER TABLE user_second_factors
31
+ -- The credential the authenticator minted, base64url as the browser reports
32
+ -- it. Bytes would be tidier in the abstract and worse here: every comparison
33
+ -- this column takes part in arrives as the string a browser sent.
34
+ ADD COLUMN credential_id text,
35
+
36
+ -- The public key, as a JWK. COSE and JWK are the same parameters under
37
+ -- different names, and `node:crypto` imports the second directly — so storing
38
+ -- the converted form means the verification path does no parsing at all on a
39
+ -- value that has already been through it once.
40
+ ADD COLUMN public_key jsonb,
41
+
42
+ -- The COSE algorithm: -7 ES256, -257 RS256, -8 EdDSA. Kept beside the key
43
+ -- rather than derived from it, because "which algorithm did this credential
44
+ -- register with" is a question about the registration and not about the
45
+ -- key's shape — and an RSA key alone does not say whether PKCS#1 or PSS was
46
+ -- agreed.
47
+ ADD COLUMN alg integer,
48
+
49
+ -- The authenticator's own counter. A value that goes backwards is the one
50
+ -- signal WebAuthn offers for a cloned authenticator. Most platform
51
+ -- authenticators never move it, which is allowed and is why this is not
52
+ -- NOT NULL DEFAULT 0 with a monotonic constraint — see the verifier.
53
+ ADD COLUMN sign_count bigint;
54
+
55
+ -- A TOTP factor has a secret and no key; a WebAuthn factor has a key and no
56
+ -- secret. Stated as a constraint rather than left to the application, because
57
+ -- the application is one caller and a row with neither is a factor that can
58
+ -- never answer.
59
+ ALTER TABLE user_second_factors ALTER COLUMN secret DROP NOT NULL;
60
+ ALTER TABLE user_second_factors ADD CONSTRAINT user_second_factors_shape CHECK (
61
+ (kind = 'totp' AND secret IS NOT NULL AND credential_id IS NULL AND public_key IS NULL AND alg IS NULL)
62
+ OR
63
+ (kind = 'webauthn' AND secret IS NULL AND credential_id IS NOT NULL AND public_key IS NOT NULL AND alg IS NOT NULL)
64
+ );
65
+
66
+ -- Within one organization a credential id names one factor. Not installation
67
+ -- wide, and that is the point: an assertion here is the *second* step of a
68
+ -- sign-in, so the organization and the person are already known and the lookup
69
+ -- reads through `withOrg` like everything else. A globally unique index would
70
+ -- be a cross-tenant read waiting for somebody to write the query.
71
+ CREATE UNIQUE INDEX user_second_factors_credential
72
+ ON user_second_factors (org_id, credential_id)
73
+ WHERE credential_id IS NOT NULL;
74
+
75
+ -- ─────────── the challenge ───────────
76
+ --
77
+ -- Every ceremony takes a challenge this server issued, and it has to be
78
+ -- **single-use**: the signature covers it, so an assertion captured on the wire
79
+ -- is replayable for as long as its challenge is. Nothing else in the ceremony
80
+ -- stops that.
81
+ --
82
+ -- A table rather than a signed, stateless value. A stateless challenge would
83
+ -- have to be spent somewhere anyway to be single-use, and a store that only
84
+ -- prevents replay is the same store as one that also carries the value — with
85
+ -- the difference that this one can be read to see what is outstanding.
86
+ CREATE TABLE webauthn_challenges (
87
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
88
+ org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
89
+ user_id uuid NOT NULL,
90
+
91
+ -- 'register' or 'authenticate'. A challenge issued for enrolment must not be
92
+ -- spendable on a sign-in: the first is asked for by somebody already signed
93
+ -- in, and letting the two share a pool would let a session mint the input to
94
+ -- a ceremony it is not in.
95
+ purpose text NOT NULL CHECK (purpose IN ('register', 'authenticate')),
96
+
97
+ -- base64url, exactly as it goes into `clientDataJSON` and comes back out, so
98
+ -- the comparison is a string equality and never an encoding round trip.
99
+ challenge text NOT NULL,
100
+
101
+ expires_at timestamptz NOT NULL,
102
+ used_at timestamptz,
103
+ created_at timestamptz NOT NULL DEFAULT now(),
104
+
105
+ FOREIGN KEY (user_id, org_id) REFERENCES users (id, org_id) ON DELETE CASCADE,
106
+
107
+ -- Spending one is a single UPDATE with no read in front of it that another
108
+ -- request could race, which is the same shape `password_reset_tokens` uses.
109
+ UNIQUE (org_id, challenge)
110
+ );
111
+
112
+ CREATE INDEX ON webauthn_challenges (expires_at) WHERE used_at IS NULL;
113
+
114
+ -- ─────────── second line of defense ───────────
115
+ ALTER TABLE webauthn_challenges ENABLE ROW LEVEL SECURITY;
116
+ ALTER TABLE webauthn_challenges FORCE ROW LEVEL SECURITY;
117
+ CREATE POLICY org_isolation ON webauthn_challenges USING (org_id = current_setting('app.current_org')::uuid);
118
+
119
+ -- The application reads and writes it; nothing else does. No grant to
120
+ -- `nacre_worker`, for the reason 0029 gives: the worker has no question about
121
+ -- who signed in, and a privilege nothing reads is the shape this repository
122
+ -- keeps removing.
123
+ GRANT SELECT, INSERT, UPDATE, DELETE ON webauthn_challenges TO nacre_app;
@@ -1 +1 @@
1
- {"version":3,"file":"passwords.d.ts","sourceRoot":"","sources":["../passwords.ts"],"names":[],"mappings":"AAoGA,iFAAiF;AACjF,qBAAa,OAAQ,SAAQ,KAAK;;CAKjC;AAyCD,iEAAiE;AACjE,eAAO,MAAM,WAAW,QAAO;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAI5E,CAAA;AAEF;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAWpE;AAmCD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAexF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAIpD;AAgBD,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAI5E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,cAAc,qsBAUjB,CAAA;AAEV,qCAAqC;AACrC,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAKpC;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,QACgD,CAAA;AAElF,wBAAgB,gBAAgB,IAAI,MAAM,CAQzC"}
1
+ {"version":3,"file":"passwords.d.ts","sourceRoot":"","sources":["../passwords.ts"],"names":[],"mappings":"AA4GA,iFAAiF;AACjF,qBAAa,OAAQ,SAAQ,KAAK;;CAKjC;AAyCD,iEAAiE;AACjE,eAAO,MAAM,WAAW,QAAO;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAI5E,CAAA;AAEF;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAWpE;AAmCD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAexF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAIpD;AAgBD,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAI5E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,cAAc,qsBAUjB,CAAA;AAEV,qCAAqC;AACrC,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAKpC;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,QACgD,CAAA;AAElF,wBAAgB,gBAAgB,IAAI,MAAM,CAQzC"}
package/dist/passwords.js CHANGED
@@ -76,6 +76,14 @@ const MAX_MEM = 256 * 1024 * 1024;
76
76
  * Refusing is not an oracle. It depends on how loaded the process is and not at
77
77
  * all on whether the account exists, and it is returned identically to the
78
78
  * caller whether the address matched a user or not.
79
+ *
80
+ * The 503 is `errors.ts`'s `tooBusy`, sent from the two error boundaries in
81
+ * `server.ts` — sign-in is reached without a credential and everything else
82
+ * with one, so a single catch cannot cover both. That is worth naming here
83
+ * because this paragraph claimed "the caller answers 503" while exactly one of
84
+ * the four routes that reach this gate did: the other three — creating a user,
85
+ * an administrator resetting a password, and redeeming a recovery link — turned
86
+ * a loaded process into a 500.
79
87
  */
80
88
  const poolSize = Number(process.env.UV_THREADPOOL_SIZE ?? 4);
81
89
  const MAX_CONCURRENT = Math.max(1, Math.floor((Number.isFinite(poolSize) ? poolSize : 4) / 2));
@@ -1 +1 @@
1
- {"version":3,"file":"passwords.js","sourceRoot":"","sources":["../passwords.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"passwords.js","sourceRoot":"","sources":["../passwords.ts"],"names":[],"mappings":"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"}
package/dist/totp.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ /** RFC 6238's default and what every authenticator assumes. */
2
+ export declare const TOTP_PERIOD_SECONDS = 30;
3
+ /** Six, for the same reason: it is what the apps show. */
4
+ export declare const TOTP_DIGITS = 6;
5
+ /**
6
+ * How far either side of now a code is accepted.
7
+ *
8
+ * One step, which is up to sixty seconds of tolerance in the worst alignment.
9
+ * Phones drift and people type slowly; two steps is the number that starts
10
+ * making a shoulder-surfed code useful for longer than it should be.
11
+ */
12
+ export declare const TOTP_SKEW_STEPS = 1;
13
+ export declare function base32Encode(bytes: Uint8Array): string;
14
+ export declare function base32Decode(text: string): Buffer;
15
+ /**
16
+ * 160 bits, which is what RFC 4226 recommends and what the apps expect.
17
+ *
18
+ * Shorter secrets are accepted by every authenticator and are the reason a
19
+ * generator is here rather than left to a call site: 80 bits is legal, common
20
+ * in examples, and half the strength of the thing it is protecting.
21
+ */
22
+ export declare function generateTotpSecret(): string;
23
+ /** The step a moment falls in. Exported because the replay bound is a step. */
24
+ export declare const totpStep: (at?: Date) => number;
25
+ /** RFC 6238 over RFC 4226: HMAC the counter, take the dynamic offset, mod 10^d. */
26
+ export declare function totpCode(secret: string, step: number): string;
27
+ export interface TotpVerification {
28
+ /** The step the code belonged to, to be stored so it cannot be spent twice. */
29
+ readonly step: number;
30
+ }
31
+ /**
32
+ * Check a code, and say which step it was.
33
+ *
34
+ * `after` is the last step this factor already accepted, and a code at or
35
+ * before it is refused however correct it is. Without that, a code is good for
36
+ * the whole window it was shown in — so somebody who reads it over a shoulder,
37
+ * or a phishing page that relays it, gets a second use out of it. The caller
38
+ * stores the returned step.
39
+ */
40
+ export declare function verifyTotp(secret: string, code: string, options?: {
41
+ readonly at?: Date;
42
+ readonly after?: number | null;
43
+ }): TotpVerification | undefined;
44
+ /**
45
+ * The URL an authenticator reads out of a QR code.
46
+ *
47
+ * The label carries the issuer as well as the account, which is what makes two
48
+ * installations distinguishable in a list of thirty entries — an app shows the
49
+ * label, and "dana@example.com" alone is four identical rows.
50
+ */
51
+ export declare function otpauthUrl(options: {
52
+ readonly issuer: string;
53
+ readonly account: string;
54
+ readonly secret: string;
55
+ }): string;
56
+ export declare function sealTotpSecret(secret: string, key: Buffer): string;
57
+ export declare function openTotpSecret(sealed: string, key: Buffer): string;
58
+ /** Ten, which is what a person can print on one line each and lose half of. */
59
+ export declare const RECOVERY_CODE_COUNT = 10;
60
+ /**
61
+ * A recovery code, in the shape people retype correctly.
62
+ *
63
+ * Four groups of five from a 32-character alphabet is 100 bits — far past
64
+ * anything guessable — and the groups exist because a twenty-character run
65
+ * gets transcribed wrongly and blamed on the product.
66
+ */
67
+ export declare function generateRecoveryCode(): string;
68
+ export declare const normalizeRecoveryCode: (code: string) => string;
69
+ /**
70
+ * Hashed with SHA-256 and deliberately not with scrypt.
71
+ *
72
+ * A recovery code is 100 bits from the CSPRNG, so there is no dictionary to
73
+ * slow down and no human-chosen password to protect. A cost parameter would buy
74
+ * nothing against an attacker and would make issuing ten of them a second of
75
+ * CPU on a request — on the same libuv pool the sign-in path already has to be
76
+ * careful about.
77
+ */
78
+ export declare function hashRecoveryCode(code: string): string;
79
+ //# sourceMappingURL=totp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"totp.d.ts","sourceRoot":"","sources":["../totp.ts"],"names":[],"mappings":"AA8BA,+DAA+D;AAC/D,eAAO,MAAM,mBAAmB,KAAK,CAAA;AACrC,0DAA0D;AAC1D,eAAO,MAAM,WAAW,IAAI,CAAA;AAE5B;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,IAAI,CAAA;AAKhC,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CActD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBjD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,+EAA+E;AAC/E,eAAO,MAAM,QAAQ,GAAI,KAAI,IAAiB,KAAG,MACM,CAAA;AAEvD,mFAAmF;AACnF,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAQ7D;AAED,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GACnE,gBAAgB,GAAG,SAAS,CAe9B;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB,GAAG,MAAM,CAUT;AAiBD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAMlE;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQlE;AAID,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAI7C;AAED,eAAO,MAAM,qBAAqB,GAAI,MAAM,MAAM,KAAG,MACT,CAAA;AAE5C;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD"}
package/dist/totp.js ADDED
@@ -0,0 +1,206 @@
1
+ /**
2
+ * A second factor, and the codes that get you back in without one.
3
+ *
4
+ * ## What this decides, and what it does not
5
+ *
6
+ * A second factor decides whether a **session starts**. It grants nothing: the
7
+ * permitted set is still computed per request from `grants`, and a token minted
8
+ * after a correct code reaches exactly what the same token reaches without one.
9
+ * Nothing in `authz/` reads anything here, deliberately — a factor that could
10
+ * widen access would be a second answer to the question this product exists to
11
+ * answer once.
12
+ *
13
+ * ## Why SHA-1
14
+ *
15
+ * RFC 6238 leaves the hash open and every authenticator on a phone implements
16
+ * SHA-1; several implement nothing else. The weakness SHA-1 is retired for is
17
+ * collision resistance, which HMAC does not rest on — and the key here is 160
18
+ * bits from the CSPRNG while the message is a counter and the answer lives
19
+ * thirty seconds. Choosing SHA-256 would buy nothing measurable and would meet
20
+ * a person whose authenticator shows six digits that never work.
21
+ *
22
+ * ## Standard library only
23
+ *
24
+ * The same argument the parser makes: this module is on the authentication
25
+ * path, and a dependency here is a dependency inside every sign-in. The whole
26
+ * of it is HMAC, a counter, a base32 alphabet and AES-GCM, all of which
27
+ * `node:crypto` already has.
28
+ */
29
+ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
30
+ /** RFC 6238's default and what every authenticator assumes. */
31
+ export const TOTP_PERIOD_SECONDS = 30;
32
+ /** Six, for the same reason: it is what the apps show. */
33
+ export const TOTP_DIGITS = 6;
34
+ /**
35
+ * How far either side of now a code is accepted.
36
+ *
37
+ * One step, which is up to sixty seconds of tolerance in the worst alignment.
38
+ * Phones drift and people type slowly; two steps is the number that starts
39
+ * making a shoulder-surfed code useful for longer than it should be.
40
+ */
41
+ export const TOTP_SKEW_STEPS = 1;
42
+ /** RFC 4648 base32, which is the only encoding an authenticator will read. */
43
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
44
+ export function base32Encode(bytes) {
45
+ let bits = 0;
46
+ let value = 0;
47
+ let out = '';
48
+ for (const byte of bytes) {
49
+ value = (value << 8) | byte;
50
+ bits += 8;
51
+ while (bits >= 5) {
52
+ out += ALPHABET[(value >>> (bits - 5)) & 31];
53
+ bits -= 5;
54
+ }
55
+ }
56
+ if (bits > 0)
57
+ out += ALPHABET[(value << (5 - bits)) & 31];
58
+ return out;
59
+ }
60
+ export function base32Decode(text) {
61
+ // Padding and case are both things a person retypes wrongly, and neither
62
+ // carries information. Whitespace goes first: with the order reversed, a
63
+ // value ending in `== ` keeps its padding, because it is no longer at the
64
+ // end — which a test caught on the first run.
65
+ const clean = text.replace(/\s+/gu, '').replace(/=+$/u, '').toUpperCase();
66
+ let bits = 0;
67
+ let value = 0;
68
+ const out = [];
69
+ for (const character of clean) {
70
+ const index = ALPHABET.indexOf(character);
71
+ if (index === -1)
72
+ throw new Error(`not base32: ${character}`);
73
+ value = (value << 5) | index;
74
+ bits += 5;
75
+ if (bits >= 8) {
76
+ out.push((value >>> (bits - 8)) & 0xff);
77
+ bits -= 8;
78
+ }
79
+ }
80
+ return Buffer.from(out);
81
+ }
82
+ /**
83
+ * 160 bits, which is what RFC 4226 recommends and what the apps expect.
84
+ *
85
+ * Shorter secrets are accepted by every authenticator and are the reason a
86
+ * generator is here rather than left to a call site: 80 bits is legal, common
87
+ * in examples, and half the strength of the thing it is protecting.
88
+ */
89
+ export function generateTotpSecret() {
90
+ return base32Encode(randomBytes(20));
91
+ }
92
+ /** The step a moment falls in. Exported because the replay bound is a step. */
93
+ export const totpStep = (at = new Date()) => Math.floor(at.getTime() / 1000 / TOTP_PERIOD_SECONDS);
94
+ /** RFC 6238 over RFC 4226: HMAC the counter, take the dynamic offset, mod 10^d. */
95
+ export function totpCode(secret, step) {
96
+ const counter = Buffer.alloc(8);
97
+ counter.writeBigUInt64BE(BigInt(step));
98
+ const mac = createHmac('sha1', base32Decode(secret)).update(counter).digest();
99
+ const offset = mac[mac.length - 1] & 0x0f;
100
+ const binary = ((mac[offset] & 0x7f) << 24) | (mac[offset + 1] << 16) | (mac[offset + 2] << 8) | mac[offset + 3];
101
+ return String(binary % 10 ** TOTP_DIGITS).padStart(TOTP_DIGITS, '0');
102
+ }
103
+ /**
104
+ * Check a code, and say which step it was.
105
+ *
106
+ * `after` is the last step this factor already accepted, and a code at or
107
+ * before it is refused however correct it is. Without that, a code is good for
108
+ * the whole window it was shown in — so somebody who reads it over a shoulder,
109
+ * or a phishing page that relays it, gets a second use out of it. The caller
110
+ * stores the returned step.
111
+ */
112
+ export function verifyTotp(secret, code, options = {}) {
113
+ const typed = code.replace(/\s+/gu, '');
114
+ if (!new RegExp(`^[0-9]{${String(TOTP_DIGITS)}}$`, 'u').test(typed))
115
+ return undefined;
116
+ const current = totpStep(options.at ?? new Date());
117
+ for (let offset = -TOTP_SKEW_STEPS; offset <= TOTP_SKEW_STEPS; offset += 1) {
118
+ const step = current + offset;
119
+ if (options.after !== undefined && options.after !== null && step <= options.after)
120
+ continue;
121
+ const expected = Buffer.from(totpCode(secret, step));
122
+ const given = Buffer.from(typed);
123
+ // Constant time, because the comparison is over a secret-derived value and
124
+ // an attacker controls one side of it.
125
+ if (expected.length === given.length && timingSafeEqual(expected, given))
126
+ return { step };
127
+ }
128
+ return undefined;
129
+ }
130
+ /**
131
+ * The URL an authenticator reads out of a QR code.
132
+ *
133
+ * The label carries the issuer as well as the account, which is what makes two
134
+ * installations distinguishable in a list of thirty entries — an app shows the
135
+ * label, and "dana@example.com" alone is four identical rows.
136
+ */
137
+ export function otpauthUrl(options) {
138
+ const label = `${encodeURIComponent(options.issuer)}:${encodeURIComponent(options.account)}`;
139
+ const query = new URLSearchParams({
140
+ secret: options.secret,
141
+ issuer: options.issuer,
142
+ algorithm: 'SHA1',
143
+ digits: String(TOTP_DIGITS),
144
+ period: String(TOTP_PERIOD_SECONDS),
145
+ });
146
+ return `otpauth://totp/${label}?${query.toString()}`;
147
+ }
148
+ // ── the secret at rest ───────────────────────────────────────────────────────
149
+ /**
150
+ * Sealed with AES-256-GCM under a key this process is given.
151
+ *
152
+ * A TOTP secret in the clear is a second factor that a database dump defeats,
153
+ * which is the same blast-radius argument that made the token signing key
154
+ * asymmetric: the point of the factor is that stealing one thing is not enough.
155
+ *
156
+ * The nonce and the tag travel inside the stored value the way `password_hash`
157
+ * carries its scrypt parameters — a column that needs a second column to be
158
+ * readable is two things that can come apart.
159
+ */
160
+ const SEALED = 'v1';
161
+ export function sealTotpSecret(secret, key) {
162
+ if (key.length !== 32)
163
+ throw new Error('a 2FA key is 32 bytes');
164
+ const nonce = randomBytes(12);
165
+ const cipher = createCipheriv('aes-256-gcm', key, nonce);
166
+ const body = Buffer.concat([cipher.update(secret, 'utf8'), cipher.final()]);
167
+ return [SEALED, nonce.toString('base64url'), body.toString('base64url'), cipher.getAuthTag().toString('base64url')].join('.');
168
+ }
169
+ export function openTotpSecret(sealed, key) {
170
+ const [version, nonce, body, tag] = sealed.split('.');
171
+ if (version !== SEALED || nonce === undefined || body === undefined || tag === undefined) {
172
+ throw new Error('not a sealed TOTP secret');
173
+ }
174
+ const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(nonce, 'base64url'));
175
+ decipher.setAuthTag(Buffer.from(tag, 'base64url'));
176
+ return Buffer.concat([decipher.update(Buffer.from(body, 'base64url')), decipher.final()]).toString('utf8');
177
+ }
178
+ // ── recovery codes ───────────────────────────────────────────────────────────
179
+ /** Ten, which is what a person can print on one line each and lose half of. */
180
+ export const RECOVERY_CODE_COUNT = 10;
181
+ /**
182
+ * A recovery code, in the shape people retype correctly.
183
+ *
184
+ * Four groups of five from a 32-character alphabet is 100 bits — far past
185
+ * anything guessable — and the groups exist because a twenty-character run
186
+ * gets transcribed wrongly and blamed on the product.
187
+ */
188
+ export function generateRecoveryCode() {
189
+ const bytes = randomBytes(20);
190
+ const text = base32Encode(bytes).slice(0, 20);
191
+ return [text.slice(0, 5), text.slice(5, 10), text.slice(10, 15), text.slice(15, 20)].join('-');
192
+ }
193
+ export const normalizeRecoveryCode = (code) => code.replace(/[\s-]+/gu, '').toUpperCase();
194
+ /**
195
+ * Hashed with SHA-256 and deliberately not with scrypt.
196
+ *
197
+ * A recovery code is 100 bits from the CSPRNG, so there is no dictionary to
198
+ * slow down and no human-chosen password to protect. A cost parameter would buy
199
+ * nothing against an attacker and would make issuing ten of them a second of
200
+ * CPU on a request — on the same libuv pool the sign-in path already has to be
201
+ * careful about.
202
+ */
203
+ export function hashRecoveryCode(code) {
204
+ return createHash('sha256').update(normalizeRecoveryCode(code)).digest('hex');
205
+ }
206
+ //# sourceMappingURL=totp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"totp.js","sourceRoot":"","sources":["../totp.ts"],"names":[],"mappings":"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"}