赞
踩
国密SM3算法是我国自研设计的商用密码杂凑算法,是在SHA-256的基础上进行改造的,其安全性与SHA-256相当。《SM3密码杂凑算法》于2010年12月份由国家密码管理局首次发布。后于2012年发布为密码行业标准《GM/T 0004-2012 SM3密码杂凑算法》,2016年发布为国家密码杂凑算法标准《GB/T 32905-2016 信息安全技术 SM3密码杂凑算法》。
标准中阐述了SM3算法的基本算法、实现步骤及实现原理。 SM3和MD5的迭代过程类似,也采用Merkle-Damgard结构。消息分组长度为512位,杂凑输出值长度为256位。
SM3密码杂凑算法的执行过程包括3步:消息填充、迭代压缩、输出杂凑值。其中迭代压缩的过程中又包括了消息扩展。
SM3的消息扩展步骤是使用512比特
的数据分组作为输入的,所以,在开始的时候需要将输入的原始数据数据填充至512比特的倍数
。
假设消息m的长度为l
比特,填充的规则如下:
l + 1 + k = 448 mod 512
的最小非负整数;64比特
的比特串,该比特串是长度l
的二进制表示,采用大端
存放;m'
的比特长度为512的位数。迭代的过程为先将填充后的消息m'
按512比特进行分组,分成n组,其中 n=(l + k + 65) / 512
然后对分组通过压缩函数执行压缩操作,如下所示
其中CF是压缩函数,V(0)为256比特初始值IV,B(i)为填充后的消息分组,迭代压缩的结果为V(n)。
在对分组执行压缩前,需要对消息进行扩展,需要将分组扩展为132个消息字
,每个消息字32比特
,即4字节
,在压缩函数CF中会用到。
扩展过程:
B(i)
划分成16
个消息字,W(0),W(1),…,W(15)。W
64个W‘
SM3整个过程中最复杂、最核心的地方就是压缩函数, 令A、B、C、D、E、F、G、H
为字寄存器
(字的存储为大端格式),每个字为32位,初始值存储的是IV
。
SS1、SS2、TT1、TT2
为中间变量,压缩函数V(i+1) = CF(V(i),B(i))
, 0 <= i <= n-1,压缩函数CF
对ABCDEFGH
8个消息字及分组扩展后的消息执行64轮
相同的过程。
过程中还会涉及到其它一些函数,相当复杂,具体计算的过程如下:
V(i)赋值给寄存器
,第一次时也就是将初始IV
赋值给寄存器的ABCDEFGH。ABCDEFGH
再与B(i)进行异或运算得到V(i+1)
V(i+1)
用于下一分组
的压缩。当所有的分组都执行完压缩后V(n)
,将V(n)赋值给ABCDEFGH,输出ABCDEFGH即为256比特的杂凑值
。
整体的过程可如下图表示:
在整个代码实现中,会将输入的字节数组消息转换成32字节的整数进行运算处理,输出杂凑值时再将整数转换为字节数组。
因为将所有的消息缓存后再填充,会占用大量内存,所以采用循环分组处理。即对输入的字节消息进行顺序处理,当够一个分组的数据后就执行消息扩展、压缩,压缩后的结果缓存起来,为下一分组使用作准备,直到没有消息输入后调用final时再进行填充操作,并处理填充后的分组数据,然后产生杂凑值。
算法中规定了初始化IV为32个字节的常量,
这里我们在类中定义一个常量数组,这里我们使用int数组来存放,因为一个字为4个字节,刚好为一个int的长度。
在整个算法过程中,我们会将输入的字节转换成4字节int进行处理,最后杂凑值输出的时候再将int转换成字节数组输出。
// 初始的IV值
private static final int[] IV = {0x7380166F, 0x4914B2B9, 0x172442D7, 0xDA8A0600,
0xA96F30BC, 0x163138AA, 0xE38DEE4D, 0xB0FB0E4E};
常量数组T在压缩的过程中会用到,算法中如下对常量T进行了定义
我们对常量T进行定义,并在静态块中进行初始化。
因为在后面的压缩过程中,常量T(j)的值会循环向左移j,所以这里初始化的时候直接完成左移操作。
// 常量T private static final int[] T = new int[64]; // 初始化常量T static { for (int i = 0; i < 16; ++i) { int t = 0x79CC4519; // 循环左移i位,等价于 左移i位后按位或无符号右移32-i位 T[i] = (t << i) | (t >>> (32 - i)); } for (int i = 16; i < 64; ++i) { // 压缩算法中有mod32 int n = i % 32; int t = 0x7A879D8A; T[i] = (t << n) | (t >>> (32 - n)); } }
上述代码中循环左移i位,涉及到位运算,等于是向左移动i位,然后或
向右无符号右移(32-i)位,不明白的可以查阅资料。
布尔函数也是压缩中用到的函数,其算法如下:
因为其函数都规定了0到15,和16到63的两种情况,所以我们将FF函数分别定义为FF0和FF1
,将GG函数定义为GG0和GG1
private int FF0(final int x, final int y, final int z) {
return (x ^ y ^ z);
}
private int FF1(final int x, final int y, final int z) {
return ((x & y) | (x & z) | (y & z));
}
private int GG0(final int x, final int y, final int z) {
return (x ^ y ^ z);
}
private int GG1(final int x, final int y, final int z) {
return ((x & y) | ((~x) & z));
}
置换函数包括P0和P1
,P1在会消息扩展时使用,P0会在压缩过程中使用,定义如下:
代码如下
private int P0(final int x) {
final int r9 = ((x << 9) | (x >>> (32 - 9)));
final int r17 = ((x << 17) | (x >>> (32 - 17)));
return (x ^ r9 ^ r17);
}
private int P1(final int x) {
final int r15 = ((x << 15) | (x >>> (32 - 15)));
final int r23 = ((x << 23) | (x >>> (32 - 23)));
return (x ^ r15 ^ r23);
}
定义一个消息分组的缓存
,循环存放每组的消息,以字为单位,16个字
,刚好为512比特。
// 每个分组多少个消息字
private static final int BLOCK_SIZE = 64 / 4;
// 消息组缓存,存储的是4字节的消息字
private int[] intWord = new int[BLOCK_SIZE];
// 消息分组的偏移量
private int wordOff;
定义一个4字节数组缓存
,用来处理输入的字节数组消息,当满4个字节
时转换成一个消息字
,放入消息分组的缓存;
private final byte[] byteBuf = new byte[4]; // 组成一个字的buff
private int byteBufOff; // xBuf的偏移量
定义个变量来记录输入消息的总大小
// 比特大小,记录输入数据长度
private long byteCount;
定义寄存器
,用来存放ABCDEFGH
的值,
// 寄存器,存初始IV和中间结果
private int[] V = new int[8];
定义初始化init()
方法,对上面定义的一些变量进行初始化,
/** * 初始化方法 */ protected void init() { byteCount = 0; wordOff = 0; byteBufOff = 0; // 初始化字节数组缓存 for (int i = 0; i < byteBuf.length; i++) { byteBuf[i] = 0; } // 初始化寄存器 this.V = IV; }
update方法用来接收传入的字节数组消息,并对消息进行处理。消息处理过程中,先将字节数组转换成消息字,再将消息字组成分组数据,然后对调用压缩函数,
update分为两个方法,一个是处理输入字节数组,一个是内部处理单字节。
/** * 处理输入的字节数组 * @param data 输入数据 * @param inOff 偏移量 * @param len 长度 */ public void update(byte[] data, int inOff, int len) { // 防止len小于0 len = Math.max(0, len); int i = 0; // 多次update时,前一次update最后的数据可能不够一个消息字, // 字节缓存中还有数据,需要先进行处理 if (byteBufOff != 0) { while (i < len) { // 循环处理 byteBuf[byteBufOff++] = data[inOff + i++]; if (byteBufOff == 4) { // 处理消息 processWord(byteBuf, 0); byteBufOff = 0; break; } } } // & ~3 的目地是变成能被4整除的最大整数 int limit = ((len - i) & ~3) + i; for (; i < limit; i += 4) { processWord(data, inOff + i); } // 剩余不够一个消息字的数据放入字节缓存中 while (i < len) { byteBuf[byteBufOff++] = data[inOff + i++]; } byteCount += len; } /** * 处理输入的单字节 * @param in */ protected void update(byte in) { byteBuf[byteBufOff++] = in; // 字节数组满了后,转换成消息字 if (byteBufOff == byteBuf.length) { processWord(byteBuf, 0); byteBufOff = 0; } byteCount++; }
压缩函数是整个算法的核心,压缩过程需要先进行消息扩展,处理消息扩展的方法如下
/** * 消息扩展生成消息字 * 这里没有生成W’,因为W'可以直接通过W异或得到 */ protected void processW() { for (int j = 0; j < 16; ++j) { this.W[j] = this.intWord[j]; } for (int j = 16; j < 68; ++j) { int wj3 = this.W[j - 3]; int r15 = ((wj3 << 15) | (wj3 >>> (32 - 15))); int wj13 = this.W[j - 13]; int r7 = ((wj13 << 7) | (wj13 >>> (32 - 7))); this.W[j] = P1(this.W[j - 16] ^ this.W[j - 9] ^ r15) ^ r7 ^ this.W[j - 6]; } }
压缩函数方法代码如下
/** * 压缩函数 */ public void CF() { processW(); int A = this.V[0]; int B = this.V[1]; int C = this.V[2]; int D = this.V[3]; int E = this.V[4]; int F = this.V[5]; int G = this.V[6]; int H = this.V[7]; for (int j = 0; j < 16; j++) { int a12 = ((A << 12) | (A >>> (32 - 12))); // 直接加T[j],因为前面已经mod过了 int s1_ = a12 + E + T[j]; int SS1 = ((s1_ << 7) | (s1_ >>> (32 - 7))); int SS2 = SS1 ^ a12; int TT1 = FF0(A, B, C) + D + SS2 + (this.W[j] ^ this.W[j + 4]); int TT2 = GG0(E, F, G) + H + SS1 + this.W[j]; D = C; C = ((B << 9) | (B >>> (32 - 9))); B = A; A = TT1; H = G; G = ((F << 19) | (F >>> (32 - 19))); F = E; E = P0(TT2); } for (int j = 16; j < 64; j++) { int a12 = ((A << 12) | (A >>> (32 - 12))); // 直接加T[j],因为前面已经mod过了 int s1_ = a12 + E + T[j]; int SS1 = ((s1_ << 7) | (s1_ >>> (32 - 7))); int SS2 = SS1 ^ a12; int TT1 = FF1(A, B, C) + D + SS2 + (this.W[j] ^ this.W[j + 4]); int TT2 = GG1(E, F, G) + H + SS1 + this.W[j]; D = C; C = ((B << 9) | (B >>> (32 - 9))); B = A; A = TT1; H = G; G = ((F << 19) | (F >>> (32 - 19))); F = E; E = P0(TT2); } this.V[0] ^= A; this.V[1] ^= B; this.V[2] ^= C; this.V[3] ^= D; this.V[4] ^= E; this.V[5] ^= F; this.V[6] ^= G; this.V[7] ^= H; this.wordOff = 0; }
可以看到上面的代码调用了两个for循环,是因为布尔函数FF、GG
调用不同,没有直接在一个循环里面用if判断是为了提升性能。
当处理完输入的数据后,调用final来进行填充并处理最后的分组,然后输出杂凑值,同时将一些变量恢复初始化。
/** * 杂凑结束 * @param out 最终的杂凑值 * @return */ public int doFinal(byte[] out) { long bitLength = (byteCount << 3); // 最后进行填充,先填充1 update((byte) 128); // 填充后不够一个字长度时,继续补0至一个字 while (this.byteBufOff != 0) { update((byte)0); } // 输入字节数组消息总长度填充 processLength(bitLength); // 处理最后的分组 CF(); // 转换成字节并输出给out intToBigEndian(V, out, 0); // 初始值恢复原样 init(); return DIGEST_LENGTH; }
处理数据长度的填充方法如下
/** * 处理数据长度填充 * @param bitLength */ protected void processLength(long bitLength) { // wordOff=15,说明当前块不够填充64字节的长度了 if (this.wordOff > (BLOCK_SIZE - 2)) { this.intWord[wordOff++] = 0; // 填充为0 CF(); } // 填充0 直到剩两个消息字 while (this.wordOff < (BLOCK_SIZE - 2)) { this.intWord[wordOff++] = 0; } // 填充64字节的长度 this.intWord[wordOff++] = (int) (bitLength >>> 32); this.intWord[wordOff++] = (int) (bitLength); }
完成后进行简单测试,测试代码如下
// 测试
public static void main(String[] args) {
SM3_1 sm3Test = new SM3_1();
byte[] plain = hexStringToBytes("FC7D61FD1D392EB692C5C7E0723CA637ADDA");
byte[] plain1 = hexStringToBytes("FC");
sm3Test.update(plain, 0, plain.length);
sm3Test.update(plain1, 0, plain1.length);
byte[] out = new byte[32];
sm3Test.doFinal(out);
System.out.println(bytes2HexString(out));
}
测试出来的杂凑结果和网上其它工具杂凑值结果一致,说明算法实现正确。
以上就是国密SM3杂凑算法的原理介绍和Java代码实现,希望对大家有帮助!如有错误,欢迎大家指正!
需要完整代码的可以私信发邮箱。
参考资料
1.https://www.oscca.gov.cn/sca/xxgk/2010-12/17/content_1002389.shtml
2.BC包的SM3算法
3.https://zhuanlan.zhihu.com/p/129692191
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。