@nacre.work/core 0.17.4 → 0.18.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,111 @@
1
+ -- 0029 — a second factor, and the codes that get you back in without one.
2
+ --
3
+ -- Everything that authenticates a person here has been one secret: a password,
4
+ -- or an ID token from an issuer the operator trusts. A password that leaks is
5
+ -- an account, and on this product an account is a set of documents somebody
6
+ -- decided who may read.
7
+ --
8
+ -- The specification is docs/authz.md, "Authentication", and the rule it does
9
+ -- **not** change: a second factor decides whether a session starts. It grants
10
+ -- nothing. Every permission is still computed per request from `grants`.
11
+
12
+ -- ─────────── the factors ───────────
13
+ --
14
+ -- One row per enrolled authenticator, so a person can carry two and lose one.
15
+ -- `kind` admits only 'totp' today; WebAuthn widens this CHECK in its own
16
+ -- migration rather than being written here on speculation — a column shaped for
17
+ -- a feature nobody has built is a column the next person has to guess about.
18
+ CREATE TABLE user_second_factors (
19
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
20
+ org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
21
+ user_id uuid NOT NULL,
22
+ kind text NOT NULL CHECK (kind IN ('totp')),
23
+
24
+ -- The shared secret, sealed. See `packages/core/authn/totp.ts`: AES-256-GCM
25
+ -- under a key from `NACRE_2FA_KEY_REF`, with the nonce and the tag inside the
26
+ -- value the way `password_hash` carries its scrypt parameters. A database
27
+ -- dump is a thing that happens; a dump that hands over every second factor in
28
+ -- plaintext is a second factor that was never one.
29
+ secret text NOT NULL,
30
+
31
+ -- What a person calls it, because "which of my two phones is this" is a
32
+ -- question they will have and the database cannot answer.
33
+ label text NOT NULL,
34
+
35
+ -- Enrolment is two steps and this is the second: a secret that has never
36
+ -- produced a correct code is a secret the person has not actually got into
37
+ -- their authenticator, and treating it as live is how somebody locks
38
+ -- themselves out at the moment they turn 2FA on. Nothing counts an
39
+ -- unconfirmed row.
40
+ confirmed_at timestamptz,
41
+
42
+ -- The time step of the last accepted code. TOTP without this accepts the same
43
+ -- six digits for the whole window, so an attacker who sees one over a
44
+ -- shoulder — or in a phishing proxy — can replay it. Monotonic per factor.
45
+ last_step bigint,
46
+
47
+ -- Brute force is bounded here rather than in Redis. The rate limiter fails
48
+ -- **open** by design, on the argument that it is not an authorization control
49
+ -- and a cache restart must not be an outage. This one is an authorization
50
+ -- control: six digits is a million, and a limiter that forgets is a limiter
51
+ -- an attacker waits out.
52
+ failed_attempts integer NOT NULL DEFAULT 0,
53
+ locked_until timestamptz,
54
+
55
+ created_at timestamptz NOT NULL DEFAULT now(),
56
+ last_used_at timestamptz,
57
+
58
+ -- Composite, so a row cannot join a factor in one organization to a user in
59
+ -- another. `group_members` had the plain version of this shape and it would
60
+ -- have handed a foreign user every grant a group held.
61
+ FOREIGN KEY (user_id, org_id) REFERENCES users (id, org_id) ON DELETE CASCADE,
62
+
63
+ -- Two authenticators called the same thing is a person unable to tell which
64
+ -- one they are removing.
65
+ UNIQUE (user_id, kind, label)
66
+ );
67
+
68
+ CREATE INDEX ON user_second_factors (user_id) WHERE confirmed_at IS NOT NULL;
69
+
70
+ -- ─────────── the way back in ───────────
71
+ --
72
+ -- One row per code, spent once. They are the whole of the answer for the
73
+ -- account this installation cannot e-mail: the platform administrator has no
74
+ -- organization to administer, and a deployment with no SMTP configured has no
75
+ -- recovery link for anybody.
76
+ --
77
+ -- Hashed with SHA-256 and deliberately **not** with scrypt. A recovery code is
78
+ -- 128 bits from the CSPRNG, so there is no dictionary to slow down and no
79
+ -- password to protect — the cost parameter would buy nothing and would make
80
+ -- issuing ten of them a second of CPU on a request.
81
+ CREATE TABLE user_recovery_codes (
82
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
83
+ org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
84
+ user_id uuid NOT NULL,
85
+ code_hash text NOT NULL,
86
+ used_at timestamptz,
87
+ created_at timestamptz NOT NULL DEFAULT now(),
88
+
89
+ FOREIGN KEY (user_id, org_id) REFERENCES users (id, org_id) ON DELETE CASCADE,
90
+
91
+ -- Within one installation a code is unique, so spending one is a single
92
+ -- UPDATE with no read in front of it that another request could race.
93
+ UNIQUE (code_hash)
94
+ );
95
+
96
+ CREATE INDEX ON user_recovery_codes (user_id) WHERE used_at IS NULL;
97
+
98
+ -- ─────────── second line of defense ───────────
99
+ ALTER TABLE user_second_factors ENABLE ROW LEVEL SECURITY;
100
+ ALTER TABLE user_second_factors FORCE ROW LEVEL SECURITY;
101
+ ALTER TABLE user_recovery_codes ENABLE ROW LEVEL SECURITY;
102
+ ALTER TABLE user_recovery_codes FORCE ROW LEVEL SECURITY;
103
+
104
+ CREATE POLICY org_isolation ON user_second_factors USING (org_id = current_setting('app.current_org')::uuid);
105
+ CREATE POLICY org_isolation ON user_recovery_codes USING (org_id = current_setting('app.current_org')::uuid);
106
+
107
+ -- The application reads and writes both; nothing else does. No grant to
108
+ -- `nacre_worker`: the worker has no question about who signed in, and a
109
+ -- privilege nothing reads is the shape this repository keeps removing.
110
+ GRANT SELECT, INSERT, UPDATE, DELETE ON user_second_factors TO nacre_app;
111
+ GRANT SELECT, INSERT, UPDATE, DELETE ON user_recovery_codes TO nacre_app;
@@ -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;
@@ -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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"}