mindoodb 0.0.51 → 0.0.53
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/README-server.md +11 -2
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +13 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +45 -20
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts +2 -0
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +6 -62
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +24 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +75 -24
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/browser/index.mjs +57 -0
- package/dist/core/BaseMindooDB.d.ts +85 -10
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +687 -196
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +59 -7
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +407 -86
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +61 -9
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +363 -35
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/BaseMindooTenantFactory.d.ts +17 -1
- package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantFactory.js +286 -17
- package/dist/core/BaseMindooTenantFactory.js.map +1 -1
- package/dist/core/accesscontrol/DirectoryStateNode.d.ts +68 -1
- package/dist/core/accesscontrol/DirectoryStateNode.d.ts.map +1 -1
- package/dist/core/accesscontrol/DirectoryStateNode.js +157 -4
- package/dist/core/accesscontrol/DirectoryStateNode.js.map +1 -1
- package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -1
- package/dist/core/accesscontrol/directoryProjection.js +14 -5
- package/dist/core/accesscontrol/directoryProjection.js.map +1 -1
- package/dist/core/accesscontrol/grantKeys.d.ts +17 -14
- package/dist/core/accesscontrol/grantKeys.d.ts.map +1 -1
- package/dist/core/accesscontrol/grantKeys.js +51 -21
- package/dist/core/accesscontrol/grantKeys.js.map +1 -1
- package/dist/core/accesscontrol/types.d.ts +2 -1
- package/dist/core/accesscontrol/types.d.ts.map +1 -1
- package/dist/core/accesscontrol/types.js +3 -2
- package/dist/core/accesscontrol/types.js.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts +7 -0
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.js +70 -14
- package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
- package/dist/core/appendonlystores/network/entryWireCodec.d.ts +51 -0
- package/dist/core/appendonlystores/network/entryWireCodec.d.ts.map +1 -0
- package/dist/core/appendonlystores/network/entryWireCodec.js +93 -0
- package/dist/core/appendonlystores/network/entryWireCodec.js.map +1 -0
- package/dist/core/builtinDbInvariants.d.ts +53 -0
- package/dist/core/builtinDbInvariants.d.ts.map +1 -0
- package/dist/core/builtinDbInvariants.js +147 -0
- package/dist/core/builtinDbInvariants.js.map +1 -0
- package/dist/core/copy/feasibility.d.ts.map +1 -1
- package/dist/core/copy/feasibility.js +3 -0
- package/dist/core/copy/feasibility.js.map +1 -1
- package/dist/core/copy/flatten.d.ts.map +1 -1
- package/dist/core/copy/flatten.js +6 -1
- package/dist/core/copy/flatten.js.map +1 -1
- package/dist/core/copy/host.d.ts +5 -0
- package/dist/core/copy/host.d.ts.map +1 -1
- package/dist/core/copy/preflight.d.ts.map +1 -1
- package/dist/core/copy/preflight.js +7 -1
- package/dist/core/copy/preflight.js.map +1 -1
- package/dist/core/copy/types.d.ts +7 -0
- package/dist/core/copy/types.d.ts.map +1 -1
- package/dist/core/crypto/EntrySignature.d.ts +8 -2
- package/dist/core/crypto/EntrySignature.d.ts.map +1 -1
- package/dist/core/crypto/EntrySignature.js +14 -5
- package/dist/core/crypto/EntrySignature.js.map +1 -1
- package/dist/core/crypto/MindooDocSigner.d.ts.map +1 -1
- package/dist/core/crypto/MindooDocSigner.js +11 -2
- package/dist/core/crypto/MindooDocSigner.js.map +1 -1
- package/dist/core/crypto/RSAEncryption.d.ts +19 -0
- package/dist/core/crypto/RSAEncryption.d.ts.map +1 -1
- package/dist/core/crypto/RSAEncryption.js +36 -3
- package/dist/core/crypto/RSAEncryption.js.map +1 -1
- package/dist/core/crypto/privateKeyEncryption.js +1 -1
- package/dist/core/crypto/privateKeyEncryption.js.map +1 -1
- package/dist/core/crypto/rsaOaep3072.d.ts +12 -0
- package/dist/core/crypto/rsaOaep3072.d.ts.map +1 -0
- package/dist/core/crypto/rsaOaep3072.js +30 -0
- package/dist/core/crypto/rsaOaep3072.js.map +1 -0
- package/dist/core/index.d.ts +7 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +30 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/join/deviceJoinBootstrap.d.ts +23 -0
- package/dist/core/join/deviceJoinBootstrap.d.ts.map +1 -0
- package/dist/core/join/deviceJoinBootstrap.js +37 -0
- package/dist/core/join/deviceJoinBootstrap.js.map +1 -0
- package/dist/core/keys/KeyBag.d.ts +20 -2
- package/dist/core/keys/KeyBag.d.ts.map +1 -1
- package/dist/core/keys/KeyBag.js +22 -10
- package/dist/core/keys/KeyBag.js.map +1 -1
- package/dist/core/logging/MindooLogger.d.ts.map +1 -1
- package/dist/core/logging/MindooLogger.js +65 -40
- package/dist/core/logging/MindooLogger.js.map +1 -1
- package/dist/core/tenantIdValidation.d.ts.map +1 -1
- package/dist/core/tenantIdValidation.js +1 -1
- package/dist/core/tenantIdValidation.js.map +1 -1
- package/dist/core/tenantSetup.d.ts +94 -9
- package/dist/core/tenantSetup.d.ts.map +1 -1
- package/dist/core/tenantSetup.js +208 -20
- package/dist/core/tenantSetup.js.map +1 -1
- package/dist/core/types.d.ts +295 -33
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +14 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/uri/joinRequestUri.d.ts.map +1 -1
- package/dist/core/uri/joinRequestUri.js +12 -3
- package/dist/core/uri/joinRequestUri.js.map +1 -1
- package/dist/core/userid/canonicalUsername.d.ts +66 -0
- package/dist/core/userid/canonicalUsername.d.ts.map +1 -0
- package/dist/core/userid/canonicalUsername.js +235 -0
- package/dist/core/userid/canonicalUsername.js.map +1 -0
- package/dist/core/userid/index.d.ts +9 -0
- package/dist/core/userid/index.d.ts.map +1 -1
- package/dist/core/userid/index.js +14 -0
- package/dist/core/userid/index.js.map +1 -1
- package/dist/core/userkeys/UserKeyDocument.d.ts +50 -0
- package/dist/core/userkeys/UserKeyDocument.d.ts.map +1 -0
- package/dist/core/userkeys/UserKeyDocument.js +145 -0
- package/dist/core/userkeys/UserKeyDocument.js.map +1 -0
- package/dist/core/userkeys/UserKeyManager.d.ts +170 -0
- package/dist/core/userkeys/UserKeyManager.d.ts.map +1 -0
- package/dist/core/userkeys/UserKeyManager.js +1277 -0
- package/dist/core/userkeys/UserKeyManager.js.map +1 -0
- package/dist/core/userkeys/fingerprint.d.ts +17 -0
- package/dist/core/userkeys/fingerprint.d.ts.map +1 -0
- package/dist/core/userkeys/fingerprint.js +59 -0
- package/dist/core/userkeys/fingerprint.js.map +1 -0
- package/dist/core/userkeys/index.d.ts +11 -0
- package/dist/core/userkeys/index.d.ts.map +1 -0
- package/dist/core/userkeys/index.js +49 -0
- package/dist/core/userkeys/index.js.map +1 -0
- package/dist/core/userkeys/recipients.d.ts +51 -0
- package/dist/core/userkeys/recipients.d.ts.map +1 -0
- package/dist/core/userkeys/recipients.js +292 -0
- package/dist/core/userkeys/recipients.js.map +1 -0
- package/dist/core/userkeys/resolveUserKeyDocument.d.ts +21 -0
- package/dist/core/userkeys/resolveUserKeyDocument.d.ts.map +1 -0
- package/dist/core/userkeys/resolveUserKeyDocument.js +98 -0
- package/dist/core/userkeys/resolveUserKeyDocument.js.map +1 -0
- package/dist/core/userkeys/sealedCrypto.d.ts +21 -0
- package/dist/core/userkeys/sealedCrypto.d.ts.map +1 -0
- package/dist/core/userkeys/sealedCrypto.js +79 -0
- package/dist/core/userkeys/sealedCrypto.js.map +1 -0
- package/dist/core/userkeys/sealedTypes.d.ts +72 -0
- package/dist/core/userkeys/sealedTypes.d.ts.map +1 -0
- package/dist/core/userkeys/sealedTypes.js +14 -0
- package/dist/core/userkeys/sealedTypes.js.map +1 -0
- package/dist/core/userkeys/types.d.ts +70 -0
- package/dist/core/userkeys/types.d.ts.map +1 -0
- package/dist/core/userkeys/types.js +48 -0
- package/dist/core/userkeys/types.js.map +1 -0
- package/dist/core/userkeys/validateUserKeyDocument.d.ts +19 -0
- package/dist/core/userkeys/validateUserKeyDocument.d.ts.map +1 -0
- package/dist/core/userkeys/validateUserKeyDocument.js +42 -0
- package/dist/core/userkeys/validateUserKeyDocument.js.map +1 -0
- package/dist/node/server/CapabilityMatcher.d.ts +5 -0
- package/dist/node/server/CapabilityMatcher.d.ts.map +1 -1
- package/dist/node/server/CapabilityMatcher.js +10 -1
- package/dist/node/server/CapabilityMatcher.js.map +1 -1
- package/dist/node/server/DeviceDiscoveryService.d.ts +36 -0
- package/dist/node/server/DeviceDiscoveryService.d.ts.map +1 -0
- package/dist/node/server/DeviceDiscoveryService.js +165 -0
- package/dist/node/server/DeviceDiscoveryService.js.map +1 -0
- package/dist/node/server/MindooDBServer.d.ts +13 -0
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +99 -61
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/SystemAdminAuth.d.ts +7 -0
- package/dist/node/server/SystemAdminAuth.d.ts.map +1 -1
- package/dist/node/server/SystemAdminAuth.js +4 -1
- package/dist/node/server/SystemAdminAuth.js.map +1 -1
- package/dist/node/server/TenantManager.d.ts +29 -0
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +146 -7
- package/dist/node/server/TenantManager.js.map +1 -1
- package/package.json +5 -5
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BaseMindooTenant = void 0;
|
|
4
4
|
const types_1 = require("./types");
|
|
5
|
+
const userid_1 = require("./userid");
|
|
5
6
|
const BaseMindooDB_1 = require("./BaseMindooDB");
|
|
6
7
|
const BaseMindooTenantDirectory_1 = require("./BaseMindooTenantDirectory");
|
|
7
8
|
const grantKeys_1 = require("./accesscontrol/grantKeys");
|
|
9
|
+
const UserKeyManager_1 = require("./userkeys/UserKeyManager");
|
|
10
|
+
const types_2 = require("./userkeys/types");
|
|
11
|
+
const validateUserKeyDocument_1 = require("./userkeys/validateUserKeyDocument");
|
|
12
|
+
const sealedTypes_1 = require("./userkeys/sealedTypes");
|
|
13
|
+
const recipients_1 = require("./userkeys/recipients");
|
|
14
|
+
const sealedCrypto_1 = require("./userkeys/sealedCrypto");
|
|
8
15
|
const MindooDocSigner_1 = require("./crypto/MindooDocSigner");
|
|
9
16
|
const RSAEncryption_1 = require("./crypto/RSAEncryption");
|
|
17
|
+
const deviceJoinBootstrap_1 = require("./join/deviceJoinBootstrap");
|
|
10
18
|
const privateKeyEncryption_1 = require("./crypto/privateKeyEncryption");
|
|
11
19
|
const EntrySignature_1 = require("./crypto/EntrySignature");
|
|
12
20
|
const DomainStatement_1 = require("./crypto/DomainStatement");
|
|
@@ -48,6 +56,7 @@ class BaseMindooTenant {
|
|
|
48
56
|
*/
|
|
49
57
|
preDecryptedMaterial) {
|
|
50
58
|
this.databaseCache = new Map();
|
|
59
|
+
this.databaseOpenInFlight = new Map();
|
|
51
60
|
this.directoryCache = null;
|
|
52
61
|
this.remoteStoreCache = new Map();
|
|
53
62
|
/**
|
|
@@ -89,6 +98,9 @@ class BaseMindooTenant {
|
|
|
89
98
|
// recursing or overlapping. Reconcile is idempotent, so a skipped overlap is
|
|
90
99
|
// harmless — the next trigger re-runs it.
|
|
91
100
|
this.reconcileInFlight = false;
|
|
101
|
+
this.userKeyReconcileScheduled = false;
|
|
102
|
+
this.userKeys = new UserKeyManager_1.UserKeyManager(this);
|
|
103
|
+
this.sealedGenerations = new Map();
|
|
92
104
|
// Local cache support
|
|
93
105
|
this.cacheManager = null;
|
|
94
106
|
this.factory = factory;
|
|
@@ -134,6 +146,9 @@ class BaseMindooTenant {
|
|
|
134
146
|
if (preDecryptedMaterial?.encryptionKey) {
|
|
135
147
|
this.decryptedUserEncryptionKeyCache = preDecryptedMaterial.encryptionKey;
|
|
136
148
|
}
|
|
149
|
+
if (preDecryptedMaterial?.userKeyPrivateBytes) {
|
|
150
|
+
this.userKeys.seedDecryptedUserKeyBytes(preDecryptedMaterial.userKeyPrivateBytes);
|
|
151
|
+
}
|
|
137
152
|
// Set up cache if a local cache store is provided. When a pre-derived
|
|
138
153
|
// cache encryption key is available, prefer that to skip the password
|
|
139
154
|
// path and avoid retaining the plaintext password inside the encrypted
|
|
@@ -193,6 +208,148 @@ class BaseMindooTenant {
|
|
|
193
208
|
}
|
|
194
209
|
return timestamp;
|
|
195
210
|
}
|
|
211
|
+
getCurrentPrivateUser() {
|
|
212
|
+
return this.currentUser;
|
|
213
|
+
}
|
|
214
|
+
getCurrentUserPassword() {
|
|
215
|
+
return this.currentUserPassword;
|
|
216
|
+
}
|
|
217
|
+
getUserKeyManager() {
|
|
218
|
+
return this.userKeys;
|
|
219
|
+
}
|
|
220
|
+
isDatabaseOpen(dbId) {
|
|
221
|
+
return this.databaseCache.has(dbId) || this.databaseOpenInFlight.has(dbId);
|
|
222
|
+
}
|
|
223
|
+
/** Fully initialized (not merely in-flight). See UserKeyManager.canReconcileAgainstUserDirectory. */
|
|
224
|
+
isDatabaseReady(dbId) {
|
|
225
|
+
return this.databaseCache.has(dbId);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Coalesce grant-driven user-key compares onto one run after the current
|
|
229
|
+
* `updateUnifiedCache` stack unwinds. Calling reconcile synchronously from
|
|
230
|
+
* the cache pass re-enters the cache via `findGrantAccessDocuments` and
|
|
231
|
+
* unbounded-allocates.
|
|
232
|
+
*/
|
|
233
|
+
scheduleUserKeyReconcile() {
|
|
234
|
+
if (this.userKeys.isReconciling() || this.userKeyReconcileScheduled)
|
|
235
|
+
return;
|
|
236
|
+
this.userKeyReconcileScheduled = true;
|
|
237
|
+
queueMicrotask(() => {
|
|
238
|
+
this.userKeyReconcileScheduled = false;
|
|
239
|
+
if (this.userKeys.isReconciling())
|
|
240
|
+
return;
|
|
241
|
+
void this.reconcileUserKeysSafe();
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
noteUserDirectoryFetched() {
|
|
245
|
+
this.userKeys.noteUserDirectoryFetched();
|
|
246
|
+
}
|
|
247
|
+
noteUserDirectoryFetchFailed(error) {
|
|
248
|
+
this.userKeys.noteUserDirectoryFetchFailed(error);
|
|
249
|
+
}
|
|
250
|
+
async listPendingUserKeyDevices() {
|
|
251
|
+
return this.userKeys.listPendingUserKeyDevices();
|
|
252
|
+
}
|
|
253
|
+
async listUserKeyDevices() {
|
|
254
|
+
return this.userKeys.listUserKeyDevices();
|
|
255
|
+
}
|
|
256
|
+
async approveUserKeyDevice(fingerprint) {
|
|
257
|
+
return this.userKeys.approveUserKeyDevice(fingerprint);
|
|
258
|
+
}
|
|
259
|
+
async declineUserKeyDevice(fingerprint) {
|
|
260
|
+
return this.userKeys.declineUserKeyDevice(fingerprint);
|
|
261
|
+
}
|
|
262
|
+
async undoDeclineUserKeyDevice(fingerprint) {
|
|
263
|
+
return this.userKeys.undoDeclineUserKeyDevice(fingerprint);
|
|
264
|
+
}
|
|
265
|
+
async rotateUserKey() {
|
|
266
|
+
return this.userKeys.rotateUserKey();
|
|
267
|
+
}
|
|
268
|
+
async reconcileUserKeys(options) {
|
|
269
|
+
return this.userKeys.reconcile(options);
|
|
270
|
+
}
|
|
271
|
+
async reconcileUserKeysSafe() {
|
|
272
|
+
try {
|
|
273
|
+
await this.userKeys.reconcile();
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
this.logger.warn(`reconcileUserKeysSafe: ${error}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async getUserKeyEnrollmentStatus() {
|
|
280
|
+
return this.userKeys.getEnrollmentStatus();
|
|
281
|
+
}
|
|
282
|
+
async ensureLocalUserKeyPair() {
|
|
283
|
+
await this.userKeys.ensureLocalUserKeyPair(this.currentUser, this.currentUserPassword);
|
|
284
|
+
}
|
|
285
|
+
rememberSealedGenerations(keyId, generations) {
|
|
286
|
+
this.sealedGenerations.set(keyId, generations);
|
|
287
|
+
}
|
|
288
|
+
getSealedGenerations(keyId) {
|
|
289
|
+
return this.sealedGenerations.get(keyId) ?? [];
|
|
290
|
+
}
|
|
291
|
+
async ingestSealedRecipients(keyId, recipients) {
|
|
292
|
+
const cached = this.sealedGenerations.get(keyId);
|
|
293
|
+
const epoch = typeof recipients.epoch === "number" ? recipients.epoch : 0;
|
|
294
|
+
if (cached && cached.length >= epoch && epoch > 0) {
|
|
295
|
+
console.log("[sealed] ingest: cache already has generations", {
|
|
296
|
+
keyId,
|
|
297
|
+
cached: cached.length,
|
|
298
|
+
epoch,
|
|
299
|
+
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const keys = [
|
|
303
|
+
...(await this.userKeys.getUserKeyCryptoKeysForReconcile()),
|
|
304
|
+
await this.getEncryptionPrivateKeyForReconcile(),
|
|
305
|
+
].filter((k) => !!k);
|
|
306
|
+
console.log("[sealed] ingest: trying unwrap", {
|
|
307
|
+
keyId,
|
|
308
|
+
wrapCount: recipients.wraps?.length ?? 0,
|
|
309
|
+
wrapKinds: (recipients.wraps ?? []).map((wrap) => wrap.kind),
|
|
310
|
+
epoch,
|
|
311
|
+
keyCount: keys.length,
|
|
312
|
+
cached: cached?.length ?? 0,
|
|
313
|
+
});
|
|
314
|
+
if (keys.length === 0) {
|
|
315
|
+
// Keep a create-time session DEK. Without unwrap keys we cannot prove
|
|
316
|
+
// the wrap list excludes us, and deleting would make the author unable
|
|
317
|
+
// to read a document they just sealed.
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
for (const key of keys) {
|
|
321
|
+
try {
|
|
322
|
+
const gens = await (0, sealedCrypto_1.openBundle)({
|
|
323
|
+
crypto: this.cryptoAdapter,
|
|
324
|
+
recipients,
|
|
325
|
+
privateKey: key,
|
|
326
|
+
});
|
|
327
|
+
this.sealedGenerations.set(keyId, gens);
|
|
328
|
+
console.log("[sealed] ingest: unwrapped", { keyId, generations: gens.length });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// try next key
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Newest wrap list is not for us (removed, or never added). Drop any
|
|
336
|
+
// session DEK so hasDecryptionKey / visibility reconcile hide the
|
|
337
|
+
// document instead of leaving the last readable plaintext editable.
|
|
338
|
+
console.log("[sealed] ingest: unwrap failed, dropping session DEK", {
|
|
339
|
+
keyId,
|
|
340
|
+
cached: cached?.length ?? 0,
|
|
341
|
+
epoch,
|
|
342
|
+
});
|
|
343
|
+
this.sealedGenerations.delete(keyId);
|
|
344
|
+
}
|
|
345
|
+
async ingestSealedFromEntries(keyId, entries) {
|
|
346
|
+
if (!(0, sealedTypes_1.isSealedKeyId)(keyId))
|
|
347
|
+
return;
|
|
348
|
+
const newest = (0, recipients_1.newestRecipientBlock)(entries);
|
|
349
|
+
if (!newest)
|
|
350
|
+
return;
|
|
351
|
+
await this.ingestSealedRecipients(keyId, newest);
|
|
352
|
+
}
|
|
196
353
|
getFactory() {
|
|
197
354
|
return this.factory;
|
|
198
355
|
}
|
|
@@ -225,6 +382,9 @@ class BaseMindooTenant {
|
|
|
225
382
|
*/
|
|
226
383
|
async importAesKeyCached(symmetricKey, usage) {
|
|
227
384
|
let cacheKey = usage + ":";
|
|
385
|
+
if (symmetricKey.length !== 32) {
|
|
386
|
+
throw new Error(`AES-256 key must be 32 bytes, got ${symmetricKey.length}`);
|
|
387
|
+
}
|
|
228
388
|
for (let i = 0; i < symmetricKey.length; i++) {
|
|
229
389
|
cacheKey += symmetricKey[i].toString(16).padStart(2, "0");
|
|
230
390
|
}
|
|
@@ -233,9 +393,8 @@ class BaseMindooTenant {
|
|
|
233
393
|
return cached;
|
|
234
394
|
}
|
|
235
395
|
const subtle = this.cryptoAdapter.getSubtle();
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
const cryptoKey = await subtle.importKey("raw", keyArray.buffer, { name: "AES-GCM" }, false, // not extractable
|
|
396
|
+
const keyArray = symmetricKey.slice();
|
|
397
|
+
const cryptoKey = await subtle.importKey("raw", keyArray, { name: "AES-GCM" }, false, // not extractable
|
|
239
398
|
[usage]);
|
|
240
399
|
if (this.importedAesKeyCache.size >= BaseMindooTenant.MAX_IMPORTED_KEY_CACHE_ENTRIES) {
|
|
241
400
|
this.importedAesKeyCache.clear();
|
|
@@ -254,7 +413,14 @@ class BaseMindooTenant {
|
|
|
254
413
|
// Get the symmetric key for this key ID
|
|
255
414
|
let symmetricKey;
|
|
256
415
|
try {
|
|
257
|
-
if (
|
|
416
|
+
if ((0, sealedTypes_1.isSealedKeyId)(decryptionKeyId)) {
|
|
417
|
+
const generations = this.sealedGenerations.get(decryptionKeyId);
|
|
418
|
+
if (!generations?.length) {
|
|
419
|
+
throw new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
|
|
420
|
+
}
|
|
421
|
+
symmetricKey = generations[0];
|
|
422
|
+
}
|
|
423
|
+
else if (decryptionKeyId === "default") {
|
|
258
424
|
// Use cached tenant encryption key if available
|
|
259
425
|
if (this.decryptedTenantKeyCache) {
|
|
260
426
|
symmetricKey = this.decryptedTenantKeyCache;
|
|
@@ -388,7 +554,9 @@ class BaseMindooTenant {
|
|
|
388
554
|
}
|
|
389
555
|
}
|
|
390
556
|
this.logger.debug(`Decrypt failed against all ${candidates.length} version(s) of key "${decryptionKeyId}"`);
|
|
391
|
-
throw lastError
|
|
557
|
+
throw lastError instanceof Error
|
|
558
|
+
? new errors_1.SymmetricKeyNotFoundError(decryptionKeyId)
|
|
559
|
+
: new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
|
|
392
560
|
}
|
|
393
561
|
/**
|
|
394
562
|
* Return every stored version of the symmetric key for `decryptionKeyId`,
|
|
@@ -400,6 +568,9 @@ class BaseMindooTenant {
|
|
|
400
568
|
* tenant-default version so encryption keeps using the latest key.
|
|
401
569
|
*/
|
|
402
570
|
async getDecryptionKeyCandidates(decryptionKeyId) {
|
|
571
|
+
if ((0, sealedTypes_1.isSealedKeyId)(decryptionKeyId)) {
|
|
572
|
+
return this.sealedGenerations.get(decryptionKeyId) ?? [];
|
|
573
|
+
}
|
|
403
574
|
const id = decryptionKeyId === "default" ? types_1.DEFAULT_TENANT_KEY_ID : decryptionKeyId;
|
|
404
575
|
const versions = await this.keyBag.getAllKeys("doc", this.tenantId, id);
|
|
405
576
|
if (decryptionKeyId === "default" && versions.length > 0) {
|
|
@@ -415,7 +586,7 @@ class BaseMindooTenant {
|
|
|
415
586
|
// Sign the payload
|
|
416
587
|
const signature = await subtle.sign({
|
|
417
588
|
name: "Ed25519",
|
|
418
|
-
}, signingKey, payload.
|
|
589
|
+
}, signingKey, payload.slice());
|
|
419
590
|
this.logger.debug(`Signed payload (signature: ${signature.byteLength} bytes)`);
|
|
420
591
|
return new Uint8Array(signature);
|
|
421
592
|
}
|
|
@@ -467,11 +638,23 @@ class BaseMindooTenant {
|
|
|
467
638
|
}, false, // not extractable
|
|
468
639
|
["sign"]);
|
|
469
640
|
// Sign the payload
|
|
470
|
-
const
|
|
641
|
+
const signatureBuffer = await subtle.sign({
|
|
471
642
|
name: "Ed25519",
|
|
472
643
|
}, signingKey, payload.buffer);
|
|
644
|
+
const signature = new Uint8Array(signatureBuffer);
|
|
645
|
+
// Prove the decrypted private key belongs to the claimed public key.
|
|
646
|
+
// Directory helpers historically paired `getAdministrationPublicKey()` with
|
|
647
|
+
// whatever private key the UI passed; admin-only then compared only the
|
|
648
|
+
// claimed PEM and accepted the write into local memory. Verifying here
|
|
649
|
+
// closes that gap before any store entry is attributed to that public key.
|
|
650
|
+
const verifyKey = await this.importVerifyKeyCached(signingKeyPair.publicKey);
|
|
651
|
+
const matches = await subtle.verify({ name: "Ed25519" }, verifyKey, signature.buffer, payload.buffer);
|
|
652
|
+
if (!matches) {
|
|
653
|
+
throw new Error("Signing key pair mismatch: the decrypted private key does not correspond to the claimed public key. "
|
|
654
|
+
+ "For admin-only databases this usually means a non-admin identity was used as the tenant admin.");
|
|
655
|
+
}
|
|
473
656
|
this.logger.debug(`Signed payload with provided key (signature: ${signature.byteLength} bytes)`);
|
|
474
|
-
return
|
|
657
|
+
return signature;
|
|
475
658
|
}
|
|
476
659
|
async verifySignature(payload, signature, publicKey) {
|
|
477
660
|
this.logger.debug(`Verifying signature`);
|
|
@@ -645,7 +828,8 @@ class BaseMindooTenant {
|
|
|
645
828
|
// Every local database belonging to this tenant: the directory plus any
|
|
646
829
|
// opened/known data databases. Wipe is a whole-tenant operation.
|
|
647
830
|
const dbIds = new Set([
|
|
648
|
-
|
|
831
|
+
types_1.DIRECTORY_DB_ID,
|
|
832
|
+
types_1.USER_DIRECTORY_DB_ID,
|
|
649
833
|
...this.databaseCache.keys(),
|
|
650
834
|
...(additionalDbIds ?? []),
|
|
651
835
|
]);
|
|
@@ -701,7 +885,7 @@ class BaseMindooTenant {
|
|
|
701
885
|
}
|
|
702
886
|
}
|
|
703
887
|
async assertCurrentUserCanOpenDB(id) {
|
|
704
|
-
if (id ===
|
|
888
|
+
if (id === types_1.DIRECTORY_DB_ID || id === types_1.USER_DIRECTORY_DB_ID) {
|
|
705
889
|
return;
|
|
706
890
|
}
|
|
707
891
|
const currentUser = await this.getCurrentUserId();
|
|
@@ -757,7 +941,7 @@ class BaseMindooTenant {
|
|
|
757
941
|
const validDbId = (0, databaseIdValidation_1.validateDatabaseId)(id, "dbId");
|
|
758
942
|
// Enforce admin-only mode for directory database - this is a security invariant
|
|
759
943
|
// The directory database must only accept entries signed by the admin key
|
|
760
|
-
const effectiveOptions = validDbId ===
|
|
944
|
+
const effectiveOptions = validDbId === types_1.DIRECTORY_DB_ID
|
|
761
945
|
? { ...options, adminOnlyDb: true }
|
|
762
946
|
: options ?? {};
|
|
763
947
|
const normalizedTimeTravelDate = this.normalizeTimeTravelDate(effectiveOptions.timeTravelDate);
|
|
@@ -765,22 +949,41 @@ class BaseMindooTenant {
|
|
|
765
949
|
? validDbId
|
|
766
950
|
: `${validDbId}::tt:${normalizedTimeTravelDate}`;
|
|
767
951
|
await this.assertCurrentUserCanOpenDB(validDbId);
|
|
768
|
-
// Return cached database if it exists
|
|
769
952
|
const cached = this.databaseCache.get(databaseCacheKey);
|
|
770
953
|
if (cached) {
|
|
771
|
-
|
|
772
|
-
if (validDbId === "directory" && !cached.isAdminOnlyDb()) {
|
|
954
|
+
if (validDbId === types_1.DIRECTORY_DB_ID && !cached.isAdminOnlyDb()) {
|
|
773
955
|
throw new Error("Directory database was cached without adminOnlyDb - this should never happen");
|
|
774
956
|
}
|
|
775
|
-
// Live databases must reflect the current KeyBag state before they
|
|
776
|
-
// are handed back to callers. Time-travel snapshots are immutable
|
|
777
|
-
// historical views and intentionally do not participate in
|
|
778
|
-
// visibility reconciliation, so we skip the call for them.
|
|
779
957
|
if (normalizedTimeTravelDate == null) {
|
|
780
958
|
await this.reconcileKeyBagChanges();
|
|
781
959
|
}
|
|
782
960
|
return cached;
|
|
783
961
|
}
|
|
962
|
+
const inFlight = this.databaseOpenInFlight.get(databaseCacheKey);
|
|
963
|
+
if (inFlight) {
|
|
964
|
+
return inFlight;
|
|
965
|
+
}
|
|
966
|
+
let resolveOpen;
|
|
967
|
+
let rejectOpen;
|
|
968
|
+
const opening = new Promise((resolve, reject) => {
|
|
969
|
+
resolveOpen = resolve;
|
|
970
|
+
rejectOpen = reject;
|
|
971
|
+
});
|
|
972
|
+
this.databaseOpenInFlight.set(databaseCacheKey, opening);
|
|
973
|
+
try {
|
|
974
|
+
const db = await this.openDBUncached(validDbId, databaseCacheKey, effectiveOptions, normalizedTimeTravelDate);
|
|
975
|
+
resolveOpen(db);
|
|
976
|
+
return db;
|
|
977
|
+
}
|
|
978
|
+
catch (error) {
|
|
979
|
+
rejectOpen(error);
|
|
980
|
+
throw error;
|
|
981
|
+
}
|
|
982
|
+
finally {
|
|
983
|
+
this.databaseOpenInFlight.delete(databaseCacheKey);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
async openDBUncached(validDbId, databaseCacheKey, effectiveOptions, normalizedTimeTravelDate) {
|
|
784
987
|
// Extract store options and DB-specific options
|
|
785
988
|
const { adminOnlyDb, timeTravelDate: _timeTravelDate, persistTimeTravelCache, autoIndexing, attachmentConfig, documentCacheConfig, snapshotConfig, performanceCallback, ...storeOptions } = effectiveOptions;
|
|
786
989
|
// Snapshot opens can share an already-open live store instance. This keeps
|
|
@@ -1056,50 +1259,23 @@ class BaseMindooTenant {
|
|
|
1056
1259
|
const signerLogger = this.logger.createChild("MindooDocSigner");
|
|
1057
1260
|
return new MindooDocSigner_1.MindooDocSigner(this, signKey, signerLogger);
|
|
1058
1261
|
}
|
|
1059
|
-
async exportJoinResponseDocKeyVersions(keyId,
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
.filter((detail) => detail.scopedKeyId === scopedKeyId)
|
|
1063
|
-
.sort((a, b) => a.versionIndex - b.versionIndex);
|
|
1064
|
-
if (details.length === 0) {
|
|
1262
|
+
async exportJoinResponseDocKeyVersions(keyId, recipientEncryptionPublicKey) {
|
|
1263
|
+
const keyVersions = await this.keyBag.getAllKeyVersions("doc", this.tenantId, keyId);
|
|
1264
|
+
if (keyVersions.length === 0) {
|
|
1065
1265
|
throw new Error(`Failed to export document key "${keyId}" for tenant "${this.tenantId}"`);
|
|
1066
1266
|
}
|
|
1267
|
+
const rsa = new RSAEncryption_1.RSAEncryption(this.cryptoAdapter, this.logger.createChild("RSAEncryption"));
|
|
1067
1268
|
const versions = [];
|
|
1068
|
-
for (const
|
|
1069
|
-
const encryptedKey = await this.keyBag.encryptAndExportKeyVersion("doc", this.tenantId, keyId, detail.versionIndex, sharePassword);
|
|
1070
|
-
if (!encryptedKey) {
|
|
1071
|
-
throw new Error(`Failed to export document key "${keyId}" version ${detail.versionIndex} for tenant "${this.tenantId}"`);
|
|
1072
|
-
}
|
|
1073
|
-
const { createdAt: encryptedKeyCreatedAt, ...encryptedKeyPayload } = encryptedKey;
|
|
1269
|
+
for (const version of keyVersions) {
|
|
1074
1270
|
versions.push({
|
|
1075
|
-
createdAt:
|
|
1076
|
-
|
|
1271
|
+
createdAt: version.createdAt,
|
|
1272
|
+
wrappedKey: await rsa.wrapKeyToBase64(version.key, recipientEncryptionPublicKey),
|
|
1077
1273
|
});
|
|
1078
1274
|
}
|
|
1079
1275
|
return { keyId, versions };
|
|
1080
1276
|
}
|
|
1081
|
-
resolveJoinResponseDocKeyIds(
|
|
1082
|
-
|
|
1083
|
-
const keyIds = new Set();
|
|
1084
|
-
keyIds.add(types_1.PUBLIC_INFOS_KEY_ID);
|
|
1085
|
-
for (const keyId of requestedKeyIds) {
|
|
1086
|
-
const normalizedKeyId = keyId.trim();
|
|
1087
|
-
if (normalizedKeyId) {
|
|
1088
|
-
keyIds.add(normalizedKeyId);
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
return [
|
|
1092
|
-
types_1.PUBLIC_INFOS_KEY_ID,
|
|
1093
|
-
...Array.from(keyIds)
|
|
1094
|
-
.filter((keyId) => keyId !== types_1.PUBLIC_INFOS_KEY_ID)
|
|
1095
|
-
.sort((a, b) => {
|
|
1096
|
-
if (a === types_1.DEFAULT_TENANT_KEY_ID)
|
|
1097
|
-
return -1;
|
|
1098
|
-
if (b === types_1.DEFAULT_TENANT_KEY_ID)
|
|
1099
|
-
return 1;
|
|
1100
|
-
return a.localeCompare(b);
|
|
1101
|
-
}),
|
|
1102
|
-
];
|
|
1277
|
+
resolveJoinResponseDocKeyIds(_options) {
|
|
1278
|
+
return [types_1.PUBLIC_INFOS_KEY_ID];
|
|
1103
1279
|
}
|
|
1104
1280
|
async approveJoinRequest(joinRequest, options) {
|
|
1105
1281
|
// Parse the join request if it's a URI string
|
|
@@ -1138,17 +1314,81 @@ class BaseMindooTenant {
|
|
|
1138
1314
|
// overrides the one suggested by the joining user in the request.
|
|
1139
1315
|
const deviceLabel = typeof options.label === "string" && options.label.trim().length > 0
|
|
1140
1316
|
? options.label.trim()
|
|
1141
|
-
: request.label
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1317
|
+
: typeof request.label === "string"
|
|
1318
|
+
? request.label.trim()
|
|
1319
|
+
: "";
|
|
1320
|
+
if (!deviceLabel) {
|
|
1321
|
+
throw new Error("Device label is required when granting access");
|
|
1322
|
+
}
|
|
1323
|
+
await directory.registerUser(publicUserId, options.adminSigningKey, options.adminPassword, undefined, deviceLabel, {
|
|
1324
|
+
distributeKeyIds: [],
|
|
1325
|
+
});
|
|
1326
|
+
const userPublicKey = typeof request.userPublicKey === "string" ? request.userPublicKey.trim() : "";
|
|
1327
|
+
if (userPublicKey) {
|
|
1328
|
+
const userKeyDoc = await this.userKeys.createPendingFromJoin({
|
|
1329
|
+
username,
|
|
1330
|
+
userPublicKey,
|
|
1331
|
+
signingKeyPair: {
|
|
1332
|
+
publicKey: this.administrationPublicKey,
|
|
1333
|
+
privateKey: options.adminSigningKey,
|
|
1334
|
+
},
|
|
1335
|
+
signingKeyPassword: options.adminPassword,
|
|
1336
|
+
});
|
|
1337
|
+
const payload = (0, validateUserKeyDocument_1.asUserKeyPayload)(userKeyDoc.getData());
|
|
1338
|
+
const epoch = payload ? (0, types_2.currentUserKeyEpoch)(payload) : null;
|
|
1339
|
+
const publishedPublicKey = epoch ? payload?.userKeys[epoch]?.publicKey : undefined;
|
|
1340
|
+
// First device: wrap `default` to the join-request User-Key (pending is
|
|
1341
|
+
// OK). Additional device: wrap to the already-published User-Key — never
|
|
1342
|
+
// to this device's freshly generated pair.
|
|
1343
|
+
if (publishedPublicKey === userPublicKey) {
|
|
1344
|
+
await directory.autoDistributeKeysToUser(username, [types_1.DEFAULT_TENANT_KEY_ID], options.adminSigningKey, options.adminPassword, userPublicKey);
|
|
1345
|
+
}
|
|
1346
|
+
else if (publishedPublicKey) {
|
|
1347
|
+
await directory.autoDistributeKeysToUser(username, [types_1.DEFAULT_TENANT_KEY_ID], options.adminSigningKey, options.adminPassword);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
else {
|
|
1351
|
+
const existingUserKey = await this.userKeys.publishedUserKeyFor(username);
|
|
1352
|
+
if (!existingUserKey) {
|
|
1353
|
+
throw new Error("Cannot approve join request: it carries no userPublicKey and the person has no published User-Key. Generate a User-Key on the joining device first.");
|
|
1354
|
+
}
|
|
1355
|
+
await directory.autoDistributeKeysToUser(username, [types_1.DEFAULT_TENANT_KEY_ID], options.adminSigningKey, options.adminPassword);
|
|
1356
|
+
}
|
|
1357
|
+
// Same person: wrap the published User-Key to this device's encryption
|
|
1358
|
+
// key (from the grant). A different existing person stays pending until
|
|
1359
|
+
// one of *their* devices writes the wrap — this replica cannot.
|
|
1360
|
+
const current = await this.getCurrentUserId();
|
|
1361
|
+
const samePerson = (0, userid_1.usernamesEqual)(current.username, username);
|
|
1362
|
+
console.log("[approveJoinRequest] User-Key wrap decision", {
|
|
1363
|
+
joiningUsername: username,
|
|
1364
|
+
currentUsername: current.username,
|
|
1365
|
+
samePerson,
|
|
1366
|
+
hasEncryptionPublicKey: !!request.encryptionPublicKey,
|
|
1367
|
+
});
|
|
1368
|
+
if (samePerson) {
|
|
1369
|
+
try {
|
|
1370
|
+
await this.userKeys.wrapUserKeyForGrantDevice(request.encryptionPublicKey, deviceLabel);
|
|
1371
|
+
console.log(`[approveJoinRequest] ✓ wrapped User-Key for joining device of "${username}"`);
|
|
1372
|
+
this.logger.info(`approveJoinRequest: wrapped User-Key for joining device of "${username}"`);
|
|
1373
|
+
}
|
|
1374
|
+
catch (error) {
|
|
1375
|
+
console.error("[approveJoinRequest] User-Key wrap for joining device failed", error);
|
|
1376
|
+
this.logger.error(`approveJoinRequest: User-Key wrap for joining device failed (${error instanceof Error ? error.message : error})`);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
else {
|
|
1380
|
+
this.logger.info(`approveJoinRequest: User-Key wrap deferred; joining "${username}" is not the current user "${current.username}"`);
|
|
1381
|
+
}
|
|
1382
|
+
// 2. Export selected document keys RSA-OAEP-wrapped to the requester's
|
|
1383
|
+
// encryption public key. `$publicinfos` is mandatory because the joining
|
|
1384
|
+
// user needs it for directory access; all selected key ids include every
|
|
1385
|
+
// rotated version.
|
|
1386
|
+
const encryptedDocKeys = await Promise.all(this.resolveJoinResponseDocKeyIds(options).map((keyId) => this.exportJoinResponseDocKeyVersions(keyId, request.encryptionPublicKey)));
|
|
1147
1387
|
// 3. Build the join response. It echoes the registered username so the
|
|
1148
1388
|
// joining device can adopt it instead of keeping a name the directory
|
|
1149
1389
|
// does not hold.
|
|
1150
1390
|
const joinResponse = {
|
|
1151
|
-
v:
|
|
1391
|
+
v: 3,
|
|
1152
1392
|
tenantId: this.tenantId,
|
|
1153
1393
|
adminSigningPublicKey: this.administrationPublicKey,
|
|
1154
1394
|
adminEncryptionPublicKey: this.administrationEncryptionPublicKey,
|
|
@@ -1168,7 +1408,7 @@ class BaseMindooTenant {
|
|
|
1168
1408
|
else {
|
|
1169
1409
|
try {
|
|
1170
1410
|
const directoryDb = await this.openDB("directory", { adminOnlyDb: true });
|
|
1171
|
-
const setupLabel = await (0, tenantSetup_1.readTenantSetupLabel)(directoryDb);
|
|
1411
|
+
const setupLabel = await (0, tenantSetup_1.readTenantSetupLabel)(directoryDb, this);
|
|
1172
1412
|
if (setupLabel) {
|
|
1173
1413
|
joinResponse.tenantLabel = setupLabel;
|
|
1174
1414
|
}
|
|
@@ -1177,6 +1417,36 @@ class BaseMindooTenant {
|
|
|
1177
1417
|
// Best-effort: older tenants or missing setup doc.
|
|
1178
1418
|
}
|
|
1179
1419
|
}
|
|
1420
|
+
const rsa = new RSAEncryption_1.RSAEncryption(this.cryptoAdapter, this.logger.createChild("RSAEncryption"));
|
|
1421
|
+
const wrappedJoinResponse = await (0, deviceJoinBootstrap_1.wrapDeviceJoinBootstrap)(rsa, {
|
|
1422
|
+
username,
|
|
1423
|
+
tenantLabel: joinResponse.tenantLabel,
|
|
1424
|
+
adminUsername: joinResponse.adminUsername,
|
|
1425
|
+
}, request.encryptionPublicKey);
|
|
1426
|
+
await directory.attachDeviceJoinBootstrap(username, request.signingPublicKey, wrappedJoinResponse, options.adminSigningKey, options.adminPassword);
|
|
1427
|
+
// Tenants created before `tenantsetup` recorded administrators have no
|
|
1428
|
+
// entry, leaving members with a fingerprint instead of a name. A grant is
|
|
1429
|
+
// where the admin supplies both its name and its keys, so heal it here.
|
|
1430
|
+
if (options.adminUsername) {
|
|
1431
|
+
try {
|
|
1432
|
+
const directoryDb = await this.openDB("directory", { adminOnlyDb: true });
|
|
1433
|
+
const written = await (0, tenantSetup_1.ensureTenantSetupAdministrator)(directoryDb, {
|
|
1434
|
+
username: options.adminUsername,
|
|
1435
|
+
keyPairs: [
|
|
1436
|
+
{
|
|
1437
|
+
signingPublicKey: this.administrationPublicKey,
|
|
1438
|
+
encryptionPublicKey: this.administrationEncryptionPublicKey,
|
|
1439
|
+
},
|
|
1440
|
+
],
|
|
1441
|
+
}, { publicKey: this.administrationPublicKey, privateKey: options.adminSigningKey }, options.adminPassword, this);
|
|
1442
|
+
if (written) {
|
|
1443
|
+
this.logger.info(`approveJoinRequest: recorded administrator "${options.adminUsername}" in tenantsetup`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
catch (error) {
|
|
1447
|
+
this.logger.warn(`approveJoinRequest: recording the administrator in tenantsetup failed: ${error instanceof Error ? error.message : error}`);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1180
1450
|
console.log(`[approveJoinRequest] ✓ Join request approved for user "${username}"`);
|
|
1181
1451
|
this.logger.info(`Join request approved for user: ${username}`);
|
|
1182
1452
|
// Return as URI string or object depending on format option
|
|
@@ -1342,8 +1612,9 @@ class BaseMindooTenant {
|
|
|
1342
1612
|
const signingKey = await this.getDecryptedSigningKey();
|
|
1343
1613
|
// Get the current user's decrypted RSA encryption private key (for decrypting entries)
|
|
1344
1614
|
const decryptedEncryptionKey = await this.getDecryptedEncryptionKey();
|
|
1345
|
-
// Create the client network store
|
|
1346
|
-
|
|
1615
|
+
// Create the client network store. Pass the device signing public key so
|
|
1616
|
+
// auth challenges can identify this specific device on multi-device grants.
|
|
1617
|
+
const store = new ClientNetworkContentAddressedStore(validDbId, storeKind, transport, this.cryptoAdapter, this.currentUser.username, signingKey, decryptedEncryptionKey, this.logger.createChild(`ClientNetworkStore:${validDbId}:${storeKind}`), this.currentUser.userSigningKeyPair.publicKey);
|
|
1347
1618
|
console.log(`[connectToServer] ✓ Connected to server for db "${validDbId}"`);
|
|
1348
1619
|
this.logger.info(`Connected to server for db "${validDbId}"`);
|
|
1349
1620
|
return store;
|
|
@@ -1507,6 +1778,9 @@ class BaseMindooTenant {
|
|
|
1507
1778
|
* @returns true if the key can be resolved, false otherwise
|
|
1508
1779
|
*/
|
|
1509
1780
|
async hasDecryptionKey(decryptionKeyId) {
|
|
1781
|
+
if ((0, sealedTypes_1.isSealedKeyId)(decryptionKeyId)) {
|
|
1782
|
+
return (this.sealedGenerations.get(decryptionKeyId)?.length ?? 0) > 0;
|
|
1783
|
+
}
|
|
1510
1784
|
try {
|
|
1511
1785
|
await this.getSymmetricKey(decryptionKeyId);
|
|
1512
1786
|
return true;
|
|
@@ -1529,14 +1803,14 @@ class BaseMindooTenant {
|
|
|
1529
1803
|
return this.keyBag.getDocKeyFingerprint(this.tenantId);
|
|
1530
1804
|
}
|
|
1531
1805
|
/**
|
|
1532
|
-
* Remove a **named** document key from the local KeyBag. Refuses to delete
|
|
1533
|
-
*
|
|
1534
|
-
*
|
|
1535
|
-
* removed.
|
|
1806
|
+
* Remove a **named** document key from the local KeyBag. Refuses to delete
|
|
1807
|
+
* `$publicinfos` (required for directory access). The tenant `default` key
|
|
1808
|
+
* may be removed when a key-distribution `pullfrom` revokes content access.
|
|
1809
|
+
* Returns true if a key was removed.
|
|
1536
1810
|
*/
|
|
1537
1811
|
async removeNamedDecryptionKey(decryptionKeyId) {
|
|
1538
|
-
if (decryptionKeyId ===
|
|
1539
|
-
this.logger.debug(`removeNamedDecryptionKey: refusing to delete the
|
|
1812
|
+
if (decryptionKeyId === types_1.PUBLIC_INFOS_KEY_ID) {
|
|
1813
|
+
this.logger.debug(`removeNamedDecryptionKey: refusing to delete the public-infos key`);
|
|
1540
1814
|
return false;
|
|
1541
1815
|
}
|
|
1542
1816
|
try {
|
|
@@ -1573,19 +1847,24 @@ class BaseMindooTenant {
|
|
|
1573
1847
|
const directory = await this.openDirectory();
|
|
1574
1848
|
const removed = [];
|
|
1575
1849
|
const imported = [];
|
|
1576
|
-
// Revoke pass — no key, no comparison, idempotent.
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1850
|
+
// Revoke pass — no key, no comparison, idempotent. Prefer signing-key
|
|
1851
|
+
// lookup when the local username is empty (nameless device bootstrap).
|
|
1852
|
+
let revokedIds = [];
|
|
1853
|
+
try {
|
|
1854
|
+
if (!username.trim() &&
|
|
1855
|
+
typeof directory.getRevokedDecryptionKeyIdsForSigningKey === "function") {
|
|
1856
|
+
revokedIds = await directory.getRevokedDecryptionKeyIdsForSigningKey(this.currentUser.userSigningKeyPair.publicKey);
|
|
1581
1857
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1858
|
+
else if (typeof directory.getRevokedDecryptionKeyIdsForUser === "function") {
|
|
1859
|
+
revokedIds = await directory.getRevokedDecryptionKeyIdsForUser(username);
|
|
1584
1860
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1861
|
+
}
|
|
1862
|
+
catch (error) {
|
|
1863
|
+
this.logger.warn(`reconcileKeyDistributionsForCurrentUser: revoked-id lookup failed: ${error}`);
|
|
1864
|
+
}
|
|
1865
|
+
for (const keyId of revokedIds) {
|
|
1866
|
+
if (await this.removeNamedDecryptionKey(keyId)) {
|
|
1867
|
+
removed.push(keyId);
|
|
1589
1868
|
}
|
|
1590
1869
|
}
|
|
1591
1870
|
// Import pass — sources the encryption private key from this tenant. The
|
|
@@ -1605,7 +1884,41 @@ class BaseMindooTenant {
|
|
|
1605
1884
|
this.logger.warn(`reconcileKeyDistributionsForCurrentUser: import pass failed: ${error}`);
|
|
1606
1885
|
}
|
|
1607
1886
|
}
|
|
1608
|
-
|
|
1887
|
+
let adoptedUsername;
|
|
1888
|
+
if (imported.includes(types_1.DEFAULT_TENANT_KEY_ID) ||
|
|
1889
|
+
imported.includes("default")) {
|
|
1890
|
+
adoptedUsername = await this.adoptRegisteredUsernameFromGrant(directory);
|
|
1891
|
+
}
|
|
1892
|
+
return { imported, removed, ...(adoptedUsername ? { adoptedUsername } : {}) };
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* After importing the tenant `default` key, decrypt grant `user_details` and
|
|
1896
|
+
* adopt the registered username so local identity matches the directory
|
|
1897
|
+
* (same outcome as join-response `username`).
|
|
1898
|
+
*/
|
|
1899
|
+
async adoptRegisteredUsernameFromGrant(directory) {
|
|
1900
|
+
try {
|
|
1901
|
+
if (typeof directory.getUserBySigningPublicKey !== "function")
|
|
1902
|
+
return undefined;
|
|
1903
|
+
const lookup = await directory.getUserBySigningPublicKey(this.currentUser.userSigningKeyPair.publicKey);
|
|
1904
|
+
const registered = (typeof lookup?.details?.username === "string" && lookup.details.username.trim()) ||
|
|
1905
|
+
(typeof lookup?.username === "string" &&
|
|
1906
|
+
lookup.username.trim() &&
|
|
1907
|
+
// Server-side lookups return username_hash when details are opaque;
|
|
1908
|
+
// only adopt cleartext names (contain '/' for DN form, or non-hex).
|
|
1909
|
+
(lookup.username.includes("/") || !/^[a-f0-9]{64}$/i.test(lookup.username))
|
|
1910
|
+
? lookup.username.trim()
|
|
1911
|
+
: "");
|
|
1912
|
+
if (!registered || registered === this.currentUser.username)
|
|
1913
|
+
return undefined;
|
|
1914
|
+
this.logger.info(`Adopting registered username "${registered}" (was "${this.currentUser.username}")`);
|
|
1915
|
+
this.currentUser = { ...this.currentUser, username: registered };
|
|
1916
|
+
return registered;
|
|
1917
|
+
}
|
|
1918
|
+
catch (error) {
|
|
1919
|
+
this.logger.debug(`adoptRegisteredUsernameFromGrant failed: ${error}`);
|
|
1920
|
+
return undefined;
|
|
1921
|
+
}
|
|
1609
1922
|
}
|
|
1610
1923
|
/**
|
|
1611
1924
|
* Single-flight, best-effort wrapper around
|
|
@@ -1717,13 +2030,14 @@ class BaseMindooTenant {
|
|
|
1717
2030
|
/**
|
|
1718
2031
|
* Remove exactly the key versions of `keyId` whose raw-bytes fingerprint is in
|
|
1719
2032
|
* `fingerprints` (version-scoped `pullfrom` revocation). Versions obtained
|
|
1720
|
-
* elsewhere — i.e. not in the distribution manifest — survive. Refuses
|
|
1721
|
-
*
|
|
1722
|
-
*
|
|
2033
|
+
* elsewhere — i.e. not in the distribution manifest — survive. Refuses
|
|
2034
|
+
* `$publicinfos` (directory access). The tenant `default` key may be version-
|
|
2035
|
+
* revoked via key distribution. The KeyBag change feed triggers visibility
|
|
2036
|
+
* reconciliation (scope purge when nothing remains).
|
|
1723
2037
|
* Returns the number of versions removed.
|
|
1724
2038
|
*/
|
|
1725
2039
|
async removeDecryptionKeyVersionsByFingerprint(keyId, fingerprints) {
|
|
1726
|
-
if (keyId ===
|
|
2040
|
+
if (keyId === types_1.PUBLIC_INFOS_KEY_ID) {
|
|
1727
2041
|
this.logger.debug(`removeDecryptionKeyVersionsByFingerprint: refusing protected key ${keyId}`);
|
|
1728
2042
|
return 0;
|
|
1729
2043
|
}
|
|
@@ -1768,6 +2082,13 @@ class BaseMindooTenant {
|
|
|
1768
2082
|
* @returns The decrypted symmetric key bytes
|
|
1769
2083
|
*/
|
|
1770
2084
|
async getSymmetricKey(decryptionKeyId) {
|
|
2085
|
+
if ((0, sealedTypes_1.isSealedKeyId)(decryptionKeyId)) {
|
|
2086
|
+
const generations = this.sealedGenerations.get(decryptionKeyId);
|
|
2087
|
+
if (!generations?.length) {
|
|
2088
|
+
throw new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
|
|
2089
|
+
}
|
|
2090
|
+
return generations[0];
|
|
2091
|
+
}
|
|
1771
2092
|
if (decryptionKeyId === "default") {
|
|
1772
2093
|
// Use cached tenant encryption key if available
|
|
1773
2094
|
if (this.decryptedTenantKeyCache) {
|