1. 什么是对称加密
对称密钥算法(Symmetric-key algorithm),又称为私钥加密、共享密钥加密,是密码学中的一类加密算法。对称加密的特点是,在加密和解密时使用相同的密钥,或是使用两个可以简单地相互推算的密钥。常见的对称加密算法有 DES,3DES,AES,RC2,RC4,RC5,SM4等。
2. 对称加密的优缺点
优点:
加密计算量小,速度快,适合对大量数据进行加密的场景。
缺点:
密钥传输问题:如上所说,由于对称加密的加密和解密使用的是同一个密钥,所以对称加密的安全性就不仅仅取决于加密算法本身的强度,更取决于密钥是否被安全的保管,因此加密者如何把密钥安全的传递到解密者手里,就成了对称加密面临的关键问题。
密钥管理问题:再者随着密钥数量的增多,密钥的管理问题会逐渐显现出来。比如我们在加密用户的信息时,不可能所有用户都用同一个密钥加密解密,这样的话,一旦密钥泄漏,就相当于泄露了所有用户的信息,因此需要为每一个用户单独的生成一个密钥并且管理,这样密钥管理的代价也会非常大。
3. 分组模式
对称加密的一个主要的实现方式为分组加密:先将明文切分成一个个固定大小的块,再对每个块进行加密,这种方式被称为分组加密或块加密。
分组加密算法要解决的问题就是: 如何将分组后的各个块组织起来,协同实现明文整体的加解密。这个问题就是分组密码工作模式要解决的问题。常见的分组密码工作模式
- ECB : 电子密码本(Electronic codebook)
ECB, 电子密码本(Electronic codebook), 是最简单的分组模式。它对每个块使用同一个key进行独立的块加密。
- CBC : 密码块链接(Cipher-block chaining)
CBC, 密码块链接(Cipher-block chaining)。CBC同样是对每个块使用同一个key进行块加密,但CBC会在块加密之前,先将明文块与前一个密文块进行异或计算,然后再对异或计算结果进行块加密。
对于首个明文块,因为不存在前一个密文块,所以需要额外的一个字节数组来充当"前一个密文块",这个字节数组被称为初始化向量,IV。同一次加密和解密使用相同的IV,但原则上,不同的加解密回合应该使用不同的IV以确保随机性。
- PCBC : 填充密码块链接(Propagating cipher-block chaining), 也被称为 明文密码块链接(Plaintext cipher-block chaining)
PCBC, 填充密码块链接(Propagating cipher-block chaining), 也被称为明文密码块链接(Plaintext cipher-block chaining)。
PCBC在CBC的基础上,对异或计算的参数进行了改变。它在对明文块进行异或计算时,不是直接和前一个密文块进行异或,而是先将前一个明文块和密文块进行异或计算得到一个中间数组,然后再和当前明文块进行异或运算,然后再做块加密。
- CFB : 密文反馈(Cipher feedback)
CFB, 密文反馈(Cipher feedback), 该模式的流程与CBC类似,但对异或、块加密的使用则完全不同。
CBC是先对明文块和前一个密文块做异或计算,再对异或结果做块加密,而CFB是先对前一个密文块做块加密,再和明文块做异或计算。
- OFB : 输出反馈(Output feedback)
OFB, 输出反馈(Output feedback)。
OFB在CFB的基础上,对块加密的参数进行了改变。它在做块加密得到异或参数时,使用的不再是前一个密文块,而是前一个块加密得到的异或参数。
- CTR : 计数器模式(Counter mode), 也被称为 ICM整数计数模式(Integer Counter Mode), 或 SIC模式(Segmented Integer Counter)
CTR, 计数器模式(Counter mode), 也被称为ICM整数计数模式(Integer Counter Mode), 或SIC模式(Segmented Integer Counter)。
CTR模式与OFB类似,不同的地方在于,CTR使用块加密计算异或参数时,不再使用前一个异或参数作为块加密参数,而是用计数器获取的计数作为块加密参数。计数器的计数通过某种累加计算得出,首次计算使用初始化向量IV。计数器需要保证相同的块拿到相同的计数,不同的块拿到不重复的计数。
- GCM : 伽罗瓦/计数器模式(Galois/Counter Mode)。
GCM, 伽罗瓦/计数器模式(Galois/Counter Mode)。
GCM是在CTR模式的基础上,增加了认证操作的分组模式,这种模式也被称为认证加密模式。
在TLS协议中,握手结束后的通信使用的是对称加密。在较低的TLS版本中,主要使用CBC这种单纯的分组模式。这些分组模式不能保证消息的完整性与真实性,因此在对称加密结束后,还需要一个HMAC算法,用来计算消息的MAC并将MAC附在消息后面。这是起初的认证加密(Authenticated encryption, AE)的思路,加密和认证是分开的。
但无论是先对明文做HMAC再加密,还是先加密再进行HMAC,后来都被认为不够安全。于是密码学家们开始设计一种将加密和认证放在一起完成的模式,这就是authenticated encryption with associated data,简称AEAD,是AE的变种。TLS1.3的密码套件开始支持这种AEAD的模式,如GCM。AEAD是一种设计,一种接口;而GCM模式就是AEAD的一种具体实现。无论是AEAD还是AE,只要支持认证加密,一旦传播过程发生随机性误差或被篡改,那么解密过程就直接终止,因此误差传播这个特征就没有意了。
4. 填充方式
加密算法使用块加密的方式对明文进行加密,需要将明文划分为固定大小的块进行加密操作。如果明文的长度不足一个块大小,就需要用填充方式将其扩充至一个完整的块大小,否则加密过程会出错。填充方式的目的是使明文长度能够被块大小整除,同时还可以防止攻击者通过分析加密结果来推断明文的长度和内容,增强了加密算法的可靠性和安全性,下面列举一下常见的填充方式。
- NoPadding
不进行填充,但是这里要求明文必须要是16个字节的整数倍,这个可以使用者本身自己去实现填充,除了该种模式以外的其他填充模式,如果已经是16个字节的数据的话,会再填充一个16字节的数据
- PKCS#7_PADDING
每个填充字节的值都等于填充的字节数,例如,若需要填充5个字节,则填充的字节为0x05。这种填充方式可用于块大小在1-255的加密算法。
- PKCS#5_PADDING
PKCS#5_PADDING与PKCS#7_PADDING是一致的,支持的块大小不一样。v2.0以及更早版本,说明只能填充8字节块大小的数据。但是在PKCS#5_PADDING v2.1中,将AES加入进来了,所以PKCS#5_PADDING与PKCS#7_PADDING在AES中是一致的,没有区别,都是都支持16字节的块填充。
- ZeroPadding
在原始数据后面填充0,保证数据块长度一致,简单易用,但是存在缺点,比如数据块中可能有本来就是0的数据,会导致导致填充异常,无法区分是原始数据还是填充数据,实际中比较少用。
- ISO10126Padding
该填充方式,随机数填充到块长度的整数倍,填充数为需要填充的字节数,最后一个字节填充填充数。因为填充随机数不可预测,提高了数据的安全性,常用于AES和DES加密。
5. DES算法
DES 全称 Data Encryption Standard,是一种使用密钥加密的块算法。现在认为是一种不安全的加密算法,因为现在已经有用穷举法攻破 DES 密码的报道了。尽管如此,该加密算法还是运用非常普遍,是一种标准的加密算法,3DES 是 DES 的加强版本。
DES密钥必须是8的倍数,否则会报 java.security.InvalidKeyException: Wrong key size。DES 标准密钥就是56位,8个字符即8个字节,每个字节的最高位不用,即每个字节只用7位,8个字符正好是56位。
示例代码:
java
代码解读
复制代码
public class DESUtil { public static String encrypt(String plainText, String key) { try { SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "DES"); Cipher cipher = Cipher.getInstance("DES"); cipher.init(Cipher.ENCRYPT_MODE, keySpec); byte[] bytes = cipher.doFinal(plainText.getBytes()); return Base64Utils.encodeToString(bytes); }catch (Exception ex){ ex.printStackTrace(); return null; } } public static String decrypt(String cipherText, String key) { try{ SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "DES"); Cipher cipher = Cipher.getInstance("DES"); cipher.init(Cipher.DECRYPT_MODE, keySpec); byte[] bytes = Base64Utils.decodeFromString(cipherText); byte[] plainBytes = cipher.doFinal(bytes); return new String(plainBytes); }catch (Exception ex){ ex.printStackTrace(); return null; } } public static void main(String[] args) { String key = "12345678"; String cipherStr = DESUtil.encrypt("hello world", key); System.out.println("cipherStr: " + cipherStr); String plainStr = DESUtil.decrypt(cipherStr, key); System.out.println("plainStr: " + plainStr); } }
输出:
cipherStr: KNugLrX23UddguNoHIO7dw==
plainStr: hello world
6. AES算法
AES是一种对称加密算法,全称为“高级加密标准”(Advanced Encryption Standard)。它是一个区块加密算法,适用于大多数应用场景,包括加密和解密,还可以在不同的平台和设备之间进行加密和解密操作。
AES 是一种分组密码算法,它将明文数据分成固定大小的分组进行处理。每个分组的大小通常为 128 位(16 字节),也可以是 192 位或 256 位。在加密过程中,对每个分组使用相同的密钥和加密算法进行操作,将明文分组转换为密文分组。这种分组处理方式使得 AES 能够高效地处理大量数据,同时保证数据的安全性。
AES 支持不同长度的密钥,分别为 128 位、192 位和 256 位。密钥长度越长,加密的安全性越高,但计算复杂度也会相应增加。在实际应用中,需要根据安全需求和计算资源等因素选择合适的密钥长度。例如,对于一般的商业应用,128 位密钥通常已经能够提供足够的安全性;而对于一些对安全性要求极高的场景,如军事、金融等领域,可能会选择使用 256 位密钥。
示例代码:
java
代码解读
复制代码
public class AESUtil { public static String encrypt(String plainText, String key) { try { SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec); byte[] bytes = cipher.doFinal(plainText.getBytes()); return Base64Utils.encodeToString(bytes); }catch (Exception ex){ ex.printStackTrace(); return null; } } public static String decrypt(String cipherText, String key) { try { SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, keySpec); byte[] bytes = Base64Utils.decodeFromString(cipherText); byte[] plainBytes = cipher.doFinal(bytes); return new String(plainBytes); }catch (Exception ex){ ex.printStackTrace(); return null; } } public static void main(String[] args) { String key = "1234567890123456"; String cipherStr = AESUtil.encrypt("hello world", key); System.out.println("cipherStr: " + cipherStr); String plainStr = AESUtil.decrypt(cipherStr, key); System.out.println("plainStr: " + plainStr); } }
输出
cipherStr: roLzT3GBhVQw22WrUPAdsw==
plainStr: hello world 下面演示一下AES采用CBC分组模式的加密算法:
java
代码解读
复制代码
public class AESUtil { public static String encryptByCBC(String plainText, String key, String iv){ try { SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES"); IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes()); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] bytes = cipher.doFinal(plainText.getBytes()); return Base64Utils.encodeToString(bytes); }catch (Exception ex){ ex.printStackTrace(); return null; } } public static String decryptByCBC(String cipherText, String key, String iv){ try { SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES"); IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes()); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] cipherBytes = Base64Utils.decodeFromString(cipherText); byte[] bytes = cipher.doFinal(cipherBytes); return new String(bytes); }catch (Exception ex){ ex.printStackTrace(); return null; } } public static void main(String[] args) { String key = "1234567890123456"; String iv = "9876543210654321"; String cipherStr = AESUtil.encryptByCBC("This is a test plainText", key, iv); System.out.println("cipherStr: " + cipherStr); String plainStr = AESUtil.decryptByCBC(cipherStr, key, iv); System.out.println("plainStr: " + plainStr); } }
7. SM4算法
SM4 是一种中国国家密码标准(GB/T 32907-2016)中定义的分组加密算法,又称为“中国商用密码算法SM4”。它是由中国国家密码管理局发布的,并广泛应用于金融、电子商务和其他需要数据加密的场景。
SM4 是一种对称加密算法,意味着加密和解密使用相同的密钥。它是一个 128 位(16 字节)分组密码,意味着它将明文分成 128 位的块,然后对每个块进行加密。SM4 使用了 128 位的密钥进行加密和解密。
SM4 算法的主要特点是:
分组长度:128 位。
密钥长度:128 位。
分组模式:支持多种模式,如ECB(电子密码本)、CBC(加密分组链接)、CTR(计数器模式)等。
示例代码:
首先引用maven依赖包:
xml
代码解读
复制代码
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>1.78.1</version> </dependency>
Java代码示例:
java
代码解读
复制代码
public class SM4Util { static { if(Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null){ Security.addProvider(new BouncyCastleProvider()); } } public static String encrypt(String plainText, String key) { try{ SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "SM4"); Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding", BouncyCastleProvider.PROVIDER_NAME); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); byte[] bytes = cipher.doFinal(plainText.getBytes()); return Base64Utils.encodeToString(bytes); }catch (Exception ex){ ex.printStackTrace(); return null; } } public static String decrypt(String cipherText, String key) { try{ SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "SM4"); Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding", BouncyCastleProvider.PROVIDER_NAME); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); byte[] cipherBytes = Base64Utils.decodeFromString(cipherText); byte[] bytes = cipher.doFinal(cipherBytes); return new String(bytes); }catch (Exception ex){ return null; } } public static void main(String[] args) { String key = "1234567890123456"; String cipherStr = SM4Util.encrypt("this is sm4 test", key); System.out.println("cipherStr: " + cipherStr); String plainStr = SM4Util.decrypt(cipherStr, key); System.out.println("plainStr: " + plainStr); } }
输出:
cipherStr: UptAXiDiT7j98cIKRwdAt2yI9zmvKimnNTgfVne63vc=
plainStr: this is sm4 test