背景
这篇文章主要是记录下,sm4密钥生成的过程。因为对称加密暂时没什么好说的,分组加密的模式ECB和CBC等,优劣如果大家有疑问可以自行百度下。
先说下背景,是因为项目需要改造为sm4的前后端加解密算法,然后和前端同事一起改造。
问题
涉及到128bits的密钥生成出现了岔子,导致前端生成的密钥后端无法解密了,报的错误就是:密钥不是128bits。原因是前端随机生成了32长度的字符串,但却一直报上面的错误!由此,对密钥的生成产生了一些疑问。最终搞清楚了,下面说下这个过程。
密钥生成
先来科普下小常识,8bits=1byte,就是8位等于1字节,2个字节等于一个字符。
那sm4需要的是128bits的字符串,那怎么生成32长度字符串呢?那32长度的字符串是怎么来的呢?看代码很容易就看出来了。
java
new String(Hex.encodeHex(generateKey(DEFAULT_KEY_SIZE), false))
这段代码可以分三个来看:
java
byte[] key = generateKey(DEFAULT_KEY_SIZE);// length = 16
char[] keyCharArr = Hex.encodeHex(key, false);// length = 32
new String(keyCharArr , false));// length = 32
这就是说:随机来128比特的二进制,得到16字节数组,再用Hex把16字节的数组转化为32长度的字符数组,注意1字节=2个十六进制字符,所以到此生成了32长度的字符串。
注意!!!
按照这个思路反推前端同事随机生成32长度字符串,32字符理论上能推回是128bits,但是套到代码里确没有如愿!后来发现了问题,
十六进制(简写为hex或下标16)是一种基数为16的计数系统,逢16进1。通常用数字0、1、2、3、4、5、6、7、8、9和字母A、B、C、D、E、F(或其大写形式A~F)表示,其中,A~F表示10~15,这些称作十六进制数字
问题就在这里,随机生成的字符串超过了F,用到了其他的字母,导致无法反推,改为按照十六进制的正常表示生成随机字符串就没问题了。
SM4实现
Sm4加密算法Java工具类代码如下:
java
import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;
public class Sm4Util {
static {
Security.addProvider(new BouncyCastleProvider());
}
private static final String ENCODING = "UTF-8";
public static final String ALGORITHM_NAME = "SM4";
// 加密算法/分组加密模式/分组填充方式
// PKCS5Padding-以8个字节为一组进行分组加密
// 定义分组加密模式使用:PKCS5Padding
public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";
// 128-32位16进制;256-64位16进制
public static final int DEFAULT_KEY_SIZE = 128;
/**
* 自动生成密钥
*
* @return
* @explain
*/
public static String generateKey() throws Exception {
return new String(Hex.encodeHex(generateKey(DEFAULT_KEY_SIZE), false));
}
/**
* @param keySize
* @return
* @throws Exception
* @explain
*/
public static byte[] generateKey(int keySize) throws Exception {
KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
kg.init(keySize, new SecureRandom());
return kg.generateKey().getEncoded();
}
/**
* 生成ECB暗号
*
* @param algorithmName 算法名称
* @param mode 模式
* @param key
* @return
* @throws Exception
* @explain ECB模式(电子密码本模式:Electronic codebook)
*/
private static Cipher generateEcbCipher(String algorithmName, int mode, byte[] key) throws Exception {
Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME);
cipher.init(mode, sm4Key);
return cipher;
}
/**
* sm4加密
*
* @param hexKey 16进制密钥(忽略大小写)
* @param paramStr 待加密字符串
* @return 返回16进制的加密字符串
* @explain 加密模式:ECB
* 密文长度不固定,会随着被加密字符串长度的变化而变化
*/
public static String encryptEcb(String hexKey, String paramStr) {
try {
String cipherText = "";
// 16进制字符串-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// String-->byte[]
byte[] srcData = paramStr.getBytes(ENCODING);
// 加密后的数组
byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
// byte[]-->hexString
cipherText = ByteUtils.toHexString(cipherArray);
return cipherText;
} catch (Exception e) {
return paramStr;
}
}
/**
* 加密模式之Ecb
*
* @param key
* @param data
* @return
* @throws Exception
* @explain
*/
public static byte[] encrypt_Ecb_Padding(byte[] key, byte[] data) throws Exception {
Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(data);
}
/**
* sm4解密
*
* @param hexKey 16进制密钥
* @param cipherText 16进制的加密字符串(忽略大小写)
* @return 解密后的字符串
* @throws Exception
* @explain 解密模式:采用ECB
*/
public static String decryptEcb(String hexKey, String cipherText) {
// 用于接收解密后的字符串
String decryptStr = "";
// hexString-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// hexString-->byte[]
byte[] cipherData = ByteUtils.fromHexString(cipherText);
// 解密
byte[] srcData = new byte[0];
try {
srcData = decrypt_Ecb_Padding(keyData, cipherData);
// byte[]-->String
decryptStr = new String(srcData, ENCODING);
} catch (Exception e) {
e.printStackTrace();
/*解密失败,返回原报文*/
return cipherText;
}
return decryptStr;
}
/**
* 解密
*
* @param key
* @param cipherText
* @return
* @throws Exception
* @explain
*/
public static byte[] decrypt_Ecb_Padding(byte[] key, byte[] cipherText) throws Exception {
Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key);
return cipher.doFinal(cipherText);
}
/**
* 校验加密前后的字符串是否为同一数据
*
* @param hexKey 16进制密钥(忽略大小写)
* @param cipherText 16进制加密后的字符串
* @param paramStr 加密前的字符串
* @return 是否为同一数据
* @throws Exception
* @explain
*/
public static boolean verifyEcb(String hexKey, String cipherText, String paramStr) throws Exception {
// 用于接收校验结果
boolean flag = false;
// hexString-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// 将16进制字符串转换成数组
byte[] cipherData = ByteUtils.fromHexString(cipherText);
// 解密
byte[] decryptData = decrypt_Ecb_Padding(keyData, cipherData);
// 将原字符串转换成byte[]
byte[] srcData = paramStr.getBytes(ENCODING);
// 判断2个数组是否一致
flag = Arrays.equals(decryptData, srcData);
return flag;
}
}