@storacha/encrypt-upload-client 1.1.80 → 1.1.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crypto/adapters/kms-crypto-adapter.d.ts +4 -10
- package/dist/crypto/adapters/kms-crypto-adapter.d.ts.map +1 -1
- package/dist/crypto/adapters/kms-crypto-adapter.js +4 -4
- package/dist/examples/decrypt-test.d.ts +2 -0
- package/dist/examples/decrypt-test.d.ts.map +1 -0
- package/dist/examples/decrypt-test.js +102 -0
- package/dist/examples/encrypt-test.d.ts +2 -0
- package/dist/examples/encrypt-test.d.ts.map +1 -0
- package/dist/examples/encrypt-test.js +93 -0
- package/dist/test/cid-verification.spec.d.ts +2 -0
- package/dist/test/cid-verification.spec.d.ts.map +1 -0
- package/dist/test/cid-verification.spec.js +314 -0
- package/dist/test/crypto-compatibility.spec.d.ts +2 -0
- package/dist/test/crypto-compatibility.spec.d.ts.map +1 -0
- package/dist/test/crypto-compatibility.spec.js +124 -0
- package/dist/test/crypto-counter-security.spec.d.ts +2 -0
- package/dist/test/crypto-counter-security.spec.d.ts.map +1 -0
- package/dist/test/crypto-counter-security.spec.js +147 -0
- package/dist/test/crypto-streaming.spec.d.ts +2 -0
- package/dist/test/crypto-streaming.spec.d.ts.map +1 -0
- package/dist/test/crypto-streaming.spec.js +129 -0
- package/dist/test/encrypted-metadata.spec.d.ts +2 -0
- package/dist/test/encrypted-metadata.spec.d.ts.map +1 -0
- package/dist/test/encrypted-metadata.spec.js +68 -0
- package/dist/test/factories.spec.d.ts +2 -0
- package/dist/test/factories.spec.d.ts.map +1 -0
- package/dist/test/factories.spec.js +129 -0
- package/dist/test/file-metadata.spec.d.ts +2 -0
- package/dist/test/file-metadata.spec.d.ts.map +1 -0
- package/dist/test/file-metadata.spec.js +433 -0
- package/dist/test/fixtures/test-fixtures.d.ts +28 -0
- package/dist/test/fixtures/test-fixtures.d.ts.map +1 -0
- package/dist/test/fixtures/test-fixtures.js +63 -0
- package/dist/test/helpers/test-file-utils.d.ts +60 -0
- package/dist/test/helpers/test-file-utils.d.ts.map +1 -0
- package/dist/test/helpers/test-file-utils.js +139 -0
- package/dist/test/https-enforcement.spec.d.ts +2 -0
- package/dist/test/https-enforcement.spec.d.ts.map +1 -0
- package/dist/test/https-enforcement.spec.js +125 -0
- package/dist/test/kms-crypto-adapter.spec.d.ts +2 -0
- package/dist/test/kms-crypto-adapter.spec.d.ts.map +1 -0
- package/dist/test/kms-crypto-adapter.spec.js +305 -0
- package/dist/test/lit-crypto-adapter.spec.d.ts +2 -0
- package/dist/test/lit-crypto-adapter.spec.d.ts.map +1 -0
- package/dist/test/lit-crypto-adapter.spec.js +76 -0
- package/dist/test/memory-efficiency.spec.d.ts +2 -0
- package/dist/test/memory-efficiency.spec.d.ts.map +1 -0
- package/dist/test/memory-efficiency.spec.js +93 -0
- package/dist/test/mocks/key-manager.d.ts +58 -0
- package/dist/test/mocks/key-manager.d.ts.map +1 -0
- package/dist/test/mocks/key-manager.js +137 -0
- package/dist/test/node-crypto-adapter.spec.d.ts +2 -0
- package/dist/test/node-crypto-adapter.spec.d.ts.map +1 -0
- package/dist/test/node-crypto-adapter.spec.js +103 -0
- package/dist/test/node-generic-crypto-adapter.spec.d.ts +2 -0
- package/dist/test/node-generic-crypto-adapter.spec.d.ts.map +1 -0
- package/dist/test/node-generic-crypto-adapter.spec.js +95 -0
- package/dist/test/setup.d.ts +2 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/test/setup.js +12 -0
- package/dist/tsconfig.spec.tsbuildinfo +1 -0
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { KMSMetadata } from '../../src/core/metadata/encrypted-metadata.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create test data with specific patterns for easy verification
|
|
4
|
+
*
|
|
5
|
+
* @param {number} sizeMB - Size of the test file in megabytes
|
|
6
|
+
* @returns {Blob} A Blob containing test data with predictable patterns
|
|
7
|
+
*/
|
|
8
|
+
export function createTestFile(sizeMB) {
|
|
9
|
+
const chunkSize = 64 * 1024; // 64KB chunks
|
|
10
|
+
const totalSize = sizeMB * 1024 * 1024;
|
|
11
|
+
const numChunks = Math.ceil(totalSize / chunkSize);
|
|
12
|
+
const chunks = [];
|
|
13
|
+
for (let i = 0; i < numChunks; i++) {
|
|
14
|
+
const isLastChunk = i === numChunks - 1;
|
|
15
|
+
const currentChunkSize = isLastChunk
|
|
16
|
+
? totalSize % chunkSize || chunkSize
|
|
17
|
+
: chunkSize;
|
|
18
|
+
const chunk = new Uint8Array(currentChunkSize);
|
|
19
|
+
// Create pattern: chunk index in first byte, then sequence
|
|
20
|
+
chunk[0] = i % 256;
|
|
21
|
+
for (let j = 1; j < currentChunkSize; j++) {
|
|
22
|
+
chunk[j] = (i + j) % 256;
|
|
23
|
+
}
|
|
24
|
+
chunks.push(chunk);
|
|
25
|
+
}
|
|
26
|
+
return new Blob(chunks, { type: 'application/octet-stream' });
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Convert ReadableStream to Uint8Array
|
|
30
|
+
*
|
|
31
|
+
* @param {ReadableStream} stream - The stream to convert
|
|
32
|
+
* @returns {Promise<Uint8Array>} The stream content as a Uint8Array
|
|
33
|
+
*/
|
|
34
|
+
export async function streamToUint8Array(stream) {
|
|
35
|
+
const reader = stream.getReader();
|
|
36
|
+
const chunks = [];
|
|
37
|
+
// eslint-disable-next-line no-constant-condition
|
|
38
|
+
while (true) {
|
|
39
|
+
const { done, value } = await reader.read();
|
|
40
|
+
if (done)
|
|
41
|
+
break;
|
|
42
|
+
chunks.push(value);
|
|
43
|
+
}
|
|
44
|
+
const totalLength = chunks.reduce((acc, val) => acc + val.length, 0);
|
|
45
|
+
const result = new Uint8Array(totalLength);
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (const chunk of chunks) {
|
|
48
|
+
result.set(chunk, offset);
|
|
49
|
+
offset += chunk.length;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* @param {Uint8Array} arr
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function uint8ArrayToString(arr) {
|
|
58
|
+
return new TextDecoder().decode(arr);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} str
|
|
62
|
+
* @returns {Uint8Array}
|
|
63
|
+
*/
|
|
64
|
+
export function stringToUint8Array(str) {
|
|
65
|
+
return new TextEncoder().encode(str);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if an error is a memory-related error (out of heap space, etc.)
|
|
69
|
+
*
|
|
70
|
+
* @param {unknown} error - The error to check
|
|
71
|
+
* @returns {boolean} True if the error appears to be memory-related
|
|
72
|
+
*/
|
|
73
|
+
export function isMemoryError(error) {
|
|
74
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
75
|
+
return (errorMessage.includes('heap') ||
|
|
76
|
+
errorMessage.includes('memory') ||
|
|
77
|
+
errorMessage.includes('allocation failed') ||
|
|
78
|
+
errorMessage.includes('out of memory'));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Test an encryption operation and expect it might fail with memory errors
|
|
82
|
+
*
|
|
83
|
+
* @param {Function} encryptOperation - Function that performs encryption
|
|
84
|
+
* @param {string} operationName - Name of the operation for logging
|
|
85
|
+
* @returns {Promise<{success: boolean, error?: Error}>} Result of the operation
|
|
86
|
+
*/
|
|
87
|
+
export async function testEncryptionWithMemoryHandling(encryptOperation, operationName) {
|
|
88
|
+
try {
|
|
89
|
+
await encryptOperation();
|
|
90
|
+
return { success: true };
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (isMemoryError(error)) {
|
|
94
|
+
console.log(`✓ ${operationName} failed as expected: Out of memory`);
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Re-throw if it's not a memory error
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Create a CAR file with KMS metadata content
|
|
108
|
+
*
|
|
109
|
+
* @param {any} content - The KMS metadata content
|
|
110
|
+
* @returns {Promise<{car: Uint8Array, actualRootCID: import('multiformats').UnknownLink}>}
|
|
111
|
+
*/
|
|
112
|
+
export async function createTestCar(content) {
|
|
113
|
+
// Create KMS metadata and archive it to get the CAR
|
|
114
|
+
const kmsMetadata = KMSMetadata.create(content);
|
|
115
|
+
const { cid, bytes } = await kmsMetadata.archiveBlock();
|
|
116
|
+
// Use UCANTO's CAR encoding to create a proper CAR file
|
|
117
|
+
const { CAR } = await import('@ucanto/core');
|
|
118
|
+
const car = CAR.encode({ roots: [{ cid, bytes }] });
|
|
119
|
+
return { car, actualRootCID: cid };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Create a mock BlobLike object for testing
|
|
123
|
+
*
|
|
124
|
+
* @param {Uint8Array} data
|
|
125
|
+
* @returns {import('../../src/types.js').BlobLike}
|
|
126
|
+
*/
|
|
127
|
+
export function createMockBlob(data) {
|
|
128
|
+
return {
|
|
129
|
+
stream() {
|
|
130
|
+
return new ReadableStream({
|
|
131
|
+
start(controller) {
|
|
132
|
+
controller.enqueue(data);
|
|
133
|
+
controller.close();
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=test-file-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"https-enforcement.spec.d.ts","sourceRoot":"","sources":["../../test/https-enforcement.spec.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
import { KMSCryptoAdapter } from '../src/crypto/adapters/kms-crypto-adapter.js';
|
|
4
|
+
import { GenericAesCtrStreamingCrypto } from '../src/crypto/symmetric/generic-aes-ctr-streaming-crypto.js';
|
|
5
|
+
await describe('HTTPS Enforcement', async () => {
|
|
6
|
+
await describe('KMSCryptoAdapter Constructor', async () => {
|
|
7
|
+
await test('should accept valid HTTPS URL as string', async () => {
|
|
8
|
+
const symmetricCrypto = new GenericAesCtrStreamingCrypto();
|
|
9
|
+
// Should not throw
|
|
10
|
+
const adapter = new KMSCryptoAdapter(symmetricCrypto, 'https://private.storacha.link', 'did:web:private.storacha.link');
|
|
11
|
+
assert(adapter instanceof KMSCryptoAdapter, 'Should create adapter successfully');
|
|
12
|
+
assert.strictEqual(adapter.keyManagerServiceURL.protocol, 'https:', 'Should store HTTPS protocol');
|
|
13
|
+
assert.strictEqual(adapter.keyManagerServiceURL.toString(), 'https://private.storacha.link/', 'Should store correct URL');
|
|
14
|
+
});
|
|
15
|
+
await test('should accept valid HTTPS URL object', async () => {
|
|
16
|
+
const symmetricCrypto = new GenericAesCtrStreamingCrypto();
|
|
17
|
+
const httpsURL = new URL('https://example.com:8443/path');
|
|
18
|
+
// Should not throw
|
|
19
|
+
const adapter = new KMSCryptoAdapter(symmetricCrypto, httpsURL, 'did:web:example.com');
|
|
20
|
+
assert(adapter instanceof KMSCryptoAdapter, 'Should create adapter successfully');
|
|
21
|
+
assert.strictEqual(adapter.keyManagerServiceURL.protocol, 'https:', 'Should store HTTPS protocol');
|
|
22
|
+
assert.strictEqual(adapter.keyManagerServiceURL.toString(), 'https://example.com:8443/path', 'Should preserve URL structure');
|
|
23
|
+
});
|
|
24
|
+
await test('should reject HTTP URL string', async () => {
|
|
25
|
+
const symmetricCrypto = new GenericAesCtrStreamingCrypto();
|
|
26
|
+
assert.throws(() => new KMSCryptoAdapter(symmetricCrypto, 'http://insecure.example.com', 'did:web:example.com'), /Key manager service must use HTTPS protocol for security/, 'Should reject HTTP protocol');
|
|
27
|
+
});
|
|
28
|
+
await test('should reject HTTP URL object', async () => {
|
|
29
|
+
const symmetricCrypto = new GenericAesCtrStreamingCrypto();
|
|
30
|
+
const httpURL = new URL('http://insecure.example.com');
|
|
31
|
+
assert.throws(() => new KMSCryptoAdapter(symmetricCrypto, httpURL, 'did:web:example.com'), /Key manager service must use HTTPS protocol for security/, 'Should reject HTTP URL object');
|
|
32
|
+
});
|
|
33
|
+
await test('should reject other protocols', async () => {
|
|
34
|
+
const symmetricCrypto = new GenericAesCtrStreamingCrypto();
|
|
35
|
+
const protocolTestCases = [
|
|
36
|
+
'ftp://example.com',
|
|
37
|
+
'ws://example.com',
|
|
38
|
+
'file://example.com',
|
|
39
|
+
'data:text/plain;base64,SGVsbG8=',
|
|
40
|
+
];
|
|
41
|
+
for (const testURL of protocolTestCases) {
|
|
42
|
+
assert.throws(() => new KMSCryptoAdapter(symmetricCrypto, testURL, 'did:web:example.com'), /Key manager service must use HTTPS protocol for security/, `Should reject protocol: ${testURL}`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
await test('should provide helpful error message', async () => {
|
|
46
|
+
const symmetricCrypto = new GenericAesCtrStreamingCrypto();
|
|
47
|
+
try {
|
|
48
|
+
new KMSCryptoAdapter(symmetricCrypto, 'http://example.com', 'did:web:example.com');
|
|
49
|
+
assert.fail('Should have thrown an error');
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
assert(error instanceof Error, 'Should throw Error instance');
|
|
53
|
+
assert(error.message.includes('Key manager service must use HTTPS protocol'), 'Should include main error message');
|
|
54
|
+
assert(error.message.includes('Received: http:'), 'Should include received protocol');
|
|
55
|
+
assert(error.message.includes('https://your-key-manager-service.com'), 'Should include example of correct format');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
await test('should handle localhost development URLs correctly', async () => {
|
|
59
|
+
const symmetricCrypto = new GenericAesCtrStreamingCrypto();
|
|
60
|
+
// Even localhost should require HTTPS for consistency
|
|
61
|
+
assert.throws(() => new KMSCryptoAdapter(symmetricCrypto, 'http://localhost:3000', 'did:web:localhost'), /Key manager service must use HTTPS protocol for security/, 'Should reject HTTP even for localhost');
|
|
62
|
+
// But HTTPS localhost should work
|
|
63
|
+
const adapter = new KMSCryptoAdapter(symmetricCrypto, 'https://localhost:3000', 'did:web:localhost');
|
|
64
|
+
assert(adapter instanceof KMSCryptoAdapter, 'Should accept HTTPS localhost');
|
|
65
|
+
});
|
|
66
|
+
await test('should handle invalid URL strings gracefully', async () => {
|
|
67
|
+
const symmetricCrypto = new GenericAesCtrStreamingCrypto();
|
|
68
|
+
assert.throws(() => new KMSCryptoAdapter(symmetricCrypto, 'not-a-valid-url', 'did:web:example.com'), /Invalid URL/, 'Should throw URL parsing error for invalid URLs');
|
|
69
|
+
});
|
|
70
|
+
await test('should preserve all adapter functionality after HTTPS validation', async () => {
|
|
71
|
+
const symmetricCrypto = new GenericAesCtrStreamingCrypto();
|
|
72
|
+
const adapter = new KMSCryptoAdapter(symmetricCrypto, 'https://private.storacha.link', 'did:web:private.storacha.link');
|
|
73
|
+
// Verify all expected methods exist
|
|
74
|
+
assert.strictEqual(typeof adapter.encryptStream, 'function', 'Should have encryptStream method');
|
|
75
|
+
assert.strictEqual(typeof adapter.decryptStream, 'function', 'Should have decryptStream method');
|
|
76
|
+
assert.strictEqual(typeof adapter.encryptSymmetricKey, 'function', 'Should have encryptSymmetricKey method');
|
|
77
|
+
assert.strictEqual(typeof adapter.decryptSymmetricKey, 'function', 'Should have decryptSymmetricKey method');
|
|
78
|
+
assert.strictEqual(typeof adapter.extractEncryptedMetadata, 'function', 'Should have extractEncryptedMetadata method');
|
|
79
|
+
assert.strictEqual(typeof adapter.getEncryptedKey, 'function', 'Should have getEncryptedKey method');
|
|
80
|
+
assert.strictEqual(typeof adapter.encodeMetadata, 'function', 'Should have encodeMetadata method');
|
|
81
|
+
// Verify adapter properties are set correctly
|
|
82
|
+
assert(adapter.symmetricCrypto === symmetricCrypto, 'Should store symmetric crypto reference');
|
|
83
|
+
assert.strictEqual(adapter.keyManagerServiceDID.did(), 'did:web:private.storacha.link', 'Should store gateway DID');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
await describe('Secure by Default Principle', async () => {
|
|
87
|
+
await test('should demonstrate secure by default - HTTPS is automatically used', async () => {
|
|
88
|
+
const symmetricCrypto = new GenericAesCtrStreamingCrypto();
|
|
89
|
+
// All of these should work without any special configuration
|
|
90
|
+
const validHttpsUrls = [
|
|
91
|
+
'https://gateway.example.com',
|
|
92
|
+
'https://localhost:8080',
|
|
93
|
+
'https://192.168.1.100:3000',
|
|
94
|
+
'https://api.storacha.network:443/v1',
|
|
95
|
+
];
|
|
96
|
+
for (const url of validHttpsUrls) {
|
|
97
|
+
const adapter = new KMSCryptoAdapter(symmetricCrypto, url, 'did:web:example.com');
|
|
98
|
+
assert.strictEqual(adapter.keyManagerServiceURL.protocol, 'https:', `Should store HTTPS protocol for URL: ${url}`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
await test('should require explicit insecure configuration to bypass HTTPS', async () => {
|
|
102
|
+
// This demonstrates that HTTP is never accidentally allowed
|
|
103
|
+
// If someone really needs HTTP (like for testing), they would need to
|
|
104
|
+
// modify our security validation code intentionally
|
|
105
|
+
const symmetricCrypto = new GenericAesCtrStreamingCrypto();
|
|
106
|
+
const httpUrls = [
|
|
107
|
+
'http://example.com',
|
|
108
|
+
'http://localhost:3000',
|
|
109
|
+
'http://192.168.1.100:8080',
|
|
110
|
+
];
|
|
111
|
+
for (const url of httpUrls) {
|
|
112
|
+
assert.throws(() => new KMSCryptoAdapter(symmetricCrypto, url, 'did:web:example.com'), /Key manager service must use HTTPS protocol for security/, `Should reject HTTP URL: ${url}`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
await test('should allow HTTP for testing when explicitly enabled', async () => {
|
|
116
|
+
// This demonstrates the testing escape hatch
|
|
117
|
+
const symmetricCrypto = new GenericAesCtrStreamingCrypto();
|
|
118
|
+
const adapter = new KMSCryptoAdapter(symmetricCrypto, 'http://localhost:8080', 'did:web:localhost', { allowInsecureHttp: true } // Explicit testing option
|
|
119
|
+
);
|
|
120
|
+
assert.strictEqual(adapter.keyManagerServiceURL.protocol, 'http:', 'Should allow HTTP when explicitly enabled for testing');
|
|
121
|
+
assert.strictEqual(adapter.keyManagerServiceURL.toString(), 'http://localhost:8080/', 'Should preserve HTTP URL when testing option is enabled');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
//# sourceMappingURL=https-enforcement.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kms-crypto-adapter.spec.d.ts","sourceRoot":"","sources":["../../test/kms-crypto-adapter.spec.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,305 @@
|
|
|
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';
|
|
12
|
+
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lit-crypto-adapter.spec.d.ts","sourceRoot":"","sources":["../../test/lit-crypto-adapter.spec.js"],"names":[],"mappings":""}
|