一个极简的加解密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
  }
}