概要
国算SM3加密算法JavaScript源码深入解析
我的理解:
SM3密码杂凑算法,它将输入的任意长度(小于2^64bite)的数据经过填充、迭代压缩,生成杂凑值,杂凑值输出长度为256比特的摘要。
这个摘要是通过一系列复杂的数学运算生成的,具有以下特点:不可逆性、唯一性、固定长度、消息扩散。
总的来说,SM3密码杂凑算法是一种用于生成数据指纹的安全且高效的算法,主要用于数据完整性验证、数字签名等场景。
其他信息:
SM3密码杂凑算法是中国国家密码管理局2010年公布的中国商用密码杂凑算法标准。具体算法标准原始文本参见参考文献[1]。该算法于2012年发布为密码行业标准(GM/T 0004-2012),2016年发布为国家密码杂凑算法标准(GB/T 32905-2016)。
SM3适用于商用密码应用中的数字签名和验证,是在[SHA-256]基础上改进实现的一种算法,其安全性和SHA-256相当。SM3和MD5的迭代过程类似,也采用Merkle-Damgard结构。消息分组长度为512位,摘要值长度为256位。
整个算法的执行过程可以概括成四个步骤:消息填充、消息扩展、迭代压缩、输出结果。
知识点:
SM3杂凑算法——属于常见密码学算法中的摘要算法,指把任意长度的输入消息数据转化为固定长度的输出数据的一种密码算法,又称为 散列函数 、 哈希函数 、 杂凑函数 、单向函数 等。
摘要算法所产生的固定长度的输出数据称为 摘要值 、 散列值 或 哈希值 ,摘要算法无秘钥。
摘要算法 通常用来做数据完整性的判定,即对数据进行哈希计算然后比较 摘要值 是否一致。
摘要算法主要分为三大类:MD(Message Digest,消息摘要算法)、SHA-1(Secure Hash Algorithm,安全散列算法)和 MAC(Message Authentication Code,消息认证码算法);另国密标准 SM3 也属于摘要算法。
SM3算法JavaScript代码
// 消息扩展
const W = new Uint32Array(68)
const M = new Uint32Array(64) // W'
/**
* 循环左移
*/
function rotl(x, n) {
const s = n & 31
return (x << s) | (x >>> (32 - s))
}
/**
* 二进制异或运算
*/
function xor(x, y) {
const result = []
for (let i = x.length - 1; i >= 0; i--) result[i] = (x[i] ^ y[i]) & 0xff
return result
}
/**
* 压缩函数中的置换函数 P0(X) = X xor (X <<< 9) xor (X <<< 17)
*/
function P0(X) {
return (X ^ rotl(X, 9)) ^ rotl(X, 17)
}
/**
* 消息扩展中的置换函数 P1(X) = X xor (X <<< 15) xor (X <<< 23)
*/
function P1(X) {
return (X ^ rotl(X, 15)) ^ rotl(X, 23)
}
/**
* sm3 本体
*/
function sm3(array) {
let len = array.length * 8
// k 是满足 len + 1 + k = 448mod512 的最小的非负整数
let k = len % 512
// 如果 448 <= (512 % len) < 512,需要多补充 (len % 448) 比特'0'以满足总比特长度为512的倍数
k = k >= 448 ? 512 - (k % 448) - 1 : 448 - k - 1
// 填充
const kArr = new Array((k - 7) / 8)
const lenArr = new Array(8)
for (let i = 0, len = kArr.length; i < len; i++) kArr[i] = 0
for (let i = 0, len = lenArr.length; i < len; i++) lenArr[i] = 0
len = len.toString(2)
for (let i = 7; i >= 0; i--) {
if (len.length > 8) {
const start = len.length - 8
lenArr[i] = parseInt(len.substr(start), 2)
len = len.substr(0, start)
} else if (len.length > 0) {
lenArr[i] = parseInt(len, 2)
len = ''
}
}
const m = new Uint8Array([...array, 0x80, ...kArr, ...lenArr])
const dataView = new DataView(m.buffer, 0)
// 迭代压缩
const n = m.length / 64
const V = new Uint32Array([0x7380166f, 0x4914b2b9, 0x172442d7, 0xda8a0600, 0xa96f30bc, 0x163138aa, 0xe38dee4d, 0xb0fb0e4e])
for (let i = 0; i < n; i++) {
W.fill(0)
M.fill(0)
// 将消息分组B划分为 16 个字 W0, W1,……,W15
const start = 16 * i
for (let j = 0; j < 16; j++) {
W[j] = dataView.getUint32((start + j) * 4, false)
}
// W16 ~ W67:W[j] <- P1(W[j−16] xor W[j−9] xor (W[j−3] <<< 15)) xor (W[j−13] <<< 7) xor W[j−6]
for (let j = 16; j < 68; j++) {
W[j] = (P1((W[j - 16] ^ W[j - 9]) ^ rotl(W[j - 3], 15)) ^ rotl(W[j - 13], 7)) ^ W[j - 6]
}
// W′0 ~ W′63:W′[j] = W[j] xor W[j+4]
for (let j = 0; j < 64; j++) {
M[j] = W[j] ^ W[j + 4]
}
// 压缩
const T1 = 0x79cc4519
const T2 = 0x7a879d8a
// 字寄存器
let A = V[0]
let B = V[1]
let C = V[2]
let D = V[3]
let E = V[4]
let F = V[5]
let G = V[6]
let H = V[7]
// 中间变量
let SS1
let SS2
let TT1
let TT2
let T
for (let j = 0; j < 64; j++) {
T = j >= 0 && j <= 15 ? T1 : T2
SS1 = rotl(rotl(A, 12) + E + rotl(T, j), 7)
SS2 = SS1 ^ rotl(A, 12)
TT1 = (j >= 0 && j <= 15 ? ((A ^ B) ^ C) : (((A & B) | (A & C)) | (B & C))) + D + SS2 + M[j]
TT2 = (j >= 0 && j <= 15 ? ((E ^ F) ^ G) : ((E & F) | ((~E) & G))) + H + SS1 + W[j]
D = C
C = rotl(B, 9)
B = A
A = TT1
H = G
G = rotl(F, 19)
F = E
E = P0(TT2)
}
V[0] ^= A
V[1] ^= B
V[2] ^= C
V[3] ^= D
V[4] ^= E
V[5] ^= F
V[6] ^= G
V[7] ^= H
}
// 转回 uint8
const result = []
for (let i = 0, len = V.length; i < len; i++) {
const word = V[i]
result.push((word & 0xff000000) >>> 24, (word & 0xff0000) >>> 16, (word & 0xff00) >>> 8, word & 0xff)
}
return result
}
/**
* hmac 实现
*/
const blockLen = 64
const iPad = new Uint8Array(blockLen)
const oPad = new Uint8Array(blockLen)
for (let i = 0; i < blockLen; i++) {
iPad[i] = 0x36
oPad[i] = 0x5c
}
function hmac(input, key) {
// 密钥填充
if (key.length > blockLen) key = sm3(key)
while (key.length < blockLen) key.push(0)
const iPadKey = xor(key, iPad)
const oPadKey = xor(key, oPad)
const hash = sm3([...iPadKey, ...input])
return sm3([...oPadKey, ...hash])
}
// module.exports = {
// sm3,
// hmac,
// }
// const {sm3, hmac} = require('../sm2/sm3')
/**
* 补全16进制字符串
*/
function leftPad(input, num) {
if (input.length >= num) return input
return (new Array(num - input.length + 1)).join('0') + input
}
/**
* 字节数组转 16 进制串
*/
function ArrayToHex(arr) {
return arr.map(item => {
item = item.toString(16)
return item.length === 1 ? '0' + item : item
}).join('')
}
/**
* 转成字节数组
*/
function hexToArray(hexStr) {
const words = []
let hexStrLength = hexStr.length
if (hexStrLength % 2 !== 0) {
hexStr = leftPad(hexStr, hexStrLength + 1)
}
hexStrLength = hexStr.length
for (let i = 0; i < hexStrLength; i += 2) {
words.push(parseInt(hexStr.substr(i, 2), 16))
}
return words
}
/**
* utf8 串转字节数组
*/
function utf8ToArray(str) {
const arr = []
for (let i = 0, len = str.length; i < len; i++) {
const point = str.codePointAt(i)
if (point <= 0x007f) {
// 单字节,标量值:00000000 00000000 0zzzzzzz
arr.push(point)
} else if (point <= 0x07ff) {
// 双字节,标量值:00000000 00000yyy yyzzzzzz
arr.push(0xc0 | (point >>> 6)) // 110yyyyy(0xc0-0xdf)
arr.push(0x80 | (point & 0x3f)) // 10zzzzzz(0x80-0xbf)
} else if (point <= 0xD7FF || (point >= 0xE000 && point <= 0xFFFF)) {
// 三字节:标量值:00000000 xxxxyyyy yyzzzzzz
arr.push(0xe0 | (point >>> 12)) // 1110xxxx(0xe0-0xef)
arr.push(0x80 | ((point >>> 6) & 0x3f)) // 10yyyyyy(0x80-0xbf)
arr.push(0x80 | (point & 0x3f)) // 10zzzzzz(0x80-0xbf)
} else if (point >= 0x010000 && point <= 0x10FFFF) {
// 四字节:标量值:000wwwxx xxxxyyyy yyzzzzzz
i++
arr.push((0xf0 | (point >>> 18) & 0x1c)) // 11110www(0xf0-0xf7)
arr.push((0x80 | ((point >>> 12) & 0x3f))) // 10xxxxxx(0x80-0xbf)
arr.push((0x80 | ((point >>> 6) & 0x3f))) // 10yyyyyy(0x80-0xbf)
arr.push((0x80 | (point & 0x3f))) // 10zzzzzz(0x80-0xbf)
} else {
// 五、六字节,暂时不支持
arr.push(point)
throw new Error('input is not supported')
}
}
return arr
}
// module.exports = function (input, options) {
// input = typeof input === 'string' ? utf8ToArray(input) : Array.prototype.slice.call(input)
//
// if (options) {
// const mode = options.mode || 'hmac'
// if (mode !== 'hmac') throw new Error('invalid mode')
//
// let key = options.key
// if (!key) throw new Error('invalid key')
//
// key = typeof key === 'string' ? hexToArray(key) : Array.prototype.slice.call(key)
// return ArrayToHex(hmac(input, key))
// }
//
// return ArrayToHex(sm3(input))
// }
// sm3加密
function sm3Encrypt(input, options) {
input = typeof input === 'string' ? utf8ToArray(input) : Array.prototype.slice.call(input)
if (options) {
const mode = options.mode || 'hmac'
if (mode !== 'hmac') throw new Error('invalid mode')
let key = options.key
if (!key) throw new Error('invalid key')
key = typeof key === 'string' ? hexToArray(key) : Array.prototype.slice.call(key)
return ArrayToHex(hmac(input, key))
}
return ArrayToHex(sm3(input))
}
剥离出编码算法
function sm3Encrypt(input, options) {
input = typeof input === 'string' ? utf8ToArray(input) : Array.prototype.slice.call(input)
if (options) {
const mode = options.mode || 'hmac'
if (mode !== 'hmac') throw new Error('invalid mode')
let key = options.key
if (!key) throw new Error('invalid key')
key = typeof key === 'string' ? hexToArray(key) : Array.prototype.slice.call(key)
return ArrayToHex(hmac(input, key))
}
return ArrayToHex(sm3(input))
}
技术细节
循环左移函数
rotl
:接受一个32位整数 x
和移动位数 n
,返回 x
循环左移 n
位的结果。
/**
* 循环左移
*/
function rotl(x, n) {
const s = n & 31
return (x << s) | (x >>> (32 - s))
}
这个rotl
函数实现了一个对32位整数x
进行循环左移n
位的操作。函数接受两个参数:x
是要移位的整数,n
是要移动的位数。函数的工作原理如下:
1、n & 31
确保s
(要移动的位数)在0到31之间,这是因为移动超过32位的效果与移动n÷32的余数相同。&31(10进制)&11111(二进制),字面:与11111进行与运算。
2、(x << s)
将x
的二进制表示向左移动s
位。这相当于将x
乘以2的s
次方。
3、(x >>> (32 - s))
将x
的二进制表示向右移动(32 - s)
位。这相当于将x
向右移动(32 - s)
位,并用0填充左侧的位。
4、(x << s) | (x >>> (32 - s))
使用按位OR(|
)运算符将两个移动后的值组合起来。这实际上实现了对x
进行循环左移s
位的操作,被移出的位从左侧重新进入。
二进制异或运算函数 xor
接受两个数组 x
和 y
,对每个元素进行异或运算,返回结果数组。
注:x、y均为二进制数组
在这个函数中,(x[i] ^ y[i])
是对两个数字 x[i]
和 y[i]
对应位进行异或运算的结果。这是因为异或运算符 ^
用于比较两个数字的二进制位,如果相应位不同则结果为1,否则为0。
因为 JavaScript 中整数的表示范围是 -2^53 到 2^53,所以为了确保结果在一个字节的范围内,使用 & 0xff
将结果截断为一个字节。
/**
* 二进制异或运算
*/
function xor(x, y) {
const result = []
for (let i = x.length - 1; i >= 0; i--) result[i] = (x[i] ^ y[i]) & 0xff
return result
}
置换函数 P0
和 P1
实现SM3算法中的置换运算,用于消息扩展中和压缩函数中的处理。
这些置换函数在SHA-256算法中用于将输入消息的比特位进行混淆和扩散,以增加密码学安全性。在压缩函数中,P0
和 P1
会与输入消息的不同部分进行混合,从而在压缩过程中引入更多的随机性和扩散性,增强了SHA-256算法的安全性。
/**
* 压缩函数中的置换函数 P0(X) = X xor (X <<< 9) xor (X <<< 17)
*/
function P0(X) {
return (X ^ rotl(X, 9)) ^ rotl(X, 17)
}
/**
* 消息扩展中的置换函数 P1(X) = X xor (X <<< 15) xor (X <<< 23)
*/
function P1(X) {
return (X ^ rotl(X, 15)) ^ rotl(X, 23)
}
***SM3算法实现***
这段代码实现了SM3算法的主体部分,包括消息的填充、分组处理和压缩。下面逐步解析代码的功能:
一、填充消息: m->m`
根据算法规范,首先计算消息长度 len
(以比特为单位,所以*8)
然后计算填充位数 k
,先填充一个“1”,后面加上k个“0”。其中k是满足(len
+1+k) mod 512 = 448的最小正整数。
接着根据 k
的值,构造填充后的消息 m
,包括在消息末尾添加一个比特值为1,然后补充0直到满足消息长度为512比特的倍数。
最后追加64位的数据长度(bit为单位,大端序存放。观察算法标准原文附录A运算示例可以推知,下方附附录A部分图片。)
二、消息分块: m`划分512比特为一组
消息分块:将填充后的消息m'按512比特进行分组,分为n组:
m'=B0 B1…B(n-1),其中 组数 n = (len+k+65)/512。
消息扩展:
(1)、将消息块划分为16个字 W0, W1, ..., W15
然后根据一定的规则计算出 W16
~ W67
:
(2)、对于 W[16]
到 W[67]
的字,根据消息扩展的置换函数 P1
和循环左移函数 rotl
计算得到。对于 W
的计算,将 W
的部分字进行异或操作。
(3)、对于W[0]
到 W[63]
的字, W[i]
与 W[i+4]
进行异或运算后存入M[i]
。
三、迭代压缩运算: 迭代运算后输出一个256比特的杂凑值(摘要值)
1、每一轮压缩的处理
根据算法规定的一系列操作,包括对字寄存器 A, B, C, D, E, F, G, H
的更新,以及中间变量 SS1, SS2, TT1, TT2
的计算。
在这段代码中,循环执行64次,每次处理一个消息块。具体步骤如下:
T
的选择:根据j
的值,如果j
在0到15之间,则选择T1
,否则选择T2
。SS1
的计算:先将A
循环左移12位(rotl(A, 12)
),然后与E
相加,再与T
循环左移j
位相加,最后整体循环左移7位。SS2
的计算:SS1
与A
循环左移12位异或运算。TT1
的计算:根据j
的值,如果j
在0到15之间,则计算((A ^ B) ^ C)
,否则计算((A & B) | (A & C)) | (B & C)
,再加上D
、SS2
和M[j]
。TT2
的计算:根据j
的值,如果j
在0到15之间,则计算((E ^ F) ^ G)
,否则计算((E & F) | ((~E) & G))
,再加上H
、SS1
和W[j]
。- 保存
A
、B
、C
、D
、E
、F
、G
、H
的值,供下一轮循环使用。
2、结果转换:
最后将字寄存器 V
中的每个32比特字转换为4个8比特字,并将结果拼接在一起,得到最终的消息摘要。
总体来说,这段代码实现了SM3算法的核心部分,通过填充、分组处理和压缩等步骤对输入消息进行处理,最终得到256比特(32字节)的消息摘要。
3、简化迭代过程:
sm3整体流程图:
sm3本体代码:
/**
* sm3 本体
*/
function sm3(array) {
let len = array.length * 8
// k 是满足 len + 1 + k = 448mod512 的最小的非负整数
let k = len % 512
// 如果 448 <= (512 % len) < 512,需要多补充 (len % 448) 比特'0'以满足总比特长度为512的倍数
k = k >= 448 ? 512 - (k % 448) - 1 : 448 - k - 1
// 填充
const kArr = new Array((k - 7) / 8)
const lenArr = new Array(8)
for (let i = 0, len = kArr.length; i < len; i++) kArr[i] = 0
for (let i = 0, len = lenArr.length; i < len; i++) lenArr[i] = 0
len = len.toString(2)
for (let i = 7; i >= 0; i--) {
if (len.length > 8) {
const start = len.length - 8
lenArr[i] = parseInt(len.substr(start), 2)
len = len.substr(0, start)
} else if (len.length > 0) {
lenArr[i] = parseInt(len, 2)
len = ''
}
}
const m = new Uint8Array([...array, 0x80, ...kArr, ...lenArr])
const dataView = new DataView(m.buffer, 0)
// 迭代压缩
const n = m.length / 64
const V = new Uint32Array([0x7380166f, 0x4914b2b9, 0x172442d7, 0xda8a0600, 0xa96f30bc, 0x163138aa, 0xe38dee4d, 0xb0fb0e4e])
for (let i = 0; i < n; i++) {
W.fill(0)
M.fill(0)
// 将消息分组B划分为 16 个字 W0, W1,……,W15
const start = 16 * i
for (let j = 0; j < 16; j++) {
W[j] = dataView.getUint32((start + j) * 4, false)
}
// W16 ~ W67:W[j] <- P1(W[j−16] xor W[j−9] xor (W[j−3] <<< 15)) xor (W[j−13] <<< 7) xor W[j−6]
for (let j = 16; j < 68; j++) {
W[j] = (P1((W[j - 16] ^ W[j - 9]) ^ rotl(W[j - 3], 15)) ^ rotl(W[j - 13], 7)) ^ W[j - 6]
}
// W′0 ~ W′63:W′[j] = W[j] xor W[j+4]
for (let j = 0; j < 64; j++) {
M[j] = W[j] ^ W[j + 4]
}
// 压缩
const T1 = 0x79cc4519
const T2 = 0x7a879d8a
// 字寄存器
let A = V[0]
let B = V[1]
let C = V[2]
let D = V[3]
let E = V[4]
let F = V[5]
let G = V[6]
let H = V[7]
// 中间变量
let SS1
let SS2
let TT1
let TT2
let T
for (let j = 0; j < 64; j++) {
T = j >= 0 && j <= 15 ? T1 : T2
SS1 = rotl(rotl(A, 12) + E + rotl(T, j), 7)
SS2 = SS1 ^ rotl(A, 12)
TT1 = (j >= 0 && j <= 15 ? ((A ^ B) ^ C) : (((A & B) | (A & C)) | (B & C))) + D + SS2 + M[j]
TT2 = (j >= 0 && j <= 15 ? ((E ^ F) ^ G) : ((E & F) | ((~E) & G))) + H + SS1 + W[j]
D = C
C = rotl(B, 9)
B = A
A = TT1
H = G
G = rotl(F, 19)
F = E
E = P0(TT2)
}
V[0] ^= A
V[1] ^= B
V[2] ^= C
V[3] ^= D
V[4] ^= E
V[5] ^= F
V[6] ^= G
V[7] ^= H
}
// 转回 uint8
const result = []
for (let i = 0, len = V.length; i < len; i++) {
const word = V[i]
result.push((word & 0xff000000) >>> 24, (word & 0xff0000) >>> 16, (word & 0xff00) >>> 8, word & 0xff)
}
return result
}
HMAC算法实现(×)
hmac
:对密钥进行填充和异或操作,生成带密钥的消息摘要。
定义了常量 blockLen
为 64,用于 HMAC 计算中的填充长度。创建了长度为 blockLen
的 iPad
和 oPad
数组,分别用 0x36
和 0x5c
填充,用于 HMAC 计算中的内部和外部填充。
hmac
函数接受两个参数 input
和 key
,表示要计算 HMAC 的消息和密钥:
1、对密钥进行填充,如果密钥长度超过 blockLen
,则对密钥进行 SM3 哈希,并用哈希结果作为密钥。
2、对填充后的密钥分别与 iPad
和 oPad
进行异或运算,得到 iPadKey
和 oPadKey
。将 iPadKey
与 input
拼接,计算拼接后的数据的 SM3 哈希值,得到 hash
。将 oPadKey
与 hash
拼接,再次计算拼接后的数据的 SM3 哈希值,得到最终的 HMAC 值。返回最终的 HMAC 值。
/**
* hmac 实现
*/
const blockLen = 64
const iPad = new Uint8Array(blockLen)
const oPad = new Uint8Array(blockLen)
for (let i = 0; i < blockLen; i++) {
iPad[i] = 0x36
oPad[i] = 0x5c
}
function hmac(input, key) {
// 密钥填充
if (key.length > blockLen) key = sm3(key)
while (key.length < blockLen) key.push(0)
const iPadKey = xor(key, iPad)
const oPadKey = xor(key, oPad)
const hash = sm3([...iPadKey, ...input])
return sm3([...oPadKey, ...hash])
}
我的调用接口:
说明:当options为空时,直接调用SM3本体算法加密字符串并返回 十六进制 的杂凑值。
function sm3Encrypt(input, options) {
input = typeof input === 'string' ? utf8ToArray(input) : Array.prototype.slice.call(input)
if (options) {
const mode = options.mode || 'hmac'
if (mode !== 'hmac') throw new Error('invalid mode')
let key = options.key
if (!key) throw new Error('invalid key')
key = typeof key === 'string' ? hexToArray(key) : Array.prototype.slice.call(key)
return ArrayToHex(hmac(input, key))
}
return ArrayToHex(sm3(input))
}
辅助函数
legtPad
:补全16进制字符串。
arrayToHex
:将字节数组转换为十六进制字串。
hexToArray
:将十六进制字串转成字节数组。
utf8ToArray
:将UTF-8字符串转换为字节数组。
小结
sm3算法总结:先填充,再分块,分块后迭代压缩得到杂凑值Vn。
首先将比特“1”添加到消息的末尾,再添加k 个“0”。然后再添加一个64位比特串。
将填充后的消息m′按512比特进行分组:m′ = B(0)B(1) · · · B(n−1) 其中n=(l+k+65)/512。 对m′按下列方式迭代:
FOR i=0 TO n-1
V (i+1) = CF(V (i) , B(i) )
ENDFOR
在最近的学习中,我深入研究 了SM3 算法,这是一种密码学安全的哈希函数,用于生成消息的摘要。通过深入学习 SM3 算法,我对密码学哈希函数的工作原理有了更深入的理解。我还通过阅读相关文献和参考资料,了解了密码学领域的一些基本概念和理论知识。但是,目前以我的能力有些复杂的算法我也没看太懂。
在未来,我计划继续深入学习密码学和网络安全领域的知识,包括了解更多的哈希函数和加密算法,探索网络安全领域的前沿技术和挑战。我希望能够将所学到的知识应用到实际项目中,为网络安全做出贡献,并不断提升自己的专业水平。
参考:
密码学基础(一)常见密码算法分类 - 知乎 (zhihu.com)
【SM3加密算法】|密码杂凑算法 | Hash算法 | 密码学 | 信息安全| 消息摘要_哔哩哔哩_bilibili