aardio 文档

aardio 范例: ==== 分块加密 + 流式上传 独立范例 ====

// ==== 分块加密 + 流式上传 独立范例 ====
import crypt.aes;
import inet.http;
import console;

/*────────────────────────────────────────────────────────────────
  背景知识:

  AES 块大小为 16 字节。PKCS7 填充规则:
  - 缺 N 字节就填充 N 个值为 N 的字节
  - 即使原始数据是 16 的倍数,也要追加一整块 16 字节的填充
  - 加密后大小 = ceil((原始大小 + 1) / 16) * 16

  crypt.aes 的 encrypt(buf, dataSize) 行为:
  - dataSize < buf 总长度 → final=true,应用 PKCS7 填充
  - dataSize == buf 总长度 → final=false,不填充
────────────────────────────────────────────────────────────────*/

// ██████████████████████████████████████████████████████████████████
// 第一部分:AES-128-ECB 分块加密(核心算法)
// ██████████████████████████████████████████████████████████████████

// 1. 创建测试文件(故意不对齐 16 字节边界)
var testFile = io.tmpname("aes_demo_", ".bin");
var rawSize = 1024 * 1024 + 7; // 1MB + 7 字节
var plaintext = string.repeat(rawSize, "X");
string.save(testFile, plaintext);
console.log("[1] 测试文件:", testFile);
console.log("    原始大小:", rawSize, "字节 (", math.floor(rawSize/16), "块 +", rawSize % 16, "字节)");

// 2. 生成 128 位随机密钥
var aesKey = string.random(16);
console.log("[2] AES 密钥:", string.hex(aesKey));

// 3. 计算加密后大小
var paddedSize = math.ceil((rawSize + 1) / 16) * 16;
console.log("[3] 加密后大小:", paddedSize, "字节 (含 PKCS7 填充)");
console.log("");

// ───────────────────────────────────────────────────────────────────
// 核心:AES-ECB 分块加密
// ───────────────────────────────────────────────────────────────────

var CHUNK = 0xA00000; // 10MB 分块缓冲区
var buf = raw.buffer(CHUNK);

var aesEnc = crypt.aes();
aesEnc.setPassword(aesKey);
aesEnc.setKeyParamMode(2/*_CRYPT_MODE_ECB*/);

var file = io.file(testFile, "rbR");
var encryptedChunks = {}; // 收集密文用于验证

while(true){
    var readSize = file.readBuffer(buf);
    if(readSize <= 0) break;

    // ★ 检测后面是否还有数据(file.read(0) 不移动文件指针)
    var hasMore = file.read(0) ? true : false;

    var encBlock, blockLen;

    if(!hasMore && readSize == CHUNK){
        // ── 情况A:最后一块恰好填满缓冲区 ──
        //    readSize == bufSize → final 不会触发
        //    解决:创建大 1 字节的临时缓冲区,使 readSize < bufSize
        var tmpBuf = raw.buffer(readSize + 1, buf);
        encBlock, blockLen = aesEnc.encrypt(tmpBuf, readSize);
        // tmpBuf 大小 = readSize+1,readSize < tmpBufSize → final=true
    }
    else{
        // ── 情况B:readSize < CHUNK → 自然是最后一块,final=true ──
        // ── 情况C:readSize == CHUNK 且后面还有 → final=false ──
        encBlock, blockLen = aesEnc.encrypt(buf, readSize);
    }

    table.push(encryptedChunks, raw.tostring(encBlock, 1, blockLen));
}

file.close();
var ciphertext = string.join(encryptedChunks);

console.log("[4] 分块加密完成,密文大小:", #ciphertext, "字节");

// 验证:解密还原
var aesDec = crypt.aes();
aesDec.setPassword(aesKey);
aesDec.setKeyParamMode(2/*_CRYPT_MODE_ECB*/);
var decrypted = aesDec.decrypt(ciphertext);
console.log("    解密验证:", decrypted == plaintext ? "✓ 通过" : "✗ 失败");
console.log("");

// ██████████████████████████████████████████████████████████████████
// 第二部分:流式 HTTP 上传(边读文件、边加密、边发送)
// ██████████████████████████████████████████████████████████████████
// 
// 方案A:onSend 回调(iLinkClient 使用的方案)
//   优点:代码最简洁,http.post 自动管理 HTTP 生命周期
//   用法:设 onSendBegin/onSend → http.post(url, null, headers)
//
// 方案B:手动分步(本文档演示的方案,更底层)
//   优点:不依赖回调,流程完全可控
//   用法:beginRequest → beginSendData → writeData → endSendData
//
// 下面演示方案B,方案A见文末附注( 或参考 web.rest.iLinkClient 的 upladFile 方法源代码 )

console.log("[5] 流式加密上传到 httpbin.org/post ...");

var http = inet.http();
http.setTimeouts(30000, 30000, 120000);

// 步骤1:打开连接
var ok, err, code = http.beginRequest("https://httpbin.org/post", "POST");
if(!ok) { console.error("beginRequest 失败", err); return; }

// 步骤2:设置请求头
http.writeHeader("Content-Type: application/octet-stream");

// 步骤3:告知服务器密文总长度
var ok, err, code = http.beginSendData(paddedSize);
if(!ok) { console.error("beginSendData 失败", err); http.endRequest(); return; }

// 步骤4:循环发送加密数据块
var file2 = io.file(testFile, "rbR");
var buf2 = raw.buffer(CHUNK);
var aes2 = crypt.aes();
aes2.setPassword(aesKey);
aes2.setKeyParamMode(2/*_CRYPT_MODE_ECB*/);
var sentBytes = 0;

while(true){
    var readSize = file2.readBuffer(buf2);
    if(readSize <= 0) break;

    var hasMore = file2.read(0) ? true : false;
    var encBlock, blockLen;

    if(!hasMore && readSize == CHUNK){
        // ★ 同一个巧计
        var tmpBuf = raw.buffer(readSize + 1, buf2);
        encBlock, blockLen = aes2.encrypt(tmpBuf, readSize);
    }
    else{
        encBlock, blockLen = aes2.encrypt(buf2, readSize);
    }

    // 发送加密块
    var ok = http.writeData(raw.tostring(encBlock, 1, blockLen));
    if(!ok) { console.error("writeData 失败"); break; }
    sentBytes += blockLen;
}

file2.close();

// 步骤5:结束发送
var ok, status, total = http.endSendData();
if(ok){
    console.log("    发送完成:", sentBytes, "字节, HTTP状态:", status);

    // 读取响应
    var response = http.readAll();
    if(response){
        import JSON;
        var respData = JSON.parse(response);
        if(respData){
            var cl = respData.headers["Content-Length"];
            console.log("    服务器收到 Content-Length:", cl);
        }
    }
}
else{
    console.log("    发送失败:", status, total);
}

http.endRequest();

// 清理测试文件
io.remove(testFile);

console.log(`
============== 要点总结 ============== 

【1. AES-ECB 分块加密的 final 判定】
   aes.encrypt(buf, dataSize):
     dataSize < bufSize  → final=true  → 追加PKCS7填充
     dataSize == bufSize → final=false → 等待更多数据

【2. 最后一块恰好填满缓冲区的巧计】
   var tmpBuf = raw.buffer(readSize + 1, buf);
   aes.encrypt(tmpBuf, readSize);
   // tmpBuf 比 readSize 大 1 → readSize < bufSize → final!

【3. HTTP 流式上传(手动分步)】
   beginRequest(url, 'POST')
   → writeHeader('Content-Type: ...')
   → beginSendData(totalSize)       // 告知总长度
   → writeData(chunk1), writeData(chunk2), ...
   → endSendData()

【3-alt. HTTP 流式上传(onSend 回调)】
   http.onSendBegin = function(){ return totalSize; };
   http.onSend = function(remainSize){
console.dump("       return 数据块, 长度;  // 返回 null 表示结束
   };
   http.post(url, null, headers);  // payload=null 触发流式

【4. AES-ECB 分块解密(流式下载对应)】
   除最后一块外,每块大小必须是 16 的倍数
   aes.decrypt(chunk, false)  → 非最后块(不处理填充)
   aes.decrypt(lastChunk)     → 最后块(自动去PKCS7填充)

【5. 内存占用】
   无论文件多大,内存占用 = CHUNK 大小 (` + (CHUNK/1024/1024) + `MB)
   整个过程不会将整个文件加载到内存
`)

console.pause();
Markdown 格式