You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js解密Rails Active Record AES-256-GCM加密数据失败

AES-256-GCM解密认证失败排查与修复方案

该问题是Rails Active Record Encryption默认实现细节与Node.js crypto模块默认约定不对齐导致,按以下优先级排查即可解决:

1. 错误解析数据库存储的加密Payload(90%场景的根因)

Rails不会将密文、IV、认证标签拆分存储,数据库字段中保存的加密值是两层包装结构:

  • 外层为URL安全Base64编码,解码后得到JSON格式的元数据+密文串
  • JSON结构固定包含两个字段:
    • p:Base64编码的原始AES-GCM密文
    • h:元数据对象,内嵌两个Base64编码字段:
      • iv:12字节GCM初始向量
      • at:16字节GCM认证标签
        不要直接将数据库读出的整串值作为密文传入,也不要手动从密文尾部截断16字节作为认证标签,必须按结构解析:
// 从数据库读取的原始加密字符串,不要做trim、转义等额外处理
const storedVal = "数据库取出的加密字段值";
// 处理Rails用的URL安全Base64:替换特殊字符、补全padding
const base64Val = storedVal.replace(/-/g, '+').replace(/_/g, '/').padEnd(
  storedVal.length + (4 - storedVal.length % 4) % 4,
  '='
);
const payload = JSON.parse(Buffer.from(base64Val, 'base64').toString('utf8'));
const ciphertext = Buffer.from(payload.p, 'base64');
const iv = Buffer.from(payload.h.iv, 'base64');
const authTag = Buffer.from(payload.h.at, 'base64');

2. 密钥编码/派生逻辑不匹配

Rails配置中的primary_key是Base64编码的32字节原始密钥,不要使用hex编码转Buffer,转码后必须校验长度为32字节(AES-256密钥固定长度要求)。
默认配置下Rails不会直接使用primary_key作为加密密钥,会通过HKDF-SHA256基于salt做密钥派生,Node.js必须使用完全一致的派生参数:

const crypto = require('crypto');
// 从Rails配置中取出的primary_key和key_derivation_salt
const railsPrimaryKey = "Rails配置里的primary_key";
const railsSalt = "Rails配置里的key_derivation_salt";
// 与Rails源码完全一致的HKDF参数
const derivedKey = Buffer.from(crypto.hkdfSync(
  'sha256',
  Buffer.from(railsPrimaryKey, 'base64'),
  Buffer.from(railsSalt, 'base64'),
  'ActiveRecord::Encryption', // Rails源码硬编码的info字段,不能修改
  32
));

3. 遗漏GCM附加认证数据(AAD)

如果加密时开启了确定性加密(模型中配置encrypts :字段名, deterministic: true),Rails会将模型名.字段名的ASCII编码串作为AAD参与GCM校验,Node.js解密时必须传入完全一致的AAD,否则必然触发认证失败。非确定性加密默认AAD为空字符串,无需额外设置。
正确的解密逻辑示例:

const decipher = crypto.createDecipheriv('aes-256-gcm', derivedKey, iv);
// 确定性加密必须加这行,字符串与Rails侧的"模型名.字段名"完全一致
decipher.setAAD(Buffer.from('User.phone', 'ascii'));
// setAuthTag必须在update调用前、final调用前执行
decipher.setAuthTag(authTag);
let decrypted = decipher.update(ciphertext, null, 'utf8');
decrypted += decipher.final('utf8');
// Rails默认对明文开启zlib压缩,解密后如果是乱码,执行解压即可
const zlib = require('zlib');
const finalPlaintext = zlib.inflateSync(Buffer.from(decrypted, 'binary')).toString('utf8');

其他校验点

  • 确认IV长度为12字节、认证标签长度为16字节,长度不符合说明Payload解析步骤错误
  • 不要对数据库读出的加密值做任何截断、转义、trim处理,单个字符缺失就会导致GCM校验失败
  • 若需要兼容Rails 7.0之前的非内置加密格式(如attr_encrypted、lockbox),才需要按「前12字节为IV、最后16字节为认证标签、中间为密文」的规则拆分Payload

内容的提问来源于stack exchange,提问作者Steve Johnstone

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 23:18:33