加密算法

加密算法是一种通过对数据进行编码或转换,使其难以被未经授权的人解读或访问的方法,常见的加解密算法大致包括以下几类

  1. 对称加密算法:使用相同的密钥进行加密和解密,包括DES、3DES、AES等,对称加密算法适用于数据量较小的场景。
  2. 非对称加密算法:使用一对密钥(公钥和私钥)进行加密和解密,包括RSA、ECC等,非对称加密算法适用于需要安全性较高的场景。
  3. 哈希算法:将任意长度的消息压缩到固定长度的摘要中,包括MD5、SHA-1、SHA-256等,哈希算法适用于数据完整性校验场景。
  4. 消息认证码算法:对消息进行完整性校验和防篡改校验,包括HMAC等,消息认证码算法适用于防篡改场景。

AES加密模式:

加密算法的选择应根据具体需求和应用场景来决定,今天就来详细说说对称加密算法中的AES,AES是一种对称加密算法,全称为“高级加密标准”(Advanced Encryption Standard)。它是一个区块加密算法,适用于大多数应用场景,包括加密和解密,还可以在不同的平台和设备之间进行加密和解密操作,AES加密算法支持多种加密模式,常见的加密模式有以下五种:

AES加密算法支持多种加密模式,常见的加密模式有以下五种:

  1. ECB (Electronic CodeBook)

ECB是最简单的加密模式,它将明文按块处理,每个块独立加密,最后输出密文。这种加密方式的缺点是相同的明文块生成相同的密文块,因此不利于安全性。这种加密方式一般不建议使用。

  1. CBC (Cipher Block Chaining)

CBC模式是最常见的加密模式之一,它需要一个初始化向量(IV)。加密过程中,每个明文块与前一个密文块进行异或操作,然后再进行加密。这种加密方式的优点是不容易受到字典攻击,缺点是不容易并行处理,加密效率低于ECB,是SSL、IPSec的标准。

  1. CFB (Cipher FeedBack)

CFB模式是一种比较常见的流加密模式,它将明文按照位进行加密,每次处理一个比特位。加密过程中,将前一次的密文块作为下一次加密的输入,同时输出本次密文块。这种加密方式的优点是不需要填充,缺点是需要保存前一次的密文块。

  1. OFB (Output FeedBack)

OFB模式也是一种流加密模式,它的加密过程类似CFB,但是OFB不需要保存前一次的密文块,每次加密过程中,将前一次加密的输出作为本次的输入。这种加密方式的优点是不需要填充,缺点是不容易检测出位错误。

  1. CTR (Counter)

CTR模式是一种比较常见的分组加密模式,它的加密过程类似于OFB,但是CTR使用了不同的加密方式。CTR不需要填充,加密效率高于CBC和OFB,适用于加密大量数据。CTR模式需要一个计数器,每次加密时将计数器作为密钥与明文块进行异或操作,然后输出密文块。每次加密结束后,计数器+1,用于下一次加密。CTR模式可以与GCM模式结合使用,提供更强的安全性和认证机制。

为什么有的加密模式需要填充?

AES是一种块加密算法,块的大小固定为128位,因此也称为AES-128。块加密算法将明文按固定大小分成块,然后对每个块进行加密操作,从而得到密文。块加密算法需要对数据进行填充以保证其长度是块大小的整数倍,因为最后一个块可能会比其他块短,所以需要填充。块加密算法通常比流加密算法更安全,但是对于较长的数据流来说,需要更多的存储空间。

实现AES指定密钥和偏移量的加解密

在 Java 中,Cipher 类是一个提供加密和解密功能的抽象类。它提供了一个通用的加密或解密的接口,可供许多加密算法使用。该类提供了许多加密和解密操作,包括对对称加密算法(如 AES、DES、3DES)和非对称加密算法(如 RSA、DSA)的支持。

使用 Cipher 类加密或解密时,需要指定加密算法、加密模式和填充模式,形如

AES/CBC/PKCS5Padding
  1. 实现有指定的AES秘钥和指定偏移量AES加解密,代码如下:
package com.zlc.blog.aes;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * @author xiangcaimiao
 */
public class AesUtils {

    /**
     * AES 对称加密(RSA非对称加密)
     * CBC 有向量 (ECB 无向量)
     * PKCS5Padding 填充模式(NoPadding 无填充)
     */
    private static final String ALG_AES_CBC_PKCS5 = "AES/CBC/PKCS5Padding";

    private static final String ALGORITHM = "AES";

    private static final Charset UTF8 = StandardCharsets.UTF_8;

    /**
     * 指定好的秘钥,非Base64和16进制
     * 长度为16(128bit),24(192bit)或者32(256bit)
     */
    private static final String AES_KEY = "12e476beac1a4g20";

    /**
     * 偏移量16(用于构造IvParameterSpec)
     * IvParameterSpec可以直接使用默认的偏移量进行加解密,但如果使用自定
     */
    private static final String AES_IV = "2e119e58a526bc64";

    private static SecretKeySpec skeySpec;

    private static IvParameterSpec iv;

    /**
     * AES加密
     * @param plainText 明文
     * @return Base64编码的密文
     * @throws Exception 加密异常
     */
    public static String encrypt(String plainText) throws Exception {
        Cipher cipher = Cipher.getInstance(ALG_AES_CBC_PKCS5);
        skeySpec = new SecretKeySpec(AES_KEY.getBytes(), ALGORITHM);
        iv = new IvParameterSpec(AES_IV.getBytes());
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
        // 这里的编码格式需要与解密编码一致
        byte[] encryptText = cipher.doFinal(plainText.getBytes(UTF8));
        return Base64.getEncoder().encodeToString(encryptText);
    }

    /**
     * 解密方法
     * @param cipherStr Base64编码的加密字符串
     * @return 解密后的字符串(UTF8编码)
     * @throws Exception 异常
     */
    public static String decrypt(String cipherStr) throws Exception {
        // step 1 获得一个密码器
        Cipher cipher = Cipher.getInstance(ALG_AES_CBC_PKCS5);
        // step 2 初始化密码器,指定是加密还是解密(Cipher.DECRYPT_MODE 解密; Cipher.ENCRYPT_MODE 加密)
        // 加密时使用的盐来够造秘钥对象
        skeySpec = new SecretKeySpec(AES_KEY.getBytes(), ALGORITHM);
        // 加密时使用的向量,16位字符串(也可以不显示构造)
        // String AES_IV = "3e119e58a526bc62";
        iv = new IvParameterSpec(AES_IV.getBytes());
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
        // 对加密报文进行base64解码
        byte[] encrypted1 = Base64.getDecoder().decode(cipherStr);
        // 解密后的报文数组
        byte[] original = cipher.doFinal(encrypted1);
        // 输出utf8编码的字符串,输出字符串需要指定编码格式
        return new String(original, UTF8);
    }
}

单元测试结果如下图所示:
在这里插入图片描述

说明:

  • 如果是通过KeyGenerator生成的秘钥的话,他返回的是一个字节数组,需要进行Base64(不同Base64的实现得到的字符串结果可能不同)或者转换为16进制进行保存(或者将字节数组存放到文件或者数据库中,但不具有一般可读性)

  • Cipher 提供了多种方式的加密,通过Cipher.getInstance(“AES/CBC/PKCS5Padding”);参数按照 算法、模式、添加模式的方式来获取密码器。

  • Cipher常见构造密码器参数如下:

密码器参数位数
AES/CBC/NoPadding128
AES/CBC/PKCS5Padding128
AES/ECB/NoPadding128
AES/ECB/PKCS5Padding128
DES/CBC/NoPadding56
DES/CBC/PKCS5Padding56
DES/ECB/NoPadding56
DES/ECB/PKCS5Padding56
DESede/CBC/NoPadding168
DESede/CBC/PKCS5Padding168
DESede/ECB/NoPadding168
DESede/ECB/PKCS5Padding168
RSA/ECB/PKCS1Padding2024 2048
RSA/ECB/OAEPWithSHA-1AndMGF1Padding1024 2048
RSA/ECB/OAEPWithSHA-256AndMGF1Padding1024 2048
  1. 偏移量的作用

    AES/CBC模式下的偏移量长度必须为16字节(128位),因为AES算法的块长度是128位。如果偏移量长度不是16字节,则会抛出InvalidAlgorithmParameterException异常。

    在使用块密码模式加密时,需要提供一个初始向量(IV)来保证加密结果的唯一性和安全性,因此在加密和解密的过程中需要指定相同的 IV。如果 IV 不同,那么同一个明文可能会加密成不同的密文,这就会破坏加密算法的可靠性。

    在提供 IV 的情况下,如果 IV 不正确,解密过程会抛出异常,解密失败。在代码中,IV 被设置为 aesIv,并通过 new IvParameterSpec(aesIv.getBytes()) 传入到解密函数中,确保了解密时使用相同的 IV。

    如果在解密的过程中不提供 IV,那么 Cipher 会使用默认的 IV 值进行解密。默认的 IV 值通常是一个全 0 的字符串,这样在解密过程中可能会得到正确的结果。但是这种方式不安全,因为 IV 没有保密性,如果加密多个相同的明文,由于 IV 不变,加密结果也将相同,容易被攻击者破解。

    因此,为了确保加密算法的安全性,推荐在加密和解密的过程中都提供相同的 IV 值,并根据具体的加密模式选择不同的偏移量。

    如下为在加密阶段与解密阶段使用不同的偏移量得到被加密字符串与解密后的字符串不一致结果

在这里插入图片描述

AES 生成随机密钥并实现加解密

  1. 添加生成秘钥方法
private static final String SECURE_RANDOM_SEED= "123987";
public static SecretKey geneKey() throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
        // 固定随机种子,SecureRandom类还会使用系统当前时间和其他熵源来生成随机数。
        // 指定长度为16(128bit),24(192bit)或者32(256bit)
        // 如下方式不会生成固定的密钥
        keyGenerator.init(128, new SecureRandom(SECURE_RANDOM_SEED.getBytes()));
        // 随机源,每次生成的秘钥都会变,需要保存秘钥
        //keyGenerator.init(new SecureRandom());
        return keyGenerator.generateKey();
    }

生成的Base64秘钥为:

8183ncqKcLf4Ks0o8dd0Qg==
  1. 使用生成的秘钥进行加解密单元测试
@Test
    void testGeneAesKey(){
        String cipherStr;
        try {
            byte[] aesKey = AesUtils.geneKey().getEncoded();
            System.out.println("Base64后的密钥:" + Base64.getEncoder().encodeToString(aesKey));
            assertEquals(16, aesKey.length);
            cipherStr = AesUtils.encrypt(plainText, aesKey);
            System.out.println("AES 加密后的Base64报文:[ " + cipherStr + " ]");
            System.out.println("对加密后的报文解密后的明文为:[ " + AesUtils.decrypt(cipherStr, aesKey) + " ]");
            assertEquals(plainText, AesUtils.decrypt(cipherStr, aesKey));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

单元测试运行结果如下:
在这里插入图片描述
3. 说明

  • 如果使用AES/CBC/PKCS5Padding ,需要指定一个向量
  • KeyGenerator如果使用固定的随机源,则每次生成的秘钥都是一样的
  • 如果以后想要使用该密钥进行加解密需要将生成的密钥保存起来,因为上述生成密钥的方式在方法每次被调用的时候会生成新的密钥

生成固定的秘钥进行加解密

  1. 生成固定的秘钥
 private static final String PBE_KEY_ALGORITHM = "PBKDF2WithHmacSHA256";
    private static final char[] PASSWORD = "myPassword".toCharArray();
    private static final byte[] SALT = "mySaltValue".getBytes(StandardCharsets.UTF_8);
    private static final int ITERATIONS = 10000;
    private static final int KEY_LENGTH = 128;
public static SecretKey geneRegularKey() throws Exception {
        SecretKeyFactory factory = SecretKeyFactory.getInstance(PBE_KEY_ALGORITHM);
        PBEKeySpec spec = new PBEKeySpec(PASSWORD, SALT, ITERATIONS, KEY_LENGTH);
        return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), ALGORITHM);
    }

  1. 单元测试每次运行是否生成固定的密钥以及使用该密钥进行加解密
@Test
    void testGeneRegularKey(){
        String cipherStr;
        try {
            byte [] firstKey = AesUtils.geneRegularKey().getEncoded();
            byte [] secondKey = AesUtils.geneRegularKey().getEncoded();
            // assertEquals(firstKey, secondKey); false 生成的对象的地址不同
            assertEquals(Base64.getEncoder().encodeToString(firstKey),
                    Base64.getEncoder().encodeToString(secondKey));
            System.out.println("生成的固定key为:" + Base64.getEncoder().encodeToString(secondKey));
            cipherStr = AesUtils.encrypt(plainText, firstKey);
            System.out.println("AES 加密后的Base64报文:[ " + cipherStr + " ]");
            System.out.println("对加密后的报文解密后的明文为:[ " + AesUtils.decrypt(cipherStr, secondKey) + " ]");
            assertEquals(plainText, AesUtils.decrypt(cipherStr, firstKey));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

单元测试运行结果如下:
在这里插入图片描述

  1. 说明:

    PBEKeySpec是Java中用于生成基于口令的加密密钥的KeySpec接口的实现类。PBE即Password-Based Encryption,即基于口令的加密算法。它可以根据用户输入的密码和盐生成密钥,并用于加密和解密数据。

    PBE算法通常用于需要用户输入口令的场合,例如保护用户数据。PBE算法将用户输入的密码和随机生成的盐值结合使用,通过计算生成密钥。由于用户输入的密码可能比较短或者简单,容易受到字典攻击等方式的破解,因此通过使用盐值增加了加密强度。

DES与AES

在计算机领域,安全性是至关重要的。密码学技术是一种应对计算机安全问题的有效手段。其中,对称密钥加密算法是密码学技术中最常用的一种算法,其中 DES 和 AES 算法是应用最广泛的两种对称密钥加密算法。DES 算法使用56位的密钥,AES 算法则可以使用128位、192位或256位的密钥,因此 AES 算法相较于 DES 算法更加安全。

AES 算法具有许多优势。首先,AES 算法在硬件和软件中的实现效率比 DES 算法高,同时 AES 算法的安全性也更强。其次,AES 算法可以实现对大块数据的高效加密,使其在网络传输时更加安全。最后,由于 AES 算法可以使用更长的密钥长度,因此在安全性方面也更有优势。

尽管 AES 算法比 DES 算法更加安全和高效,但是在一些特殊场合下,DES 算法也有其独特的应用价值。例如,一些旧系统中可能只支持 DES 算法,因此必须使用 DES 算法进行加密和解密。此外,由于 DES 算法的密钥长度较短,因此在某些嵌入式设备中,DES 算法的实现可能更加方便。因此,在实际应用中,根据具体情况选择合适的对称密钥加密算法是非常重要的。

本文首发于香菜喵,打开微信随时随地读,文章下方 ↓ ↓ ↓

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐