一、概述
国密算法定义:即国家密码局认定的国产密码算法。
通过定义我们可以知道,国密算法有两个要素:
1、国家密码局认定
在国家密码局官网上,可以看到由其发布的标准规范。
2、密码算法
首先知道什么是密码,密码就是将正常的信息加密后变为无法正常识别的编码,可以认为是一种混淆技术。
将明文数据通过密码算法变成密文后,有三个优点:
(1)、数据保密,混淆后的密文一般无直接意义。
(2)、数据完整,混淆后的密文缺失无法正常恢复。
(3)、身份验证,数据加解密或者对照,都要有密钥或算法的信息,一般可以认为是自己人。
严格意义上的加密是必须保证能恢复明文信息的,但是我们平时说的一些加密,又要要求不能恢复原来的信息,如账号密码里的密码,一般是要求不允许恢复原文的。这时候就出现了一个问题,加密后还能不能恢复成加密前的样子?技术上能恢复的叫加密算法,不能恢复的叫哈希算法。能恢复明文的算法里又分为两种:对称加密和非对称加密。
对称加密:加密时使用的密钥和解密时使用的密钥为同一个。
非对称加密:加解密时使用的密钥是一对,公开密钥和私有密钥,公开密钥用于数据加密,私有密钥用于数据解密。
哈希算法:通过算法对明文数据进行混淆,同一个明文在同一个哈希算法下混淆结果一致。
具体分类可见下表
下图是实际开发中遇到的要求:
为什么用国密算法
(1)、国密算法算法好,速度快。
(2)、支持国产。
(3)、有些企业或部门要求使用,从最近的情况来看,还是自家的比较安全。
与DES和AES算法相似,国密SM4算法是一种分组加密算法。SM4分组密码(block cipher)算法是一种迭代分组密码算法,由加解密算法和密钥扩展算法组成。
SM4是一种Feistel结构的分组密码算法,其分组长度和密钥长度均为128bits。加密算法和密钥扩展算法迭代轮数均为32轮。SM4加解密过程的算法相同但是轮密钥的使用顺序相反。
SM4密码算法使用模2加和循环移位作为基本运算。
密钥扩展算法:SM4算法使用128位的加密密钥,并采用32轮迭代加密结构,每一轮加密使用一个32位的轮密钥,总共使用32个轮密钥。因此需要使用密钥扩展算法,从加密密钥中产生32个轮密钥。
SM4算法使用128位的加密密钥,而UUID是一个128位的二进制数,故我们可以使用UUID来当作SM4算法的密钥,注意:需要将UUID的横杠-替换成空字符串。
二、SM4加解密流程
SM4算法的加密大致流程如下:
密钥:加密密钥的长度为128比特,表示为MK = (MK0, MK1, MK2, MK3),其中MKi为32位,
轮密钥表示为(rk0, rk1, ……, rk31),其中rki为32位。
轮函数F:假设输入为(X0, X1, X2, X3),Xi 为32位,则轮函数F为:F=(X0, X1, X2, X3, rk) = X0 ⊕ T(X1 ⊕ X2 ⊕ X3 ⊕ rk)
合成置换:T函数是一个可逆变换,由一个非线性变换r和线性变换L复合而成的,即T( )=L(r( ))
非线性变换有四个并行的S盒构成的,设输入为A=(a0, a1, a2, a3),输出为B=(b0, b1, b2, b3),其中ai和bi为8位。每个S盒的输入都是一个8位的字节,将这8位的前四位对应的16进制数作为行编号,后四位对应的16进制数作为列编号,然后用相应位置中的字节代替输入的字节。下图为S盒置换表:
线性变换L:线形变换的输入就是S盒的输出,即C=L(B)=B ⊕ (B<<<2) ⊕ (B<<<10) ⊕ (B<<<18) ⊕ (B<<<24),线性变换的输入和输出都是32位的。
经过了32轮的迭代运算后,最后再进行一次反序变换即可得到加密的密文,即密文C=(Y0, Y1, Y2, Y3)=R(X32. X33, X34, X35)=(X35, X34, X33, X32)。
SM4算法的解密流程和加密流程一致,只不过轮密钥的使用顺序变成了(rk31, rk30, ……, rk0)
三、密钥扩展算法
密钥参量:轮密钥由加密密钥生成。FK=(FK0, FK1, FK2, FK3)为系统参数,以及固定参数CK=(CK0, CK1, ……, CK31),其中FKi和CKi均为32位并用于密钥扩展算法。
系统参数FK的具体取值如下:
FK0=(A3B1BAC6), FK1=(56AA3350), FK2=(677D9197), FK3=(B27022DC)
固定参数CK的具体取值如下:
密钥扩展方法:设(K0, K1, K2, K3)=(MK0⊕FK0, MK1⊕FK1, MK2⊕FK2, MK3⊕FK3)
则rki=Ki+4=Ki⊕T‘(Ki+1⊕Ki+2⊕Ki+3⊕CKi)
其中T’()是将原来的T()中的线形变换L()替换成L'(B)=B⊕(B<<<13)⊕(B<<<23)
四、Sm4Utils工具类
代码如下:
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; /** * 国密sm4算法 * author: dongliyuan */ public class Sm4Utils { static { // 防止内存中出现多次BouncyCastleProvider的实例 if(null == Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)) { Security.addProvider(new BouncyCastleProvider()); } } private static final String ENCODING = "UTF-8"; private 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; public static final String CCBID = "7bc1b525c0964716bc2f1dbc97e316be"; /** * 生成密钥 * 建议使用org.bouncycastle.util.encoders.Hex将二进制转成HEX字符串 * @return 密钥16位 * @throws Exception */ public static byte[] generateKey() throws Exception { KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME); kg.init(DEFAULT_KEY_SIZE, new SecureRandom()); return kg.generateKey().getEncoded(); } /** * 生成ECB暗号 * @param algorithmName 算法名称 * @param mode 模式 * @param key * @return * @throws Exception */ 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; } /** * 加密 * @param hexKey 16进制字符串 * @param paramStr 待加密字符 * @return 加密后的结果 * @throws Exception */ public static String encryptEcb(String hexKey, String paramStr) throws Exception { 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; } /** * 加密模式Ecb * @param key * @param data * @return * @throws Exception */ 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 */ public static String decryptEcb(String hexKey, String cipherText) throws Exception { //用于接收解密后的字符串 String decryptStr = ""; //hexString ---> byte[] byte[] keyData = ByteUtils.fromHexString(hexKey); // hexString --->byte[] byte[] cipherData = ByteUtils.fromHexString(cipherText); //解密 byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData); //byte[] ---> String decryptStr = new String(srcData); return decryptStr; } /** * 解密 * @param key * @param cipherText * @return * @throws Exception */ 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); } public static boolean verifyEcb(String hexKey, String cipherText, String paramStr) throws Exception { //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个数组是否一致 return Arrays.equals(decryptData, srcData); } public static void main(String[] args) throws Exception { System.out.println(encryptEcb(CCBID,"pts")); System.out.println(encryptEcb(CCBID,"ptS#1234")); } }
使用工具类
我们可以使用UUID来作为SM4算法的密钥,只不过要将横杠-替换为空字符串,代码如下:
String salt = UUID.randomUUID().toString().replace("-", "");
自定义密钥:ad07f399bc438b4777bba85bfa05ca28
public class Sm4Test { public static void main(String[] args) { JSONObject jsonObject = new JSONObject(); jsonObject.put("userNo","zhangsan"); jsonObject.put("passwd","1234"); try { System.out.println(Sm4Util.encryptEcb("ad07f399bc438b4777bba85bfa05ca28",JSONObject.toJSONString(jsonObject))); System.out.println(Sm4Util.decryptEcb("ad07f399bc438b4777bba85bfa05ca28","9a0fd3c7ad5e41681766171c08522db37065b4327a915de2dfb03d321d8df87190bbfc67ce38b9a028b8e066bff46c53")); } catch (Exception e) { e.printStackTrace(); } } }
结果如下:
9a0fd3c7ad5e41681766171c08522db37065b4327a915de2dfb03d321d8df87190bbfc67ce38b9a028b8e066bff46c53 {"userNo":"zhangsan","passwd":"1234"}
五、微信小程序国密算法实现库sm-crypto
1、sm-crypto安装
使用此组件需要依赖小程序基础库 2.2.1 以上版本,同时依赖开发者工具的 npm 构建。
微信小程序使用npm引入三方包详解
(1)、创建package.json文件
若没有package.json文件,直接在小程序项目根文件夹下,使用终端输入如下命令初始化环境:
npm init
注:使用该命令需要电脑安装好node环境;初次init直接一路回车即可,在package name:
输入包名 sm-crypto
查看node和npm配置情况
若发现无法查找命令node和npm,证明还未安装nodejs,可以参考下面博客链接进行安装配置node安装进
输入yes
(2)、修改project.private.config.json配置
project.private.config.json文件配置会覆盖project.config.json文件配置,需要修改project.private.config.json里面的setting
,初学者可以直接删除setting。
(3)、使用以下指令进行安装,打开终端,cd到小程序项目的根目录,在终端里执行以下命令:
npm install --save miniprogram-sm-crypto
(4)、构建 npm
点击 工具 -> 构建 npm,构建完成,项目根目录多出miniprogram_npm文件夹
构建完成后,即可引入组件。
2、加密
引入:
const sm4 = require('miniprogram-sm-crypto').sm4
对请求体进行加密
var data = { userNo: userno, passwd: password }; var encryptData = sm4.encrypt(JSON.stringify(data), app.globalData.key)
后台返回数据后解密
let decryptData = sm4.decrypt(res.data.result, app.globalData.key);
完整代码:
formSubmit: function(e) { var obj = e.detail.value; var userno = obj.userno.trim(); var password = obj.password.trim(); var data = { userNo: userno, passwd: password }; var encryptData = sm4.encrypt(JSON.stringify(data), app.globalData.key) if (userno == '' || password == '') { wx.showToast({ title: '请输入账号和密码', icon: 'none', duration: 2000 }) } else { wx.showLoading({ title: '登录中...', }); wx.request({ method: 'POST', url: app.globalData.serverApi + "/login", header: { 'content-type': 'application/json', 'appKey': 'app_key' }, data: { applyData: encryptData }, success(res) { wx.hideLoading(); if (res.data.code == 0) { //登录成功 wx.showToast({ title: res.data.message, icon: 'success', duration: 2000 }) let decryptData = sm4.decrypt(res.data.result, app.globalData.key); //存到缓存 wx.setStorageSync('userInfo', JSON.parse(decryptData)); //跳转到首页 wx.reLaunch({ url: '/pages/codeActive/index' }) } else { //登录失败 wx.showToast({ title: res.data.message, icon: 'none', duration: 2000 }) } } }) } }
后台使用上面的Sm4Utils工具类,在RequestBodyAdvice中对请求体进行解密,在ResponseBodyAdvice中对要返回前端的响应体进行加密。