![]() 什么是二要素二要素(姓名和身份证)是敏感数据,,很多网站仅仅依靠二要素来确认你是谁,若以明文形式存储在数据库中,存在被攻破的风险。若这些信息被不法分子获取, 后果严重。 如何保护二要素(姓名和身份证)单向散列算法,如MD5、SHA-256等,虽然可以对数据生成唯一的指纹,但由于其不可逆,无法用于加密需要解密的数据。因此,它不适合用于对二要素信息(如姓名和身份证)进行加密保存。 在此情况下,需要选择真正的加密算法来实现数据的加密存储与解密。 加密算法分类
场景选择在需要加密保存二要素信息的场景下,加密和解密都是由同一个服务端程序执行,双方并不需要通过网络传输密钥,因此密钥分发的安全性问题不是关键点。相对而言,对称加密算法具有速度快、效率高的优势,更适合在服务端加密存储二要素数据。 因此,尽管非对称加密在解决密钥传输安全性上有独特优势,但在保存敏感数据的场景中,采用对称加密算法(如AES-CBC或AES-CTR模式)更为合适。 算法选择对称加密常用算法有 DES、3DES 和 AES。
AES 是目前较为主流的对称加密算法,兼具高安全性和高性能。AES 是由 NIST 选拔出的 Rijndael 算法作为标准,支持分组加密模式。AES 每次处理 128 位明文,生成相应的 128 位密文。对于较长的明文,需要通过分组迭代加密。 AES - ECB 模式 (不推荐)![]() 加密一段包含 16 个字符的字符串,得到密文 A;然后把这段字符串复制一份成为一个32 个字符的字符串,再进行加密得到密文 B。我们验证下密文 B 是不是重复了一遍的密文 A。 模拟银行转账的场景,假设整个数据由发送方账号、接收方账号、金额三个字段构成。我们尝试改变密文中数据的顺序来操纵明文 [code] private static final String KEY = "secretkey1234567"; @GetMapping("ecb") public void ecb() throws Exception { Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); test(cipher, null); } private static SecretKeySpec setKey(String secret) { return new SecretKeySpec(secret.getBytes(), "AES"); } private static void test(Cipher cipher, AlgorithmParameterSpec parameterSpec) throws Exception { cipher.init(Cipher.ENCRYPT_MODE, setKey(KEY), parameterSpec); System.out.println("一次:" + Hex.encodeHexString(cipher.doFinal("abcdefghijklmnop".getBytes()))); System.out.println("两次:" + Hex.encodeHexString(cipher.doFinal("abcdefghijklmnopabcdefghijklmnop".getBytes()))); byte[] sender = "1000000000012345".getBytes(); byte[] receiver = "1000000000034567".getBytes(); byte[] money = "0000000010000000".getBytes(); //加密发送方账号 System.out.println("发送方账号:" + Hex.encodeHexString(cipher.doFinal(sender))); //加密接收方账号 System.out.println("接收方账号:" + Hex.encodeHexString(cipher.doFinal(receiver))); //加密金额 System.out.println("金额:" + Hex.encodeHexString(cipher.doFinal(money))); byte[] result = cipher.doFinal(ByteUtils.concatAll(sender, receiver, money)); //加密三个数据 System.out.println("完整数据:" + Hex.encodeHexString(result)); byte[] hack = new byte[result.length]; //把密文前两段交换 System.arraycopy(result, 16, hack, 0, 16); System.arraycopy(result, 0, hack, 16, 16); System.arraycopy(result, 32, hack, 32, 16); cipher.init(Cipher.DECRYPT_MODE, setKey(KEY), parameterSpec); //尝试解密 System.out.println("原始明文:" + new String(ByteUtils.concatAll(sender, receiver, money))); System.out.println("操纵密文:" + new String(cipher.doFinal(hack))); } [/code]输出 [code]一次:a6025aaadd429e8c13073fc3512a7250 两次:a6025aaadd429e8c13073fc3512a7250a6025aaadd429e8c13073fc3512a7250 发送方账号:fdfc03515d95e2fa33edc9ca67cf43ae 接收方账号:e70eecf4baa8decf117d294e12d850c0 金额:f317ed23783f4babb607bd88ba076d0c 完整数据:fdfc03515d95e2fa33edc9ca67cf43aee70eecf4baa8decf117d294e12d850c0f317ed23783f4babb607bd88ba076d0c 原始明文:100000000001234510000000000345670000000010000000 操纵密文:100000000003456710000000000123450000000010000000 [/code]如上代码示例展示了 ECB 模式的漏洞,攻击者可以在不解密的情况下操纵密文,实现对敏感数据(如银行转账信息)的修改。 -----> 在不知道密钥的情况下,我们操纵密文实现了对明文数据的修改,对调了发送方账号和接收方账号 [code]原始明文:100000000001234510000000000345670000000010000000 操纵密文:100000000003456710000000000123450000000010000000 [/code]代码运行的结果证明了:
因此,ECB 模式简单但不安全,不推荐使用。 AES - CBC 模式CBC 模式,在解密或解密之前引入了 XOR 运算,第一个分组使用外部提供的初始化向量IV,从第二个分组开始使用前一个分组的数据,这样即使明文是一样的,加密后的密文也是不同的,并且分组的顺序不能任意调换。这就解决了 ECB 模式的缺陷. ![]() 把之前的代码修改为 CBC 模式,再次进行测试 [code] private static final String initVector = "abcdefghijklmnop"; @GetMapping("cbc") public void cbc() throws Exception { Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8")); test(cipher, iv); } [/code]可以看到,相同的明文字符串复制一遍得到的密文并不是重复两个密文分组,并且调换密文分组的顺序无法操纵明文。 [code]一次:6fa7a7b2c0979abecc1b59fe17b663c6 两次:6fa7a7b2c0979abecc1b59fe17b663c6e873cb4abb4b46b76cb748447373103c 发送方账号:ff4f74de614be6905951fa2ac68a529a 接收方账号:0dfdd3116d26dac4a7349167dfa0ce0a 金额:5521773b79160a1a51b9d8f8bfb0a346 完整数据:ff4f74de614be6905951fa2ac68a529abb54065906129619b122c978541f0076347086b16d09934e4f9d9dc4ab942af0 原始明文:100000000001234510000000000345670000000010000000 SD�A�x�%B[3t+B�Wi@��Cb��b� [/code]GCM(Galois/Counter Mode)AES-256-GCM简介GCM(Galois/Counter Mode)是一种结合计数器模式(Counter Mode)和Galois域认证的分组加密模式。它不仅能够提供高效的加密服务,还能实现消息认证(即验证消息的完整性和真实性)。与传统的CBC模式不同,GCM模式可以并行处理,极大提升了性能,特别适合高吞吐量的环境。 这是一种 AEAD(Authenticated Encryption with Associated Data)认证加密算法,除了能实现普通加密算法提供的保密性之外,还能实现可认证性和密文完整性,是目前最推荐的 AES 模式。 使用类似 GCM 的 AEAD 算法进行加解密,除了需要提供初始化向量和密钥之外,还可以提供一个 AAD(附加认证数据,additional authenticated data),用于验证未包含在明文中的附加信息,解密时不使用加密时的 AAD 将解密失败。其实,GCM 模式的内部使用的就是 CTR 模式,只不过还使用了 GMAC 签名算法,对密文进行签名实现完整性校验。 AES-256-GCM工作原理AES-256-GCM结合了AES-256加密算法和GCM模式,具备如下功能:
安全优势相较于其他常见的模式,如CBC(Cipher Block Chaining),AES-256-GCM提供了显著的优势:
应用场景AES-256-GCM的广泛应用场景包括:
其他模式 和 敏感数据加密建议除了 ECB 模式外,AES 还有 CBC、CFB、OFB 和 CTR 模式。推荐使用 CBC 或 CTR 模式。ECB 和 CBC 模式需要设置合适的填充方式来处理超过一个分组的数据。 此外,对于敏感数据加密,建议:
Code接下来,我们按照如上策略完成相关代码实现: 第一步,对于用户姓名和身份证,我们分别保存三个信息,脱敏后的明文、密文和加密ID。加密服务加密后返回密文和加密 ID,随后使用加密 ID 来请求加密服务进行解密 [code]import lombok.Data; import javax.persistence.Entity; import javax.persistence.Id; @Data @Entity public class UserData { @Id private Long id; private String idcard;//脱敏的身份证 private Long idcardCipherId;//身份证加密ID private String idcardCipherText;//身份证密文 private String name;//脱敏的姓名 private Long nameCipherId;//姓名加密ID private String nameCipherText;//姓名密文 } [/code]第二步,加密服务数据表保存加密 ID、初始化向量和密钥。加密服务表中没有密文,实现了密文和密钥分离保存. [code]import lombok.Data; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import static javax.persistence.GenerationType.AUTO; @Data @Entity public class CipherData { @Id @GeneratedValue(strategy = AUTO) private Long id; private String iv;//初始化向量 private String secureKey;//密钥 } [/code]第三步,加密服务使用 GCM 模式( Galois/Counter Mode)的 AES-256 对称加密算法,也就是 AES-256-GCM 接下来,我们实现基于 AES-256-GCM 的加密服务,包含下面的主要逻辑:
第四步,分别实现加密和解密接口用于测试。 可以让用户选择,如果需要保护二要素的话,就自己输入一个查询密码作为 AAD。系统需要读取用户敏感信息的时候,还需要用户提供这个密码,否则无法解密。这样一来,即使黑客拿到了用户数据库的密文、加密服务的密钥和 IV,也会因为缺少 AAD 无法解密. [code] @Autowired private CipherService cipherService; // 加密 @GetMapping("right") public UserData right(@RequestParam(value = "name", defaultValue = "小工匠") String name, @RequestParam(value = "idcard", defaultValue = "300000000000001234") String idCard, @RequestParam(value = "aad", required = false) String aad) throws Exception { UserData userData = new UserData(); userData.setId(1L); //脱敏姓名 userData.setName(chineseName(name)); //脱敏身份证 userData.setIdcard(idCard(idCard)); //加密姓名 CipherResult cipherResultName = cipherService.encrypt(name, aad); userData.setNameCipherId(cipherResultName.getId()); userData.setNameCipherText(cipherResultName.getCipherText()); //加密身份证 CipherResult cipherResultIdCard = cipherService.encrypt(idCard, aad); userData.setIdcardCipherId(cipherResultIdCard.getId()); userData.setIdcardCipherText(cipherResultIdCard.getCipherText()); return userRepository.save(userData); } // 解密 @GetMapping("read") public void read(@RequestParam(value = "aad", required = false) String aad) throws Exception { UserData userData = userRepository.findById(1L).get(); log.info("name : {} idcard : {}", cipherService.decrypt(userData.getNameCipherId(), userData.getNameCipherText(), aad), cipherService.decrypt(userData.getIdcardCipherId(), userData.getIdcardCipherText(), aad)); } // 脱敏身份证 private static String idCard(String idCard) { String num = StringUtils.right(idCard, 4); return StringUtils.leftPad(num, StringUtils.length(idCard), "*"); } // 脱敏姓名 public static String chineseName(String chineseName) { String name = StringUtils.left(chineseName, 1); return StringUtils.rightPad(name, StringUtils.length(chineseName), "*"); } [/code]启动服务,访问 http://localhost:45678/storeidcard/right ![]() 访问解密接口: http://localhost:45678/storeidcard/read ![]() 如果AAD错误 ![]() 经过这样的设计,二要素就比较安全了。黑客要查询用户二要素的话,需要同时拿到密文、IV+ 密钥、AAD。而这三者可能由三方掌管,要全部拿到比较困难。 ![]() 免责声明:本内容来源于网络,如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |