项目安全-----加密算法实现

目录

对称加密算法

AES (ECB模式)

AES(CBC 模式)。

非对称加密


对称加密算法

对称加密算法,是使用相同的密钥进行加密和解密。使用对称加密算法来加密双方的通信的话,双方需要先约定一个密钥,加密方才能加密,接收方才能 解密。常用的加密算法,有 DES、3DES 和 AES,国密算法包括SM1,SM4和SM7。 目前,使用 DES 来加密数据非常不安全。因此,在业务代码中要避免使用 DES 加密。而 3DES 算法,是使用不同的密钥进行三次 DES 串联调用,虽然解决 了 DES 不够安全的问题,但是比 AES 慢,也不太推荐。我们来看看AES的算法,AES 算法有ECB、CBC、 CFB、OFB、CTR 模式

AES (ECB模式)

private static final String KEY = "secretkey1234567"; //密钥
    //测试ECB模式
    @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
        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);
        分区 业务常见问题 的第 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)));
    }

 两个相同明文分组产生的密文,就是两个相同的密文分组叠在一起。在不知道密钥的情况下,我们操纵密文实现了对明文数据的修改,对调了发送方账号 和接收方账号。所以说,ECB 模式虽然简单,但是不安全,不推荐使用。

AES(CBC 模式)。

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);
}

可以看到,相同的明文字符串复制一遍得到的密文并不是重复两个密文分组,并且调换密文分组的顺序无法操纵明文。 不要在代码中写死一个固定的密钥和初始化向量,最好和之前提到的盐一样,是唯一、独立并且每次都变化的。推荐使用独立的加密服务来管控密钥、做 加密操作,千万不要把密钥和密文存在一个数据库,加密服务需要设置非常高的管控标准。数据库中不能保存明文的敏感信息,但可以保存脱敏的信息。 普通查询的时候,直接查脱敏信息即可。下面举个例子:

@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;//姓名密文
}
@Data
@Entity
public class CipherData {
    @Id
    @GeneratedValue(strategy = AUTO)
    private Long id;
    private String iv;//初始化向量
    private String secureKey;//密钥
}

 加密服务使用 GCM 模式( Galois/Counter Mode)的 AES-256 对称加密算法,也就是 AES-256-GCM,这是一种AEAD(Authenticated Encryption with Associated Data)认证加密算法,除了能实现普通加密算法提供的保密性之外,还能实现可认证性和密文完整性,是目前最推荐的 AES 模式。使用类似 GCM 的 AEAD 算法进行加解密,除了需要提供初始化向量和密钥之外,还可以提供一个 AAD(附加认证数据,additional authenticated data),用于验证未 包含在明文中的附加信息,解密时不使用加密时的 AAD 将解密失败。其实,GCM 模式的内部使用的就是 CTR 模式,只不过还使用了 GMAC 签名算法,对 密文进行签名实现完整性校验。

我们实现基于 AES-256-GCM 的加密服务,包含下面的主要逻辑:加密时允许外部传入一个 AAD 用于认证,加密服务每次都会使用新生成的随机值作为密 钥和初始化向量。在加密后,加密服务密钥和初始化向量保存到数据库中,返回加密 ID 作为本次加密的标识。应用解密时,需要提供加密 ID、密文和加 密时的 AAD 来解密。加密服务使用加密 ID,从数据库查询出密钥和初始化向量。


@Service
public class CipherService {
    //密钥长度
    public static final int AES_KEY_SIZE = 256;
    //初始化向量长度
    public static final int GCM_IV_LENGTH = 12;
    //GCM身份认证Tag长度
    public static final int GCM_TAG_LENGTH = 16;
    @Autowired
    private CipherRepository cipherRepository;
    //内部加密方法
    public static byte[] doEncrypt(byte[] plaintext, SecretKey key, byte[] iv, byte[] aad) throws Exception {
        //加密算法
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        //Key规范
        SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), "AES");
        //GCM参数规范
        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
        //加密模式
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec);
        //设置aad
        if (aad != null)
            cipher.updateAAD(aad);
        //加密
        byte[] cipherText = cipher.doFinal(plaintext);
        return cipherText;
    }
    //内部解密方法
    public static String doDecrypt(byte[] cipherText, SecretKey key, byte[] iv, byte[] aad) throws Exception {
        //加密算法
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        //Key规范
        SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), "AES");
        //GCM参数规范
        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
        //解密模式
        cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec);
        //设置aad
        if (aad != null)
            cipher.updateAAD(aad);
        //解密
        byte[] decryptedText = cipher.doFinal(cipherText);
        return new String(decryptedText);
    }
    //加密入口
    public CipherResult encrypt(String data, String aad) throws Exception {
        //加密结果
        CipherResult encryptResult = new CipherResult();
        //密钥生成器
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        //生成密钥
        keyGenerator.init(AES_KEY_SIZE);
        SecretKey key = keyGenerator.generateKey();
        //IV数据
        byte[] iv = new byte[GCM_IV_LENGTH];
        //随机生成IV
        SecureRandom random = new SecureRandom();
        random.nextBytes(iv);
        //处理aad
        byte[] aaddata = null;
        if (!StringUtils.isEmpty(aad))
            aaddata = aad.getBytes();
        aaddata = aad.getBytes();
        //获得密文
        encryptResult.setCipherText(Base64.getEncoder().encodeToString(doEncrypt(data.getBytes(), key, iv, aaddata)));
        //加密上下文数据
        CipherData cipherData = new CipherData();
        //保存IV
        cipherData.setIv(Base64.getEncoder().encodeToString(iv));
        //保存密钥
        cipherData.setSecureKey(Base64.getEncoder().encodeToString(key.getEncoded()));
        cipherRepository.save(cipherData);
        //返回本地加密ID
        encryptResult.setId(cipherData.getId());
        return encryptResult;
    }
    //解密入口
    public String decrypt(long cipherId, String cipherText, String aad) throws Exception {
        //使用加密ID找到加密上下文数据
        CipherData cipherData = cipherRepository.findById(cipherId).orElseThrow(() -> new IllegalArgumentException("invlaid cipherId"));
        //加载密钥
        byte[] decodedKey = Base64.getDecoder().decode(cipherData.getSecureKey());
        //初始化密钥
        SecretKey originalKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");
        //加载IV
        byte[] decodedIv = Base64.getDecoder().decode(cipherData.getIv());
        //处理aad
        byte[] aaddata = null;
        if (!StringUtils.isEmpty(aad))
            aaddata = aad.getBytes();
        //解密
        return doDecrypt(Base64.getDecoder().decode(cipherText.getBytes()), originalKey, decodedIv, aaddata);
    }
}

我们可以让用户选择,如果需要保护二要素的话,就自己输入一个查询密码作为 AAD。系统需要读取用户敏感信息的时候,还需要用户提供这个密码,否 则无法解密。这样一来,即使黑客拿到了用户数据库的密文、加密服务的密钥和 IV,也会因为缺少 AAD 无法解密。

    @Autowired
    private CipherService cipherService;
    //加密
    @GetMapping("right")
    public UserData right(@RequestParam(value = "name", defaultValue = "test") 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();
        //使用AAD来解密姓名和身份证
        log.info("name : {} idcard : {}",
                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), "*");

{"id":1,"name":"朱*","idcard":"************** 1234","idcardCipherId":26346,"idcardCipherText":"t/wIh1XTj00wJP1Lt3aGzSvn9GcqQWEwthN58KKU4KZ4Tw==","nameCipherId":26347,"name CipherText":"+gHrk1 mWmveBMVUo+CYon8Zjj9QAtw=="} [21:46:00.079] [http-nio-45678-exec-6] [INFO ] [o.g.t.c.s.s.StoreIdCardController:102 ] - name : test idcard : 300000000000001234

错误的aad会抛出异常 javax.crypto.AEADBadTagException: Tag mismatch! at com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:578) at com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1116) at com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1053) at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853) at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446) at javax.crypto.Cipher.doFinal(Cipher.java:2164)

非对称加密

公钥密码算法。公钥密码是由一对密钥对构成的,使用公钥或者说加密密钥来加密,使用私钥或者说解密密钥来解密,公钥可以任意公开,私钥不能公 开。使用非对称加密的话,通信双方可以仅分享公钥用于加密,加密后的数据没有私钥无法解密,国密算法包括SM2,SM9。

  • 24
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ADRU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值