GF(256)乘法计算的JS实现和分析

448 阅读17分钟

在AES算法实现的算法中,要用到伽罗瓦域的相关理论和知识。本文的目的,就是试图在其应用的基础,再稍微深入一点,从原理和实作的角度,对其理论基础到实际编程进行了相关的研究和分析。在这个过程中,笔者也感到收获很多,觉得有意义分享出来和大家共同提高和进步。

基本概念

伽罗瓦有限域(Galois Field,简称为GF,伽罗瓦域),被广泛应用于编码理论、密码学和计算机科学等领域,如AES加密算法中的S盒替代、MixColumn编码,Hash编码等等,都使用其作为其数据结构、算法设计和实现的理论基础。

伽罗瓦域是一种代数结构,其通用形式为GF(p^n),就是一个包含p^n个(也称为阶)元素的有限域。其中p是一个质数,n为正整数。在信息技术领域,最常用的GF域就是GF(2^8)或者GF(256)。我们先来看看,GF有一些数学和运算上的特性,具体包括:

  1. 加法和乘法满足结合律和交换律

a + b = b + a, a * b = b * a

a + (b + c) = (a + b) + c, a * (b * c) = (a * b) * c

  1. 加法和乘法满足分配律,即 a * (b + c) = (a * b) + (a * c)

  2. 对于每个非零元素a,都存在一个逆元素a^-1,使得a * a^-1 = 1,其中1是GF中的单位元素。

  3. GF中有一个零元素0,它是加法的单位元素,即a + 0 = a对于任何元素a成立。

  4. GF中的乘法具有零因子自由性,即如果a * b = 0,则a=0或b=0。这个特性在信息编码和密码学中具有重要的应用。

  5. GF中的加法和乘法满足有限域的特性,即具有有限的元素个数。如在GF(2^m)中,元素的个数为2^m个,其中m为正整数。

  6. 在GF(2^m)中,多项式运算是一种常见的运算形式,可以用于表示和计算数学问题。因此,GF(2^m)中的数学特性也包括多项式的加法、乘法、取模等运算特性。

其次,在信息技术的应用领域中,特别是密码学相关的领域,会大量的使用各种数字处理和编码转化操作,显然,我们希望相应算法的设计可以快速处理和计算数据,并且范围可控(无论如何转化都是一个有限集合,形成闭环),还应该可以方便的进行逆运算。这样,基于GF的数学特性,围绕其设计算法和实现,是一个非常合理的选择。

我们再来看看其具体的实现。GF域计算规则中,我们最常用的就是加法和乘法。这里要注意,这里的加法和乘法是一个逻辑概念,方便理解和讨论,并不能简单的套用整数计算的规则。

GF的加法运算实际就是上是异或运算(^),也称为模2加法,通常用⊕来表示。 对于两个元素a和b,它们的加法定义为:a ⊕ b = a ^ b。

这个非常简单,一般在编程语言中,整数的计算直接就可以操作。GF中的乘法情况更复杂一点,我们需要详细讨论一下。

GF乘法操作和实现

GF乘法通常⊗来表示。在GF运算规则的定义中,乘法运算实际上是复合的一个多项式计算,首先使用多项式乘法,然后需要使用一个特定的不可约多项式来取模吗,从而约化到域中。具体可以表示为:

a ⊗ b = c(x) mod f(x)

其中,c(x) 是a和b的两个多项式乘积;f(x)是一个不可约多项式(根元),由GF的选择和规则决定,GF(2^8)的标准根元是: 0x11B(100011011,多项式为 x8+x4+x3+x+1)。mod表示模运算,也有一套操作规则。

为了更方便的理解,我们以一个例子来简单说明。如我们要计算 0x57 * 0x83 的GF乘积,要进行操作和步骤如下:

1 将数据转化为多项式,原始数据是16进制,可以使用高低位分别转化为2进制表示,并标记1的位置作为多项式的结构

0x57 = 01010111 = x6+x4+x2+x1+1;

0x83 = 10000011 = x7+x+1

2 将多项式相乘

可以使用简单算数的多项式计算方式,然后约除相同的项目:

  
  (x6+x4+x2+x1+1)(x7+x+1)
= x13+x11+x9+x8+x7 + x7+x5+x3+x2+x + x6+x4+x2+x+1 // 分配结合
= x13+x11+x9+x8+2x7+x6+x5+x4+x3+2x2+2x+1 // 合并
= x13+x11+x9+x8+x6+x5+x4+x3+1 // 约除 %2 != 1
= 10 1011 0111 1001

也可以用列式运算和表达方式如下:

           0101 0111
           1000 0011 - 三个1,分别在017位(左移)
-*------------------------
           0101 0111	
         0 1010 111
  010 1011 1 
-^---------------------------------
   10 1011 0111 1001 

运算的结果,转化为二进制表示,方便后续的取模计算

3 取模,这里需要进行循环计算

按照规则,这个计算要用的不可约多项式是 x8+x4+x3+x+1 (100011011,规则约定),16进制数值为0x11B,在AES算法中广泛使用,有时候也称为AES多项式。计算的具体操作过程如下:

   10101101111001 (mod) 100011011
  ^100011011
---------------------------------  1
   00100000011001
    ^100011011
---------------------------------  2  
     000011000001 
     

结果为11000001,即C1。可以看到,这个基本上就是从高到低,迭代在不为零的区段上和标准不可约多项式进行异或计算,从而消除为零的高位,直到结果小于256。

上述方法看起来比较复杂,但其实在支持移位运算的系统中,操作是比较简单的。

JS中实现对GF乘法运算的实现

基于前面讨论的原理和抽象操作过程,笔者用JS语言实现了相关的算法。由于能够找到的资料比较少(chatGPT提供的代码好像有误),实现的代码不见得是最好最高效的,但应该能保证逻辑和结果正确,如果读者有更好的方法,或者发现由什么问题,希望不吝赐教。

const 
gfMulti = (va, vb)=>{
    // stage 1 va(x)*vb(x)
    let vm = 0;
    for (let i=0; i<8; i++) if (vb >> i & 1) vm ^= va << i;
    
    // let vm = Array(8).fill(0).reduce((c,v,i)=> vb >> i & 1 ? c ^ va << i : c, 0);
    // vm = vb.toString(2).split("").reverse() 
    //    .reduce((c,v,i)=> parseInt(v) ? c ^ va<<i : c, 0 );       

    // mod 
    let l,atail, aresult= vm; // init 
    while(aresult > 255) {
        // shift length, trim 0x11B length
        l = aresult.toString(2).length - 9; 

        // tail
        atail    = vm >> l << l ^ vm; 

        // trim, mod and merge trail
        aresult =  (aresult >> l ^ 0x11B) << l ^ atail;
    }
    return aresult;
};

// 测试用例:
let va = [0x53,0xCA,0x57,0x83,0x3B,0x7D,0x17,0xA8];
let r = va
    .reduce((c,v,i,l)=>(i % 2 == 1) ? c 
        : [...c, strHex(v) + " x " + strHex(l[i+1]) + " = " + strHex(gfMulti(v,l[i+1]))],[])
    .join("\n")

console.log(r);

53 x CA = 01
57 x 83 = C1
3B x 7D = EA
17 x A8 = 1B

第一阶段,两个多项式相乘。实现方法是,先将一个乘数b,转化为用2进制表示的数组,然后从低到高,如果当前的值为1,则将乘数a后面加i个零(左移),然后做异或操作,最后就得到第一阶段的结果,乘积1。 (后面有所改进,使用for循环,直接将乘数依次右移并判断其位置有效性,决定参与异或运算)

第二阶段,取模。循环内容的核心是,先确定需要异或操作的位置,然后计算不参与异或计算的尾数,拼接(左移并且异或)后,进入下一循环。

每个循环的尾数,可以这样确定,先右移去掉尾数,然后左移补回0,然后和原始数据异或,就可以得到尾数。

A Better Way 更好的方法

更新一下,不久后,在查看资料和一些实现源码的时候,看到了下列代码,觉得是更好的实现方式。

const 
GF_MLT= (a, b)=>{
  let p = 0; // init result
  while(b > 0) {
    // b, mul operation if last bit is 1 and shift right until 0
    if (b & 1) p ^= a;  b>>=1; 

    // a, shift left and check if carry on add value    
    if ((a<<=1) & 0x100) a^= 0x1B;
  };
  return p & 0xFF;
};

这个方法的思路,是在乘积的同时,加入了需要求余的多项式。 同样使用一个8次循环,首先检查b的最后一位,如果为1,就和当前的a相乘,得到当前的p(结果)。然后右移准备检查下一位。计算(或轮空)完成后,对a左移进位;然后检查进位前,8位的最高位是否为1,笔者这里优化为进位后检查(&100);如果是,则再加一个1B(GF多项式)。 循环直到8位都计算完成,最终结果p再对256求余,就是最后的计算结果了。这个过程,相比原理推导出的代码,更加清晰简单。

查表计算法

前面对GF域的乘法计算的原理和过程进行了讨论。我们就会发现,其实际计算的过程和步骤还是比较复杂的,特别是对于大规模重复性操作的场景更是如此(AES算法实现就是典型)。我们由从GF域的特性知晓,这个乘法操作,无论是输入还是结果,都是一个有限的集合,就是说,是可以穷举的,而且这个表的规模对应GF8而言并不算太大(输入256x256,输出256),这样理论上,就可以使用一个预计算好的表格,按照输入直接查询这个表格来得到结果。称为查表法,这个方法会付出一些内存占用的代价,但显然可以大幅度减少CPU计算和操作的过程。所以,这个方法,在现实的编程实现操作场景中,才是被广泛使用的方法。

完整的查找法,是计算一个完整的表格,显然这个表格由 256 * 256 / 2个元素构成; 对于一些小的嵌入式系统而言,也是一个不小的内存占用了。但其中里面有可以优化的空间。 GF计算,满足运算律,所以完整的乘法运算,可以拆解成更小的数据和步骤。示例如下(此处+为 GF求和,实际上是 ^ ):

0x11 + 0xA2 = 0xB3
0xB3 * 0x57 = 0xC8
= (0x11 + 0xA2) * 0x57
= 0x11 * 0x57 + 0xA2 * 0x57 	
= 0x50 + 0x98  // 确认0x44 * 0x55 = 0xD3
0xD3 * 0x57 = 0XDA
= (0x44 * 0x55) * 0x57
= 0x44 * (0x55 * 0x57) // 确认

然后结合查找基础计算表,来完成计算过程。原理可能如此,但是,如何高效的具体实现和相关规则,查找表应用的实例代码和数据,笔者还未找到,找到后会补充到文档中。

补充,笔者在网上找到了一种算法实现,原始链接在:

opensource.apple.com/source/Secu…

随后,按照上述开放代码的启发,笔者用JS实现了一个使用查找表快速实现GF乘法计算的方法:

const 
LOGTABLE  = [ // index table
    0,   0,  25,   1,  50,   2,  26, 198,  75, 199,  27, 104,  51, 238, 223,   3, 
  100,   4, 224,  14,  52, 141, 129, 239,  76, 113,   8, 200, 248, 105,  28, 193, 
  125, 194,  29, 181, 249, 185,  39, 106,  77, 228, 166, 114, 154, 201,   9, 120, 
  101,  47, 138,   5,  33,  15, 225,  36,  18, 240, 130,  69,  53, 147, 218, 142, 
  150, 143, 219, 189,  54, 208, 206, 148,  19,  92, 210, 241,  64,  70, 131,  56, 
  102, 221, 253,  48, 191,   6, 139,  98, 179,  37, 226, 152,  34, 136, 145,  16, 
  126, 110,  72, 195, 163, 182,  30,  66,  58, 107,  40,  84, 250, 133,  61, 186, 
   43, 121,  10,  21, 155, 159,  94, 202,  78, 212, 172, 229, 243, 115, 167,  87, 
  175,  88, 168,  80, 244, 234, 214, 116,  79, 174, 233, 213, 231, 230, 173, 232, 
   44, 215, 117, 122, 235,  22,  11, 245,  89, 203,  95, 176, 156, 169,  81, 160, 
  127,  12, 246, 111,  23, 196,  73, 236, 216,  67,  31,  45, 164, 118, 123, 183, 
  204, 187,  62,  90, 251,  96, 177, 134,  59,  82, 161, 108, 170,  85,  41, 157, 
  151, 178, 135, 144,  97, 190, 220, 252, 188, 149, 207, 205,  55,  63,  91, 209, 
   83,  57, 132,  60,  65, 162, 109,  71,  20,  42, 158,  93,  86, 242, 211, 171, 
   68,  17, 146, 217,  35,  32,  46, 137, 180, 124, 184,  38, 119, 153, 227, 165, 
  103,  74, 237, 222, 197,  49, 254,  24,  13,  99, 140, 128, 192, 247, 112,   7, 
],  
ALOGTABLE = [ // value table
    1,   3,   5,  15,  17,  51,  85, 255,  26,  46, 114, 150, 161, 248,  19,  53, 
   95, 225,  56,  72, 216, 115, 149, 164, 247,   2,   6,  10,  30,  34, 102, 170, 
  229,  52,  92, 228,  55,  89, 235,  38, 106, 190, 217, 112, 144, 171, 230,  49, 
   83, 245,   4,  12,  20,  60,  68, 204,  79, 209, 104, 184, 211, 110, 178, 205, 
   76, 212, 103, 169, 224,  59,  77, 215,  98, 166, 241,   8,  24,  40, 120, 136, 
  131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206,  73, 219, 118, 154, 
  181, 196,  87, 249,  16,  48,  80, 240,  11,  29,  39, 105, 187, 214,  97, 163, 
  254,  25,  43, 125, 135, 146, 173, 236,  47, 113, 147, 174, 233,  32,  96, 160, 
  251,  22,  58,  78, 210, 109, 183, 194,  93, 231,  50,  86, 250,  21,  63,  65, 
  195,  94, 226,  61,  71, 201,  64, 192,  91, 237,  44, 116, 156, 191, 218, 117, 
  159, 186, 213, 100, 172, 239,  42, 126, 130, 157, 188, 223, 122, 142, 137, 128, 
  155, 182, 193,  88, 232,  35, 101, 175, 234,  37, 111, 177, 200,  67, 197,  84, 
  252,  31,  33,  99, 165, 244,   7,   9,  27,  45, 119, 153, 176, 203,  70, 202, 
   69, 207,  74, 222, 121, 139, 134, 145, 168, 227,  62,  66, 198,  81, 243,  14, 
   18,  54,  90, 238,  41, 123, 141, 140, 143, 138, 133, 148, 167, 242,  13,  23, 
   57,  75, 221, 124, 132, 151, 162, 253,  28,  36, 108, 180, 199,  82, 246,   1, 
],
gfTable = (a,b) =>(a && b) ? ALOGTABLE[(LOGTABLE[a] + LOGTABLE[b]) % 255] : 0;

// 调用示例:
let a = gfTable(0x57,0x83);


从实现代码可以看到,要进行两个数的GF乘积计算,首先在一个LogTable中,使用输入值进行查询,相加后取余,然后用这个余数作为索引到另一个表中查询的结果,就是乘积。这个设计只用到两个16x16的数值表,而且使用方法也非常巧妙简洁,非常方便实现和移植,可以猜想其运行效率和资源占用也会非常高效,令人叹为观止。

这里需要说明,这个表的构成,和执行的原理,对笔者而言原理是不明的,但已经简单验证是可以工作的。如果由高人能够知晓其工作原理,希望不吝赐教。

撰后笔记

在本文成文的过程当中,其实是经过了一段学习提高的过程,和一些反复的,这里也顺便记录下列,和大家分享。

  • GF运算规则的理解

由于接触的编解码算法应用和操作比较多,其实对字节数组元素的异或操作也比较熟悉,但那是从应用的角度来理解,并没有从理论的角度,来理解其作为有限域运算的特性用作实际的工程领域。 这次整理让这个概念更加清晰明确,还特别通过对于乘法计算的研究(了解了其两阶段的操作),更深入了理解了其数学基础。提升了数学作为信息计算理论基础的认知。

  • 多项式相乘

GF元素相加运算,就是异或计算,这个非常容易理解,但相乘运算就比较麻烦。首先它不是简单的数学相乘后取模,而是要化为多项式的形式。网上的常见方法,是直接的多项式相乘分解后,消除重复项目的方法,显得比较繁杂。为了更好的理解这个问题,笔者改进使用了列式计算的方法,更加清晰直观。

  • 多项式取模

在网上的很多的资料包括视频教材,这部分很多都是语焉不详,或者只是简单的说是2模法。后来找到一个材料,使用列式计算的方式,清晰易懂,才明确了这个算法的操作。

  • 代码实现

数学的原理,和数字在纸面上的操作是一回式,但将这些想法,转化为可实现的代码,并保证其能够高效稳定运行,还可能需要考虑这种异常和错误的情况,就是另一回事了。

比起前面提到的GF乘法的计算,已经明确了原理,但如果按照人类的思考和信息处理方式,就可能会直接使用一些字符串的操作,笔者实现的一些初始版本确实就是这样。比如将一个数字,转换为2进制字符串,甚至数组,然后进行循环操作,虽然也可以达到目的,但本质上还是人的处理方式,并没有用好计算机的特性。

  • 移位运算

在成文和编写示例代码的过程中,笔者逐渐更深入的理解和掌握了移位运算和操作。原始代码基于二进制数组的字符操作,虽然语义好像比较明确,但实际效率不高,因为数学原理而言,本身就应该直接在整数数值上进行操作,而不应该进行类型的转换。 在二进制数字的字符串上,常见的操作包括在后面补位,截取一定位数用于对位操作,截取尾位等等,这些其实都可以直接通过二进制数移位操作结合异或操作来完成。就是在这些需求分析和实现的过程中,也有了实际的应用场景和用途,可以逐渐熟悉和掌握数字移位的操作,从而提升程序执行的效率。开发者也可以以此提升编程的水平和认知。

  • 工程处理

在检索资料的过程中,笔者无意发现了有人使用查找表,来实现GF乘法运算。但当时的重点是乘法原理,所以只是将其作为结果验证的方式。而且其实那段代码有错误(也可能是笔者理解和移植有误),所以总是和自己计算的结果有出入,当时以为算法、数值表或者用法不对,就没有再深究。

后来在编写查表计算的章节的过程中,特别是看到了苹果开源的算法源码后,对实现代码进行了相应修改,可以匹配计算出来的结果后,才确认这一方法和实现的有效。

在实际的工程中,大概率会直接使用这一方法,简单高效,容易移植。所以工程处理,并不一定要求使用者完全掌握其原理,只要能够有能力使用工具和现成的材料,来有效的处理问题即可。但如果要进一步提升,或者排除比较深层次的问题,就需要对原理和过程有更深入的了解和理解。

  • chatGPT

本文成文过程中,特别是前期一些概念和基础代码阶段,笔者使用了chatGPT来辅助研究。看起来效果并不是很好,特别是它给的一些示例,运行和过程是有问题的,经常缺一些重要的环节。一致性也不好,同一个问题,使用不同的角度,可能得出的结论也不好。笔者猜想可能是这一领域的成熟权威有效的材料比较少,导致其训练效果并不是很好。

所以可以使用它作为参考和灵感的来源,起码在现在这个阶段,对于比较冷僻的知识领域,还是需要比较谨慎的使用和信任的。