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.
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +17 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +63 -4
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts +10 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +45 -3
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +22 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +33 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/browser/index.mjs +37 -0
- package/dist/core/BaseMindooDB.d.ts +9 -0
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +65 -2
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +75 -1
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +229 -7
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +165 -2
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +648 -112
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/BaseMindooTenantFactory.d.ts +30 -0
- package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantFactory.js +95 -0
- package/dist/core/BaseMindooTenantFactory.js.map +1 -1
- package/dist/core/accesscontrol/DirectoryStateNode.d.ts +45 -0
- package/dist/core/accesscontrol/DirectoryStateNode.d.ts.map +1 -1
- package/dist/core/accesscontrol/DirectoryStateNode.js +58 -0
- package/dist/core/accesscontrol/DirectoryStateNode.js.map +1 -1
- package/dist/core/accesscontrol/adminKeys.d.ts +130 -0
- package/dist/core/accesscontrol/adminKeys.d.ts.map +1 -0
- package/dist/core/accesscontrol/adminKeys.js +164 -0
- package/dist/core/accesscontrol/adminKeys.js.map +1 -0
- package/dist/core/accesscontrol/directoryAdminResolver.d.ts +54 -0
- package/dist/core/accesscontrol/directoryAdminResolver.d.ts.map +1 -0
- package/dist/core/accesscontrol/directoryAdminResolver.js +217 -0
- package/dist/core/accesscontrol/directoryAdminResolver.js.map +1 -0
- package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -1
- package/dist/core/accesscontrol/directoryProjection.js +25 -0
- package/dist/core/accesscontrol/directoryProjection.js.map +1 -1
- package/dist/core/accesscontrol/grantKeys.d.ts +16 -0
- package/dist/core/accesscontrol/grantKeys.d.ts.map +1 -1
- package/dist/core/accesscontrol/grantKeys.js +37 -0
- package/dist/core/accesscontrol/grantKeys.js.map +1 -1
- package/dist/core/accesscontrol/types.d.ts +11 -2
- package/dist/core/accesscontrol/types.d.ts.map +1 -1
- package/dist/core/accesscontrol/types.js +7 -0
- package/dist/core/accesscontrol/types.js.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.js +8 -0
- package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts +16 -1
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.d.ts +13 -0
- package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.js.map +1 -1
- package/dist/core/builtinDbInvariants.d.ts +18 -2
- package/dist/core/builtinDbInvariants.d.ts.map +1 -1
- package/dist/core/builtinDbInvariants.js +41 -5
- package/dist/core/builtinDbInvariants.js.map +1 -1
- package/dist/core/index.d.ts +4 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +26 -5
- package/dist/core/index.js.map +1 -1
- package/dist/core/live/LiveChannel.d.ts +41 -0
- package/dist/core/live/LiveChannel.d.ts.map +1 -0
- package/dist/core/live/LiveChannel.js +151 -0
- package/dist/core/live/LiveChannel.js.map +1 -0
- package/dist/core/types.d.ts +182 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/uri/MindooURI.d.ts +1 -1
- package/dist/core/uri/MindooURI.d.ts.map +1 -1
- package/dist/core/uri/MindooURI.js +2 -0
- package/dist/core/uri/MindooURI.js.map +1 -1
- package/dist/core/uri/adminUri.d.ts +52 -0
- package/dist/core/uri/adminUri.d.ts.map +1 -0
- package/dist/core/uri/adminUri.js +100 -0
- package/dist/core/uri/adminUri.js.map +1 -0
- package/dist/core/uri/joinRequestUri.d.ts.map +1 -1
- package/dist/core/uri/joinRequestUri.js +14 -1
- package/dist/core/uri/joinRequestUri.js.map +1 -1
- package/dist/core/userid/index.d.ts +9 -0
- package/dist/core/userid/index.d.ts.map +1 -1
- package/dist/core/userkeys/UserKeyDocument.d.ts +7 -1
- package/dist/core/userkeys/UserKeyDocument.d.ts.map +1 -1
- package/dist/core/userkeys/UserKeyDocument.js +14 -3
- package/dist/core/userkeys/UserKeyDocument.js.map +1 -1
- package/dist/core/userkeys/UserKeyManager.d.ts +95 -4
- package/dist/core/userkeys/UserKeyManager.d.ts.map +1 -1
- package/dist/core/userkeys/UserKeyManager.js +606 -59
- package/dist/core/userkeys/UserKeyManager.js.map +1 -1
- package/dist/core/userkeys/deviceCertificate.d.ts +46 -0
- package/dist/core/userkeys/deviceCertificate.d.ts.map +1 -0
- package/dist/core/userkeys/deviceCertificate.js +86 -0
- package/dist/core/userkeys/deviceCertificate.js.map +1 -0
- package/dist/core/userkeys/generationSignature.d.ts +32 -0
- package/dist/core/userkeys/generationSignature.d.ts.map +1 -0
- package/dist/core/userkeys/generationSignature.js +136 -0
- package/dist/core/userkeys/generationSignature.js.map +1 -0
- package/dist/core/userkeys/generationValidity.d.ts +75 -0
- package/dist/core/userkeys/generationValidity.d.ts.map +1 -0
- package/dist/core/userkeys/generationValidity.js +175 -0
- package/dist/core/userkeys/generationValidity.js.map +1 -0
- package/dist/core/userkeys/index.d.ts +5 -2
- package/dist/core/userkeys/index.d.ts.map +1 -1
- package/dist/core/userkeys/index.js +21 -1
- package/dist/core/userkeys/index.js.map +1 -1
- package/dist/core/userkeys/resolveUserKeyDocument.d.ts +16 -0
- package/dist/core/userkeys/resolveUserKeyDocument.d.ts.map +1 -1
- package/dist/core/userkeys/resolveUserKeyDocument.js +42 -1
- package/dist/core/userkeys/resolveUserKeyDocument.js.map +1 -1
- package/dist/core/userkeys/types.d.ts +26 -0
- package/dist/core/userkeys/types.d.ts.map +1 -1
- package/dist/core/userkeys/types.js +3 -1
- package/dist/core/userkeys/types.js.map +1 -1
- package/dist/core/userkeys/validateUserKeyDocument.d.ts.map +1 -1
- package/dist/core/userkeys/validateUserKeyDocument.js +7 -0
- package/dist/core/userkeys/validateUserKeyDocument.js.map +1 -1
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +11 -0
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/server/SyncEventBus.d.ts +22 -0
- package/dist/server/SyncEventBus.d.ts.map +1 -1
- package/dist/server/SyncEventBus.js +21 -0
- package/dist/server/SyncEventBus.js.map +1 -1
- package/dist/server/TenantManager.d.ts +9 -0
- package/dist/server/TenantManager.d.ts.map +1 -1
- package/dist/server/TenantManager.js +96 -0
- package/dist/server/TenantManager.js.map +1 -1
- package/dist/server/http/MindooDBServerCore.d.ts +39 -1
- package/dist/server/http/MindooDBServerCore.d.ts.map +1 -1
- package/dist/server/http/MindooDBServerCore.js +121 -2
- package/dist/server/http/MindooDBServerCore.js.map +1 -1
- package/dist/server/http/types.d.ts +10 -1
- package/dist/server/http/types.d.ts.map +1 -1
- package/dist/server/http/types.js.map +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -1
- package/dist/server/index.js.map +1 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
595
|
-
|
|
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
|
-
...
|
|
1135
|
+
...raw.userKeys,
|
|
615
1136
|
[epoch]: {
|
|
616
1137
|
publicKey: pair.publicKey,
|
|
617
1138
|
fingerprint,
|
|
618
|
-
createdAt
|
|
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
|
-
|
|
886
|
-
|
|
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
|
-
|
|
931
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
|
997
|
-
|
|
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
|
|
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
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
...
|
|
1017
|
-
[current]: { ...
|
|
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
|
-
|
|
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)) {
|