密碼學之對稱加密

Gundy發表於2019-09-05

本文將介紹位元序列運算中的異或運算,同時簡單介紹DES、3DES、AES等對稱加密演算法,最後給出對應的Golang加密程式碼。

位元序列密碼

首先我們要明白二個概念,一個是計算機編碼,我們都知道計算機操作的物件並不是文字影象,而是有0和1排列組成的位元序列;一個是異或運算(XOR), 二個不同的位元位異或運算結果是1,不同的位元位異或運算結果是0。

假設我們將01001100這個位元序列成為A,將10101010這個位元序列成為B,那麼A與B的異或運算結果就如下所示:

由於二個相同的數進行異或運算的結果是0,因此如果將A與B異或的結果再與B進行異或預算,則結果就會變回A。

可能你已經發現了,上面的計算和加密、解密的步驟非常相似。

  • 將明文A與祕鑰B進行加密運算,得到密文A⊕B
  • 將密文A⊕B用祕鑰B進行解密,得到明文A

實際上,主要選擇一個合適的B,僅僅使用異或運算就可以實現一個高強度的密碼。

DES

DES(Data EncryptionStandard) 是1977年美國聯邦資訊處理標準(FIPS)中所採用的一種對稱密碼(FIPS46.3)。DES一直以來被美國以及其他國家的政府和銀行等廣泛使用。然而,隨著計算機的進步,現在DES已經能夠被暴力破解,強度大不如前了。

RSA公司舉辦過破澤DES金鑰的比賽(DESChallenge),我們可以看一看RSA公司官方公佈的比賽結果:

  • 1997年的DES Challenge1中用了96天破譯金鑰
  • 1998年的DES ChallengeIl-I中用了41天破譯金鑰
  • 1998年的DES ChallengeII-2中用了56小時破譯金鑰
  • 1999年的DES ChallengeIll中只用了22小時15分鐘破譯金鑰

由於DES的密文可以在短時間內被破譯,因此除了用它來解密以前的密文以外,現在我們不應該再使用DES了。

加密和解密

DES是一種將64位元的明文加密成64位元的密文的對稱密碼演算法, 它的金鑰長度是56位元。儘管從規格上來說,DES的金鑰長度是64位元,但由於每隔7位元會設定一個用於錯誤檢查的位元,因此實質上其金鑰長度是56位元。

DES是以64位元的明文(位元序列)為一個單位來進行加密的,這個64位元的單位稱為分組。一般來說,以分組為單位進行處理的密碼演算法稱為分組密碼(blockcipher),DES就是分組密碼的一種。

DES每次只能加密64位元的資料,如果要加密的明文比較長,就需要對DES加密進行迭代(反覆),而迭代的具體方式就稱為模式(mode)(後續文章詳細講解分組密碼的模式)。

大B -> bit

小b -> byte

祕鑰長度(56bit + 8bit)/8 = 8byte

  • DES的加密與解密 - 圖例

Go中對DES的操作

加解密實現思路
  • 加密 - CBC分組模式

    1. 建立並返回一個使用DES演算法的cipher.Block介面
      • 祕鑰長度為64bit, 即 64/8 = 8位元組(byte)
    2. 對最後一個明文分組進行資料填充
      • DES是以64位元的明文(位元序列)為一個單位來進行加密的
      • 最後一組不夠64bit, 則需要進行資料填充( 參考第三章)
    3. 建立一個密碼分組為連結模式的, 底層使用DES加密的BlockMode介面
    4. 加密連續的資料塊
  • 解密

    1. 建立並返回一個使用DES演算法的cipher.Block介面
    2. 建立一個密碼分組為連結模式的, 底層使用DES解密的BlockMode介面
    3. 資料塊解密
    4. 去掉最後一組的填充資料
加解密的程式碼實現

在Go中使用DES需要匯入的包:

import (
    "crypto/des"
    "crypto/cipher"
    "fmt"
    "bytes"
)

DES加密程式碼:

// src -> 要加密的明文
// key -> 祕鑰, 大小為: 8byte
func DesEncrypt_CBC(src, key []byte) []byte{
    // 1. 建立並返回一個使用DES演算法的cipher.Block介面
    block, err := des.NewCipher(key)
    // 2. 判斷是否建立成功
    if err != nil{
        panic(err)
    }
    // 3. 對最後一個明文分組進行資料填充
    src = PKCS5Padding(src, block.BlockSize())
    // 4. 建立一個密碼分組為連結模式的, 底層使用DES加密的BlockMode介面
    //    引數iv的長度, 必須等於b的塊尺寸
    tmp := []byte("helloAAA")
    blackMode := cipher.NewCBCEncrypter(block, tmp)
    // 5. 加密連續的資料塊
    dst := make([]byte, len(src))
    blackMode.CryptBlocks(dst, src)

    fmt.Println("加密之後的資料: ", dst)

    // 6. 將加密資料返回
    return dst
}

DES解密程式碼

// src -> 要解密的密文
// key -> 祕鑰, 和加密祕鑰相同, 大小為: 8byte
func DesDecrypt_CBC(src, key []byte) []byte {
    // 1. 建立並返回一個使用DES演算法的cipher.Block介面
    block, err := des.NewCipher(key)
    // 2. 判斷是否建立成功
    if err != nil{
        panic(err)
    }
    // 3. 建立一個密碼分組為連結模式的, 底層使用DES解密的BlockMode介面
    tmp := []byte("helloAAA")
    blockMode := cipher.NewCBCDecrypter(block, tmp)
    // 4. 解密資料
    dst := src
    blockMode.CryptBlocks(src, dst)
    // 5. 去掉最後一組填充的資料
    dst = PKCS5UnPadding(dst)

    // 6. 返回結果
    return dst
}

最後一個分組新增填充資料和移除新增資料程式碼

// 使用pks5的方式填充
func PKCS5Padding(ciphertext []byte, blockSize int) []byte{
    // 1. 計算最後一個分組缺多少個位元組
    padding := blockSize - (len(ciphertext)%blockSize)
    // 2. 建立一個大小為padding的切片, 每個位元組的值為padding
    padText := bytes.Repeat([]byte{byte(padding)}, padding)
    // 3. 將padText新增到原始資料的後邊, 將最後一個分組缺少的位元組數補齊
    newText := append(ciphertext, padText...)
    return newText
}

// 刪除pks5填充的尾部資料
func PKCS5UnPadding(origData []byte) []byte{
    // 1. 計算資料的總長度
    length := len(origData)
    // 2. 根據填充的位元組值得到填充的次數
    number := int(origData[length-1])
    // 3. 將尾部填充的number個位元組去掉
    return origData[:(length-number)]
}

測試函式

func DESText() {
    // 加密
    key := []byte("11111111")
    result := DesEncrypt_CBC([]byte("床前明月光, 疑是地上霜. 舉頭望明月, 低頭思故鄉."), key)
    fmt.Println(base64.StdEncoding.EncodeToString(result))
    // 解密
    result = DesDecrypt_CBC(result, key)
    fmt.Println("解密之後的資料: ", string(result))
}

重要的函式說明

  1. 生成一個底層使用DES加/解密的Block介面物件

    函式對應的包: import "crypto/des"
    func NewCipher(key []byte) (cipher.Block, error)
    - 引數 key: des對稱加密使用的密碼, 密碼長度為64bit, 即8byte
    - 返回值 cipher.Block: 建立出的使用DES加/解密的Block介面物件
  2. 建立一個密碼分組為CBC模式, 底層使用b加密的BlockMode介面物件

    函式對應的包: import "crypto/cipher"
    func NewCBCEncrypter(b Block, iv []byte) BlockMode
       - 引數 b: 使用des.NewCipher函式建立出的Block介面物件
       - 引數 iv: 事先準備好的一個長度為一個分組長度的位元序列, 每個分組為64bit, 即8byte
       - 返回值: 得到的BlockMode介面物件
  3. 使用cipher包的BlockMode介面物件對資料進行加/解密

    介面對應的包: import "crypto/cipher"
    type BlockMode interface {
       // 返回加密位元組塊的大小
       BlockSize() int
       // 加密或解密連續的資料塊,src的尺寸必須是塊大小的整數倍,src和dst可指向同一記憶體地址
       CryptBlocks(dst, src []byte)
    }
    介面中的 CryptBlocks(dst, src []byte) 方法:
       - 引數 dst: 傳出引數, 儲存加密或解密運算之後的結果 
       - 引數 src: 傳入引數, 需要進行加密或解密的資料切片(字串)
  4. 建立一個密碼分組為CBC模式, 底層使用b解密的BlockMode介面物件

    函式對應的包: import "crypto/cipher"
    func NewCBCDecrypter(b Block, iv []byte) BlockMode
       - 引數 b: 使用des.NewCipher函式建立出的Block介面物件
       - 引數 iv: 事先準備好的一個長度為一個分組長度的位元序列, 每個分組為64bit, 即8byte, 
                  該序列的值需要和NewCBCEncrypter函式的第二個引數iv值相同
       - 返回值: 得到的BlockMode介面物件
  5. 自定義函式介紹

    對稱加密加密需要對資料進行分組, 保證每個分組的資料長度相等, 如果最後一個分組長度不夠, 需要進行填充
    func PKCS5Padding(ciphertext []byte, blockSize int) []byte
       - 引數 ciphertext: 需要加密的原始資料
       - 引數 blockSize: 每個分組的長度, 跟使用的加密演算法有關係
        * des:64bit, 8byte
        * 3des:64bit, 8byte
        * aes: 128bit, 16byte

三重DES

現在DES已經可以在現實的時間內被暴力破解,因此我們需要一種用來替代DES的分組密碼,三重DES就是出於這個目的被開發出來的。

三重DES(triple-DES)是為了增加DES的強度,將DES重複3次所得到的一種密碼演算法,通常縮寫為3DES

三重DES的加密

三重DES的加解密機制如下圖所示:

加->解->加 -> 目的是為了相容DES(如果三個祕鑰都一樣)

3des祕鑰長度24位元組 = 1234567a 1234567b 1234567a

明文: 10

祕鑰1: 2

祕鑰2: 3

祕鑰3: 4

加密演算法: 明文+祕鑰

解密演算法: 密文-祕鑰

10+2-3+4

)

明文經過三次DES處理才能變成最後的密文,由於DES金鑰的長度實質上是56位元,因此三重DES的金鑰長度就是56×3=168位元, 加上用於錯誤檢測的標誌位8x3, 共192bit。

從上圖我們可以發現,三重DES並不是進行三次DES加密(加密-->加密-->加密),而是加密-->解密-->加密的過程。在加密演算法中加人解密操作讓人感覺很不可思議,實際上這個方法是IBM公司設計出來的,目的是為了讓三重DES能夠相容普通的DES。

當三重DES中所有的金鑰都相同時,三重DES也就等同於普通的DES了。這是因為在前兩步加密-->解密之後,得到的就是最初的明文。因此,以前用DES加密的密文,就可以通過這種方式用三重DES來進行解密。也就是說,三重DES對DES具備向下相容性。

如果金鑰1和金鑰3使用相同的金鑰,而金鑰2使用不同的金鑰(也就是隻使用兩個DES金鑰),這種三重DES就稱為DES-EDE2。EDE表示的是加密(Encryption) -->解密(Decryption)-->加密(Encryption)這個流程。

金鑰1、金鑰2、金鑰3全部使用不同的位元序列的三重DES稱為DES-EDE3。

儘管三重DES目前還被銀行等機構使用,但其處理速度不高,而且在安全性方面也逐漸顯現出了一些問題。

Go中對3DES的操作

加解密實現思路
  • 加密 - CBC分組模式

    1. 建立並返回一個使用3DES演算法的cipher.Block介面
      • *祕鑰長度為64bit3=192bit, 即 192/8 = 24位元組(byte)**
    2. 對最後一個明文分組進行資料填充
      • 3DES是以64位元的明文(位元序列)為一個單位來進行加密的
      • 最後一組不夠64bit, 則需要進行資料填充( 參考第三章)
    3. 建立一個密碼分組為連結模式的, 底層使用3DES加密的BlockMode介面
    4. 加密連續的資料塊
  • 解密

    1. 建立並返回一個使用3DES演算法的cipher.Block介面
    2. 建立一個密碼分組為連結模式的, 底層使用3DES解密的BlockMode介面
    3. 資料塊解密
    4. 去掉最後一組的填充資料
加解密的程式碼實現

3DES加密程式碼

// 3DES加密
func TripleDESEncrypt(src, key []byte) []byte {
    // 1. 建立並返回一個使用3DES演算法的cipher.Block介面
    block, err := des.NewTripleDESCipher(key)
    if err != nil{
        panic(err)
    }
    // 2. 對最後一組明文進行填充
    src = PKCS5Padding(src, block.BlockSize())
    // 3. 建立一個密碼分組為連結模式, 底層使用3DES加密的BlockMode模型
    blockMode := cipher.NewCBCEncrypter(block, key[:8])
    // 4. 加密資料
    dst := src
    blockMode.CryptBlocks(dst, src)
    return dst
}

3DES解密程式碼

// 3DES解密
func TripleDESDecrypt(src, key []byte) []byte {
    // 1. 建立3DES演算法的Block介面物件
    block, err := des.NewTripleDESCipher(key)
    if err != nil{
        panic(err)
    }
    // 2. 建立密碼分組為連結模式, 底層使用3DES解密的BlockMode模型
    blockMode := cipher.NewCBCDecrypter(block, key[:8])
    // 3. 解密
    dst := src
    blockMode.CryptBlocks(dst, src)
    // 4. 去掉尾部填充的資料
    dst = PKCS5UnPadding(dst)
    return dst
}

重要的函式說明

  1. 生成一個底層使用3DES加/解密的Block介面物件

    函式對應的包: import "crypto/des"
    func NewTripleDESCipher(key []byte) (cipher.Block, error)
    - 引數 key: 3des對稱加密使用的密碼, 密碼長度為(64*3)bit, 即(8*3)byte
    - 返回值 cipher.Block: 建立出的使用DES加/解密的Block介面物件
  2. 建立一個密碼分組為CBC模式, 底層使用b加密的BlockMode介面物件

    函式對應的包: import "crypto/cipher"
    func NewCBCEncrypter(b Block, iv []byte) BlockMode
       - 引數 b: 使用des.NewTripleDESCipher 函式建立出的Block介面物件
       - 引數 iv: 事先準備好的一個長度為一個分組長度的位元序列, 每個分組為64bit, 即8byte
       - 返回值: 得到的BlockMode介面物件
  3. 使用cipher包的BlockMode介面物件對資料進行加/解密

    介面對應的包: import "crypto/cipher"
    type BlockMode interface {
       // 返回加密位元組塊的大小
       BlockSize() int
       // 加密或解密連續的資料塊,src的尺寸必須是塊大小的整數倍,src和dst可指向同一記憶體地址
       CryptBlocks(dst, src []byte)
    }
    介面中的 CryptBlocks(dst, src []byte) 方法:
       - 引數 dst: 傳出引數, 儲存加密或解密運算之後的結果 
       - 引數 src: 傳入引數, 需要進行加密或解密的資料切片(字串)
  4. 建立一個密碼分組為CBC模式, 底層使用b解密的BlockMode介面物件

    函式對應的包: import "crypto/cipher"
    func NewCBCDecrypter(b Block, iv []byte) BlockMode
       - 引數 b: 使用des.NewTripleDESCipher 函式建立出的Block介面物件
       - 引數 iv: 事先準備好的一個長度為一個分組長度的位元序列, 每個分組為64bit, 即8byte, 
                  該序列的值需要和NewCBCEncrypter函式的第二個引數iv值相同
       - 返回值: 得到的BlockMode介面物件

AES

AES(Advanced Encryption Standard)是取代其前任標準(DES)而成為新標準的一種對稱密碼演算法。全世界的企業和密碼學家提交了多個對稱密碼演算法作為AES的候選,最終在2000年從這些候選演算法中選出了一種名為Rijndael的對稱密碼演算法,並將其確定為了AES。

Rijndael的分組長度為128位元,金鑰長度可以以32位元為單位在128位元到256位元的範圍內進行選擇(不過在AES的規格中,金鑰長度只有128、192和256位元三種)。

  • 128bit = 16位元組
  • 192bit = 24位元組
  • 256bit = 32位元組

在go提供的介面中祕鑰長度只能是16位元組

AES的加密和解密

和DES—樣,AES演算法也是由多輪所構成的,下圖展示了每一輪的大致計算步驟。DES使用Feistel網路作為其基本結構,而AES沒有使用Feistel網路,而是使用了SPN Rijndael的輸人分組為128位元,也就是16位元組。首先,需要逐個位元組地對16位元組的輸入資料進行SubBytes處理。所謂SubBytes,就是以每個位元組的值(0~255中的任意值)為索引,從一張擁有256個值的替換表(S-Box)中查詢出對應值的處理,也是說,將一個1位元組的值替換成另一個1位元組的值。

SubBytes之後需要進行ShiftRows處理,即將SubBytes的輸出以位元組為單位進行打亂處理。從下圖的線我們可以看出,這種打亂處理是有規律的。

ShiftRows之後需要進行MixCo1umns處理,即對一個4位元組的值進行位元運算,將其變為另外一個4位元組值。

最後,需要將MixColumns的輸出與輪金鑰進行XOR,即進行AddRoundKey處理。到這裡,AES的一輪就結東了。實際上,在AES中需要重複進行10 ~ 14輪計算。

通過上面的結構我們可以發現輸入的所有位元在一輪中都會被加密。和每一輪都只加密一半輸人的位元的Feistel網路相比,這種方式的優勢在於加密所需要的輪數更少。此外,這種方式還有一個優勢,即SubBytes,ShiftRows和MixColumns可以分別按位元組、行和列為單位進行平行計算。

  • SubBytes -- 位元組代換
  • ShiftRows -- 行移位代換
  • MixColumns -- 列混淆
  • AddRoundKey -- 輪金鑰加

下圖展示了AES中一輪的解密過程。從圖中我們可以看出,SubBytes、ShiftRows、MixColumns分別存在反向運算InvSubBytes、InvShiftRows、InvMixColumns,這是因為AES不像Feistel網路一樣能夠用同一種結構實現加密和解密。

  • InvSubBytes -- 逆位元組替代
  • InvShiftRows -- 逆行移位
  • InvMixColumns -- 逆列混淆

Go中對AES的使用

加解密實現思路
  • 加密 - CBC分組模式

    1. 建立並返回一個使用AES演算法的cipher.Block介面
      • 祕鑰長度為128bit, 即 128/8 = 16位元組(byte)
    2. 對最後一個明文分組進行資料填充
      • AES是以128位元的明文(位元序列)為一個單位來進行加密的
      • 最後一組不夠128bit, 則需要進行資料填充( 參考第三章)
    3. 建立一個密碼分組為連結模式的, 底層使用AES加密的BlockMode介面
    4. 加密連續的資料塊
  • 解密

    1. 建立並返回一個使用AES演算法的cipher.Block介面
    2. 建立一個密碼分組為連結模式的, 底層使用AES解密的BlockMode介面
    3. 資料塊解密
    4. 去掉最後一組的填充資料
加解密的程式碼實現

AES加密程式碼

// AES加密
func AESEncrypt(src, key []byte) []byte{
    // 1. 建立一個使用AES加密的塊物件
    block, err := aes.NewCipher(key)
    if err != nil{
        panic(err)
    }
    // 2. 最後一個分組進行資料填充
    src = PKCS5Padding(src, block.BlockSize())
    // 3. 建立一個分組為連結模式, 底層使用AES加密的塊模型物件
    blockMode := cipher.NewCBCEncrypter(block, key[:block.BlockSize()])
    // 4. 加密
    dst := src
    blockMode.CryptBlocks(dst, src)
    return dst
}

AES解密

// AES解密
func AESDecrypt(src, key []byte) []byte{
    // 1. 建立一個使用AES解密的塊物件
    block, err := aes.NewCipher(key)
    if err != nil{
        panic(err)
    }
    // 2. 建立分組為連結模式, 底層使用AES的解密模型物件
    blockMode := cipher.NewCBCDecrypter(block, key[:block.BlockSize()])
    // 3. 解密
    dst := src
    blockMode.CryptBlocks(dst, src)
    // 4. 去掉尾部填充的字
    dst = PKCS5UnPadding(dst)
    return dst
}

重要的函式說明

  1. 生成一個底層使用AES加/解密的Block介面物件

    函式對應的包: import "crypto/aes"
    func NewCipher(key []byte) (cipher.Block, error)
    - 引數 key: aes對稱加密使用的密碼, 密碼長度為128bit, 即16byte
    - 返回值 cipher.Block: 建立出的使用AES加/解密的Block介面物件
  2. 建立一個密碼分組為CBC模式, 底層使用b加密的BlockMode介面物件

    函式對應的包: import "crypto/cipher"
    func NewCBCEncrypter(b Block, iv []byte) BlockMode
       - 引數 b: 使用aes.NewCipher函式建立出的Block介面物件
       - 引數 iv: 事先準備好的一個長度為一個分組長度的位元序列, 每個分組為64bit, 即8byte
       - 返回值: 得到的BlockMode介面物件
  3. 使用cipher包的BlockMode介面物件對資料進行加/解密

    介面對應的包: import "crypto/cipher"
    type BlockMode interface {
       // 返回加密位元組塊的大小
       BlockSize() int
       // 加密或解密連續的資料塊,src的尺寸必須是塊大小的整數倍,src和dst可指向同一記憶體地址
       CryptBlocks(dst, src []byte)
    }
    介面中的 CryptBlocks(dst, src []byte) 方法:
       - 引數 dst: 傳出引數, 儲存加密或解密運算之後的結果 
       - 引數 src: 傳入引數, 需要進行加密或解密的資料切片(字串)
  4. 建立一個密碼分組為CBC模式, 底層使用b解密的BlockMode介面物件

    函式對應的包: import "crypto/cipher"
    func NewCBCDecrypter(b Block, iv []byte) BlockMode
       - 引數 b: 使用des.NewCipher函式建立出的Block介面物件
       - 引數 iv: 事先準備好的一個長度為一個分組長度的位元序列, 每個分組為128bit, 即16byte, 
                  該序列的值需要和NewCBCEncrypter函式的第二個引數iv值相同
       - 返回值: 得到的BlockMode介面物件

應選擇哪種對稱加密

前面介紹了DES、三重DES和AES等對稱密碼,那麼我們到底應該使用哪一種對稱密碼演算法呢?

  1. 今後最好不要將DES用於新的用途,因為隨著計算機技術的進步,現在用暴力破解法已經能夠在現實的時間內完成對DES的破譯。但是,在某些情況下也需要保持與舊版本軟體的相容性。
  2. 出於相容性的因素三重DES在今後還會使用一段時間,但會逐漸被AES所取代。
  3. 今後大家應該使用的演算法是AES(Rijndael),因為它安全、快速,而且能夠在各種平臺上工作。此外,由於全世界的密碼學家都在對AES進行不斷的驗證,因此即便萬一發現它有什麼缺陷,也會立刻告知全世界並修復這些缺陷。

一般來說,我們不應該使用任何自制的密碼演算法,而是應該使用AES。因為AES在其選定過程中,經過了全世界密碼學家所進行的高品質的驗證工作,而對於自制的密碼演算法則很難進行這樣的驗證。

歡迎與我交流

相關文章