mindoodb 0.0.94 → 0.0.95

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.
Files changed (146) hide show
  1. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +17 -1
  2. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  3. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +63 -4
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  5. package/dist/appendonlystores/network/HttpTransport.d.ts +10 -1
  6. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
  7. package/dist/appendonlystores/network/HttpTransport.js +45 -3
  8. package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
  9. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +22 -0
  10. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
  11. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +33 -0
  12. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
  13. package/dist/browser/index.mjs +37 -0
  14. package/dist/core/BaseMindooDB.d.ts +9 -0
  15. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  16. package/dist/core/BaseMindooDB.js +65 -2
  17. package/dist/core/BaseMindooDB.js.map +1 -1
  18. package/dist/core/BaseMindooTenant.d.ts +75 -1
  19. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  20. package/dist/core/BaseMindooTenant.js +229 -7
  21. package/dist/core/BaseMindooTenant.js.map +1 -1
  22. package/dist/core/BaseMindooTenantDirectory.d.ts +165 -2
  23. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  24. package/dist/core/BaseMindooTenantDirectory.js +648 -112
  25. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  26. package/dist/core/BaseMindooTenantFactory.d.ts +30 -0
  27. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
  28. package/dist/core/BaseMindooTenantFactory.js +95 -0
  29. package/dist/core/BaseMindooTenantFactory.js.map +1 -1
  30. package/dist/core/accesscontrol/DirectoryStateNode.d.ts +45 -0
  31. package/dist/core/accesscontrol/DirectoryStateNode.d.ts.map +1 -1
  32. package/dist/core/accesscontrol/DirectoryStateNode.js +58 -0
  33. package/dist/core/accesscontrol/DirectoryStateNode.js.map +1 -1
  34. package/dist/core/accesscontrol/adminKeys.d.ts +130 -0
  35. package/dist/core/accesscontrol/adminKeys.d.ts.map +1 -0
  36. package/dist/core/accesscontrol/adminKeys.js +164 -0
  37. package/dist/core/accesscontrol/adminKeys.js.map +1 -0
  38. package/dist/core/accesscontrol/directoryAdminResolver.d.ts +54 -0
  39. package/dist/core/accesscontrol/directoryAdminResolver.d.ts.map +1 -0
  40. package/dist/core/accesscontrol/directoryAdminResolver.js +217 -0
  41. package/dist/core/accesscontrol/directoryAdminResolver.js.map +1 -0
  42. package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -1
  43. package/dist/core/accesscontrol/directoryProjection.js +25 -0
  44. package/dist/core/accesscontrol/directoryProjection.js.map +1 -1
  45. package/dist/core/accesscontrol/grantKeys.d.ts +16 -0
  46. package/dist/core/accesscontrol/grantKeys.d.ts.map +1 -1
  47. package/dist/core/accesscontrol/grantKeys.js +37 -0
  48. package/dist/core/accesscontrol/grantKeys.js.map +1 -1
  49. package/dist/core/accesscontrol/types.d.ts +11 -2
  50. package/dist/core/accesscontrol/types.d.ts.map +1 -1
  51. package/dist/core/accesscontrol/types.js +7 -0
  52. package/dist/core/accesscontrol/types.js.map +1 -1
  53. package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
  54. package/dist/core/appendonlystores/network/AuthenticationService.js +8 -0
  55. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
  56. package/dist/core/appendonlystores/network/NetworkTransport.d.ts +16 -1
  57. package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
  58. package/dist/core/appendonlystores/network/types.d.ts +13 -0
  59. package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
  60. package/dist/core/appendonlystores/network/types.js.map +1 -1
  61. package/dist/core/builtinDbInvariants.d.ts +18 -2
  62. package/dist/core/builtinDbInvariants.d.ts.map +1 -1
  63. package/dist/core/builtinDbInvariants.js +41 -5
  64. package/dist/core/builtinDbInvariants.js.map +1 -1
  65. package/dist/core/index.d.ts +4 -1
  66. package/dist/core/index.d.ts.map +1 -1
  67. package/dist/core/index.js +26 -5
  68. package/dist/core/index.js.map +1 -1
  69. package/dist/core/live/LiveChannel.d.ts +41 -0
  70. package/dist/core/live/LiveChannel.d.ts.map +1 -0
  71. package/dist/core/live/LiveChannel.js +151 -0
  72. package/dist/core/live/LiveChannel.js.map +1 -0
  73. package/dist/core/types.d.ts +182 -0
  74. package/dist/core/types.d.ts.map +1 -1
  75. package/dist/core/types.js.map +1 -1
  76. package/dist/core/uri/MindooURI.d.ts +1 -1
  77. package/dist/core/uri/MindooURI.d.ts.map +1 -1
  78. package/dist/core/uri/MindooURI.js +2 -0
  79. package/dist/core/uri/MindooURI.js.map +1 -1
  80. package/dist/core/uri/adminUri.d.ts +52 -0
  81. package/dist/core/uri/adminUri.d.ts.map +1 -0
  82. package/dist/core/uri/adminUri.js +100 -0
  83. package/dist/core/uri/adminUri.js.map +1 -0
  84. package/dist/core/uri/joinRequestUri.d.ts.map +1 -1
  85. package/dist/core/uri/joinRequestUri.js +14 -1
  86. package/dist/core/uri/joinRequestUri.js.map +1 -1
  87. package/dist/core/userid/index.d.ts +9 -0
  88. package/dist/core/userid/index.d.ts.map +1 -1
  89. package/dist/core/userkeys/UserKeyDocument.d.ts +7 -1
  90. package/dist/core/userkeys/UserKeyDocument.d.ts.map +1 -1
  91. package/dist/core/userkeys/UserKeyDocument.js +14 -3
  92. package/dist/core/userkeys/UserKeyDocument.js.map +1 -1
  93. package/dist/core/userkeys/UserKeyManager.d.ts +95 -4
  94. package/dist/core/userkeys/UserKeyManager.d.ts.map +1 -1
  95. package/dist/core/userkeys/UserKeyManager.js +606 -59
  96. package/dist/core/userkeys/UserKeyManager.js.map +1 -1
  97. package/dist/core/userkeys/deviceCertificate.d.ts +46 -0
  98. package/dist/core/userkeys/deviceCertificate.d.ts.map +1 -0
  99. package/dist/core/userkeys/deviceCertificate.js +86 -0
  100. package/dist/core/userkeys/deviceCertificate.js.map +1 -0
  101. package/dist/core/userkeys/generationSignature.d.ts +32 -0
  102. package/dist/core/userkeys/generationSignature.d.ts.map +1 -0
  103. package/dist/core/userkeys/generationSignature.js +136 -0
  104. package/dist/core/userkeys/generationSignature.js.map +1 -0
  105. package/dist/core/userkeys/generationValidity.d.ts +75 -0
  106. package/dist/core/userkeys/generationValidity.d.ts.map +1 -0
  107. package/dist/core/userkeys/generationValidity.js +175 -0
  108. package/dist/core/userkeys/generationValidity.js.map +1 -0
  109. package/dist/core/userkeys/index.d.ts +5 -2
  110. package/dist/core/userkeys/index.d.ts.map +1 -1
  111. package/dist/core/userkeys/index.js +21 -1
  112. package/dist/core/userkeys/index.js.map +1 -1
  113. package/dist/core/userkeys/resolveUserKeyDocument.d.ts +16 -0
  114. package/dist/core/userkeys/resolveUserKeyDocument.d.ts.map +1 -1
  115. package/dist/core/userkeys/resolveUserKeyDocument.js +42 -1
  116. package/dist/core/userkeys/resolveUserKeyDocument.js.map +1 -1
  117. package/dist/core/userkeys/types.d.ts +26 -0
  118. package/dist/core/userkeys/types.d.ts.map +1 -1
  119. package/dist/core/userkeys/types.js +3 -1
  120. package/dist/core/userkeys/types.js.map +1 -1
  121. package/dist/core/userkeys/validateUserKeyDocument.d.ts.map +1 -1
  122. package/dist/core/userkeys/validateUserKeyDocument.js +7 -0
  123. package/dist/core/userkeys/validateUserKeyDocument.js.map +1 -1
  124. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  125. package/dist/node/server/MindooDBServer.js +11 -0
  126. package/dist/node/server/MindooDBServer.js.map +1 -1
  127. package/dist/server/SyncEventBus.d.ts +22 -0
  128. package/dist/server/SyncEventBus.d.ts.map +1 -1
  129. package/dist/server/SyncEventBus.js +21 -0
  130. package/dist/server/SyncEventBus.js.map +1 -1
  131. package/dist/server/TenantManager.d.ts +9 -0
  132. package/dist/server/TenantManager.d.ts.map +1 -1
  133. package/dist/server/TenantManager.js +96 -0
  134. package/dist/server/TenantManager.js.map +1 -1
  135. package/dist/server/http/MindooDBServerCore.d.ts +39 -1
  136. package/dist/server/http/MindooDBServerCore.d.ts.map +1 -1
  137. package/dist/server/http/MindooDBServerCore.js +121 -2
  138. package/dist/server/http/MindooDBServerCore.js.map +1 -1
  139. package/dist/server/http/types.d.ts +10 -1
  140. package/dist/server/http/types.d.ts.map +1 -1
  141. package/dist/server/http/types.js.map +1 -1
  142. package/dist/server/index.d.ts +2 -2
  143. package/dist/server/index.d.ts.map +1 -1
  144. package/dist/server/index.js +2 -1
  145. package/dist/server/index.js.map +1 -1
  146. package/package.json +1 -1
@@ -8,6 +8,9 @@ const rsaOaep3072_1 = require("../crypto/rsaOaep3072");
8
8
  const types_1 = require("../types");
9
9
  const fingerprint_1 = require("./fingerprint");
10
10
  const resolveUserKeyDocument_1 = require("./resolveUserKeyDocument");
11
+ const generationSignature_1 = require("./generationSignature");
12
+ const fingerprint_2 = require("./fingerprint");
13
+ const deviceCertificate_1 = require("./deviceCertificate");
11
14
  const validateUserKeyDocument_1 = require("./validateUserKeyDocument");
12
15
  const UserKeyDocument_1 = require("./UserKeyDocument");
13
16
  const types_2 = require("./types");
@@ -22,6 +25,9 @@ class UserKeyManager {
22
25
  this.seededBytesVerifiedForPublicKey = null;
23
26
  this.resolvingOwnDeviceWrap = false;
24
27
  this.reconcileInFlight = null;
28
+ this.decryptedSelfGrantBytes = null;
29
+ this.seededSelfGrantBytes = null;
30
+ this.resolvingSelfGrantWrap = false;
25
31
  }
26
32
  noteUserDirectoryFetched() {
27
33
  this.userDirectoryFetched = true;
@@ -76,6 +82,15 @@ class UserKeyManager {
76
82
  this.seededBytesVerifiedForPublicKey = publicKeyPem;
77
83
  return matches;
78
84
  }
85
+ /**
86
+ * Inject the device self-grant private key (PKCS8) the host decrypted from
87
+ * the identity (Haven unlock), for sessions opened with an empty password.
88
+ * Used only when it belongs to `PrivateUserId.deviceSelfGrantKeyPair`.
89
+ */
90
+ seedDecryptedSelfGrantKeyBytes(bytes) {
91
+ this.seededSelfGrantBytes = bytes.slice();
92
+ this.decryptedSelfGrantBytes = null;
93
+ }
79
94
  hasFetchedUserDirectory() {
80
95
  return this.userDirectoryFetched;
81
96
  }
@@ -125,8 +140,471 @@ class UserKeyManager {
125
140
  this.decryptedUserKeyCache = null;
126
141
  this.seededBytesMatchLocalPublic = true;
127
142
  this.seededBytesVerifiedForPublicKey = pair.publicKey;
143
+ if (!user.deviceSelfGrantKeyPair) {
144
+ await this.generateLocalSelfGrantKey(user, password);
145
+ }
128
146
  return pair;
129
147
  }
148
+ // ==================== Device certificates (docs/userkeys.md §9.6) ====================
149
+ /**
150
+ * Vouch for another device of this person (docs/userkeys.md §9.6): write a
151
+ * `dcert_<fingerprint>` certificate signed with the person's self-grant key
152
+ * and wrap the User-Key (and self-grant key) to the device, so it can sync,
153
+ * write and read without the admin. Requires the admin to have granted
154
+ * device self-grant with the key this device holds.
155
+ */
156
+ async issueDeviceCertificate(device, label) {
157
+ const granted = await this.grantedSelfGrantKey();
158
+ if (!granted) {
159
+ throw new Error("Cannot certify a device: the administrator has not granted device self-grant to this person.");
160
+ }
161
+ const signer = await this.selfGrantSigner();
162
+ if (!signer || (0, fingerprint_2.normalizePublicKeyPem)(signer.publicKeyPem) !== (0, fingerprint_2.normalizePublicKeyPem)(granted)) {
163
+ throw new Error("Cannot certify a device: this device does not hold the self-grant key named in the grant.");
164
+ }
165
+ const resolved = await this.resolveOwnUserKeyDocument();
166
+ const usernameHash = resolved?.payload.username_hash ??
167
+ (await this.mintBindingForUsername((await this.tenant.getCurrentUserId()).username))?.usernameHash;
168
+ if (!usernameHash)
169
+ throw new Error("Cannot certify a device: no grant for this person");
170
+ const crypto = this.tenant.getCryptoAdapter();
171
+ const me = await this.tenant.getCurrentUserId();
172
+ const trimmedLabel = label?.trim();
173
+ const certificate = await (0, deviceCertificate_1.signDeviceCertificate)(crypto, signer.privateKey, {
174
+ username_hash: usernameHash,
175
+ deviceSigningPublicKey: device.signingPublicKey,
176
+ deviceEncryptionPublicKey: device.encryptionPublicKey,
177
+ ...(trimmedLabel ? { label: trimmedLabel } : {}),
178
+ issuedAt: Date.now(),
179
+ issuedBySigningPublicKey: me.userSigningPublicKey,
180
+ selfGrantPublicKey: signer.publicKeyPem,
181
+ });
182
+ const fingerprint = await (0, fingerprint_2.fingerprintPublicKeyPem)(device.signingPublicKey, crypto.getSubtle());
183
+ const db = await this.openUserDirectory();
184
+ const create = {
185
+ decryptionKeyId: types_1.PUBLIC_INFOS_KEY_ID,
186
+ initialValues: certificate,
187
+ };
188
+ let doc;
189
+ try {
190
+ doc = await db.createDocument({
191
+ ...create,
192
+ id: (0, deviceCertificate_1.deviceCertificateDocId)(fingerprint),
193
+ // The certificate must be in the doc_create itself: readers take it
194
+ // from there only (see deviceCertificate.ts).
195
+ assumeUniqueId: true,
196
+ });
197
+ }
198
+ catch {
199
+ // Id taken (an earlier, withdrawn certificate): any `dcert_` id works.
200
+ doc = await db.createDocument({ ...create, idPrefix: "dcert" });
201
+ }
202
+ if (resolved) {
203
+ await this.wrapUserKeyForGrantDevice(device.encryptionPublicKey, trimmedLabel);
204
+ }
205
+ return { docId: doc.getId(), certificate };
206
+ }
207
+ /** This person's device certificates in the local `userdirectory` replica. */
208
+ async listOwnDeviceCertificates() {
209
+ const resolved = await this.resolveOwnUserKeyDocument();
210
+ if (!resolved)
211
+ return [];
212
+ const db = await this.openUserDirectory();
213
+ const crypto = this.tenant.getCryptoAdapter();
214
+ const result = [];
215
+ for (const id of await db.getAllDocumentIds()) {
216
+ if (!id.startsWith(deviceCertificate_1.DEVICE_CERT_DOC_ID_PREFIX))
217
+ continue;
218
+ try {
219
+ const doc = await db.getDocument(id);
220
+ if (doc.isDeleted())
221
+ continue;
222
+ const cert = (0, deviceCertificate_1.asDeviceCertificate)(doc.getData());
223
+ if (!cert || cert.username_hash !== resolved.payload.username_hash)
224
+ continue;
225
+ result.push({
226
+ docId: id,
227
+ fingerprint: await (0, fingerprint_2.fingerprintPublicKeyPem)(cert.deviceSigningPublicKey, crypto.getSubtle()),
228
+ certificate: cert,
229
+ });
230
+ }
231
+ catch {
232
+ // unreadable
233
+ }
234
+ }
235
+ return result;
236
+ }
237
+ /**
238
+ * Withdraw a certificate this person issued (a device they sold or retired):
239
+ * delete its document, so the device stops counting from the delete's
240
+ * trusted time. `fingerprint` is the device's signing-key fingerprint or the
241
+ * document id. Any device of the person can do this, so against a stolen
242
+ * device the admin revokes the key on the grant instead.
243
+ */
244
+ async revokeOwnDeviceCertificate(fingerprint) {
245
+ const certs = await this.listOwnDeviceCertificates();
246
+ const db = await this.openUserDirectory();
247
+ let removed = 0;
248
+ for (const entry of certs) {
249
+ if (entry.fingerprint !== fingerprint && entry.docId !== fingerprint)
250
+ continue;
251
+ await db.deleteDocument(entry.docId);
252
+ removed += 1;
253
+ }
254
+ if (removed === 0) {
255
+ throw new Error(`No device certificate of this person matches "${fingerprint}"`);
256
+ }
257
+ return removed;
258
+ }
259
+ /**
260
+ * Grant devices plus devices this person certified, for wrapping the
261
+ * User-Key and the self-grant key (rotation, self-grant distribution).
262
+ */
263
+ async devicesForWrapping(username) {
264
+ const directory = await this.directory();
265
+ const overview = await directory.getUserGrantOverview(username);
266
+ const devices = [
267
+ ...overview.activeDevices,
268
+ ];
269
+ const hashes = new Set();
270
+ for (const grant of await directory.findGrantAccessDocuments(username)) {
271
+ const hash = grant.getData().username_hash;
272
+ if (typeof hash === "string")
273
+ hashes.add(hash);
274
+ }
275
+ for (const hash of hashes) {
276
+ for (const cert of await directory.listCertifiedDevices(hash)) {
277
+ if (devices.some((d) => (0, fingerprint_2.normalizePublicKeyPem)(d.signingPublicKey) === (0, fingerprint_2.normalizePublicKeyPem)(cert.deviceSigningPublicKey))) {
278
+ continue;
279
+ }
280
+ devices.push({
281
+ signingPublicKey: cert.deviceSigningPublicKey,
282
+ encryptionPublicKey: cert.deviceEncryptionPublicKey,
283
+ ...(cert.label ? { label: cert.label } : {}),
284
+ });
285
+ }
286
+ }
287
+ return devices;
288
+ }
289
+ // ==================== Device self-grant key (docs/userkeys.md §9) ====================
290
+ async generateLocalSelfGrantKey(user, password) {
291
+ const crypto = this.tenant.getCryptoAdapter();
292
+ const generated = await (0, generationSignature_1.generateSelfGrantKeyPair)(crypto);
293
+ user.deviceSelfGrantKeyPair = {
294
+ publicKey: generated.publicKeyPem,
295
+ privateKey: await (0, privateKeyEncryption_1.encryptPrivateKey)(crypto, generated.privateKeyBytes, password, types_2.DEVICE_SELF_GRANT_PRIVATE_SALT),
296
+ };
297
+ this.decryptedSelfGrantBytes = generated.privateKeyBytes;
298
+ }
299
+ /**
300
+ * Public half of this person's device self-grant key, as held by this
301
+ * device (docs/userkeys.md §9). The admin names it in the grant with
302
+ * `directory.setDeviceSelfGrant`. `null` while this device holds none.
303
+ */
304
+ async getDeviceSelfGrantPublicKey() {
305
+ const bytes = await this.getSelfGrantPrivateKeyBytes();
306
+ if (!bytes)
307
+ return null;
308
+ return this.tenant.getCurrentPrivateUser().deviceSelfGrantKeyPair?.publicKey ?? null;
309
+ }
310
+ /**
311
+ * The self-grant private key (PKCS8): from memory, the password-encrypted
312
+ * copy on the identity, or this device's wrap in the own User-Key document.
313
+ * When the admin named a key in the grant, a held key that differs is not
314
+ * returned as long as a wrap of the granted key exists.
315
+ */
316
+ async getSelfGrantPrivateKeyBytes() {
317
+ if (this.decryptedSelfGrantBytes)
318
+ return this.decryptedSelfGrantBytes;
319
+ const user = this.tenant.getCurrentPrivateUser();
320
+ const pair = user.deviceSelfGrantKeyPair;
321
+ const password = this.tenant.getCurrentUserPassword();
322
+ // A device that made its own key before joining holds one nobody else
323
+ // knows; the person's key (the one in the grant) comes from a wrap.
324
+ const granted = await this.grantedSelfGrantKey();
325
+ if (granted && pair && (0, fingerprint_2.normalizePublicKeyPem)(pair.publicKey) !== (0, fingerprint_2.normalizePublicKeyPem)(granted)) {
326
+ const fromWrap = await this.importSelfGrantKeyFromWrap();
327
+ if (fromWrap)
328
+ return fromWrap;
329
+ }
330
+ if (pair && this.seededSelfGrantBytes) {
331
+ try {
332
+ const derived = await (0, generationSignature_1.ed25519FromPkcs8)(this.tenant.getCryptoAdapter(), this.seededSelfGrantBytes);
333
+ if ((0, fingerprint_2.normalizePublicKeyPem)(derived.publicKeyPem) === (0, fingerprint_2.normalizePublicKeyPem)(pair.publicKey)) {
334
+ this.decryptedSelfGrantBytes = this.seededSelfGrantBytes;
335
+ return this.decryptedSelfGrantBytes;
336
+ }
337
+ }
338
+ catch {
339
+ // not a usable key; fall through
340
+ }
341
+ }
342
+ if (pair && password) {
343
+ try {
344
+ const bytes = new Uint8Array(await this.tenant.decryptPrivateKey(pair.privateKey, password, types_2.DEVICE_SELF_GRANT_PRIVATE_SALT));
345
+ const derived = await (0, generationSignature_1.ed25519FromPkcs8)(this.tenant.getCryptoAdapter(), bytes);
346
+ if ((0, fingerprint_2.normalizePublicKeyPem)(derived.publicKeyPem) === (0, fingerprint_2.normalizePublicKeyPem)(pair.publicKey)) {
347
+ this.decryptedSelfGrantBytes = bytes;
348
+ return bytes;
349
+ }
350
+ }
351
+ catch {
352
+ // fall through to the wrap
353
+ }
354
+ }
355
+ return this.importSelfGrantKeyFromWrap();
356
+ }
357
+ /** Unwrap the self-grant key from this device's wrap and keep it on the identity. */
358
+ async importSelfGrantKeyFromWrap() {
359
+ if (this.resolvingSelfGrantWrap)
360
+ return null;
361
+ this.resolvingSelfGrantWrap = true;
362
+ try {
363
+ if (!this.canReconcileAgainstUserDirectory())
364
+ return null;
365
+ const deviceKey = await this.tenant.getEncryptionPrivateKeyForReconcile();
366
+ if (!deviceKey)
367
+ return null;
368
+ const resolved = await this.resolveOwnUserKeyDocument();
369
+ if (!resolved)
370
+ return null;
371
+ const crypto = this.tenant.getCryptoAdapter();
372
+ const user = this.tenant.getCurrentPrivateUser();
373
+ const deviceFp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(user.userEncryptionKeyPair.publicKey, crypto.getSubtle());
374
+ const granted = await this.grantedSelfGrantKey();
375
+ let fallback = null;
376
+ for (const epoch of this.epochsNewestFirst(resolved.payload)) {
377
+ const wrapped = resolved.payload.userKeys[epoch]?.deviceWraps?.[deviceFp]?.wrappedSelfGrantKey;
378
+ if (!wrapped)
379
+ continue;
380
+ try {
381
+ const bytes = await (0, UserKeyDocument_1.unwrapPrivateKeyFromDevice)({
382
+ cryptoAdapter: crypto,
383
+ wrappedKeyB64: wrapped,
384
+ deviceEncryptionPrivateKey: deviceKey,
385
+ });
386
+ const { publicKeyPem } = await (0, generationSignature_1.ed25519FromPkcs8)(crypto, bytes);
387
+ if (!granted || (0, fingerprint_2.normalizePublicKeyPem)(publicKeyPem) === (0, fingerprint_2.normalizePublicKeyPem)(granted)) {
388
+ fallback = { bytes, publicKeyPem };
389
+ break;
390
+ }
391
+ fallback ?? (fallback = { bytes, publicKeyPem });
392
+ }
393
+ catch (error) {
394
+ console.warn("[userkeys] self-grant key unwrap failed", { epoch }, error);
395
+ }
396
+ }
397
+ if (!fallback)
398
+ return null;
399
+ user.deviceSelfGrantKeyPair = {
400
+ publicKey: fallback.publicKeyPem,
401
+ privateKey: await (0, privateKeyEncryption_1.encryptPrivateKey)(crypto, fallback.bytes, this.tenant.getCurrentUserPassword(), types_2.DEVICE_SELF_GRANT_PRIVATE_SALT),
402
+ };
403
+ this.decryptedSelfGrantBytes = fallback.bytes;
404
+ return fallback.bytes;
405
+ }
406
+ finally {
407
+ this.resolvingSelfGrantWrap = false;
408
+ }
409
+ }
410
+ /** The self-grant key the admin named in this person's grant, or `null`. */
411
+ async grantedSelfGrantKey() {
412
+ try {
413
+ const directory = await this.directory();
414
+ const username = (await this.tenant.getCurrentUserId()).username;
415
+ const grant = await directory.getDeviceSelfGrant(username);
416
+ return grant && grant.revokedAt === undefined ? grant.signingPublicKey : null;
417
+ }
418
+ catch {
419
+ return null;
420
+ }
421
+ }
422
+ /** Self-grant signer for this device, if it holds the key. */
423
+ async selfGrantSigner() {
424
+ const bytes = await this.getSelfGrantPrivateKeyBytes();
425
+ if (!bytes)
426
+ return null;
427
+ try {
428
+ return await (0, generationSignature_1.ed25519FromPkcs8)(this.tenant.getCryptoAdapter(), bytes);
429
+ }
430
+ catch {
431
+ return null;
432
+ }
433
+ }
434
+ /** Wrap the self-grant key to a device, when this device holds it. */
435
+ async wrapSelfGrantKeyFor(deviceEncryptionPublicKey) {
436
+ const bytes = await this.getSelfGrantPrivateKeyBytes();
437
+ if (!bytes)
438
+ return undefined;
439
+ return (0, UserKeyDocument_1.wrapPrivateKeyForDevice)({
440
+ cryptoAdapter: this.tenant.getCryptoAdapter(),
441
+ privateKeyBytes: bytes,
442
+ deviceEncryptionPublicKey,
443
+ });
444
+ }
445
+ /**
446
+ * Signing key and signatures for a generation (docs/userkeys.md §9.3).
447
+ * `ownBytes` derive the generation's own signing key. Signs with the
448
+ * previous generation's key when `previous` is given, with the self-grant
449
+ * key when this device holds it, and with its own key when `selfSign`
450
+ * (pending adoption; harmless otherwise).
451
+ */
452
+ async generationSignatureFields(input) {
453
+ const crypto = this.tenant.getCryptoAdapter();
454
+ const own = await (0, generationSignature_1.deriveGenerationSigningKey)(crypto, input.ownBytes);
455
+ const message = (0, generationSignature_1.userKeyGenerationSigningMessage)(input.usernameHash, input.epoch, {
456
+ ...input.gen,
457
+ signingPublicKey: own.publicKeyPem,
458
+ });
459
+ const signatures = [];
460
+ if (input.previous?.gen.signingPublicKey) {
461
+ const prev = await (0, generationSignature_1.deriveGenerationSigningKey)(crypto, input.previous.bytes);
462
+ if ((0, fingerprint_2.normalizePublicKeyPem)(prev.publicKeyPem) ===
463
+ (0, fingerprint_2.normalizePublicKeyPem)(input.previous.gen.signingPublicKey)) {
464
+ signatures.push({
465
+ signerPublicKey: prev.publicKeyPem,
466
+ signature: await (0, generationSignature_1.signEd25519)(crypto, prev.privateKey, message),
467
+ });
468
+ }
469
+ }
470
+ const selfGrant = await this.selfGrantSigner();
471
+ if (selfGrant) {
472
+ signatures.push({
473
+ signerPublicKey: selfGrant.publicKeyPem,
474
+ signature: await (0, generationSignature_1.signEd25519)(crypto, selfGrant.privateKey, message),
475
+ });
476
+ }
477
+ if (input.selfSign) {
478
+ signatures.push({
479
+ signerPublicKey: own.publicKeyPem,
480
+ signature: await (0, generationSignature_1.signEd25519)(crypto, own.privateKey, message),
481
+ });
482
+ }
483
+ return { signingPublicKey: own.publicKeyPem, signatures };
484
+ }
485
+ /** The document's generations that count (docs/userkeys.md §9.3). */
486
+ async validated(doc) {
487
+ const raw = (0, validateUserKeyDocument_1.asUserKeyPayload)(doc.getData());
488
+ if (!raw)
489
+ return null;
490
+ return (0, resolveUserKeyDocument_1.applyGenerationValidity)(doc, raw, {
491
+ db: doc.getDatabase(),
492
+ directory: await this.directory(),
493
+ });
494
+ }
495
+ /** Effective cutoff for unsigned generations of this person (tenant policy, self-grant). */
496
+ async generationSignatureCutoff(usernameHash) {
497
+ const directory = await this.directory();
498
+ const tenantCutoff = await directory.getRequireUserKeyGenerationSignatureSince();
499
+ const selfGrant = await directory.getDeviceSelfGrantForUsernameHash(usernameHash);
500
+ const cutoffs = [tenantCutoff, selfGrant?.grantedAt].filter((t) => typeof t === "number" && Number.isFinite(t));
501
+ return cutoffs.length > 0 ? Math.min(...cutoffs) : undefined;
502
+ }
503
+ /**
504
+ * Give the current generation its signing key when it has none (or one this
505
+ * device cannot derive), so the next rotation can be chained to it. Written
506
+ * only when the change will count: while the person's document is pending,
507
+ * before the cutoff, or signed by a granted self-grant key.
508
+ */
509
+ async ensureCurrentGenerationSigningKey(resolved) {
510
+ const epoch = (0, types_2.currentUserKeyEpoch)(resolved.payload);
511
+ if (!epoch)
512
+ return;
513
+ const gen = resolved.payload.userKeys[epoch];
514
+ const bytes = await this.privateKeyBytesForEpoch(resolved.payload, epoch);
515
+ if (!gen || !bytes)
516
+ return;
517
+ const crypto = this.tenant.getCryptoAdapter();
518
+ const derived = await (0, generationSignature_1.deriveGenerationSigningKey)(crypto, bytes);
519
+ if (gen.signingPublicKey &&
520
+ (0, fingerprint_2.normalizePublicKeyPem)(gen.signingPublicKey) === (0, fingerprint_2.normalizePublicKeyPem)(derived.publicKeyPem)) {
521
+ return;
522
+ }
523
+ const usernameHash = resolved.payload.username_hash;
524
+ const pending = (0, types_2.isPendingUserKeyDocument)(resolved.rawPayload);
525
+ const cutoff = await this.generationSignatureCutoff(usernameHash);
526
+ const granted = await this.grantedSelfGrantKey();
527
+ const selfGrant = granted ? await this.selfGrantSigner() : null;
528
+ const holdsGranted = !!selfGrant && (0, fingerprint_2.normalizePublicKeyPem)(selfGrant.publicKeyPem) === (0, fingerprint_2.normalizePublicKeyPem)(granted);
529
+ const lowest = this.epochsNewestFirst(resolved.payload).reverse()[0] === epoch;
530
+ const counts = (pending && lowest) || cutoff === undefined || Date.now() < cutoff || holdsGranted;
531
+ if (!counts)
532
+ return;
533
+ const fields = await this.generationSignatureFields({
534
+ usernameHash,
535
+ epoch,
536
+ gen,
537
+ ownBytes: bytes,
538
+ selfSign: pending && lowest,
539
+ });
540
+ const live = (0, validateUserKeyDocument_1.asUserKeyPayload)(resolved.doc.getData()) ?? resolved.rawPayload;
541
+ await (0, UserKeyDocument_1.replaceUserKeys)(resolved.doc.getDatabase(), resolved.doc, {
542
+ ...live.userKeys,
543
+ [epoch]: { ...(live.userKeys[epoch] ?? gen), ...fields },
544
+ });
545
+ }
546
+ /**
547
+ * Make sure this person has a self-grant key and every approved device of
548
+ * the current generation holds it (docs/userkeys.md §9). The first updated
549
+ * device that holds the User-Key and finds no wrapped self-grant key creates
550
+ * one; every device wraps the key it holds to devices that lack it.
551
+ */
552
+ async ensureSelfGrantKeyDistributed(resolved) {
553
+ const epoch = (0, types_2.currentUserKeyEpoch)(resolved.payload);
554
+ if (!epoch)
555
+ return;
556
+ const gen = resolved.payload.userKeys[epoch];
557
+ if (!gen)
558
+ return;
559
+ const userKeyBytes = await this.privateKeyBytesForEpoch(resolved.payload, epoch);
560
+ if (!userKeyBytes)
561
+ return;
562
+ let bytes = await this.getSelfGrantPrivateKeyBytes();
563
+ const anyWrapped = Object.values(gen.deviceWraps ?? {}).some((w) => !!w.wrappedSelfGrantKey);
564
+ const user = this.tenant.getCurrentPrivateUser();
565
+ // Generate only for an identity that has none: one this session merely
566
+ // cannot open (host without seed, empty password) must not be replaced.
567
+ if (!bytes && !anyWrapped && !user.deviceSelfGrantKeyPair) {
568
+ await this.generateLocalSelfGrantKey(user, this.tenant.getCurrentUserPassword());
569
+ bytes = this.decryptedSelfGrantBytes;
570
+ }
571
+ if (!bytes)
572
+ return;
573
+ const username = (await this.tenant.getCurrentUserId()).username;
574
+ const devices = await this.devicesForWrapping(username);
575
+ const crypto = this.tenant.getCryptoAdapter();
576
+ const db = resolved.doc.getDatabase();
577
+ for (const device of devices) {
578
+ const fp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(device.encryptionPublicKey, crypto.getSubtle());
579
+ const wrap = gen.deviceWraps?.[fp];
580
+ if (!wrap?.wrappedKey || wrap.wrappedSelfGrantKey)
581
+ continue;
582
+ await (0, UserKeyDocument_1.writeDeviceWrap)({
583
+ db,
584
+ doc: resolved.doc,
585
+ epoch,
586
+ deviceFingerprint: fp,
587
+ wrap: { ...wrap, wrappedSelfGrantKey: await this.wrapSelfGrantKeyFor(device.encryptionPublicKey) },
588
+ });
589
+ }
590
+ // Wraps written by other devices may not be in the grant overview yet
591
+ // (directory not pulled). This device's own wrap is covered either way.
592
+ const ownFp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(user.userEncryptionKeyPair.publicKey, crypto.getSubtle());
593
+ const live = (0, validateUserKeyDocument_1.asUserKeyPayload)(resolved.doc.getData());
594
+ const ownWrap = live?.userKeys[epoch]?.deviceWraps?.[ownFp];
595
+ if (ownWrap?.wrappedKey && !ownWrap.wrappedSelfGrantKey) {
596
+ await (0, UserKeyDocument_1.writeDeviceWrap)({
597
+ db,
598
+ doc: resolved.doc,
599
+ epoch,
600
+ deviceFingerprint: ownFp,
601
+ wrap: {
602
+ ...ownWrap,
603
+ wrappedSelfGrantKey: await this.wrapSelfGrantKeyFor(user.userEncryptionKeyPair.publicKey),
604
+ },
605
+ });
606
+ }
607
+ }
130
608
  async getLocalUserPublicKey() {
131
609
  const user = this.tenant.getCurrentPrivateUser();
132
610
  if (user.userKeyPair?.publicKey)
@@ -374,6 +852,17 @@ class UserKeyManager {
374
852
  return null;
375
853
  const { grantDocId, usernameHash, usernameHashVersion } = binding;
376
854
  const fingerprint = await (0, UserKeyDocument_1.fingerprintUserPublicKey)(pair.publicKey, this.tenant.getCryptoAdapter());
855
+ const mintBytes = await this.getDecryptedUserKeyBytes();
856
+ const createdAt = Date.now();
857
+ const signed = mintBytes
858
+ ? await this.generationSignatureFields({
859
+ usernameHash,
860
+ epoch: "1",
861
+ gen: { publicKey: pair.publicKey, fingerprint, createdAt },
862
+ ownBytes: mintBytes,
863
+ selfSign: true,
864
+ })
865
+ : undefined;
377
866
  const db = await this.openUserDirectory();
378
867
  let doc;
379
868
  let mintedHere = false;
@@ -394,7 +883,8 @@ class UserKeyManager {
394
883
  usernameHashVersion,
395
884
  publicKey: pair.publicKey,
396
885
  fingerprint,
397
- createdAt: Date.now(),
886
+ createdAt,
887
+ ...signed,
398
888
  });
399
889
  doc = await db.createDocument({
400
890
  idPrefix: "userkey",
@@ -410,6 +900,8 @@ class UserKeyManager {
410
900
  usernameHashVersion,
411
901
  publicKey: pair.publicKey,
412
902
  fingerprint,
903
+ createdAt,
904
+ ...signed,
413
905
  });
414
906
  }
415
907
  mintedHere = true;
@@ -451,10 +943,12 @@ class UserKeyManager {
451
943
  }
452
944
  const live = (0, validateUserKeyDocument_1.asUserKeyPayload)(doc.getData()) ?? payload;
453
945
  const localPublic = await this.getLocalUserPublicKey();
454
- const epoch = (0, types_2.currentUserKeyEpoch)(live);
946
+ // "Current" only among generations that count (§9.3); write into `live`.
947
+ const valid = (await this.validated(doc))?.payload ?? payload;
948
+ const epoch = (0, types_2.currentUserKeyEpoch)(valid);
455
949
  if (!epoch || !localPublic)
456
950
  return;
457
- const gen = live.userKeys[epoch];
951
+ const gen = live.userKeys[epoch] ?? valid.userKeys[epoch];
458
952
  if (!gen || gen.publicKey !== localPublic)
459
953
  return;
460
954
  const directory = await this.directory();
@@ -488,16 +982,13 @@ class UserKeyManager {
488
982
  }
489
983
  continue;
490
984
  }
491
- deviceWraps[deviceFp] = {
492
- wrappedKey: await (0, UserKeyDocument_1.wrapPrivateKeyForDevice)({
493
- cryptoAdapter: this.tenant.getCryptoAdapter(),
494
- privateKeyBytes: bytes,
495
- deviceEncryptionPublicKey: device.encryptionPublicKey,
496
- }),
497
- ...(device.label ? { label: device.label } : {}),
985
+ deviceWraps[deviceFp] = await this.buildDeviceWrap({
986
+ userKeyBytes: bytes,
987
+ deviceEncryptionPublicKey: device.encryptionPublicKey,
988
+ label: device.label,
498
989
  approvedAt: now,
499
990
  approvedBySigningPublicKey: signer.userSigningPublicKey,
500
- };
991
+ });
501
992
  changed = true;
502
993
  }
503
994
  if (!changed)
@@ -507,6 +998,21 @@ class UserKeyManager {
507
998
  [epoch]: { ...gen, deviceWraps },
508
999
  });
509
1000
  }
1001
+ /** A device wrap of the User-Key, plus the self-grant key when this device holds it. */
1002
+ async buildDeviceWrap(input) {
1003
+ const wrappedSelfGrantKey = await this.wrapSelfGrantKeyFor(input.deviceEncryptionPublicKey);
1004
+ return {
1005
+ wrappedKey: await (0, UserKeyDocument_1.wrapPrivateKeyForDevice)({
1006
+ cryptoAdapter: this.tenant.getCryptoAdapter(),
1007
+ privateKeyBytes: input.userKeyBytes,
1008
+ deviceEncryptionPublicKey: input.deviceEncryptionPublicKey,
1009
+ }),
1010
+ ...(input.label ? { label: input.label } : {}),
1011
+ approvedAt: input.approvedAt,
1012
+ approvedBySigningPublicKey: input.approvedBySigningPublicKey,
1013
+ ...(wrappedSelfGrantKey ? { wrappedSelfGrantKey } : {}),
1014
+ };
1015
+ }
510
1016
  async tryImportUserKeyFromWrap(payload) {
511
1017
  const user = this.tenant.getCurrentPrivateUser();
512
1018
  const deviceKey = await this.tenant.getEncryptionPrivateKeyForReconcile();
@@ -533,6 +1039,12 @@ class UserKeyManager {
533
1039
  this.decryptedUserKeyCache = null;
534
1040
  this.seededBytesMatchLocalPublic = true;
535
1041
  this.seededBytesVerifiedForPublicKey = gen.publicKey;
1042
+ // The person's self-grant key travels with their User-Key and
1043
+ // replaces one this device may have made for itself before joining.
1044
+ if (wrap.wrappedSelfGrantKey) {
1045
+ this.decryptedSelfGrantBytes = null;
1046
+ await this.importSelfGrantKeyFromWrap().catch(() => null);
1047
+ }
536
1048
  return true;
537
1049
  }
538
1050
  catch {
@@ -546,7 +1058,7 @@ class UserKeyManager {
546
1058
  if (!localPublic) {
547
1059
  throw new types_2.UserKeyMismatchError("This device has no local User-Key to compare against the published document");
548
1060
  }
549
- const live = (0, validateUserKeyDocument_1.asUserKeyPayload)(doc.getData()) ?? payload;
1061
+ const live = (await this.validated(doc))?.payload ?? (0, validateUserKeyDocument_1.asUserKeyPayload)(doc.getData()) ?? payload;
550
1062
  const currentEpoch = (0, types_2.currentUserKeyEpoch)(live);
551
1063
  if (!currentEpoch) {
552
1064
  throw new types_2.UserKeyMismatchError("Published user-key document has no generations");
@@ -580,22 +1092,18 @@ class UserKeyManager {
580
1092
  if (!bytes) {
581
1093
  throw new types_2.UserKeyMismatchError("Local User-Key private material is unavailable");
582
1094
  }
583
- const wrapped = await (0, UserKeyDocument_1.wrapPrivateKeyForDevice)({
584
- cryptoAdapter: this.tenant.getCryptoAdapter(),
585
- privateKeyBytes: bytes,
586
- deviceEncryptionPublicKey: user.userEncryptionKeyPair.publicKey,
587
- });
588
1095
  await (0, UserKeyDocument_1.writeDeviceWrap)({
589
1096
  db,
590
1097
  doc,
591
1098
  epoch,
592
1099
  deviceFingerprint: deviceFp,
593
- wrap: {
594
- wrappedKey: wrapped,
595
- ...(ownLabel ? { label: ownLabel } : {}),
1100
+ wrap: await this.buildDeviceWrap({
1101
+ userKeyBytes: bytes,
1102
+ deviceEncryptionPublicKey: user.userEncryptionKeyPair.publicKey,
1103
+ label: ownLabel,
596
1104
  approvedAt: Date.now(),
597
1105
  approvedBySigningPublicKey: signer.userSigningPublicKey,
598
- },
1106
+ }),
599
1107
  });
600
1108
  }
601
1109
  }
@@ -610,13 +1118,27 @@ class UserKeyManager {
610
1118
  const epoch = (0, types_2.currentUserKeyEpoch)(payload) ?? "1";
611
1119
  const gen = payload.userKeys[epoch];
612
1120
  const fingerprint = await (0, UserKeyDocument_1.fingerprintUserPublicKey)(pair.publicKey, this.tenant.getCryptoAdapter());
1121
+ const createdAt = gen?.createdAt ?? Date.now();
1122
+ const bytes = await this.getDecryptedUserKeyBytes();
1123
+ // Adopting a pending document counts only self-signed (§9.3, rule 3).
1124
+ const signed = bytes
1125
+ ? await this.generationSignatureFields({
1126
+ usernameHash: payload.username_hash,
1127
+ epoch,
1128
+ gen: { publicKey: pair.publicKey, fingerprint, createdAt },
1129
+ ownBytes: bytes,
1130
+ selfSign: true,
1131
+ })
1132
+ : {};
1133
+ const raw = (0, validateUserKeyDocument_1.asUserKeyPayload)(doc.getData()) ?? payload;
613
1134
  await (0, UserKeyDocument_1.replaceUserKeys)(doc.getDatabase(), doc, {
614
- ...payload.userKeys,
1135
+ ...raw.userKeys,
615
1136
  [epoch]: {
616
1137
  publicKey: pair.publicKey,
617
1138
  fingerprint,
618
- createdAt: gen?.createdAt ?? Date.now(),
1139
+ createdAt,
619
1140
  deviceWraps: {},
1141
+ ...signed,
620
1142
  },
621
1143
  });
622
1144
  const db = await this.openUserDirectory();
@@ -649,6 +1171,11 @@ class UserKeyManager {
649
1171
  const canUsePlantedKey = Boolean(localPublic && published && localPublic === published && bytes);
650
1172
  try {
651
1173
  if (canUsePlantedKey) {
1174
+ // While still pending, give generation 1 its signing key first, so the
1175
+ // self-signed change counts as an adoption (§9.3, rule 3).
1176
+ const current = await this.validated(doc);
1177
+ if (current)
1178
+ await this.ensureCurrentGenerationSigningKey(current);
652
1179
  await this.writeOwnDeviceWrap(doc, payload);
653
1180
  await this.wrapPrivateKeyForActiveGrantDevices(doc, payload, {
654
1181
  legacyUnstampedOnly: true,
@@ -766,6 +1293,18 @@ class UserKeyManager {
766
1293
  }
767
1294
  }
768
1295
  await this.dropRevokedDeviceWraps(resolved.doc, resolved.payload);
1296
+ try {
1297
+ const latest = (await this.resolveOwnUserKeyDocument()) ?? (await this.validated(resolved.doc));
1298
+ if (latest) {
1299
+ await this.ensureCurrentGenerationSigningKey(latest);
1300
+ const afterSign = (await this.resolveOwnUserKeyDocument()) ?? latest;
1301
+ await this.ensureSelfGrantKeyDistributed(afterSign);
1302
+ resolved = (await this.resolveOwnUserKeyDocument()) ?? afterSign;
1303
+ }
1304
+ }
1305
+ catch (error) {
1306
+ console.warn("[userkeys] generation signing key / self-grant key upkeep failed", error);
1307
+ }
769
1308
  return this.enrollmentStatus(resolved.payload, this.waitState());
770
1309
  }
771
1310
  catch (error) {
@@ -871,22 +1410,18 @@ class UserKeyManager {
871
1410
  });
872
1411
  if (!bytes)
873
1412
  continue;
874
- const wrapped = await (0, UserKeyDocument_1.wrapPrivateKeyForDevice)({
875
- cryptoAdapter: this.tenant.getCryptoAdapter(),
876
- privateKeyBytes: bytes,
877
- deviceEncryptionPublicKey: encryptionPublicKey,
878
- });
879
1413
  await (0, UserKeyDocument_1.writeDeviceWrap)({
880
1414
  db,
881
1415
  doc: resolved.doc,
882
1416
  epoch,
883
1417
  deviceFingerprint: fingerprint,
884
- wrap: {
885
- wrappedKey: wrapped,
886
- ...(label ? { label } : {}),
1418
+ wrap: await this.buildDeviceWrap({
1419
+ userKeyBytes: bytes,
1420
+ deviceEncryptionPublicKey: encryptionPublicKey,
1421
+ label,
887
1422
  approvedAt: Date.now(),
888
1423
  approvedBySigningPublicKey: signer.userSigningPublicKey,
889
- },
1424
+ }),
890
1425
  });
891
1426
  wroteWrap = true;
892
1427
  }
@@ -916,22 +1451,18 @@ class UserKeyManager {
916
1451
  const bytes = await this.privateKeyBytesForEpoch(resolved.payload, epoch);
917
1452
  if (!bytes)
918
1453
  continue;
919
- const wrapped = await (0, UserKeyDocument_1.wrapPrivateKeyForDevice)({
920
- cryptoAdapter: this.tenant.getCryptoAdapter(),
921
- privateKeyBytes: bytes,
922
- deviceEncryptionPublicKey: match.encryptionPublicKey,
923
- });
924
1454
  await (0, UserKeyDocument_1.writeDeviceWrap)({
925
1455
  db,
926
1456
  doc: resolved.doc,
927
1457
  epoch,
928
1458
  deviceFingerprint: fingerprint,
929
- wrap: {
930
- wrappedKey: wrapped,
931
- ...(match.label ? { label: match.label } : {}),
1459
+ wrap: await this.buildDeviceWrap({
1460
+ userKeyBytes: bytes,
1461
+ deviceEncryptionPublicKey: match.encryptionPublicKey,
1462
+ label: match.label,
932
1463
  approvedAt: Date.now(),
933
1464
  approvedBySigningPublicKey: signer.userSigningPublicKey,
934
- },
1465
+ }),
935
1466
  });
936
1467
  wroteWrap = true;
937
1468
  }
@@ -977,10 +1508,13 @@ class UserKeyManager {
977
1508
  if (!current) {
978
1509
  throw new Error("Cannot rotate: user-key document has no generations");
979
1510
  }
980
- const nextEpoch = (BigInt(current) + 1n).toString();
981
- if (resolved.payload.userKeys[nextEpoch]) {
982
- throw new Error(`Cannot rotate: epoch ${nextEpoch} already exists`);
983
- }
1511
+ // Above every stored epoch, including ones that do not count (§9.3).
1512
+ const highestStored = Object.keys(resolved.rawPayload.userKeys ?? {})
1513
+ .filter((e) => /^\d+$/.test(e))
1514
+ .reduce((max, e) => (BigInt(e) > max ? BigInt(e) : max), BigInt(current));
1515
+ const nextEpoch = (highestStored + 1n).toString();
1516
+ const previousGen = resolved.payload.userKeys[current];
1517
+ const previousBytes = await this.privateKeyBytesForEpoch(resolved.payload, current);
984
1518
  const user = this.tenant.getCurrentPrivateUser();
985
1519
  const generated = await (0, rsaOaep3072_1.generateRsaOaep3072)(this.tenant.getCryptoAdapter());
986
1520
  const encrypted = await (0, privateKeyEncryption_1.encryptPrivateKey)(this.tenant.getCryptoAdapter(), generated.privateKeyBytes, this.tenant.getCurrentUserPassword(), types_2.USERKEY_PRIVATE_SALT);
@@ -993,33 +1527,44 @@ class UserKeyManager {
993
1527
  const fingerprint = await (0, UserKeyDocument_1.fingerprintUserPublicKey)(generated.publicKeyPem, this.tenant.getCryptoAdapter());
994
1528
  const signer = await this.tenant.getCurrentUserId();
995
1529
  const now = Date.now();
996
- const directory = await this.directory();
997
- const overview = await directory.getUserGrantOverview(signer.username);
1530
+ const signed = await this.generationSignatureFields({
1531
+ usernameHash: resolved.payload.username_hash,
1532
+ epoch: nextEpoch,
1533
+ gen: { publicKey: generated.publicKeyPem, fingerprint, createdAt: now },
1534
+ ownBytes: generated.privateKeyBytes,
1535
+ previous: previousGen && previousBytes ? { gen: previousGen, bytes: previousBytes } : null,
1536
+ });
1537
+ if (signed.signatures.length === 0) {
1538
+ const cutoff = await this.generationSignatureCutoff(resolved.payload.username_hash);
1539
+ if (cutoff !== undefined && now >= cutoff) {
1540
+ throw new Error("Cannot rotate: this device can sign the new User-Key generation neither with the previous generation nor with a granted device self-grant key, so it would not count.");
1541
+ }
1542
+ }
1543
+ const devices = await this.devicesForWrapping(signer.username);
998
1544
  const previousWraps = resolved.payload.userKeys[current]?.deviceWraps ?? {};
999
1545
  const deviceWraps = {};
1000
- for (const device of overview.activeDevices) {
1546
+ for (const device of devices) {
1001
1547
  const deviceFp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(device.encryptionPublicKey, this.tenant.getCryptoAdapter().getSubtle());
1002
1548
  if (!previousWraps[deviceFp])
1003
1549
  continue;
1004
- deviceWraps[deviceFp] = {
1005
- wrappedKey: await (0, UserKeyDocument_1.wrapPrivateKeyForDevice)({
1006
- cryptoAdapter: this.tenant.getCryptoAdapter(),
1007
- privateKeyBytes: generated.privateKeyBytes,
1008
- deviceEncryptionPublicKey: device.encryptionPublicKey,
1009
- }),
1010
- ...(device.label ? { label: device.label } : {}),
1550
+ deviceWraps[deviceFp] = await this.buildDeviceWrap({
1551
+ userKeyBytes: generated.privateKeyBytes,
1552
+ deviceEncryptionPublicKey: device.encryptionPublicKey,
1553
+ label: device.label,
1011
1554
  approvedAt: now,
1012
1555
  approvedBySigningPublicKey: signer.userSigningPublicKey,
1013
- };
1556
+ });
1014
1557
  }
1558
+ const raw = (0, validateUserKeyDocument_1.asUserKeyPayload)(resolved.doc.getData()) ?? resolved.rawPayload;
1015
1559
  await (0, UserKeyDocument_1.replaceUserKeys)(resolved.doc.getDatabase(), resolved.doc, {
1016
- ...resolved.payload.userKeys,
1017
- [current]: { ...resolved.payload.userKeys[current], retiredAt: now },
1560
+ ...raw.userKeys,
1561
+ [current]: { ...(raw.userKeys[current] ?? previousGen), retiredAt: now },
1018
1562
  [nextEpoch]: {
1019
1563
  publicKey: generated.publicKeyPem,
1020
1564
  fingerprint,
1021
1565
  createdAt: now,
1022
1566
  deviceWraps,
1567
+ ...signed,
1023
1568
  },
1024
1569
  });
1025
1570
  }
@@ -1202,7 +1747,9 @@ class UserKeyManager {
1202
1747
  return;
1203
1748
  let changed = false;
1204
1749
  const userKeys = {};
1205
- for (const [epoch, gen] of Object.entries(payload.userKeys ?? {})) {
1750
+ // Write back every stored generation, also those that do not count.
1751
+ const raw = (0, validateUserKeyDocument_1.asUserKeyPayload)(doc.getData()) ?? payload;
1752
+ for (const [epoch, gen] of Object.entries(raw.userKeys ?? {})) {
1206
1753
  const deviceWraps = { ...(gen.deviceWraps ?? {}) };
1207
1754
  for (const fp of Object.keys(deviceWraps)) {
1208
1755
  if (revokedFps.has(fp)) {