@tgoliveira/vault-core 0.1.1 → 0.2.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.
- package/AGENTS.md +77 -0
- package/API_REFERENCE.md +196 -26
- package/ARCHITECTURE.md +5 -0
- package/CHANGELOG.md +51 -0
- package/MIGRATION_FROM_LIQSENSE.md +3 -1
- package/PASSKEY_PRF_ENVELOPES.md +2 -1
- package/PASSWORD_ENVELOPES.md +3 -1
- package/README.md +42 -2
- package/RECOVERY_PHRASE.md +2 -1
- package/SECURITY.md +22 -2
- package/dist/browser.d.ts +12 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +46 -18
- package/dist/browser.js.map +1 -1
- package/dist/envelopes/passkey-prf.d.ts +3 -3
- package/dist/envelopes/passkey-prf.d.ts.map +1 -1
- package/dist/envelopes/passkey-prf.js +7 -5
- package/dist/envelopes/passkey-prf.js.map +1 -1
- package/dist/envelopes/password.d.ts +1 -1
- package/dist/envelopes/password.d.ts.map +1 -1
- package/dist/envelopes/password.js +3 -1
- package/dist/envelopes/password.js.map +1 -1
- package/dist/envelopes/recovery.d.ts +2 -2
- package/dist/envelopes/recovery.d.ts.map +1 -1
- package/dist/envelopes/recovery.js +15 -6
- package/dist/envelopes/recovery.js.map +1 -1
- package/dist/kdf/argon2id.d.ts.map +1 -1
- package/dist/kdf/argon2id.js +15 -2
- package/dist/kdf/argon2id.js.map +1 -1
- package/dist/kdf/params.d.ts +24 -0
- package/dist/kdf/params.d.ts.map +1 -1
- package/dist/kdf/params.js +22 -0
- package/dist/kdf/params.js.map +1 -1
- package/dist/payload/encrypted-payload.d.ts +4 -2
- package/dist/payload/encrypted-payload.d.ts.map +1 -1
- package/dist/payload/encrypted-payload.js +3 -1
- package/dist/payload/encrypted-payload.js.map +1 -1
- package/dist/react/session/use-vault-session.d.ts +1 -0
- package/dist/react/session/use-vault-session.d.ts.map +1 -1
- package/dist/react/session/use-vault-session.js +7 -2
- package/dist/react/session/use-vault-session.js.map +1 -1
- package/dist/react/session/vault-session-provider.d.ts +2 -1
- package/dist/react/session/vault-session-provider.d.ts.map +1 -1
- package/dist/react/session/vault-session-provider.js +7 -2
- package/dist/react/session/vault-session-provider.js.map +1 -1
- package/dist/session/auto-lock.d.ts +2 -1
- package/dist/session/auto-lock.d.ts.map +1 -1
- package/dist/session/auto-lock.js +15 -1
- package/dist/session/auto-lock.js.map +1 -1
- package/dist/validation/aad-assert.d.ts +5 -3
- package/dist/validation/aad-assert.d.ts.map +1 -1
- package/dist/validation/aad-assert.js +15 -8
- package/dist/validation/aad-assert.js.map +1 -1
- package/dist/validation/plaintext-reject.d.ts.map +1 -1
- package/dist/validation/plaintext-reject.js +18 -4
- package/dist/validation/plaintext-reject.js.map +1 -1
- package/dist/validation/schemas.d.ts +148 -56
- package/dist/validation/schemas.d.ts.map +1 -1
- package/dist/validation/schemas.js +29 -10
- package/dist/validation/schemas.js.map +1 -1
- package/docs/ADOPTING_VAULT_CORE_IN_EXISTING_APPS.md +575 -0
- package/docs/IMPLEMENTATION_GUIDE.md +577 -0
- package/docs/README.md +30 -0
- package/docs/RELEASING.md +102 -0
- package/package.json +10 -3
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
# Complete Implementation Guide
|
|
2
|
+
|
|
3
|
+
This guide is the end-to-end consumer contract for `@tgoliveira/vault-core`. It is written so a
|
|
4
|
+
developer or coding agent can implement the package without reading its source code.
|
|
5
|
+
|
|
6
|
+
## 1. What the package owns
|
|
7
|
+
|
|
8
|
+
The package owns:
|
|
9
|
+
|
|
10
|
+
- Generation and import/export of the 256-bit User Vault Key (UVK).
|
|
11
|
+
- AES-GCM encryption of generic JSON payloads with authenticated context.
|
|
12
|
+
- Password and recovery phrase envelopes using bounded Argon2id.
|
|
13
|
+
- Passkey PRF envelopes when the application provides WebAuthn PRF output bytes.
|
|
14
|
+
- BIP39 English recovery phrase generation, validation, confirmation, and recovery kit text.
|
|
15
|
+
- Runtime schemas for encrypted payloads and stored envelopes.
|
|
16
|
+
- Browser-only in-memory session, inactivity lock, storage namespace inspection, and recovery kit UI
|
|
17
|
+
helpers.
|
|
18
|
+
- Optional React session and status helpers.
|
|
19
|
+
- Plaintext leak guards and testing sentinels.
|
|
20
|
+
|
|
21
|
+
The consuming application owns:
|
|
22
|
+
|
|
23
|
+
- Account authentication and authorization.
|
|
24
|
+
- API routes, database schemas, persistence, rate limiting, and audit logging.
|
|
25
|
+
- The product-specific plaintext payload schema and migrations.
|
|
26
|
+
- WebAuthn registration and authentication ceremonies.
|
|
27
|
+
- UI, password policy, recovery education, and destructive recovery decisions.
|
|
28
|
+
|
|
29
|
+
Account login must never unlock the vault. Account password reset must never replace vault recovery.
|
|
30
|
+
|
|
31
|
+
## 2. Requirements and installation
|
|
32
|
+
|
|
33
|
+
- Node.js 20 or newer for build, SSR, and tests.
|
|
34
|
+
- Web Crypto (`globalThis.crypto.subtle`) in the runtime performing encryption.
|
|
35
|
+
- React 18 or newer only when using `@tgoliveira/vault-core/react`.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @tgoliveira/vault-core
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use only documented package entry points:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { createUserVaultKey } from "@tgoliveira/vault-core";
|
|
45
|
+
import { unlockVaultSession } from "@tgoliveira/vault-core/browser";
|
|
46
|
+
import { VaultSessionProvider } from "@tgoliveira/vault-core/react";
|
|
47
|
+
import { SENTINEL_VAULT_PASSWORD } from "@tgoliveira/vault-core/testing";
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Do not import internal `dist/*` files. They are not stable public APIs.
|
|
51
|
+
|
|
52
|
+
## 3. Freeze the application crypto profile
|
|
53
|
+
|
|
54
|
+
Choose profile strings once, before production data exists:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import type { VaultCryptoProfile } from "@tgoliveira/vault-core";
|
|
58
|
+
|
|
59
|
+
export const VAULT_PROFILE: VaultCryptoProfile = {
|
|
60
|
+
cryptoVersion: "vault-v1",
|
|
61
|
+
aadContextVault: "acme:vault:v1",
|
|
62
|
+
aadContextEnvelope: "acme:vault-envelope:v1",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function vaultScope(userId: string) {
|
|
66
|
+
return { userId, resourceId: userId };
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Both identifiers must be UUID strings when data is validated by `encryptedPayloadSchema`. A profile
|
|
71
|
+
change makes existing high-level decrypt and unlock operations fail by design. Treat profile strings
|
|
72
|
+
as persisted protocol constants, not environment labels.
|
|
73
|
+
|
|
74
|
+
For multi-resource vaults, use the authenticated resource identifier as `resourceId`. Always pass the
|
|
75
|
+
same expected scope back during decrypt or unlock.
|
|
76
|
+
|
|
77
|
+
## 4. Persisted data model
|
|
78
|
+
|
|
79
|
+
A minimal server record contains only encrypted structures and non-secret status metadata:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import type {
|
|
83
|
+
EncryptedVaultPayload,
|
|
84
|
+
PasskeyPrfEnvelope,
|
|
85
|
+
PasswordEnvelope,
|
|
86
|
+
RecoveryPhraseEnvelope,
|
|
87
|
+
} from "@tgoliveira/vault-core";
|
|
88
|
+
|
|
89
|
+
export type StoredVaultRecord = {
|
|
90
|
+
cryptoVersion: "vault-v1";
|
|
91
|
+
encryptedBlob: EncryptedVaultPayload;
|
|
92
|
+
passwordEnvelope: PasswordEnvelope;
|
|
93
|
+
recoveryEnvelope: RecoveryPhraseEnvelope;
|
|
94
|
+
passkeyPrfEnvelope?: PasskeyPrfEnvelope | null;
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The server may store these structures because ciphertext, IV, salt, bounded KDF metadata, and AAD are
|
|
99
|
+
not plaintext secrets. The server must never receive the vault password, recovery phrase, UVK, PRF
|
|
100
|
+
output, or decrypted payload.
|
|
101
|
+
|
|
102
|
+
Validate records at every untrusted boundary:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { vaultSetupEnvelopeFieldsSchema } from "@tgoliveira/vault-core";
|
|
106
|
+
|
|
107
|
+
const record = vaultSetupEnvelopeFieldsSchema.parse(untrustedDatabaseValue);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The envelope schemas are discriminated by `method`. Password and recovery envelopes require
|
|
111
|
+
Argon2id metadata; passkey PRF envelopes require `kdfMetadata: null`.
|
|
112
|
+
|
|
113
|
+
## 5. Initial vault setup
|
|
114
|
+
|
|
115
|
+
Run the complete setup flow in a trusted client runtime:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import {
|
|
119
|
+
createPasskeyPrfEnvelope,
|
|
120
|
+
createPasswordEnvelope,
|
|
121
|
+
createRecoveryEnvelope,
|
|
122
|
+
createRecoveryPhrase,
|
|
123
|
+
createUserVaultKey,
|
|
124
|
+
encryptVaultPayload,
|
|
125
|
+
vaultSetupEnvelopeFieldsSchema,
|
|
126
|
+
} from "@tgoliveira/vault-core";
|
|
127
|
+
import { VAULT_PROFILE, vaultScope } from "./vault-profile.js";
|
|
128
|
+
|
|
129
|
+
export async function createInitialVault<T>(input: {
|
|
130
|
+
userId: string;
|
|
131
|
+
vaultPassword: string;
|
|
132
|
+
initialPayload: T;
|
|
133
|
+
passkeyPrfOutput?: Uint8Array;
|
|
134
|
+
}) {
|
|
135
|
+
const scope = vaultScope(input.userId);
|
|
136
|
+
const vaultKey = await createUserVaultKey();
|
|
137
|
+
const recoveryPhrase = createRecoveryPhrase({ wordCount: 24 });
|
|
138
|
+
|
|
139
|
+
const { envelope: passwordEnvelope } = await createPasswordEnvelope(
|
|
140
|
+
vaultKey,
|
|
141
|
+
input.vaultPassword,
|
|
142
|
+
scope,
|
|
143
|
+
VAULT_PROFILE
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const { envelope: recoveryEnvelope } = await createRecoveryEnvelope(
|
|
147
|
+
vaultKey,
|
|
148
|
+
recoveryPhrase,
|
|
149
|
+
scope,
|
|
150
|
+
VAULT_PROFILE,
|
|
151
|
+
{ phraseLength: 24 }
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const passkeyPrfEnvelope = input.passkeyPrfOutput
|
|
155
|
+
? await createPasskeyPrfEnvelope(
|
|
156
|
+
vaultKey,
|
|
157
|
+
input.passkeyPrfOutput,
|
|
158
|
+
scope,
|
|
159
|
+
VAULT_PROFILE
|
|
160
|
+
)
|
|
161
|
+
: null;
|
|
162
|
+
|
|
163
|
+
const encryptedBlob = await encryptVaultPayload(
|
|
164
|
+
input.initialPayload,
|
|
165
|
+
vaultKey,
|
|
166
|
+
scope,
|
|
167
|
+
VAULT_PROFILE
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const serverRecord = vaultSetupEnvelopeFieldsSchema.parse({
|
|
171
|
+
cryptoVersion: "vault-v1",
|
|
172
|
+
encryptedBlob,
|
|
173
|
+
passwordEnvelope,
|
|
174
|
+
recoveryEnvelope,
|
|
175
|
+
passkeyPrfEnvelope,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
serverRecord,
|
|
180
|
+
recoveryPhrase,
|
|
181
|
+
clientOnlyVaultKey: vaultKey,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Send only `serverRecord` to the server. Keep `recoveryPhrase` in the recovery confirmation UI and
|
|
187
|
+
`clientOnlyVaultKey` in the in-memory session. Never serialize either value into analytics, logs,
|
|
188
|
+
URLs, cookies, localStorage, IndexedDB, server actions, or API requests.
|
|
189
|
+
|
|
190
|
+
Argon2id work is deliberately sequential in this example to avoid doubling peak browser memory.
|
|
191
|
+
|
|
192
|
+
## 6. Recovery phrase confirmation and kit
|
|
193
|
+
|
|
194
|
+
Generate the required confirmation prompts and reject partial answers:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import {
|
|
198
|
+
assertRecoveryPhraseWordConfirmation,
|
|
199
|
+
createRecoveryKitText,
|
|
200
|
+
getRecoveryConfirmationPromptCount,
|
|
201
|
+
pickRecoveryConfirmationIndices,
|
|
202
|
+
} from "@tgoliveira/vault-core";
|
|
203
|
+
|
|
204
|
+
const words = recoveryPhrase.split(" ");
|
|
205
|
+
const count = getRecoveryConfirmationPromptCount(24);
|
|
206
|
+
const requiredIndices = pickRecoveryConfirmationIndices(words.length, count);
|
|
207
|
+
|
|
208
|
+
assertRecoveryPhraseWordConfirmation(
|
|
209
|
+
recoveryPhrase,
|
|
210
|
+
answersByOneBasedIndex,
|
|
211
|
+
requiredIndices
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const recoveryKit = createRecoveryKitText({
|
|
215
|
+
recoveryPhrase,
|
|
216
|
+
wordCount: 24,
|
|
217
|
+
productName: "Acme",
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
In a browser, `createRecoveryKitDownload()` and `printRecoveryKitContent()` are available from the
|
|
222
|
+
browser entry. Explain that anyone holding the phrase can unlock the vault. Do not automatically save
|
|
223
|
+
the kit to cloud storage.
|
|
224
|
+
|
|
225
|
+
## 7. Password unlock
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import {
|
|
229
|
+
decryptVaultPayload,
|
|
230
|
+
encryptedPayloadSchema,
|
|
231
|
+
passwordEnvelopeSchema,
|
|
232
|
+
unlockWithPasswordEnvelope,
|
|
233
|
+
} from "@tgoliveira/vault-core";
|
|
234
|
+
import { VAULT_PROFILE, vaultScope } from "./vault-profile.js";
|
|
235
|
+
|
|
236
|
+
export async function unlockWithPassword<T>(input: {
|
|
237
|
+
userId: string;
|
|
238
|
+
vaultPassword: string;
|
|
239
|
+
passwordEnvelope: unknown;
|
|
240
|
+
encryptedBlob: unknown;
|
|
241
|
+
}) {
|
|
242
|
+
const scope = vaultScope(input.userId);
|
|
243
|
+
const envelope = passwordEnvelopeSchema.parse(input.passwordEnvelope);
|
|
244
|
+
const encryptedBlob = encryptedPayloadSchema.parse(input.encryptedBlob);
|
|
245
|
+
const vaultKey = await unlockWithPasswordEnvelope(
|
|
246
|
+
input.vaultPassword,
|
|
247
|
+
envelope,
|
|
248
|
+
scope,
|
|
249
|
+
VAULT_PROFILE
|
|
250
|
+
);
|
|
251
|
+
const payload = await decryptVaultPayload<T>(
|
|
252
|
+
encryptedBlob,
|
|
253
|
+
vaultKey,
|
|
254
|
+
scope,
|
|
255
|
+
VAULT_PROFILE
|
|
256
|
+
);
|
|
257
|
+
return { vaultKey, payload };
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Do not expose whether a password failed during KDF derivation versus AES-GCM authentication. Present
|
|
262
|
+
a generic unlock failure to the user. UI throttling can improve local UX but cannot prevent offline
|
|
263
|
+
attacks against copied envelopes, so require a strong vault password and protect ciphertext access.
|
|
264
|
+
|
|
265
|
+
## 8. Recovery phrase unlock
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import {
|
|
269
|
+
decryptVaultPayload,
|
|
270
|
+
encryptedPayloadSchema,
|
|
271
|
+
parseRecoveryPhraseWordCount,
|
|
272
|
+
recoveryPhraseEnvelopeSchema,
|
|
273
|
+
unlockWithRecoveryEnvelope,
|
|
274
|
+
} from "@tgoliveira/vault-core";
|
|
275
|
+
|
|
276
|
+
const envelope = recoveryPhraseEnvelopeSchema.parse(serverRecord.recoveryEnvelope);
|
|
277
|
+
const expectedWordCount = parseRecoveryPhraseWordCount(envelope.publicMetadata);
|
|
278
|
+
const vaultKey = await unlockWithRecoveryEnvelope(
|
|
279
|
+
enteredRecoveryPhrase,
|
|
280
|
+
envelope,
|
|
281
|
+
vaultScope(userId),
|
|
282
|
+
VAULT_PROFILE,
|
|
283
|
+
{ expectedWordCount }
|
|
284
|
+
);
|
|
285
|
+
const payload = await decryptVaultPayload(
|
|
286
|
+
encryptedPayloadSchema.parse(serverRecord.encryptedBlob),
|
|
287
|
+
vaultKey,
|
|
288
|
+
vaultScope(userId),
|
|
289
|
+
VAULT_PROFILE
|
|
290
|
+
);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
After successful recovery, let the user create a new password envelope around the same UVK and
|
|
294
|
+
replace the old password envelope atomically on the server.
|
|
295
|
+
|
|
296
|
+
## 9. Passkey PRF integration
|
|
297
|
+
|
|
298
|
+
The package does not run WebAuthn ceremonies. The application must request the PRF extension and pass
|
|
299
|
+
the first PRF result to vault-core.
|
|
300
|
+
|
|
301
|
+
Use a stable, application-specific PRF salt:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import {
|
|
305
|
+
buildPrfSaltBytes,
|
|
306
|
+
extractPasskeyPrfOutput,
|
|
307
|
+
isPasskeySupported,
|
|
308
|
+
isPrfExtensionSupported,
|
|
309
|
+
} from "@tgoliveira/vault-core/browser";
|
|
310
|
+
|
|
311
|
+
const salt = await buildPrfSaltBytes("acme-passkey-prf-v1:", userId);
|
|
312
|
+
|
|
313
|
+
if (!isPasskeySupported() || !isPrfExtensionSupported()) {
|
|
314
|
+
// Offer password or recovery phrase unlock instead.
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const credential = await navigator.credentials.get({
|
|
318
|
+
publicKey: {
|
|
319
|
+
...applicationOwnedRequestOptions,
|
|
320
|
+
extensions: { prf: { eval: { first: salt } } },
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
if (!(credential instanceof PublicKeyCredential)) {
|
|
325
|
+
throw new Error("Passkey ceremony did not return a public-key credential");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const prfOutput = extractPasskeyPrfOutput(
|
|
329
|
+
credential.getClientExtensionResults()
|
|
330
|
+
);
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Exact WebAuthn option typing and credential verification belong to the application. Never send
|
|
334
|
+
`prfOutput` to the server.
|
|
335
|
+
|
|
336
|
+
Unlock after obtaining the PRF output:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import {
|
|
340
|
+
passkeyPrfEnvelopeSchema,
|
|
341
|
+
unlockWithPasskeyPrfEnvelope,
|
|
342
|
+
} from "@tgoliveira/vault-core";
|
|
343
|
+
|
|
344
|
+
const envelope = passkeyPrfEnvelopeSchema.parse(serverRecord.passkeyPrfEnvelope);
|
|
345
|
+
const vaultKey = await unlockWithPasskeyPrfEnvelope(
|
|
346
|
+
envelope,
|
|
347
|
+
prfOutput,
|
|
348
|
+
vaultScope(userId),
|
|
349
|
+
VAULT_PROFILE
|
|
350
|
+
);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Treat PRF support as an optional unlock method. Always preserve password or recovery fallback.
|
|
354
|
+
|
|
355
|
+
## 10. Save and update encrypted payloads
|
|
356
|
+
|
|
357
|
+
Keep the typed product schema in the application:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import { z } from "zod";
|
|
361
|
+
import { encryptVaultPayload } from "@tgoliveira/vault-core";
|
|
362
|
+
|
|
363
|
+
const appVaultPayloadSchema = z.object({
|
|
364
|
+
version: z.literal(1),
|
|
365
|
+
entries: z.array(z.object({ id: z.string(), secret: z.string() })),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const validatedPayload = appVaultPayloadSchema.parse(nextPayload);
|
|
369
|
+
const encryptedBlob = await encryptVaultPayload(
|
|
370
|
+
validatedPayload,
|
|
371
|
+
inMemoryVaultKey,
|
|
372
|
+
vaultScope(userId),
|
|
373
|
+
VAULT_PROFILE
|
|
374
|
+
);
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Persist only `encryptedBlob`. Use application-owned optimistic concurrency or record versions to
|
|
378
|
+
prevent lost updates and ciphertext rollback. Vault-core authenticates content and AAD but does not
|
|
379
|
+
provide server freshness or synchronization.
|
|
380
|
+
|
|
381
|
+
## 11. Browser session without React
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
import {
|
|
385
|
+
configureVaultSession,
|
|
386
|
+
getSessionVaultKey,
|
|
387
|
+
lockVaultSession,
|
|
388
|
+
registerVaultActivityGuard,
|
|
389
|
+
registerVaultUnloadGuard,
|
|
390
|
+
unlockVaultSession,
|
|
391
|
+
} from "@tgoliveira/vault-core/browser";
|
|
392
|
+
|
|
393
|
+
configureVaultSession({ autoLockMinutes: 15 });
|
|
394
|
+
|
|
395
|
+
const removeActivityGuard = registerVaultActivityGuard();
|
|
396
|
+
const removeUnloadGuard = registerVaultUnloadGuard();
|
|
397
|
+
|
|
398
|
+
unlockVaultSession(vaultKey);
|
|
399
|
+
const currentKey = getSessionVaultKey();
|
|
400
|
+
|
|
401
|
+
// On explicit lock or logout:
|
|
402
|
+
lockVaultSession();
|
|
403
|
+
|
|
404
|
+
// On application teardown:
|
|
405
|
+
removeActivityGuard();
|
|
406
|
+
removeUnloadGuard();
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
There is no public direct key setter. This ensures unlock, lock, timers, and subscribers remain in
|
|
410
|
+
sync. Call `touchVaultSession()` manually only for meaningful activity not represented by the default
|
|
411
|
+
pointer, keyboard, touch, or focus events.
|
|
412
|
+
|
|
413
|
+
## 12. React session integration
|
|
414
|
+
|
|
415
|
+
Mount one provider near the client application root:
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
import type { ReactNode } from "react";
|
|
419
|
+
import { VaultSessionProvider } from "@tgoliveira/vault-core/react";
|
|
420
|
+
|
|
421
|
+
export function AppProviders({ children }: { children: ReactNode }) {
|
|
422
|
+
return (
|
|
423
|
+
<VaultSessionProvider
|
|
424
|
+
sessionConfig={{ autoLockMinutes: 15 }}
|
|
425
|
+
registerActivityGuard
|
|
426
|
+
registerUnloadGuard
|
|
427
|
+
>
|
|
428
|
+
{children}
|
|
429
|
+
</VaultSessionProvider>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Read and control state:
|
|
435
|
+
|
|
436
|
+
```tsx
|
|
437
|
+
import {
|
|
438
|
+
useVaultClientStatus,
|
|
439
|
+
useVaultSession,
|
|
440
|
+
useVaultUnlocked,
|
|
441
|
+
} from "@tgoliveira/vault-core/react";
|
|
442
|
+
|
|
443
|
+
const unlocked = useVaultUnlocked();
|
|
444
|
+
const { lock, touch } = useVaultSession({
|
|
445
|
+
registerActivityGuard: false,
|
|
446
|
+
registerUnloadGuard: false,
|
|
447
|
+
});
|
|
448
|
+
const status = useVaultClientStatus(serverStatus, browserSupportsPrf);
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Avoid mounting both `VaultSessionProvider` and a default `useVaultSession()` solely to register the
|
|
452
|
+
same guards. When the provider owns guards, use the hook with guard registration disabled or call the
|
|
453
|
+
browser lifecycle functions directly.
|
|
454
|
+
|
|
455
|
+
## 13. Storage policy and inspection
|
|
456
|
+
|
|
457
|
+
Do not persist decrypted payloads or the UVK. Storage helpers inspect namespace presence; they cannot
|
|
458
|
+
classify arbitrary record contents:
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
import {
|
|
462
|
+
inspectIndexedDBPrefix,
|
|
463
|
+
inspectLocalStoragePrefix,
|
|
464
|
+
} from "@tgoliveira/vault-core/browser";
|
|
465
|
+
|
|
466
|
+
const localResult = inspectLocalStoragePrefix("acme:vault:");
|
|
467
|
+
const idbResult = await inspectIndexedDBPrefix("acme-vault-");
|
|
468
|
+
|
|
469
|
+
if (localResult !== "clear" || idbResult !== "clear") {
|
|
470
|
+
// Investigate "found" and treat "unavailable" as a failed security check.
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
`inspectIndexedDBPrefix()` checks database names, not object-store contents. Enforce the real no-
|
|
475
|
+
plaintext rule through architecture, code review, CSP/XSS controls, and sentinel-based integration
|
|
476
|
+
tests.
|
|
477
|
+
|
|
478
|
+
## 14. Server request validation
|
|
479
|
+
|
|
480
|
+
Reject known plaintext fields recursively before accepting an encrypted vault request:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
import {
|
|
484
|
+
assertNoVaultPlaintextFields,
|
|
485
|
+
vaultSetupEnvelopeFieldsSchema,
|
|
486
|
+
} from "@tgoliveira/vault-core";
|
|
487
|
+
|
|
488
|
+
export function parseVaultSetupRequest(body: unknown) {
|
|
489
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
490
|
+
throw new Error("Vault request body must be an object");
|
|
491
|
+
}
|
|
492
|
+
assertNoVaultPlaintextFields(body as Record<string, unknown>);
|
|
493
|
+
return vaultSetupEnvelopeFieldsSchema.parse(body);
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
This guard is defense in depth, not a complete data-loss-prevention system. Use closed route schemas,
|
|
498
|
+
never log request bodies, and verify the authenticated account owns the AAD `userId` and resource.
|
|
499
|
+
|
|
500
|
+
## 15. Password and recovery rotation
|
|
501
|
+
|
|
502
|
+
To change a vault password:
|
|
503
|
+
|
|
504
|
+
1. Unlock the existing envelope and obtain the UVK in memory.
|
|
505
|
+
2. Call `createPasswordEnvelope()` with the new password, same expected scope, and same profile.
|
|
506
|
+
3. Atomically replace only the password envelope on the server.
|
|
507
|
+
4. Keep the encrypted payload and other envelopes unchanged.
|
|
508
|
+
|
|
509
|
+
To rotate a recovery phrase, generate a new phrase and recovery envelope around the same UVK. Require
|
|
510
|
+
confirmation and replace the old recovery envelope atomically. Deleting an envelope is an application
|
|
511
|
+
authorization decision.
|
|
512
|
+
|
|
513
|
+
## 16. Error handling
|
|
514
|
+
|
|
515
|
+
Expected domain errors include:
|
|
516
|
+
|
|
517
|
+
- `VaultPlaintextRejectionError`
|
|
518
|
+
- `PasskeyPrfRequiredError`
|
|
519
|
+
- `PasskeyUnlockError`
|
|
520
|
+
- `RecoveryPhraseConfirmationError`
|
|
521
|
+
- `VaultConflictError`
|
|
522
|
+
- `VaultNotFoundError`
|
|
523
|
+
|
|
524
|
+
Web Crypto, JSON parsing, Zod, and Argon2id validation may also throw standard errors. Convert detailed
|
|
525
|
+
internal failures into generic user-facing unlock messages. Never include entered secrets, decrypted
|
|
526
|
+
data, PRF bytes, or full encrypted request bodies in logs.
|
|
527
|
+
|
|
528
|
+
## 17. Testing a consuming application
|
|
529
|
+
|
|
530
|
+
Use the testing entry to prove plaintext never crosses persistence or network boundaries:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
import {
|
|
534
|
+
SENTINEL_PRIVATE_NOTE,
|
|
535
|
+
SENTINEL_VAULT_PASSWORD,
|
|
536
|
+
validateNoPlaintextLeak,
|
|
537
|
+
} from "@tgoliveira/vault-core/testing";
|
|
538
|
+
|
|
539
|
+
const result = validateNoPlaintextLeak(capturedRequestBody);
|
|
540
|
+
expect(result.ok).toBe(true);
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Required integration tests:
|
|
544
|
+
|
|
545
|
+
- Password, recovery, and passkey round trips.
|
|
546
|
+
- Wrong password, wrong phrase, missing PRF, tampered ciphertext, and wrong expected AAD.
|
|
547
|
+
- API bodies contain no password, phrase, UVK, PRF output, or product plaintext sentinels.
|
|
548
|
+
- localStorage and IndexedDB contain no decrypted vault state.
|
|
549
|
+
- Auto-lock clears the in-memory key and updates subscribed UI.
|
|
550
|
+
- Stored legacy fixtures still decrypt through the documented migration path.
|
|
551
|
+
|
|
552
|
+
## 18. Legacy ciphertext
|
|
553
|
+
|
|
554
|
+
High-level APIs require the configured AAD context. For a legacy record with missing context:
|
|
555
|
+
|
|
556
|
+
1. Use `decryptField()` only inside an explicit migration path.
|
|
557
|
+
2. Validate every available AAD field against the authenticated user and resource.
|
|
558
|
+
3. Parse and validate the decrypted product payload.
|
|
559
|
+
4. Re-encrypt immediately with `encryptVaultPayload()` and the frozen profile.
|
|
560
|
+
5. Remove the compatibility path after migration completes.
|
|
561
|
+
|
|
562
|
+
Never make missing AAD context a permanent high-level fallback.
|
|
563
|
+
|
|
564
|
+
## 19. Production readiness checklist
|
|
565
|
+
|
|
566
|
+
- [ ] Profile strings are unique, stable, documented, and frozen.
|
|
567
|
+
- [ ] User and resource IDs passed to AAD match authenticated ownership.
|
|
568
|
+
- [ ] Product payloads are validated before encryption and after decryption.
|
|
569
|
+
- [ ] Server routes accept only runtime-validated encrypted structures.
|
|
570
|
+
- [ ] Password, phrase, UVK, PRF output, and decrypted payload never reach the server or logs.
|
|
571
|
+
- [ ] Decrypted data is absent from localStorage, IndexedDB, cookies, URLs, and analytics.
|
|
572
|
+
- [ ] Password and recovery unlock remain available if passkey PRF is unsupported.
|
|
573
|
+
- [ ] Recovery confirmation and offline storage education are implemented.
|
|
574
|
+
- [ ] In-memory sessions auto-lock and clear on `pagehide`.
|
|
575
|
+
- [ ] Rotation and recovery updates are atomic and authorization-protected.
|
|
576
|
+
- [ ] Wrong-AAD, tamper, leak, storage, and auto-lock tests pass.
|
|
577
|
+
- [ ] The application pins a compatible package version and reviews `CHANGELOG.md` before upgrades.
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Documentation Index
|
|
2
|
+
|
|
3
|
+
Use this page as the documentation router for `@tgoliveira/vault-core`.
|
|
4
|
+
|
|
5
|
+
## Consumers
|
|
6
|
+
|
|
7
|
+
- [`IMPLEMENTATION_GUIDE.md`](IMPLEMENTATION_GUIDE.md): complete greenfield implementation from
|
|
8
|
+
setup through password, recovery phrase, passkey PRF, persistence boundaries, and sessions.
|
|
9
|
+
- [`ADOPTING_VAULT_CORE_IN_EXISTING_APPS.md`](ADOPTING_VAULT_CORE_IN_EXISTING_APPS.md): phased
|
|
10
|
+
migration for an application that already has vault code or stored ciphertext.
|
|
11
|
+
- [`../API_REFERENCE.md`](../API_REFERENCE.md): public entry points and their security preconditions.
|
|
12
|
+
- [`../SECURITY.md`](../SECURITY.md): threat model, secret boundaries, and storage rules.
|
|
13
|
+
|
|
14
|
+
## Maintainers and agents
|
|
15
|
+
|
|
16
|
+
- [`../AGENTS.md`](../AGENTS.md): repository operating rules and definition of done.
|
|
17
|
+
- [`RELEASING.md`](RELEASING.md): versioning, changelog, tag, validation, and publication procedure.
|
|
18
|
+
- [`../CHANGELOG.md`](../CHANGELOG.md): released and unreleased consumer-visible changes.
|
|
19
|
+
- [`../ARCHITECTURE.md`](../ARCHITECTURE.md): package layers and cryptographic data flow.
|
|
20
|
+
|
|
21
|
+
## Topic references
|
|
22
|
+
|
|
23
|
+
- [`../PASSWORD_ENVELOPES.md`](../PASSWORD_ENVELOPES.md)
|
|
24
|
+
- [`../RECOVERY_PHRASE.md`](../RECOVERY_PHRASE.md)
|
|
25
|
+
- [`../PASSKEY_PRF_ENVELOPES.md`](../PASSKEY_PRF_ENVELOPES.md)
|
|
26
|
+
- [`../MIGRATION_FROM_LIQSENSE.md`](../MIGRATION_FROM_LIQSENSE.md)
|
|
27
|
+
|
|
28
|
+
If documentation and implementation disagree, treat the TypeScript declarations and runtime tests as
|
|
29
|
+
the immediate source of truth, then fix the documentation in the same change.
|
|
30
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Release Process
|
|
2
|
+
|
|
3
|
+
Releases are initiated manually, but version calculation, validation, npm publication, Git commit,
|
|
4
|
+
Git tag, and GitHub release creation are automated. Do not create release tags manually.
|
|
5
|
+
|
|
6
|
+
## Version policy
|
|
7
|
+
|
|
8
|
+
This package follows Semantic Versioning. While the major version is `0`:
|
|
9
|
+
|
|
10
|
+
- Patch releases contain backward-compatible fixes or documentation-only changes.
|
|
11
|
+
- Minor releases contain new features or any breaking public API change.
|
|
12
|
+
- After `1.0.0`, breaking changes increment the major version.
|
|
13
|
+
|
|
14
|
+
Every consumer-visible change belongs under `CHANGELOG.md` → `Unreleased` in one of these sections:
|
|
15
|
+
|
|
16
|
+
- `Added`
|
|
17
|
+
- `Changed`
|
|
18
|
+
- `Deprecated`
|
|
19
|
+
- `Removed`
|
|
20
|
+
- `Fixed`
|
|
21
|
+
- `Security`
|
|
22
|
+
|
|
23
|
+
Mark breaking changes with `**Breaking:**` and include a migration path.
|
|
24
|
+
|
|
25
|
+
## One-time repository and npm setup
|
|
26
|
+
|
|
27
|
+
1. Create a protected GitHub environment named `npmjs`.
|
|
28
|
+
2. Add required reviewers to that environment if publication needs human approval.
|
|
29
|
+
3. Allow the GitHub Actions bot to push the release commit and `vault-core-v*` tags to `main`, or
|
|
30
|
+
provide an equivalently scoped GitHub App token if branch protection forbids `GITHUB_TOKEN` pushes.
|
|
31
|
+
4. In the npm package settings, configure a GitHub Actions trusted publisher with:
|
|
32
|
+
- Repository owner: the GitHub owner of this repository.
|
|
33
|
+
- Repository: `vault-core`.
|
|
34
|
+
- Workflow filename: `publish-vault-core.yml`.
|
|
35
|
+
- Environment: `npmjs`.
|
|
36
|
+
- Allowed action: `npm publish`.
|
|
37
|
+
5. After one successful OIDC publication, remove the legacy `NPM_TOKEN` secret and disallow token
|
|
38
|
+
publishing in npm package settings. The workflow retains token fallback only for migration.
|
|
39
|
+
|
|
40
|
+
Trusted publishing requires a GitHub-hosted runner, Node 22.14 or newer, npm 11.5.1 or newer, and
|
|
41
|
+
`id-token: write`. The workflow uses Node 24 and verifies the npm version before continuing.
|
|
42
|
+
|
|
43
|
+
## Start a release
|
|
44
|
+
|
|
45
|
+
Use the GitHub Actions UI:
|
|
46
|
+
|
|
47
|
+
1. Open **Actions** → **Publish package to npmjs**.
|
|
48
|
+
2. Select **Run workflow** on `main`.
|
|
49
|
+
3. Leave `version` blank for automatic versioning, or enter one of:
|
|
50
|
+
- An exact stable version such as `0.3.0`.
|
|
51
|
+
- `patch`, `minor`, or `major`.
|
|
52
|
+
|
|
53
|
+
Equivalent GitHub CLI commands:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Automatic version
|
|
57
|
+
gh workflow run publish-vault-core.yml --ref main
|
|
58
|
+
|
|
59
|
+
# Exact or explicit bump
|
|
60
|
+
gh workflow run publish-vault-core.yml --ref main -f version=0.3.0
|
|
61
|
+
gh workflow run publish-vault-core.yml --ref main -f version=patch
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Automatic version calculation
|
|
65
|
+
|
|
66
|
+
When `version` is blank or `auto`, `scripts/prepare-release.mjs` reads the `Unreleased` changelog:
|
|
67
|
+
|
|
68
|
+
1. Any `**Breaking:**` entry selects major, or minor while the current major version is `0`.
|
|
69
|
+
2. Otherwise, at least one `Added` entry selects minor.
|
|
70
|
+
3. Otherwise, the release selects patch.
|
|
71
|
+
|
|
72
|
+
When `Unreleased` is empty, the workflow enters recovery mode for the current released version. It
|
|
73
|
+
completes missing npm, tag, or GitHub release state without publishing a duplicate.
|
|
74
|
+
|
|
75
|
+
## Publication gates and ordering
|
|
76
|
+
|
|
77
|
+
The workflow serializes releases so two versions cannot publish concurrently. It then:
|
|
78
|
+
|
|
79
|
+
1. Checks out `main` with full tag history.
|
|
80
|
+
2. Installs the exact lockfile with `npm ci`.
|
|
81
|
+
3. Runs `npm audit` at the `high` threshold, which also blocks critical vulnerabilities.
|
|
82
|
+
4. Calculates the version and moves `Unreleased` entries to a dated release section.
|
|
83
|
+
5. Runs typecheck, all tests, per-file coverage gates, and the production build.
|
|
84
|
+
6. Builds one tarball that becomes the exact validated publication artifact.
|
|
85
|
+
7. Rejects version collisions or inconsistent pre-existing tags.
|
|
86
|
+
8. Commits `package.json`, `package-lock.json`, and `CHANGELOG.md` as `Release x.y.z`.
|
|
87
|
+
9. Publishes to npm with OIDC/provenance when trusted publishing is configured.
|
|
88
|
+
10. Creates and pushes `vault-core-vx.y.z` only after npm publication succeeds.
|
|
89
|
+
11. Creates GitHub release notes and a workflow summary.
|
|
90
|
+
|
|
91
|
+
The npm registry is immutable: a published version cannot be overwritten. If publication succeeds
|
|
92
|
+
but tag or GitHub release creation fails, rerun the workflow with a blank version. Recovery mode
|
|
93
|
+
detects the released changelog version, skips duplicate npm publication, and completes missing Git
|
|
94
|
+
metadata.
|
|
95
|
+
|
|
96
|
+
## Post-release verification
|
|
97
|
+
|
|
98
|
+
- Confirm npmjs shows the expected version and provenance badge.
|
|
99
|
+
- Confirm all four package entry points resolve.
|
|
100
|
+
- Confirm README, changelog, and implementation guide render on npmjs.
|
|
101
|
+
- Confirm `vault-core-vx.y.z` and the GitHub release point to the release commit.
|
|
102
|
+
- Confirm `CHANGELOG.md` has a new empty `Unreleased` section.
|