區塊鏈背後的資訊保安(2) DES、3DES加密演算法原理及其GO語言實現

尹成發表於2018-05-21
# DES、3DES加密演算法原理及其GO語言實現

DES加密演算法,為對稱加密演算法中的一種。70年代初由IBM研發,後1977年被美國國家標準局採納為資料加密標準,即DES全稱的由來:Data Encryption Standard。
對稱加密演算法,是相對於非對稱加密演算法而言的。兩者區別在於,對稱加密在加密和解密時使用同一金鑰,而非對稱加密在加密和解密時使用不同的金鑰,即公鑰和私鑰。
常見的DES、3DES、AES均為對稱加密演算法,而RSA、橢圓曲線加密演算法,均為非對稱加密演算法。

DES是以64位元的明文為一個單位來進行加密的,超過64位元的資料,要求按固定的64位元的大小分組,分組有很多模式,後續單獨總結,暫時先介紹DES加密演算法。
DES使用的金鑰長度為64位元,但由於每隔7個位元設定一個奇偶校驗位,因此其金鑰長度實際為56位元。奇偶校驗為最簡單的錯誤檢測碼,即根據一組二進位制程式碼中1的個數是奇數或偶數來檢測錯誤。

## Feistel網路

DES的基本結構,由IBM公司的Horst Feistel設計,因此稱Feistel網路。
在Feistel網路中,加密的每個步驟稱為輪,經過初始置換後的64位明文,進行了16輪Feistel輪的加密過程,最後經過終結置換後形成最終的64位密文。
如下為Feistel網路的示意圖:



64位元明文被分為左、右兩部分處理,右側資料和子金鑰經過輪函式f生成用於加密左側資料的位元序列,與左側資料異或運算,運算結果輸出為加密後的左側,右側資料則直接輸出為右側。
其中子金鑰為本輪加密使用的金鑰,每次Feistel均使用不同的子金鑰。子金鑰的計算,以及輪函式的細節,稍後下文介紹。
由於一次Feistel輪並不會加密右側,因此需要將上一輪輸出後的左右兩側對調後,重複Feistel輪的過程,DES演算法共計進行16次Feistel輪,最後一輪輸出後左右兩側無需對調。

DES加密和解密的過程一致,均使用Feistel網路實現,區別僅在於解密時,密文作為輸入,並逆序使用子金鑰。

go標準庫中DES演算法實現如下:

```go
func cryptBlock(subkeys []uint64, dst, src []byte, decrypt bool) {
    b := binary.BigEndian.Uint64(src)
    //初始置換
    b = permuteInitialBlock(b)
    left, right := uint32(b>>32), uint32(b)

    var subkey uint64
    //共計16次feistel輪
    for i := 0; i < 16; i++ {
        //加密和解密使用子金鑰順序相反
        if decrypt {
            subkey = subkeys[15-i]
        } else {
            subkey = subkeys[i]
        }
        //feistel輪函式
        left, right = right, left^feistel(right, subkey)
    }
    //最後一輪無需對調
    preOutput := (uint64(right) << 32) | uint64(left)
    //終結置換
    binary.BigEndian.PutUint64(dst, permuteFinalBlock(preOutput))
}
//程式碼位置src/crypto/des/block.go
```

## 初始置換和終結置換

進入Feistel輪之前,64位明文需做一次初始置換。Feistel輪結束後,需做一次反向操作,即終結置換。
初始置換和終結置換目的是為加強硬體的破解難度而加的。

附go標準庫中使用的初始置換表和終結置換表如下:

```go
//初始置換表
var initialPermutation = [64]byte{
    6, 14, 22, 30, 38, 46, 54, 62,
    4, 12, 20, 28, 36, 44, 52, 60,
    2, 10, 18, 26, 34, 42, 50, 58,
    0, 8, 16, 24, 32, 40, 48, 56,
    7, 15, 23, 31, 39, 47, 55, 63,
    5, 13, 21, 29, 37, 45, 53, 61,
    3, 11, 19, 27, 35, 43, 51, 59,
    1, 9, 17, 25, 33, 41, 49, 57,
}

//終結置換表
var finalPermutation = [64]byte{
    24, 56, 16, 48, 8, 40, 0, 32,
    25, 57, 17, 49, 9, 41, 1, 33,
    26, 58, 18, 50, 10, 42, 2, 34,
    27, 59, 19, 51, 11, 43, 3, 35,
    28, 60, 20, 52, 12, 44, 4, 36,
    29, 61, 21, 53, 13, 45, 5, 37,
    30, 62, 22, 54, 14, 46, 6, 38,
    31, 63, 23, 55, 15, 47, 7, 39,
}
//程式碼位置src/crypto/des/const.go
```

## 子金鑰的計算

DES初始金鑰為64位,其中8位用於奇偶校驗,實際金鑰為56位,64位初始金鑰經過PC-1金鑰置換後,生成56位串。
經PC-1置換後56位的串,分為左右兩部分,各28位,分別左移1位,形成C0和D0,C0和D0合併成56位,經PC-2置換後生成48位子金鑰K0。
C0和D0分別左移1位,形成C1和D1,C1和D1合併成56位,經PC-2置換後生成子金鑰K1。
以此類推,直至生成子金鑰K15。但注意每輪迴圈左移的位數,有如下規定:

```go
var ksRotations = [16]uint8{1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1}
//程式碼位置src/crypto/des/const.go
```

如下為子金鑰計算示意圖:



go標準庫中DES子金鑰計算的程式碼如下:

```go
func (c *desCipher) generateSubkeys(keyBytes []byte) {
    key := binary.BigEndian.Uint64(keyBytes)
    //PC-1金鑰置換,生成56位串
    permutedKey := permuteBlock(key, permutedChoice1[:])

    //56位串分左右兩部分,各28位,ksRotate為依次迴圈左移1位
    leftRotations := ksRotate(uint32(permutedKey >> 28))
    rightRotations := ksRotate(uint32(permutedKey<<4) >> 4)

    //生成子金鑰
    for i := 0; i < 16; i++ {
        //合併左右兩部分,之後PC-2置換
        pc2Input := uint64(leftRotations[i])<<28 | uint64(rightRotations[i])
        c.subkeys[i] = permuteBlock(pc2Input, permutedChoice2[:])
    }
}
//程式碼位置src/crypto/des/block.go
```

附go標準庫中使用的PC-1置換表和PC-2置換表:

```go
//PC-1置換表
var permutedChoice1 = [56]byte{
    7, 15, 23, 31, 39, 47, 55, 63,
    6, 14, 22, 30, 38, 46, 54, 62,
    5, 13, 21, 29, 37, 45, 53, 61,
    4, 12, 20, 28, 1, 9, 17, 25,
    33, 41, 49, 57, 2, 10, 18, 26,
    34, 42, 50, 58, 3, 11, 19, 27,
    35, 43, 51, 59, 36, 44, 52, 60,
}

//PC-2置換表
var permutedChoice2 = [48]byte{
    42, 39, 45, 32, 55, 51, 53, 28,
    41, 50, 35, 46, 33, 37, 44, 52,
    30, 48, 40, 49, 29, 36, 43, 54,
    15, 4, 25, 19, 9, 1, 26, 16,
    5, 11, 23, 8, 12, 7, 17, 0,
    22, 3, 10, 14, 6, 20, 27, 24,
}
//程式碼位置src/crypto/des/const.go
```

## Feistel輪函式

每次Feistel輪函式內部,均經過4種運算,即:
* 1、擴充套件置換:右側32位做擴充套件置換,擴充套件置換將32位輸入擴充套件成為48位輸出,使得擴充套件後輸出資料長度與48位子金鑰等長。
* 2、異或運算:右側32位擴充套件置換為48位後,與48位子金鑰做異或運算。
* 3、S盒置換:將異或運算後的48位結果,分成8個6位的塊,每塊通過S盒置換產生4位的輸出,8個塊S盒置換後組成32位的輸出。
S盒置換的過程為:6位中取第1位和第6位組成行號,剩餘第2、3、4、5位組成列號,從S盒置換表中取出相應行、列的十進位制數,並轉化為4位二進位制數,即為S盒輸出。
* 4、P盒置換:S盒置換後的32位輸出資料,進行P盒置換,仍然輸出為32位資料。

如下為Feistel輪函式示意圖:



go標準庫中DES Feistel輪函式程式碼如下:

```go
func feistel(right uint32, key uint64) (result uint32) {
    //右側32位擴充套件置換為48位,並與48位子金鑰做異或運算
    sBoxLocations := key ^ expandBlock(right)
    var sBoxResult uint32
    for i := uint8(0); i < 8; i++ {
        //sBoxLocations>>42、sBoxLocations <<= 6,按每6位分塊
        sBoxLocation := uint8(sBoxLocations>>42) & 0x3f
        sBoxLocations <<= 6
        //6位中取第1位和第6位組成行號
        row := (sBoxLocation & 0x1) | ((sBoxLocation & 0x20) >> 4)
        //剩餘第2、3、4、5位組成列號
        column := (sBoxLocation >> 1) & 0xf
        //feistelBox包括了S盒置換和P盒置換的實現
        sBoxResult ^= feistelBox[i][16*row+column]
    }
    return sBoxResult
}

var feistelBox [8][64]uint32

//P盒置換
func permuteBlock(src uint64, permutation []uint8) (block uint64) {
    for position, n := range permutation {
        bit := (src >> n) & 1
        block |= bit << uint((len(permutation)-1)-position)
    }
    return
}

//初始化feistelBox
func init() {
    for s := range sBoxes {
        for i := 0; i < 4; i++ {
            for j := 0; j < 16; j++ {
                f := uint64(sBoxes[s][i][j]) << (4 * (7 - uint(s)))
                f = permuteBlock(f, permutationFunction[:])
                feistelBox[s][16*i+j] = uint32(f)
            }
        }
    }
}
//程式碼位置src/crypto/des/block.go
```

附go標準庫中使用的擴充套件置換表和P盒置換表:

```go
//擴充套件置換表
var expansionFunction = [48]byte{
    0, 31, 30, 29, 28, 27, 28, 27,
    26, 25, 24, 23, 24, 23, 22, 21,
    20, 19, 20, 19, 18, 17, 16, 15,
    16, 15, 14, 13, 12, 11, 12, 11,
    10, 9, 8, 7, 8, 7, 6, 5,
    4, 3, 4, 3, 2, 1, 0, 31,
}

//P盒置換表
var permutationFunction = [32]byte{
    16, 25, 12, 11, 3, 20, 4, 15,
    31, 17, 9, 6, 27, 14, 1, 22,
    30, 24, 8, 18, 0, 5, 29, 23,
    13, 19, 2, 26, 10, 21, 28, 7,
}
//程式碼位置src/crypto/des/const.go
```

附go標準庫中使用的S盒置換表:

```go
var sBoxes = [8][4][16]uint8{
    // S-box 1
    {
        {14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7},
        {0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8},
        {4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0},
        {15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13},
    },
    // S-box 2
    {
        {15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10},
        {3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5},
        {0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15},
        {13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9},
    },
    // S-box 3
    {
        {10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8},
        {13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1},
        {13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7},
        {1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12},
    },
    // S-box 4
    {
        {7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15},
        {13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9},
        {10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4},
        {3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14},
    },
    // S-box 5
    {
        {2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9},
        {14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6},
        {4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14},
        {11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3},
    },
    // S-box 6
    {
        {12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11},
        {10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8},
        {9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6},
        {4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13},
    },
    // S-box 7
    {
        {4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1},
        {13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6},
        {1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2},
        {6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12},
    },
    // S-box 8
    {
        {13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7},
        {1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2},
        {7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8},
        {2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11},
    },
}
//程式碼位置src/crypto/des/const.go
```

## 3DES

DES是一個經典的對稱加密演算法,但也缺陷明顯,即56位的金鑰安全性不足,已被證實可以在短時間內破解。
為解決此問題,出現了3DES,也稱Triple DES,3DES為DES向AES過渡的加密演算法,它使用3條56位的金鑰對資料進行三次加密。
為了相容普通的DES,3DES並沒有直接使用加密->加密->加密的方式,而是採用了加密->解密->加密的方式。
當三重金鑰均相同時,前兩步相互抵消,相當於僅實現了一次加密,因此可實現對普通DES加密演算法的相容。
3DES解密過程,與加密過程相反,即逆序使用金鑰。

如下為三重DES示意圖:



如下為3DES相容DES示意圖:



go標準中3DES加密演算法的實現如下:

```go
type tripleDESCipher struct {
    cipher1, cipher2, cipher3 desCipher
}

func NewTripleDESCipher(key []byte) (cipher.Block, error) {
    if len(key) != 24 {
        return nil, KeySizeError(len(key))
    }

    c := new(tripleDESCipher)
    c.cipher1.generateSubkeys(key[:8])
    c.cipher2.generateSubkeys(key[8:16])
    c.cipher3.generateSubkeys(key[16:])
    return c, nil
}

//3DES加密
func (c *tripleDESCipher) Encrypt(dst, src []byte) {
    c.cipher1.Encrypt(dst, src)
    c.cipher2.Decrypt(dst, dst)
    c.cipher3.Encrypt(dst, dst)
}

//3DES解密
func (c *tripleDESCipher) Decrypt(dst, src []byte) {
    c.cipher3.Decrypt(dst, src)
    c.cipher2.Encrypt(dst, dst)
    c.cipher1.Decrypt(dst, dst)
}
//程式碼位置src/crypto/des/cipher.go
```

## 後記

相比DES,3DES因金鑰長度變長,安全性有所提高,但其處理速度不高。
因此又出現了AES加密演算法,AES較於3DES速度更快、安全性更高,後續單獨總結。




網址:http://www.qukuailianxueyuan.io/



欲領取造幣技術與全套虛擬機器資料

區塊鏈技術交流QQ群:756146052  備註:CSDN

尹成學院微信:備註:CSDN

相關文章