mindoodb 0.0.50 → 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 +72 -0
- package/dist/core/BaseMindooDB.d.ts +89 -14
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +692 -201
- 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 +427 -90
- 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 +292 -16
- 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 +13 -7
- package/dist/core/accesscontrol/types.d.ts.map +1 -1
- package/dist/core/accesscontrol/types.js +16 -10
- 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 +7 -2
- 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 +8 -1
- 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/databaseIdValidation.d.ts +23 -0
- package/dist/core/databaseIdValidation.d.ts.map +1 -1
- package/dist/core/databaseIdValidation.js +59 -1
- package/dist/core/databaseIdValidation.js.map +1 -1
- package/dist/core/index.d.ts +9 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +40 -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 +109 -0
- package/dist/core/tenantSetup.d.ts.map +1 -0
- package/dist/core/tenantSetup.js +250 -0
- package/dist/core/tenantSetup.js.map +1 -0
- package/dist/core/types.d.ts +333 -43
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +27 -9
- package/dist/core/types.js.map +1 -1
- package/dist/core/uri/joinRequestUri.d.ts +37 -0
- package/dist/core/uri/joinRequestUri.d.ts.map +1 -0
- package/dist/core/uri/joinRequestUri.js +168 -0
- package/dist/core/uri/joinRequestUri.js.map +1 -0
- 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/core/utils/idGeneration.d.ts +39 -20
- package/dist/core/utils/idGeneration.d.ts.map +1 -1
- package/dist/core/utils/idGeneration.js +82 -62
- package/dist/core/utils/idGeneration.js.map +1 -1
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +4 -1
- package/dist/core/utils/index.js.map +1 -1
- 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,20 +2,30 @@
|
|
|
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");
|
|
13
21
|
const storeEntryTime_1 = require("./storeEntryTime");
|
|
14
22
|
const idGeneration_1 = require("./utils/idGeneration");
|
|
15
23
|
const timeSource_1 = require("./utils/timeSource");
|
|
24
|
+
const tenantSetup_1 = require("./tenantSetup");
|
|
16
25
|
const errors_1 = require("./errors");
|
|
17
26
|
const logging_1 = require("./logging");
|
|
18
27
|
const MindooURI_1 = require("./uri/MindooURI");
|
|
28
|
+
const joinRequestUri_1 = require("./uri/joinRequestUri");
|
|
19
29
|
const EncryptedLocalCacheStore_1 = require("./cache/EncryptedLocalCacheStore");
|
|
20
30
|
const CacheManager_1 = require("./cache/CacheManager");
|
|
21
31
|
const databaseIdValidation_1 = require("./databaseIdValidation");
|
|
@@ -46,6 +56,7 @@ class BaseMindooTenant {
|
|
|
46
56
|
*/
|
|
47
57
|
preDecryptedMaterial) {
|
|
48
58
|
this.databaseCache = new Map();
|
|
59
|
+
this.databaseOpenInFlight = new Map();
|
|
49
60
|
this.directoryCache = null;
|
|
50
61
|
this.remoteStoreCache = new Map();
|
|
51
62
|
/**
|
|
@@ -87,6 +98,9 @@ class BaseMindooTenant {
|
|
|
87
98
|
// recursing or overlapping. Reconcile is idempotent, so a skipped overlap is
|
|
88
99
|
// harmless — the next trigger re-runs it.
|
|
89
100
|
this.reconcileInFlight = false;
|
|
101
|
+
this.userKeyReconcileScheduled = false;
|
|
102
|
+
this.userKeys = new UserKeyManager_1.UserKeyManager(this);
|
|
103
|
+
this.sealedGenerations = new Map();
|
|
90
104
|
// Local cache support
|
|
91
105
|
this.cacheManager = null;
|
|
92
106
|
this.factory = factory;
|
|
@@ -132,6 +146,9 @@ class BaseMindooTenant {
|
|
|
132
146
|
if (preDecryptedMaterial?.encryptionKey) {
|
|
133
147
|
this.decryptedUserEncryptionKeyCache = preDecryptedMaterial.encryptionKey;
|
|
134
148
|
}
|
|
149
|
+
if (preDecryptedMaterial?.userKeyPrivateBytes) {
|
|
150
|
+
this.userKeys.seedDecryptedUserKeyBytes(preDecryptedMaterial.userKeyPrivateBytes);
|
|
151
|
+
}
|
|
135
152
|
// Set up cache if a local cache store is provided. When a pre-derived
|
|
136
153
|
// cache encryption key is available, prefer that to skip the password
|
|
137
154
|
// path and avoid retaining the plaintext password inside the encrypted
|
|
@@ -191,6 +208,148 @@ class BaseMindooTenant {
|
|
|
191
208
|
}
|
|
192
209
|
return timestamp;
|
|
193
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
|
+
}
|
|
194
353
|
getFactory() {
|
|
195
354
|
return this.factory;
|
|
196
355
|
}
|
|
@@ -223,6 +382,9 @@ class BaseMindooTenant {
|
|
|
223
382
|
*/
|
|
224
383
|
async importAesKeyCached(symmetricKey, usage) {
|
|
225
384
|
let cacheKey = usage + ":";
|
|
385
|
+
if (symmetricKey.length !== 32) {
|
|
386
|
+
throw new Error(`AES-256 key must be 32 bytes, got ${symmetricKey.length}`);
|
|
387
|
+
}
|
|
226
388
|
for (let i = 0; i < symmetricKey.length; i++) {
|
|
227
389
|
cacheKey += symmetricKey[i].toString(16).padStart(2, "0");
|
|
228
390
|
}
|
|
@@ -231,9 +393,8 @@ class BaseMindooTenant {
|
|
|
231
393
|
return cached;
|
|
232
394
|
}
|
|
233
395
|
const subtle = this.cryptoAdapter.getSubtle();
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
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
|
|
237
398
|
[usage]);
|
|
238
399
|
if (this.importedAesKeyCache.size >= BaseMindooTenant.MAX_IMPORTED_KEY_CACHE_ENTRIES) {
|
|
239
400
|
this.importedAesKeyCache.clear();
|
|
@@ -252,7 +413,14 @@ class BaseMindooTenant {
|
|
|
252
413
|
// Get the symmetric key for this key ID
|
|
253
414
|
let symmetricKey;
|
|
254
415
|
try {
|
|
255
|
-
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") {
|
|
256
424
|
// Use cached tenant encryption key if available
|
|
257
425
|
if (this.decryptedTenantKeyCache) {
|
|
258
426
|
symmetricKey = this.decryptedTenantKeyCache;
|
|
@@ -386,7 +554,9 @@ class BaseMindooTenant {
|
|
|
386
554
|
}
|
|
387
555
|
}
|
|
388
556
|
this.logger.debug(`Decrypt failed against all ${candidates.length} version(s) of key "${decryptionKeyId}"`);
|
|
389
|
-
throw lastError
|
|
557
|
+
throw lastError instanceof Error
|
|
558
|
+
? new errors_1.SymmetricKeyNotFoundError(decryptionKeyId)
|
|
559
|
+
: new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
|
|
390
560
|
}
|
|
391
561
|
/**
|
|
392
562
|
* Return every stored version of the symmetric key for `decryptionKeyId`,
|
|
@@ -398,6 +568,9 @@ class BaseMindooTenant {
|
|
|
398
568
|
* tenant-default version so encryption keeps using the latest key.
|
|
399
569
|
*/
|
|
400
570
|
async getDecryptionKeyCandidates(decryptionKeyId) {
|
|
571
|
+
if ((0, sealedTypes_1.isSealedKeyId)(decryptionKeyId)) {
|
|
572
|
+
return this.sealedGenerations.get(decryptionKeyId) ?? [];
|
|
573
|
+
}
|
|
401
574
|
const id = decryptionKeyId === "default" ? types_1.DEFAULT_TENANT_KEY_ID : decryptionKeyId;
|
|
402
575
|
const versions = await this.keyBag.getAllKeys("doc", this.tenantId, id);
|
|
403
576
|
if (decryptionKeyId === "default" && versions.length > 0) {
|
|
@@ -413,7 +586,7 @@ class BaseMindooTenant {
|
|
|
413
586
|
// Sign the payload
|
|
414
587
|
const signature = await subtle.sign({
|
|
415
588
|
name: "Ed25519",
|
|
416
|
-
}, signingKey, payload.
|
|
589
|
+
}, signingKey, payload.slice());
|
|
417
590
|
this.logger.debug(`Signed payload (signature: ${signature.byteLength} bytes)`);
|
|
418
591
|
return new Uint8Array(signature);
|
|
419
592
|
}
|
|
@@ -465,11 +638,23 @@ class BaseMindooTenant {
|
|
|
465
638
|
}, false, // not extractable
|
|
466
639
|
["sign"]);
|
|
467
640
|
// Sign the payload
|
|
468
|
-
const
|
|
641
|
+
const signatureBuffer = await subtle.sign({
|
|
469
642
|
name: "Ed25519",
|
|
470
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
|
+
}
|
|
471
656
|
this.logger.debug(`Signed payload with provided key (signature: ${signature.byteLength} bytes)`);
|
|
472
|
-
return
|
|
657
|
+
return signature;
|
|
473
658
|
}
|
|
474
659
|
async verifySignature(payload, signature, publicKey) {
|
|
475
660
|
this.logger.debug(`Verifying signature`);
|
|
@@ -643,7 +828,8 @@ class BaseMindooTenant {
|
|
|
643
828
|
// Every local database belonging to this tenant: the directory plus any
|
|
644
829
|
// opened/known data databases. Wipe is a whole-tenant operation.
|
|
645
830
|
const dbIds = new Set([
|
|
646
|
-
|
|
831
|
+
types_1.DIRECTORY_DB_ID,
|
|
832
|
+
types_1.USER_DIRECTORY_DB_ID,
|
|
647
833
|
...this.databaseCache.keys(),
|
|
648
834
|
...(additionalDbIds ?? []),
|
|
649
835
|
]);
|
|
@@ -699,7 +885,7 @@ class BaseMindooTenant {
|
|
|
699
885
|
}
|
|
700
886
|
}
|
|
701
887
|
async assertCurrentUserCanOpenDB(id) {
|
|
702
|
-
if (id ===
|
|
888
|
+
if (id === types_1.DIRECTORY_DB_ID || id === types_1.USER_DIRECTORY_DB_ID) {
|
|
703
889
|
return;
|
|
704
890
|
}
|
|
705
891
|
const currentUser = await this.getCurrentUserId();
|
|
@@ -755,7 +941,7 @@ class BaseMindooTenant {
|
|
|
755
941
|
const validDbId = (0, databaseIdValidation_1.validateDatabaseId)(id, "dbId");
|
|
756
942
|
// Enforce admin-only mode for directory database - this is a security invariant
|
|
757
943
|
// The directory database must only accept entries signed by the admin key
|
|
758
|
-
const effectiveOptions = validDbId ===
|
|
944
|
+
const effectiveOptions = validDbId === types_1.DIRECTORY_DB_ID
|
|
759
945
|
? { ...options, adminOnlyDb: true }
|
|
760
946
|
: options ?? {};
|
|
761
947
|
const normalizedTimeTravelDate = this.normalizeTimeTravelDate(effectiveOptions.timeTravelDate);
|
|
@@ -763,22 +949,41 @@ class BaseMindooTenant {
|
|
|
763
949
|
? validDbId
|
|
764
950
|
: `${validDbId}::tt:${normalizedTimeTravelDate}`;
|
|
765
951
|
await this.assertCurrentUserCanOpenDB(validDbId);
|
|
766
|
-
// Return cached database if it exists
|
|
767
952
|
const cached = this.databaseCache.get(databaseCacheKey);
|
|
768
953
|
if (cached) {
|
|
769
|
-
|
|
770
|
-
if (validDbId === "directory" && !cached.isAdminOnlyDb()) {
|
|
954
|
+
if (validDbId === types_1.DIRECTORY_DB_ID && !cached.isAdminOnlyDb()) {
|
|
771
955
|
throw new Error("Directory database was cached without adminOnlyDb - this should never happen");
|
|
772
956
|
}
|
|
773
|
-
// Live databases must reflect the current KeyBag state before they
|
|
774
|
-
// are handed back to callers. Time-travel snapshots are immutable
|
|
775
|
-
// historical views and intentionally do not participate in
|
|
776
|
-
// visibility reconciliation, so we skip the call for them.
|
|
777
957
|
if (normalizedTimeTravelDate == null) {
|
|
778
958
|
await this.reconcileKeyBagChanges();
|
|
779
959
|
}
|
|
780
960
|
return cached;
|
|
781
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) {
|
|
782
987
|
// Extract store options and DB-specific options
|
|
783
988
|
const { adminOnlyDb, timeTravelDate: _timeTravelDate, persistTimeTravelCache, autoIndexing, attachmentConfig, documentCacheConfig, snapshotConfig, performanceCallback, ...storeOptions } = effectiveOptions;
|
|
784
989
|
// Snapshot opens can share an already-open live store instance. This keeps
|
|
@@ -1054,50 +1259,23 @@ class BaseMindooTenant {
|
|
|
1054
1259
|
const signerLogger = this.logger.createChild("MindooDocSigner");
|
|
1055
1260
|
return new MindooDocSigner_1.MindooDocSigner(this, signKey, signerLogger);
|
|
1056
1261
|
}
|
|
1057
|
-
async exportJoinResponseDocKeyVersions(keyId,
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
.filter((detail) => detail.scopedKeyId === scopedKeyId)
|
|
1061
|
-
.sort((a, b) => a.versionIndex - b.versionIndex);
|
|
1062
|
-
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) {
|
|
1063
1265
|
throw new Error(`Failed to export document key "${keyId}" for tenant "${this.tenantId}"`);
|
|
1064
1266
|
}
|
|
1267
|
+
const rsa = new RSAEncryption_1.RSAEncryption(this.cryptoAdapter, this.logger.createChild("RSAEncryption"));
|
|
1065
1268
|
const versions = [];
|
|
1066
|
-
for (const
|
|
1067
|
-
const encryptedKey = await this.keyBag.encryptAndExportKeyVersion("doc", this.tenantId, keyId, detail.versionIndex, sharePassword);
|
|
1068
|
-
if (!encryptedKey) {
|
|
1069
|
-
throw new Error(`Failed to export document key "${keyId}" version ${detail.versionIndex} for tenant "${this.tenantId}"`);
|
|
1070
|
-
}
|
|
1071
|
-
const { createdAt: encryptedKeyCreatedAt, ...encryptedKeyPayload } = encryptedKey;
|
|
1269
|
+
for (const version of keyVersions) {
|
|
1072
1270
|
versions.push({
|
|
1073
|
-
createdAt:
|
|
1074
|
-
|
|
1271
|
+
createdAt: version.createdAt,
|
|
1272
|
+
wrappedKey: await rsa.wrapKeyToBase64(version.key, recipientEncryptionPublicKey),
|
|
1075
1273
|
});
|
|
1076
1274
|
}
|
|
1077
1275
|
return { keyId, versions };
|
|
1078
1276
|
}
|
|
1079
|
-
resolveJoinResponseDocKeyIds(
|
|
1080
|
-
|
|
1081
|
-
const keyIds = new Set();
|
|
1082
|
-
keyIds.add(types_1.PUBLIC_INFOS_KEY_ID);
|
|
1083
|
-
for (const keyId of requestedKeyIds) {
|
|
1084
|
-
const normalizedKeyId = keyId.trim();
|
|
1085
|
-
if (normalizedKeyId) {
|
|
1086
|
-
keyIds.add(normalizedKeyId);
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
return [
|
|
1090
|
-
types_1.PUBLIC_INFOS_KEY_ID,
|
|
1091
|
-
...Array.from(keyIds)
|
|
1092
|
-
.filter((keyId) => keyId !== types_1.PUBLIC_INFOS_KEY_ID)
|
|
1093
|
-
.sort((a, b) => {
|
|
1094
|
-
if (a === types_1.DEFAULT_TENANT_KEY_ID)
|
|
1095
|
-
return -1;
|
|
1096
|
-
if (b === types_1.DEFAULT_TENANT_KEY_ID)
|
|
1097
|
-
return 1;
|
|
1098
|
-
return a.localeCompare(b);
|
|
1099
|
-
}),
|
|
1100
|
-
];
|
|
1277
|
+
resolveJoinResponseDocKeyIds(_options) {
|
|
1278
|
+
return [types_1.PUBLIC_INFOS_KEY_ID];
|
|
1101
1279
|
}
|
|
1102
1280
|
async approveJoinRequest(joinRequest, options) {
|
|
1103
1281
|
// Parse the join request if it's a URI string
|
|
@@ -1106,11 +1284,9 @@ class BaseMindooTenant {
|
|
|
1106
1284
|
if (!(0, MindooURI_1.isMindooURI)(joinRequest)) {
|
|
1107
1285
|
throw new Error("Invalid join request: expected a JoinRequest object or a mdb://join-request/... URI string");
|
|
1108
1286
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1113
|
-
request = decoded.payload;
|
|
1287
|
+
// Normalizes the compact v3 transport back to PEM-armored keys; older
|
|
1288
|
+
// verbose requests pass through unchanged.
|
|
1289
|
+
request = (0, joinRequestUri_1.decodeJoinRequestUri)(joinRequest);
|
|
1114
1290
|
}
|
|
1115
1291
|
else {
|
|
1116
1292
|
request = joinRequest;
|
|
@@ -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,
|
|
@@ -1161,6 +1401,52 @@ class BaseMindooTenant {
|
|
|
1161
1401
|
if (options.adminUsername) {
|
|
1162
1402
|
joinResponse.adminUsername = options.adminUsername;
|
|
1163
1403
|
}
|
|
1404
|
+
const explicitTenantLabel = typeof options.tenantLabel === "string" ? options.tenantLabel.trim() : "";
|
|
1405
|
+
if (explicitTenantLabel) {
|
|
1406
|
+
joinResponse.tenantLabel = explicitTenantLabel;
|
|
1407
|
+
}
|
|
1408
|
+
else {
|
|
1409
|
+
try {
|
|
1410
|
+
const directoryDb = await this.openDB("directory", { adminOnlyDb: true });
|
|
1411
|
+
const setupLabel = await (0, tenantSetup_1.readTenantSetupLabel)(directoryDb, this);
|
|
1412
|
+
if (setupLabel) {
|
|
1413
|
+
joinResponse.tenantLabel = setupLabel;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
catch {
|
|
1417
|
+
// Best-effort: older tenants or missing setup doc.
|
|
1418
|
+
}
|
|
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
|
+
}
|
|
1164
1450
|
console.log(`[approveJoinRequest] ✓ Join request approved for user "${username}"`);
|
|
1165
1451
|
this.logger.info(`Join request approved for user: ${username}`);
|
|
1166
1452
|
// Return as URI string or object depending on format option
|
|
@@ -1326,8 +1612,9 @@ class BaseMindooTenant {
|
|
|
1326
1612
|
const signingKey = await this.getDecryptedSigningKey();
|
|
1327
1613
|
// Get the current user's decrypted RSA encryption private key (for decrypting entries)
|
|
1328
1614
|
const decryptedEncryptionKey = await this.getDecryptedEncryptionKey();
|
|
1329
|
-
// Create the client network store
|
|
1330
|
-
|
|
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);
|
|
1331
1618
|
console.log(`[connectToServer] ✓ Connected to server for db "${validDbId}"`);
|
|
1332
1619
|
this.logger.info(`Connected to server for db "${validDbId}"`);
|
|
1333
1620
|
return store;
|
|
@@ -1491,6 +1778,9 @@ class BaseMindooTenant {
|
|
|
1491
1778
|
* @returns true if the key can be resolved, false otherwise
|
|
1492
1779
|
*/
|
|
1493
1780
|
async hasDecryptionKey(decryptionKeyId) {
|
|
1781
|
+
if ((0, sealedTypes_1.isSealedKeyId)(decryptionKeyId)) {
|
|
1782
|
+
return (this.sealedGenerations.get(decryptionKeyId)?.length ?? 0) > 0;
|
|
1783
|
+
}
|
|
1494
1784
|
try {
|
|
1495
1785
|
await this.getSymmetricKey(decryptionKeyId);
|
|
1496
1786
|
return true;
|
|
@@ -1513,14 +1803,14 @@ class BaseMindooTenant {
|
|
|
1513
1803
|
return this.keyBag.getDocKeyFingerprint(this.tenantId);
|
|
1514
1804
|
}
|
|
1515
1805
|
/**
|
|
1516
|
-
* Remove a **named** document key from the local KeyBag. Refuses to delete
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
* 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.
|
|
1520
1810
|
*/
|
|
1521
1811
|
async removeNamedDecryptionKey(decryptionKeyId) {
|
|
1522
|
-
if (decryptionKeyId ===
|
|
1523
|
-
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`);
|
|
1524
1814
|
return false;
|
|
1525
1815
|
}
|
|
1526
1816
|
try {
|
|
@@ -1557,19 +1847,24 @@ class BaseMindooTenant {
|
|
|
1557
1847
|
const directory = await this.openDirectory();
|
|
1558
1848
|
const removed = [];
|
|
1559
1849
|
const imported = [];
|
|
1560
|
-
// Revoke pass — no key, no comparison, idempotent.
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
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);
|
|
1565
1857
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1858
|
+
else if (typeof directory.getRevokedDecryptionKeyIdsForUser === "function") {
|
|
1859
|
+
revokedIds = await directory.getRevokedDecryptionKeyIdsForUser(username);
|
|
1568
1860
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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);
|
|
1573
1868
|
}
|
|
1574
1869
|
}
|
|
1575
1870
|
// Import pass — sources the encryption private key from this tenant. The
|
|
@@ -1589,7 +1884,41 @@ class BaseMindooTenant {
|
|
|
1589
1884
|
this.logger.warn(`reconcileKeyDistributionsForCurrentUser: import pass failed: ${error}`);
|
|
1590
1885
|
}
|
|
1591
1886
|
}
|
|
1592
|
-
|
|
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
|
+
}
|
|
1593
1922
|
}
|
|
1594
1923
|
/**
|
|
1595
1924
|
* Single-flight, best-effort wrapper around
|
|
@@ -1701,13 +2030,14 @@ class BaseMindooTenant {
|
|
|
1701
2030
|
/**
|
|
1702
2031
|
* Remove exactly the key versions of `keyId` whose raw-bytes fingerprint is in
|
|
1703
2032
|
* `fingerprints` (version-scoped `pullfrom` revocation). Versions obtained
|
|
1704
|
-
* elsewhere — i.e. not in the distribution manifest — survive. Refuses
|
|
1705
|
-
*
|
|
1706
|
-
*
|
|
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).
|
|
1707
2037
|
* Returns the number of versions removed.
|
|
1708
2038
|
*/
|
|
1709
2039
|
async removeDecryptionKeyVersionsByFingerprint(keyId, fingerprints) {
|
|
1710
|
-
if (keyId ===
|
|
2040
|
+
if (keyId === types_1.PUBLIC_INFOS_KEY_ID) {
|
|
1711
2041
|
this.logger.debug(`removeDecryptionKeyVersionsByFingerprint: refusing protected key ${keyId}`);
|
|
1712
2042
|
return 0;
|
|
1713
2043
|
}
|
|
@@ -1752,6 +2082,13 @@ class BaseMindooTenant {
|
|
|
1752
2082
|
* @returns The decrypted symmetric key bytes
|
|
1753
2083
|
*/
|
|
1754
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
|
+
}
|
|
1755
2092
|
if (decryptionKeyId === "default") {
|
|
1756
2093
|
// Use cached tenant encryption key if available
|
|
1757
2094
|
if (this.decryptedTenantKeyCache) {
|