资讯动态

HoRain云--Java 密码学实战:AES-GCM、随机数与密钥管理

发布时间:2026/10/21 11:37:45 来源:尧图企业网站定制
把数据库中的敏感字段“加密一下”听起来只是调用一次Cipher.doFinal。真正容易出错的却是算法模式、随机数、同一密钥下 nonce 重复、密文格式以及密钥存放位置。加密解决的是特定威胁模型里的机密性与完整性不是把字符串 Base64 一下更不能代替访问控制和审计。一、先明确要保护什么传输中的敏感信息通常首先依靠 TLS应用层字段加密针对的是备份、某些存储层或特定人员访问数据库时的暴露。若应用和密钥都在同一台机器、同一权限域攻击者拿到整个运行环境时加密效果可能有限。要列出攻击者能访问什么、解密由谁执行、查询是否需要明文、删除和轮换怎样实施再选择方案。密码的保存与可逆加密不同登录密码应采用专门的加盐、抗暴力破解密码哈希方案而不是 AES 加密后存库。需要按密文字段查询或去重时也不能直接假设随机化加密后的字节可做等值比较设计上可能需要额外的索引策略与泄漏风险分析。二、为什么选择带认证的加密模式AES/GCM/NoPadding同时提供加密与认证解密时能发现密文或关联数据被改动。只用AES/ECB/PKCS5Padding会泄露重复数据的模式只加密不校验完整性则可能让篡改后的数据进入业务逻辑。GCM 中 nonce 必须在同一密钥下保持唯一。常见做法是每次生成新的 12 字节随机 nonce并与密文一起存储。随机并不意味着在无限次数使用下绝无碰撞超大规模部署还应制定使用上限和轮换策略。GCMParameterSpec(128, nonce)里的 128 是认证标签的位数不是 AES 密钥长度。AES 密钥可用KeyGenerator生成nonce 不需要保密但绝不能为了方便而使用固定全零数组。示例每次启动生成临时密钥因此只展示一次运行内的往返进程退出后无法再解密此前生成的数据。三、完整可运行的加解密示例保存为GcmDemo.java运行javac GcmDemo.java和java GcmDemo。输出经 Base64 编码格式是nonce || ciphertext-and-tag。Base64 是编码而不是加密。importjava.nio.ByteBuffer;importjava.nio.charset.StandardCharsets;importjava.security.SecureRandom;importjava.util.Base64;importjavax.crypto.Cipher;importjavax.crypto.KeyGenerator;importjavax.crypto.SecretKey;importjavax.crypto.spec.GCMParameterSpec;publicclassGcmDemo{privatestaticfinalSecureRandomRANDOMnewSecureRandom();privatestaticfinalintNONCE_BYTES12;privatestaticfinalintTAG_BITS128;staticStringencrypt(SecretKeykey,Stringvalue,byte[]aad)throwsException{byte[]noncenewbyte[NONCE_BYTES];RANDOM.nextBytes(nonce);CiphercipherCipher.getInstance(AES/GCM/NoPadding);cipher.init(Cipher.ENCRYPT_MODE,key,newGCMParameterSpec(TAG_BITS,nonce));cipher.updateAAD(aad);byte[]encryptedcipher.doFinal(value.getBytes(StandardCharsets.UTF_8));returnBase64.getEncoder().encodeToString(ByteBuffer.allocate(nonce.lengthencrypted.length).put(nonce).put(encrypted).array());}staticStringdecrypt(SecretKeykey,Stringencoded,byte[]aad)throwsException{byte[]combinedBase64.getDecoder().decode(encoded);if(combined.lengthNONCE_BYTESTAG_BITS/8)thrownewIllegalArgumentException(invalid ciphertext);ByteBufferbufferByteBuffer.wrap(combined);byte[]noncenewbyte[NONCE_BYTES];buffer.get(nonce);byte[]encryptednewbyte[buffer.remaining()];buffer.get(encrypted);CiphercipherCipher.getInstance(AES/GCM/NoPadding);cipher.init(Cipher.DECRYPT_MODE,key,newGCMParameterSpec(TAG_BITS,nonce));cipher.updateAAD(aad);returnnewString(cipher.doFinal(encrypted),StandardCharsets.UTF_8);}publicstaticvoidmain(String[]args)throwsException{KeyGeneratorgeneratorKeyGenerator.getInstance(AES);generator.init(256);SecretKeykeygenerator.generateKey();byte[]aadcustomer:42.getBytes(StandardCharsets.UTF_8);Stringsavedencrypt(key,example secret,aad);System.out.println(decrypt(key,saved,aad));}}关联数据 AAD 不加密却受认证保护。示例把记录 ID 作为 AAD目的是防止把某条记录的密文简单搬到另一条记录后仍被接受。加密与解密两侧必须使用完全相同的字节改动 ID 后解密会失败。异常不要降级为“返回空字符串”这会把数据损坏或攻击变成貌似正常的业务值。四、密文格式要能演进真实存储格式至少应包含版本、密钥标识、nonce 和密文及标签并严格校验长度和字段。示例省略版本与密钥 ID只适合教学。轮换时新写入使用新密钥旧数据按旧密钥 ID 解密可在读取时重加密也可计划后台迁移。直接换掉旧密钥且没有迁移历史数据就无法解密。不要把原始密钥写进源码、环境日志、数据库同一字段或客户端安装包。密钥应由受权限控制的密钥管理服务或硬件支持系统提供并定义调用权限、审计、备份与灾难恢复。KMS 保存主密钥、数据密钥按记录或批次包裹是常见方向具体架构必须依据吞吐、成本、合规和故障模式设计。五、随机数与 nonce 的生命周期SecureRandom用于生成安全敏感随机值Random不适合这里。一个长期使用的 AES 密钥若执行大量加密操作随机 nonce 的碰撞风险会随操作次数增加必须对密钥作用域、最大调用量和轮换频率做工程约束。也可用严格管理的计数式 nonce但跨实例分配、重启恢复和防回退更难。最危险的是程序重启后重用同一个固定值或多实例共享密钥却各自从零计数。Cipher实例含内部状态不要当成全局单例被多个线程并发使用每次操作创建或采用经过审查的池化方案。调用updateAAD必须在doFinal前、处理正文前完成。解密失败时不要告诉外部用户“密钥错误还是标签错误”统一暴露可操作的内部错误并避免泄露敏感细节。六、测试与上线清单测试往返正确性之外还要篡改 nonce、密文、标签和 AAD确保失败测试空字符串、中文、长文本和无效 Base64。每次加密相同明文应产生不同结果同时都能解密。检查日志、异常、监控标签是否意外记录明文和密钥。备份恢复演练必须同时包括密文与可用密钥只有数据库备份而没有密钥恢复方案等于无法恢复数据。本文的底线是正确算法仅是第一层密钥权限、nonce 唯一性、格式演进和运维恢复同样属于密码学系统。安全方案应由熟悉威胁模型的团队评审不能把一段能运行的演示代码直接贴进生产服务。七、密钥从哪里来谁能使用示例中的KeyGenerator每次启动生成新密钥展示了正确的随机密钥生成方式却无法满足长期存储的解密要求。生产系统需要有受控密钥来源主密钥由密钥管理服务管理应用在授权范围内取得或调用解密能力数据密钥可被主密钥保护。只有取得数据密钥权限的服务能够解密相应字段普通数据库只持有密文和元数据。权限应按服务身份与业务环境划分。开发、测试、生产不能共享同一把密钥后台报表任务未必需要解密全部用户信息。记录密钥访问审计定期检查权限和异常调用量。不要把密钥放入异常堆栈、环境变量转储、配置仓库或 CI 构建日志。依赖环境变量传递密钥时也要认识到进程诊断、容器配置和部署系统的读取权限风险。密钥轮换并非改一条配置旧密文仍需旧密钥解密。一个可操作流程是格式中存密钥版本新数据使用新版本读旧数据时按版本选择旧密钥并以受控速率重新加密。完成迁移、备份过期和恢复演练后才能考虑禁用旧密钥。必须提前定义旧密钥被怀疑泄露时的应急流程而不是等事故发生才发现无法批量迁移。八、认证失败与错误处理GCM 解密期间密文、nonce、AAD 或密钥任何一项不一致都可能表现为认证失败。对外接口通常应给统一失败响应内部则记录足够的上下文例如记录 ID、密钥版本、操作阶段和受控错误码但不记录明文、原始密钥或完整密文。不能在失败时试图“忽略标签继续解密”那会放弃模式最重要的完整性保证。解密前先校验 Base64 长度和版本字段给输入总长度设置上限以防止超大恶意数据造成内存问题。认证通过之后才能使用明文不要在流式处理过程中提前把未经认证的片段提交到下游。若数据经压缩后再加密读取时还需限制解压后的大小并检查业务数据格式。九、加密不是访问控制的替代品加密可减少部分静态数据泄露但合法应用仍会解密数据具有应用权限的攻击者可能通过正常接口取得明文。应将 API 授权、最小权限、日志脱敏、审计与速率限制一同设计。尤其不能因为数据库字段是密文就允许任意服务访问解密接口否则解密服务只是另一个不受控的明文数据库。业务字段如果要做前缀查询、排序或模糊搜索普通随机化加密无法直接满足。为搜索单独创建索引会带来可观察信息泄露必须分析攻击者可见的查询模式与频率不能简单把密文拿去建索引。不同字段也不应因“实现方便”随意复用同一密钥和 nonce 规划隔离作用域可以限制单点失误的影响。十、恢复演练比文档更可靠把数据备份拷贝到隔离恢复环境验证密钥服务、权限、版本元数据和应用代码能够一起恢复。只验证加密成功而不验证历史数据轮换与恢复系统可能在真正需要备份时才发现密钥已停用。测试还应覆盖密钥服务短暂不可用是快速失败、排队等待还是允许只读降级都应由业务确定。加密系统的可用性与安全性必须同时纳入设计。参考资料Cipher API、GCMParameterSpec API、SecureRandom API。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑