加密解密加签验签——接口安全的最后一道防线
加密解密加签验签——接口安全的最后一道防线密评来了等保密评密码应用安全性评估要求下发。整改项用户信息存储明文 → 加密数据传输明文 → HTTPS 加密接口调用无认证 → 加签验签登录口令明文传输 → SM3 摘要加密方案演进算法研究加密算法的问题 加密研究 加密算法实现 vc解密算法实现当时还在做技术预研试过各种算法RSA 1024/2048AES 128/256DES/3DESMD5/SHA结论政务系统要求用国密算法不能用国际算法。接口加密新接口加密功能开发及动态库改写接口升级首次引入加密// 动态库中的加密函数VC编写intEncryptData(constchar*plainText,// 明文char*cipherText,// 密文输出constchar*key// 密钥);intDecryptData(constchar*cipherText,// 密文char*plainText,// 明文输出constchar*key// 密钥);问题密钥硬编码在动态库里每发一个版本都要重新编译。电子签名电子签名文件生产函数编写PDF电子签名用于医保凭证publicbyte[]signPdf(byte[]pdfData,X509Certificatecert,PrivateKeykey){// 1. 计算PDF摘要MessageDigestmdMessageDigest.getInstance(SM3);byte[]digestmd.digest(pdfData);// 2. 用私钥签名SignaturesigSignature.getInstance(SM2);sig.initSign(key);sig.update(digest);byte[]signaturesig.sign();// 3. 嵌入PDFPdfSignersignernewPdfSigner();signer.sign(pdfData,cert,signature);returnsigner.getSignedPdf();}全面密评改造1. HTTPS 证书问题解决认证https不能访问的问题 证书不符合要求问题自签名证书被浏览器拦截。解决用 OpenSSL 生成合法证书。# 生成RSA私钥和自签名证书openssl req-newkeyrsa:2048-nodes-keyoutrsa_private.key\-x509-days365-outcert.crt# 导出为PFX格式Tomcat使用openssl pkcs12-export-outcertificate.pfx\-inkeyrsa_private.key-incert.crt注意生产环境必须用 CA 颁发的证书不能自签名。2. 用户信息加密存储用户信息加密解密开发、部署需求数据库中存储的用户信息姓名、身份证、手机号必须加密。// 加密存储方案publicclassUserInfoEncryption{// 存储时加密publicvoidsaveUser(Useruser){StringencryptedNameSM4.encrypt(user.getName(),secretKey);StringencryptedIdCardSM4.encrypt(user.getIdCard(),secretKey);jdbcTemplate.update(INSERT INTO t_user (name, id_card, ...) VALUES (?, ?, ...),encryptedName,encryptedIdCard);}// 查询时解密publicUsergetUser(Longid){MapString,ObjectrowjdbcTemplate.queryForMap(SELECT * FROM t_user WHERE id ?,id);UserusernewUser();user.setName(SM4.decrypt((String)row.get(name),secretKey));user.setIdCard(SM4.decrypt((String)row.get(id_card),secretKey));returnuser;}}问题加密后无法模糊查询LIKE %张%。解决// 方案1加密字段精确查询SELECT*FROMt_userWHEREnameSM4.encrypt(张三,key)// 方案2增加明文索引字段脱敏后ALTERTABLEt_userADDname_indexVARCHAR(50);// name_index 张**只存姓氏不存全名// 方案3使用可搜索加密Searchable Encryption// 但国密不支持暂不采用3. 接口加签验签加密、解密、加签、验签测试 加密、解密、加签、验签方法确定、代码编写、更新数据、查询数据测试完整方案publicclassApiSecurity{// 加签发送方 publicStringsignRequest(MapString,Stringparams,StringsecretKey){// 1. 参数排序TreeMapString,StringsortedParamsnewTreeMap(params);// 2. 拼接字符串StringBuildersbnewStringBuilder();for(Map.EntryString,Stringentry:sortedParams.entrySet()){sb.append(entry.getKey()).append().append(entry.getValue()).append();}sb.append(key).append(secretKey);// 3. SM3 摘要byte[]digestSM3.digest(sb.toString().getBytes(StandardCharsets.UTF_8));// 4. 转十六进制字符串returnHex.encodeHexString(digest);}// 验签接收方 publicbooleanverifySign(MapString,Stringparams,Stringsign,StringsecretKey){// 1. 去除签名字段MapString,StringparamsWithoutSignnewHashMap(params);paramsWithoutSign.remove(sign);// 2. 重新计算签名StringcalculatedSignsignRequest(paramsWithoutSign,secretKey);// 3. 比对returncalculatedSign.equals(sign);}// 接口加密 publicStringencryptRequest(Stringdata,StringsessionKey){// 1. 生成随机密钥byte[]randomKeySM4.generateKey();// 2. 用会话密钥加密随机密钥byte[]encryptedKeySM4.encrypt(randomKey,sessionKey);// 3. 用随机密钥加密数据byte[]encryptedDataSM4.encrypt(data.getBytes(),randomKey);// 4. 组装returnBase64.encode(encryptedKey).Base64.encode(encryptedData);}}验签流程请求方 接收方 │ │ │ 1. 参数排序 │ │ 2. 拼接 keyvaluekeyvalue │ │ 3. SM3 摘要 │ │ 4. 得到签名 sign │ │ │ │ ──── 请求参数 sign ──────▶ │ │ │ │ │ 1. 去除 sign 字段 │ │ 2. 同样方式计算签名 │ │ 3. 比对两个签名 │ │ │ ◀──── 响应 sign ────────── │ │ │ │ 验签响应 │4. 登录口令密文传输登录口令密文传输改造历史数据查询 前端sm3算法加密口令实际为计算摘要 后端解密存储在数据中的口令再sm3算法加密 比较是否相同改造前// 登录时明文传输$.ajax({url:/login,data:{username:admin,password:123456// 明文抓包就能看到}});改造后// 前端SM3 摘要后传输asyncfunctionlogin(username,password){// 1. 获取随机盐值constsaltawaitgetSalt(username);// 2. 计算 SM3(password salt)consthashsm3(passwordsalt);// 3. 传输摘要不是原文$.ajax({url:/login,data:{username:username,passwordHash:hash,salt:salt}});}// 后端验证逻辑publicbooleanverifyPassword(Stringusername,StringpasswordHash,Stringsalt){// 1. 从数据库取出加密存储的口令StringstoredPassworduserDao.getPassword(username);// 2. 解密存储的口令SM4解密StringdecryptedPasswordSM4.decrypt(storedPassword,secretKey);// 3. 计算 SM3(解密口令 salt)StringexpectedHashSM3.digest(decryptedPasswordsalt);// 4. 比对returnexpectedHash.equals(passwordHash);}国密算法选择为什么用国密等保密评要求政务系统必须使用国密算法。算法对照表用途国际算法国密算法说明对称加密AESSM4分组加密128位密钥非对称加密RSASM2基于椭圆曲线256位密钥摘要SHA-256SM3256位摘要随机数DRBGSM3派生用于生成密钥实际使用对比// AES 加密旧CiphercipherCipher.getInstance(AES/CBC/PKCS5Padding);cipher.init(Cipher.ENCRYPT_MODE,aesKey,iv);byte[]encryptedcipher.doFinal(plainText);// SM4 加密新SMS4sm4newSMS4();byte[]encryptedsm4.encrypt(plainText,sm4Key,sm4Iv);SM2 vs RSA 性能算法密钥生成签名(100次)验签(100次)安全性RSA-2048慢(2s)慢(1s)快(0.3s)中等SM2-256快(0.1s)快(0.3s)慢(1s)高同等RSA-3072性能问题加密对性能的影响测试数据 - 100万条用户信息加密存储 - SM4加密每条耗时0.1ms - SM4解密每条耗时0.1ms - 总耗时100万 × 0.1ms 100秒 优化后 - 批量加密1000条/批 - 并行处理4线程 - 总耗时100万 × 0.1ms / 4 / 1000 25秒查询性能对比操作明文加密后下降精确查询2ms5ms2.5x范围查询10ms无法-模糊查询15ms无法-批量查询5ms15ms3x教训加密不是免费的需要为性能下降买单。常见坑坑1编码不一致// 前端JavaScriptconsthashsm3(张三);// 输出e8d7...UTF-8编码// 后端Javabyte[]digestSM3.digest(张三.getBytes(StandardCharsets.UTF_8));StringhashHex.encodeHexString(digest);// 输出e8d7...一致// 如果后端用了GBKbyte[]digestSM3.digest(张三.getBytes(GBK));// 输出f3a2...不一致坑2Base64 vs Hex// Base64大小写敏感有和/字符// Hex只含0-9a-fURL安全// 建议传输用Base64URL无无/无StringsafeBase64.getUrlEncoder().withoutPadding().encodeToString(data);坑3密钥泄露# 密钥被上传到Git仓库 git add . git commit -m add config git push # 密钥文件被公开 # 预防措施 echo *.key .gitignore echo keystore.* .gitignore坑4证书过期# 证书过期服务不可用 javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path validation failed: java.security.cert.CertPathValidatorException: validity check failed # 监控证书过期时间 openssl x509 -in cert.pem -noout -enddate # 设置提前30天告警经验教训1. 加密不是万能的加密解决的是存储和传输安全业务安全需要权限控制、审计日志配合数据脱敏和加密是两回事2. 国密算法生态不完善Java 默认不支持 SM2/SM3/SM4需要 BouncyCastle部分数据库不支持加密函数硬件加密机HSM兼容性差3. 性能损耗不可忽视加密后无法做数据库内计算模糊查询需要额外设计批量操作需要分页4. 密钥管理是最薄弱的环节最常见的密钥管理问题 1. 密钥硬编码 → 泄露 2. 密钥定期轮换 → 旧数据无法解密 3. 密钥多环境 → 开发、测试、生产混用 4. 密钥备份 → 丢失后数据永久丢失最后的话加密解密加签验签看起来是纯技术问题实际上是一个管理问题。技术上SM2/SM3/SM4 算法都是现成的BouncyCastle 库也成熟。真正难的是密钥怎么管——谁持有密钥泄露了怎么办性能怎么保——加密后查询慢了10倍业务能接受吗兼容怎么搞——旧数据没加密新数据加密了怎么平滑过渡最实在加密、解密、加签、验签方法确定、代码编写、更新数据、查询数据测试方法确定放在代码编写前面——先想清楚再做加密这事一步错步步错。