@storacha/encrypt-upload-client 0.0.39 → 1.0.0
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/config/env.d.ts.map +1 -1
- package/dist/config/env.js +19 -6
- package/dist/core/client.d.ts +8 -12
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +12 -21
- package/dist/core/metadata/encrypted-metadata.d.ts +8 -0
- package/dist/core/metadata/encrypted-metadata.d.ts.map +1 -0
- package/dist/core/metadata/encrypted-metadata.js +69 -0
- package/dist/core/metadata/kms-metadata.d.ts +36 -0
- package/dist/core/metadata/kms-metadata.d.ts.map +1 -0
- package/dist/core/metadata/kms-metadata.js +156 -0
- package/dist/core/{encrypted-metadata.d.ts → metadata/lit-metadata.d.ts} +11 -11
- package/dist/core/metadata/lit-metadata.d.ts.map +1 -0
- package/dist/core/{encrypted-metadata.js → metadata/lit-metadata.js} +32 -42
- package/dist/crypto/adapters/kms-crypto-adapter.d.ts +148 -0
- package/dist/crypto/adapters/kms-crypto-adapter.d.ts.map +1 -0
- package/dist/crypto/adapters/kms-crypto-adapter.js +321 -0
- package/dist/crypto/adapters/lit-crypto-adapter.d.ts +96 -0
- package/dist/crypto/adapters/lit-crypto-adapter.d.ts.map +1 -0
- package/dist/crypto/adapters/lit-crypto-adapter.js +210 -0
- package/dist/crypto/factories.browser.d.ts +11 -0
- package/dist/crypto/factories.browser.d.ts.map +1 -0
- package/dist/crypto/factories.browser.js +16 -0
- package/dist/crypto/factories.node.d.ts +26 -0
- package/dist/crypto/factories.node.d.ts.map +1 -0
- package/dist/crypto/factories.node.js +38 -0
- package/dist/crypto/index.d.ts +5 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +7 -0
- package/dist/crypto/symmetric/generic-aes-ctr-streaming-crypto.d.ts +76 -0
- package/dist/crypto/symmetric/generic-aes-ctr-streaming-crypto.d.ts.map +1 -0
- package/dist/crypto/symmetric/generic-aes-ctr-streaming-crypto.js +177 -0
- package/dist/crypto/symmetric/node-aes-cbc-crypto.d.ts +43 -0
- package/dist/crypto/symmetric/node-aes-cbc-crypto.d.ts.map +1 -0
- package/dist/crypto/symmetric/node-aes-cbc-crypto.js +110 -0
- package/dist/handlers/decrypt-handler.d.ts +9 -4
- package/dist/handlers/decrypt-handler.d.ts.map +1 -1
- package/dist/handlers/decrypt-handler.js +62 -93
- package/dist/handlers/encrypt-handler.d.ts +1 -1
- package/dist/handlers/encrypt-handler.d.ts.map +1 -1
- package/dist/handlers/encrypt-handler.js +31 -41
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/protocols/lit.d.ts +1 -3
- package/dist/protocols/lit.d.ts.map +1 -1
- package/dist/types.d.ts +135 -20
- package/dist/types.d.ts.map +1 -1
- package/package.json +27 -18
- package/dist/core/encrypted-metadata.d.ts.map +0 -1
- package/dist/crypto-adapters/browser-crypto-adapter.d.ts +0 -42
- package/dist/crypto-adapters/browser-crypto-adapter.d.ts.map +0 -1
- package/dist/crypto-adapters/browser-crypto-adapter.js +0 -109
- package/dist/crypto-adapters/node-crypto-adapter.d.ts +0 -17
- package/dist/crypto-adapters/node-crypto-adapter.d.ts.map +0 -1
- package/dist/crypto-adapters/node-crypto-adapter.js +0 -66
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as Type from '../../types.js';
|
|
2
|
+
const ENCRYPTION_ALGORITHM = 'AES-CTR';
|
|
3
|
+
const KEY_LENGTH = 256; // bits
|
|
4
|
+
const IV_LENGTH = 16; // bytes (128 bits, used as counter)
|
|
5
|
+
const COUNTER_LENGTH = 64; // bits (Web Crypto API default for AES-CTR)
|
|
6
|
+
/**
|
|
7
|
+
* GenericAesCtrStreamingCrypto implements TRUE streaming AES-CTR encryption for any JavaScript environment.
|
|
8
|
+
*
|
|
9
|
+
* This implementation:
|
|
10
|
+
* - Uses Web Crypto API (available in both Node.js 16+ and modern browsers)
|
|
11
|
+
* - Emits encrypted chunks immediately without buffering
|
|
12
|
+
* - Supports files of any size with bounded memory usage
|
|
13
|
+
* - Uses TransformStream for clean, standardized streaming
|
|
14
|
+
* - Provides identical results across Node.js and browser environments
|
|
15
|
+
*
|
|
16
|
+
* Key features:
|
|
17
|
+
* - Memory usage: O(1) - constant memory regardless of file size
|
|
18
|
+
* - Supports unlimited file sizes (1TB+)
|
|
19
|
+
* - Cross-platform compatibility (Node.js 16+ and modern browsers)
|
|
20
|
+
* - Clean streaming implementation with automatic resource management
|
|
21
|
+
* - Built-in error handling via TransformStream
|
|
22
|
+
*
|
|
23
|
+
* @class
|
|
24
|
+
* @implements {Type.SymmetricCrypto}
|
|
25
|
+
*/
|
|
26
|
+
export class GenericAesCtrStreamingCrypto {
|
|
27
|
+
constructor() {
|
|
28
|
+
if (typeof globalThis.crypto === 'undefined') {
|
|
29
|
+
throw new Error('Web Crypto API is not available.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Generate a random AES key
|
|
34
|
+
*
|
|
35
|
+
* @returns {Promise<Uint8Array>} A random AES key
|
|
36
|
+
*/
|
|
37
|
+
async generateKey() {
|
|
38
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(KEY_LENGTH / 8));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Properly increment AES-CTR counter with 128-bit arithmetic
|
|
42
|
+
*
|
|
43
|
+
* @param {Uint8Array} counter - The base counter (16 bytes)
|
|
44
|
+
* @param {number} increment - The value to add
|
|
45
|
+
* @returns {Uint8Array} - New counter with proper carry propagation
|
|
46
|
+
*/
|
|
47
|
+
incrementCounter(counter, increment) {
|
|
48
|
+
const result = new Uint8Array(counter);
|
|
49
|
+
let carry = increment;
|
|
50
|
+
// Implement proper 128-bit arithmetic with carry propagation
|
|
51
|
+
// Start from the least significant byte (rightmost) and propagate carry
|
|
52
|
+
for (let i = result.length - 1; i >= 0 && carry > 0; i--) {
|
|
53
|
+
const sum = result[i] + carry;
|
|
54
|
+
result[i] = sum & 0xff; // Keep only the low 8 bits
|
|
55
|
+
carry = sum >> 8; // Carry the high bits to next position
|
|
56
|
+
}
|
|
57
|
+
// Check for counter overflow (extremely unlikely with 128-bit counter)
|
|
58
|
+
if (carry > 0) {
|
|
59
|
+
throw new Error('Counter overflow: exceeded 128-bit limit. This should never happen in practice.');
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Encrypt a stream of data using AES-CTR with TRUE streaming (no buffering).
|
|
65
|
+
*
|
|
66
|
+
* @param {Blob} data The data to encrypt.
|
|
67
|
+
* @returns {Promise<{ key: Uint8Array, iv: Uint8Array, encryptedStream: ReadableStream }>}
|
|
68
|
+
*/
|
|
69
|
+
async encryptStream(data) {
|
|
70
|
+
const key = await this.generateKey();
|
|
71
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
72
|
+
// Pre-import the crypto key for reuse across chunks
|
|
73
|
+
const cryptoKey = await globalThis.crypto.subtle.importKey('raw', key, { name: ENCRYPTION_ALGORITHM }, false, ['encrypt', 'decrypt']);
|
|
74
|
+
// State for AES-CTR counter management
|
|
75
|
+
let counter = new Uint8Array(iv); // Copy the IV for counter
|
|
76
|
+
let totalBlocks = 0; // Track total blocks processed
|
|
77
|
+
// Create TransformStream (inspired by Node.js approach)
|
|
78
|
+
const encryptTransform = new TransformStream({
|
|
79
|
+
transform: async (chunk, controller) => {
|
|
80
|
+
try {
|
|
81
|
+
// SECURITY: Calculate counter based on total blocks, not chunks
|
|
82
|
+
const chunkCounter = this.incrementCounter(counter, totalBlocks);
|
|
83
|
+
// SECURITY: Increment by blocks in this chunk (16 bytes per block)
|
|
84
|
+
const blocksInChunk = Math.ceil(chunk.length / 16);
|
|
85
|
+
totalBlocks += blocksInChunk;
|
|
86
|
+
// Encrypt chunk using Web Crypto API
|
|
87
|
+
const encrypted = new Uint8Array(await globalThis.crypto.subtle.encrypt({
|
|
88
|
+
name: ENCRYPTION_ALGORITHM,
|
|
89
|
+
counter: chunkCounter,
|
|
90
|
+
length: COUNTER_LENGTH,
|
|
91
|
+
}, cryptoKey, chunk));
|
|
92
|
+
controller.enqueue(encrypted);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
controller.error(error);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
// Note: No flush needed for AES-CTR (unlike CBC which needs final padding)
|
|
99
|
+
});
|
|
100
|
+
const encryptedStream = data.stream().pipeThrough(encryptTransform);
|
|
101
|
+
return { key, iv, encryptedStream };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Decrypt a stream of data using AES-CTR with TRUE streaming (no buffering).
|
|
105
|
+
*
|
|
106
|
+
* @param {ReadableStream} encryptedData The encrypted data stream.
|
|
107
|
+
* @param {Uint8Array} key The encryption key.
|
|
108
|
+
* @param {Uint8Array} iv The initialization vector (counter).
|
|
109
|
+
* @returns {Promise<ReadableStream>} A stream of decrypted data.
|
|
110
|
+
*/
|
|
111
|
+
async decryptStream(encryptedData, key, iv) {
|
|
112
|
+
// Pre-import the crypto key for reuse across chunks
|
|
113
|
+
const cryptoKey = await globalThis.crypto.subtle.importKey('raw', key, { name: ENCRYPTION_ALGORITHM }, false, ['encrypt', 'decrypt']);
|
|
114
|
+
// State for AES-CTR counter management
|
|
115
|
+
let counter = new Uint8Array(iv);
|
|
116
|
+
let totalBlocks = 0; // Track total blocks processed (CRITICAL for security)
|
|
117
|
+
// Create TransformStream (inspired by Node.js approach)
|
|
118
|
+
const decryptTransform = new TransformStream({
|
|
119
|
+
transform: async (chunk, controller) => {
|
|
120
|
+
try {
|
|
121
|
+
// SECURITY: Calculate counter based on total blocks, not chunks
|
|
122
|
+
const chunkCounter = this.incrementCounter(counter, totalBlocks);
|
|
123
|
+
// SECURITY: Increment by blocks in this chunk (16 bytes per block)
|
|
124
|
+
const blocksInChunk = Math.ceil(chunk.length / 16);
|
|
125
|
+
totalBlocks += blocksInChunk;
|
|
126
|
+
// Decrypt chunk using Web Crypto API
|
|
127
|
+
const decrypted = new Uint8Array(await globalThis.crypto.subtle.decrypt({
|
|
128
|
+
name: ENCRYPTION_ALGORITHM,
|
|
129
|
+
counter: chunkCounter,
|
|
130
|
+
length: COUNTER_LENGTH,
|
|
131
|
+
}, cryptoKey, chunk));
|
|
132
|
+
controller.enqueue(decrypted);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
controller.error(error);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
// Note: No flush needed for AES-CTR (unlike CBC which needs final padding)
|
|
139
|
+
});
|
|
140
|
+
return encryptedData.pipeThrough(decryptTransform);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Combine key and IV into a single array for AES-CTR
|
|
144
|
+
*
|
|
145
|
+
* @param {Uint8Array} key - The AES key (KEY_LENGTH/8 bytes)
|
|
146
|
+
* @param {Uint8Array} iv - The AES-CTR IV (IV_LENGTH bytes)
|
|
147
|
+
* @returns {Uint8Array} Combined key and IV (KEY_LENGTH/8 + IV_LENGTH bytes)
|
|
148
|
+
*/
|
|
149
|
+
combineKeyAndIV(key, iv) {
|
|
150
|
+
const keyBytes = KEY_LENGTH / 8;
|
|
151
|
+
if (key.length !== keyBytes) {
|
|
152
|
+
throw new Error(`AES-${KEY_LENGTH} key must be ${keyBytes} bytes, got ${key.length}`);
|
|
153
|
+
}
|
|
154
|
+
if (iv.length !== IV_LENGTH) {
|
|
155
|
+
throw new Error(`AES-CTR IV must be ${IV_LENGTH} bytes, got ${iv.length}`);
|
|
156
|
+
}
|
|
157
|
+
return new Uint8Array([...key, ...iv]);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Split combined key and IV for AES-CTR
|
|
161
|
+
*
|
|
162
|
+
* @param {Uint8Array} combined - Combined key and IV (KEY_LENGTH/8 + IV_LENGTH bytes)
|
|
163
|
+
* @returns {{ key: Uint8Array, iv: Uint8Array }} Separated key and IV
|
|
164
|
+
*/
|
|
165
|
+
splitKeyAndIV(combined) {
|
|
166
|
+
const keyBytes = KEY_LENGTH / 8;
|
|
167
|
+
const expectedLength = keyBytes + IV_LENGTH;
|
|
168
|
+
if (combined.length !== expectedLength) {
|
|
169
|
+
throw new Error(`AES-${KEY_LENGTH}-CTR combined key+IV must be ${expectedLength} bytes, got ${combined.length}`);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
key: combined.subarray(0, keyBytes),
|
|
173
|
+
iv: combined.subarray(keyBytes, keyBytes + IV_LENGTH),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=generic-aes-ctr-streaming-crypto.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NodeAesCbcCrypto implements AES-CBC symmetric encryption for Node.js environments.
|
|
3
|
+
* It uses AES-CBC mode for encryption via the Node.js crypto module.
|
|
4
|
+
* If you already encrypted a file with this class, you still need to use this class to decrypt it.
|
|
5
|
+
*
|
|
6
|
+
* @deprecated Use GenericAesCtrStreamingCrypto instead for new uploads
|
|
7
|
+
* @class
|
|
8
|
+
* @implements {Type.SymmetricCrypto}
|
|
9
|
+
*/
|
|
10
|
+
export class NodeAesCbcCrypto implements Type.SymmetricCrypto {
|
|
11
|
+
/** @param {Type.BlobLike} data */
|
|
12
|
+
encryptStream(data: Type.BlobLike): Promise<{
|
|
13
|
+
key: Buffer<ArrayBufferLike>;
|
|
14
|
+
iv: Buffer<ArrayBufferLike>;
|
|
15
|
+
encryptedStream: ReadableStream<any>;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* @param {ReadableStream} encryptedData
|
|
19
|
+
* @param {Uint8Array} key
|
|
20
|
+
* @param {Uint8Array} iv
|
|
21
|
+
*/
|
|
22
|
+
decryptStream(encryptedData: ReadableStream, key: Uint8Array, iv: Uint8Array): Promise<ReadableStream<any>>;
|
|
23
|
+
/**
|
|
24
|
+
* Combine key and IV into a single array for AES-CBC
|
|
25
|
+
*
|
|
26
|
+
* @param {Uint8Array} key - The AES key (KEY_LENGTH/8 bytes)
|
|
27
|
+
* @param {Uint8Array} iv - The AES-CBC IV (IV_LENGTH bytes)
|
|
28
|
+
* @returns {Uint8Array} Combined key and IV (KEY_LENGTH/8 + IV_LENGTH bytes)
|
|
29
|
+
*/
|
|
30
|
+
combineKeyAndIV(key: Uint8Array, iv: Uint8Array): Uint8Array;
|
|
31
|
+
/**
|
|
32
|
+
* Split combined key and IV for AES-CBC
|
|
33
|
+
*
|
|
34
|
+
* @param {Uint8Array} combined - Combined key and IV (KEY_LENGTH/8 + IV_LENGTH bytes)
|
|
35
|
+
* @returns {{ key: Uint8Array, iv: Uint8Array }} Separated key and IV
|
|
36
|
+
*/
|
|
37
|
+
splitKeyAndIV(combined: Uint8Array): {
|
|
38
|
+
key: Uint8Array;
|
|
39
|
+
iv: Uint8Array;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
import * as Type from '../../types.js';
|
|
43
|
+
//# sourceMappingURL=node-aes-cbc-crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-aes-cbc-crypto.d.ts","sourceRoot":"","sources":["../../../src/crypto/symmetric/node-aes-cbc-crypto.js"],"names":[],"mappings":"AAOA;;;;;;;;GAQG;AACH,yCAFgB,IAAI,CAAC,eAAe;IAGlC,mCAAmC;IACnC,oBADY,IAAI,CAAC,QAAQ;;;;OA+BxB;IAED;;;;OAIG;IACH,6BAJW,cAAc,OACd,UAAU,MACV,UAAU,gCA8BpB;IAED;;;;;;OAMG;IACH,qBAJW,UAAU,MACV,UAAU,GACR,UAAU,CAatB;IAED;;;;;OAKG;IACH,wBAHW,UAAU,GACR;QAAE,GAAG,EAAE,UAAU,CAAC;QAAC,EAAE,EAAE,UAAU,CAAA;KAAE,CAc/C;CACF;sBA3HqB,gBAAgB"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { randomBytes, createCipheriv, createDecipheriv } from 'crypto';
|
|
2
|
+
import * as Type from '../../types.js';
|
|
3
|
+
const ENCRYPTION_ALGORITHM = 'aes-256-cbc';
|
|
4
|
+
const KEY_LENGTH = 256; // bits
|
|
5
|
+
const IV_LENGTH = 16; // bytes (128 bits, used as initialization vector)
|
|
6
|
+
/**
|
|
7
|
+
* NodeAesCbcCrypto implements AES-CBC symmetric encryption for Node.js environments.
|
|
8
|
+
* It uses AES-CBC mode for encryption via the Node.js crypto module.
|
|
9
|
+
* If you already encrypted a file with this class, you still need to use this class to decrypt it.
|
|
10
|
+
*
|
|
11
|
+
* @deprecated Use GenericAesCtrStreamingCrypto instead for new uploads
|
|
12
|
+
* @class
|
|
13
|
+
* @implements {Type.SymmetricCrypto}
|
|
14
|
+
*/
|
|
15
|
+
export class NodeAesCbcCrypto {
|
|
16
|
+
/** @param {Type.BlobLike} data */
|
|
17
|
+
async encryptStream(data) {
|
|
18
|
+
const symmetricKey = randomBytes(KEY_LENGTH / 8); // KEY_LENGTH bits for AES
|
|
19
|
+
const initializationVector = randomBytes(IV_LENGTH); // IV_LENGTH bytes for AES
|
|
20
|
+
const cipher = createCipheriv(ENCRYPTION_ALGORITHM, symmetricKey, initializationVector);
|
|
21
|
+
const encryptStream = new TransformStream({
|
|
22
|
+
transform: async (chunk, controller) => {
|
|
23
|
+
const encryptedChunk = cipher.update(chunk);
|
|
24
|
+
if (encryptedChunk.length) {
|
|
25
|
+
controller.enqueue(encryptedChunk);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
flush: (controller) => {
|
|
29
|
+
const final = cipher.final();
|
|
30
|
+
if (final.length) {
|
|
31
|
+
controller.enqueue(final);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return Promise.resolve({
|
|
36
|
+
key: symmetricKey,
|
|
37
|
+
iv: initializationVector,
|
|
38
|
+
encryptedStream: data.stream().pipeThrough(encryptStream),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @param {ReadableStream} encryptedData
|
|
43
|
+
* @param {Uint8Array} key
|
|
44
|
+
* @param {Uint8Array} iv
|
|
45
|
+
*/
|
|
46
|
+
async decryptStream(encryptedData, key, iv) {
|
|
47
|
+
const decipher = createDecipheriv(ENCRYPTION_ALGORITHM, key, iv);
|
|
48
|
+
const decryptor = new TransformStream({
|
|
49
|
+
async transform(chunk, controller) {
|
|
50
|
+
try {
|
|
51
|
+
const decryptedChunk = decipher.update(chunk);
|
|
52
|
+
if (decryptedChunk.length > 0) {
|
|
53
|
+
controller.enqueue(decryptedChunk);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
controller.error(err);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
flush(controller) {
|
|
61
|
+
try {
|
|
62
|
+
const finalChunk = decipher.final();
|
|
63
|
+
if (finalChunk.length > 0) {
|
|
64
|
+
controller.enqueue(finalChunk);
|
|
65
|
+
}
|
|
66
|
+
controller.terminate();
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
controller.error(err);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
return Promise.resolve(encryptedData.pipeThrough(decryptor));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Combine key and IV into a single array for AES-CBC
|
|
77
|
+
*
|
|
78
|
+
* @param {Uint8Array} key - The AES key (KEY_LENGTH/8 bytes)
|
|
79
|
+
* @param {Uint8Array} iv - The AES-CBC IV (IV_LENGTH bytes)
|
|
80
|
+
* @returns {Uint8Array} Combined key and IV (KEY_LENGTH/8 + IV_LENGTH bytes)
|
|
81
|
+
*/
|
|
82
|
+
combineKeyAndIV(key, iv) {
|
|
83
|
+
const keyBytes = KEY_LENGTH / 8;
|
|
84
|
+
if (key.length !== keyBytes) {
|
|
85
|
+
throw new Error(`AES-${KEY_LENGTH} key must be ${keyBytes} bytes, got ${key.length}`);
|
|
86
|
+
}
|
|
87
|
+
if (iv.length !== IV_LENGTH) {
|
|
88
|
+
throw new Error(`AES-CBC IV must be ${IV_LENGTH} bytes, got ${iv.length}`);
|
|
89
|
+
}
|
|
90
|
+
return new Uint8Array([...key, ...iv]);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Split combined key and IV for AES-CBC
|
|
94
|
+
*
|
|
95
|
+
* @param {Uint8Array} combined - Combined key and IV (KEY_LENGTH/8 + IV_LENGTH bytes)
|
|
96
|
+
* @returns {{ key: Uint8Array, iv: Uint8Array }} Separated key and IV
|
|
97
|
+
*/
|
|
98
|
+
splitKeyAndIV(combined) {
|
|
99
|
+
const keyBytes = KEY_LENGTH / 8;
|
|
100
|
+
const expectedLength = keyBytes + IV_LENGTH;
|
|
101
|
+
if (combined.length !== expectedLength) {
|
|
102
|
+
throw new Error(`AES-${KEY_LENGTH}-CBC combined key+IV must be ${expectedLength} bytes, got ${combined.length}`);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
key: combined.subarray(0, keyBytes),
|
|
106
|
+
iv: combined.subarray(keyBytes, keyBytes + IV_LENGTH),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=node-aes-cbc-crypto.js.map
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Decrypt file content using the decrypted symmetric key and IV.
|
|
3
|
+
*
|
|
2
4
|
* @param {Type.CryptoAdapter} cryptoAdapter - The crypto adapter responsible for performing
|
|
3
5
|
* encryption and decryption operations.
|
|
4
|
-
* @param {
|
|
5
|
-
* @param {Uint8Array}
|
|
6
|
+
* @param {Uint8Array} key - The symmetric key
|
|
7
|
+
* @param {Uint8Array} iv - The initialization vector
|
|
8
|
+
* @param {Uint8Array} content - The encrypted file content
|
|
9
|
+
* @returns {Promise<ReadableStream>} The decrypted file stream
|
|
6
10
|
*/
|
|
7
|
-
export function decryptFileWithKey(cryptoAdapter: Type.CryptoAdapter,
|
|
8
|
-
export function retrieveAndDecrypt(storachaClient: import("@storacha/client").Client,
|
|
11
|
+
export function decryptFileWithKey(cryptoAdapter: Type.CryptoAdapter, key: Uint8Array, iv: Uint8Array, content: Uint8Array): Promise<ReadableStream>;
|
|
12
|
+
export function retrieveAndDecrypt(storachaClient: import("@storacha/client").Client, cryptoAdapter: Type.CryptoAdapter, gatewayURL: URL, cid: Type.AnyLink, delegationCAR: Uint8Array, decryptionOptions: Type.DecryptionOptions): Promise<ReadableStream>;
|
|
13
|
+
export function getCarFileFromPublicGateway(gatewayURL: URL, cid: string): Promise<Uint8Array>;
|
|
9
14
|
import * as Type from '../types.js';
|
|
10
15
|
//# sourceMappingURL=decrypt-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decrypt-handler.d.ts","sourceRoot":"","sources":["../../src/handlers/decrypt-handler.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"decrypt-handler.d.ts","sourceRoot":"","sources":["../../src/handlers/decrypt-handler.js"],"names":[],"mappings":"AA4DA;;;;;;;;;GASG;AACH,kDAPW,IAAI,CAAC,aAAa,OAElB,UAAU,MACV,UAAU,WACV,UAAU,GACR,OAAO,CAAC,cAAc,CAAC,CAanC;AA9DM,mDATI,OAAO,kBAAkB,EAAE,MAAM,iBACjC,IAAI,CAAC,aAAa,cAElB,GAAG,OACH,IAAI,CAAC,OAAO,iBACZ,UAAU,qBACV,IAAI,CAAC,iBAAiB,GACpB,OAAO,CAAC,cAAc,CAAC,CAyCnC;AAoCM,wDAJI,GAAG,OACH,MAAM,GACJ,OAAO,CAAC,UAAU,CAAC,CA+B/B;sBAtHqB,aAAa"}
|
|
@@ -1,144 +1,113 @@
|
|
|
1
1
|
import { CID } from 'multiformats';
|
|
2
|
-
import { CarIndexer } from '@ipld/car';
|
|
2
|
+
import { CarIndexer, CarReader } from '@ipld/car';
|
|
3
3
|
import { exporter } from 'ipfs-unixfs-exporter';
|
|
4
4
|
import { MemoryBlockstore } from 'blockstore-core';
|
|
5
|
-
import { base64 } from 'multiformats/bases/base64';
|
|
6
|
-
import * as Lit from '../protocols/lit.js';
|
|
7
5
|
import * as Type from '../types.js';
|
|
8
|
-
import * as EncryptedMetadata from '../core/encrypted-metadata.js';
|
|
9
|
-
import { createDecryptWrappedInvocation } from '../utils.js';
|
|
10
6
|
/**
|
|
11
|
-
* Retrieve and decrypt a file from the IPFS gateway.
|
|
7
|
+
* Retrieve and decrypt a file from the IPFS gateway using any supported encryption strategy.
|
|
12
8
|
*
|
|
13
9
|
* @param {import('@storacha/client').Client} storachaClient - The Storacha client
|
|
14
|
-
* @param {import('@lit-protocol/lit-node-client').LitNodeClient} litClient - The Lit client
|
|
15
10
|
* @param {Type.CryptoAdapter} cryptoAdapter - The crypto adapter responsible for performing
|
|
16
11
|
* encryption and decryption operations.
|
|
17
12
|
* @param {URL} gatewayURL - The IPFS gateway URL
|
|
18
|
-
* @param {Type.LitWalletSigner | Type.LitPkpSigner} signer - The wallet or PKP key signer to decrypt the file
|
|
19
13
|
* @param {Type.AnyLink} cid - The link to the file to retrieve
|
|
20
|
-
* @param {Uint8Array} delegationCAR - The delegation that gives permission to decrypt
|
|
14
|
+
* @param {Uint8Array} delegationCAR - The delegation that gives permission to decrypt (required for both strategies)
|
|
15
|
+
* @param {Type.DecryptionOptions} decryptionOptions - User-provided decryption options
|
|
16
|
+
* @returns {Promise<ReadableStream>} The decrypted file stream
|
|
21
17
|
*/
|
|
22
|
-
export const retrieveAndDecrypt = async (storachaClient,
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/** @type import('@lit-protocol/types').AccessControlConditions */ (
|
|
35
|
-
/** @type {unknown} */ (accessControlConditions));
|
|
36
|
-
const expiration = new Date(Date.now() + 1000 * 60 * 5).toISOString(); // 5 min
|
|
37
|
-
// TODO: store the session signature (https://developer.litprotocol.com/intro/first-request/generating-session-sigs#nodejs)
|
|
38
|
-
let sessionSigs;
|
|
39
|
-
if ('wallet' in signer) {
|
|
40
|
-
sessionSigs = await Lit.getSessionSigs(litClient, {
|
|
41
|
-
wallet: signer.wallet,
|
|
42
|
-
dataToEncryptHash: plaintextKeyHash,
|
|
43
|
-
expiration,
|
|
44
|
-
accessControlConditions: acc,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
sessionSigs = await Lit.getPkpSessionSigs(litClient, {
|
|
49
|
-
pkpPublicKey: signer.pkpPublicKey,
|
|
50
|
-
authMethod: signer.authMethod,
|
|
51
|
-
dataToEncryptHash: plaintextKeyHash,
|
|
52
|
-
expiration,
|
|
53
|
-
accessControlConditions: acc,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
const wrappedInvocationJSON = await createDecryptWrappedInvocation({
|
|
18
|
+
export const retrieveAndDecrypt = async (storachaClient, cryptoAdapter, gatewayURL, cid, delegationCAR, decryptionOptions) => {
|
|
19
|
+
// Step 1: Get the encrypted metadata from the public gateway
|
|
20
|
+
const encryptedMetadataCar = await getCarFileFromPublicGateway(gatewayURL, cid.toString());
|
|
21
|
+
// Step 2: Extract encrypted metadata from the CAR file
|
|
22
|
+
const metadata = cryptoAdapter.extractEncryptedMetadata(encryptedMetadataCar);
|
|
23
|
+
// Step 3: Get the encrypted data from the CAR file
|
|
24
|
+
const encryptedData = await getEncryptedDataFromCar(encryptedMetadataCar, metadata.encryptedDataCID);
|
|
25
|
+
// Step 4: Decrypt the encrypted symmetric key
|
|
26
|
+
const encryptedSymmetricKey = cryptoAdapter.getEncryptedKey(metadata);
|
|
27
|
+
const { key, iv } = await cryptoAdapter.decryptSymmetricKey(encryptedSymmetricKey, {
|
|
28
|
+
decryptionOptions,
|
|
29
|
+
metadata,
|
|
57
30
|
delegationCAR,
|
|
58
|
-
spaceDID,
|
|
59
31
|
resourceCID: cid,
|
|
60
32
|
issuer: storachaClient.agent.issuer,
|
|
61
33
|
audience: storachaClient.defaultProvider(),
|
|
62
|
-
expiration: new Date(Date.now() + 1000 * 60 * 10).getTime(), // 10 min
|
|
63
34
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
spaceDID,
|
|
67
|
-
identityBoundCiphertext,
|
|
68
|
-
plaintextKeyHash,
|
|
69
|
-
accessControlConditions,
|
|
70
|
-
wrappedInvocationJSON,
|
|
71
|
-
});
|
|
72
|
-
return decryptFileWithKey(cryptoAdapter, decryptKey, encryptedData);
|
|
35
|
+
// Step 5: Decrypt the encrypted file content using the decrypted symmetric key and IV
|
|
36
|
+
return decryptFileWithKey(cryptoAdapter, key, iv, encryptedData);
|
|
73
37
|
};
|
|
74
38
|
/**
|
|
39
|
+
* Decrypt file content using the decrypted symmetric key and IV.
|
|
40
|
+
*
|
|
75
41
|
* @param {Type.CryptoAdapter} cryptoAdapter - The crypto adapter responsible for performing
|
|
76
42
|
* encryption and decryption operations.
|
|
77
|
-
* @param {
|
|
78
|
-
* @param {Uint8Array}
|
|
43
|
+
* @param {Uint8Array} key - The symmetric key
|
|
44
|
+
* @param {Uint8Array} iv - The initialization vector
|
|
45
|
+
* @param {Uint8Array} content - The encrypted file content
|
|
46
|
+
* @returns {Promise<ReadableStream>} The decrypted file stream
|
|
79
47
|
*/
|
|
80
|
-
export function decryptFileWithKey(cryptoAdapter,
|
|
81
|
-
// Split the decrypted data back into key and initializationVector
|
|
82
|
-
const decryptedKeyData = base64.decode(combinedKey);
|
|
83
|
-
const symmetricKey = decryptedKeyData.subarray(0, 32);
|
|
84
|
-
const initializationVector = decryptedKeyData.subarray(32);
|
|
85
|
-
// Create a ReadableStream from the Uint8Array
|
|
48
|
+
export function decryptFileWithKey(cryptoAdapter, key, iv, content) {
|
|
86
49
|
const contentStream = new ReadableStream({
|
|
87
50
|
start(controller) {
|
|
88
51
|
controller.enqueue(content);
|
|
89
52
|
controller.close();
|
|
90
53
|
},
|
|
91
54
|
});
|
|
92
|
-
const decryptedStream = cryptoAdapter.decryptStream(contentStream,
|
|
55
|
+
const decryptedStream = cryptoAdapter.decryptStream(contentStream, key, iv);
|
|
93
56
|
return decryptedStream;
|
|
94
57
|
}
|
|
95
58
|
/**
|
|
59
|
+
* Fetch a CAR file from the public IPFS gateway with root CID verification.
|
|
96
60
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
61
|
+
* SECURITY: This function provides metadata integrity protection (P0.2).
|
|
62
|
+
* Verifies the returned CAR matches the requested CID to prevent metadata tampering.
|
|
63
|
+
* Content integrity (P2.2) is handled by existing IPFS tools in getEncryptedDataFromCar.
|
|
64
|
+
*
|
|
65
|
+
* @param {URL} gatewayURL - The IPFS gateway URL
|
|
66
|
+
* @param {string} cid - The CID to fetch
|
|
67
|
+
* @returns {Promise<Uint8Array>} The verified CAR file bytes
|
|
99
68
|
*/
|
|
100
|
-
const
|
|
69
|
+
export const getCarFileFromPublicGateway = async (gatewayURL, cid) => {
|
|
101
70
|
const url = new URL(`/ipfs/${cid}?format=car`, gatewayURL);
|
|
102
71
|
const response = await fetch(url);
|
|
103
72
|
if (!response.ok) {
|
|
104
73
|
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
|
|
105
74
|
}
|
|
106
75
|
const car = new Uint8Array(await response.arrayBuffer());
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
throw encryptedContentResult.error;
|
|
76
|
+
// SECURITY: Verify the CAR's root CID matches what we requested
|
|
77
|
+
const reader = await CarReader.fromBytes(car);
|
|
78
|
+
const roots = await reader.getRoots();
|
|
79
|
+
const expectedCID = CID.parse(cid);
|
|
80
|
+
if (roots.length !== 1) {
|
|
81
|
+
throw new Error(`CAR file must have exactly one root CID, found ${roots.length}`);
|
|
82
|
+
}
|
|
83
|
+
if (!roots[0].equals(expectedCID)) {
|
|
84
|
+
throw new Error(`CID verification failed: expected ${expectedCID} but CAR contains ${roots[0]}`);
|
|
117
85
|
}
|
|
118
|
-
|
|
119
|
-
return encryptedContent;
|
|
86
|
+
return car;
|
|
120
87
|
};
|
|
121
88
|
/**
|
|
89
|
+
* Extract encrypted data from a CAR file.
|
|
122
90
|
*
|
|
123
|
-
* @param {Uint8Array} car
|
|
124
|
-
* @param {string} encryptedDataCID
|
|
91
|
+
* @param {Uint8Array} car - The CAR file bytes
|
|
92
|
+
* @param {string} encryptedDataCID - The CID of the encrypted data
|
|
93
|
+
* @returns {Promise<Uint8Array>} The encrypted data bytes
|
|
125
94
|
*/
|
|
126
95
|
const getEncryptedDataFromCar = async (car, encryptedDataCID) => {
|
|
127
|
-
//
|
|
96
|
+
// Step 1: Index the CAR file for efficient block lookup
|
|
128
97
|
const iterable = await CarIndexer.fromBytes(car);
|
|
129
|
-
const
|
|
98
|
+
const blockIndex = new Map();
|
|
130
99
|
for await (const { cid, blockLength, blockOffset } of iterable) {
|
|
131
|
-
|
|
132
|
-
await blockstore.put(cid, blockBytes);
|
|
100
|
+
blockIndex.set(cid.toString(), { blockOffset, blockLength });
|
|
133
101
|
}
|
|
134
|
-
//
|
|
102
|
+
// Step 2: Use the index to extract the encrypted data block bytes as needed
|
|
103
|
+
const { blockOffset, blockLength } = blockIndex.get(encryptedDataCID);
|
|
104
|
+
const blockBytes = car.subarray(blockOffset, blockOffset + blockLength);
|
|
105
|
+
// Step 3: Put the block in a blockstore for exporter compatibility
|
|
106
|
+
const blockstore = new MemoryBlockstore();
|
|
107
|
+
await blockstore.put(CID.parse(encryptedDataCID), blockBytes);
|
|
108
|
+
// Step 4: Get the encrypted data from the CAR file
|
|
135
109
|
const encryptedDataEntry = await exporter(CID.parse(encryptedDataCID), blockstore);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
for await (const chunk of encryptedDataEntry.content()) {
|
|
139
|
-
encryptedDataBytes.set(chunk, offset);
|
|
140
|
-
offset += chunk.length;
|
|
141
|
-
}
|
|
142
|
-
return encryptedDataBytes;
|
|
110
|
+
// Step 5: Return the async iterable (stream of chunks)
|
|
111
|
+
return encryptedDataEntry.content(); // async iterable of Uint8Array
|
|
143
112
|
};
|
|
144
113
|
//# sourceMappingURL=decrypt-handler.js.map
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export function encryptAndUpload(storachaClient: import("@storacha/client").Client,
|
|
1
|
+
export function encryptAndUpload(storachaClient: import("@storacha/client").Client, cryptoAdapter: Type.CryptoAdapter, file: Type.BlobLike, encryptionConfig: Type.EncryptionConfig, uploadOptions?: Type.UploadOptions): Promise<Type.AnyLink>;
|
|
2
2
|
import * as Type from '../types.js';
|
|
3
3
|
//# sourceMappingURL=encrypt-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encrypt-handler.d.ts","sourceRoot":"","sources":["../../src/handlers/encrypt-handler.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"encrypt-handler.d.ts","sourceRoot":"","sources":["../../src/handlers/encrypt-handler.js"],"names":[],"mappings":"AAgBO,iDARI,OAAO,kBAAkB,EAAE,MAAM,iBACjC,IAAI,CAAC,aAAa,QAElB,IAAI,CAAC,QAAQ,oBACb,IAAI,CAAC,gBAAgB,kBACrB,IAAI,CAAC,aAAa,GAChB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CA6BjC;sBAxCqB,aAAa"}
|