rootcard 0.2.1 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rootcard",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "A portable, self-certifying identity for online logins (core library, zero dependencies, WebCrypto)",
5
5
  "keywords": [
6
6
  "identity",
@@ -11,13 +11,13 @@
11
11
  "self-certifying"
12
12
  ],
13
13
  "license": "(MIT OR Apache-2.0)",
14
- "homepage": "https://github.com/Hexagon/rootcard",
14
+ "homepage": "https://github.com/rootcard/rootcard",
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "git+https://github.com/Hexagon/rootcard.git",
17
+ "url": "git+https://github.com/rootcard/rootcard.git",
18
18
  "directory": "packages/rootcard"
19
19
  },
20
- "bugs": "https://github.com/Hexagon/rootcard/issues",
20
+ "bugs": "https://github.com/rootcard/rootcard/issues",
21
21
  "type": "module",
22
22
  "exports": {
23
23
  ".": "./src/index.js",
package/src/account.js CHANGED
@@ -6,21 +6,33 @@ import { base64urlEncode } from "./encoding.js";
6
6
  import { inception } from "./id.js";
7
7
  import { checkRecoveryKey, eventHash, pendingInstance, rotateRecoveryEvent, verifyLog } from "./log.js";
8
8
  import { signPublic } from "./profile.js";
9
- import { deriveRecoveryKeys } from "./recovery.js";
9
+ import { checkNewRecoveryKey, deriveRecoveryKeys } from "./recovery.js";
10
+ import { DRAW_ATTEMPTS, usableSeed } from "./crypto.js";
10
11
  import { recoveryCard } from "./file.js";
11
12
  import { carriedLocks, checkPassword, instanceVault, regrant } from "./vault.js";
12
13
 
14
+ /** The account seed of a new instance: 32 random bytes, drawn again while unusable ([CRY-9]). → bad_key (broken generator) */
15
+ function drawAccountSeed(random) {
16
+ for (let i = 0; i < DRAW_ATTEMPTS; i++) {
17
+ const seed = random(32);
18
+ if (usableSeed(seed)) return seed;
19
+ }
20
+ fail("bad_key");
21
+ }
22
+
13
23
  /**
14
- * Build a registration ([CLT-1]) from a recovery key the caller generated ([RK-3]). `home` is `{ key, url }` with
15
- * a key string. Draws: the account seed (32), then instanceVault with one new slot.
24
+ * Build a registration ([CLT-1]) from a recovery key the caller generated ([RK-3]; usable by [CRY-9], see
25
+ * newRecoveryKey → bad_key). `home` is `{ key, url }` with a key string. Draws: the account seed (32, again while
26
+ * unusable, [CRY-9]), then instanceVault with one new slot.
16
27
  * @returns {Promise<{ log, vault, public, card, account }>}
17
28
  */
18
29
  export async function register(recoveryKey, realm, username, password, slotLabel, home, time, kdf, random) {
19
30
  checkPassword(password, username);
20
31
  const keys = await deriveRecoveryKeys(recoveryKey, realm, username);
32
+ checkNewRecoveryKey(keys);
21
33
  const inc = await inception(keys, realm, username);
22
34
  const state = await verifyLog([inc], { realms: [realm] });
23
- const seed = random(32);
35
+ const seed = drawAccountSeed(random);
24
36
  const pending = await pendingInstance(state, keys.signSeed, seed, home, "register", time);
25
37
  const instance = base64urlEncode(await eventHash(pending));
26
38
  const vault = await instanceVault(
@@ -30,7 +42,7 @@ export async function register(recoveryKey, realm, username, password, slotLabel
30
42
  recovery: keys,
31
43
  contents: { accountSeed: seed, data: {} },
32
44
  keep: [],
33
- add: { label: slotLabel, password, kdf },
45
+ add: { label: slotLabel, password, username, kdf },
34
46
  },
35
47
  random,
36
48
  );
@@ -45,12 +57,19 @@ export async function register(recoveryKey, realm, username, password, slotLabel
45
57
  /**
46
58
  * Build a new instance on top of `state` ([KT-3]). `mode` is `{ restore: true }`, `{ revoke: [slot ids] }`,
47
59
  * `{ reset: slot }` or `{ fresh: slot }`, where a slot is `{ label, password, kdf }`. `old` is the current vault
48
- * unlocked with the recovery lock (not needed for fresh). Draws: the account seed (32), then instanceVault.
60
+ * unlocked with the recovery lock (not needed for fresh). Draws: the account seed (32, again while unusable,
61
+ * [CRY-9]), then instanceVault.
49
62
  * @returns {Promise<{ event, vault, public }>}
50
63
  */
51
64
  export async function newInstance(state, recovery, old, oldPublicData, mode, home, time, random) {
52
65
  await checkRecoveryKey(state, recovery.signSeed);
53
- const seed = random(32);
66
+ const slot = mode.reset ?? mode.fresh;
67
+ if (slot) {
68
+ // The new password is checked before anything is drawn ([LCK-8]); its username must be this identity's.
69
+ if (slot.username !== state.username) fail("bad_field");
70
+ checkPassword(slot.password, state.username);
71
+ }
72
+ const seed = drawAccountSeed(random);
54
73
  const reason = mode.restore ? "restore" : mode.fresh ? "fresh" : "revoke";
55
74
  const event = await pendingInstance(state, recovery.signSeed, seed, home, reason, time);
56
75
  const instance = base64urlEncode(await eventHash(event));
@@ -77,11 +96,12 @@ export async function newInstance(state, recovery, old, oldPublicData, mode, hom
77
96
  }
78
97
 
79
98
  /**
80
- * Rotate the recovery key ([KT-5]) to `newKey`. `unlocked` is the current vault unlocked. Draws: regrant.
99
+ * Rotate the recovery key ([KT-5]) to `newKey` (usable by [CRY-9] → bad_key; see newRecoveryKey). `unlocked` is the current vault unlocked. Draws: regrant.
81
100
  * @returns {Promise<{ event, vault, card }>}
82
101
  */
83
102
  export async function rotateRecovery(state, log, oldKeys, newKey, oldVault, unlocked, time, random) {
84
103
  const next = await deriveRecoveryKeys(newKey, state.realm, state.username);
104
+ checkNewRecoveryKey(next);
85
105
  const event = await rotateRecoveryEvent(state, oldKeys.signSeed, next.signSeed, time);
86
106
  const vault = await regrant(unlocked, oldVault, next, random);
87
107
  const card = await recoveryCard(state.realm, state.username, state.account, newKey, [...log, event]);
package/src/binding.js CHANGED
@@ -15,6 +15,11 @@ export function isVersionSegment(segment) {
15
15
  return /^[vV][0-9]+$/.test(segment);
16
16
  }
17
17
 
18
+ // A base URL path segment ([HST-22]): non-empty, only unreserved characters (no percent-encoding), not a dot segment.
19
+ function isPathSegment(segment) {
20
+ return /^[A-Za-z0-9\-._~]+$/.test(segment) && segment !== "." && segment !== "..";
21
+ }
22
+
18
23
  function authorityHost(rest) {
19
24
  const authority = rest.split("/")[0];
20
25
  if (authority.startsWith("[")) {
@@ -26,8 +31,8 @@ function authorityHost(rest) {
26
31
 
27
32
  /**
28
33
  * Check a base URL ([HST-22]): `https://` and a host, an optional port and an optional path without a trailing slash,
29
- * query, fragment or user info; plain `http://` only for `localhost`, `127.0.0.1` and `[::1]`; and its last path
30
- * segment is not an API version segment. → `bad_field`
34
+ * query, fragment or user info; plain `http://` only for `localhost`, `127.0.0.1` and `[::1]`; path segments of
35
+ * unreserved characters only, never `.` or `..`; and its last path segment is not an API version segment. → `bad_field`
31
36
  */
32
37
  export function checkBaseUrl(url) {
33
38
  checkUrl(url);
@@ -43,7 +48,7 @@ export function checkBaseUrl(url) {
43
48
  if (authorityHost(authority) === "" || !/^[A-Za-z0-9.\-:[\]]+$/.test(authority)) fail("bad_field");
44
49
  if (slash >= 0) {
45
50
  const segments = rest.slice(slash + 1).split("/");
46
- if (segments.some((x) => x === "")) fail("bad_field");
51
+ if (!segments.every(isPathSegment)) fail("bad_field");
47
52
  if (isVersionSegment(segments[segments.length - 1])) fail("bad_field");
48
53
  }
49
54
  }
package/src/crypto.js CHANGED
@@ -78,14 +78,76 @@ async function publicOf(prefix, alg, secret, usages) {
78
78
  return x;
79
79
  }
80
80
 
81
+ /** How often a seed is drawn again before a generator is taken to be broken ([CRY-9]). */
82
+ export const DRAW_ATTEMPTS = 64;
83
+
84
+ /** Whether an Ed25519 seed may be used for a new key: its first byte is not zero ([CRY-9]). */
85
+ export function usableSeed(seed) {
86
+ return seed[0] !== 0;
87
+ }
88
+
89
+ // [IMP-6]: WebKit's WebCrypto rejects a pkcs8 Ed25519 seed whose first byte is zero, but signs with it when it is
90
+ // imported as a JWK with its public key. Public keys of seeds checked against a key on record (ed25519PublicIs) are
91
+ // kept here, keyed by the seed's buffer, so that later signatures with the same buffer can use that import. Nothing
92
+ // here is secret, and nothing is added without the check.
93
+ const CHECKED = new WeakMap();
94
+
95
+ function b64u(bytes) {
96
+ let b = "";
97
+ for (const x of bytes) b += String.fromCharCode(x);
98
+ return btoa(b).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
99
+ }
100
+
101
+ async function importJwk(seed, publicKey, extractable) {
102
+ const jwk = { kty: "OKP", crv: "Ed25519", d: b64u(seed), x: b64u(publicKey), ext: extractable };
103
+ try {
104
+ return await subtle.importKey("jwk", jwk, { name: "Ed25519" }, extractable, ["sign"]);
105
+ } finally {
106
+ jwk.d = "";
107
+ }
108
+ }
109
+
81
110
  /** The Ed25519 public key of a 32-byte seed ([CRY-2]). */
82
111
  export async function ed25519Public(seed) {
83
- return publicOf(ED_PKCS8, "Ed25519", seed, ["sign"]);
112
+ try {
113
+ return await publicOf(ED_PKCS8, "Ed25519", seed, ["sign"]);
114
+ } catch (e) {
115
+ const known = CHECKED.get(seed);
116
+ if (known) return known.slice();
117
+ throw e;
118
+ }
84
119
  }
85
120
 
86
- /** Import an Ed25519 seed as a non-extractable signing key ([IMP-2]). */
121
+ /**
122
+ * Whether the Ed25519 public key of `seed` is `publicKey` (raw 32 bytes). Where the platform cannot import the seed
123
+ * ([IMP-6]), the import of the seed with `publicKey` answers it, since the platform rejects a public key that does
124
+ * not belong to the seed; the key is then remembered for signing with this seed buffer.
125
+ */
126
+ export async function ed25519PublicIs(seed, publicKey) {
127
+ let pub;
128
+ try {
129
+ pub = await publicOf(ED_PKCS8, "Ed25519", seed, ["sign"]);
130
+ } catch {
131
+ try {
132
+ await importJwk(seed, publicKey, false);
133
+ } catch {
134
+ return false;
135
+ }
136
+ CHECKED.set(seed, Uint8Array.from(publicKey));
137
+ return true;
138
+ }
139
+ return pub.length === publicKey.length && pub.every((b, i) => b === publicKey[i]);
140
+ }
141
+
142
+ /** Import an Ed25519 seed as a non-extractable signing key ([IMP-2], [IMP-6]). */
87
143
  export async function ed25519SigningKey(seed) {
88
- return importPrivate(ED_PKCS8, "Ed25519", seed, false, ["sign"]);
144
+ try {
145
+ return await importPrivate(ED_PKCS8, "Ed25519", seed, false, ["sign"]);
146
+ } catch (e) {
147
+ const known = CHECKED.get(seed);
148
+ if (known) return importJwk(seed, known, false);
149
+ throw e;
150
+ }
89
151
  }
90
152
 
91
153
  /** A deterministic Ed25519 signature ([CRY-8]). `key` is a seed or a key from ed25519SigningKey. */
@@ -150,20 +212,41 @@ export async function ed25519Verify(publicKey, m, sig) {
150
212
  if (!ok) fail("bad_signature");
151
213
  }
152
214
 
215
+ // An X25519 secret as WebCrypto imports it. WebKit's WebCrypto on Linux rejects a secret whose first byte is zero
216
+ // (DataError), probably because it handles the key as a big integer, so ~1 in 256 secrets failed there. X25519 clamps
217
+ // every secret (RFC 7748 §5: the low 3 bits of the first byte cleared, the top bit of the last cleared and the next
218
+ // set), so a zero first byte is given as 1 and a zero last byte as 0x40: the same scalar, the same keys and results.
219
+ function x25519Secret(secret) {
220
+ const c = Uint8Array.from(secret);
221
+ if (c.length === 32) {
222
+ if (c[0] === 0) c[0] = 1;
223
+ if (c[31] === 0) c[31] = 0x40;
224
+ }
225
+ return c;
226
+ }
227
+
153
228
  /** The X25519 public key of a 32-byte secret ([CRY-2]). */
154
229
  export async function x25519Public(secret) {
155
- return publicOf(X_PKCS8, "X25519", secret, ["deriveBits"]);
230
+ const c = x25519Secret(secret);
231
+ try {
232
+ return await publicOf(X_PKCS8, "X25519", c, ["deriveBits"]);
233
+ } finally {
234
+ c.fill(0);
235
+ }
156
236
  }
157
237
 
158
238
  /** X25519 key agreement; an all-zero result fails ([CRY-4]). → bad_key */
159
239
  export async function x25519(secret, publicKey) {
160
240
  let bits;
241
+ const c = x25519Secret(secret);
161
242
  try {
162
- const priv = await importPrivate(X_PKCS8, "X25519", secret, false, ["deriveBits"]);
243
+ const priv = await importPrivate(X_PKCS8, "X25519", c, false, ["deriveBits"]);
163
244
  const pub = await subtle.importKey("raw", publicKey, { name: "X25519" }, false, []);
164
245
  bits = new Uint8Array(await subtle.deriveBits({ name: "X25519", public: pub }, priv, 256));
165
246
  } catch {
166
247
  fail("bad_key");
248
+ } finally {
249
+ c.fill(0);
167
250
  }
168
251
  if (bits.every((b) => b === 0)) fail("bad_key");
169
252
  return bits;
package/src/index.js CHANGED
@@ -13,9 +13,9 @@ export { canonical, canonicalString, parseJson, parseLimited, validate } from ".
13
13
  export {
14
14
  base64urlEncode, base64urlDecode, base32Encode, base32Decode, base32DecodeLenient, hexEncode, hexDecode,
15
15
  } from "./encoding.js";
16
- export { checkSupport, sha256, ed25519Public, ed25519Sign, ed25519Verify, ed25519SigningKey } from "./crypto.js";
16
+ export { checkSupport, sha256, ed25519Public, ed25519PublicIs, ed25519Sign, ed25519Verify, ed25519SigningKey, usableSeed } from "./crypto.js";
17
17
  export { formatKey, parseKey } from "./keys.js";
18
- export { formatRecoveryKey, parseRecoveryKey, deriveRecoveryKeys } from "./recovery.js";
18
+ export { formatRecoveryKey, parseRecoveryKey, deriveRecoveryKeys, newRecoveryKey, checkNewRecoveryKey } from "./recovery.js";
19
19
  export {
20
20
  checkRealm, checkUsername, defaultUsernamePolicy, similarUsernames, accountId, inception, tag, handle, parseHandle,
21
21
  handleMatches, extendedTags,
package/src/log.js CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { fail } from "./errors.js";
5
5
  import { base64urlEncode, base64urlDecode, bytesEqual } from "./encoding.js";
6
- import { ed25519Public, ed25519Sign, ed25519Verify, frameObject, sha256 } from "./crypto.js";
6
+ import { ed25519Public, ed25519PublicIs, ed25519Sign, ed25519Verify, frameObject, sha256 } from "./crypto.js";
7
7
  import { canonical, without, isObject, MAX_SAFE } from "./json.js";
8
8
  import * as s from "./keys.js";
9
9
  import { formatKey } from "./keys.js";
@@ -275,7 +275,7 @@ export async function addReceipt(e, hostSeed) {
275
275
 
276
276
  /** [RK-5] → wrong_recovery_key */
277
277
  export async function checkRecoveryKey(state, seed) {
278
- if (formatKey("ed25519", await ed25519Public(seed)) !== state.recovery) fail("wrong_recovery_key");
278
+ if (!(await ed25519PublicIs(seed, s.parseKey(state.recovery, "ed25519")))) fail("wrong_recovery_key");
279
279
  }
280
280
 
281
281
  /** A pending instance ([LOG-9]). `home` is `{ key: "ed25519:…", url }`. */
package/src/proof.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { fail } from "./errors.js";
4
4
  import { base64urlEncode } from "./encoding.js";
5
- import { ed25519Public, ed25519Sign, ed25519Verify, frameObject, sha256 } from "./crypto.js";
5
+ import { ed25519PublicIs, ed25519Sign, ed25519Verify, frameObject, sha256 } from "./crypto.js";
6
6
  import { utf8 } from "./encoding.js";
7
7
  import { validate, without } from "./json.js";
8
8
  import * as s from "./keys.js";
@@ -24,11 +24,10 @@ function checkRecipient(r) {
24
24
  /** Make a proof ([PRF-1], [PRF-2]). `by` is "account" or "recovery"; `challenge` is 32 bytes. */
25
25
  export async function prove(state, by, seed, challenge, recipient) {
26
26
  checkRecipient(recipient);
27
- const pub = formatKey("ed25519", await ed25519Public(seed));
28
27
  const p = { v: 1, by, account: state.account, challenge: base64urlEncode(challenge), recipient };
29
28
  if (by === "account") {
30
29
  if (!state.instance) fail("no_instance");
31
- if (state.instance.account_key !== pub) fail("wrong_account_key");
30
+ if (!(await ed25519PublicIs(seed, s.parseKey(state.instance.account_key, "ed25519")))) fail("wrong_account_key");
32
31
  p.instance = state.instance.hash;
33
32
  } else {
34
33
  await checkRecoveryKey(state, seed);
package/src/recovery.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { fail } from "./errors.js";
4
4
  import { B32, base32Decode, base32Encode, base32Normalize, bytesEqual } from "./encoding.js";
5
- import { ed25519Public, frame, frameObject, hkdf, sha256 } from "./crypto.js";
5
+ import { DRAW_ATTEMPTS, ed25519Public, frame, frameObject, hkdf, sha256, usableSeed } from "./crypto.js";
6
6
  import { label } from "./labels.js";
7
7
 
8
8
  async function checksum(key) {
@@ -41,6 +41,28 @@ export async function deriveRecoveryKeys(rk, realm, username) {
41
41
  };
42
42
  }
43
43
 
44
+ /**
45
+ * Generate a recovery key for an identity ([RK-3]): 32 random bytes, drawn again while the derived signing seed is
46
+ * unusable ([CRY-9]). Draws: 32 bytes per attempt. → bad_key only for a broken generator.
47
+ */
48
+ export async function newRecoveryKey(realm, username, random) {
49
+ for (let i = 0; i < DRAW_ATTEMPTS; i++) {
50
+ const rk = random(32);
51
+ const keys = await deriveRecoveryKeys(rk, realm, username);
52
+ const ok = usableSeed(keys.signSeed);
53
+ keys.signSeed.fill(0);
54
+ keys.boxSecret.fill(0);
55
+ if (ok) return rk;
56
+ rk.fill(0);
57
+ }
58
+ fail("bad_key");
59
+ }
60
+
61
+ /** A new recovery key must give a usable signing seed ([CRY-9]). → bad_key */
62
+ export function checkNewRecoveryKey(keys) {
63
+ if (!usableSeed(keys.signSeed)) fail("bad_key");
64
+ }
65
+
44
66
  /** The public recovery signing key. */
45
67
  export async function recoverySignPublic(keys) {
46
68
  return ed25519Public(keys.signSeed);
package/src/vault.js CHANGED
@@ -7,7 +7,7 @@
7
7
  import { fail } from "./errors.js";
8
8
  import { base64urlDecode, base64urlEncode, concat, utf8 } from "./encoding.js";
9
9
  import {
10
- aesGcmDecrypt, aesGcmEncrypt, ed25519Public, ed25519Sign, frameObject, hkdf, sha256, x25519, x25519Public,
10
+ aesGcmDecrypt, aesGcmEncrypt, DRAW_ATTEMPTS, ed25519Public, ed25519PublicIs, ed25519Sign, frameObject, hkdf, sha256, usableSeed, x25519, x25519Public,
11
11
  } from "./crypto.js";
12
12
  import { argon2id } from "./argon2.js";
13
13
  import { canonical, parseLimited } from "./json.js";
@@ -136,9 +136,13 @@ export async function unlockVault(vault, state, secret) {
136
136
  const contents = checkContents(parseLimited(pt, CONTENTS_LIMIT));
137
137
  pt.fill(0);
138
138
  if (!state.instance) fail("no_instance");
139
- if (formatKey("ed25519", await ed25519Public(contents.accountSeed)) !== state.instance.account_key) {
139
+ if (!(await ed25519PublicIs(contents.accountSeed, s.parseKey(state.instance.account_key, "ed25519")))) {
140
140
  fail("wrong_account_key");
141
141
  }
142
+ // Check the signing seed of the lock against its key on record, so that a platform that cannot import it signs
143
+ // with it anyway ([IMP-6]); a mismatch is not this function's to judge (a login with it fails at the host).
144
+ const signKey = lockId === "recovery" ? state.recovery : lock.value.auth;
145
+ await ed25519PublicIs(lockSeed, s.parseKey(signKey, "ed25519"));
142
146
  return { vaultKey: vk, contents, lock: lockId, lockSeed, info };
143
147
  }
144
148
 
@@ -167,13 +171,29 @@ function kdfValue(kdf, salt) {
167
171
  return { alg: "argon2id", m: kdf.m, t: kdf.t, p: kdf.p, salt: base64urlEncode(salt) };
168
172
  }
169
173
 
170
- /** Create a new password lock's public part. Draws: slot id (8), salt (16). Returns `[slot, lockPublic]`. */
174
+ /** A new salt and its lock secrets, drawn again while the auth seed is unusable ([CRY-9]). → bad_key (broken generator) */
175
+ async function newLockSecrets(account, lock, password, kdf, random) {
176
+ for (let i = 0; i < DRAW_ATTEMPTS; i++) {
177
+ const salt = random(16);
178
+ const ls = await deriveLockSecrets(account, lock, password, kdf, salt);
179
+ if (usableSeed(ls.authSeed)) return [salt, ls];
180
+ ls.authSeed.fill(0);
181
+ ls.boxSecret.fill(0);
182
+ }
183
+ fail("bad_key");
184
+ }
185
+
186
+ /**
187
+ * Create a new password lock's public part. `slot` is `{ label, password, username, kdf }`; the password must satisfy
188
+ * [LCK-8] for `username` (`weak_password`), as in every function that creates a password lock. Draws: slot id (8),
189
+ * salt (16, again while the auth seed is unusable, [CRY-9]). Returns `[slot, lockPublic]`.
190
+ */
171
191
  export async function newPasswordLock(account, slot, random) {
192
+ checkPassword(slot.password, slot.username);
172
193
  checkLabel(slot.label);
173
194
  const id = base64urlEncode(random(8));
174
- const salt = random(16);
175
195
  const kdf = { m: slot.kdf.m, t: slot.kdf.t, p: slot.kdf.p ?? 1 };
176
- const ls = await deriveLockSecrets(account, id, slot.password, kdf, salt);
196
+ const [salt, ls] = await newLockSecrets(account, id, slot.password, kdf, random);
177
197
  const pub = {
178
198
  id,
179
199
  kdf: kdfValue(kdf, salt),
@@ -244,16 +264,17 @@ export async function updateContents(u, vault, contents, random) {
244
264
  }
245
265
 
246
266
  /**
247
- * A password change ([CHG-1] step 1). Draws: salt (16), wrap (32 + 12), contents nonce (12).
267
+ * A password change ([CHG-1] step 1); the new password must satisfy [LCK-8] for `username` (`weak_password`).
268
+ * Draws: salt (16), wrap (32 + 12), contents nonce (12).
248
269
  * Returns `{ vault, new }` where `new` is `SHA-256(canonical(new lock))`.
249
270
  */
250
- export async function changePassword(u, vault, slot, password, kdf, random) {
271
+ export async function changePassword(u, vault, slot, password, username, kdf, random) {
272
+ checkPassword(password, username);
251
273
  const { account, instance, grant } = idsOf(u, vault);
252
274
  const index = u.info.locks.findIndex((l) => l.kind === "password" && l.id === slot);
253
275
  if (index < 0) fail("bad_locks");
254
- const salt = random(16);
255
276
  const k = { m: kdf.m, t: kdf.t, p: kdf.p ?? 1 };
256
- const ls = await deriveLockSecrets(account, slot, password, k, salt);
277
+ const [salt, ls] = await newLockSecrets(account, slot, password, k, random);
257
278
  const box = await x25519Public(ls.boxSecret);
258
279
  const pub = { id: slot, kdf: kdfValue(k, salt), auth: formatKey("ed25519", await ed25519Public(ls.authSeed)), box: formatKey("x25519", box) };
259
280
  ls.authSeed.fill(0);