@noy-db/on-password 0.1.0-pre.5 → 0.1.0-pre.8
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.
- package/dist/index.cjs +44 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -28
- package/dist/index.d.ts +67 -28
- package/dist/index.js +47 -55
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -25,14 +25,13 @@ __export(index_exports, {
|
|
|
25
25
|
PasswordInvalidError: () => PasswordInvalidError,
|
|
26
26
|
PasswordTooWeakError: () => PasswordTooWeakError,
|
|
27
27
|
enrollPasswordAuthenticator: () => enrollPasswordAuthenticator,
|
|
28
|
-
|
|
28
|
+
unwrapDeksWithPassword: () => unwrapDeksWithPassword,
|
|
29
29
|
verifyPasswordSlot: () => verifyPasswordSlot
|
|
30
30
|
});
|
|
31
31
|
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
var import_hub = require("@noy-db/hub");
|
|
32
33
|
var PASSWORD_PBKDF2_ITERATIONS = 6e5;
|
|
33
34
|
var PASSWORD_DEFAULT_MIN_LENGTH = 12;
|
|
34
|
-
var SALT_BYTES = 32;
|
|
35
|
-
var subtle = globalThis.crypto.subtle;
|
|
36
35
|
var PasswordTooWeakError = class extends Error {
|
|
37
36
|
code = "PASSWORD_TOO_WEAK";
|
|
38
37
|
minLength;
|
|
@@ -62,84 +61,73 @@ async function enrollPasswordAuthenticator(keyring, options) {
|
|
|
62
61
|
`Password does not match the configured pattern: ${options.pattern.toString()}.`
|
|
63
62
|
);
|
|
64
63
|
}
|
|
65
|
-
if (
|
|
64
|
+
if (keyring.deks.size === 0) {
|
|
66
65
|
throw new Error(
|
|
67
|
-
"enrollPasswordAuthenticator: the supplied keyring has no
|
|
66
|
+
"enrollPasswordAuthenticator: the supplied keyring has no DEKs in memory. Re-authenticate at tier 1 first."
|
|
68
67
|
);
|
|
69
68
|
}
|
|
70
|
-
const
|
|
71
|
-
const wrappingKey = await derivePasswordWrappingKey(options.password, salt);
|
|
72
|
-
const wrapped = await subtle.wrapKey("raw", keyring.kek, wrappingKey, "AES-KW");
|
|
69
|
+
const blob = await (0, import_hub.mintWrappedDeksBlob)(keyring.deks, options.password);
|
|
73
70
|
return {
|
|
74
71
|
id: options.id ?? "password-daily",
|
|
75
72
|
method: "password",
|
|
76
|
-
|
|
73
|
+
wrapKind: "deks",
|
|
74
|
+
wrapped_deks: blob.wrappedDeks,
|
|
75
|
+
iv: blob.iv,
|
|
77
76
|
meta: {
|
|
78
|
-
salt:
|
|
77
|
+
salt: blob.salt,
|
|
79
78
|
minLength,
|
|
80
79
|
...options.pattern !== void 0 ? { pattern: options.pattern.source } : {}
|
|
81
80
|
},
|
|
82
81
|
enrolled_via_tier: options.enrolledViaTier ?? 1
|
|
83
82
|
};
|
|
84
83
|
}
|
|
85
|
-
async function
|
|
84
|
+
async function unwrapDeksWithPassword(slot, password) {
|
|
85
|
+
if (slot.wrapKind !== "deks") {
|
|
86
|
+
throw new PasswordInvalidError(
|
|
87
|
+
"Password slot is not a wrap-DEKs slot. Pre-pre.8 wrap-KEK password slots are no longer supported \u2014 re-enrol via enrollPasswordAuthenticator."
|
|
88
|
+
);
|
|
89
|
+
}
|
|
86
90
|
const meta = slot.meta;
|
|
87
91
|
if (typeof meta.salt !== "string") {
|
|
88
92
|
throw new PasswordInvalidError(
|
|
89
93
|
"Password slot is missing the per-slot salt \u2014 keyring may be corrupted."
|
|
90
94
|
);
|
|
91
95
|
}
|
|
92
|
-
const salt = base64ToBytes(meta.salt);
|
|
93
|
-
const wrappingKey = await derivePasswordWrappingKey(password, salt);
|
|
94
96
|
try {
|
|
95
|
-
return await
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
wrappingKey,
|
|
99
|
-
"AES-KW",
|
|
100
|
-
{ name: "AES-KW", length: 256 },
|
|
101
|
-
false,
|
|
102
|
-
["wrapKey", "unwrapKey"]
|
|
97
|
+
return await (0, import_hub.unwrapDeksFromBlob)(
|
|
98
|
+
{ salt: meta.salt, iv: slot.iv, wrappedDeks: slot.wrapped_deks },
|
|
99
|
+
password
|
|
103
100
|
);
|
|
104
101
|
} catch {
|
|
105
102
|
throw new PasswordInvalidError();
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
105
|
async function verifyPasswordSlot(slot, password, options) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let s = "";
|
|
135
|
-
for (const b of bytes) s += String.fromCharCode(b);
|
|
136
|
-
return btoa(s);
|
|
137
|
-
}
|
|
138
|
-
function base64ToBytes(b64) {
|
|
139
|
-
const s = atob(b64);
|
|
140
|
-
const out = new Uint8Array(s.length);
|
|
141
|
-
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
|
142
|
-
return out;
|
|
106
|
+
const deks = await unwrapDeksWithPassword(slot, password);
|
|
107
|
+
const env = await options.store.get(options.vault, "_keyring", options.userId);
|
|
108
|
+
if (!env) {
|
|
109
|
+
throw new PasswordInvalidError(
|
|
110
|
+
`verifyPasswordSlot: no keyring found at "${options.vault}/_keyring/${options.userId}". Verify the vault and userId are correct.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const file = JSON.parse(env._data);
|
|
114
|
+
const salt = new Uint8Array((0, import_hub.base64ToBuffer)(file.salt));
|
|
115
|
+
return {
|
|
116
|
+
userId: file.user_id,
|
|
117
|
+
displayName: file.display_name,
|
|
118
|
+
role: file.role,
|
|
119
|
+
permissions: file.permissions,
|
|
120
|
+
authenticators: file.authenticators ?? [],
|
|
121
|
+
salt,
|
|
122
|
+
...file.export_capability !== void 0 && { exportCapability: file.export_capability },
|
|
123
|
+
...file.import_capability !== void 0 && { importCapability: file.import_capability },
|
|
124
|
+
...file.policy !== void 0 && { policy: file.policy },
|
|
125
|
+
deks,
|
|
126
|
+
// Wrap-DEKs unlock cannot recover the KEK. Sensitive ops route
|
|
127
|
+
// through tier-1 via re-entry of the master phrase. Matches the
|
|
128
|
+
// existing tier-3 (`@noy-db/on-pin`) pattern.
|
|
129
|
+
kek: null
|
|
130
|
+
};
|
|
143
131
|
}
|
|
144
132
|
// Annotate the CommonJS export names for ESM import in node:
|
|
145
133
|
0 && (module.exports = {
|
|
@@ -148,7 +136,7 @@ function base64ToBytes(b64) {
|
|
|
148
136
|
PasswordInvalidError,
|
|
149
137
|
PasswordTooWeakError,
|
|
150
138
|
enrollPasswordAuthenticator,
|
|
151
|
-
|
|
139
|
+
unwrapDeksWithPassword,
|
|
152
140
|
verifyPasswordSlot
|
|
153
141
|
});
|
|
154
142
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/on-password** — tier-2 daily-password authenticator slot.\n *\n * The user's tier-1 *phrase* is the rarely-typed master that derives\n * the KEK. This package adds a SEPARATE secret — a daily-typed\n * password — as a tier-2 slot in the multi-slot keyring. The two\n * credentials have:\n *\n * - **Different lifecycles** — the phrase rotates yearly; the password\n * rotates per the developer's policy.\n * - **Different strength rules** — the phrase is validated against the\n * phrase format (issue #7); the password is validated against a\n * length / regex rule the developer chooses.\n * - **Different storage** — the phrase derives the KEK; the password\n * derives a wrapping key that wraps the SAME KEK in its own\n * keyring slot (LUKS pattern).\n *\n * The slot is added to the keyring via `db.enrollAuthenticator`; the\n * KEK is recovered via `db.unlockViaAuthenticator` — both routes hit\n * the policy gate engine (issue #9).\n *\n * @see docs/subsystems/session-tiers.md → Tier 2 — `on-password`\n *\n * @packageDocumentation\n */\nimport type {\n EnrollAuthenticatorOptions,\n KeyringAuthenticator,\n UnlockedKeyring,\n} from '@noy-db/hub'\n\n/** PBKDF2 iteration count — matches the tier-1 phrase derivation. */\nexport const PASSWORD_PBKDF2_ITERATIONS = 600_000\n\n/** Default minimum password length. Override per-app via `enrollPasswordAuthenticator`. */\nexport const PASSWORD_DEFAULT_MIN_LENGTH = 12\n\n/** Per-slot salt size. */\nconst SALT_BYTES = 32\n\nconst subtle = globalThis.crypto.subtle\n\n// ─── Errors ────────────────────────────────────────────────────────────\n\nexport class PasswordTooWeakError extends Error {\n readonly code = 'PASSWORD_TOO_WEAK' as const\n readonly minLength: number\n constructor(minLength: number, message?: string) {\n super(\n message ??\n `Password must be at least ${String(minLength)} characters. ` +\n 'For accounts with a separate tier-1 phrase, prefer a longer password ' +\n 'or pair with TOTP/email-OTP via @noy-db/on-totp or @noy-db/on-email-otp.',\n )\n this.name = 'PasswordTooWeakError'\n this.minLength = minLength\n }\n}\n\nexport class PasswordInvalidError extends Error {\n readonly code = 'PASSWORD_INVALID' as const\n constructor(message = 'Password does not unlock this slot.') {\n super(message)\n this.name = 'PasswordInvalidError'\n }\n}\n\n// ─── Public API ────────────────────────────────────────────────────────\n\n/** Options for {@link enrollPasswordAuthenticator}. */\nexport interface EnrollPasswordOptions {\n /** Slot id. Default: `'password-daily'`. */\n readonly id?: string\n /** Daily password the user will type. Distinct from the tier-1 phrase. */\n readonly password: string\n /** Minimum length. Default 12. */\n readonly minLength?: number\n /** Optional regex the password must satisfy in addition to length. */\n readonly pattern?: RegExp\n /** Tier the active session held when enrolling. Default 1. */\n readonly enrolledViaTier?: 1 | 2\n}\n\n/**\n * Build the keyring slot for a tier-2 password authenticator. Returns\n * an `EnrollAuthenticatorOptions` value the caller hands to\n * `db.enrollAuthenticator(vault, slot)` — separating the cryptographic\n * step (this function) from the persistence step (the hub) keeps the\n * package small and lets the hub's policy gate run between the two.\n *\n * Usage:\n *\n * ```ts\n * import { enrollPasswordAuthenticator } from '@noy-db/on-password'\n *\n * const slot = await enrollPasswordAuthenticator(unlocked, {\n * password: 'daily-password-2026',\n * minLength: 14,\n * })\n * await db.enrollAuthenticator('acme', slot, {\n * factors: [{ kind: 'totp' }],\n * })\n * ```\n */\nexport async function enrollPasswordAuthenticator(\n keyring: UnlockedKeyring,\n options: EnrollPasswordOptions,\n): Promise<EnrollAuthenticatorOptions> {\n const minLength = options.minLength ?? PASSWORD_DEFAULT_MIN_LENGTH\n if (options.password.length < minLength) {\n throw new PasswordTooWeakError(minLength)\n }\n if (options.pattern && !options.pattern.test(options.password)) {\n throw new PasswordTooWeakError(\n minLength,\n `Password does not match the configured pattern: ${options.pattern.toString()}.`,\n )\n }\n\n if (!keyring.kek) {\n throw new Error(\n 'enrollPasswordAuthenticator: the supplied keyring has no KEK in memory. ' +\n 'Tier-3 quick-resume keyrings cannot enrol new tier-2 slots; re-authenticate at tier 1 first.',\n )\n }\n\n const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES))\n const wrappingKey = await derivePasswordWrappingKey(options.password, salt)\n const wrapped = await subtle.wrapKey('raw', keyring.kek, wrappingKey, 'AES-KW')\n\n return {\n id: options.id ?? 'password-daily',\n method: 'password',\n wrapped_kek: bytesToBase64(new Uint8Array(wrapped)),\n meta: {\n salt: bytesToBase64(salt),\n minLength,\n ...(options.pattern !== undefined ? { pattern: options.pattern.source } : {}),\n },\n enrolled_via_tier: options.enrolledViaTier ?? 1,\n }\n}\n\n/**\n * Recover the KEK from a password slot's `wrapped_kek` ciphertext.\n * Returns the unwrapped KEK as a non-extractable `CryptoKey` ready for\n * AES-KW unwrap of DEKs. Used inside the verify callback passed to\n * `db.unlockViaAuthenticator`.\n *\n * @throws {@link PasswordInvalidError} when the password is wrong.\n */\nexport async function unwrapKekWithPassword(\n slot: KeyringAuthenticator,\n password: string,\n): Promise<CryptoKey> {\n const meta = slot.meta as { salt?: unknown }\n if (typeof meta.salt !== 'string') {\n throw new PasswordInvalidError(\n 'Password slot is missing the per-slot salt — keyring may be corrupted.',\n )\n }\n const salt = base64ToBytes(meta.salt)\n const wrappingKey = await derivePasswordWrappingKey(password, salt)\n try {\n return await subtle.unwrapKey(\n 'raw',\n base64ToBytes(slot.wrapped_kek) as BufferSource,\n wrappingKey,\n 'AES-KW',\n { name: 'AES-KW', length: 256 },\n false,\n ['wrapKey', 'unwrapKey'],\n )\n } catch {\n throw new PasswordInvalidError()\n }\n}\n\n/**\n * Hub-friendly verify callback. Pass to `db.unlockViaAuthenticator`:\n *\n * ```ts\n * const unlocked = await db.unlockViaAuthenticator('acme', 'password-daily',\n * (slot) => verifyPasswordSlot(slot, 'daily-password-2026', { adapter: store, vault: 'acme', userId: 'alice' }),\n * )\n * ```\n *\n * The callback re-loads the keyring file via the supplied adapter,\n * unwraps every DEK with the recovered KEK, and returns the\n * `UnlockedKeyring` the hub installs in its keyring cache.\n *\n * @throws {@link PasswordInvalidError} when the password is wrong.\n */\nexport async function verifyPasswordSlot(\n slot: KeyringAuthenticator,\n password: string,\n options: VerifyPasswordSlotOptions,\n): Promise<UnlockedKeyring> {\n const kek = await unwrapKekWithPassword(slot, password)\n return options.materialize(kek)\n}\n\n/** Adapter shape required by {@link verifyPasswordSlot}. */\nexport interface VerifyPasswordSlotOptions {\n /**\n * Caller-supplied \"given the recovered KEK, build the\n * `UnlockedKeyring`\" routine. The hub provides the standard\n * implementation in {@link buildUnlockedKeyringFromKek}; consumers\n * with non-standard storage (e.g. encrypted browser-extension\n * stores) can pass their own.\n */\n readonly materialize: (kek: CryptoKey) => Promise<UnlockedKeyring>\n}\n\n// ─── Helpers ───────────────────────────────────────────────────────────\n\nasync function derivePasswordWrappingKey(\n password: string,\n salt: Uint8Array,\n): Promise<CryptoKey> {\n const ikm = await subtle.importKey(\n 'raw',\n new TextEncoder().encode(password),\n 'PBKDF2',\n false,\n ['deriveKey'],\n )\n return subtle.deriveKey(\n {\n name: 'PBKDF2',\n salt: salt as BufferSource,\n iterations: PASSWORD_PBKDF2_ITERATIONS,\n hash: 'SHA-256',\n },\n ikm,\n { name: 'AES-KW', length: 256 },\n false,\n ['wrapKey', 'unwrapKey'],\n )\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n let s = ''\n for (const b of bytes) s += String.fromCharCode(b)\n return btoa(s)\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n const s = atob(b64)\n const out = new Uint8Array(s.length)\n for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i)\n return out\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/on-password** — tier-2 daily-password authenticator slot.\n *\n * The user's tier-1 *phrase* is the rarely-typed master that derives\n * the KEK. This package adds a SEPARATE secret — a daily-typed\n * password — as a tier-2 slot in the multi-slot keyring. The two\n * credentials have:\n *\n * - **Different lifecycles** — the phrase rotates yearly; the password\n * rotates per the developer's policy.\n * - **Different strength rules** — the phrase is validated against the\n * phrase format (issue #7); the password is validated against a\n * length / regex rule the developer chooses.\n * - **Different storage** — the phrase derives the KEK; the password\n * derives a wrapping key that wraps the SAME DEK SET in its own\n * keyring slot (LUKS-like multi-slot, wrap-DEKs variant).\n *\n * ## Wrap-DEKs format (#26 Path C)\n *\n * Slots produced by this package use the wrap-DEKs variant of\n * `KeyringAuthenticator` — they encrypt the serialized DEK set under\n * a password-derived AES-GCM key, NOT the KEK. This unifies tier-2\n * password slots with the tier-0 (paper recovery, `mintPaperRecoveryEntry`)\n * and tier-3 (`@noy-db/on-pin`'s `wrappedKeyring`) primitives — all\n * three sidestep the non-extractable-KEK constraint by wrapping the\n * DEK set rather than the KEK itself.\n *\n * Trade-off: an `UnlockedKeyring` produced via password-slot unlock\n * has `kek: null`. Sensitive operations (`enrollAuthenticator`,\n * `rotatePassphrase`) require a tier-1 unlock anyway — re-enter the\n * master phrase.\n *\n * @see docs/subsystems/session-tiers.md → Tier 2 — `on-password`\n *\n * @packageDocumentation\n */\nimport {\n base64ToBuffer,\n mintWrappedDeksBlob,\n unwrapDeksFromBlob,\n type EnrollAuthenticatorOptions,\n type KeyringAuthenticator,\n type KeyringFile,\n type NoydbStore,\n type UnlockedKeyring,\n} from '@noy-db/hub'\n\n/**\n * PBKDF2 iteration count — matches the tier-1 phrase derivation.\n * Exported as a documented invariant; the actual derivation lives\n * in hub's canonical wrap-DEKs primitive (#44).\n */\nexport const PASSWORD_PBKDF2_ITERATIONS = 600_000\n\n/** Default minimum password length. Override per-app via `enrollPasswordAuthenticator`. */\nexport const PASSWORD_DEFAULT_MIN_LENGTH = 12\n\n// ─── Errors ────────────────────────────────────────────────────────────\n\nexport class PasswordTooWeakError extends Error {\n readonly code = 'PASSWORD_TOO_WEAK' as const\n readonly minLength: number\n constructor(minLength: number, message?: string) {\n super(\n message ??\n `Password must be at least ${String(minLength)} characters. ` +\n 'For accounts with a separate tier-1 phrase, prefer a longer password ' +\n 'or pair with TOTP/email-OTP via @noy-db/on-totp or @noy-db/on-email-otp.',\n )\n this.name = 'PasswordTooWeakError'\n this.minLength = minLength\n }\n}\n\nexport class PasswordInvalidError extends Error {\n readonly code = 'PASSWORD_INVALID' as const\n constructor(message = 'Password does not unlock this slot.') {\n super(message)\n this.name = 'PasswordInvalidError'\n }\n}\n\n// ─── Public API ────────────────────────────────────────────────────────\n\n/** Options for {@link enrollPasswordAuthenticator}. */\nexport interface EnrollPasswordOptions {\n /** Slot id. Default: `'password-daily'`. */\n readonly id?: string\n /** Daily password the user will type. Distinct from the tier-1 phrase. */\n readonly password: string\n /** Minimum length. Default 12. */\n readonly minLength?: number\n /** Optional regex the password must satisfy in addition to length. */\n readonly pattern?: RegExp\n /** Tier the active session held when enrolling. Default 1. */\n readonly enrolledViaTier?: 1 | 2\n}\n\n/**\n * Build the keyring slot for a tier-2 password authenticator. Returns\n * an `EnrollAuthenticatorOptions` value the caller hands to\n * `db.enrollAuthenticator(vault, slot)` — separating the cryptographic\n * step (this function) from the persistence step (the hub) keeps the\n * package small and lets the hub's policy gate run between the two.\n *\n * The slot uses the **wrap-DEKs** variant of `KeyringAuthenticator`:\n * the DEK set is serialized to JSON and encrypted with AES-GCM under\n * a PBKDF2-derived key. No requirement on `keyring.kek` — works with\n * tier-1 unlocked keyrings; throws if `keyring.deks` is empty.\n *\n * Usage:\n *\n * ```ts\n * import { enrollPasswordAuthenticator } from '@noy-db/on-password'\n *\n * const keyring = await db.getKeyring('acme')\n * const slot = await enrollPasswordAuthenticator(keyring, {\n * password: 'daily-password-2026',\n * minLength: 14,\n * })\n * await db.enrollAuthenticator('acme', slot, {\n * factors: [{ kind: 'totp' }],\n * })\n * ```\n */\nexport async function enrollPasswordAuthenticator(\n keyring: UnlockedKeyring,\n options: EnrollPasswordOptions,\n): Promise<EnrollAuthenticatorOptions> {\n const minLength = options.minLength ?? PASSWORD_DEFAULT_MIN_LENGTH\n if (options.password.length < minLength) {\n throw new PasswordTooWeakError(minLength)\n }\n if (options.pattern && !options.pattern.test(options.password)) {\n throw new PasswordTooWeakError(\n minLength,\n `Password does not match the configured pattern: ${options.pattern.toString()}.`,\n )\n }\n\n if (keyring.deks.size === 0) {\n throw new Error(\n 'enrollPasswordAuthenticator: the supplied keyring has no DEKs in memory. ' +\n 'Re-authenticate at tier 1 first.',\n )\n }\n\n // Delegate the wrap-DEKs crypto to the canonical hub primitive (#44).\n // The slot envelope still stores `salt` inside `meta` for backward\n // compatibility with the pre-#44 slot format; the issue defers\n // moving it to top-level for parity with PaperRecoveryEntry.\n const blob = await mintWrappedDeksBlob(keyring.deks, options.password)\n\n return {\n id: options.id ?? 'password-daily',\n method: 'password',\n wrapKind: 'deks',\n wrapped_deks: blob.wrappedDeks,\n iv: blob.iv,\n meta: {\n salt: blob.salt,\n minLength,\n ...(options.pattern !== undefined ? { pattern: options.pattern.source } : {}),\n },\n enrolled_via_tier: options.enrolledViaTier ?? 1,\n }\n}\n\n/**\n * Recover the DEK set from a wrap-DEKs password slot. Returns the raw\n * DEK map; the hub-friendly verifier {@link verifyPasswordSlot} wraps\n * this and produces a full `UnlockedKeyring`.\n *\n * @throws {@link PasswordInvalidError} when the password is wrong or\n * the slot is not a wrap-DEKs slot (e.g. a legacy wrap-KEK password\n * slot from before pre.8 — those need re-enrollment).\n */\nexport async function unwrapDeksWithPassword(\n slot: KeyringAuthenticator,\n password: string,\n): Promise<Map<string, CryptoKey>> {\n if (slot.wrapKind !== 'deks') {\n throw new PasswordInvalidError(\n 'Password slot is not a wrap-DEKs slot. Pre-pre.8 wrap-KEK password ' +\n 'slots are no longer supported — re-enrol via enrollPasswordAuthenticator.',\n )\n }\n\n const meta = slot.meta as { salt?: unknown }\n if (typeof meta.salt !== 'string') {\n throw new PasswordInvalidError(\n 'Password slot is missing the per-slot salt — keyring may be corrupted.',\n )\n }\n\n // Delegate to the canonical wrap-DEKs primitive (#44). The blob's\n // three fields (salt / iv / wrappedDeks) are reconstructed from\n // the slot's split layout: salt lives in meta, iv + wrappedDeks\n // at top level. Pre-#44, this code re-implemented the same crypto\n // inline.\n try {\n return await unwrapDeksFromBlob(\n { salt: meta.salt, iv: slot.iv, wrappedDeks: slot.wrapped_deks },\n password,\n )\n } catch {\n throw new PasswordInvalidError()\n }\n}\n\n/**\n * Hub-friendly verify callback. Pass to `db.unlockViaAuthenticator`:\n *\n * ```ts\n * import { verifyPasswordSlot } from '@noy-db/on-password'\n *\n * const unlocked = await db.unlockViaAuthenticator('acme', 'password-daily',\n * (slot) => verifyPasswordSlot(slot, 'daily-password-2026',\n * { store, vault: 'acme', userId: 'alice' }),\n * )\n * ```\n *\n * Unwraps the DEK set with the supplied password and returns an\n * `UnlockedKeyring` the hub installs in its keyring cache. The\n * returned keyring has `kek: null` — sensitive operations (enrol new\n * slot, rotate phrase) require a tier-1 unlock from the master phrase.\n *\n * The `{ store, vault, userId }` shape works in both contexts:\n * - **Warm re-unlock** — db is alive; consumer already has identity\n * in memory but pays one cheap `_keyring/<userId>` read.\n * - **Cold-start** (`createNoydb({ getKeyring: ... })`) — consumer\n * does NOT have a keyring yet; the verifier loads identity from\n * disk before returning the unlocked keyring. This is the\n * primary cold-start tier-2 path (Niwat tier-2b uses this).\n *\n * Identity fields (userId, displayName, role, permissions,\n * authenticators, salt, capability bits, per-keyring policy) are read\n * directly from the `_keyring/<userId>` envelope's plaintext header —\n * those fields are not encrypted because the sync engine + grant flow\n * need them without a key.\n *\n * @throws {@link PasswordInvalidError} when the password is wrong or\n * the keyring file is missing.\n */\nexport async function verifyPasswordSlot(\n slot: KeyringAuthenticator,\n password: string,\n options: VerifyPasswordSlotOptions,\n): Promise<UnlockedKeyring> {\n const deks = await unwrapDeksWithPassword(slot, password)\n\n const env = await options.store.get(options.vault, '_keyring', options.userId)\n if (!env) {\n throw new PasswordInvalidError(\n `verifyPasswordSlot: no keyring found at \"${options.vault}/_keyring/${options.userId}\". ` +\n 'Verify the vault and userId are correct.',\n )\n }\n const file = JSON.parse(env._data) as KeyringFile\n const salt = new Uint8Array(base64ToBuffer(file.salt))\n\n return {\n userId: file.user_id,\n displayName: file.display_name,\n role: file.role,\n permissions: file.permissions,\n authenticators: file.authenticators ?? [],\n salt,\n ...(file.export_capability !== undefined && { exportCapability: file.export_capability }),\n ...(file.import_capability !== undefined && { importCapability: file.import_capability }),\n ...(file.policy !== undefined && { policy: file.policy }),\n deks,\n // Wrap-DEKs unlock cannot recover the KEK. Sensitive ops route\n // through tier-1 via re-entry of the master phrase. Matches the\n // existing tier-3 (`@noy-db/on-pin`) pattern.\n kek: null,\n }\n}\n\n/** Adapter shape required by {@link verifyPasswordSlot}. */\nexport interface VerifyPasswordSlotOptions {\n /** The vault's NoydbStore — used to load `_keyring/<userId>`. */\n readonly store: NoydbStore\n /** Vault name — same value passed to `db.openVault(name)`. */\n readonly vault: string\n /** User id — same value passed to `createNoydb({ user })`. */\n readonly userId: string\n}\n\n// All wrap-DEKs crypto helpers (PBKDF2 derivation, base64\n// codecs) moved to hub's `team/wrapped-deks.ts` in #44 — both this\n// package and `mintPaperRecoveryEntry` now share one implementation.\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UnlockedKeyring, EnrollAuthenticatorOptions, KeyringAuthenticator } from '@noy-db/hub';
|
|
1
|
+
import { NoydbStore, UnlockedKeyring, EnrollAuthenticatorOptions, KeyringAuthenticator } from '@noy-db/hub';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* **@noy-db/on-password** — tier-2 daily-password authenticator slot.
|
|
@@ -14,19 +14,34 @@ import { UnlockedKeyring, EnrollAuthenticatorOptions, KeyringAuthenticator } fro
|
|
|
14
14
|
* phrase format (issue #7); the password is validated against a
|
|
15
15
|
* length / regex rule the developer chooses.
|
|
16
16
|
* - **Different storage** — the phrase derives the KEK; the password
|
|
17
|
-
* derives a wrapping key that wraps the SAME
|
|
18
|
-
* keyring slot (LUKS
|
|
17
|
+
* derives a wrapping key that wraps the SAME DEK SET in its own
|
|
18
|
+
* keyring slot (LUKS-like multi-slot, wrap-DEKs variant).
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* ## Wrap-DEKs format (#26 Path C)
|
|
21
|
+
*
|
|
22
|
+
* Slots produced by this package use the wrap-DEKs variant of
|
|
23
|
+
* `KeyringAuthenticator` — they encrypt the serialized DEK set under
|
|
24
|
+
* a password-derived AES-GCM key, NOT the KEK. This unifies tier-2
|
|
25
|
+
* password slots with the tier-0 (paper recovery, `mintPaperRecoveryEntry`)
|
|
26
|
+
* and tier-3 (`@noy-db/on-pin`'s `wrappedKeyring`) primitives — all
|
|
27
|
+
* three sidestep the non-extractable-KEK constraint by wrapping the
|
|
28
|
+
* DEK set rather than the KEK itself.
|
|
29
|
+
*
|
|
30
|
+
* Trade-off: an `UnlockedKeyring` produced via password-slot unlock
|
|
31
|
+
* has `kek: null`. Sensitive operations (`enrollAuthenticator`,
|
|
32
|
+
* `rotatePassphrase`) require a tier-1 unlock anyway — re-enter the
|
|
33
|
+
* master phrase.
|
|
23
34
|
*
|
|
24
35
|
* @see docs/subsystems/session-tiers.md → Tier 2 — `on-password`
|
|
25
36
|
*
|
|
26
37
|
* @packageDocumentation
|
|
27
38
|
*/
|
|
28
39
|
|
|
29
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* PBKDF2 iteration count — matches the tier-1 phrase derivation.
|
|
42
|
+
* Exported as a documented invariant; the actual derivation lives
|
|
43
|
+
* in hub's canonical wrap-DEKs primitive (#44).
|
|
44
|
+
*/
|
|
30
45
|
declare const PASSWORD_PBKDF2_ITERATIONS = 600000;
|
|
31
46
|
/** Default minimum password length. Override per-app via `enrollPasswordAuthenticator`. */
|
|
32
47
|
declare const PASSWORD_DEFAULT_MIN_LENGTH = 12;
|
|
@@ -59,12 +74,18 @@ interface EnrollPasswordOptions {
|
|
|
59
74
|
* step (this function) from the persistence step (the hub) keeps the
|
|
60
75
|
* package small and lets the hub's policy gate run between the two.
|
|
61
76
|
*
|
|
77
|
+
* The slot uses the **wrap-DEKs** variant of `KeyringAuthenticator`:
|
|
78
|
+
* the DEK set is serialized to JSON and encrypted with AES-GCM under
|
|
79
|
+
* a PBKDF2-derived key. No requirement on `keyring.kek` — works with
|
|
80
|
+
* tier-1 unlocked keyrings; throws if `keyring.deks` is empty.
|
|
81
|
+
*
|
|
62
82
|
* Usage:
|
|
63
83
|
*
|
|
64
84
|
* ```ts
|
|
65
85
|
* import { enrollPasswordAuthenticator } from '@noy-db/on-password'
|
|
66
86
|
*
|
|
67
|
-
* const
|
|
87
|
+
* const keyring = await db.getKeyring('acme')
|
|
88
|
+
* const slot = await enrollPasswordAuthenticator(keyring, {
|
|
68
89
|
* password: 'daily-password-2026',
|
|
69
90
|
* minLength: 14,
|
|
70
91
|
* })
|
|
@@ -75,40 +96,58 @@ interface EnrollPasswordOptions {
|
|
|
75
96
|
*/
|
|
76
97
|
declare function enrollPasswordAuthenticator(keyring: UnlockedKeyring, options: EnrollPasswordOptions): Promise<EnrollAuthenticatorOptions>;
|
|
77
98
|
/**
|
|
78
|
-
* Recover the
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* `db.unlockViaAuthenticator`.
|
|
99
|
+
* Recover the DEK set from a wrap-DEKs password slot. Returns the raw
|
|
100
|
+
* DEK map; the hub-friendly verifier {@link verifyPasswordSlot} wraps
|
|
101
|
+
* this and produces a full `UnlockedKeyring`.
|
|
82
102
|
*
|
|
83
|
-
* @throws {@link PasswordInvalidError} when the password is wrong
|
|
103
|
+
* @throws {@link PasswordInvalidError} when the password is wrong or
|
|
104
|
+
* the slot is not a wrap-DEKs slot (e.g. a legacy wrap-KEK password
|
|
105
|
+
* slot from before pre.8 — those need re-enrollment).
|
|
84
106
|
*/
|
|
85
|
-
declare function
|
|
107
|
+
declare function unwrapDeksWithPassword(slot: KeyringAuthenticator, password: string): Promise<Map<string, CryptoKey>>;
|
|
86
108
|
/**
|
|
87
109
|
* Hub-friendly verify callback. Pass to `db.unlockViaAuthenticator`:
|
|
88
110
|
*
|
|
89
111
|
* ```ts
|
|
112
|
+
* import { verifyPasswordSlot } from '@noy-db/on-password'
|
|
113
|
+
*
|
|
90
114
|
* const unlocked = await db.unlockViaAuthenticator('acme', 'password-daily',
|
|
91
|
-
* (slot) => verifyPasswordSlot(slot, 'daily-password-2026',
|
|
115
|
+
* (slot) => verifyPasswordSlot(slot, 'daily-password-2026',
|
|
116
|
+
* { store, vault: 'acme', userId: 'alice' }),
|
|
92
117
|
* )
|
|
93
118
|
* ```
|
|
94
119
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* `
|
|
120
|
+
* Unwraps the DEK set with the supplied password and returns an
|
|
121
|
+
* `UnlockedKeyring` the hub installs in its keyring cache. The
|
|
122
|
+
* returned keyring has `kek: null` — sensitive operations (enrol new
|
|
123
|
+
* slot, rotate phrase) require a tier-1 unlock from the master phrase.
|
|
124
|
+
*
|
|
125
|
+
* The `{ store, vault, userId }` shape works in both contexts:
|
|
126
|
+
* - **Warm re-unlock** — db is alive; consumer already has identity
|
|
127
|
+
* in memory but pays one cheap `_keyring/<userId>` read.
|
|
128
|
+
* - **Cold-start** (`createNoydb({ getKeyring: ... })`) — consumer
|
|
129
|
+
* does NOT have a keyring yet; the verifier loads identity from
|
|
130
|
+
* disk before returning the unlocked keyring. This is the
|
|
131
|
+
* primary cold-start tier-2 path (Niwat tier-2b uses this).
|
|
132
|
+
*
|
|
133
|
+
* Identity fields (userId, displayName, role, permissions,
|
|
134
|
+
* authenticators, salt, capability bits, per-keyring policy) are read
|
|
135
|
+
* directly from the `_keyring/<userId>` envelope's plaintext header —
|
|
136
|
+
* those fields are not encrypted because the sync engine + grant flow
|
|
137
|
+
* need them without a key.
|
|
98
138
|
*
|
|
99
|
-
* @throws {@link PasswordInvalidError} when the password is wrong
|
|
139
|
+
* @throws {@link PasswordInvalidError} when the password is wrong or
|
|
140
|
+
* the keyring file is missing.
|
|
100
141
|
*/
|
|
101
142
|
declare function verifyPasswordSlot(slot: KeyringAuthenticator, password: string, options: VerifyPasswordSlotOptions): Promise<UnlockedKeyring>;
|
|
102
143
|
/** Adapter shape required by {@link verifyPasswordSlot}. */
|
|
103
144
|
interface VerifyPasswordSlotOptions {
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
*/
|
|
111
|
-
readonly materialize: (kek: CryptoKey) => Promise<UnlockedKeyring>;
|
|
145
|
+
/** The vault's NoydbStore — used to load `_keyring/<userId>`. */
|
|
146
|
+
readonly store: NoydbStore;
|
|
147
|
+
/** Vault name — same value passed to `db.openVault(name)`. */
|
|
148
|
+
readonly vault: string;
|
|
149
|
+
/** User id — same value passed to `createNoydb({ user })`. */
|
|
150
|
+
readonly userId: string;
|
|
112
151
|
}
|
|
113
152
|
|
|
114
|
-
export { type EnrollPasswordOptions, PASSWORD_DEFAULT_MIN_LENGTH, PASSWORD_PBKDF2_ITERATIONS, PasswordInvalidError, PasswordTooWeakError, type VerifyPasswordSlotOptions, enrollPasswordAuthenticator,
|
|
153
|
+
export { type EnrollPasswordOptions, PASSWORD_DEFAULT_MIN_LENGTH, PASSWORD_PBKDF2_ITERATIONS, PasswordInvalidError, PasswordTooWeakError, type VerifyPasswordSlotOptions, enrollPasswordAuthenticator, unwrapDeksWithPassword, verifyPasswordSlot };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UnlockedKeyring, EnrollAuthenticatorOptions, KeyringAuthenticator } from '@noy-db/hub';
|
|
1
|
+
import { NoydbStore, UnlockedKeyring, EnrollAuthenticatorOptions, KeyringAuthenticator } from '@noy-db/hub';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* **@noy-db/on-password** — tier-2 daily-password authenticator slot.
|
|
@@ -14,19 +14,34 @@ import { UnlockedKeyring, EnrollAuthenticatorOptions, KeyringAuthenticator } fro
|
|
|
14
14
|
* phrase format (issue #7); the password is validated against a
|
|
15
15
|
* length / regex rule the developer chooses.
|
|
16
16
|
* - **Different storage** — the phrase derives the KEK; the password
|
|
17
|
-
* derives a wrapping key that wraps the SAME
|
|
18
|
-
* keyring slot (LUKS
|
|
17
|
+
* derives a wrapping key that wraps the SAME DEK SET in its own
|
|
18
|
+
* keyring slot (LUKS-like multi-slot, wrap-DEKs variant).
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* ## Wrap-DEKs format (#26 Path C)
|
|
21
|
+
*
|
|
22
|
+
* Slots produced by this package use the wrap-DEKs variant of
|
|
23
|
+
* `KeyringAuthenticator` — they encrypt the serialized DEK set under
|
|
24
|
+
* a password-derived AES-GCM key, NOT the KEK. This unifies tier-2
|
|
25
|
+
* password slots with the tier-0 (paper recovery, `mintPaperRecoveryEntry`)
|
|
26
|
+
* and tier-3 (`@noy-db/on-pin`'s `wrappedKeyring`) primitives — all
|
|
27
|
+
* three sidestep the non-extractable-KEK constraint by wrapping the
|
|
28
|
+
* DEK set rather than the KEK itself.
|
|
29
|
+
*
|
|
30
|
+
* Trade-off: an `UnlockedKeyring` produced via password-slot unlock
|
|
31
|
+
* has `kek: null`. Sensitive operations (`enrollAuthenticator`,
|
|
32
|
+
* `rotatePassphrase`) require a tier-1 unlock anyway — re-enter the
|
|
33
|
+
* master phrase.
|
|
23
34
|
*
|
|
24
35
|
* @see docs/subsystems/session-tiers.md → Tier 2 — `on-password`
|
|
25
36
|
*
|
|
26
37
|
* @packageDocumentation
|
|
27
38
|
*/
|
|
28
39
|
|
|
29
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* PBKDF2 iteration count — matches the tier-1 phrase derivation.
|
|
42
|
+
* Exported as a documented invariant; the actual derivation lives
|
|
43
|
+
* in hub's canonical wrap-DEKs primitive (#44).
|
|
44
|
+
*/
|
|
30
45
|
declare const PASSWORD_PBKDF2_ITERATIONS = 600000;
|
|
31
46
|
/** Default minimum password length. Override per-app via `enrollPasswordAuthenticator`. */
|
|
32
47
|
declare const PASSWORD_DEFAULT_MIN_LENGTH = 12;
|
|
@@ -59,12 +74,18 @@ interface EnrollPasswordOptions {
|
|
|
59
74
|
* step (this function) from the persistence step (the hub) keeps the
|
|
60
75
|
* package small and lets the hub's policy gate run between the two.
|
|
61
76
|
*
|
|
77
|
+
* The slot uses the **wrap-DEKs** variant of `KeyringAuthenticator`:
|
|
78
|
+
* the DEK set is serialized to JSON and encrypted with AES-GCM under
|
|
79
|
+
* a PBKDF2-derived key. No requirement on `keyring.kek` — works with
|
|
80
|
+
* tier-1 unlocked keyrings; throws if `keyring.deks` is empty.
|
|
81
|
+
*
|
|
62
82
|
* Usage:
|
|
63
83
|
*
|
|
64
84
|
* ```ts
|
|
65
85
|
* import { enrollPasswordAuthenticator } from '@noy-db/on-password'
|
|
66
86
|
*
|
|
67
|
-
* const
|
|
87
|
+
* const keyring = await db.getKeyring('acme')
|
|
88
|
+
* const slot = await enrollPasswordAuthenticator(keyring, {
|
|
68
89
|
* password: 'daily-password-2026',
|
|
69
90
|
* minLength: 14,
|
|
70
91
|
* })
|
|
@@ -75,40 +96,58 @@ interface EnrollPasswordOptions {
|
|
|
75
96
|
*/
|
|
76
97
|
declare function enrollPasswordAuthenticator(keyring: UnlockedKeyring, options: EnrollPasswordOptions): Promise<EnrollAuthenticatorOptions>;
|
|
77
98
|
/**
|
|
78
|
-
* Recover the
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* `db.unlockViaAuthenticator`.
|
|
99
|
+
* Recover the DEK set from a wrap-DEKs password slot. Returns the raw
|
|
100
|
+
* DEK map; the hub-friendly verifier {@link verifyPasswordSlot} wraps
|
|
101
|
+
* this and produces a full `UnlockedKeyring`.
|
|
82
102
|
*
|
|
83
|
-
* @throws {@link PasswordInvalidError} when the password is wrong
|
|
103
|
+
* @throws {@link PasswordInvalidError} when the password is wrong or
|
|
104
|
+
* the slot is not a wrap-DEKs slot (e.g. a legacy wrap-KEK password
|
|
105
|
+
* slot from before pre.8 — those need re-enrollment).
|
|
84
106
|
*/
|
|
85
|
-
declare function
|
|
107
|
+
declare function unwrapDeksWithPassword(slot: KeyringAuthenticator, password: string): Promise<Map<string, CryptoKey>>;
|
|
86
108
|
/**
|
|
87
109
|
* Hub-friendly verify callback. Pass to `db.unlockViaAuthenticator`:
|
|
88
110
|
*
|
|
89
111
|
* ```ts
|
|
112
|
+
* import { verifyPasswordSlot } from '@noy-db/on-password'
|
|
113
|
+
*
|
|
90
114
|
* const unlocked = await db.unlockViaAuthenticator('acme', 'password-daily',
|
|
91
|
-
* (slot) => verifyPasswordSlot(slot, 'daily-password-2026',
|
|
115
|
+
* (slot) => verifyPasswordSlot(slot, 'daily-password-2026',
|
|
116
|
+
* { store, vault: 'acme', userId: 'alice' }),
|
|
92
117
|
* )
|
|
93
118
|
* ```
|
|
94
119
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* `
|
|
120
|
+
* Unwraps the DEK set with the supplied password and returns an
|
|
121
|
+
* `UnlockedKeyring` the hub installs in its keyring cache. The
|
|
122
|
+
* returned keyring has `kek: null` — sensitive operations (enrol new
|
|
123
|
+
* slot, rotate phrase) require a tier-1 unlock from the master phrase.
|
|
124
|
+
*
|
|
125
|
+
* The `{ store, vault, userId }` shape works in both contexts:
|
|
126
|
+
* - **Warm re-unlock** — db is alive; consumer already has identity
|
|
127
|
+
* in memory but pays one cheap `_keyring/<userId>` read.
|
|
128
|
+
* - **Cold-start** (`createNoydb({ getKeyring: ... })`) — consumer
|
|
129
|
+
* does NOT have a keyring yet; the verifier loads identity from
|
|
130
|
+
* disk before returning the unlocked keyring. This is the
|
|
131
|
+
* primary cold-start tier-2 path (Niwat tier-2b uses this).
|
|
132
|
+
*
|
|
133
|
+
* Identity fields (userId, displayName, role, permissions,
|
|
134
|
+
* authenticators, salt, capability bits, per-keyring policy) are read
|
|
135
|
+
* directly from the `_keyring/<userId>` envelope's plaintext header —
|
|
136
|
+
* those fields are not encrypted because the sync engine + grant flow
|
|
137
|
+
* need them without a key.
|
|
98
138
|
*
|
|
99
|
-
* @throws {@link PasswordInvalidError} when the password is wrong
|
|
139
|
+
* @throws {@link PasswordInvalidError} when the password is wrong or
|
|
140
|
+
* the keyring file is missing.
|
|
100
141
|
*/
|
|
101
142
|
declare function verifyPasswordSlot(slot: KeyringAuthenticator, password: string, options: VerifyPasswordSlotOptions): Promise<UnlockedKeyring>;
|
|
102
143
|
/** Adapter shape required by {@link verifyPasswordSlot}. */
|
|
103
144
|
interface VerifyPasswordSlotOptions {
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
*/
|
|
111
|
-
readonly materialize: (kek: CryptoKey) => Promise<UnlockedKeyring>;
|
|
145
|
+
/** The vault's NoydbStore — used to load `_keyring/<userId>`. */
|
|
146
|
+
readonly store: NoydbStore;
|
|
147
|
+
/** Vault name — same value passed to `db.openVault(name)`. */
|
|
148
|
+
readonly vault: string;
|
|
149
|
+
/** User id — same value passed to `createNoydb({ user })`. */
|
|
150
|
+
readonly userId: string;
|
|
112
151
|
}
|
|
113
152
|
|
|
114
|
-
export { type EnrollPasswordOptions, PASSWORD_DEFAULT_MIN_LENGTH, PASSWORD_PBKDF2_ITERATIONS, PasswordInvalidError, PasswordTooWeakError, type VerifyPasswordSlotOptions, enrollPasswordAuthenticator,
|
|
153
|
+
export { type EnrollPasswordOptions, PASSWORD_DEFAULT_MIN_LENGTH, PASSWORD_PBKDF2_ITERATIONS, PasswordInvalidError, PasswordTooWeakError, type VerifyPasswordSlotOptions, enrollPasswordAuthenticator, unwrapDeksWithPassword, verifyPasswordSlot };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
base64ToBuffer,
|
|
4
|
+
mintWrappedDeksBlob,
|
|
5
|
+
unwrapDeksFromBlob
|
|
6
|
+
} from "@noy-db/hub";
|
|
2
7
|
var PASSWORD_PBKDF2_ITERATIONS = 6e5;
|
|
3
8
|
var PASSWORD_DEFAULT_MIN_LENGTH = 12;
|
|
4
|
-
var SALT_BYTES = 32;
|
|
5
|
-
var subtle = globalThis.crypto.subtle;
|
|
6
9
|
var PasswordTooWeakError = class extends Error {
|
|
7
10
|
code = "PASSWORD_TOO_WEAK";
|
|
8
11
|
minLength;
|
|
@@ -32,84 +35,73 @@ async function enrollPasswordAuthenticator(keyring, options) {
|
|
|
32
35
|
`Password does not match the configured pattern: ${options.pattern.toString()}.`
|
|
33
36
|
);
|
|
34
37
|
}
|
|
35
|
-
if (
|
|
38
|
+
if (keyring.deks.size === 0) {
|
|
36
39
|
throw new Error(
|
|
37
|
-
"enrollPasswordAuthenticator: the supplied keyring has no
|
|
40
|
+
"enrollPasswordAuthenticator: the supplied keyring has no DEKs in memory. Re-authenticate at tier 1 first."
|
|
38
41
|
);
|
|
39
42
|
}
|
|
40
|
-
const
|
|
41
|
-
const wrappingKey = await derivePasswordWrappingKey(options.password, salt);
|
|
42
|
-
const wrapped = await subtle.wrapKey("raw", keyring.kek, wrappingKey, "AES-KW");
|
|
43
|
+
const blob = await mintWrappedDeksBlob(keyring.deks, options.password);
|
|
43
44
|
return {
|
|
44
45
|
id: options.id ?? "password-daily",
|
|
45
46
|
method: "password",
|
|
46
|
-
|
|
47
|
+
wrapKind: "deks",
|
|
48
|
+
wrapped_deks: blob.wrappedDeks,
|
|
49
|
+
iv: blob.iv,
|
|
47
50
|
meta: {
|
|
48
|
-
salt:
|
|
51
|
+
salt: blob.salt,
|
|
49
52
|
minLength,
|
|
50
53
|
...options.pattern !== void 0 ? { pattern: options.pattern.source } : {}
|
|
51
54
|
},
|
|
52
55
|
enrolled_via_tier: options.enrolledViaTier ?? 1
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
|
-
async function
|
|
58
|
+
async function unwrapDeksWithPassword(slot, password) {
|
|
59
|
+
if (slot.wrapKind !== "deks") {
|
|
60
|
+
throw new PasswordInvalidError(
|
|
61
|
+
"Password slot is not a wrap-DEKs slot. Pre-pre.8 wrap-KEK password slots are no longer supported \u2014 re-enrol via enrollPasswordAuthenticator."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
56
64
|
const meta = slot.meta;
|
|
57
65
|
if (typeof meta.salt !== "string") {
|
|
58
66
|
throw new PasswordInvalidError(
|
|
59
67
|
"Password slot is missing the per-slot salt \u2014 keyring may be corrupted."
|
|
60
68
|
);
|
|
61
69
|
}
|
|
62
|
-
const salt = base64ToBytes(meta.salt);
|
|
63
|
-
const wrappingKey = await derivePasswordWrappingKey(password, salt);
|
|
64
70
|
try {
|
|
65
|
-
return await
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
wrappingKey,
|
|
69
|
-
"AES-KW",
|
|
70
|
-
{ name: "AES-KW", length: 256 },
|
|
71
|
-
false,
|
|
72
|
-
["wrapKey", "unwrapKey"]
|
|
71
|
+
return await unwrapDeksFromBlob(
|
|
72
|
+
{ salt: meta.salt, iv: slot.iv, wrappedDeks: slot.wrapped_deks },
|
|
73
|
+
password
|
|
73
74
|
);
|
|
74
75
|
} catch {
|
|
75
76
|
throw new PasswordInvalidError();
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
async function verifyPasswordSlot(slot, password, options) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
let s = "";
|
|
105
|
-
for (const b of bytes) s += String.fromCharCode(b);
|
|
106
|
-
return btoa(s);
|
|
107
|
-
}
|
|
108
|
-
function base64ToBytes(b64) {
|
|
109
|
-
const s = atob(b64);
|
|
110
|
-
const out = new Uint8Array(s.length);
|
|
111
|
-
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
|
112
|
-
return out;
|
|
80
|
+
const deks = await unwrapDeksWithPassword(slot, password);
|
|
81
|
+
const env = await options.store.get(options.vault, "_keyring", options.userId);
|
|
82
|
+
if (!env) {
|
|
83
|
+
throw new PasswordInvalidError(
|
|
84
|
+
`verifyPasswordSlot: no keyring found at "${options.vault}/_keyring/${options.userId}". Verify the vault and userId are correct.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const file = JSON.parse(env._data);
|
|
88
|
+
const salt = new Uint8Array(base64ToBuffer(file.salt));
|
|
89
|
+
return {
|
|
90
|
+
userId: file.user_id,
|
|
91
|
+
displayName: file.display_name,
|
|
92
|
+
role: file.role,
|
|
93
|
+
permissions: file.permissions,
|
|
94
|
+
authenticators: file.authenticators ?? [],
|
|
95
|
+
salt,
|
|
96
|
+
...file.export_capability !== void 0 && { exportCapability: file.export_capability },
|
|
97
|
+
...file.import_capability !== void 0 && { importCapability: file.import_capability },
|
|
98
|
+
...file.policy !== void 0 && { policy: file.policy },
|
|
99
|
+
deks,
|
|
100
|
+
// Wrap-DEKs unlock cannot recover the KEK. Sensitive ops route
|
|
101
|
+
// through tier-1 via re-entry of the master phrase. Matches the
|
|
102
|
+
// existing tier-3 (`@noy-db/on-pin`) pattern.
|
|
103
|
+
kek: null
|
|
104
|
+
};
|
|
113
105
|
}
|
|
114
106
|
export {
|
|
115
107
|
PASSWORD_DEFAULT_MIN_LENGTH,
|
|
@@ -117,7 +109,7 @@ export {
|
|
|
117
109
|
PasswordInvalidError,
|
|
118
110
|
PasswordTooWeakError,
|
|
119
111
|
enrollPasswordAuthenticator,
|
|
120
|
-
|
|
112
|
+
unwrapDeksWithPassword,
|
|
121
113
|
verifyPasswordSlot
|
|
122
114
|
};
|
|
123
115
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/on-password** — tier-2 daily-password authenticator slot.\n *\n * The user's tier-1 *phrase* is the rarely-typed master that derives\n * the KEK. This package adds a SEPARATE secret — a daily-typed\n * password — as a tier-2 slot in the multi-slot keyring. The two\n * credentials have:\n *\n * - **Different lifecycles** — the phrase rotates yearly; the password\n * rotates per the developer's policy.\n * - **Different strength rules** — the phrase is validated against the\n * phrase format (issue #7); the password is validated against a\n * length / regex rule the developer chooses.\n * - **Different storage** — the phrase derives the KEK; the password\n * derives a wrapping key that wraps the SAME KEK in its own\n * keyring slot (LUKS pattern).\n *\n * The slot is added to the keyring via `db.enrollAuthenticator`; the\n * KEK is recovered via `db.unlockViaAuthenticator` — both routes hit\n * the policy gate engine (issue #9).\n *\n * @see docs/subsystems/session-tiers.md → Tier 2 — `on-password`\n *\n * @packageDocumentation\n */\nimport type {\n EnrollAuthenticatorOptions,\n KeyringAuthenticator,\n UnlockedKeyring,\n} from '@noy-db/hub'\n\n/** PBKDF2 iteration count — matches the tier-1 phrase derivation. */\nexport const PASSWORD_PBKDF2_ITERATIONS = 600_000\n\n/** Default minimum password length. Override per-app via `enrollPasswordAuthenticator`. */\nexport const PASSWORD_DEFAULT_MIN_LENGTH = 12\n\n/** Per-slot salt size. */\nconst SALT_BYTES = 32\n\nconst subtle = globalThis.crypto.subtle\n\n// ─── Errors ────────────────────────────────────────────────────────────\n\nexport class PasswordTooWeakError extends Error {\n readonly code = 'PASSWORD_TOO_WEAK' as const\n readonly minLength: number\n constructor(minLength: number, message?: string) {\n super(\n message ??\n `Password must be at least ${String(minLength)} characters. ` +\n 'For accounts with a separate tier-1 phrase, prefer a longer password ' +\n 'or pair with TOTP/email-OTP via @noy-db/on-totp or @noy-db/on-email-otp.',\n )\n this.name = 'PasswordTooWeakError'\n this.minLength = minLength\n }\n}\n\nexport class PasswordInvalidError extends Error {\n readonly code = 'PASSWORD_INVALID' as const\n constructor(message = 'Password does not unlock this slot.') {\n super(message)\n this.name = 'PasswordInvalidError'\n }\n}\n\n// ─── Public API ────────────────────────────────────────────────────────\n\n/** Options for {@link enrollPasswordAuthenticator}. */\nexport interface EnrollPasswordOptions {\n /** Slot id. Default: `'password-daily'`. */\n readonly id?: string\n /** Daily password the user will type. Distinct from the tier-1 phrase. */\n readonly password: string\n /** Minimum length. Default 12. */\n readonly minLength?: number\n /** Optional regex the password must satisfy in addition to length. */\n readonly pattern?: RegExp\n /** Tier the active session held when enrolling. Default 1. */\n readonly enrolledViaTier?: 1 | 2\n}\n\n/**\n * Build the keyring slot for a tier-2 password authenticator. Returns\n * an `EnrollAuthenticatorOptions` value the caller hands to\n * `db.enrollAuthenticator(vault, slot)` — separating the cryptographic\n * step (this function) from the persistence step (the hub) keeps the\n * package small and lets the hub's policy gate run between the two.\n *\n * Usage:\n *\n * ```ts\n * import { enrollPasswordAuthenticator } from '@noy-db/on-password'\n *\n * const slot = await enrollPasswordAuthenticator(unlocked, {\n * password: 'daily-password-2026',\n * minLength: 14,\n * })\n * await db.enrollAuthenticator('acme', slot, {\n * factors: [{ kind: 'totp' }],\n * })\n * ```\n */\nexport async function enrollPasswordAuthenticator(\n keyring: UnlockedKeyring,\n options: EnrollPasswordOptions,\n): Promise<EnrollAuthenticatorOptions> {\n const minLength = options.minLength ?? PASSWORD_DEFAULT_MIN_LENGTH\n if (options.password.length < minLength) {\n throw new PasswordTooWeakError(minLength)\n }\n if (options.pattern && !options.pattern.test(options.password)) {\n throw new PasswordTooWeakError(\n minLength,\n `Password does not match the configured pattern: ${options.pattern.toString()}.`,\n )\n }\n\n if (!keyring.kek) {\n throw new Error(\n 'enrollPasswordAuthenticator: the supplied keyring has no KEK in memory. ' +\n 'Tier-3 quick-resume keyrings cannot enrol new tier-2 slots; re-authenticate at tier 1 first.',\n )\n }\n\n const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES))\n const wrappingKey = await derivePasswordWrappingKey(options.password, salt)\n const wrapped = await subtle.wrapKey('raw', keyring.kek, wrappingKey, 'AES-KW')\n\n return {\n id: options.id ?? 'password-daily',\n method: 'password',\n wrapped_kek: bytesToBase64(new Uint8Array(wrapped)),\n meta: {\n salt: bytesToBase64(salt),\n minLength,\n ...(options.pattern !== undefined ? { pattern: options.pattern.source } : {}),\n },\n enrolled_via_tier: options.enrolledViaTier ?? 1,\n }\n}\n\n/**\n * Recover the KEK from a password slot's `wrapped_kek` ciphertext.\n * Returns the unwrapped KEK as a non-extractable `CryptoKey` ready for\n * AES-KW unwrap of DEKs. Used inside the verify callback passed to\n * `db.unlockViaAuthenticator`.\n *\n * @throws {@link PasswordInvalidError} when the password is wrong.\n */\nexport async function unwrapKekWithPassword(\n slot: KeyringAuthenticator,\n password: string,\n): Promise<CryptoKey> {\n const meta = slot.meta as { salt?: unknown }\n if (typeof meta.salt !== 'string') {\n throw new PasswordInvalidError(\n 'Password slot is missing the per-slot salt — keyring may be corrupted.',\n )\n }\n const salt = base64ToBytes(meta.salt)\n const wrappingKey = await derivePasswordWrappingKey(password, salt)\n try {\n return await subtle.unwrapKey(\n 'raw',\n base64ToBytes(slot.wrapped_kek) as BufferSource,\n wrappingKey,\n 'AES-KW',\n { name: 'AES-KW', length: 256 },\n false,\n ['wrapKey', 'unwrapKey'],\n )\n } catch {\n throw new PasswordInvalidError()\n }\n}\n\n/**\n * Hub-friendly verify callback. Pass to `db.unlockViaAuthenticator`:\n *\n * ```ts\n * const unlocked = await db.unlockViaAuthenticator('acme', 'password-daily',\n * (slot) => verifyPasswordSlot(slot, 'daily-password-2026', { adapter: store, vault: 'acme', userId: 'alice' }),\n * )\n * ```\n *\n * The callback re-loads the keyring file via the supplied adapter,\n * unwraps every DEK with the recovered KEK, and returns the\n * `UnlockedKeyring` the hub installs in its keyring cache.\n *\n * @throws {@link PasswordInvalidError} when the password is wrong.\n */\nexport async function verifyPasswordSlot(\n slot: KeyringAuthenticator,\n password: string,\n options: VerifyPasswordSlotOptions,\n): Promise<UnlockedKeyring> {\n const kek = await unwrapKekWithPassword(slot, password)\n return options.materialize(kek)\n}\n\n/** Adapter shape required by {@link verifyPasswordSlot}. */\nexport interface VerifyPasswordSlotOptions {\n /**\n * Caller-supplied \"given the recovered KEK, build the\n * `UnlockedKeyring`\" routine. The hub provides the standard\n * implementation in {@link buildUnlockedKeyringFromKek}; consumers\n * with non-standard storage (e.g. encrypted browser-extension\n * stores) can pass their own.\n */\n readonly materialize: (kek: CryptoKey) => Promise<UnlockedKeyring>\n}\n\n// ─── Helpers ───────────────────────────────────────────────────────────\n\nasync function derivePasswordWrappingKey(\n password: string,\n salt: Uint8Array,\n): Promise<CryptoKey> {\n const ikm = await subtle.importKey(\n 'raw',\n new TextEncoder().encode(password),\n 'PBKDF2',\n false,\n ['deriveKey'],\n )\n return subtle.deriveKey(\n {\n name: 'PBKDF2',\n salt: salt as BufferSource,\n iterations: PASSWORD_PBKDF2_ITERATIONS,\n hash: 'SHA-256',\n },\n ikm,\n { name: 'AES-KW', length: 256 },\n false,\n ['wrapKey', 'unwrapKey'],\n )\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n let s = ''\n for (const b of bytes) s += String.fromCharCode(b)\n return btoa(s)\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n const s = atob(b64)\n const out = new Uint8Array(s.length)\n for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i)\n return out\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/on-password** — tier-2 daily-password authenticator slot.\n *\n * The user's tier-1 *phrase* is the rarely-typed master that derives\n * the KEK. This package adds a SEPARATE secret — a daily-typed\n * password — as a tier-2 slot in the multi-slot keyring. The two\n * credentials have:\n *\n * - **Different lifecycles** — the phrase rotates yearly; the password\n * rotates per the developer's policy.\n * - **Different strength rules** — the phrase is validated against the\n * phrase format (issue #7); the password is validated against a\n * length / regex rule the developer chooses.\n * - **Different storage** — the phrase derives the KEK; the password\n * derives a wrapping key that wraps the SAME DEK SET in its own\n * keyring slot (LUKS-like multi-slot, wrap-DEKs variant).\n *\n * ## Wrap-DEKs format (#26 Path C)\n *\n * Slots produced by this package use the wrap-DEKs variant of\n * `KeyringAuthenticator` — they encrypt the serialized DEK set under\n * a password-derived AES-GCM key, NOT the KEK. This unifies tier-2\n * password slots with the tier-0 (paper recovery, `mintPaperRecoveryEntry`)\n * and tier-3 (`@noy-db/on-pin`'s `wrappedKeyring`) primitives — all\n * three sidestep the non-extractable-KEK constraint by wrapping the\n * DEK set rather than the KEK itself.\n *\n * Trade-off: an `UnlockedKeyring` produced via password-slot unlock\n * has `kek: null`. Sensitive operations (`enrollAuthenticator`,\n * `rotatePassphrase`) require a tier-1 unlock anyway — re-enter the\n * master phrase.\n *\n * @see docs/subsystems/session-tiers.md → Tier 2 — `on-password`\n *\n * @packageDocumentation\n */\nimport {\n base64ToBuffer,\n mintWrappedDeksBlob,\n unwrapDeksFromBlob,\n type EnrollAuthenticatorOptions,\n type KeyringAuthenticator,\n type KeyringFile,\n type NoydbStore,\n type UnlockedKeyring,\n} from '@noy-db/hub'\n\n/**\n * PBKDF2 iteration count — matches the tier-1 phrase derivation.\n * Exported as a documented invariant; the actual derivation lives\n * in hub's canonical wrap-DEKs primitive (#44).\n */\nexport const PASSWORD_PBKDF2_ITERATIONS = 600_000\n\n/** Default minimum password length. Override per-app via `enrollPasswordAuthenticator`. */\nexport const PASSWORD_DEFAULT_MIN_LENGTH = 12\n\n// ─── Errors ────────────────────────────────────────────────────────────\n\nexport class PasswordTooWeakError extends Error {\n readonly code = 'PASSWORD_TOO_WEAK' as const\n readonly minLength: number\n constructor(minLength: number, message?: string) {\n super(\n message ??\n `Password must be at least ${String(minLength)} characters. ` +\n 'For accounts with a separate tier-1 phrase, prefer a longer password ' +\n 'or pair with TOTP/email-OTP via @noy-db/on-totp or @noy-db/on-email-otp.',\n )\n this.name = 'PasswordTooWeakError'\n this.minLength = minLength\n }\n}\n\nexport class PasswordInvalidError extends Error {\n readonly code = 'PASSWORD_INVALID' as const\n constructor(message = 'Password does not unlock this slot.') {\n super(message)\n this.name = 'PasswordInvalidError'\n }\n}\n\n// ─── Public API ────────────────────────────────────────────────────────\n\n/** Options for {@link enrollPasswordAuthenticator}. */\nexport interface EnrollPasswordOptions {\n /** Slot id. Default: `'password-daily'`. */\n readonly id?: string\n /** Daily password the user will type. Distinct from the tier-1 phrase. */\n readonly password: string\n /** Minimum length. Default 12. */\n readonly minLength?: number\n /** Optional regex the password must satisfy in addition to length. */\n readonly pattern?: RegExp\n /** Tier the active session held when enrolling. Default 1. */\n readonly enrolledViaTier?: 1 | 2\n}\n\n/**\n * Build the keyring slot for a tier-2 password authenticator. Returns\n * an `EnrollAuthenticatorOptions` value the caller hands to\n * `db.enrollAuthenticator(vault, slot)` — separating the cryptographic\n * step (this function) from the persistence step (the hub) keeps the\n * package small and lets the hub's policy gate run between the two.\n *\n * The slot uses the **wrap-DEKs** variant of `KeyringAuthenticator`:\n * the DEK set is serialized to JSON and encrypted with AES-GCM under\n * a PBKDF2-derived key. No requirement on `keyring.kek` — works with\n * tier-1 unlocked keyrings; throws if `keyring.deks` is empty.\n *\n * Usage:\n *\n * ```ts\n * import { enrollPasswordAuthenticator } from '@noy-db/on-password'\n *\n * const keyring = await db.getKeyring('acme')\n * const slot = await enrollPasswordAuthenticator(keyring, {\n * password: 'daily-password-2026',\n * minLength: 14,\n * })\n * await db.enrollAuthenticator('acme', slot, {\n * factors: [{ kind: 'totp' }],\n * })\n * ```\n */\nexport async function enrollPasswordAuthenticator(\n keyring: UnlockedKeyring,\n options: EnrollPasswordOptions,\n): Promise<EnrollAuthenticatorOptions> {\n const minLength = options.minLength ?? PASSWORD_DEFAULT_MIN_LENGTH\n if (options.password.length < minLength) {\n throw new PasswordTooWeakError(minLength)\n }\n if (options.pattern && !options.pattern.test(options.password)) {\n throw new PasswordTooWeakError(\n minLength,\n `Password does not match the configured pattern: ${options.pattern.toString()}.`,\n )\n }\n\n if (keyring.deks.size === 0) {\n throw new Error(\n 'enrollPasswordAuthenticator: the supplied keyring has no DEKs in memory. ' +\n 'Re-authenticate at tier 1 first.',\n )\n }\n\n // Delegate the wrap-DEKs crypto to the canonical hub primitive (#44).\n // The slot envelope still stores `salt` inside `meta` for backward\n // compatibility with the pre-#44 slot format; the issue defers\n // moving it to top-level for parity with PaperRecoveryEntry.\n const blob = await mintWrappedDeksBlob(keyring.deks, options.password)\n\n return {\n id: options.id ?? 'password-daily',\n method: 'password',\n wrapKind: 'deks',\n wrapped_deks: blob.wrappedDeks,\n iv: blob.iv,\n meta: {\n salt: blob.salt,\n minLength,\n ...(options.pattern !== undefined ? { pattern: options.pattern.source } : {}),\n },\n enrolled_via_tier: options.enrolledViaTier ?? 1,\n }\n}\n\n/**\n * Recover the DEK set from a wrap-DEKs password slot. Returns the raw\n * DEK map; the hub-friendly verifier {@link verifyPasswordSlot} wraps\n * this and produces a full `UnlockedKeyring`.\n *\n * @throws {@link PasswordInvalidError} when the password is wrong or\n * the slot is not a wrap-DEKs slot (e.g. a legacy wrap-KEK password\n * slot from before pre.8 — those need re-enrollment).\n */\nexport async function unwrapDeksWithPassword(\n slot: KeyringAuthenticator,\n password: string,\n): Promise<Map<string, CryptoKey>> {\n if (slot.wrapKind !== 'deks') {\n throw new PasswordInvalidError(\n 'Password slot is not a wrap-DEKs slot. Pre-pre.8 wrap-KEK password ' +\n 'slots are no longer supported — re-enrol via enrollPasswordAuthenticator.',\n )\n }\n\n const meta = slot.meta as { salt?: unknown }\n if (typeof meta.salt !== 'string') {\n throw new PasswordInvalidError(\n 'Password slot is missing the per-slot salt — keyring may be corrupted.',\n )\n }\n\n // Delegate to the canonical wrap-DEKs primitive (#44). The blob's\n // three fields (salt / iv / wrappedDeks) are reconstructed from\n // the slot's split layout: salt lives in meta, iv + wrappedDeks\n // at top level. Pre-#44, this code re-implemented the same crypto\n // inline.\n try {\n return await unwrapDeksFromBlob(\n { salt: meta.salt, iv: slot.iv, wrappedDeks: slot.wrapped_deks },\n password,\n )\n } catch {\n throw new PasswordInvalidError()\n }\n}\n\n/**\n * Hub-friendly verify callback. Pass to `db.unlockViaAuthenticator`:\n *\n * ```ts\n * import { verifyPasswordSlot } from '@noy-db/on-password'\n *\n * const unlocked = await db.unlockViaAuthenticator('acme', 'password-daily',\n * (slot) => verifyPasswordSlot(slot, 'daily-password-2026',\n * { store, vault: 'acme', userId: 'alice' }),\n * )\n * ```\n *\n * Unwraps the DEK set with the supplied password and returns an\n * `UnlockedKeyring` the hub installs in its keyring cache. The\n * returned keyring has `kek: null` — sensitive operations (enrol new\n * slot, rotate phrase) require a tier-1 unlock from the master phrase.\n *\n * The `{ store, vault, userId }` shape works in both contexts:\n * - **Warm re-unlock** — db is alive; consumer already has identity\n * in memory but pays one cheap `_keyring/<userId>` read.\n * - **Cold-start** (`createNoydb({ getKeyring: ... })`) — consumer\n * does NOT have a keyring yet; the verifier loads identity from\n * disk before returning the unlocked keyring. This is the\n * primary cold-start tier-2 path (Niwat tier-2b uses this).\n *\n * Identity fields (userId, displayName, role, permissions,\n * authenticators, salt, capability bits, per-keyring policy) are read\n * directly from the `_keyring/<userId>` envelope's plaintext header —\n * those fields are not encrypted because the sync engine + grant flow\n * need them without a key.\n *\n * @throws {@link PasswordInvalidError} when the password is wrong or\n * the keyring file is missing.\n */\nexport async function verifyPasswordSlot(\n slot: KeyringAuthenticator,\n password: string,\n options: VerifyPasswordSlotOptions,\n): Promise<UnlockedKeyring> {\n const deks = await unwrapDeksWithPassword(slot, password)\n\n const env = await options.store.get(options.vault, '_keyring', options.userId)\n if (!env) {\n throw new PasswordInvalidError(\n `verifyPasswordSlot: no keyring found at \"${options.vault}/_keyring/${options.userId}\". ` +\n 'Verify the vault and userId are correct.',\n )\n }\n const file = JSON.parse(env._data) as KeyringFile\n const salt = new Uint8Array(base64ToBuffer(file.salt))\n\n return {\n userId: file.user_id,\n displayName: file.display_name,\n role: file.role,\n permissions: file.permissions,\n authenticators: file.authenticators ?? [],\n salt,\n ...(file.export_capability !== undefined && { exportCapability: file.export_capability }),\n ...(file.import_capability !== undefined && { importCapability: file.import_capability }),\n ...(file.policy !== undefined && { policy: file.policy }),\n deks,\n // Wrap-DEKs unlock cannot recover the KEK. Sensitive ops route\n // through tier-1 via re-entry of the master phrase. Matches the\n // existing tier-3 (`@noy-db/on-pin`) pattern.\n kek: null,\n }\n}\n\n/** Adapter shape required by {@link verifyPasswordSlot}. */\nexport interface VerifyPasswordSlotOptions {\n /** The vault's NoydbStore — used to load `_keyring/<userId>`. */\n readonly store: NoydbStore\n /** Vault name — same value passed to `db.openVault(name)`. */\n readonly vault: string\n /** User id — same value passed to `createNoydb({ user })`. */\n readonly userId: string\n}\n\n// All wrap-DEKs crypto helpers (PBKDF2 derivation, base64\n// codecs) moved to hub's `team/wrapped-deks.ts` in #44 — both this\n// package and `mintPaperRecoveryEntry` now share one implementation.\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noy-db/on-password",
|
|
3
|
-
"version": "0.1.0-pre.
|
|
3
|
+
"version": "0.1.0-pre.8",
|
|
4
4
|
"description": "Tier-2 daily-password authenticator slot for noy-db. PBKDF2-SHA256 600K wraps the KEK in its own keyring slot, distinct from the rarely-typed tier-1 phrase. Pair with @noy-db/on-email-otp or @noy-db/on-totp for the SaaS UX.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vLannaAi <vicio@lanna.ai>",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"node": ">=18.0.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@noy-db/hub": "0.1.0-pre.
|
|
42
|
+
"@noy-db/hub": "0.1.0-pre.8"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@noy-db/hub": "0.1.0-pre.
|
|
45
|
+
"@noy-db/hub": "0.1.0-pre.8"
|
|
46
46
|
},
|
|
47
47
|
"keywords": [
|
|
48
48
|
"noy-db",
|