'use strict';
const crypto = require('crypto');
class Cipher {
constructor(key) {
this.key = Buffer.alloc(32);
this.key.write(key);
this.nonce = [undefined, undefined]; // [encryptNonce, decryptNonce]
this.aad = [Buffer.alloc(16, 0xaa), Buffer.alloc(16, 0xaa)]; // [encryptAAD, decryptAAD]
}
encrypt(raw) {
let cipher;
if (this.nonce[0] === undefined) {
// 生成完整的 12 字节 nonce
this.nonce[0] = Buffer.alloc(12, 0x70);
let sec = Math.floor(Date.now() / 1000);
let rand = Math.floor((Math.random() - 0.5) * 10); // 减少随机偏移范围
this.nonce[0].writeBigUInt64BE(BigInt(sec + rand)); // 前 8 字节为时间戳 + 随机偏移
crypto.randomBytes(4).copy(this.nonce[0], 8); // 后 4 字节随机填充
} else {
// 后续加密,递增 nonce
let n = this.nonce[0].readBigUInt64BE();
n++;
this.nonce[0].writeBigUInt64BE(n);
}
cipher = crypto.createCipheriv('chacha20-poly1305', this.key, this.nonce[0], { authTagLength: 16 });
cipher.setAAD(this.aad[0]);
let data = cipher.update(raw);
cipher.final();
let mac = cipher.getAuthTag();
// 返回完整 nonce (12 字节) + 加密数据 + authTag
return Buffer.concat([this.nonce[0], data, mac]);
}
decrypt(msg) {
if (msg.length < 12 + 16) { // nonce(12) + authTag(16)
throw new Error('message too short to decrypt.');
}
// 提取完整的 12 字节 nonce
const nonce = msg.slice(0, 12);
const data = msg.slice(12, -16);
const mac = msg.slice(-16);
let decipher;
try {
// 直接使用提取的 nonce
decipher = crypto.createDecipheriv('chacha20-poly1305', this.key, nonce, { authTagLength: 16 });
decipher.setAAD(this.aad[1]);
let raw = decipher.update(data);
decipher.setAuthTag(mac);
decipher.final();
// 更新解密 nonce 状态
this.nonce[1] = nonce;
return raw;
} catch (e) {
throw new Error('Failed to decrypt: invalid nonce or key.');
}
}
}
module.exports = Cipher;
动态 nonce 的作用
动态 nonce 的主要目的是确保每次加密时使用的 nonce 都是唯一的,从而防止重放攻击(replay attacks)和确保加密的随机性。在你的原始代码中,nonce 的前 8 字节是基于时间戳和随机偏移生成的,后 4 字节是随机填充的。
修改后的 nonce 逻辑
加密时:仍然生成动态 nonce(前 8 字节基于时间戳 + 随机偏移,后 4 字节随机填充)。
解密时:直接使用加密数据中传递的完整 nonce,不再需要时间戳猜测逻辑。
是否影响加密效果
安全性:修改后的代码仍然保持了动态 nonce 的唯一性和随机性,因此安全性没有降低。
重放攻击防护:由于 nonce 仍然是动态生成的,重放攻击的风险仍然被有效避免。