@storacha/encrypt-upload-client 1.1.56 → 1.1.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/config/constants.d.ts +3 -3
  2. package/dist/config/constants.js +4 -3
  3. package/dist/config/env.d.ts +9 -6
  4. package/dist/config/service.d.ts +13 -13
  5. package/dist/core/client.d.ts +54 -41
  6. package/dist/core/client.js +68 -56
  7. package/dist/core/errors.d.ts +6 -6
  8. package/dist/core/metadata/encrypted-metadata.d.ts +13 -8
  9. package/dist/core/metadata/kms-metadata.d.ts +68 -36
  10. package/dist/core/metadata/lit-metadata.d.ts +63 -28
  11. package/dist/crypto/adapters/kms-crypto-adapter.d.ts +172 -137
  12. package/dist/crypto/adapters/lit-crypto-adapter.d.ts +107 -86
  13. package/dist/crypto/factories.browser.d.ts +9 -5
  14. package/dist/crypto/factories.browser.js +15 -7
  15. package/dist/crypto/factories.node.d.ts +13 -6
  16. package/dist/crypto/factories.node.js +19 -13
  17. package/dist/crypto/index.d.ts +5 -5
  18. package/dist/crypto/index.js +5 -5
  19. package/dist/crypto/symmetric/generic-aes-ctr-streaming-crypto.d.ts +58 -54
  20. package/dist/crypto/symmetric/generic-aes-ctr-streaming-crypto.js +174 -146
  21. package/dist/crypto/symmetric/node-aes-cbc-crypto.d.ts +36 -32
  22. package/dist/crypto/symmetric/node-aes-cbc-crypto.js +101 -95
  23. package/dist/examples/decrypt-test.d.ts +2 -2
  24. package/dist/examples/decrypt-test.js +78 -69
  25. package/dist/examples/encrypt-test.d.ts +5 -3
  26. package/dist/examples/encrypt-test.js +58 -55
  27. package/dist/handlers/decrypt-handler.d.ts +19 -5
  28. package/dist/handlers/encrypt-handler.d.ts +9 -3
  29. package/dist/handlers/encrypt-handler.js +93 -57
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +2 -2
  32. package/dist/protocols/lit.d.ts +33 -9
  33. package/dist/protocols/lit.js +134 -98
  34. package/dist/test/cid-verification.spec.d.ts +2 -2
  35. package/dist/test/cid-verification.spec.js +341 -313
  36. package/dist/test/crypto-compatibility.spec.d.ts +2 -2
  37. package/dist/test/crypto-compatibility.spec.js +184 -120
  38. package/dist/test/crypto-counter-security.spec.d.ts +2 -2
  39. package/dist/test/crypto-counter-security.spec.js +177 -138
  40. package/dist/test/crypto-streaming.spec.d.ts +2 -2
  41. package/dist/test/crypto-streaming.spec.js +208 -126
  42. package/dist/test/encrypted-metadata.spec.d.ts +2 -2
  43. package/dist/test/encrypted-metadata.spec.js +89 -62
  44. package/dist/test/factories.spec.d.ts +2 -2
  45. package/dist/test/factories.spec.js +275 -139
  46. package/dist/test/file-metadata.spec.d.ts +2 -2
  47. package/dist/test/file-metadata.spec.js +472 -416
  48. package/dist/test/fixtures/test-fixtures.d.ts +25 -20
  49. package/dist/test/fixtures/test-fixtures.js +61 -53
  50. package/dist/test/helpers/test-file-utils.d.ts +19 -14
  51. package/dist/test/helpers/test-file-utils.js +78 -76
  52. package/dist/test/https-enforcement.spec.d.ts +2 -2
  53. package/dist/test/https-enforcement.spec.js +278 -124
  54. package/dist/test/kms-crypto-adapter.spec.d.ts +2 -2
  55. package/dist/test/kms-crypto-adapter.spec.js +473 -304
  56. package/dist/test/lit-crypto-adapter.spec.d.ts +2 -2
  57. package/dist/test/lit-crypto-adapter.spec.js +206 -118
  58. package/dist/test/memory-efficiency.spec.d.ts +2 -2
  59. package/dist/test/memory-efficiency.spec.js +100 -87
  60. package/dist/test/mocks/key-manager.d.ts +71 -38
  61. package/dist/test/mocks/key-manager.js +129 -113
  62. package/dist/test/node-crypto-adapter.spec.d.ts +2 -2
  63. package/dist/test/node-crypto-adapter.spec.js +155 -102
  64. package/dist/test/node-generic-crypto-adapter.spec.d.ts +2 -2
  65. package/dist/test/node-generic-crypto-adapter.spec.js +134 -94
  66. package/dist/test/setup.d.ts +2 -2
  67. package/dist/test/setup.js +8 -9
  68. package/dist/tsconfig.spec.tsbuildinfo +1 -1
  69. package/dist/types.d.ts +219 -181
  70. package/dist/utils/file-metadata.d.ts +19 -13
  71. package/dist/utils.d.ts +14 -5
  72. package/package.json +4 -4
@@ -1,305 +1,474 @@
1
- import './setup.js';
2
- import { test, describe } from 'node:test';
3
- import assert from 'node:assert';
4
- import * as Server from '@ucanto/server';
5
- import { base64 } from 'multiformats/bases/base64';
6
- import * as Space from '@storacha/capabilities/space';
7
- import { GenericAesCtrStreamingCrypto } from '../src/crypto/symmetric/generic-aes-ctr-streaming-crypto.js';
8
- import { KMSCryptoAdapter } from '../src/crypto/adapters/kms-crypto-adapter.js';
9
- import { createMockKeyManagerService, createMockKeyManagerServer, } from './mocks/key-manager.js';
10
- import { createTestFixtures } from './fixtures/test-fixtures.js';
11
- import { stringToUint8Array, streamToUint8Array, uint8ArrayToString, } from './helpers/test-file-utils.js';
1
+ import './setup.js'
2
+ import { test, describe } from 'node:test'
3
+ import assert from 'node:assert'
4
+ import * as Server from '@ucanto/server'
5
+ import { base64 } from 'multiformats/bases/base64'
6
+ import * as Space from '@storacha/capabilities/space'
7
+ import { GenericAesCtrStreamingCrypto } from '../src/crypto/symmetric/generic-aes-ctr-streaming-crypto.js'
8
+ import { KMSCryptoAdapter } from '../src/crypto/adapters/kms-crypto-adapter.js'
9
+ import {
10
+ createMockKeyManagerService,
11
+ createMockKeyManagerServer,
12
+ } from './mocks/key-manager.js'
13
+ import { createTestFixtures } from './fixtures/test-fixtures.js'
14
+ import {
15
+ stringToUint8Array,
16
+ streamToUint8Array,
17
+ uint8ArrayToString,
18
+ } from './helpers/test-file-utils.js'
12
19
  await describe('KMSCryptoAdapter', async () => {
13
- await describe('Unit Tests', async () => {
14
- await test('should delegate symmetric crypto operations to the injected implementation', async () => {
15
- const symmetricCrypto = new GenericAesCtrStreamingCrypto();
16
- const adapter = new KMSCryptoAdapter(symmetricCrypto, 'https://private.storacha.link', 'did:web:private.storacha.link');
17
- const originalText = 'Op, this is a test for KMS strategy-based encryption!';
18
- const blob = new Blob([stringToUint8Array(originalText)]);
19
- // Test that it delegates to the symmetric crypto implementation
20
- const { key, iv, encryptedStream } = await adapter.encryptStream(blob);
21
- assert(key instanceof Uint8Array, 'Key should be a Uint8Array');
22
- assert(iv instanceof Uint8Array, 'IV should be a Uint8Array');
23
- assert(encryptedStream instanceof ReadableStream, 'Encrypted stream should be a ReadableStream');
24
- // Test decryption delegation
25
- const decryptedStream = await adapter.decryptStream(encryptedStream, key, iv);
26
- const decryptedBytes = await streamToUint8Array(decryptedStream);
27
- const decryptedText = uint8ArrayToString(decryptedBytes);
28
- assert.strictEqual(decryptedText, originalText, 'Decrypted text should match original');
29
- });
30
- await test('should initialize KMS adapter with correct configuration', async () => {
31
- const symmetricCrypto = new GenericAesCtrStreamingCrypto();
32
- const adapter = new KMSCryptoAdapter(symmetricCrypto, 'https://private.storacha.link', 'did:web:private.storacha.link');
33
- // Test that the adapter can handle encryption options directly
34
- assert(typeof adapter.encryptSymmetricKey === 'function', 'encryptSymmetricKey should be a function');
35
- // Verify adapter constructor sets properties correctly
36
- assert(typeof adapter.keyManagerServiceDID === 'object', 'Adapter should have gateway DID object');
37
- assert.strictEqual(adapter.keyManagerServiceDID.did(), 'did:web:private.storacha.link', 'Adapter should have correct gateway DID');
38
- assert(adapter.keyManagerServiceURL instanceof URL, 'Adapter should have gateway URL');
39
- });
40
- await test('should handle metadata extraction with invalid CAR data', async () => {
41
- const symmetricCrypto = new GenericAesCtrStreamingCrypto();
42
- const adapter = new KMSCryptoAdapter(symmetricCrypto, 'https://private.storacha.link', 'did:web:private.storacha.link');
43
- // Test that the method exists
44
- assert(typeof adapter.extractEncryptedMetadata === 'function', 'extractEncryptedMetadata should be a function');
45
- assert(typeof adapter.getEncryptedKey === 'function', 'getEncryptedKey should be a function');
46
- // Should throw error for invalid CAR data
47
- const mockCar = new Uint8Array([1, 2, 3]);
48
- assert.throws(() => {
49
- adapter.extractEncryptedMetadata(mockCar);
50
- }, /Invalid CAR header format/, 'Should throw error for invalid CAR data');
51
- });
52
- await test('should sanitize space DID for KMS key ID', async () => {
53
- const symmetricCrypto = new GenericAesCtrStreamingCrypto();
54
- const adapter = new KMSCryptoAdapter(symmetricCrypto, 'https://mock-gateway.example.com', 'did:web:mock');
55
- const spaceDID = 'did:key:z6MkwDK3M4PxU1FqcSt6quBH1xRBSGnPRdQYP9B13h3Wq5X1';
56
- const sanitized = adapter.sanitizeSpaceDIDForKMSKeyId(spaceDID);
57
- assert.strictEqual(sanitized, 'z6MkwDK3M4PxU1FqcSt6quBH1xRBSGnPRdQYP9B13h3Wq5X1');
58
- assert(!sanitized.includes('did:key:'));
59
- });
60
- });
61
- await describe('Integration Tests', async () => {
62
- await test('should complete full encryption workflow with mocked private gateway', async () => {
63
- const fixtures = await createTestFixtures();
64
- const { keyManagerServiceDID, spaceDID, issuer, publicKeyPem, keyPair, delegationProof, } = fixtures;
65
- let setupCalled = false;
66
- let decryptCalled = false;
67
- let actualEncryptedKey = '';
68
- // Create mock gateway service that performs real RSA encryption/decryption
69
- const service = createMockKeyManagerService({
70
- mockPublicKey: publicKeyPem,
71
- onEncryptionSetup: (/** @type {any} */ input) => {
72
- setupCalled = true;
73
- assert.strictEqual(input.capability.with, spaceDID);
74
- assert.strictEqual(input.capability.can, 'space/encryption/setup');
75
- },
76
- onKeyDecrypt: (/** @type {any} */ input) => {
77
- decryptCalled = true;
78
- assert.strictEqual(input.capability.with, spaceDID);
79
- assert.strictEqual(input.capability.can, 'space/encryption/key/decrypt');
80
- },
81
- });
82
- // Override the decrypt handler to actually decrypt with the private key
83
- service.space.encryption.key.decrypt = Server.provide(Space.EncryptionKeyDecrypt, async (input) => {
84
- decryptCalled = true;
85
- assert.strictEqual(input.capability.with, spaceDID);
86
- assert.strictEqual(input.capability.can, 'space/encryption/key/decrypt');
87
- // Get the encrypted key from the request
88
- const encryptedKeyBytes = input.capability.nb.key;
89
- const encryptedSymmetricKey = base64.encode(encryptedKeyBytes);
90
- actualEncryptedKey = encryptedSymmetricKey;
91
- // Decrypt with RSA private key (simulate real KMS decryption)
92
- const encryptedBytes = encryptedKeyBytes;
93
- const decryptedBytes = await globalThis.crypto.subtle.decrypt({ name: 'RSA-OAEP' }, keyPair.privateKey, encryptedBytes);
94
- // Return the decrypted symmetric key as base64
95
- const decryptedKey = base64.encode(new Uint8Array(decryptedBytes));
96
- return Server.ok({
97
- decryptedSymmetricKey: decryptedKey,
98
- });
99
- });
100
- // Create mock gateway server (HTTPS by default)
101
- const keyManagerServiceServer = await createMockKeyManagerServer(service, keyManagerServiceDID, 5555);
102
- // Create KMS adapter with HTTP allowed for testing
103
- const symmetricCrypto = new GenericAesCtrStreamingCrypto();
104
- const adapter = new KMSCryptoAdapter(symmetricCrypto, keyManagerServiceServer.url, keyManagerServiceDID.did(), { allowInsecureHttp: true } // Allow HTTP for testing
105
- );
106
- try {
107
- // Create test file and encrypt it to get real symmetric keys
108
- const testFile = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
109
- const testBlob = new Blob([testFile], {
110
- type: 'application/octet-stream',
111
- });
112
- // Encrypt the file to get real symmetric keys
113
- const { key, iv } = await adapter.encryptStream(testBlob);
114
- const encryptionConfig = {
115
- issuer,
116
- spaceDID,
117
- location: 'us-central1',
118
- keyring: 'test-keyring',
119
- };
120
- // Test key encryption with real symmetric keys - this will call the mock setup
121
- const encryptResult = await adapter.encryptSymmetricKey(key, iv, encryptionConfig);
122
- assert(setupCalled, 'EncryptionSetup should have been called');
123
- assert.strictEqual(encryptResult.strategy, 'kms');
124
- const kmsMetadata =
125
- /** @type {import('../src/types.js').KMSKeyMetadata} */ (encryptResult.metadata);
126
- assert.strictEqual(kmsMetadata.space, spaceDID);
127
- assert.strictEqual(kmsMetadata.kms.provider, 'google-kms');
128
- assert.strictEqual(kmsMetadata.kms.algorithm, 'RSA-OAEP-2048-SHA256');
129
- assert(typeof encryptResult.encryptedKey === 'string');
130
- // Test key decryption - this will call the mock decrypt
131
- const decryptionConfig = {
132
- spaceDID,
133
- decryptDelegation: delegationProof,
134
- proofs: [],
135
- };
136
- const mockMetadata = {
137
- strategy: /** @type {'kms'} */ ('kms'),
138
- encryptedDataCID: 'bafybeid',
139
- encryptedSymmetricKey: encryptResult.encryptedKey,
140
- space: spaceDID,
141
- kms: kmsMetadata.kms,
142
- };
143
- const decryptResult = await adapter.decryptSymmetricKey(encryptResult.encryptedKey, {
144
- decryptionConfig,
145
- metadata: mockMetadata,
146
- resourceCID:
147
- /** @type {import('@storacha/upload-client/types').AnyLink} */ (
148
- /** @type {any} */ ('bafybeid')),
149
- issuer,
150
- audience: keyManagerServiceDID.did(),
151
- });
152
- // Verify the round-trip worked
153
- assert(setupCalled, 'EncryptionSetup should have been called');
154
- assert(decryptCalled, 'KeyDecrypt should have been called');
155
- assert(decryptResult.key instanceof Uint8Array, 'Decrypted key should be Uint8Array');
156
- assert(decryptResult.iv instanceof Uint8Array, 'Decrypted IV should be Uint8Array');
157
- // Most importantly: verify the decrypted keys match the original
158
- assert.deepStrictEqual(decryptResult.key, key, 'Decrypted key should match original key');
159
- assert.deepStrictEqual(decryptResult.iv, iv, 'Decrypted IV should match original IV');
160
- // Verify the encrypted key was actually encrypted (different from original)
161
- const originalCombined = adapter.symmetricCrypto.combineKeyAndIV(key, iv);
162
- const originalBase64 = base64.encode(originalCombined);
163
- assert.notStrictEqual(actualEncryptedKey, originalBase64, 'Encrypted key should be different from original');
164
- }
165
- catch (error) {
166
- console.error('Test failed with error:', error);
167
- throw error;
168
- }
169
- finally {
170
- // Clean up server
171
- await keyManagerServiceServer.close();
172
- }
173
- });
174
- await test('should handle encryption setup errors gracefully', async () => {
175
- const fixtures = await createTestFixtures();
176
- const { keyManagerServiceDID, spaceDID, issuer } = fixtures;
177
- // Create service that returns errors
178
- const service = createMockKeyManagerService({
179
- mockPublicKey: 'invalid',
180
- onEncryptionSetup: () => {
181
- // This will be called but service will return error
182
- },
183
- });
184
- // Override service to return error
185
- service.space.encryption.setup = Server.provide(Space.EncryptionSetup, async () => {
186
- return Server.error({
187
- name: 'SpaceNotProvisioned',
188
- message: 'Space is not provisioned for encryption',
189
- });
190
- });
191
- const keyManagerServiceServer = await createMockKeyManagerServer(service, keyManagerServiceDID, 5556);
192
- const symmetricCrypto = new GenericAesCtrStreamingCrypto();
193
- const adapter = new KMSCryptoAdapter(symmetricCrypto, keyManagerServiceServer.url, keyManagerServiceDID.did(), { allowInsecureHttp: true } // Allow HTTP for testing
194
- );
195
- try {
196
- const testKey = new Uint8Array(32).fill(1);
197
- const testIV = new Uint8Array(16).fill(2);
198
- const encryptionConfig = {
199
- issuer,
200
- spaceDID,
201
- };
202
- // Should throw error
203
- await assert.rejects(() => adapter.encryptSymmetricKey(testKey, testIV, encryptionConfig), /Space is not provisioned for encryption/);
204
- }
205
- finally {
206
- await keyManagerServiceServer.close();
207
- }
208
- });
209
- await test('should handle key decryption errors gracefully', async () => {
210
- const fixtures = await createTestFixtures();
211
- const { keyManagerServiceDID, spaceDID, issuer, delegationProof } = fixtures;
212
- // Create service that returns errors for decrypt
213
- const service = createMockKeyManagerService({
214
- mockPublicKey: 'mock-key',
215
- });
216
- // Override decrypt service to return error
217
- service.space.encryption.key.decrypt = Server.provide(Space.EncryptionKeyDecrypt, async () => {
218
- return Server.error({
219
- name: 'KeyNotFound',
220
- message: 'KMS key not found',
221
- });
222
- });
223
- const keyManagerServiceServer = await createMockKeyManagerServer(service, keyManagerServiceDID, 5557);
224
- const symmetricCrypto = new GenericAesCtrStreamingCrypto();
225
- const adapter = new KMSCryptoAdapter(symmetricCrypto, keyManagerServiceServer.url, keyManagerServiceDID.did(), { allowInsecureHttp: true } // Allow HTTP for testing
226
- );
227
- const decryptionOptions = {
228
- spaceDID,
229
- decryptDelegation: delegationProof,
230
- };
231
- const mockKey = new Uint8Array([1, 2, 3]); // test value as bytes
232
- const mockKeyString = base64.encode(mockKey);
233
- const mockMetadata = {
234
- strategy: /** @type {'kms'} */ ('kms'),
235
- encryptedDataCID: 'bafybeid',
236
- key: mockKey, // use bytes, not string
237
- space: spaceDID,
238
- kms: {
239
- provider: /** @type {'google-kms'} */ ('google-kms'),
240
- keyId: 'test-key',
241
- algorithm: /** @type {'RSA-OAEP-2048-SHA256'} */ ('RSA-OAEP-2048-SHA256'),
242
- },
243
- };
244
- const decryptConfigs = /** @type {any} */ ({
245
- decryptionConfig: decryptionOptions,
246
- metadata: mockMetadata,
247
- delegationCAR: new Uint8Array(),
248
- resourceCID: 'bafybeid',
249
- issuer,
250
- audience: keyManagerServiceDID.did(),
251
- });
252
- try {
253
- // Should throw error
254
- await assert.rejects(() => adapter.decryptSymmetricKey(mockKeyString, decryptConfigs), /KMS key not found/);
255
- }
256
- finally {
257
- await keyManagerServiceServer.close();
258
- }
259
- });
260
- });
261
- await describe('Validation Tests', async () => {
262
- await test('should validate required decryption parameters', async () => {
263
- const symmetricCrypto = new GenericAesCtrStreamingCrypto();
264
- const adapter = new KMSCryptoAdapter(symmetricCrypto, 'https://mock-gateway.example.com', 'did:web:mock');
265
- const invalidConfigs = /** @type {any} */ ({
266
- decryptionConfig: {}, // Missing spaceDID and decryptDelegation
267
- metadata: { strategy: 'kms' },
268
- delegationCAR: new Uint8Array(),
269
- resourceCID: 'bafybeid',
270
- issuer: null,
271
- audience: 'did:web:mock',
272
- });
273
- await assert.rejects(() => adapter.decryptSymmetricKey('key', invalidConfigs), /SpaceDID and decryptDelegation are required/);
274
- });
275
- await test('should validate issuer is provided', async () => {
276
- const fixtures = await createTestFixtures();
277
- const { spaceDID, delegationProof } = fixtures;
278
- const symmetricCrypto = new GenericAesCtrStreamingCrypto();
279
- const adapter = new KMSCryptoAdapter(symmetricCrypto, 'https://mock-gateway.example.com', 'did:web:mock');
280
- const invalidConfigs = /** @type {any} */ ({
281
- decryptionConfig: { spaceDID, decryptDelegation: delegationProof },
282
- metadata: { strategy: 'kms' },
283
- delegationCAR: new Uint8Array(),
284
- resourceCID: 'bafybeid',
285
- issuer: null, // Missing issuer
286
- audience: 'did:web:mock',
287
- });
288
- await assert.rejects(() => adapter.decryptSymmetricKey('key', invalidConfigs), /Issuer is required/);
289
- });
290
- await test('should reject non-KMS metadata', async () => {
291
- const symmetricCrypto = new GenericAesCtrStreamingCrypto();
292
- const adapter = new KMSCryptoAdapter(symmetricCrypto, 'https://mock-gateway.example.com', 'did:web:mock');
293
- const invalidConfigs = /** @type {any} */ ({
294
- decryptionOptions: { spaceDID: 'did:key:test', delegationProof: {} },
295
- metadata: { strategy: 'lit' }, // Wrong strategy
296
- delegationCAR: new Uint8Array(),
297
- resourceCID: 'bafybeid',
298
- issuer: {},
299
- audience: 'did:web:mock',
300
- });
301
- await assert.rejects(() => adapter.decryptSymmetricKey('key', invalidConfigs), /KMSCryptoAdapter can only handle KMS metadata/);
302
- });
303
- });
304
- });
305
- //# sourceMappingURL=kms-crypto-adapter.spec.js.map
20
+ await describe('Unit Tests', async () => {
21
+ await test('should delegate symmetric crypto operations to the injected implementation', async () => {
22
+ const symmetricCrypto = new GenericAesCtrStreamingCrypto()
23
+ const adapter = new KMSCryptoAdapter(
24
+ symmetricCrypto,
25
+ 'https://private.storacha.link',
26
+ 'did:web:private.storacha.link'
27
+ )
28
+ const originalText =
29
+ 'Op, this is a test for KMS strategy-based encryption!'
30
+ const blob = new Blob([stringToUint8Array(originalText)])
31
+ // Test that it delegates to the symmetric crypto implementation
32
+ const { key, iv, encryptedStream } = await adapter.encryptStream(blob)
33
+ assert(key instanceof Uint8Array, 'Key should be a Uint8Array')
34
+ assert(iv instanceof Uint8Array, 'IV should be a Uint8Array')
35
+ assert(
36
+ encryptedStream instanceof ReadableStream,
37
+ 'Encrypted stream should be a ReadableStream'
38
+ )
39
+ // Test decryption delegation
40
+ const decryptedStream = await adapter.decryptStream(
41
+ encryptedStream,
42
+ key,
43
+ iv
44
+ )
45
+ const decryptedBytes = await streamToUint8Array(decryptedStream)
46
+ const decryptedText = uint8ArrayToString(decryptedBytes)
47
+ assert.strictEqual(
48
+ decryptedText,
49
+ originalText,
50
+ 'Decrypted text should match original'
51
+ )
52
+ })
53
+ await test('should initialize KMS adapter with correct configuration', async () => {
54
+ const symmetricCrypto = new GenericAesCtrStreamingCrypto()
55
+ const adapter = new KMSCryptoAdapter(
56
+ symmetricCrypto,
57
+ 'https://private.storacha.link',
58
+ 'did:web:private.storacha.link'
59
+ )
60
+ // Test that the adapter can handle encryption options directly
61
+ assert(
62
+ typeof adapter.encryptSymmetricKey === 'function',
63
+ 'encryptSymmetricKey should be a function'
64
+ )
65
+ // Verify adapter constructor sets properties correctly
66
+ assert(
67
+ typeof adapter.keyManagerServiceDID === 'object',
68
+ 'Adapter should have gateway DID object'
69
+ )
70
+ assert.strictEqual(
71
+ adapter.keyManagerServiceDID.did(),
72
+ 'did:web:private.storacha.link',
73
+ 'Adapter should have correct gateway DID'
74
+ )
75
+ assert(
76
+ adapter.keyManagerServiceURL instanceof URL,
77
+ 'Adapter should have gateway URL'
78
+ )
79
+ })
80
+ await test('should handle metadata extraction with invalid CAR data', async () => {
81
+ const symmetricCrypto = new GenericAesCtrStreamingCrypto()
82
+ const adapter = new KMSCryptoAdapter(
83
+ symmetricCrypto,
84
+ 'https://private.storacha.link',
85
+ 'did:web:private.storacha.link'
86
+ )
87
+ // Test that the method exists
88
+ assert(
89
+ typeof adapter.extractEncryptedMetadata === 'function',
90
+ 'extractEncryptedMetadata should be a function'
91
+ )
92
+ assert(
93
+ typeof adapter.getEncryptedKey === 'function',
94
+ 'getEncryptedKey should be a function'
95
+ )
96
+ // Should throw error for invalid CAR data
97
+ const mockCar = new Uint8Array([1, 2, 3])
98
+ assert.throws(
99
+ () => {
100
+ adapter.extractEncryptedMetadata(mockCar)
101
+ },
102
+ /Invalid CAR header format/,
103
+ 'Should throw error for invalid CAR data'
104
+ )
105
+ })
106
+ await test('should sanitize space DID for KMS key ID', async () => {
107
+ const symmetricCrypto = new GenericAesCtrStreamingCrypto()
108
+ const adapter = new KMSCryptoAdapter(
109
+ symmetricCrypto,
110
+ 'https://mock-gateway.example.com',
111
+ 'did:web:mock'
112
+ )
113
+ const spaceDID =
114
+ 'did:key:z6MkwDK3M4PxU1FqcSt6quBH1xRBSGnPRdQYP9B13h3Wq5X1'
115
+ const sanitized = adapter.sanitizeSpaceDIDForKMSKeyId(spaceDID)
116
+ assert.strictEqual(
117
+ sanitized,
118
+ 'z6MkwDK3M4PxU1FqcSt6quBH1xRBSGnPRdQYP9B13h3Wq5X1'
119
+ )
120
+ assert(!sanitized.includes('did:key:'))
121
+ })
122
+ })
123
+ await describe('Integration Tests', async () => {
124
+ await test('should complete full encryption workflow with mocked private gateway', async () => {
125
+ const fixtures = await createTestFixtures()
126
+ const {
127
+ keyManagerServiceDID,
128
+ spaceDID,
129
+ issuer,
130
+ publicKeyPem,
131
+ keyPair,
132
+ delegationProof,
133
+ } = fixtures
134
+ let setupCalled = false
135
+ let decryptCalled = false
136
+ let actualEncryptedKey = ''
137
+ // Create mock gateway service that performs real RSA encryption/decryption
138
+ const service = createMockKeyManagerService({
139
+ mockPublicKey: publicKeyPem,
140
+ onEncryptionSetup: (/** @type {any} */ input) => {
141
+ setupCalled = true
142
+ assert.strictEqual(input.capability.with, spaceDID)
143
+ assert.strictEqual(input.capability.can, 'space/encryption/setup')
144
+ },
145
+ onKeyDecrypt: (/** @type {any} */ input) => {
146
+ decryptCalled = true
147
+ assert.strictEqual(input.capability.with, spaceDID)
148
+ assert.strictEqual(
149
+ input.capability.can,
150
+ 'space/encryption/key/decrypt'
151
+ )
152
+ },
153
+ })
154
+ // Override the decrypt handler to actually decrypt with the private key
155
+ service.space.encryption.key.decrypt = Server.provide(
156
+ Space.EncryptionKeyDecrypt,
157
+ async (input) => {
158
+ decryptCalled = true
159
+ assert.strictEqual(input.capability.with, spaceDID)
160
+ assert.strictEqual(
161
+ input.capability.can,
162
+ 'space/encryption/key/decrypt'
163
+ )
164
+ // Get the encrypted key from the request
165
+ const encryptedKeyBytes = input.capability.nb.key
166
+ const encryptedSymmetricKey = base64.encode(encryptedKeyBytes)
167
+ actualEncryptedKey = encryptedSymmetricKey
168
+ // Decrypt with RSA private key (simulate real KMS decryption)
169
+ const encryptedBytes = encryptedKeyBytes
170
+ const decryptedBytes = await globalThis.crypto.subtle.decrypt(
171
+ { name: 'RSA-OAEP' },
172
+ keyPair.privateKey,
173
+ encryptedBytes
174
+ )
175
+ // Return the decrypted symmetric key as base64
176
+ const decryptedKey = base64.encode(new Uint8Array(decryptedBytes))
177
+ return Server.ok({
178
+ decryptedSymmetricKey: decryptedKey,
179
+ })
180
+ }
181
+ )
182
+ // Create mock gateway server (HTTPS by default)
183
+ const keyManagerServiceServer = await createMockKeyManagerServer(
184
+ service,
185
+ keyManagerServiceDID,
186
+ 5555
187
+ )
188
+ // Create KMS adapter with HTTP allowed for testing
189
+ const symmetricCrypto = new GenericAesCtrStreamingCrypto()
190
+ const adapter = new KMSCryptoAdapter(
191
+ symmetricCrypto,
192
+ keyManagerServiceServer.url,
193
+ keyManagerServiceDID.did(),
194
+ { allowInsecureHttp: true } // Allow HTTP for testing
195
+ )
196
+ try {
197
+ // Create test file and encrypt it to get real symmetric keys
198
+ const testFile = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
199
+ const testBlob = new Blob([testFile], {
200
+ type: 'application/octet-stream',
201
+ })
202
+ // Encrypt the file to get real symmetric keys
203
+ const { key, iv } = await adapter.encryptStream(testBlob)
204
+ const encryptionConfig = {
205
+ issuer,
206
+ spaceDID,
207
+ location: 'us-central1',
208
+ keyring: 'test-keyring',
209
+ }
210
+ // Test key encryption with real symmetric keys - this will call the mock setup
211
+ const encryptResult = await adapter.encryptSymmetricKey(
212
+ key,
213
+ iv,
214
+ encryptionConfig
215
+ )
216
+ assert(setupCalled, 'EncryptionSetup should have been called')
217
+ assert.strictEqual(encryptResult.strategy, 'kms')
218
+ const kmsMetadata =
219
+ /** @type {import('../src/types.js').KMSKeyMetadata} */ (
220
+ encryptResult.metadata
221
+ )
222
+ assert.strictEqual(kmsMetadata.space, spaceDID)
223
+ assert.strictEqual(kmsMetadata.kms.provider, 'google-kms')
224
+ assert.strictEqual(kmsMetadata.kms.algorithm, 'RSA-OAEP-2048-SHA256')
225
+ assert(typeof encryptResult.encryptedKey === 'string')
226
+ // Test key decryption - this will call the mock decrypt
227
+ const decryptionConfig = {
228
+ spaceDID,
229
+ decryptDelegation: delegationProof,
230
+ proofs: [],
231
+ }
232
+ const mockMetadata = {
233
+ strategy: /** @type {'kms'} */ ('kms'),
234
+ encryptedDataCID: 'bafybeid',
235
+ encryptedSymmetricKey: encryptResult.encryptedKey,
236
+ space: spaceDID,
237
+ kms: kmsMetadata.kms,
238
+ }
239
+ const decryptResult = await adapter.decryptSymmetricKey(
240
+ encryptResult.encryptedKey,
241
+ {
242
+ decryptionConfig,
243
+ metadata: mockMetadata,
244
+ resourceCID:
245
+ /** @type {import('@storacha/upload-client/types').AnyLink} */ (
246
+ /** @type {any} */ ('bafybeid')
247
+ ),
248
+ issuer,
249
+ audience: keyManagerServiceDID.did(),
250
+ }
251
+ )
252
+ // Verify the round-trip worked
253
+ assert(setupCalled, 'EncryptionSetup should have been called')
254
+ assert(decryptCalled, 'KeyDecrypt should have been called')
255
+ assert(
256
+ decryptResult.key instanceof Uint8Array,
257
+ 'Decrypted key should be Uint8Array'
258
+ )
259
+ assert(
260
+ decryptResult.iv instanceof Uint8Array,
261
+ 'Decrypted IV should be Uint8Array'
262
+ )
263
+ // Most importantly: verify the decrypted keys match the original
264
+ assert.deepStrictEqual(
265
+ decryptResult.key,
266
+ key,
267
+ 'Decrypted key should match original key'
268
+ )
269
+ assert.deepStrictEqual(
270
+ decryptResult.iv,
271
+ iv,
272
+ 'Decrypted IV should match original IV'
273
+ )
274
+ // Verify the encrypted key was actually encrypted (different from original)
275
+ const originalCombined = adapter.symmetricCrypto.combineKeyAndIV(
276
+ key,
277
+ iv
278
+ )
279
+ const originalBase64 = base64.encode(originalCombined)
280
+ assert.notStrictEqual(
281
+ actualEncryptedKey,
282
+ originalBase64,
283
+ 'Encrypted key should be different from original'
284
+ )
285
+ } catch (error) {
286
+ console.error('Test failed with error:', error)
287
+ throw error
288
+ } finally {
289
+ // Clean up server
290
+ await keyManagerServiceServer.close()
291
+ }
292
+ })
293
+ await test('should handle encryption setup errors gracefully', async () => {
294
+ const fixtures = await createTestFixtures()
295
+ const { keyManagerServiceDID, spaceDID, issuer } = fixtures
296
+ // Create service that returns errors
297
+ const service = createMockKeyManagerService({
298
+ mockPublicKey: 'invalid',
299
+ onEncryptionSetup: () => {
300
+ // This will be called but service will return error
301
+ },
302
+ })
303
+ // Override service to return error
304
+ service.space.encryption.setup = Server.provide(
305
+ Space.EncryptionSetup,
306
+ async () => {
307
+ return Server.error({
308
+ name: 'SpaceNotProvisioned',
309
+ message: 'Space is not provisioned for encryption',
310
+ })
311
+ }
312
+ )
313
+ const keyManagerServiceServer = await createMockKeyManagerServer(
314
+ service,
315
+ keyManagerServiceDID,
316
+ 5556
317
+ )
318
+ const symmetricCrypto = new GenericAesCtrStreamingCrypto()
319
+ const adapter = new KMSCryptoAdapter(
320
+ symmetricCrypto,
321
+ keyManagerServiceServer.url,
322
+ keyManagerServiceDID.did(),
323
+ { allowInsecureHttp: true } // Allow HTTP for testing
324
+ )
325
+ try {
326
+ const testKey = new Uint8Array(32).fill(1)
327
+ const testIV = new Uint8Array(16).fill(2)
328
+ const encryptionConfig = {
329
+ issuer,
330
+ spaceDID,
331
+ }
332
+ // Should throw error
333
+ await assert.rejects(
334
+ () => adapter.encryptSymmetricKey(testKey, testIV, encryptionConfig),
335
+ /Space is not provisioned for encryption/
336
+ )
337
+ } finally {
338
+ await keyManagerServiceServer.close()
339
+ }
340
+ })
341
+ await test('should handle key decryption errors gracefully', async () => {
342
+ const fixtures = await createTestFixtures()
343
+ const { keyManagerServiceDID, spaceDID, issuer, delegationProof } =
344
+ fixtures
345
+ // Create service that returns errors for decrypt
346
+ const service = createMockKeyManagerService({
347
+ mockPublicKey: 'mock-key',
348
+ })
349
+ // Override decrypt service to return error
350
+ service.space.encryption.key.decrypt = Server.provide(
351
+ Space.EncryptionKeyDecrypt,
352
+ async () => {
353
+ return Server.error({
354
+ name: 'KeyNotFound',
355
+ message: 'KMS key not found',
356
+ })
357
+ }
358
+ )
359
+ const keyManagerServiceServer = await createMockKeyManagerServer(
360
+ service,
361
+ keyManagerServiceDID,
362
+ 5557
363
+ )
364
+ const symmetricCrypto = new GenericAesCtrStreamingCrypto()
365
+ const adapter = new KMSCryptoAdapter(
366
+ symmetricCrypto,
367
+ keyManagerServiceServer.url,
368
+ keyManagerServiceDID.did(),
369
+ { allowInsecureHttp: true } // Allow HTTP for testing
370
+ )
371
+ const decryptionOptions = {
372
+ spaceDID,
373
+ decryptDelegation: delegationProof,
374
+ }
375
+ const mockKey = new Uint8Array([1, 2, 3]) // test value as bytes
376
+ const mockKeyString = base64.encode(mockKey)
377
+ const mockMetadata = {
378
+ strategy: /** @type {'kms'} */ ('kms'),
379
+ encryptedDataCID: 'bafybeid',
380
+ key: mockKey, // use bytes, not string
381
+ space: spaceDID,
382
+ kms: {
383
+ provider: /** @type {'google-kms'} */ ('google-kms'),
384
+ keyId: 'test-key',
385
+ algorithm: /** @type {'RSA-OAEP-2048-SHA256'} */ (
386
+ 'RSA-OAEP-2048-SHA256'
387
+ ),
388
+ },
389
+ }
390
+ const decryptConfigs = /** @type {any} */ ({
391
+ decryptionConfig: decryptionOptions,
392
+ metadata: mockMetadata,
393
+ delegationCAR: new Uint8Array(),
394
+ resourceCID: 'bafybeid',
395
+ issuer,
396
+ audience: keyManagerServiceDID.did(),
397
+ })
398
+ try {
399
+ // Should throw error
400
+ await assert.rejects(
401
+ () => adapter.decryptSymmetricKey(mockKeyString, decryptConfigs),
402
+ /KMS key not found/
403
+ )
404
+ } finally {
405
+ await keyManagerServiceServer.close()
406
+ }
407
+ })
408
+ })
409
+ await describe('Validation Tests', async () => {
410
+ await test('should validate required decryption parameters', async () => {
411
+ const symmetricCrypto = new GenericAesCtrStreamingCrypto()
412
+ const adapter = new KMSCryptoAdapter(
413
+ symmetricCrypto,
414
+ 'https://mock-gateway.example.com',
415
+ 'did:web:mock'
416
+ )
417
+ const invalidConfigs = /** @type {any} */ ({
418
+ decryptionConfig: {}, // Missing spaceDID and decryptDelegation
419
+ metadata: { strategy: 'kms' },
420
+ delegationCAR: new Uint8Array(),
421
+ resourceCID: 'bafybeid',
422
+ issuer: null,
423
+ audience: 'did:web:mock',
424
+ })
425
+ await assert.rejects(
426
+ () => adapter.decryptSymmetricKey('key', invalidConfigs),
427
+ /SpaceDID and decryptDelegation are required/
428
+ )
429
+ })
430
+ await test('should validate issuer is provided', async () => {
431
+ const fixtures = await createTestFixtures()
432
+ const { spaceDID, delegationProof } = fixtures
433
+ const symmetricCrypto = new GenericAesCtrStreamingCrypto()
434
+ const adapter = new KMSCryptoAdapter(
435
+ symmetricCrypto,
436
+ 'https://mock-gateway.example.com',
437
+ 'did:web:mock'
438
+ )
439
+ const invalidConfigs = /** @type {any} */ ({
440
+ decryptionConfig: { spaceDID, decryptDelegation: delegationProof },
441
+ metadata: { strategy: 'kms' },
442
+ delegationCAR: new Uint8Array(),
443
+ resourceCID: 'bafybeid',
444
+ issuer: null, // Missing issuer
445
+ audience: 'did:web:mock',
446
+ })
447
+ await assert.rejects(
448
+ () => adapter.decryptSymmetricKey('key', invalidConfigs),
449
+ /Issuer is required/
450
+ )
451
+ })
452
+ await test('should reject non-KMS metadata', async () => {
453
+ const symmetricCrypto = new GenericAesCtrStreamingCrypto()
454
+ const adapter = new KMSCryptoAdapter(
455
+ symmetricCrypto,
456
+ 'https://mock-gateway.example.com',
457
+ 'did:web:mock'
458
+ )
459
+ const invalidConfigs = /** @type {any} */ ({
460
+ decryptionOptions: { spaceDID: 'did:key:test', delegationProof: {} },
461
+ metadata: { strategy: 'lit' }, // Wrong strategy
462
+ delegationCAR: new Uint8Array(),
463
+ resourceCID: 'bafybeid',
464
+ issuer: {},
465
+ audience: 'did:web:mock',
466
+ })
467
+ await assert.rejects(
468
+ () => adapter.decryptSymmetricKey('key', invalidConfigs),
469
+ /KMSCryptoAdapter can only handle KMS metadata/
470
+ )
471
+ })
472
+ })
473
+ })
474
+ //# sourceMappingURL=kms-crypto-adapter.spec.js.map