如何在Node.js视频流服务器中实现动态解密?Electron LMS场景求助
问题分析与解决方案
你的核心问题在于使用AES-CBC模式处理视频流的范围请求——CBC模式不支持随机访问,必须按顺序解密所有前置块才能正确解密目标块,这直接导致了WRONG_FINAL_BLOCK_LENGTH和BAD_DECRYPT错误。此外,你未将加密流通过解密器管道传输,也未保存原始视频尺寸,导致响应头信息错误。
以下是修复后的完整方案,同时推荐更适合视频场景的加密模式和优化方案:
1. 切换到AES-256-CTR模式(支持随机访问)
CTR模式将块密码转换为流密码,允许直接解密任意位置的字节,完美适配视频流的范围请求需求。
修复后的加密函数
新增保存原始视频尺寸和随机IV的逻辑(IV需唯一,保障安全性):
import { createCipheriv, randomBytes } from "crypto"; import { createReadStream, createWriteStream, statSync } from "fs"; function encryptFile(filePath: string, encryptedPath: string, secretKey: string) { const originalSize = statSync(filePath).size; const iv = randomBytes(16); // 生成随机16字节IV const sizeBuffer = Buffer.alloc(8); sizeBuffer.writeBigUInt64BE(BigInt(originalSize)); const cipher = createCipheriv("aes-256-ctr", Buffer.from(secretKey, "hex"), iv); const input = createReadStream(filePath); const output = createWriteStream(encryptedPath); // 先写入IV和原始尺寸作为元数据 output.write(iv); output.write(sizeBuffer); // 再写入加密后的视频数据 input.pipe(cipher).pipe(output); output.on("finish", () => { console.log("文件加密完成"); }); }
修复后的视频流服务器函数
处理范围请求时,计算正确的解密起始位置,并通过CTR模式直接解密目标块:
import { createDecipheriv } from "crypto"; import { IpcMainInvokeEvent } from "electron"; import { createReadStream, readFileSync } from "fs"; import { createServer } from "http"; function startVideoServer(_event: IpcMainInvokeEvent, encryptedFilePath: string) { encryptedFilePath = "C:\\Users\\mikha\\AppData\\Roaming\\dr-youssef-nagah\\videos\\contentProtected.mp4.enc"; // 测试路径 return new Promise((resolve, reject) => { try { // 读取元数据:16字节IV + 8字节原始尺寸 const metadata = readFileSync(encryptedFilePath, { length: 24 }); const iv = metadata.slice(0, 16); const originalSize = Number(metadata.slice(16, 24).readBigUInt64BE()); const BLOCK_SIZE = 16; const METADATA_SIZE = 24; const server = createServer((req, res) => { let start = 0; let end = originalSize - 1; let contentLength = originalSize; let statusCode = 200; const headers: Record<string, string> = { "Content-Type": "video/mp4", "Content-Length": contentLength.toString(), "Accept-Ranges": "bytes", }; // 处理范围请求 if (req.headers.range) { const parts = req.headers.range.replace(/bytes=/, "").split("-"); start = parseInt(parts[0], 10); end = parts[1] ? parseInt(parts[1], 10) : originalSize - 1; // 修正范围到合法区间 start = Math.max(0, Math.min(start, originalSize - 1)); end = Math.max(start, Math.min(end, originalSize - 1)); contentLength = end - start + 1; statusCode = 206; Object.assign(headers, { "Content-Range": `bytes ${start}-${end}/${originalSize}`, "Content-Length": contentLength.toString(), }); } res.writeHead(statusCode, headers); // 计算CTR模式的起始计数器偏移 const blockOffset = Math.floor(start / BLOCK_SIZE); // 将IV转换为BigInt并添加偏移量 const ivHigh = iv.readBigUInt64BE(0); const ivLow = iv.readBigUInt64BE(8); let ivInt = (ivHigh << BigInt(64)) | ivLow; ivInt += BigInt(blockOffset); // 生成新的IV用于起始位置解密 const newIV = Buffer.alloc(16); newIV.writeBigUInt64BE(ivInt >> BigInt(64), 0); newIV.writeBigUInt64BE(ivInt & BigInt(0xFFFFFFFFFFFFFFFF), 8); // 创建解密器 const decipher = createDecipheriv( "aes-256-ctr", Buffer.from("your-secret-key", "hex"), newIV ); decipher.setAutoPadding(false); // CTR模式不需要填充 // 读取加密文件的目标片段(跳过元数据) const stream = createReadStream(encryptedFilePath, { start: METADATA_SIZE + start, end: METADATA_SIZE + end, }); // 管道传输:加密流 -> 解密器 -> 响应 stream.pipe(decipher).pipe(res); // 错误处理 stream.on("error", (err) => { console.error("文件读取错误:", err); if (!res.writableEnded) res.end("读取视频失败"); }); decipher.on("error", (err) => { console.error("解密错误:", err); if (!res.writableEnded) res.end("解密视频失败"); }); }); server.listen(0, "127.0.0.1", () => { const port = server.address()?.port; console.log(`视频服务器运行于 http://127.0.0.1:${port}`); resolve(JSON.stringify({ port, server })); }); } catch (err) { reject(err); } }); } export default startVideoServer;
2. 额外优化建议
(1)使用Electron自定义协议替代HTTP服务器
避免启动本地HTTP服务器,直接通过自定义协议处理加密视频流,更符合Electron的安全模型:
// 主进程中注册自定义协议 import { protocol } from "electron"; protocol.registerStreamProtocol("encrypted-video", (request, callback) => { const url = new URL(request.url); const encryptedPath = decodeURIComponent(url.pathname); // 此处复用上述服务器中的元数据读取、解密逻辑 // 最终返回decipher流作为响应数据 callback({ statusCode: 200, headers: { "Content-Type": "video/mp4" }, data: decryptedStream }); }); // 渲染进程中使用 <video src="encrypted-video:///path/to/encrypted/file.enc"></video>
(2)安全存储密钥
不要硬编码密钥,使用Electron的keytar库将密钥存储到系统密钥链中:
import keytar from "keytar"; // 存储密钥 await keytar.setPassword("YourLMSApp", "encryption-key", "your-secret-key"); // 获取密钥 const secretKey = await keytar.getPassword("YourLMSApp", "encryption-key");
(3)添加文件完整性校验
在加密文件末尾添加HMAC哈希,解密前验证文件未被篡改:
// 加密时添加HMAC const hmac = createHmac("sha256", secretKey); input.pipe(cipher).pipe(hmac).pipe(output); output.write(hmac.digest()); // 解密前验证HMAC const storedHmac = readFileSync(encryptedPath, { start: encryptedSize - 32, end: encryptedSize -1 }); const computedHmac = createHmac("sha256", secretKey).update(encryptedData).digest(); if (!crypto.timingSafeEqual(storedHmac, computedHmac)) { throw new Error("文件已被篡改"); }
内容的提问来源于stack exchange,提问作者Mikhael Mounay
相关产品推荐
相关产品推荐

