密码学核心操作和JS代码

194 阅读7分钟

首先说明,这里讨论的其实并不是密码学的各种算法,包括Hash、HMAC、AES等的算法实现,而主要讨论在这些算法中,最基本的一些数据操作方式,并且用Javascript代码来进行表示。因为在一般的材料中,这些算法和示例代码都是用伪代码、C语言、Python和Java表示的,用JS的比较少,但理论上来说,这些在代码层面上,是没有区别的。

笔者的开发和学习环境主要是Nodejs,将它们移植到JS,可以帮助读者更直观的从理论上的理解这些数据和信息的操作、变换过程,同时这些操作更接近于计算机科学的基础层面,也能够帮助提升软件开发的基础认知和技能。需要注意的是,由于语法、环境和操作方式的微小差异,有些代码和处理,并不能严格的对位转换,里面还是有不少问题的,这个迁移的过程,也是学习和理解这些差异的过程。

这些代码和材料主要用于学习和研究使用,并不建议将其直接使用到实际工作当中。要应用到实际和工程中,可能还需要做一些适配性的优化。

下面就是讨论的主要内容:

基本数据操作

基本的逻辑和数学操作包括,除单独数计算外,这些操作基本上满足交换律:

  • 算数和(+)

简单算数和(+),对于两个二进制数的位计算而言,其数学意义是,求和后,如果有进位,则会累计到上一位,并可以迭代。

  • 或(&, and )

两个二进制数,对位计算,取其中小的值Min(a,b),或者两个位数据的简单算数乘积。(1x1=1,1x0=0x1=0,0x0=0)。

  • 与(|, or ):

两个二进制数,对位计算,取其中大的值Max(a,b)。

  • 非(~,Not):

二进制数,按位取反操作,可以将每个位的0和1对调。对于一个正常的正二进制数而言,其实生成的是其补码表示的负数,并不是完整形式,要完整形式,可以使用>>>无符号右移:

(~0b00101011>>>0)

而且,取反操作和其使用的数据类型有关,一般情况下整数的意思是32位整数。

  • 异或(^,XOR)

JS使用运算符^来表示。其数学意义是,将两个二进制数,进行对位的位运算,相加后求余,作为位运算的结果,也可以理解成对位的位运算中,GF2的有限域内的计算。

  • 取余 (%, Mod)

求除法的余数。 在密码学算法中,还有更常见的场景,就是当计算操作溢出时,就结果得余数。比较常见的场景包括两数相加后,溢出了有限域的范围,需要求余限定在有限域内;还有就是对数组分组时,元素在子数组内的位置。

比如,在SHA256中,就是大量使用这个操作(使用0xFFFFFFFF)。

  • 整除(/)

两数相除,保留整数部分。常用作当给数组分组时,元素所在组的序号。显然,当除数是2的时候,可以使用右移的操作方式来进行整除。

  • 简单取反

前面的非运算,虽然可以进行取反,但它是在整数域里面执行的,如果我们只是简单的将一个二进制字符串的每个位都翻转,可以使用和它等长的全为1的二进制数进行对位异或计算得到(获取此构造数的方法,后详)。

上面这些操作代码总结如下:


// 算数和 +
00100110101    309
  011010110    214 
-------------- 算数和累加  
01000001011    523

// 或 & 
00100110101    
  011010110
-------------- 对位小值  
00000010100    20 


// 与 |
00100110101    
  011010110
-------------- 对位大值  
00111110111    503 


// 异或 ^
00100110101
  011010110
-------------- 相加求余  
00111100011    483


// 取反
00100110101    
~
-------------- 取反  
- 100110110    -310


// 简单位翻转
00100110101    
11111111111    构造数
-------------- XOR  
11011100010    -310


简单位翻转方法
const FLIP = (v)=>{
  let r = 1; l = Math.floor(Math.log2(v)); // length +1 -1
  while (l--) { r <<= 1; r  |= 1; }
  return r ^ v;
}

// 求余
let a = (240 + 95),
b1 = a % 0xFF,
b2 = a & 0xFF;


// 整除
let
a  = 70,
b1 = a / 8,
b2 = (a - a % 8) / 8,
b3 = 0 | a / 8,
b4 = Math.floor(a/8);


二进制长度

获取一个二进制数的位长,可以使用移位迭代的方式,也可以使用对数计算,甚至可以使用先转换字符串长度的方式。


// 对数运算/移位计算/字符串长度
const 
bitLength  = (num) => Math.floor(Math.log2(num)) + 1,
bitLength2 = (num) => { let l=0; while(num>0){ num>>=1; l++ };return l;},
bitLength3 = (num) => num.toString(2).length;

// test 
let l1 = bitLength(0b0010101001110);
let l2 = bitLength2(0b0010101001110);
let l3 = bitLength3(0b0010101001110);

这里,注意不能使用Math.ceil,因为和Mathfloor+1并不严格相等。

有趣的是,印象中,我们觉得使用对数计算,可能计算量比较大,性能较弱,但其实可能并不是那样。在笔者的测试系统上,测试计算1亿个数,对数计算55ms,移位计算2158ms,字符串长度? 34s。说明在任何情况下,字符串操作,都是代价高昂的。

填充

下列代码,可以给定长度,生成所有位都是1的二进制数。


let all1 = (n)=> { let r = 1; n--; while(n--) { r <<=1; r |= 1; }; return r;}


移位操作

JS原生提供三种移位操作:

  • 左移位 <<,二进制数据向高移位,数字后面添0,数学上的含义是乘以二
  • 右移位 >>,二进制数据向低移位(带符号),截断最后一位(并填充符号位),数学上的意义是被二整除
  • 无符号右移 >>>,无论二进制数据是正还是负,向右位移后,前面都填0

一般情况下,我们都只操作无符号整数,如uint8, uint32,所以并无差别。当然,移位操作也可以操作n位,就是重复n次移1位操作。移位操作和可以和=号结合,简化操作。


// 右移位
0b11000011 >> 1
0b11000011 >> 2
0b11000011 >>> 2

a >>= 2 

// 左移位
0b11000011 << 1
0b11000011 << 2

b <<= 3 

ROTL/ROTR

ROT(Rotate Left/Rigth)的意思是位旋转。包括向左旋转(ROTL)和向右旋转(ROTR)。这个操作还需要指定操作数据的长度,让这个旋转不至于溢出。如ROTL8的意思是向做旋转,限定在8位二进制数(2**8)。Wiki的C语言定义,是做了一个uint8的数据类型转换,我们使用 & 0xFF,不知道会不会有问题。

NNFZBXg.png

下面是ROTL8函数定义的JS代码和使用示例

const 
ROTL8  = (x,shift) => (x << shift | x >> (8 - shift))  & 0xFF, 
ROTR8  = (x,shift) => (x >> shift | x << (8 - shift))  & 0xFF,
ROTL32 = (x,shift) => (x << shift | x >> (32 - shift)) & 0xFFFFFFFF,
ROTR32 = (x,shift) => (x >> shift | x << (32 - shift)) & 0xFFFFFFFF; 

// 8位移位,可以处理 uint8
let
a = 0x41;
a = ROTL8(a,2).toString(2);
a = ROTR8(a,2).toString(2);

// 32位移位, 可以处理4个byte一组的情况,移动单位为16
let
b = 0x12345678;
b = ROTL32(a,3*16).toString(16);
b = ROTR32(a,3*16).toString(16);


类似的,还有ROTL32/ROTR32,可以在32位正整数范围内操作数字。

AES的行位移操作,看起来是数组操作,但其实可以使用位旋转操作实现。

伽罗瓦积/和

AES大量使用的伽罗瓦有限域的计算,包括求和和乘积。求和计算就是简单的异或计算(^)。 而其乘积有其特别的实现,简单而言就是多项式相乘后,对0x11B取模。 具体代码如下:

除了数学计算之外,实际工程中大多使用预计算和查表的方式,详见代码中的GF8_TAB方法。

矩阵计算

矩阵计算的操作规则来自线性代数(这里的求和和乘积是逻辑计算,也可以是伽罗瓦乘积和求和):

┏ x1 ┓   ┏ a1 a2 a3 a4 ┓   ┏ y1 ┓ (y1 = x1 * a1 + x2 * a2 + x3 * a3 + x4 * a4)
| x2 | x | b1 b2 b3 b4 | = | y2 | (y2 = x1 * b1 + x2 * b2 + x3 * b3 + x4 * b4)
| x3 |   | c1 c2 c3 c4 |   | y3 | (y3 = x1 * c1 + x2 * c2 + x3 * c3 + x4 * c4)
┗ x4 ┛   ┗ d1 d2 d3 d4 ┛   ┗ y4 ┛ (y4 = x1 * d1 + x2 * d2 + x3 * d3 + x4 * d4)


矩阵计算,在AES的MixColumn(列混合)变换中用到了。

SBOX替换盒

通常用于数据块变换计算。设计一个和原始块信息信息大小相同的数据库(替换盒Sbox)。里面填充符合某种替换规则的元素,然后就可以按照原始块元素的位置,在替换盒中找到相同位置的元素进行替换,从而实现某种信息变换操作。

典型的使用场景,就是AES的替换盒。当然这是一个特例,但也让我们能够看到,替换盒模式的优势就是变换操作特别快(基本不需要计算,就是查找操作)。理论上而言,任何变换,都可以使用替换盒的模式来实现。关于AES-SBOX的讨论很多,这里不再赘述。