如何在浏览器中流式解密从IPFS获取的AES-256-CTR加密文件?
浏览器中AES-256-CTR流式解密IPFS文件方案
问题背景
我通过Node.js将本地文件流式加密后上传到IPFS,加密代码如下:
const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-ctr', key, iv, null); const input = fs.createReadStream(inputFilePath); const storage = makeStorageClient(); const keyString = new Int32Array(key).toString(); const cid = await storage.put([{ name: fileName, stream: () => input.pipe(cipher) }]);
现在需要在浏览器中逆向操作:从IPFS获取文件(已转为ArrayBuffer),流式解密后保存到本地。浏览器无法使用Node.js的crypto模块,原本想这么实现但行不通:
const retrieved = await ipfs.cat(cid+'/'+link.name); const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv, null); const input = new ReadableStream(retrieved); const output = streamSaver.createWriteStream(link.name); const finished = await input.pipe(decipher).pipe(output);
尝试用浏览器的SubtleCrypto.decrypt,但AES-256-CTR需要传入包含counter和length的参数对象,不知道怎么获取这两个参数,同时想找更优的浏览器流式解密方案。
解决方案
1. AES-256-CTR参数对应关系
- counter:就是你加密时生成的
iv。Node.js的aes-256-ctr模式中,传入的iv本质就是CTR模式的初始计数器,两者完全等价,直接把加密时的iv转成ArrayBuffer传入即可。 - length:计数器的比特长度,Node.js的
aes-256-ctr默认使用128位(16字节)的计数器,所以这里填128就行。
注意:加密时的
iv必须和解密时一致,所以你需要把iv和CID、密钥一起保存(比如作为IPFS文件的元数据,或者单独存储),否则无法完成解密。
2. 浏览器流式解密实现
SubtleCrypto.decrypt本身不支持流式处理,但可以通过TransformStream逐段处理数据流,结合CTR模式的特性(只要计数器连续递增,分段解密不影响结果)来实现流式解密。具体步骤和代码如下:
步骤1:准备密钥和IV
先把Node.js中使用的密钥(Buffer类型)转换为浏览器的CryptoKey对象,同时把IV转为ArrayBuffer:
// 假设key是从外部获取的Buffer/ArrayBuffer,iv是加密时保存的Buffer/ArrayBuffer async function getCryptoKey(rawKey) { return await window.crypto.subtle.importKey( 'raw', rawKey, { name: 'AES-CTR' }, false, ['decrypt'] ); } const cryptoKey = await getCryptoKey(key); const counterBuffer = iv.buffer.slice(iv.byteOffset, iv.byteOffset + iv.byteLength); // 转成ArrayBuffer
步骤2:实现流式解密的TransformStream
function createDecryptTransform(cryptoKey, initialCounter) { let counter = new DataView(initialCounter.slice(0)); // 复制初始计数器,避免修改原数据 let counterIncremented = false; return new TransformStream({ async transform(chunk, controller) { // CTR模式下,每处理一段数据后需要递增计数器 if (counterIncremented) { // 递增计数器(大端序) let carry = 1; for (let i = counter.byteLength - 1; i >= 0 && carry; i--) { const val = counter.getUint8(i) + carry; counter.setUint8(i, val & 0xff); carry = val >> 8; } } counterIncremented = true; // 构建解密参数 const params = { name: 'AES-CTR', counter: counter, length: 128 }; // 解密当前段 const decryptedChunk = await window.crypto.subtle.decrypt(params, cryptoKey, chunk); controller.enqueue(new Uint8Array(decryptedChunk)); } }); }
步骤3:IPFS数据流处理+保存到本地
async function decryptAndSaveFromIPFS(cid, fileName, cryptoKey, iv) { // 从IPFS获取数据流(ipfs.cat返回AsyncIterable,转成ReadableStream) const ipfsStream = await ipfs.cat(`${cid}/${fileName}`); const readableStream = new ReadableStream({ async start(controller) { for await (const chunk of ipfsStream) { controller.enqueue(chunk); } controller.close(); } }); // 创建解密转换流 const decryptStream = createDecryptTransform(cryptoKey, iv); // 创建保存文件的流(使用StreamSaver) const writeStream = streamSaver.createWriteStream(fileName); // 流式拼接处理 await readableStream.pipeThrough(decryptStream).pipeTo(writeStream); } // 调用示例 await decryptAndSaveFromIPFS(cid, link.name, cryptoKey, counterBuffer);
3. 优化建议
- 使用现成库简化开发:如果不想手动处理计数器递增,可以用
web-streams-polyfill或者crypto-browserify这类库,它们封装了流式加密/解密的逻辑,减少手动实现的出错概率。 - 元数据存储:加密时建议把
iv、密钥哈希(用于验证)等元数据和文件一起上传到IPFS(比如打包成一个目录),方便解密时直接获取。
内容的提问来源于stack exchange,提问作者Boris K
相关产品推荐
相关产品推荐

