国标加密算法java代码_GitHub-amao-blogSM3:密码学作
业、课程设计:国。。。
⼀、SM3算法介绍
SM3是国家密码管理局编制的商⽤算法,它是⼀种杂凑算法,可以应⽤于数字签名、验证等密码应⽤中。其计算⽅法、计算步骤和运算实例可以在国家商⽤密码管理办公室官⽹查看。
该算法的输⼊是⼀个长度 L ⽐特的消息m,其中 L < 2^64 ,经过填充、迭代压缩后,⽣成⼀个256⽐特的输出。
⼆、算法步骤
2.1 填充长度
假设消息m 的长度为 L ⽐特。⾸先将⽐特“1”添加到消息的末尾,再添加k 个“0”, k是满⾜L + 1 + k ≡ 448 mod 512 的最⼩的⾮负整数。然后再添加⼀个64位⽐特串,该⽐特串是长度L的⼆进制表⽰。填充后的消息m′的⽐特长度为512的倍数。
在具体的实现过程中,⾸先获取消息超过512⽐特整数倍部分的长度L。由于在最后⼀个分组分组中,要将1个⽐特位“1”添加到消息的末尾,并且要添加64⽐特来存储消息的长度。
当 L <= 512-(64+1)时,可以直接填充⽐特位“1”、 512-(64+1)个⽐特位“0”、64位的消息长度,;当 L > 512-(64+1)时,最后⼀个512⽐特的分组不够填充,需要再添加⼀个512位的分组,此时填充的“0”的个数为k=512-L-1+(512-64)。
2.2 迭代压缩
在迭代的过程中,⾸先对填充后的消息m′按512⽐特进⾏分组。然后对每⼀个分组进⾏迭代压缩。迭代⽅式如下:
FOR i=0 TO n-1
V (i+1) = CF (V (i) , B (i) )
ENDFOR
上述算法中,n是填充后消息分组的个数,即有多少个消息分组,就迭代多少次。Vi是256位的向量,V0为初始值IV,即前⼀个分组计算完后的结果Vi会当作下⼀个分组的参数传⼊CF函数中,此即是密码学中扩散原则,即原始消息的任意⽐特位的变化都会造成结果产⽣⼤的改变。
在CF压缩函数中,需要⽤到的参数有向量V (i)、B(i)、常量Tj、Wj和Wj′。其中Wj和Wj′是对512⽐特的消息分组进⾏扩展后产⽣的132个字。由于消息分组有多个,Wj和Wj′也对应有多个。在具体实现时,要在CF函数中对每⼀个消息分组进⾏消息扩展计算。
在迭代完最后⼀个消息分组后,CF函数返回的值Vn就是最终的计算结果。
三、实现过程
3.1 创建项⽬
打开Eclipse创建项⽬SM3,在项⽬SM3中创建类SM3。创建完成后⽬录结构如下所⽰:
3.2 定义算法中的常量、函数
算法中需要⽤到函数FFj、GGj、P0、P1、常量Tj等,以及原始消息、填充后的消息定义如下:
// 字符集
private String charset = "ISO-8859-1";
// 要哈希的字符串
private String message = "abc";
// 填充后的字符串
private String PaddingMessage;
// 获取常量T0和T1
private int T(int j){
if(j <= 15){
return 0x79cc4519;
}else{
return 0x7a879d8a;
}
}
// 布尔函数 FF
private int FF(int X, int Y, int Z, int j) {
int result = 0;
if(j >= 0 &&j <= 15)
{
result = X ^ Y ^ Z;
}else if(j >= 16 && j <= 63)
{
result = (X & Y) | (X & Z) | (Y & Z); }
return result;
}
// 布尔函数GG
private int GG(int X, int Y, int Z, int j) {
int result = 0;
if(j >= 0 &&j <= 15)
{
result = X ^ Y ^ Z;
}else
{
result = (X & Y) | (~X & Z);
}
return result;
}
// 置换函数P0
private int P0(int X)
{
return X ^ (CircleLeftShift(X, 9)) ^ CircleLeftShift(X, 17);
}
// 置换函数P1
private int P1(int X)
{
return X ^ (CircleLeftShift(X, 15)) ^ CircleLeftShift(X, 23);
}
在上述函数定义中,⽤到的CircleLeftShift函数⽤于实现循环左移,它的两个参数分别是要移位的32位int型数据和循环左移的位数。在循环左移中,循环左移k位,相当于将⼆进制位最左边的k位移动到最右边。
3.2 调试⽅法编写
在课本的运算⽰例中,每⼀步运算的中间结果都有。在编写算法时,每写⼀步都要与课本上的中间结果对照,以确定当前得到的中间结果是否正确。由于算法运⾏的中间结果都是⼆进制形式,为⽅便查看,编写了dump⽅法⽤于将中间结果显⽰为16进制的形式。如将填充后的消息打印出来的dump⽅法如下:
private void dump()
{
System.out.println("========开始打印========");
try{
byte bts[] = Bytes(this.charset);
for(int i = 0; i < bts.length; i ++)
{
if(i%16 != 0 && i%2 == 0 && i != 0){
System.out.print(" ");
}
if(i%16 == 0 && i != 0){
System.out.println();
}
System.out.printf("%02x", bts[i]);
}
}catch(Exception e){
System.out.println("Error Catch");
}
System.out.println("\n========结束打印========");
}
在输⼊消息为“abc”的情况下,打印填充后的消息的⼗六进制形式如下所⽰:
其中开头的61、62、63是字母a、b、c对应的ASCII码,80是填充消息时附加的⽐特位1,该⽐特位与后⾯填充的⽐特位0,构成了⼆进制1000 0000,所以对应的⼗六进制是80。最后的18也是⼗六进制形式,对应的⼗进制是24,表⽰消息的长度是24位。
由于在使⽤Java编写SM3算法时,计算的中间结果有字符串、整型数组等多种类型,为⽅便查看对应数据的⼗六进制形式,编写了多个dump⽅法,⽤于打印各种类型的数据。
/
* 将字符串输出为16进制形式 */
private static void dump(String str)
/* 将整型数组输出为16进制形式 */
a的ascii的编码是多少private static void dump(int nums[])
3.3 遇到的错误及解决⽅案
3.3.1. 循环左移计算结果偶尔不正确
在Java中,只有按位左移<
在实现循环左移时,假设要移位的32位⽐特位的数据为Y,则循环左移位可以分为三步:
①把Y按位左移k位的值赋值为l,此时l最右边的k位为0;
②把Y按位右移(32-k)位的值赋值为r,此时r左边的(32-k)位为0;
③将l和r进⾏按位或运算,即得到循环左移后的结果。
例如将0x1234 5678按位左移8位,则Y左移8位得到 l=0x3456 7800,Y右移32-8=24位得到r=0x0000 0012,最后将l和r进⾏按位或运算得到最终结果0x34567812。
循环左移的实现过程如下:
// 将x循环左移N位
private static int CircleLeftShift(int x, int N)
{
return (x << N) | (x >> (32 - N));
}
在使⽤此⽅法进⾏按位左移时,发现偶尔计算出来的结果与预期不符合。经过调试,发现是在按位右移时没有得到预期的结果,导致最终循环左移结果出错。具体原因及分析如下:
按位左移是直接在右边补0,⽽按位右移分为两种情况,⼀种是逻辑右移(有符号移位),⼀种是算术右移(⽆符号移位)。
逻辑右移是当最⾼位为0是,说明这是⼀个正数,右移时在最左边补0;当最⾼位为1时,说明这是⼀个负数,负数在计算机中以补码形式存储,所以逻辑右移时在最左边补1。
⽽算术左移在移位时忽略符号位,即⽆论最⾼位是0还是1,都往最左边补0。
在SM3算法中,需要的是算术右移。⽽在Java的语法中,>>是逻辑右移,>>>是算术右移。最初使⽤逻辑右移,导致循环左移最⾼位为1的数时运算结果与期望值不符。
修改后的循环左移⽅法如下:
// 将x循环左移N位
private static int CircleLeftShift(int x, int N)
{
return (x << N) | (x >>> (32 - N));
}
3.3.2. 填充消息时附加⽐特位1结果不对
根据算法的计算步骤,填充消息时,⾸先在消息后⾯附上⼀个⽐特位1。在实现算法时,由于⽤户输⼊的都是以字节为单位的字符串,所以1之后填充的0的个数k肯定是符合7+8*Z的,其中Z为⾮负整数。所以可以附加⼀个⽐特位“1”的操作可以转化为附加⼆进制1000 0000。实现代码如下:
padding += (byte)0x80;// 先填充⼀个“1000 0000”
其中padding是⼀个字符串类型的数据,⽤于存储附加的数据。填充完⽐特位1、k个0以及消息长度后,将填充后的消息打印出来,如下:
测试时输⼊的消息依旧是abc,理论上得到的是6162 6380 0000 0000 .....,将消息c即63后的⼗六进制位2d 3132 38与ASCII表对照,发现是-128。原因很明确,byte类型的0x80表⽰的数正是-128。⽽将-128与字符串padding进⾏ +=操作时,byte类型的数据被转换成字符串-128,所以得到上图的结果。
既然不能直接将byte类型的数据与字符串相连接,那可以尝试使⽤new String(byte[] bytes[])⽅法将⼀个byte数组转换成字符串。修改代码如下:
byte a[] = { (byte) 0x80 };
padding += new String(a); // 先填充⼀个“1000 0000”
再次运⾏后结果还是不正确,如下图所⽰:
结果显⽰原本的0x80变成了0x3f。经过测试发现,0x01最后会得到0x01,0x02会得到0x02,0x7f也会得到0x7f,只有当⼤于0x7f 是结果才会不正确,⽽且得到的都是0x3f。
在⽹上搜索之后,得出错误的原因:ASCII是每个字节对应⼀个字符,⼀个字节的表⽰范围是-128127,⽽ASCII只对0127这个范围进⾏了编码。也就是每个字节最⼤值是0x7f,⽤⼆进制表⽰就是最⾼位为0。上⾯的0x80的⼆进制位是1000 0000,最⾼位是1,不在ASCII 编码的范围之内。Java使⽤的是Unicode字符集,当进⾏将0x80转换成字符时,Java在Unicode代码页中查询不到对应的字符,Java会默认返回⼀个0x3f。所以上⾯试验中,⼩于0x80的byte可以正确转换成字符串,⽽⼤于等于0x80的byte数据将会返回0x3f.
解决⽅法是将byte数组转化成字符串时设置编码为“ISO-8859-1”。ISO-8859-1是按字节编码的,并且它对0~255的空间都进⾏了编码,所在在转换时它能够正确的将0x80转换为字符串。实现代码如下:
byte a[] = {(byte)0x80};
padding += new String(a, charset);
其中第⼆个参数charset是在最前⾯定义的字符集,它是⼀个字符串“ISO-8859-1”。再次运⾏并打印填充后的消息,发现结果跟预期⼀致:
3.3.3. 迭代时结果出错
在进⾏迭代的时候,导出了迭代前的数据,包括Wj,Wj′等,都与课本上的⽰例⼀样。说明迭代前的步骤已经正确的完成。迭代后的结果却不正确,说明错误出现在迭代这⾥。将中间结果ABCDEFGH导出后与课本上的对照,发现最开始出错的位置是G0,⽽G0之前的A-F都是正确的。
课本上的:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。