SM4加密的实现

前言

最近项目中需要对数据进行加密处理,由于项目的特殊性要求,加密统一采用国密算法进行加密。网上对国密算法的资料有限,且没有找到对应的JavaScript加密代码,无奈之下只得自己实现。加密的算法文档在国家密码管理局官网上可以查找到,此次的算法实现仅限于SM3与SM4的代码实现,SM2由于项目未做特殊要求因此还未来得及实现。

SM4密码杂凑算法简介

SM4算法是一种对称分组加密算法。该算法的分组长度为 128 比特,密钥长度为 128 比特。加密算法与密钥扩展算法都采用 32 轮非线性迭代结构。解密算法与加密算法的结构相同,只是轮询密钥的使用顺序相反,解密轮密钥是加密轮密钥的逆序。

SM4算法加密过程

  1. 根据加密密钥生成轮询密钥
    根据密钥的不同,按照一定规律生成32个字(每个字四个字节)的轮询密钥,用于之后数据的加密;

  2. 将分组后的数据用轮询秘钥进行加密

    加密的过程主要是用T方法和L方法,T方法转换过程中会用SBOX做一次非线性置换,经过这两个方法进行加密。

SM4算法的解密过程

  1. 根据加密密钥生成轮询密钥
    根据密钥的不同,按照一定规律生成32个字(每个字四个字节)的轮询密钥,用于之后数据的加密;

  2. 将分组后的数据逆向用轮询秘钥进行加密
    解密的过程主要是用T方法和L方法,T方法转换过程中会用SBOX做一次非线性置换,经过这两个方法进行加密。

SM4代码实现过程

java中的SM4实现

  • 代码中encrpt() 方法是供调用的Java的方法,方法的返回值是加密完成的数组,本方法有一个传入整型数据的重载方法,表示SM4加密次数;
  • keyExtend()方法为扩展秘钥的方法;
  • generateKey()方法为未指定密钥时自动生成随机密钥的方法,不需要再使用时调用,程序会在初始化时查找密钥,若未指定自动生成密钥;
  • decrpt()方法为解密方法,同样提供传入整型的重写方法,用来解密经SM4加密多次后的数据,注意使用本方法时必须指定密钥(新建SM4对象时),或者直接在加密的对象中使用。
  • getKeyStr()方法用来获取加解密的密钥十六进制字符串格式

其他方法均为以上方法的子方法,由于设计较为底层的算法故不加赘述,具体请参照官方算法说明文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 加密
*
* @return
*/
public byte[] encrpt() {
this.bytes = addBytes(new byte[16 - (this.bytes.length % 16 == 0 ? 16 : this.bytes.length % 16)]);
keyExtend();// 生成轮询密钥
int nowPo = 0;// 当前字开始位置
for (int i = 0; i < this.bytes.length / BLOCK; i++, nowPo += BLOCK) {// 分组
int[] rows = new int[4 + 32];
for (int j = 0; j < 4; j++) {
rows[j] = byteArrayToInt(this.bytes, nowPo + j * 4);
}
for (int k = 0; k < 32; k++) {
rows[k + 4] = rows[k] ^ TMethod(rows[k + 1] ^ rows[k + 2] ^ rows[k + 3] ^ this.rks[k]);
}
for (int k = 0; k < 4; k++) {
System.arraycopy(intToByteArray(rows[35 - k]), 0, this.bytes, nowPo + 4 * k, 4);
}
}
/*
* 将未分组byte移到结果
*/
System.arraycopy(this.bytes, this.bytes.length / BLOCK * BLOCK, this.bytes, this.bytes.length / BLOCK * BLOCK, this.bytes.length % BLOCK);
return this.bytes;
}
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 密钥扩展算法
*/
private void keyExtend() {
int[] K = new int[36];
int[] keys = { this.key[0] ^ this.FKS[0], this.key[1] ^ this.FKS[1], this.key[2] ^ this.FKS[2], this.key[3] ^ this.FKS[3] };
System.arraycopy(keys, 0, K, 0, keys.length);
for (int i = 0; i < 32; i++) {
this.rks[i] = K[i] ^ TPMethod(K[i + 1] ^ K[i + 2] ^ K[i + 3] ^ CK[i]);
K[i + 4] = this.rks[i];
}
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 随机生成密钥
*/
private void generateKey() {
byte[] var2 = new byte[32];
this.random.nextBytes(var2);
for (int i = 0; i < KEYLENGTH; i++) {
this.key[i] = byteArrayToInt(var2, 4 * i);
}
isKeyInit = true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 加密
*
* @return
*/
public byte[] encrpt() {
this.bytes = addBytes(new byte[16 - (this.bytes.length % 16 == 0 ? 16 : this.bytes.length % 16)]);
keyExtend();// 生成轮询密钥
int nowPo = 0;// 当前字开始位置
for (int i = 0; i < this.bytes.length / BLOCK; i++, nowPo += BLOCK) {// 分组
int[] rows = new int[4 + 32];
for (int j = 0; j < 4; j++) {
rows[j] = byteArrayToInt(this.bytes, nowPo + j * 4);
}
for (int k = 0; k < 32; k++) {
rows[k + 4] = rows[k] ^ TMethod(rows[k + 1] ^ rows[k + 2] ^ rows[k + 3] ^ this.rks[k]);
}
for (int k = 0; k < 4; k++) {
System.arraycopy(intToByteArray(rows[35 - k]), 0, this.bytes, nowPo + 4 * k, 4);
}
}
/*
* 将未分组byte移到结果
*/
System.arraycopy(this.bytes, this.bytes.length / BLOCK * BLOCK, this.bytes, this.bytes.length / BLOCK * BLOCK, this.bytes.length % BLOCK);
return this.bytes;
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 随机生成密钥
*/
private void generateKey() {
byte[] var2 = new byte[32];
this.random.nextBytes(var2);
for (int i = 0; i < KEYLENGTH; i++) {
this.key[i] = byteArrayToInt(var2, 4 * i);
}
isKeyInit = true;
}

JavaScript中的SM4实现

由于Java代码的完成时间较早,JavaScript的实现代码是从Java代码移植过来的,方法命名大同小异,因此不加赘述。代码版面较长,本文就不贴了,请移步github查看。

SM3加密的使用方法

SM4加密的Java使用

最近事务繁多无法抽身用设计模式对本算法进行优化,为了稳定运行只能牺牲性能采用实例化后调用的方式进行。由于底层计算都是位运算实际使用情况尚可,以下是在Java中的使用方式:

1
2
3
4
5
6
public static void main(String args[]){
SM4Encrp sm4 = new SM4Encrp(new byte[]{0x43,0x35});//传入要加密的数据,使用内部生成随机密钥的方式进行,也可使用提供的其他构造方法
byte bt = sm4.encrpt();//加密
SM4Encrp sm4de = new SM4Encrp(bt,sm4.getKeyStr);//传入需要解密的数据和加密用的密钥
byte btSource = sm4de.decrpt();//解密获取原数据
}

SM4加密的JavaScript使用

由于前台多是处理字符串和二进制数据,故统一采用传递二进制的方式进行杂凑计算。如果有求字符串杂凑值的必要可以调用以下方法转换为二进制byte数组进行使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 字符串转byte数组
*
* @param {*字符串}
* str
*/
function stringToByte(str)
{
var bytes = new Array();
var len, c;
len = str.length;
for (var i = 0; i < len; i++)
{
c = str.charCodeAt(i);
if (c >= 0x010000 && c <= 0x10FFFF)
{
bytes.push(((c >> 18) & 0x07) | 0xF0);
bytes.push(((c >> 12) & 0x3F) | 0x80);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if (c >= 0x000800 && c <= 0x00FFFF)
{
bytes.push(((c >> 12) & 0x0F) | 0xE0);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if (c >= 0x000080 && c <= 0x0007FF)
{
bytes.push(((c >> 6) & 0x1F) | 0xC0);
bytes.push((c & 0x3F) | 0x80);
} else
{
bytes.push(c & 0xFF);
}
}
return bytes;
}

使用方式如下所示:

1
2
3
4
5
6
7
8
var test = new SM4Encrpt({ //实例化解密类
(key/keyStr):***,//指定的数组加密/解密密钥数组/密钥字符串,此参数若是加密可以省略,加密密钥由本类自动生成;若是解密必须指定该密钥
bytes/enStr:*** //需要加密/解密的二进制/字符串
});
test.encrpt();//此为加密方法
test.encrpt(100);//此为加密100次
test.decrpt();//此为解密方法
test.decrpt(100);//此为解密100次

小结

热衷造轮子,

方便造车人。

愿别人的收获中也有属于自己的耕耘。