You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 20:34:52