# 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();
```

