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.
對於長度小於位的訊息,SHA1會產生一個160位的訊息摘要。當接收到訊息的時候,這個訊息摘要可以用來驗證資料的完整性。在傳輸的過程中,資料很可能會發生變化,那麼這時候就會產生不同的訊息摘要.
以上是維基百科和百度百科對SHA1的解釋,安全雜湊演算法,也就是說任意一段位元組,用SHA1演算法跑一下,都可以生成不一樣的20個位元組序列。但是會有的機率產生相同的位元組序列(訊息摘要),這個一般可以忽略。
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
,溢位了就相當於對取模,如果一個數加上正數x
反而這個數還小了,如果沒溢位,這是不可能的;那麼有沒有可能計算的結果溢位了,而_countLow + (length << 3) >= _countLow
,這是不可能的,除非,而uint32_t
的最大值為;
_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()