AES算法基本原理

2,409 阅读15分钟

AES算法基本原理

本文包含了用swift编写的, AES 算法的简单实现。源码地址

AES是什么

AES 是 Advanced Encryption Standard 的缩写,即 高级加密标准。在密码学中又称Rijndael加密法,是美国联邦政府采用的一种分组加密标准。

AES 是为了取代DES而诞生的加密标准。1997年1月2号,美国国家标准技术研究所宣布希望征集高级加密标准。经过五年的甄选流程,Rijndael算法最终获胜,并在2002年5月26日成为有效的标准。

Rijndael 算法与 AES 标准唯一的区别是,在 AES 的规格中,分组长度固定为 128 比特。而在 Rijndael 算法中,分组长度可以以32比特为单位在 128 比特到 256 比特的范围内选择。

根据密钥的长度不同,可分别称之为 AES-128 、 AES-192、 AES-256。他们的加密轮次也有所不同。

AES 密钥长度(Nk) 向量长度(IV) 分组大小(Nb) 加密轮数(Nr) 子密钥数
AES-128 16 16 16 10 11
AES-192 24 16 16 12 13
AES-256 32 16 16 14 15

生成子密钥的数量比AES算法的轮数多一个,因为第一个密钥加法层进行密钥漂白时也需要子密钥。

AES 的基本原理

总体结构

Rijndael算法是基于代换-置换网络(SPN,Substitution-permutation network)的迭代算法。明文数据经过多轮次的转换后方能生成密文,每个轮次的转换操作由轮函数定义。轮函数任务就是根据密钥编排序列(即轮密码)对数据进行不同的代换及置换等操作。

AES & Rijndael Architecture

图左侧为轮函数的流程,主要包含4种主要运算操作:字节代换(SubByte)、行移位(ShiftRow)、列混合(MixColumn)、轮密钥加(AddRoundKey)。图右侧为密钥编排方案,在Rijndael中称为密钥扩展算法(KeyExpansion)。

初始状态 (state)

在运算之前,我们需要把明文分组成 128 位每段进行分别处理。

对于段长 128 位的明文,需要以从上到下从左到右的次序,成一个 4x4 的矩阵(每个元素是一个字节),即初始状态(state)。

struct  Matrix {
    var content: [[UInt8]]
    init(data: [UInt8]) {
        var result = Array(repeating:Array(repeating:UInt8(0), count: 4), count: 4)
        for i in 0..<4 {
            for j in 0..<4 {
                let index = 4 * i + j
                result[i][j] = index < data.count ? data[index] : 0
            }
        }
        self.content = result
    }
}

值得注意的是,我们得到的是一个二维数组,而每一个字数组代表一列而不是一行。

为了查看效果,我们 继承 CustomStringConvertible ,方便我们将计算结果打印出来。

extension Matrix : CustomStringConvertible {
    var description: String {
        var mstr = ""
        for i in 0..<4 {
            for j in 0..<4 {
                let data = self.content[j][i]
                mstr += String(format: "%2x ", data)
            }
            mstr += "\n"
        }
        return mstr
    }
}

子密钥生成

在进行运算之前,我们还需要对密钥进行处理,生成一组子密钥。并且进行每一轮加密的时候,会依次选用不同的子密钥。因而这一步所生成的密钥又叫做 轮密钥

生成子密钥的算法叫做 密钥扩展算法 (KeyExpansion)。它是 Rijndael 的密钥编排实现算法,其目的是根据种子密钥(用户密钥)生成多组轮密钥。

密钥生成的流程图如下:

img

子密钥的生成是以列为单位进行的,一列是32Bit,四列组成子密钥共128Bit。生成子密钥的数量比AES算法的轮数多一个,因为第一个密钥加法层进行密钥漂白时也需要子密钥。密钥漂白是指在AES的输入盒输出中都使用的子密钥的XOR加法。子密钥在图中都存储在W[0]、W[1]、...、W[43]的扩展密钥数组之中。

首先我们需要将原始的密钥,生成一系列 32 比特,四个字节的 word。并且需要知道当前的密钥变换的轮次。

struct Word {
  
   private var content: [UInt8]
	
   init(_ content:[UInt8]) {
      self.content = content
  }
  
   init(value: UInt32) {
        let r0  = UInt8((value & 0xff000000) >> (3 * 8))
        let r1  = UInt8((value & 0xff0000)   >> (2 * 8))
        let r2  = UInt8((value & 0xff00)     >> (1 * 8))
        let r3  = UInt8(value & 0xff)
        self.init([r0,r1,r2,r3])
   }

    var value : UInt32 {
        var res : UInt32 = 0
        for i in 0..<4 {
            res += UInt32(self.content[i]) << ((4 - i - 1) * 8)
        }
        return res
    }
}

函数 G() 首先将4个输入字节进行翻转,并执行一个按字节的S盒代换,最后用第一个字节与轮系数Rcon进行异或运算。G()函数存在的目的有两个,一是增加密钥编排中的非线性;二是消除AES中的对称性。这两种属性都是抵抗某些分组密码攻击必要的。

轮系数 Rcon 是一个一维数组,一个元素1个字节。长度与子密钥个数相同,i 为进行密钥变换的轮次,也就是子密钥的下标。

// 轮系数
private let kRcon : [UInt8] = [ 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36 ]

g 函数实现如下:

func g () {
  var buf = [content[2], content[3], content[4], content[1]]
  buf =  buf.map({ kSbox[Int($0)] })
  buf[0] ^= kRcon[self.round]
}

那么,生成论密钥实现如下:

static func keyExpansion(_ key: [UInt8]) -> [Matrix] {
    var roundKeys = [Matrix](repeating: Matrix.zero, count: Nr + 1)

    // The first round key is the key itself.
    roundKeys[0]  = Matrix(data: key)

    for round in 1...Nr {
        var words = roundKeys[round-1].words
        words[0] = words[3].g(round: round)  ^ words[0]
        words[1] = words[0] ^ words[1]
        words[2] = words[1] ^ words[2]
        words[3] = words[2] ^ words[3]
        roundKeys[round] = Matrix(words: words)
    }

    return roundKeys
}

轮函数

轮密钥加/密钥加法层
AddRoundKey

在密钥加法层中有两个输入的参数,分别是明文和子密钥k[0],而且这两个输入都是128位的。k[0]实际上就等同于密钥k,具体原因在密钥生成中进行介绍。我们前面在介绍扩展域加减法中提到过,在扩展域中加减法操作和异或运算等价,所以这里的处理也就异常的简单了,只需要将两个输入的数据进行按字节异或操作就会得到运算的结果。

密钥加是将 轮密钥 简单地与状态 state 进行逐比异或。

private func addRoundKey (_ state:State,round: Int) -> State {
    return state ^ self.roundKeys[round]
}

其中,self.roundKeys 为之前生成的轮密钥。

两个矩阵的异或运算,即是两个矩阵中元素依次进行异或变换:

func ^ (rh: AESImpl.State,lh: AESImpl.State) -> AESImpl.State {
    return rh.xor(lh)
}
func xor (_ other: State) -> State {
    var result = [UInt8]()
    for i in 0..<16 {
        result.append( self.data[i]  ^ other.data[i] )
    }
    return State(result)
}

值得注意的是,AddRoundKey 没有逆运算,因为进行两次异或运算之后得到的是其本身。

字节代换
SubBytes

首先需要逐个字节将地对 16 字节的输入数据进行字节替换处理。也就是根据每个字节的值(16进制)为索引,从一个 16 * 16 的替换表 (S-Box) 中查找对应的值进行替换处理。

AES的 S-Box 如下:

行/列 0 1 2 3 4 5 6 7 8 9 A B C D E F
0 0x63 0x7c 0x77 0x7b 0xf2 0x6b 0x6f 0xc5 0x30 0x01 0x67 0x2b 0xfe 0xd7 0xab 0x76
1 0xca 0x82 0xc9 0x7d 0xfa 0x59 0x47 0xf0 0xad 0xd4 0xa2 0xaf 0x9c 0xa4 0x72 0xc0
2 0xb7 0xfd 0x93 0x26 0x36 0x3f 0xf7 0xcc 0x34 0xa5 0xe5 0xf1 0x71 0xd8 0x31 0x15
3 0x04 0xc7 0x23 0xc3 0x18 0x96 0x05 0x9a 0x07 0x12 0x80 0xe2 0xeb 0x27 0xb2 0x75
4 0x09 0x83 0x2c 0x1a 0x1b 0x6e 0x5a 0xa0 0x52 0x3b 0xd6 0xb3 0x29 0xe3 0x2f 0x84
5 0x53 0xd1 0x00 0xed 0x20 0xfc 0xb1 0x5b 0x6a 0xcb 0xbe 0x39 0x4a 0x4c 0x58 0xcf
6 0xd0 0xef 0xaa 0xfb 0x43 0x4d 0x33 0x85 0x45 0xf9 0x02 0x7f 0x50 0x3c 0x9f 0xa8
7 0x51 0xa3 0x40 0x8f 0x92 0x9d 0x38 0xf5 0xbc 0xb6 0xda 0x21 0x10 0xff 0xf3 0xd2
8 0xcd 0x0c 0x13 0xec 0x5f 0x97 0x44 0x17 0xc4 0xa7 0x7e 0x3d 0x64 0x5d 0x19 0x73
9 0x60 0x81 0x4f 0xdc 0x22 0x2a 0x90 0x88 0x46 0xee 0xb8 0x14 0xde 0x5e 0x0b 0xdb
A 0xe0 0x32 0x3a 0x0a 0x49 0x06 0x24 0x5c 0xc2 0xd3 0xac 0x62 0x91 0x95 0xe4 0x79
B 0xe7 0xc8 0x37 0x6d 0x8d 0xd5 0x4e 0xa9 0x6c 0x56 0xf4 0xea 0x65 0x7a 0xae 0x08
C 0xba 0x78 0x25 0x2e 0x1c 0xa6 0xb4 0xc6 0xe8 0xdd 0x74 0x1f 0x4b 0xbd 0x8b 0x8a
D 0x70 0x3e 0xb5 0x66 0x48 0x03 0xf6 0x0e 0x61 0x35 0x57 0xb9 0x86 0xc1 0x1d 0x9e
E 0xe1 0xf8 0x98 0x11 0x69 0xd9 0x8e 0x94 0x9b 0x1e 0x87 0xe9 0xce 0x55 0x28 0xdf
F 0x8c 0xa1 0x89 0x0d 0xbf 0xe6 0x42 0x68 0x41 0x99 0x2d 0x0f 0xb0 0x54 0xbb 0x16

实现起来很简单

func subBytes(datas: [UInt8]) -> [UInt8]
{
    var result = [UInt8]()
    for (index,data) in datas.enumerated() {
        result[index] = kSbox[Int(data)]
    }
    return result
}
InvSubBytes

逆字节代换也就是查逆S盒来变换,逆S盒如下:

行/列 0 1 2 3 4 5 6 7 8 9 A B C D E F
0 0x52 0x09 0x6a 0xd5 0x30 0x36 0xa5 0x38 0xbf 0x40 0xa3 0x9e 0x81 0xf3 0xd7 0xfb
1 0x7c 0xe3 0x39 0x82 0x9b 0x2f 0xff 0x87 0x34 0x8e 0x43 0x44 0xc4 0xde 0xe9 0xcb
2 0x54 0x7b 0x94 0x32 0xa6 0xc2 0x23 0x3d 0xee 0x4c 0x95 0x0b 0x42 0xfa 0xc3 0x4e
3 0x08 0x2e 0xa1 0x66 0x28 0xd9 0x24 0xb2 0x76 0x5b 0xa2 0x49 0x6d 0x8b 0xd1 0x25
4 0x72 0xf8 0xf6 0x64 0x86 0x68 0x98 0x16 0xd4 0xa4 0x5c 0xcc 0x5d 0x65 0xb6 0x92
5 0x6c 0x70 0x48 0x50 0xfd 0xed 0xb9 0xda 0x5e 0x15 0x46 0x57 0xa7 0x8d 0x9d 0x84
6 0x90 0xd8 0xab 0x00 0x8c 0xbc 0xd3 0x0a 0xf7 0xe4 0x58 0x05 0xb8 0xb3 0x45 0x06
7 0xd0 0x2c 0x1e 0x8f 0xca 0x3f 0x0f 0x02 0xc1 0xaf 0xbd 0x03 0x01 0x13 0x8a 0x6b
8 0x3a 0x91 0x11 0x41 0x4f 0x67 0xdc 0xea 0x97 0xf2 0xcf 0xce 0xf0 0xb4 0xe6 0x73
9 0x96 0xac 0x74 0x22 0xe7 0xad 0x35 0x85 0xe2 0xf9 0x37 0xe8 0x1c 0x75 0xdf 0x6e
A 0x47 0xf1 0x1a 0x71 0x1d 0x29 0xc5 0x89 0x6f 0xb7 0x62 0x0e 0xaa 0x18 0xbe 0x1b
B 0xfc 0x56 0x3e 0x4b 0xc6 0xd2 0x79 0x20 0x9a 0xdb 0xc0 0xfe 0x78 0xcd 0x5a 0xf4
C 0x1f 0xdd 0xa8 0x33 0x88 0x07 0xc7 0x31 0xb1 0x12 0x10 0x59 0x27 0x80 0xec 0x5f
D 0x60 0x51 0x7f 0xa9 0x19 0xb5 0x4a 0x0d 0x2d 0xe5 0x7a 0x9f 0x93 0xc9 0x9c 0xef
E 0xa0 0xe0 0x3b 0x4d 0xae 0x2a 0xf5 0xb0 0xc8 0xeb 0xbb 0x3c 0x83 0x53 0x99 0x61
F 0x17 0x2b 0x04 0x7e 0xba 0x77 0xd6 0x26 0xe1 0x69 0x14 0x63 0x55 0x21 0x0c 0x7d

实现起来也很简单

func invSubBytes(datas: [UInt8]) -> [UInt8]
{
    var result = [UInt8]()
    for (index,data) in datas.enumerated() {
        result[index] = kInvSBox[Int(data)]
    }
    return result
}
行位移
ShiftRows

行位移操作最为简单,只需将矩阵的字节进行位置上的置换。ShiftRows 子层属于 AES 手动的扩散层,目的是将单个位上的变换扩散到影响整个状态当中,从而达到雪崩效应

在加密时,保持矩阵的第一行不变,第二行向左移动8Bit(一个字节)、第三行向左移动2个字节、第四行向左移动3个字节。

正向行位移

static func shiftRows (_ state: Matrix) {

    var temp : UInt8 = 0

    // Rotate first row 1 columns to left
    temp         = state[0][1]
    state[0][1]  = state[1][1]
    state[1][1]  = state[2][1]
    state[2][1]  = state[3][1]
    state[3][1]  = temp

    // Rotate second row 2 columns to left
    let temp0    = state[0][2]
    let temp1    = state[1][2]
    state[0][2]  = state[2][2]
    state[1][2]  = state[3][2]
    state[2][2]  = temp0
    state[3][2]  = temp1

    // Rotate third row 3 columns to left
    temp         = state[0][3]
    state[0][3]  = state[3][3]
    state[3][3]  = state[2][3]
    state[2][3]  = state[1][3]
    state[1][3]  = temp
}
InvShiftRows

解密时只需要按照相反的方向移动字节就可以了

static func shiftRows (_ state: Matrix) {

    var temp : UInt8 = 0

    // Rotate first row 1 columns to right
    temp         = state[0][1]
    state[0][1]  = state[3][1]
    state[3][1]  = state[2][1]
    state[2][1]  = state[1][1]
    state[1][1]  = temp

    // Rotate second row 2 columns to right
    let temp0    = state[0][2]
    let temp1    = state[1][2]
    state[0][2]  = state[2][2]
    state[1][2]  = state[3][2]
    state[2][2]  = temp0
    state[3][2]  = temp1

    // Rotate third row 3 columns to right
    temp         = state[0][3]
    state[0][3]  = state[1][3]
    state[1][3]  = state[2][3]
    state[2][3]  = state[3][3]
    state[3][3]  = temp
}

#####列混淆

列混淆子层是AES算法中最为复杂的部分,属于扩散层,列混淆操作是AES算法中主要的扩散元素,它混淆了输入矩阵的每一列,使输入的每个字节都会影响到4个输出字节。行位移子层和列混淆子层的组合使得经过三轮处理以后,矩阵的每个字节都依赖于16个明文字节成可能。其中包含了矩阵乘法、伽罗瓦域内加法和乘法的相关知识。

MixColumns

列混合变换是通过矩阵相乘来实现的,经行移位后的状态矩阵与固定的矩阵相乘,得到混淆后的状态矩阵,如下图的公式所示:

col

状态矩阵中的第j列(0 ≤j≤3)的列混合可以表示为下图所示:

col2

其中,矩阵元素的乘法和加法都是定义在基于GF(2^8)上的二元运算,并不是通常意义上的乘法和加法。这里涉及到一些信息安全上的数学知识,不过不懂这些知识也行。其实这种二元运算的加法等价于两个字节的异或,乘法则复杂一点。对于一个8位的二进制数来说,使用域上的乘法乘以(00000010)等价于左移1位(低位补0)后,再根据情况同(00011011)进行异或运算,设S1 = (a7 a6 a5 a4 a3 a2 a1 a0),刚0x02 * S1如下图所示:

col3

也就是说,如果a7为1,则进行异或运算,否则不进行。 类似地,乘以(00000100)可以拆分成两次乘以(00000010)的运算:

col4

乘以(0000 0011)可以拆分成先分别乘以(0000 0001)和(0000 0010),再将两个乘积异或:

col5

具体实现为:

static func mixColumns (_ state: Matrix) {
    var t    : UInt8 = 0
    var Temp : UInt8 = 0
    var Tm   : UInt8 = 0

    for i in 0..<4 {
        t            = state[i][0]
        Temp         = state[i][0] ^ state[i][1] ^ state[i][2] ^ state[i][3]

        Tm           = state[i][0] ^ state[i][1]
        Tm           = xtime(Tm)
        state[i][0]  ^= Tm ^ Temp

        Tm           = state[i][1] ^ state[i][2]
        Tm           = xtime(Tm)
        state[i][1]  ^= Tm ^ Temp

        Tm           = state[i][2] ^ state[i][3]
        Tm           = xtime(Tm)
        state[i][2]  ^= Tm ^ Temp

        Tm           = state[i][3] ^ t
        Tm           = xtime(Tm)
        state[i][3]  ^= Tm ^ Temp
    }
}

其中 xtime 函数为

static func xtime (_ x: UInt8) -> UInt8 {
    return ((x<<1) ^ (((x>>7) & 1) * 0x1b));
}
InvMixColumns

逆向列混合变换可由下图的矩阵乘法定义:

col6

可以验证,逆变换矩阵同正变换矩阵的乘积恰好为单位矩阵。

具体实现为:

static func invMixColumns (_ state: Matrix) {
    for i in 0..<4 {
        let a = state[i][0]
        let b = state[i][1]
        let c = state[i][2]
        let d = state[i][3]

        state[i][0]  = multiply(a, 0x0e) ^ multiply(b, 0x0b) ^ multiply(c, 0x0d) ^ multiply(d, 0x09)
        state[i][1]  = multiply(a, 0x09) ^ multiply(b, 0x0e) ^ multiply(c, 0x0b) ^ multiply(d, 0x0d)
        state[i][2]  = multiply(a, 0x0d) ^ multiply(b, 0x09) ^ multiply(c, 0x0e) ^ multiply(d, 0x0b)
        state[i][3]  = multiply(a, 0x0b) ^ multiply(b, 0x0d) ^ multiply(c, 0x09) ^ multiply(d, 0x0e)
    }
}

其中,multiply 乘法运算为:

static func multiply (_ x: UInt8,_ y: UInt8) -> UInt8 {
    var result =   (y & 1) * x
        result ^=  (y>>1 & 1) * xtime(x)
        result ^=  (y>>2 & 1) * xtime(xtime(x))
        result ^=  (y>>3 & 1) * xtime(xtime(xtime(x)))
        result ^=  (y>>4 & 1) * xtime(xtime(xtime(xtime(x))))
   return result
}

参考文献