一个极简的加解密demo:
import {
createCipheriv,
createDecipheriv,
randomBytes
} from 'node:crypto'
/**
* AES-GCM 加密后的数据结构。
*
* nonce:
* AES-GCM 使用的一次性随机数,通常为 12 字节。
*
* ciphertext:
* 加密后的密文。
*
* authTag:
* GCM 生成的认证标签,用于检测密文是否被篡改。
*/
interface EncryptedMessage {
nonce: Buffer
ciphertext: Buffer
authTag: Buffer
}
/**
* 使用 AES-256-GCM 加密内容。
*
* @param content 要加密的原始内容
* @param key 32 字节的 AES-256 密钥
* @param aad 可选的附加认证数据,不会被加密,但会参与完整性校验
*/
function encryptMessage(
content: Buffer,
key: Buffer,
aad?: Buffer
): EncryptedMessage {
// AES-256 要求密钥长度必须是 32 字节
if (key.length !== 32) {
throw new Error('AES-256 的密钥必须是 32 字节')
}
// GCM 推荐使用 12 字节 nonce。
// nonce 不需要保密,但是同一个 key 下不能重复使用。
const nonce = randomBytes(12)
// 创建 AES-256-GCM 加密器
const cipher = createCipheriv(
'aes-256-gcm',
key,
nonce
)
// AAD 不会被加密,但是会参与 authTag 计算。
// 例如可以把 CAN ID、命令字、设备地址放在 AAD 中。
if (aad) {
cipher.setAAD(aad)
}
// 执行加密。
// cipher.final() 可能产生最后一部分加密数据,所以不能省略。
const ciphertext = Buffer.concat([
cipher.update(content),
cipher.final()
])
// 获取 GCM 认证标签。
// 接收端会使用它判断密文或 AAD 是否被篡改。
const authTag = cipher.getAuthTag()
return {
nonce,
ciphertext,
authTag
}
}
/**
* 使用 AES-256-GCM 解密内容。
*
* 如果密文、nonce、authTag、AAD 或 key 中任意一个不正确,
* cipher.final() 都会抛出异常,此时不能使用解密结果。
*
* @param message 加密后的数据
* @param key 与加密时相同的 32 字节密钥
* @param aad 加密时使用的附加认证数据
*/
function decryptMessage(
message: EncryptedMessage,
key: Buffer,
aad?: Buffer
): Buffer {
// AES-256 要求密钥长度必须是 32 字节
if (key.length !== 32) {
throw new Error('AES-256 的密钥必须是 32 字节')
}
// 创建 AES-256-GCM 解密器
const decipher = createDecipheriv(
'aes-256-gcm',
key,
message.nonce
)
// 解密时必须使用和加密时完全一致的 AAD
if (aad) {
decipher.setAAD(aad)
}
// 把加密时得到的 authTag 传给解密器。
// 没有这一步,无法验证数据是否被篡改。
decipher.setAuthTag(message.authTag)
// cipher.final() 会完成认证校验。
// 如果报文被篡改,这里会直接抛出异常。
return Buffer.concat([
decipher.update(message.ciphertext),
decipher.final()
])
}
/**
* 运行一个完整的 AES-256-GCM 示例。
*/
function runDemo(): void {
// AES-256 要求 32 字节密钥。
// 正式项目中不要把密钥硬编码在代码里,
// 应该从安全存储、HSM 或受保护的配置中读取。
const key = Buffer.from(
'01234567890123456789012345678901',
'utf8'
)
// 要加密的原始内容,也可以直接替换成 CAN 或 ECU 的二进制报文。
const plainText = Buffer.from(
'ecu calibration data',
'utf8'
)
// AAD 不会被加密,但会参与认证。
// 如果这里的内容在接收端发生变化,解密认证也会失败。
const aad = Buffer.from(
'device=ecu-01;command=write-calibration',
'utf8'
)
// 执行加密
const encrypted = encryptMessage(
plainText,
key,
aad
)
process.stdout.write(原文:${plainText.toString('utf8')}\n)
process.stdout.write(nonce:${encrypted.nonce.toString('hex')}\n)
process.stdout.write(密文:${encrypted.ciphertext.toString('hex')}\n)
process.stdout.write(authTag:${encrypted.authTag.toString('hex')}\n)
// 执行解密
const decrypted = decryptMessage(
encrypted,
key,
aad
)
process.stdout.write(解密结果:${decrypted.toString('utf8')}\n)
// 测试篡改检测。
// 修改密文的一个字节,正常情况下解密会失败。
const tamperedMessage: EncryptedMessage = {
nonce: encrypted.nonce,
ciphertext: Buffer.from(encrypted.ciphertext),
authTag: encrypted.authTag
}
tamperedMessage.ciphertext[0] ^= 1
try {
decryptMessage(
tamperedMessage,
key,
aad
)
process.stdout.write('错误:篡改后的报文不应该解密成功\n')
} catch {
process.stdout.write('正确:检测到报文被篡改,解密失败\n')
}
}
runDemo()
当然实际发送数据的时候一般是nonce+ciphertext(密文)+authTag(认证),所以解密的时候一般也要分别抽离出来:
interface EncryptedMessage {
nonce: Buffer
ciphertext: Buffer
authTag: Buffer
}
/**
* 解析 ECU 返回的完整加密报文。
*
* 报文格式:
* nonce(12 字节) + ciphertext(不定长) + authTag(16 字节)
*/
function unpackEncryptedPacket(
packet: Buffer
): EncryptedMessage {
const nonceLength = 12
const authTagLength = 16
const minPacketLength = nonceLength + authTagLength
// 至少要包含 nonce 和 authTag
if (packet.length < minPacketLength) {
throw new Error('加密报文长度不足')
}
const nonce = packet.subarray(
0,
nonceLength
)
const authTag = packet.subarray(
packet.length - authTagLength
)
const ciphertext = packet.subarray(
nonceLength,
packet.length - authTagLength
)
return {
nonce,
ciphertext,
authTag
}
}
评论交流
欢迎留下你的想法