mindoodb 0.0.23 → 0.0.32
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 +9 -1
- package/README.md +5 -3
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +24 -5
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts +14 -2
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +124 -27
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +159 -4
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +334 -25
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts +9 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +77 -5
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -1
- package/dist/browser/index.mjs +103 -0
- package/dist/core/BaseMindooDB.d.ts +697 -4
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +3273 -180
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +192 -1
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +633 -88
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +770 -10
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +2715 -292
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/BaseMindooTenantFactory.d.ts +2 -0
- package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantFactory.js +20 -0
- package/dist/core/BaseMindooTenantFactory.js.map +1 -1
- package/dist/core/accesscontrol/AccessDeniedError.d.ts +22 -0
- package/dist/core/accesscontrol/AccessDeniedError.d.ts.map +1 -0
- package/dist/core/accesscontrol/AccessDeniedError.js +28 -0
- package/dist/core/accesscontrol/AccessDeniedError.js.map +1 -0
- package/dist/core/accesscontrol/DirectoryStateNode.d.ts +220 -0
- package/dist/core/accesscontrol/DirectoryStateNode.d.ts.map +1 -0
- package/dist/core/accesscontrol/DirectoryStateNode.js +297 -0
- package/dist/core/accesscontrol/DirectoryStateNode.js.map +1 -0
- package/dist/core/accesscontrol/DirectoryTimeTravelIndex.d.ts +118 -0
- package/dist/core/accesscontrol/DirectoryTimeTravelIndex.d.ts.map +1 -0
- package/dist/core/accesscontrol/DirectoryTimeTravelIndex.js +173 -0
- package/dist/core/accesscontrol/DirectoryTimeTravelIndex.js.map +1 -0
- package/dist/core/accesscontrol/directoryProjection.d.ts +26 -0
- package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -0
- package/dist/core/accesscontrol/directoryProjection.js +174 -0
- package/dist/core/accesscontrol/directoryProjection.js.map +1 -0
- package/dist/core/accesscontrol/evaluate.d.ts +77 -0
- package/dist/core/accesscontrol/evaluate.d.ts.map +1 -0
- package/dist/core/accesscontrol/evaluate.js +206 -0
- package/dist/core/accesscontrol/evaluate.js.map +1 -0
- package/dist/core/accesscontrol/grantKeys.d.ts +113 -0
- package/dist/core/accesscontrol/grantKeys.d.ts.map +1 -0
- package/dist/core/accesscontrol/grantKeys.js +302 -0
- package/dist/core/accesscontrol/grantKeys.js.map +1 -0
- package/dist/core/accesscontrol/keyBagReconciler.d.ts +22 -0
- package/dist/core/accesscontrol/keyBagReconciler.d.ts.map +1 -0
- package/dist/core/accesscontrol/keyBagReconciler.js +3 -0
- package/dist/core/accesscontrol/keyBagReconciler.js.map +1 -0
- package/dist/core/accesscontrol/materializationGuard.d.ts +89 -0
- package/dist/core/accesscontrol/materializationGuard.d.ts.map +1 -0
- package/dist/core/accesscontrol/materializationGuard.js +69 -0
- package/dist/core/accesscontrol/materializationGuard.js.map +1 -0
- package/dist/core/accesscontrol/quarantine.d.ts +65 -0
- package/dist/core/accesscontrol/quarantine.d.ts.map +1 -0
- package/dist/core/accesscontrol/quarantine.js +79 -0
- package/dist/core/accesscontrol/quarantine.js.map +1 -0
- package/dist/core/accesscontrol/receiptValidation.d.ts +113 -0
- package/dist/core/accesscontrol/receiptValidation.d.ts.map +1 -0
- package/dist/core/accesscontrol/receiptValidation.js +142 -0
- package/dist/core/accesscontrol/receiptValidation.js.map +1 -0
- package/dist/core/accesscontrol/timestamp/Ed25519WitnessProvider.d.ts +31 -0
- package/dist/core/accesscontrol/timestamp/Ed25519WitnessProvider.d.ts.map +1 -0
- package/dist/core/accesscontrol/timestamp/Ed25519WitnessProvider.js +66 -0
- package/dist/core/accesscontrol/timestamp/Ed25519WitnessProvider.js.map +1 -0
- package/dist/core/accesscontrol/timestamp/TimestampProvider.d.ts +79 -0
- package/dist/core/accesscontrol/timestamp/TimestampProvider.d.ts.map +1 -0
- package/dist/core/accesscontrol/timestamp/TimestampProvider.js +22 -0
- package/dist/core/accesscontrol/timestamp/TimestampProvider.js.map +1 -0
- package/dist/core/accesscontrol/types.d.ts +825 -0
- package/dist/core/accesscontrol/types.d.ts.map +1 -0
- package/dist/core/accesscontrol/types.js +534 -0
- package/dist/core/accesscontrol/types.js.map +1 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +9 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +54 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts +38 -2
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.js +199 -26
- package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts +17 -3
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.d.ts +58 -4
- package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.js +4 -0
- package/dist/core/appendonlystores/network/types.js.map +1 -1
- package/dist/core/appendonlystores/scanUtils.d.ts +27 -0
- package/dist/core/appendonlystores/scanUtils.d.ts.map +1 -0
- package/dist/core/appendonlystores/scanUtils.js +87 -0
- package/dist/core/appendonlystores/scanUtils.js.map +1 -0
- package/dist/core/appendonlystores/types.d.ts +23 -2
- package/dist/core/appendonlystores/types.d.ts.map +1 -1
- package/dist/core/cache/CacheManager.d.ts +13 -3
- package/dist/core/cache/CacheManager.d.ts.map +1 -1
- package/dist/core/cache/CacheManager.js +7 -4
- package/dist/core/cache/CacheManager.js.map +1 -1
- package/dist/core/crypto/EntrySignature.d.ts +137 -0
- package/dist/core/crypto/EntrySignature.d.ts.map +1 -0
- package/dist/core/crypto/EntrySignature.js +210 -0
- package/dist/core/crypto/EntrySignature.js.map +1 -0
- package/dist/core/crypto/MindooDocSigner.d.ts.map +1 -1
- package/dist/core/crypto/MindooDocSigner.js +6 -2
- package/dist/core/crypto/MindooDocSigner.js.map +1 -1
- package/dist/core/crypto/WitnessReceipt.d.ts +137 -0
- package/dist/core/crypto/WitnessReceipt.d.ts.map +1 -0
- package/dist/core/crypto/WitnessReceipt.js +182 -0
- package/dist/core/crypto/WitnessReceipt.js.map +1 -0
- package/dist/core/crypto/encryptedFields.d.ts +27 -0
- package/dist/core/crypto/encryptedFields.d.ts.map +1 -0
- package/dist/core/crypto/encryptedFields.js +58 -0
- package/dist/core/crypto/encryptedFields.js.map +1 -0
- package/dist/core/crypto/pbkdf2Iterations.d.ts +12 -0
- package/dist/core/crypto/pbkdf2Iterations.d.ts.map +1 -1
- package/dist/core/crypto/pbkdf2Iterations.js +23 -0
- package/dist/core/crypto/pbkdf2Iterations.js.map +1 -1
- package/dist/core/crypto/privateKeyEncryption.d.ts.map +1 -1
- package/dist/core/crypto/privateKeyEncryption.js +3 -1
- package/dist/core/crypto/privateKeyEncryption.js.map +1 -1
- package/dist/core/databaseIdValidation.d.ts +1 -1
- package/dist/core/databaseIdValidation.d.ts.map +1 -1
- package/dist/core/databaseIdValidation.js +4 -4
- package/dist/core/databaseIdValidation.js.map +1 -1
- package/dist/core/errors.d.ts +26 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +42 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/expressions/builder.d.ts +102 -0
- package/dist/core/expressions/builder.d.ts.map +1 -0
- package/dist/core/expressions/builder.js +249 -0
- package/dist/core/expressions/builder.js.map +1 -0
- package/dist/core/expressions/evaluateExpression.d.ts +78 -0
- package/dist/core/expressions/evaluateExpression.d.ts.map +1 -0
- package/dist/core/expressions/evaluateExpression.js +386 -0
- package/dist/core/expressions/evaluateExpression.js.map +1 -0
- package/dist/core/expressions/formulaSource.d.ts +24 -0
- package/dist/core/expressions/formulaSource.d.ts.map +1 -0
- package/dist/core/expressions/formulaSource.js +765 -0
- package/dist/core/expressions/formulaSource.js.map +1 -0
- package/dist/core/expressions/index.d.ts +8 -0
- package/dist/core/expressions/index.d.ts.map +1 -0
- package/dist/core/expressions/index.js +46 -0
- package/dist/core/expressions/index.js.map +1 -0
- package/dist/core/expressions/metadata.d.ts +23 -0
- package/dist/core/expressions/metadata.d.ts.map +1 -0
- package/dist/core/expressions/metadata.js +191 -0
- package/dist/core/expressions/metadata.js.map +1 -0
- package/dist/core/expressions/types.d.ts +164 -0
- package/dist/core/expressions/types.d.ts.map +1 -0
- package/dist/core/expressions/types.js +3 -0
- package/dist/core/expressions/types.js.map +1 -0
- package/dist/core/extraction/types.d.ts +47 -0
- package/dist/core/extraction/types.d.ts.map +1 -0
- package/dist/core/extraction/types.js +42 -0
- package/dist/core/extraction/types.js.map +1 -0
- package/dist/core/index.d.ts +13 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +113 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/indexing/createViewDataProvider.d.ts +84 -0
- package/dist/core/indexing/createViewDataProvider.d.ts.map +1 -0
- package/dist/core/indexing/createViewDataProvider.js +139 -0
- package/dist/core/indexing/createViewDataProvider.js.map +1 -0
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts +180 -0
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts.map +1 -0
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.js +548 -0
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.js.map +1 -0
- package/dist/core/indexing/fulltext/SearchEngineAdapter.d.ts +69 -0
- package/dist/core/indexing/fulltext/SearchEngineAdapter.d.ts.map +1 -0
- package/dist/core/indexing/fulltext/SearchEngineAdapter.js +183 -0
- package/dist/core/indexing/fulltext/SearchEngineAdapter.js.map +1 -0
- package/dist/core/indexing/fulltext/extractFulltextText.d.ts +34 -0
- package/dist/core/indexing/fulltext/extractFulltextText.d.ts.map +1 -0
- package/dist/core/indexing/fulltext/extractFulltextText.js +129 -0
- package/dist/core/indexing/fulltext/extractFulltextText.js.map +1 -0
- package/dist/core/indexing/fulltext/index.d.ts +5 -0
- package/dist/core/indexing/fulltext/index.d.ts.map +1 -0
- package/dist/core/indexing/fulltext/index.js +20 -0
- package/dist/core/indexing/fulltext/index.js.map +1 -0
- package/dist/core/indexing/fulltext/types.d.ts +168 -0
- package/dist/core/indexing/fulltext/types.d.ts.map +1 -0
- package/dist/core/indexing/fulltext/types.js +89 -0
- package/dist/core/indexing/fulltext/types.js.map +1 -0
- package/dist/core/indexing/summary/DocumentSummaryStore.d.ts +137 -0
- package/dist/core/indexing/summary/DocumentSummaryStore.d.ts.map +1 -0
- package/dist/core/indexing/summary/DocumentSummaryStore.js +438 -0
- package/dist/core/indexing/summary/DocumentSummaryStore.js.map +1 -0
- package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.d.ts +72 -0
- package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.d.ts.map +1 -0
- package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.js +221 -0
- package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.js.map +1 -0
- package/dist/core/indexing/summary/extractSummaryFields.d.ts +75 -0
- package/dist/core/indexing/summary/extractSummaryFields.d.ts.map +1 -0
- package/dist/core/indexing/summary/extractSummaryFields.js +266 -0
- package/dist/core/indexing/summary/extractSummaryFields.js.map +1 -0
- package/dist/core/indexing/summary/index.d.ts +5 -0
- package/dist/core/indexing/summary/index.d.ts.map +1 -0
- package/dist/core/indexing/summary/index.js +23 -0
- package/dist/core/indexing/summary/index.js.map +1 -0
- package/dist/core/indexing/summary/types.d.ts +119 -0
- package/dist/core/indexing/summary/types.d.ts.map +1 -0
- package/dist/core/indexing/summary/types.js +81 -0
- package/dist/core/indexing/summary/types.js.map +1 -0
- package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.d.ts +47 -1
- package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +24 -8
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +96 -41
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -1
- package/dist/core/indexing/virtualviews/ViewEntrySortKey.d.ts +2 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKey.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/ViewEntrySortKey.js +8 -3
- package/dist/core/indexing/virtualviews/ViewEntrySortKey.js.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualView.d.ts +111 -5
- package/dist/core/indexing/virtualviews/VirtualView.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualView.js +266 -75
- package/dist/core/indexing/virtualviews/VirtualView.js.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewColumn.d.ts +12 -0
- package/dist/core/indexing/virtualviews/VirtualViewColumn.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewColumn.js +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewColumn.js.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts +12 -0
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.js +66 -5
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.js.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts +50 -6
- package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewFactory.js +54 -12
- package/dist/core/indexing/virtualviews/VirtualViewFactory.js.map +1 -1
- package/dist/core/indexing/virtualviews/index.d.ts +3 -3
- package/dist/core/indexing/virtualviews/index.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/index.js.map +1 -1
- package/dist/core/indexing/virtualviews/types.d.ts +6 -0
- package/dist/core/indexing/virtualviews/types.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/types.js +11 -3
- package/dist/core/indexing/virtualviews/types.js.map +1 -1
- package/dist/core/keys/KeyBag.d.ts +25 -0
- package/dist/core/keys/KeyBag.d.ts.map +1 -1
- package/dist/core/keys/KeyBag.js +37 -1
- package/dist/core/keys/KeyBag.js.map +1 -1
- package/dist/core/query/executeQuery.d.ts +18 -0
- package/dist/core/query/executeQuery.d.ts.map +1 -0
- package/dist/core/query/executeQuery.js +313 -0
- package/dist/core/query/executeQuery.js.map +1 -0
- package/dist/core/query/index.d.ts +5 -0
- package/dist/core/query/index.d.ts.map +1 -0
- package/dist/core/query/index.js +14 -0
- package/dist/core/query/index.js.map +1 -0
- package/dist/core/query/queryLive.d.ts +27 -0
- package/dist/core/query/queryLive.d.ts.map +1 -0
- package/dist/core/query/queryLive.js +104 -0
- package/dist/core/query/queryLive.js.map +1 -0
- package/dist/core/query/queryView.d.ts +156 -0
- package/dist/core/query/queryView.d.ts.map +1 -0
- package/dist/core/query/queryView.js +266 -0
- package/dist/core/query/queryView.js.map +1 -0
- package/dist/core/query/types.d.ts +121 -0
- package/dist/core/query/types.d.ts.map +1 -0
- package/dist/core/query/types.js +18 -0
- package/dist/core/query/types.js.map +1 -0
- package/dist/core/storeEntryTime.d.ts +57 -0
- package/dist/core/storeEntryTime.d.ts.map +1 -0
- package/dist/core/storeEntryTime.js +72 -0
- package/dist/core/storeEntryTime.js.map +1 -0
- package/dist/core/types.d.ts +1509 -39
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +13 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/uri/MindooURI.d.ts +53 -1
- package/dist/core/uri/MindooURI.d.ts.map +1 -1
- package/dist/core/uri/MindooURI.js +135 -0
- package/dist/core/uri/MindooURI.js.map +1 -1
- package/dist/core/utils/urlSafety.d.ts +46 -0
- package/dist/core/utils/urlSafety.d.ts.map +1 -0
- package/dist/core/utils/urlSafety.js +144 -0
- package/dist/core/utils/urlSafety.js.map +1 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +14 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +95 -4
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
- package/dist/node/server/CapabilityMatcher.d.ts +10 -0
- package/dist/node/server/CapabilityMatcher.d.ts.map +1 -1
- package/dist/node/server/CapabilityMatcher.js +14 -2
- package/dist/node/server/CapabilityMatcher.js.map +1 -1
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +117 -8
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/PurgedDocRegistry.d.ts +32 -0
- package/dist/node/server/PurgedDocRegistry.d.ts.map +1 -0
- package/dist/node/server/PurgedDocRegistry.js +94 -0
- package/dist/node/server/PurgedDocRegistry.js.map +1 -0
- package/dist/node/server/ServerSync.d.ts.map +1 -1
- package/dist/node/server/ServerSync.js +5 -2
- package/dist/node/server/ServerSync.js.map +1 -1
- package/dist/node/server/SystemAdminAuth.d.ts +10 -0
- package/dist/node/server/SystemAdminAuth.d.ts.map +1 -1
- package/dist/node/server/SystemAdminAuth.js +25 -0
- package/dist/node/server/SystemAdminAuth.js.map +1 -1
- package/dist/node/server/TenantManager.d.ts +95 -1
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +382 -15
- package/dist/node/server/TenantManager.js.map +1 -1
- package/dist/node/server/config.d.ts.map +1 -1
- package/dist/node/server/config.js +5 -1
- package/dist/node/server/config.js.map +1 -1
- package/dist/node/server/types.d.ts +29 -3
- package/dist/node/server/types.d.ts.map +1 -1
- package/dist/node/server/types.js +12 -0
- package/dist/node/server/types.js.map +1 -1
- package/dist/node/server/validation.d.ts +6 -0
- package/dist/node/server/validation.d.ts.map +1 -1
- package/dist/node/server/validation.js +7 -1
- package/dist/node/server/validation.js.map +1 -1
- package/package.json +4 -3
- package/dist/appendonlystores/BasicOnDiskContentAddressedStoreFactory.d.ts +0 -8
- package/dist/appendonlystores/BasicOnDiskContentAddressedStoreFactory.d.ts.map +0 -1
- package/dist/appendonlystores/BasicOnDiskContentAddressedStoreFactory.js +0 -16
- package/dist/appendonlystores/BasicOnDiskContentAddressedStoreFactory.js.map +0 -1
- package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +0 -68
- package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +0 -1
- package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.js +0 -659
- package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.js.map +0 -1
- package/dist/node/attachments/writeAttachmentToFile.d.ts +0 -15
- package/dist/node/attachments/writeAttachmentToFile.d.ts.map +0 -1
- package/dist/node/attachments/writeAttachmentToFile.js +0 -73
- package/dist/node/attachments/writeAttachmentToFile.js.map +0 -1
|
@@ -4,9 +4,13 @@ exports.BaseMindooTenant = void 0;
|
|
|
4
4
|
const types_1 = require("./types");
|
|
5
5
|
const BaseMindooDB_1 = require("./BaseMindooDB");
|
|
6
6
|
const BaseMindooTenantDirectory_1 = require("./BaseMindooTenantDirectory");
|
|
7
|
+
const grantKeys_1 = require("./accesscontrol/grantKeys");
|
|
7
8
|
const MindooDocSigner_1 = require("./crypto/MindooDocSigner");
|
|
8
9
|
const RSAEncryption_1 = require("./crypto/RSAEncryption");
|
|
9
10
|
const privateKeyEncryption_1 = require("./crypto/privateKeyEncryption");
|
|
11
|
+
const EntrySignature_1 = require("./crypto/EntrySignature");
|
|
12
|
+
const storeEntryTime_1 = require("./storeEntryTime");
|
|
13
|
+
const idGeneration_1 = require("./utils/idGeneration");
|
|
10
14
|
const errors_1 = require("./errors");
|
|
11
15
|
const logging_1 = require("./logging");
|
|
12
16
|
const MindooURI_1 = require("./uri/MindooURI");
|
|
@@ -60,6 +64,27 @@ class BaseMindooTenant {
|
|
|
60
64
|
* explicit calls cannot race against each other or duplicate work.
|
|
61
65
|
*/
|
|
62
66
|
this.keyBagReconcilePromise = null;
|
|
67
|
+
/**
|
|
68
|
+
* Cache of imported AES-GCM `CryptoKey`s, keyed by usage + raw key bytes
|
|
69
|
+
* (hex). `subtle.importKey` costs a WebCrypto round trip per call, which
|
|
70
|
+
* adds up on bulk writes/reads (one import per store entry). Because the
|
|
71
|
+
* cache is content-addressed by the key bytes, rotated key versions miss
|
|
72
|
+
* the cache naturally — no explicit invalidation is needed.
|
|
73
|
+
*/
|
|
74
|
+
this.importedAesKeyCache = new Map();
|
|
75
|
+
/**
|
|
76
|
+
* Cache of imported Ed25519 verify `CryptoKey`s, keyed by the PEM public
|
|
77
|
+
* key. Signature verification runs once per entry when materializing
|
|
78
|
+
* documents, so the per-call SPKI import is on the hot read path.
|
|
79
|
+
*/
|
|
80
|
+
this.importedVerifyKeyCache = new Map();
|
|
81
|
+
// Single-flight guard for SDK-driven key-distribution reconcile (§13). The
|
|
82
|
+
// reconcile driver itself calls getDirectoryDB / updateUnifiedCache /
|
|
83
|
+
// syncStoreChanges, any of which can re-enter a trigger; this flag keeps the
|
|
84
|
+
// run-always trigger (directory bring-up + after each directory pull) from
|
|
85
|
+
// recursing or overlapping. Reconcile is idempotent, so a skipped overlap is
|
|
86
|
+
// harmless — the next trigger re-runs it.
|
|
87
|
+
this.reconcileInFlight = false;
|
|
63
88
|
// Local cache support
|
|
64
89
|
this.cacheManager = null;
|
|
65
90
|
this.factory = factory;
|
|
@@ -189,59 +214,86 @@ class BaseMindooTenant {
|
|
|
189
214
|
getAdministrationEncryptionPublicKey() {
|
|
190
215
|
return this.administrationEncryptionPublicKey;
|
|
191
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Import an AES-GCM key for `usage`, memoized on the raw key bytes. See
|
|
219
|
+
* {@link importedAesKeyCache}. On overflow the cache is simply cleared —
|
|
220
|
+
* refilling it costs one import per active key, which is negligible.
|
|
221
|
+
*/
|
|
222
|
+
async importAesKeyCached(symmetricKey, usage) {
|
|
223
|
+
let cacheKey = usage + ":";
|
|
224
|
+
for (let i = 0; i < symmetricKey.length; i++) {
|
|
225
|
+
cacheKey += symmetricKey[i].toString(16).padStart(2, "0");
|
|
226
|
+
}
|
|
227
|
+
const cached = this.importedAesKeyCache.get(cacheKey);
|
|
228
|
+
if (cached) {
|
|
229
|
+
return cached;
|
|
230
|
+
}
|
|
231
|
+
const subtle = this.cryptoAdapter.getSubtle();
|
|
232
|
+
// Create a new Uint8Array to ensure we have a proper ArrayBuffer
|
|
233
|
+
const keyArray = new Uint8Array(symmetricKey);
|
|
234
|
+
const cryptoKey = await subtle.importKey("raw", keyArray.buffer, { name: "AES-GCM" }, false, // not extractable
|
|
235
|
+
[usage]);
|
|
236
|
+
if (this.importedAesKeyCache.size >= BaseMindooTenant.MAX_IMPORTED_KEY_CACHE_ENTRIES) {
|
|
237
|
+
this.importedAesKeyCache.clear();
|
|
238
|
+
}
|
|
239
|
+
this.importedAesKeyCache.set(cacheKey, cryptoKey);
|
|
240
|
+
return cryptoKey;
|
|
241
|
+
}
|
|
192
242
|
async encryptPayload(payload, decryptionKeyId) {
|
|
193
|
-
|
|
194
|
-
|
|
243
|
+
// The per-step debug logs below are on the hot write path (one call per
|
|
244
|
+
// store entry); skip building the interpolated strings when debug is off.
|
|
245
|
+
const debugEnabled = this.logger.isLevelEnabled(logging_1.LogLevel.DEBUG);
|
|
246
|
+
if (debugEnabled) {
|
|
247
|
+
this.logger.debug(`Encrypting payload with key: ${decryptionKeyId}`);
|
|
248
|
+
this.logger.debug(`Payload size: ${payload.length} bytes`);
|
|
249
|
+
}
|
|
195
250
|
// Get the symmetric key for this key ID
|
|
196
251
|
let symmetricKey;
|
|
197
252
|
try {
|
|
198
253
|
if (decryptionKeyId === "default") {
|
|
199
|
-
this.logger.debug(`Using default key (tenant encryption key)`);
|
|
200
254
|
// Use cached tenant encryption key if available
|
|
201
255
|
if (this.decryptedTenantKeyCache) {
|
|
202
256
|
symmetricKey = this.decryptedTenantKeyCache;
|
|
203
|
-
|
|
257
|
+
if (debugEnabled)
|
|
258
|
+
this.logger.debug(`Using cached tenant key, length: ${symmetricKey.length} bytes`);
|
|
204
259
|
}
|
|
205
260
|
else {
|
|
206
|
-
|
|
261
|
+
if (debugEnabled)
|
|
262
|
+
this.logger.debug(`Resolving tenant encryption key from KeyBag`);
|
|
207
263
|
const tenantKey = await this.keyBag.get("doc", this.tenantId, types_1.DEFAULT_TENANT_KEY_ID);
|
|
208
264
|
if (!tenantKey) {
|
|
209
265
|
throw new errors_1.SymmetricKeyNotFoundError(`doc:${this.tenantId}:${types_1.DEFAULT_TENANT_KEY_ID}`);
|
|
210
266
|
}
|
|
211
267
|
symmetricKey = tenantKey;
|
|
212
268
|
this.decryptedTenantKeyCache = symmetricKey;
|
|
213
|
-
|
|
269
|
+
if (debugEnabled)
|
|
270
|
+
this.logger.debug(`Loaded tenant key from KeyBag, length: ${symmetricKey.length} bytes`);
|
|
214
271
|
}
|
|
215
272
|
}
|
|
216
273
|
else {
|
|
217
|
-
|
|
274
|
+
if (debugEnabled)
|
|
275
|
+
this.logger.debug(`Getting named key from KeyBag: ${decryptionKeyId}`);
|
|
218
276
|
// Get the decrypted key from KeyBag
|
|
219
277
|
const decryptedKey = await this.keyBag.get("doc", this.tenantId, decryptionKeyId);
|
|
220
278
|
if (!decryptedKey) {
|
|
221
279
|
throw new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
|
|
222
280
|
}
|
|
223
281
|
symmetricKey = decryptedKey;
|
|
224
|
-
|
|
282
|
+
if (debugEnabled)
|
|
283
|
+
this.logger.debug(`Got named key, length: ${symmetricKey.length} bytes`);
|
|
225
284
|
}
|
|
226
285
|
}
|
|
227
286
|
catch (error) {
|
|
228
287
|
this.logger.error(`Error getting symmetric key:`, error);
|
|
229
288
|
throw error;
|
|
230
289
|
}
|
|
231
|
-
this.logger.debug(`Got symmetric key, length: ${symmetricKey.length} bytes`);
|
|
232
290
|
const subtle = this.cryptoAdapter.getSubtle();
|
|
233
291
|
// Bind getRandomValues to maintain 'this' context
|
|
234
292
|
const randomValues = this.cryptoAdapter.getRandomValues.bind(this.cryptoAdapter);
|
|
235
|
-
// Import the symmetric key
|
|
236
|
-
this.logger.debug(`Importing symmetric key for AES-GCM`);
|
|
293
|
+
// Import the symmetric key (memoized; see importAesKeyCached)
|
|
237
294
|
let cryptoKey;
|
|
238
295
|
try {
|
|
239
|
-
|
|
240
|
-
const keyArray = new Uint8Array(symmetricKey);
|
|
241
|
-
this.logger.debug(`Key buffer size: ${keyArray.buffer.byteLength} bytes`);
|
|
242
|
-
cryptoKey = await subtle.importKey("raw", keyArray.buffer, { name: "AES-GCM" }, false, // not extractable
|
|
243
|
-
["encrypt"]);
|
|
244
|
-
this.logger.debug(`Successfully imported key`);
|
|
296
|
+
cryptoKey = await this.importAesKeyCached(symmetricKey, "encrypt");
|
|
245
297
|
}
|
|
246
298
|
catch (error) {
|
|
247
299
|
this.logger.error(`Error importing key:`, error);
|
|
@@ -249,33 +301,26 @@ class BaseMindooTenant {
|
|
|
249
301
|
throw error;
|
|
250
302
|
}
|
|
251
303
|
// Generate IV (12 bytes for AES-GCM)
|
|
252
|
-
this.logger.debug(`Generating IV`);
|
|
253
304
|
let iv;
|
|
254
305
|
try {
|
|
255
306
|
const ivArray = new Uint8Array(12);
|
|
256
307
|
randomValues(ivArray);
|
|
257
308
|
iv = new Uint8Array(ivArray.buffer, ivArray.byteOffset, ivArray.byteLength);
|
|
258
|
-
this.logger.debug(`Generated IV: ${iv.length} bytes`);
|
|
259
309
|
}
|
|
260
310
|
catch (error) {
|
|
261
311
|
this.logger.error(`Error generating IV:`, error);
|
|
262
312
|
throw error;
|
|
263
313
|
}
|
|
264
314
|
// Encrypt the payload
|
|
265
|
-
this.logger.debug(`Encrypting payload with AES-GCM`);
|
|
266
|
-
this.logger.debug(`Payload buffer size: ${payload.buffer.byteLength} bytes`);
|
|
267
|
-
this.logger.debug(`Payload byteOffset: ${payload.byteOffset}, byteLength: ${payload.byteLength}`);
|
|
268
315
|
let encrypted;
|
|
269
316
|
try {
|
|
270
317
|
// Create a new Uint8Array to ensure we have a proper ArrayBuffer
|
|
271
318
|
const payloadArray = new Uint8Array(payload);
|
|
272
|
-
this.logger.debug(`Using payload array: ${payloadArray.length} bytes`);
|
|
273
319
|
encrypted = await subtle.encrypt({
|
|
274
320
|
name: "AES-GCM",
|
|
275
321
|
iv: iv,
|
|
276
322
|
tagLength: 128, // 128-bit authentication tag
|
|
277
323
|
}, cryptoKey, payloadArray);
|
|
278
|
-
this.logger.debug(`Successfully encrypted payload, encrypted size: ${encrypted.byteLength} bytes`);
|
|
279
324
|
}
|
|
280
325
|
catch (error) {
|
|
281
326
|
this.logger.error(`Error encrypting payload:`, error);
|
|
@@ -290,58 +335,73 @@ class BaseMindooTenant {
|
|
|
290
335
|
}
|
|
291
336
|
// Combine IV and encrypted data
|
|
292
337
|
// Format: IV (12 bytes) + encrypted data (includes authentication tag)
|
|
293
|
-
this.logger.debug(`Combining IV and encrypted data`);
|
|
294
338
|
const encryptedArray = new Uint8Array(encrypted);
|
|
295
339
|
const result = new Uint8Array(12 + encryptedArray.length);
|
|
296
340
|
result.set(iv, 0);
|
|
297
341
|
result.set(encryptedArray, 12);
|
|
298
|
-
|
|
342
|
+
if (debugEnabled)
|
|
343
|
+
this.logger.debug(`Encrypted payload (${payload.length} -> ${result.length} bytes)`);
|
|
299
344
|
return result;
|
|
300
345
|
}
|
|
301
346
|
async decryptPayload(encryptedPayload, decryptionKeyId) {
|
|
302
|
-
|
|
347
|
+
// Hot read path (one call per store entry): skip interpolations when off.
|
|
348
|
+
const debugEnabled = this.logger.isLevelEnabled(logging_1.LogLevel.DEBUG);
|
|
349
|
+
if (debugEnabled)
|
|
350
|
+
this.logger.debug(`Decrypting payload with key: ${decryptionKeyId}`);
|
|
303
351
|
if (encryptedPayload.length < 12) {
|
|
304
352
|
throw new Error("Encrypted payload too short (missing IV)");
|
|
305
353
|
}
|
|
306
354
|
// Extract IV (first 12 bytes) and encrypted data (rest)
|
|
307
355
|
const iv = encryptedPayload.slice(0, 12);
|
|
308
356
|
const encryptedData = encryptedPayload.slice(12);
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
357
|
+
// Gather ALL stored versions of the key (newest first). Key rotation keeps
|
|
358
|
+
// several versions under one keyId, and a payload may have been encrypted
|
|
359
|
+
// under any of them, so we must try each — not just the newest. The newest
|
|
360
|
+
// version is overwhelmingly the common case, so it is tried first.
|
|
361
|
+
const candidates = await this.getDecryptionKeyCandidates(decryptionKeyId);
|
|
362
|
+
if (candidates.length === 0) {
|
|
363
|
+
throw new errors_1.SymmetricKeyNotFoundError(decryptionKeyId === "default" ? `doc:${this.tenantId}:${types_1.DEFAULT_TENANT_KEY_ID}` : decryptionKeyId);
|
|
364
|
+
}
|
|
365
|
+
const subtle = this.cryptoAdapter.getSubtle();
|
|
366
|
+
let lastError = undefined;
|
|
367
|
+
for (const symmetricKey of candidates) {
|
|
368
|
+
try {
|
|
369
|
+
const cryptoKey = await this.importAesKeyCached(symmetricKey, "decrypt");
|
|
370
|
+
const decrypted = await subtle.decrypt({
|
|
371
|
+
name: "AES-GCM",
|
|
372
|
+
iv: iv,
|
|
373
|
+
tagLength: 128, // 128-bit authentication tag
|
|
374
|
+
}, cryptoKey, encryptedData);
|
|
375
|
+
if (debugEnabled) {
|
|
376
|
+
this.logger.debug(`Decrypted payload (${encryptedPayload.length} -> ${decrypted.byteLength} bytes)`);
|
|
320
377
|
}
|
|
321
|
-
|
|
322
|
-
this.decryptedTenantKeyCache = symmetricKey;
|
|
378
|
+
return new Uint8Array(decrypted);
|
|
323
379
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (!decryptedKey) {
|
|
329
|
-
throw new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
|
|
380
|
+
catch (error) {
|
|
381
|
+
// AES-GCM authentication failure means this version was not the one used
|
|
382
|
+
// to encrypt; fall through and try the next version.
|
|
383
|
+
lastError = error;
|
|
330
384
|
}
|
|
331
|
-
symmetricKey = decryptedKey;
|
|
332
385
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
386
|
+
this.logger.debug(`Decrypt failed against all ${candidates.length} version(s) of key "${decryptionKeyId}"`);
|
|
387
|
+
throw lastError ?? new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Return every stored version of the symmetric key for `decryptionKeyId`,
|
|
391
|
+
* newest first, mapping the `"default"` alias to the tenant default key id.
|
|
392
|
+
* Used by the decryption paths to support key rotation (a payload may have
|
|
393
|
+
* been encrypted under an older version than the current newest one).
|
|
394
|
+
*
|
|
395
|
+
* As a side effect, refreshes {@link decryptedTenantKeyCache} to the newest
|
|
396
|
+
* tenant-default version so encryption keeps using the latest key.
|
|
397
|
+
*/
|
|
398
|
+
async getDecryptionKeyCandidates(decryptionKeyId) {
|
|
399
|
+
const id = decryptionKeyId === "default" ? types_1.DEFAULT_TENANT_KEY_ID : decryptionKeyId;
|
|
400
|
+
const versions = await this.keyBag.getAllKeys("doc", this.tenantId, id);
|
|
401
|
+
if (decryptionKeyId === "default" && versions.length > 0) {
|
|
402
|
+
this.decryptedTenantKeyCache = versions[0];
|
|
403
|
+
}
|
|
404
|
+
return versions;
|
|
345
405
|
}
|
|
346
406
|
async signPayload(payload) {
|
|
347
407
|
this.logger.debug(`Signing payload`);
|
|
@@ -381,20 +441,100 @@ class BaseMindooTenant {
|
|
|
381
441
|
return false;
|
|
382
442
|
}
|
|
383
443
|
const subtle = this.cryptoAdapter.getSubtle();
|
|
384
|
-
//
|
|
385
|
-
const
|
|
386
|
-
// Import the public key from SPKI format
|
|
387
|
-
const cryptoKey = await subtle.importKey("spki", publicKeyBuffer, {
|
|
388
|
-
name: "Ed25519",
|
|
389
|
-
}, false, // not extractable
|
|
390
|
-
["verify"]);
|
|
444
|
+
// Import the public key from SPKI format (memoized per PEM string)
|
|
445
|
+
const cryptoKey = await this.importVerifyKeyCached(publicKey);
|
|
391
446
|
// Verify the signature
|
|
392
447
|
const isValid = await subtle.verify({
|
|
393
448
|
name: "Ed25519",
|
|
394
449
|
}, cryptoKey, signature.buffer, payload.buffer);
|
|
395
|
-
|
|
450
|
+
// Debug (not info): this runs once per verified entry on the read path.
|
|
451
|
+
if (this.logger.isLevelEnabled(logging_1.LogLevel.DEBUG)) {
|
|
452
|
+
this.logger.debug(`Signature verification result: ${isValid}`);
|
|
453
|
+
}
|
|
396
454
|
return isValid;
|
|
397
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Import an Ed25519 verify key from its PEM SPKI form, memoized per PEM
|
|
458
|
+
* string. See {@link importedVerifyKeyCache}.
|
|
459
|
+
*/
|
|
460
|
+
async importVerifyKeyCached(publicKeyPem) {
|
|
461
|
+
const cached = this.importedVerifyKeyCache.get(publicKeyPem);
|
|
462
|
+
if (cached) {
|
|
463
|
+
return cached;
|
|
464
|
+
}
|
|
465
|
+
const subtle = this.cryptoAdapter.getSubtle();
|
|
466
|
+
const cryptoKey = await subtle.importKey("spki", this.pemToArrayBuffer(publicKeyPem), {
|
|
467
|
+
name: "Ed25519",
|
|
468
|
+
}, false, // not extractable
|
|
469
|
+
["verify"]);
|
|
470
|
+
if (this.importedVerifyKeyCache.size >= BaseMindooTenant.MAX_IMPORTED_KEY_CACHE_ENTRIES) {
|
|
471
|
+
this.importedVerifyKeyCache.clear();
|
|
472
|
+
}
|
|
473
|
+
this.importedVerifyKeyCache.set(publicKeyPem, cryptoKey);
|
|
474
|
+
return cryptoKey;
|
|
475
|
+
}
|
|
476
|
+
async verifyEntrySignature(entry, encryptedData) {
|
|
477
|
+
// The author key must be trusted by the tenant directory.
|
|
478
|
+
const directory = await this.openDirectory();
|
|
479
|
+
const isTrusted = await directory.validatePublicSigningKey(entry.createdByPublicKey);
|
|
480
|
+
if (!isTrusted) {
|
|
481
|
+
this.logger.warn(`Public key not trusted: ${entry.createdByPublicKey}`);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
const subtle = this.cryptoAdapter.getSubtle();
|
|
485
|
+
// Integrity: the served bytes must hash to the signed/hashed contentHash so
|
|
486
|
+
// a relay cannot substitute payload bytes that disagree with the metadata
|
|
487
|
+
// (audit finding #5).
|
|
488
|
+
const actualHash = await (0, idGeneration_1.computeContentHash)(encryptedData, subtle);
|
|
489
|
+
if (actualHash !== entry.contentHash) {
|
|
490
|
+
this.logger.warn(`Content hash mismatch for entry ${entry.id} (expected ${entry.contentHash.substring(0, 16)}..., got ${actualHash.substring(0, 16)}...)`);
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
// Storage-format floor (requireMetadataSignatureSince): if the tenant
|
|
494
|
+
// requires the v2 metadata-binding signature for entries at/after a
|
|
495
|
+
// trusted-time cutoff, disable the legacy fallback for this entry. The
|
|
496
|
+
// cutoff is compared against the entry's trusted time so genuine older
|
|
497
|
+
// history still verifies via the legacy signature.
|
|
498
|
+
const requireMetadataSignature = await this.requiresMetadataSignature(directory, entry);
|
|
499
|
+
// Version-aware author signature: prefer the metadata-binding signature,
|
|
500
|
+
// fall back to the legacy ciphertext-only signature for v1/legacy entries
|
|
501
|
+
// (unless the floor above forbids the fallback for this entry). The
|
|
502
|
+
// author's verify key is imported once per PEM and cached — this runs for
|
|
503
|
+
// every entry a document replays.
|
|
504
|
+
const authorKey = await this.importVerifyKeyCached(entry.createdByPublicKey);
|
|
505
|
+
const isValid = await (0, EntrySignature_1.verifyEntrySignatureWithImportedKey)(entry, encryptedData, authorKey, subtle, { requireMetadataSignature });
|
|
506
|
+
if (this.logger.isLevelEnabled(logging_1.LogLevel.DEBUG)) {
|
|
507
|
+
this.logger.debug(`Entry signature verification result: ${isValid}`);
|
|
508
|
+
}
|
|
509
|
+
return isValid;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Whether the storage-format floor (`requireMetadataSignatureSince`) requires
|
|
513
|
+
* `entry` to carry the v2 metadata-binding signature, based on the entry's
|
|
514
|
+
* trusted time. Returns false when no floor is configured, when the directory
|
|
515
|
+
* implementation predates the policy, or on any resolution error (fail-open
|
|
516
|
+
* here is safe: the legacy signature is still cryptographically verified — the
|
|
517
|
+
* floor only refuses the *weaker* fallback for new entries).
|
|
518
|
+
*/
|
|
519
|
+
async requiresMetadataSignature(directory, entry) {
|
|
520
|
+
if (entry.metadataSignature) {
|
|
521
|
+
return false; // already v2 — the floor is irrelevant
|
|
522
|
+
}
|
|
523
|
+
if (typeof directory.getRequireMetadataSignatureSince !== "function") {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const cutoff = await directory.getRequireMetadataSignatureSince();
|
|
528
|
+
if (cutoff === undefined) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
return (0, storeEntryTime_1.entryTrustedTime)(entry, Date.now()) >= cutoff;
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
this.logger.warn(`Failed to resolve metadata-signature floor: ${String(error)}`);
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
398
538
|
async getCurrentUserId() {
|
|
399
539
|
// Convert PrivateUserId to PublicUserId
|
|
400
540
|
return {
|
|
@@ -417,6 +557,108 @@ class BaseMindooTenant {
|
|
|
417
557
|
}
|
|
418
558
|
return this.directoryCache;
|
|
419
559
|
}
|
|
560
|
+
/**
|
|
561
|
+
* Check whether an admin has requested a remote wipe of THIS device and, if
|
|
562
|
+
* so, delete the entire local tenant (docs/accesscontrol.md §6.5).
|
|
563
|
+
*
|
|
564
|
+
* The directive lives in the current user's admin-signed `grantaccess`
|
|
565
|
+
* document as `wipeRequestedForSigningKeys`. Because the directory database is
|
|
566
|
+
* admin-only, any grant document that materializes there is already verified
|
|
567
|
+
* as admin-signed — so finding this device's signing key in that list is a
|
|
568
|
+
* genuine, admin-issued directive (the server, which may be hostile, cannot
|
|
569
|
+
* forge it). Sync the directory before calling this so the latest directive is
|
|
570
|
+
* visible.
|
|
571
|
+
*
|
|
572
|
+
* @returns true if a wipe was applied (the tenant is now gone locally), false
|
|
573
|
+
* if this device is not targeted.
|
|
574
|
+
*/
|
|
575
|
+
async checkAndApplyRemoteWipe() {
|
|
576
|
+
const directory = await this.openDirectory();
|
|
577
|
+
const myKey = this.currentUser.userSigningKeyPair.publicKey;
|
|
578
|
+
const finder = directory;
|
|
579
|
+
if (typeof finder.findGrantAccessDocuments !== "function") {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
const grants = await finder.findGrantAccessDocuments(this.currentUser.username);
|
|
583
|
+
const targeted = grants.some((grant) => (0, grantKeys_1.extractWipeRequestedSigningKeys)(grant.getData()).includes(myKey));
|
|
584
|
+
if (!targeted) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
this.logger.warn(`Remote-wipe directive found for this device; deleting local tenant "${this.tenantId}".`);
|
|
588
|
+
await this.wipeLocalTenant();
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Delete all local data for this tenant from the device (docs/accesscontrol.md
|
|
593
|
+
* §6.5): every local database (the directory database plus all known data
|
|
594
|
+
* databases), this tenant's keys in the (multi-tenant) KeyBag, and all
|
|
595
|
+
* in-memory caches. Other tenants on the same device are untouched.
|
|
596
|
+
*
|
|
597
|
+
* Idempotent and best-effort: once the local tenant is gone there is nothing
|
|
598
|
+
* left to delete. Pass extra database ids for databases that exist locally but
|
|
599
|
+
* have not been opened in this session.
|
|
600
|
+
*
|
|
601
|
+
* @param additionalDbIds Optional extra local database ids to wipe.
|
|
602
|
+
*/
|
|
603
|
+
async wipeLocalTenant(additionalDbIds) {
|
|
604
|
+
// Every local database belonging to this tenant: the directory plus any
|
|
605
|
+
// opened/known data databases. Wipe is a whole-tenant operation.
|
|
606
|
+
const dbIds = new Set([
|
|
607
|
+
"directory",
|
|
608
|
+
...this.databaseCache.keys(),
|
|
609
|
+
...(additionalDbIds ?? []),
|
|
610
|
+
]);
|
|
611
|
+
for (const dbId of dbIds) {
|
|
612
|
+
await this.clearLocalStoresForDb(dbId);
|
|
613
|
+
}
|
|
614
|
+
// Drop in-memory caches and decrypted key material so nothing lingers.
|
|
615
|
+
this.databaseCache.clear();
|
|
616
|
+
this.directoryCache = null;
|
|
617
|
+
this.remoteStoreCache.clear();
|
|
618
|
+
this.decryptedTenantKeyCache = undefined;
|
|
619
|
+
this.decryptedUserSigningKeyCache = undefined;
|
|
620
|
+
this.decryptedUserEncryptionKeyCache = undefined;
|
|
621
|
+
this.disposeCacheManager();
|
|
622
|
+
// Remove this tenant's keys from the multi-tenant KeyBag, leaving other
|
|
623
|
+
// tenants intact.
|
|
624
|
+
try {
|
|
625
|
+
await this.keyBag.deleteTenantKeys(this.tenantId);
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
this.logger.warn(`wipeLocalTenant: failed to delete KeyBag keys: ${error}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/** Clear the document and attachment stores for one local database. */
|
|
632
|
+
async clearLocalStoresForDb(dbId) {
|
|
633
|
+
let docStore;
|
|
634
|
+
let attachmentStore;
|
|
635
|
+
const opened = this.databaseCache.get(dbId);
|
|
636
|
+
if (opened) {
|
|
637
|
+
docStore = opened.getStore();
|
|
638
|
+
attachmentStore = opened.getAttachmentStore();
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
try {
|
|
642
|
+
const created = this.storeFactory.createStore(dbId);
|
|
643
|
+
docStore = created.docStore;
|
|
644
|
+
attachmentStore = created.attachmentStore;
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
this.logger.warn(`wipeLocalTenant: could not open stores for db "${dbId}": ${error}`);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
for (const store of [docStore, attachmentStore]) {
|
|
652
|
+
if (store && typeof store.clearAllLocalData === "function") {
|
|
653
|
+
try {
|
|
654
|
+
await store.clearAllLocalData();
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
this.logger.warn(`wipeLocalTenant: clearAllLocalData failed for db "${dbId}": ${error}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
420
662
|
async assertCurrentUserCanOpenDB(id) {
|
|
421
663
|
if (id === "directory") {
|
|
422
664
|
return;
|
|
@@ -429,11 +671,35 @@ class BaseMindooTenant {
|
|
|
429
671
|
const registeredUser = await directory.getUserPublicKeys(currentUser.username);
|
|
430
672
|
const hasMatchingGrant = registeredUser?.signingPublicKey === currentUser.userSigningPublicKey &&
|
|
431
673
|
registeredUser?.encryptionPublicKey === currentUser.userEncryptionPublicKey;
|
|
432
|
-
if (hasMatchingGrant) {
|
|
433
|
-
|
|
674
|
+
if (!hasMatchingGrant) {
|
|
675
|
+
this.logger.warn(`Denied database open for ungranted user ${currentUser.username} on database ${id}`);
|
|
676
|
+
throw new Error(`User "${currentUser.username}" does not have tenant access yet; the tenant admin must grant access first.`);
|
|
677
|
+
}
|
|
678
|
+
// Directory-restricted database policy: when the tenant's default policy
|
|
679
|
+
// restricts which databases may be opened, a granted (non-admin) user may
|
|
680
|
+
// only open a listed database. "directory" is already short-circuited above
|
|
681
|
+
// and the admin returned earlier, so this never blocks the policy read
|
|
682
|
+
// itself (which opens "directory").
|
|
683
|
+
if (typeof directory.isDatabaseAllowed === "function") {
|
|
684
|
+
const allowed = await directory.isDatabaseAllowed(id);
|
|
685
|
+
if (!allowed) {
|
|
686
|
+
this.logger.warn(`Denied database open for ${currentUser.username}: database "${id}" is not in the tenant's allowed database list`);
|
|
687
|
+
throw new Error(`Database "${id}" is not in the tenant's allowed database list.`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// Database read gate (doc_read, §6.6): when the tenant policy denies read
|
|
691
|
+
// access to this database for the current user (and no doc_read allow rule
|
|
692
|
+
// grants it), refuse to open it. Because read is the coarse gate in front
|
|
693
|
+
// of every sync operation, this also prevents creating data in a database
|
|
694
|
+
// the user cannot read. "directory" and the admin are exempt (handled in
|
|
695
|
+
// canReadDatabase).
|
|
696
|
+
if (typeof directory.canReadDatabase === "function") {
|
|
697
|
+
const canRead = await directory.canReadDatabase(id);
|
|
698
|
+
if (!canRead) {
|
|
699
|
+
this.logger.warn(`Denied database open for ${currentUser.username}: no read access to database "${id}"`);
|
|
700
|
+
throw new Error(`User "${currentUser.username}" does not have read access to database "${id}".`);
|
|
701
|
+
}
|
|
434
702
|
}
|
|
435
|
-
this.logger.warn(`Denied database open for ungranted user ${currentUser.username} on database ${id}`);
|
|
436
|
-
throw new Error(`User "${currentUser.username}" does not have tenant access yet; the tenant admin must grant access first.`);
|
|
437
703
|
}
|
|
438
704
|
async openDB(id, options) {
|
|
439
705
|
const validDbId = (0, databaseIdValidation_1.validateDatabaseId)(id, "dbId");
|
|
@@ -626,7 +892,12 @@ class BaseMindooTenant {
|
|
|
626
892
|
userSigningPublicKey: request.signingPublicKey,
|
|
627
893
|
userEncryptionPublicKey: request.encryptionPublicKey,
|
|
628
894
|
};
|
|
629
|
-
|
|
895
|
+
// Device label for this key pair (§6.5): an explicit admin-provided label
|
|
896
|
+
// overrides the one suggested by the joining user in the request.
|
|
897
|
+
const deviceLabel = typeof options.label === "string" && options.label.trim().length > 0
|
|
898
|
+
? options.label.trim()
|
|
899
|
+
: request.label;
|
|
900
|
+
await directory.registerUser(publicUserId, options.adminSigningKey, options.adminPassword, undefined, deviceLabel);
|
|
630
901
|
// 2. Export selected document keys encrypted with the share password.
|
|
631
902
|
// `$publicinfos` is mandatory because the joining user needs it for
|
|
632
903
|
// directory access; all selected key ids include every rotated version.
|
|
@@ -862,6 +1133,35 @@ class BaseMindooTenant {
|
|
|
862
1133
|
this.decryptedUserEncryptionKeyCache = cryptoKey;
|
|
863
1134
|
return cryptoKey;
|
|
864
1135
|
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Inject an already-unlocked RSA-OAEP encryption private key for the current
|
|
1138
|
+
* user, priming the same cache {@link getDecryptedEncryptionKey} uses. Hosts
|
|
1139
|
+
* that create the tenant password-less (e.g. Haven, which holds the unlocked
|
|
1140
|
+
* key in its session rather than a password) call this so SDK-driven KeyBag
|
|
1141
|
+
* reconcile can unwrap pushed key versions (docs/accesscontrol.md §13).
|
|
1142
|
+
*/
|
|
1143
|
+
setSessionEncryptionKey(cryptoKey) {
|
|
1144
|
+
this.decryptedUserEncryptionKeyCache = cryptoKey;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* The RSA-OAEP encryption private key used by KeyBag reconcile to unwrap
|
|
1148
|
+
* pushed key versions. Returns the injected/cached session key when present,
|
|
1149
|
+
* else derives it from the current user password. Returns `null` when neither
|
|
1150
|
+
* is available (a locked, password-less host) so reconcile can skip its import
|
|
1151
|
+
* pass without throwing — the revoke pass needs no key and still runs.
|
|
1152
|
+
*/
|
|
1153
|
+
async getEncryptionPrivateKeyForReconcile() {
|
|
1154
|
+
if (this.decryptedUserEncryptionKeyCache) {
|
|
1155
|
+
return this.decryptedUserEncryptionKeyCache;
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
return await this.getDecryptedEncryptionKey();
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
this.logger.debug(`getEncryptionPrivateKeyForReconcile: no encryption key available (locked host): ${error}`);
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
865
1165
|
/**
|
|
866
1166
|
* Internal method to decrypt a private key using password-based key derivation.
|
|
867
1167
|
* Protected so that BaseMindooTenantDirectory can access it.
|
|
@@ -873,9 +1173,9 @@ class BaseMindooTenant {
|
|
|
873
1173
|
* @returns The decrypted private key as ArrayBuffer
|
|
874
1174
|
*/
|
|
875
1175
|
async decryptPrivateKey(encryptedKey, password, saltString) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
this.logger.debug(`decryptPrivateKey:
|
|
1176
|
+
// Do NOT log the password length or KDF iteration count (audit, Medium:
|
|
1177
|
+
// sensitive debug logging) — both narrow an offline cracking search space.
|
|
1178
|
+
this.logger.debug(`decryptPrivateKey: Starting decryption (keyCategory: ${saltString})`);
|
|
879
1179
|
this.logger.debug(`decryptPrivateKey: Delegating PBKDF2 + AES-GCM work to shared helper`);
|
|
880
1180
|
try {
|
|
881
1181
|
const decrypted = await (0, privateKeyEncryption_1.decryptPrivateKey)(this.cryptoAdapter, encryptedKey, password, saltString);
|
|
@@ -967,6 +1267,238 @@ class BaseMindooTenant {
|
|
|
967
1267
|
async getDocKeyFingerprint() {
|
|
968
1268
|
return this.keyBag.getDocKeyFingerprint(this.tenantId);
|
|
969
1269
|
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Remove a **named** document key from the local KeyBag. Refuses to delete the
|
|
1272
|
+
* shared tenant default key (`"default"` / {@link DEFAULT_TENANT_KEY_ID}),
|
|
1273
|
+
* which is required for the tenant to function. Returns true if a key was
|
|
1274
|
+
* removed.
|
|
1275
|
+
*/
|
|
1276
|
+
async removeNamedDecryptionKey(decryptionKeyId) {
|
|
1277
|
+
if (decryptionKeyId === "default" || decryptionKeyId === types_1.DEFAULT_TENANT_KEY_ID) {
|
|
1278
|
+
this.logger.debug(`removeNamedDecryptionKey: refusing to delete the tenant default key`);
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
try {
|
|
1282
|
+
await this.keyBag.deleteKey("doc", this.tenantId, decryptionKeyId);
|
|
1283
|
+
this.invalidateDecryptedKeyCaches();
|
|
1284
|
+
return true;
|
|
1285
|
+
}
|
|
1286
|
+
catch (error) {
|
|
1287
|
+
this.logger.warn(`removeNamedDecryptionKey: failed to delete key ${decryptionKeyId}: ${error}`);
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Reconcile the local KeyBag against the directory head for the current user
|
|
1293
|
+
* (docs/accesscontrol.md §13). SDK-driven so standalone apps using the
|
|
1294
|
+
* `mindoodb` package get key distribution/revocation automatically after a
|
|
1295
|
+
* directory sync — not just the Haven UI. Two independent passes:
|
|
1296
|
+
*
|
|
1297
|
+
* - **Revoke (no key required):** bulk-remove every revoked key id (from the
|
|
1298
|
+
* directory head cache) from the bag. Idempotent and a no-op when the bag
|
|
1299
|
+
* does not hold the key, so it also re-cleans a restored older KeyBag
|
|
1300
|
+
* backup on every run. The `deleteKey` mutation drives visibility
|
|
1301
|
+
* reconciliation (forgetting now-inaccessible docs) and emits
|
|
1302
|
+
* `keyBag.onChanges` so the host can persist the cleaned bag.
|
|
1303
|
+
* - **Import (needs the encryption private key):** unwrap and merge the key
|
|
1304
|
+
* versions pushed to this user. Skipped with a debug log when the host is
|
|
1305
|
+
* locked (no session key / password); the revoke pass still ran.
|
|
1306
|
+
*
|
|
1307
|
+
* Best-effort and idempotent; safe to call on every directory bring-up / pull.
|
|
1308
|
+
* Returns the key ids imported and removed.
|
|
1309
|
+
*/
|
|
1310
|
+
async reconcileKeyDistributionsForCurrentUser() {
|
|
1311
|
+
const username = this.currentUser.username;
|
|
1312
|
+
const directory = await this.openDirectory();
|
|
1313
|
+
const removed = [];
|
|
1314
|
+
const imported = [];
|
|
1315
|
+
// Revoke pass — no key, no comparison, idempotent.
|
|
1316
|
+
if (typeof directory.getRevokedDecryptionKeyIdsForUser === "function") {
|
|
1317
|
+
let revokedIds = [];
|
|
1318
|
+
try {
|
|
1319
|
+
revokedIds = await directory.getRevokedDecryptionKeyIdsForUser(username);
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
this.logger.warn(`reconcileKeyDistributionsForCurrentUser: revoked-id lookup failed: ${error}`);
|
|
1323
|
+
}
|
|
1324
|
+
for (const keyId of revokedIds) {
|
|
1325
|
+
if (await this.removeNamedDecryptionKey(keyId)) {
|
|
1326
|
+
removed.push(keyId);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
// Import pass — sources the encryption private key from this tenant. The
|
|
1331
|
+
// directory exposes this only via the internal KeyBagReconciler contract,
|
|
1332
|
+
// so the key is never passed across the public directory API.
|
|
1333
|
+
const reconciler = directory;
|
|
1334
|
+
if (typeof reconciler.reconcileImportedKeysForCurrentUser === "function") {
|
|
1335
|
+
try {
|
|
1336
|
+
const result = await reconciler.reconcileImportedKeysForCurrentUser(username);
|
|
1337
|
+
imported.push(...result.imported);
|
|
1338
|
+
for (const id of result.removed) {
|
|
1339
|
+
if (!removed.includes(id))
|
|
1340
|
+
removed.push(id);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
catch (error) {
|
|
1344
|
+
this.logger.warn(`reconcileKeyDistributionsForCurrentUser: import pass failed: ${error}`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return { imported, removed };
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Single-flight, best-effort wrapper around
|
|
1351
|
+
* {@link reconcileKeyDistributionsForCurrentUser} for the run-always trigger
|
|
1352
|
+
* (docs/accesscontrol.md §13). Fired on directory bring-up and after every
|
|
1353
|
+
* directory pull. Never throws and never blocks sync: a reconcile error is
|
|
1354
|
+
* logged and swallowed. The in-flight guard prevents the driver's own
|
|
1355
|
+
* directory access from re-triggering it.
|
|
1356
|
+
*/
|
|
1357
|
+
async reconcileKeyDistributionsForCurrentUserSafe() {
|
|
1358
|
+
if (this.reconcileInFlight) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
this.reconcileInFlight = true;
|
|
1362
|
+
try {
|
|
1363
|
+
await this.reconcileKeyDistributionsForCurrentUser();
|
|
1364
|
+
}
|
|
1365
|
+
catch (error) {
|
|
1366
|
+
this.logger.warn(`reconcileKeyDistributionsForCurrentUserSafe: ${error}`);
|
|
1367
|
+
}
|
|
1368
|
+
finally {
|
|
1369
|
+
this.reconcileInFlight = false;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Export **all** stored versions of a document key for an admin-blind key
|
|
1374
|
+
* delivery (read-side key push). A rotated key keeps multiple versions in the
|
|
1375
|
+
* KeyBag and decryption tries them all, so a delivery must carry every version
|
|
1376
|
+
* - otherwise the recipient could not read documents encrypted under an
|
|
1377
|
+
* earlier version. Each entry pairs the raw key bytes with the version's
|
|
1378
|
+
* `createdAt` (used as `keyVersionCreatedAt`); versions are newest-first.
|
|
1379
|
+
* Returns null when the caller's KeyBag lacks the key. Only a key-holder can
|
|
1380
|
+
* run this — never the admin who later publishes the wrapped bytes.
|
|
1381
|
+
*/
|
|
1382
|
+
async exportDecryptionKeyForDelivery(decryptionKeyId) {
|
|
1383
|
+
const id = decryptionKeyId === "default" ? types_1.DEFAULT_TENANT_KEY_ID : decryptionKeyId;
|
|
1384
|
+
const versions = await this.keyBag.getAllKeyVersions("doc", this.tenantId, id);
|
|
1385
|
+
if (versions.length === 0) {
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
return versions.map((v) => ({ bytes: v.key, keyVersionCreatedAt: v.createdAt ?? 0 }));
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Import a delivered document key into the local KeyBag (read-side key push,
|
|
1392
|
+
* recipient side). Writing the key triggers the existing KeyBag change
|
|
1393
|
+
* listener -> {@link reconcileKeyBagChanges} -> per-database reveal-on-add, so
|
|
1394
|
+
* documents encrypted with it surface automatically. Idempotent: a key
|
|
1395
|
+
* version with the same `keyVersionCreatedAt` is not duplicated.
|
|
1396
|
+
*/
|
|
1397
|
+
async importDeliveredDecryptionKey(keyId, bytes, keyVersionCreatedAt) {
|
|
1398
|
+
await this.importDeliveredDecryptionKeyVersions(keyId, [{ bytes, keyVersionCreatedAt }]);
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Import several delivered versions of one document key at once (read-side key
|
|
1402
|
+
* push, recipient side). Each version absent from the KeyBag (matched by
|
|
1403
|
+
* `keyVersionCreatedAt`) is added; already-present versions are skipped so the
|
|
1404
|
+
* call is idempotent. Caches are invalidated and reveal-on-add reconciliation
|
|
1405
|
+
* runs once, after all new versions are written. Returns the number of
|
|
1406
|
+
* versions actually imported.
|
|
1407
|
+
*/
|
|
1408
|
+
async importDeliveredDecryptionKeyVersions(keyId, versions) {
|
|
1409
|
+
const id = keyId === "default" ? types_1.DEFAULT_TENANT_KEY_ID : keyId;
|
|
1410
|
+
const scopedKeyId = `doc:${this.tenantId}:${id}`;
|
|
1411
|
+
const existing = await this.keyBag.listKeyDetails();
|
|
1412
|
+
const haveStamps = new Set(existing.filter((d) => d.scopedKeyId === scopedKeyId).map((d) => d.createdAt ?? 0));
|
|
1413
|
+
let importedCount = 0;
|
|
1414
|
+
for (const version of versions) {
|
|
1415
|
+
if (haveStamps.has(version.keyVersionCreatedAt)) {
|
|
1416
|
+
this.logger.debug(`importDeliveredDecryptionKeyVersions: ${keyId}@${version.keyVersionCreatedAt} already present`);
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
await this.keyBag.set("doc", this.tenantId, id, version.bytes, version.keyVersionCreatedAt || undefined);
|
|
1420
|
+
haveStamps.add(version.keyVersionCreatedAt);
|
|
1421
|
+
importedCount++;
|
|
1422
|
+
}
|
|
1423
|
+
if (importedCount > 0) {
|
|
1424
|
+
this.invalidateDecryptedKeyCaches();
|
|
1425
|
+
await this.reconcileKeyBagChanges();
|
|
1426
|
+
}
|
|
1427
|
+
return importedCount;
|
|
1428
|
+
}
|
|
1429
|
+
/** SHA-256 hex of raw key bytes — the stable version fingerprint used by key distribution. */
|
|
1430
|
+
async fingerprintKeyBytes(bytes) {
|
|
1431
|
+
const subtle = this.getCryptoAdapter().getSubtle();
|
|
1432
|
+
const digest = await subtle.digest("SHA-256", bytes);
|
|
1433
|
+
return Array.from(new Uint8Array(digest))
|
|
1434
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
1435
|
+
.join("");
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* All stored versions of a document key with their raw bytes, `createdAt`, and
|
|
1439
|
+
* SHA-256 fingerprint (newest first). The source of truth for a key
|
|
1440
|
+
* distribution's `keyVersions` manifest and per-device wrapping. Empty when the
|
|
1441
|
+
* caller's KeyBag lacks the key.
|
|
1442
|
+
*/
|
|
1443
|
+
async fingerprintKeyVersions(keyId) {
|
|
1444
|
+
const id = keyId === "default" ? types_1.DEFAULT_TENANT_KEY_ID : keyId;
|
|
1445
|
+
const versions = await this.keyBag.getAllKeyVersions("doc", this.tenantId, id);
|
|
1446
|
+
const result = [];
|
|
1447
|
+
for (const v of versions) {
|
|
1448
|
+
result.push({
|
|
1449
|
+
bytes: v.key,
|
|
1450
|
+
createdAt: v.createdAt ?? 0,
|
|
1451
|
+
fingerprint: await this.fingerprintKeyBytes(v.key),
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
return result;
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Remove exactly the key versions of `keyId` whose raw-bytes fingerprint is in
|
|
1458
|
+
* `fingerprints` (version-scoped `pullfrom` revocation). Versions obtained
|
|
1459
|
+
* elsewhere — i.e. not in the distribution manifest — survive. Refuses the
|
|
1460
|
+
* protected tenant default / `$publicinfos` keys. The KeyBag change feed
|
|
1461
|
+
* triggers visibility reconciliation (scope purge when nothing remains).
|
|
1462
|
+
* Returns the number of versions removed.
|
|
1463
|
+
*/
|
|
1464
|
+
async removeDecryptionKeyVersionsByFingerprint(keyId, fingerprints) {
|
|
1465
|
+
if (keyId === "default" || keyId === types_1.DEFAULT_TENANT_KEY_ID || keyId === types_1.PUBLIC_INFOS_KEY_ID) {
|
|
1466
|
+
this.logger.debug(`removeDecryptionKeyVersionsByFingerprint: refusing protected key ${keyId}`);
|
|
1467
|
+
return 0;
|
|
1468
|
+
}
|
|
1469
|
+
const target = new Set(fingerprints);
|
|
1470
|
+
if (target.size === 0)
|
|
1471
|
+
return 0;
|
|
1472
|
+
const id = keyId === "default" ? types_1.DEFAULT_TENANT_KEY_ID : keyId;
|
|
1473
|
+
let removed = 0;
|
|
1474
|
+
// Re-read versions each pass: deleteKeyVersion is index-based against the
|
|
1475
|
+
// current sorted list, which shifts as entries are removed.
|
|
1476
|
+
// eslint-disable-next-line no-constant-condition
|
|
1477
|
+
while (true) {
|
|
1478
|
+
const versions = await this.keyBag.getAllKeyVersions("doc", this.tenantId, id);
|
|
1479
|
+
let removedThisPass = false;
|
|
1480
|
+
for (let index = 0; index < versions.length; index++) {
|
|
1481
|
+
const fp = await this.fingerprintKeyBytes(versions[index].key);
|
|
1482
|
+
if (target.has(fp)) {
|
|
1483
|
+
await this.keyBag.deleteKeyVersion("doc", this.tenantId, id, index);
|
|
1484
|
+
removed++;
|
|
1485
|
+
removedThisPass = true;
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (!removedThisPass)
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
if (removed > 0) {
|
|
1493
|
+
this.invalidateDecryptedKeyCaches();
|
|
1494
|
+
await this.reconcileKeyBagChanges();
|
|
1495
|
+
}
|
|
1496
|
+
return removed;
|
|
1497
|
+
}
|
|
1498
|
+
/** Clear in-memory decrypted-key caches after a KeyBag mutation. */
|
|
1499
|
+
invalidateDecryptedKeyCaches() {
|
|
1500
|
+
this.decryptedTenantKeyCache = undefined;
|
|
1501
|
+
}
|
|
970
1502
|
/**
|
|
971
1503
|
* Get the symmetric key for a given key ID.
|
|
972
1504
|
* This is a protected helper method that can be used by subclasses.
|
|
@@ -1094,23 +1626,36 @@ class BaseMindooTenant {
|
|
|
1094
1626
|
const iv = encryptedPayload.slice(1, 13);
|
|
1095
1627
|
// Extract ciphertext (bytes 13 onwards)
|
|
1096
1628
|
const ciphertext = encryptedPayload.slice(13);
|
|
1097
|
-
|
|
1629
|
+
// Try every stored version of the key (newest first) so attachments
|
|
1630
|
+
// encrypted under an earlier rotation of the key still decrypt.
|
|
1631
|
+
const candidates = await this.getDecryptionKeyCandidates(decryptionKeyId);
|
|
1632
|
+
if (candidates.length === 0) {
|
|
1633
|
+
throw new errors_1.SymmetricKeyNotFoundError(decryptionKeyId === "default" ? `doc:${this.tenantId}:${types_1.DEFAULT_TENANT_KEY_ID}` : decryptionKeyId);
|
|
1634
|
+
}
|
|
1098
1635
|
const subtle = this.cryptoAdapter.getSubtle();
|
|
1099
|
-
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1636
|
+
let lastError = undefined;
|
|
1637
|
+
for (const symmetricKey of candidates) {
|
|
1638
|
+
try {
|
|
1639
|
+
const cryptoKey = await subtle.importKey("raw", new Uint8Array(symmetricKey).buffer, { name: "AES-GCM" }, false, ["decrypt"]);
|
|
1640
|
+
const decrypted = await subtle.decrypt({
|
|
1641
|
+
name: "AES-GCM",
|
|
1642
|
+
iv: iv,
|
|
1643
|
+
tagLength: 128,
|
|
1644
|
+
}, cryptoKey, ciphertext);
|
|
1645
|
+
const result = new Uint8Array(decrypted);
|
|
1646
|
+
this.logger.debug(`Decrypted attachment (${encryptedPayload.length} -> ${result.length} bytes)`);
|
|
1647
|
+
return result;
|
|
1648
|
+
}
|
|
1649
|
+
catch (error) {
|
|
1650
|
+
lastError = error;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
this.logger.debug(`Attachment decrypt failed against all ${candidates.length} version(s) of key "${decryptionKeyId}"`);
|
|
1654
|
+
throw lastError ?? new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
|
|
1111
1655
|
}
|
|
1112
1656
|
}
|
|
1113
1657
|
exports.BaseMindooTenant = BaseMindooTenant;
|
|
1658
|
+
BaseMindooTenant.MAX_IMPORTED_KEY_CACHE_ENTRIES = 128;
|
|
1114
1659
|
// ============================================================================
|
|
1115
1660
|
// Attachment Encryption Methods
|
|
1116
1661
|
// ============================================================================
|