1. 对称加密

对称加密,加密和解密使用相同的秘钥,加密速度快、效率高。常见的有 DES(淘汰)、3DES(淘汰)、AES(用于替代 DES,是目前常用的)等。

image.png

加密解密

1.1. DES(Data Encryption Standard)

DES 现在认为是一种不安全的加密算法,已经有用穷举法攻破 DES 密码的报道了。3DES 是 DES 的加强版本(也被淘汰),是 DES 向 AES 过渡的加密算法。

1.2. AES(Advanced Encryption Standard)

AES 把明文按每组16个字节分成一组一组的、长度相等的数据,每次加密一组,直到加密完整个明文。在 AES 标准中,分组长度只能是128位,但是密钥的长度可以使用128位、192位或256位。

下面先来了解“分组加密机制、填充模式、初始向量、加密模式”等基本概念,最后给出 Java 代码示例。

1.2.1. 分组密码体制

分组密码体制就是指将明文切成一段一段的来加密,而且每段数据的长度要求必须是128位16个字节,如果最后一段不够16个字节了,就需要用 Padding 来把这段数据填满16个字节,然后再把一段一段的密文拼起来形成最终密文的加密方式。

1.2.2. 填充模式 Padding

Padding 就是用来把不满16个字节的分组数据填满16个字节用的,它有三种模式 PKCS5、PKCS7 和 NOPADDING。

  • PKCS5 是指分组数据缺少几个字节,就在数据的末尾填充几个字节的几,比如缺少5个字节,就在末尾填充5个字节的5。

  • PKCS7 是指分组数据缺少几个字节,就在数据的末尾填充几个字节的0,比如缺少7个字节,就在末尾填充7个字节的0。

  • NoPadding 是指不需要填充,也就是说数据的发送方肯定会保证最后一段数据也正好是16个字节。

在 PKCS5 模式下,有这样一种特殊情况,假设最后一段数据的内容刚好就是16个16,这时解密端怎么区分是填充还是数据呢?

对于这种情况,PKCS5 模式会自动帮我们在最后一段数据后再添加16个字节的数据,而且填充数据也是16个16,这样解密端就能知道谁是有效数据谁是填充数据了。同样的道理,PKCS7 最后一段数据的内容是16个0。

解密端需要使用和加密端同样的 Padding 模式,才能准确的识别有效数据和填充数据。开发通常采用 PKCS7 Padding 模式。

1.2.3. 初始向量 IV

初始向量 IV 的作用是使加密更加安全可靠。使用 AES 加密时要主动提供初始向量,而且只需提供一个初始向量就够了,后面每段数据的加密向量都是前面一段的密文。初始向量 IV 的长度规定为128位16个字节,初始向量通常采用随机生成。

1.2.4. 密钥

AES 要求密钥的长度可以是128位、192位或者256位,位数越高,加密强度越大,但是加密的效率自然会低一些,因此要做好衡量。

1.2.5. 分组加密模式

分组加密算法只能对固定长度的分组进行加密,面对超过分组长度的明文,就需要对分组密码算法进行迭代,以便将很长的明文全部加密。而迭代的方法就称为“分组加密的模式”。分组密码的模式有很多,常见的有:

  • ECB(电子密码本模式 Electronic Codebook Book),相对的不安全,很少使用

  • CBC(密码分组链接模式 Cipher Block Chaining),不支持并行计算,比 ECB 模式多了一个初始向量 IV,是这些模式中最安全的,也是最常用的模式

  • CFB(密码反馈模式 Cipher FeedBack),可被施以”重放攻击“

  • OFB(输出反馈模式 Output FeedBack),可被主动攻击者反转密文而引起解密后明文中的相应比特也发生变化

  • CTR(计数器模式 Counter mode),与 OFB 一样可被主动攻击者反转密文,但比 OFB多了支持并发计算的特性

AES 和 RSA 都属于分组加密算法。

1.2.6. AES Java 示例

AES-128-CBC 加解密

public static String cbcEncrypt(String plain, String key, String ivSeed) {
    Assert.notNull(plain, "plain must not be null");
    Assert.notNull(key, "key must not be null");
    Assert.notNull(ivSeed, "ivSeed must not be null");
    Assert.isTrue(ivSeed.length() == 16, "ivSeed must be 16 bytes");
    String base64 = null;
        
    try {
        // 生成秘钥
        SecretKeySpec keySpec = createKey(key);
        // 设置算法/模式/填充方式
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        // 设置偏移
        IvParameterSpec iv = new IvParameterSpec(ivSeed.getBytes(StandardCharsets.UTF_8));
        // 加密模式
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
        // 加密
        byte[] encrypted = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
        // 转 base64
        base64 = Base64.getEncoder().encodeToString(encrypted);
    } catch (Exception ex) {
        log.error("exception: {}", ex.getMessage());
    }

    return base64;
}

public static String cbcDecrypt(String base64, String key, String ivSeed) {
    Assert.notNull(base64, "base64 must not be null");
    Assert.notNull(key, "key must not be null");
    Assert.notNull(ivSeed, "ivSeed must not be null");
    Assert.isTrue(ivSeed.length() == 16, "ivSeed must be 16 bytes");

    String plain = null;
    try {
        // base64 解码
        byte[] decodedBase64 = Base64.getDecoder().decode(base64);
        // 生成秘钥
        SecretKeySpec keySpec = createKey(key, isBase64Key);
        if(null != keySpec) {
            // 设置偏移
            IvParameterSpec iv = new IvParameterSpec(ivSeed.getBytes(StandardCharsets.UTF_8));
            // 设置算法/模式/填充方式
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            // 解密模式
            cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
            // 解密
            byte[] decrypted = cipher.doFinal(data);
            // 转 base64
            plain = new String(decrypted, StandardCharsets.UTF_8);
        }
    } catch (Exception ex) {
        log.error("exception: {}", ex.getMessage());
    }

    return plain;
}

private static SecretKeySpec createKey(String key) {
    Assert.notNull(key, "key must not be null");

    byte[] bytesKey = key.getBytes(StandardCharsets.UTF_8);
    Assert.isTrue(bytesKey.length == 16, "key must be 16 bytes");

    // 生成秘钥
    return new SecretKeySpec(bytesKey, "AES");
}

2. 非对称加密

非对称加密算法,需要两个密钥, 一个是公钥 (public key),公开,任何人都可以获取;另一个是 私钥 (private key),不公开,由个人保存在安全的地方。公钥用于加密,私钥用于解密。

image.png

RSA (三位数学家名字的缩写)算法是第一个能同时用于 加密 和 数字签名 的非对称加密算法,它能够 抵抗 到目前为止已知的 所有密码攻击,已被 ISO 推荐为公钥数据加密标准。

2.1. 使用场景

假设 A 和 B 之间要进行加密通信,那么:

(1)B向A发送加密数据

  • A 生成一对密钥,私钥由 A 自己保留不公开;而公钥传给 B,公开,任何人可以获取

  • B 用该公钥对消息进行加密,并发送给 A

  • A 接收到加密消息后,用私钥对消息进行解密

在这个过程中,只有2次传递过程,第一次是 A 传递公钥给 B,第二次是 B 传递加密消息给A,即使都被截获,也没有危险性,因为只有 A 的私钥才能对消息进行解密,防止了消息内容的泄露。

(2)A向B发送“已收到”回复

  • A 用私钥对消息加签形成签名,并将加签的消息和消息本身一起传递给 B

  • B 收到消息后,用公钥进行验签,如果验签出来的内容与消息本身一致,证明消息是 A 回复的。

在这个过程中,算上前面的传递公钥,也只有2次传递过程,一次是传递公钥,第二次就是 A 传递加签的消息和消息本身给 B,即使都被敌方截获,也同样没有危险性,因为只有 A 的私钥才能对消息进行签名,即使知道了消息内容,也无法伪造带签名的回复给 B,防止了消息内容的篡改。

但是,综合上面两个场景会发现:

  • 第一个场景虽然被截获的消息没有泄露,但是可以利用截获的公钥,将假指令进行加密,然后传递给 A。

  • 第二个场景虽然截获的消息不能被篡改,但是消息的内容可以利用公钥验签来获得,并不能防止泄露。

所以在实际应用中,要根据情况使用,可以双方同时使用加密和签名,比如 A 和 B 都有一套自己的公钥和私钥,当 A 要给 B 发送消息时,先用 B 的公钥对消息加密,再对加密的消息使用 A 的私钥加签名,达到既不泄露也不被篡改,更能保证消息的安全性。

2.2. RAS 加密算法

2.2.1. 填充模式 Padding

Padding 常见模式如下表:

Padding 模式

RSA 常用的加密填充模式

  • RSA/None/PKCS1Padding(Java 默认的 RSA 实现)

  • RSA/ECB/PKCS1Padding

2.2.2. RSA Java 示例

/**
 * 公钥加密
 */
public static String ecbEncrypt(String data, String publicKeyBase64) throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidKeySpecException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    
    cipher.init(Cipher.ENCRYPT_MODE, toPublicKey(publicKeyBase64));
    byte[] bytes cipher.doFinal(data.getBytes());
    
    return Base64.getEncoder().encodeToString(bytes);
}

/**
 * 私钥解密
 */
public static String ecbDecrypt(String base64, String privateKeyBase64) throws IllegalBlockSizeException, InvalidKeyException,
InvalidKeySpecException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
    byte[] bytes = Base64.getDecoder().decode(base64.getBytes());

    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    cipher.init(Cipher.DECRYPT_MODE, toPrivateKey(privateKeyBase64));

    return new String(cipher.doFinal(data));
}

/**
 * 生成随机密钥对
 */
public static HashMap<String, String> randomKeyPair() throws NoSuchAlgorithmException {
    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
    generator.initialize(2048);
    KeyPair pair = generator.generateKeyPair();
    if (null == pair) {
        return null;
    }

    PrivateKey pvt = pair.getPrivate();
    PublicKey pub = pair.getPublic();

    Base64.Encoder encoder = Base64.getEncoder();
    String pvtVal = encoder.encodeToString(pvt.getEncoded());
    String pubVal = encoder.encodeToString(pub.getEncoded());

    HashMap<String, String> rsaKeyMap = new HashMap<>(2);
    rsaKeyMap.put("privateKeyBase64", pvtVal);
    rsaKeyMap.put("publicKeyBase64", pubVal);

    return rsaKeyMap;
}

/**
 * privateKeyBase64 私钥转为 PrivateKey 对象
 */
private static PrivateKey toPrivateKey(String privateKeyBase64) throws NoSuchAlgorithmException, InvalidKeySpecException {
    byte[] bytes = Base64.getDecoder().decode(privateKeyBase64.getBytes());

    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
    KeyFactory kf = KeyFactory.getInstance("RSA");

    return kf.generatePrivate(keySpec);
}

/**
 * publicKeyBase64 公钥转为 PublicKey 对象
 */
private static PublicKey toPublicKey(String publicKeyBase64) throws NoSuchAlgorithmException, InvalidKeySpecException {
    byte[] bytes = Base64.getDecoder().decode(publicKeyBase64.getBytes());

    X509EncodedKeySpec ks = new X509EncodedKeySpec(bytes);
    KeyFactory kf = KeyFactory.getInstance("RSA");

    return kf.generatePublic(ks);
}

3. 签名

加密是为了防止信息被泄露,而签名是为了防止信息被篡改和伪造。

签名 & 验签

3.1. 摘要算法

哈希函数(Hash function),又称散列函数、散列算法,也叫摘要算法,它是一种不可逆的信息摘要算法。

好的散列算法具备如下特性:

  • 单向性(one-way)即不可逆

  • 抗冲突性(collision-resistant)即产生两个相同散列值的概率很低(但输入相同,则输出的结果一定相同)

  • 雪崩效应(avalanche effect)即原始数据的微小改动,会导致散列值的巨大差异

常见的用途:

  • 密码保护:把用户密码通过散列函数加密保存(保存散列值),只有用户自己知道密码的明文

  • 签名 & 验签:比如对接口调用、对消息进行签名,接收方进行验签

  • 数据完整性/一致性校验:比如网上提供的文件下载通常都提供散列值和算法,便于用户校验

  • 数据秒传:上传几个G的大文件只用几秒,就是通过对比文件的散列值实现的,散列值(信息的指纹)相同就认为是同一个文件

常见的散列算法有”报文摘要算法 MD“、”安全散列算法 SHA“,以及”消息认证码算法 MAC“。

摘要算法

3.1.1. 报文摘要算法(MD系列)

信息摘要算法(Message-Digest Algorithm)。最常用的是 MD5 (Message-Digest Algorithm 5),是一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),常用于确保信息传输完整一致。

3.1.2. 安全散列算法(SHA系列)

安全散列算法(Secure Hash Algorithm)是一种不可逆的信息安全算法,经过量化运算和转换,可以把任意长度的数据生成不可逆的、固定长度的字符串,这个固定长度的字符串就是对相应的原始输入字符串的散列(也称为摘要),可以作为信息的指纹。

SHA-224,SHA-256,SHA-384,SHA-512 统称为 SHA-2,而 SHA-1 算法已经不够安全,不建议继续使用。

3.1.3. 消息认证码(MAC系列)

消息认证码算法(Message Authentication Code)是含有加密密钥的散列算法,它在 MD 和 SHA 算法特性的基础上加入了加密密钥,通过特别的计算方式来构造消息认证码(MAC)的方法。因 MAC 算法融合了密钥散列函数,通常也称为 HMAC 算法(Hash-based Message Authentication Code,散列消息认证码)。

常见的有:HMAC-SHA224、HMAC-SHA256、HMAC-SHA384、HMAC-SHA512

3.2. 签名验签原理

3.2.1. 签名

对需要发送的报文 originData 计算摘要(相关摘要算法有 md5、sha256等)特征值 signBlock。使用私钥 privateKey 对 signBlock 加密获得数字签名 signatureData。将 signatureData 与 originData 打包发一起送给对方。

3.2.2. 验签

接收方接收到数据后,把消息拆分为 signatureData 与 originData 。对 originData 计算特征值 signBlock,使用的算法必须要和发送方一致。使用公钥 publicKey 对 signatureData 解密,获得 signBlock1。比较 signBlock 和 signBlock1,若匹配则验证成功,报文未被篡改。

3.3. RSA 签名示例

/**
* 用私钥对数据进行签名并返回签名后的base64
* @param data 代签名的字符串
* @param base64PrivateKey 私钥
*/
public static String sign(String data, String base64PrivateKey) 
throws InvalidKeySpecException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
    PrivateKey key = toPrivateKey(base64PrivateKey);
    Signature signature = Signature.getInstance("SHA256withRSA");
    signature.initSign(key);
    signature.update(data.getBytes());
    return new String(Base64.getEncoder().encode(signature.sign()));
}

/**
* 验签
* @param data 原始数据
* @param base64PublicKey 公钥
* @param sign 私钥签名后的数据
*/
public static boolean verify(String data, String base64PublicKey, String sign) 
throws InvalidKeySpecException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
    PublicKey key = toPublicKey(base64PublicKey);
    Signature signature = Signature.getInstance("SHA256withRSA");
    signature.initVerify(key);
    signature.update(data.getBytes());
    return signature.verify(Base64.getDecoder().decode(sign.getBytes()));
}

/**
* base64PrivateKey 私钥转为 PrivateKey 对象
*/
private static PrivateKey toPrivateKey(String base64PrivateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
    byte[] bytes = Base64.getDecoder().decode(base64PrivateKey.getBytes());

    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
    KeyFactory kf = KeyFactory.getInstance("RSA");

    return kf.generatePrivate(keySpec);
}
/**
* base64PublicKey 公钥转为 PublicKey 对象
*/
private static PublicKey toPublicKey(String base64PublicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
    byte[] bytes = Base64.getDecoder().decode(base64PublicKey.getBytes());
    X509EncodedKeySpec ks = new X509EncodedKeySpec(bytes);
    KeyFactory kf = KeyFactory.getInstance("RSA");
    return kf.generatePublic(ks);
}

小结

本文主要介绍了常用的对称加密算法、非对称加密算法;常用的摘要算法、签名算法。以及使用算法需要了解的基本概念(比如填充模式、IV等),算法的使用场景,并且分别给出了 Java 示例代码。希望对各位小伙伴们有帮助哦

转自:https://zhuanlan.zhihu.com/p/645287116