1. 程式人生 > >SHA1雜湊演算法及其C++實現

SHA1雜湊演算法及其C++實現

這裡重點說一下SHA1演算法的實現步驟與程式碼,由於本人水平有限,不過多討論SHA1的數學原理以及應用場景。

SHA1簡介

In cryptography, SHA-1 (Secure Hash Algorithm 1) is a cryptographic hash function which takes an input and produces a 160-bit (20-byte) hash value known as a message digest - typically rendered as a hexadecimal number, 40 digits long. It was designed by the United States National Security Agency, and is a U.S. Federal Information Processing Standard.

對於長度小於2642^{64}位的訊息,SHA1會產生一個160位的訊息摘要。當接收到訊息的時候,這個訊息摘要可以用來驗證資料的完整性。在傳輸的過程中,資料很可能會發生變化,那麼這時候就會產生不同的訊息摘要.

以上是維基百科和百度百科對SHA1的解釋,安全雜湊演算法,也就是說任意一段位元組,用SHA1演算法跑一下,都可以生成不一樣的20個位元組序列。但是會有11048\cfrac{1}{10^{48}}的機率產生相同的位元組序列(訊息摘要),這個一般可以忽略。

SHA1應用

  • 檔案指紋 :這個作用和MD5的作用類似,如果檔案被篡改,那麼對應的SHA1值就會變化
  • Git中標識物件 :用過Git的應該都知道,Git中的物件沒有名字,唯一標識就是物件的SHA1值

SHA1實現步驟

實現步驟就是下面四個,一一來說下。

  • 分組
  • 補位
  • 轉大端模式
  • 雜湊
  • 輸出

分組

SHA1對一段位元組序列進行雜湊,雜湊是分組進行的,512bits為一組,這是官方說法,實際上,計算機中最小的處理單元是按位元組來處理的,即使你的訊息只需要一個位元位就能表示,在計算機中也是儲存為一個位元組。

因此,這裡我們以位元組為單位,64個位元組一組,我們做的雜湊操作就是以分組為單位進行的,把所有分組的雜湊結果累積起來就是SHA1的結果。

在這裡插入圖片描述

補位

64個位元組一組,肯定有些位元組序列的數量不是64的倍數,那麼剩下的這些位元組單獨一組,餘下的空位補一個位元位1,設下的位元位全部填0而且,最後要留八個位元組表示位元組序列總的位元位數

。補位這個操作是一定進行的,如果補位後發現沒有八個位元組的位置來表示位元組序列的位元位數,那麼就要新加一個分組,繼續補位。看下圖就明白了。

在這裡插入圖片描述

注意,在程式設計中,不會出現位元位的操作,都是以位元組(補位以位元組為單位來操作)或者四個位元組(uint32_t)(雜湊是以uint32_t為單位)來操作的,因此補位第一個補一個0x80位元組,後面的都補0x00。只有sum of bits是用來表示總共有多少位元位的。

轉大端模式

補位完之後,每一個分組有64個位元組,但是雜湊是以uint32_t為單位,因此又要4個位元組為一組組成uint32_t,那麼總共就有16個uint32_t,這些uint32_t要是大端模式。

雜湊

散列出來的結果為20個位元組,也就是5個uint32_t,每一個分組的雜湊值用uint32_t h[]表示。初始時會賦值一些常量,這個程式碼中會有看到。分組的序列用uint32_t data[16]表示。

由於這裡不牽扯到數學原理的說明,所以就直接說步驟了。

準備五個變數uint32_t A, B, C, D, E、一個數組uint32_t W[80]

  • A, B, C, D, E分別等於h[0 ~ 4]
  • W[0 ~ 15] 分別等於data[0 ~ 15]W[16 ~ 79]用一個公式Expand來計算;
  • W[0 ~ 79]的值分別利用公式Round雜湊A, B, C, D, E,總共執行80次;
  • 最後h[0 ~ 4]分別等於A, B, C, D, E

至於這裡面用的公式,以及為什麼要這麼做,我不知道,這肯定有一些數學的原因,對於編碼實現SHA1來說不需要了解了。

輸出

最後得到h[0 ~ 4]就是最終的結果,我們把uint32_t又轉化成位元組陣列unsigned char digest[20],最後輸出digest的16進製表示,也就是說最後會得到40個字元的字串,你可以把這個字串看成一個超大的整數。

C++編碼實現SHA1

編碼過程中還是有一些小技巧。

在這裡插入圖片描述
這是我在編碼過程中的TaskList順序圖,也是按照上面實現步驟一步一步實現的。

SHA1Digest

先來看看這個類的定義:

class SHA1Digest
{
public:
    SHA1Digest();
    SHA1Digest(const void *input, size_t length);
    void Update(const void *input, size_t length);
    const unsigned char *DigestText()
    std::string GetString(bool isUpcase = true)
    size_t GetSize()
    void Reset()

private:
    SHA1Digest(const SHA1Digest &) = delete;
    SHA1Digest & operator = (const SHA1Digest &) = delete;

    void Transform()
    void Final()
    void InitH()

private:
    uint32_t _h[SHA1_RESULT_UINT32];
    uint32_t _data[SHA1_BLOB_UINT32];
    uint32_t _countLow;
    uint32_t _countHight;
    uint32_t _byteCount;
    unsigned char _digest[SHA1_RESULT_UINT8];

private:
    static uint32_t H_INIT[SHA1_RESULT_UINT32];
    static uint32_t K[SHA1_ROUND_CNT];
    static std::function<uint32_t(uint32_t, uint32_t, uint32_t)> F[SHA1_ROUND_CNT];
    static uint32_t S(uint32_t x, int n);
    static uint32_t Expand(uint32_t W[], int i);
    static void Round(uint32_t alpha[], uint32_t W[], int i);

public:
    static std::string BytesToHex(const unsigned char *input, size_t length, bool isUpcase);
    static void BytesReverse(uint32_t data[], size_t length);
};

介面的設計參考了Python的hashlib還有poco庫。內部有一緩衝區,作用可以參見Base64編解碼及其C++實現

巨集定義

#define SHA1_RESULT_BIT 160
#define SHA1_RESULT_UINT8 (SHA1_RESULT_BIT / 8)         // 20
#define SHA1_RESULT_UINT32 (SHA1_RESULT_UINT8 / 4)      // 5

#define SHA1_BLOB_BIT 512
#define SHA1_BLOB_UINT8 (SHA1_BLOB_BIT / 8)             // 64
#define SHA1_BLOB_UINT32 (SHA1_BLOB_UINT8 / 4)          // 16

#define SHA1_REST_BIT 448
#define SHA1_REST_UINT8 (SHA1_REST_BIT / 8)             // 56
#define SHA1_REST_UINT32 (SHA1_REST_UINT8 / 4)          // 14

#define SHA1_OPERATION_CNT 80
#define SHA1_ROUND_CNT 4
#define SHA1_ROUND_LEN (SHA1_OPERATION_CNT / SHA1_ROUND_CNT) // 20

這裡要好好體會下每個巨集,因為這些巨集體現的是操作的基本單位。

公共的靜態函式

std::string SHA1Digest::BytesToHex(const unsigned char *input, size_t length, bool isUpcase)
{
    static const char *digitUpper = "0123456789ABCDEF";
    static const char *digitLower = "0123456789abcdef";

    const char *pstr = isUpcase ? digitUpper : digitLower;

    std::string ret;
    ret.reserve(length << 1);
    for (unsigned int i = 0; i < length; ++i)
    {
        ret.push_back(pstr[(input[i] & 0xF0) >> 4]);
        ret.push_back(pstr[input[i] & 0x0F]);
    }
    return ret;
}

void SHA1Digest::BytesReverse(uint32_t data[], size_t length)
{
    static unsigned int bitsOfByte= 8;

    for (unsigned int i = 0; i < length; ++i)
    {
        data[i] = ((data[i] >> (0 * bitsOfByte) & 0xFF) << (3 * bitsOfByte)) |
                    ((data[i] >> (1 * bitsOfByte) & 0xFF) << (2 * bitsOfByte)) |
                    ((data[i] >> (2 * bitsOfByte) & 0xFF) << (1 * bitsOfByte)) |
                    ((data[i] >> (3 * bitsOfByte) & 0xFF) << (0 * bitsOfByte));
    }
}

這兩個函式分別用來把位元組轉成16進位制字串與大端模式的轉化。

私有的靜態成員

uint32_t SHA1Digest::H_INIT[SHA1_RESULT_UINT32] =
{
    0x67452301,
    0xEFCDAB89,
    0x98BADCFE,
    0x10325476,
    0xC3D2E1F0
};

uint32_t SHA1Digest::K[SHA1_ROUND_CNT] =
{
    0x5A827999,
    0x6ED9EBA1,
    0x8F1BBCDC,
    0xCA62C1D6
};

std::function<uint32_t(uint32_t, uint32_t, uint32_t)> SHA1Digest::F[SHA1_ROUND_CNT] =
{
    [] (uint32_t x, uint32_t y, uint32_t z) -> uint32_t
    {
        return (x & y) | (~x & z);
    },
    [] (uint32_t x, uint32_t y, uint32_t z) -> uint32_t
    {
        return x ^ y ^ z;
    },
    [] (uint32_t x, uint32_t y, uint32_t z) -> uint32_t
    {
        return (x & y) | (x & z) | (y & z);
    },
    [] (uint32_t x, uint32_t y, uint32_t z) -> uint32_t
    {
        return x ^ y ^ z;
    },
};

uint32_t SHA1Digest::S(uint32_t x, int n)
{
    return (x << n) | (x >> (32 - n));
}

uint32_t SHA1Digest::Expand(uint32_t W[], int i)
{
    return S((W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]), 1);
}

void SHA1Digest::Round(uint32_t alpha[], uint32_t W[], int i)
{
    uint32_t & A = alpha[0];
    uint32_t & B = alpha[1];
    uint32_t & C = alpha[2];
    uint32_t & D = alpha[3];
    uint32_t & E = alpha[4];

    uint32_t tmp = S(A, 5) + F[i / SHA1_ROUND_LEN](B, C, D) + E + W[i] + K[i / SHA1_ROUND_LEN];
    E = D;
    D = C;
    C = S(B, 30);
    B = A;
    A = tmp;
}

這是演算法的常量和公式,如果以後改成SHA2演算法時,只需要把這些常量和公式換掉。

Update成員函式

這是雜湊的開始,分組也在這裡進行;

可以把一段位元組序列分開多次呼叫Update函式,效果和對這一段字元序列單獨呼叫Update結果是一樣的。

void Update(const void *input, size_t length)
{
    const unsigned char *buff = reinterpret_cast<const unsigned char *>(input);

    if (_countLow + (length << 3) < _countLow)
    {
        ++_countHight;
    }
    _countLow += length << 3;
    _countHight += length >> 29;

    while (length--)
    {
        reinterpret_cast<unsigned char *>(&_data[0])[_byteCount++] = *buff++;
        if (_byteCount == SHA1_BLOB_UINT8)
        {
            // TODO : 1. group 512-bits/64-bytes
            BytesReverse(_data, SHA1_BLOB_UINT32);
            Transform();
            _byteCount = 0;
        }
    }
}

_countLow_countHight一起來表示一個uint64_t的資料。這裡的處理單位是bit,而length是位元組長度,轉化成bit就是lenth << 3

處理進位if (_countLow + (length << 3) < _countLow) { ++_countHight; },這個判斷在數學上等於(length << 3) < 0,但是在計算機中,這樣寫可以判斷是否溢位,由於處理的都是無符號的int,溢位了就相當於對2322^{32}取模,如果一個數加上正數x反而這個數還小了,如果沒溢位,這是不可能的;那麼有沒有可能計算的結果溢位了,而_countLow + (length << 3) >= _countLow,這是不可能的,除非(length&lt;&lt;3)&gt;=232(length &lt;&lt; 3) &gt;= 2 ^ {32},而uint32_t的最大值為23212^{32} - 1

_countHight += length >> 29;這裡移位29是因為,length << 3來表示總共有多少位,然後(length << 3) >> 32表示進位是多少,對於無符號正數來說,先左移3位再右移32位相當於直接右移29位。

Final成員函式

對應著補位操作。

void Final()
{
    static unsigned int bitsOfByte = 8;

    // TODO : 7. cover bit (1)
    reinterpret_cast<unsigned char *>(&_data[0])[_byteCount++] = 0x80;

    // TODO : 7. cover bit (000...000)
    std::memset(reinterpret_cast<unsigned char *>(&_data[0]) + _byteCount, 0, SHA1_BLOB_UINT8 - _byteCount);
    if (_byteCount > SHA1_REST_UINT8)
    {
        BytesReverse(_data, SHA1_BLOB_UINT32);
        Transform();
        std::memset(_data, 0, sizeof(_data));
    }
    
    BytesReverse(_data, SHA1_BLOB_UINT32);

    // TODO : 8. add bits count
    _data[14] = _countHight;
    _data[15] = _countLow;

    Transform();

    // TODO : 9. get bytes array from words array
    for (int i = 0; i < SHA1_RESULT_UINT8; ++i)
    {
        _digest[i] = _h[i >> 2] >> (bitsOfByte * (3 - (i & 0x03))) & 0xFF;
    }
}

用位運算來替代取模運算。

Transform成員函式

這個函式是這個演算法的核心,基本上,雜湊演算法都是分組來搞的,它們的步驟都是一樣的,都要分組,補位,唯一不一樣的就是這個Transform

void Transform()
{
    uint32_t alpha[SHA1_RESULT_UINT32];
    uint32_t W[SHA1_OPERATION_CNT];

    // TODO : 2. fill W[] (0 ~ 15)
    for (int i = 0; i < SHA1_BLOB_UINT32; W[i] = _data[i], ++i) {}

    // TODO : 3. fill W[] (16 ~ 79)
    for (int i = SHA1_BLOB_UINT32; i < SHA1_OPERATION_CNT; W[i] = Expand(W, i), ++i) {}

    // TODO : 4. fill A, B, C, D, E
    for (int i = 0; i < SHA1_RESULT_UINT32; alpha[i] = _h[i], ++i) {}

    // TODO : 5. operator round 80
    for (int i = 0; i < SHA1_OPERATION_CNT; Round(alpha, W, i++)) {}

    // TODO : 6. update H[]
    for (int i = 0; i < SHA1_RESULT_UINT32; _h[i] += alpha[i], ++i) {}
}

完整程式碼

#include <iostream>
#include <string>
#include <cstring>
#include <functional>

#define SHA1_RESULT_BIT 160
#define SHA1_RESULT_UINT8 (SHA1_RESULT_BIT / 8)         // 20
#define SHA1_RESULT_UINT32 (SHA1_RESULT_UINT8 / 4)      // 5

#define SHA1_BLOB_BIT 512
#define SHA1_BLOB_UINT8 (SHA1_BLOB_BIT / 8)             // 64
#define SHA1_BLOB_UINT32 (SHA1_BLOB_UINT8 / 4)          // 16

#define SHA1_REST_BIT 448
#define SHA1_REST_UINT8 (SHA1_REST_BIT / 8)             // 56
#define SHA1_REST_UINT32 (SHA1_REST_UINT8 / 4)          // 14

#define SHA1_OPERATION_CNT 80
#define SHA1_ROUND_CNT 4
#define SHA1_ROUND_LEN (SHA1_OPERATION_CNT / SHA1_ROUND_CNT) // 20

class SHA1Digest
{
public:
    SHA1Digest() : _byteCount(0)
    {
        Reset();
    }
    SHA1Digest(const void *input, size_t length) : SHA1Digest() {}

    void Update(const void *input, size_t length)
    {
        const unsigned char *buff = reinterpret_cast<const unsigned char *>(input);

        if (_countLow + (length << 3) < _countLow)
        {
            ++_countHight;
        }
        _countLow += length << 3;
        _countHight += length >> 29;

        while (length--)
        {
            reinterpret_cast<unsigned char *>(&_data[0])[_byteCount++] = *buff++;
            if (_byteCount == SHA1_BLOB_UINT8)
            {
                // TODO : 1. group 512-bits/64-bytes
                BytesReverse(_data, SHA1_BLOB_UINT32);
                Transform();
                _byteCount = 0;
            }
        }
    }

    const unsigned char *DigestText()
    {
        Final()