寫在前面
在之前的工作中,用到了CRC16、MD5 和 SHA1 算法,主要用來校驗下發的文件。網上關於這些算法的文章鋪天蓋地,以下內容僅僅是自己在學習時候的一個記錄,一些套話來自於互聯網。下面先來看看 SHA1。
以下算法分析基於 RFC 3174。
- Request For Comments (RFC),所有關於Internet 的正式標準都是以RFC(Request for Comment )文檔出版。需要注意的是,還有大量的RFC文檔都不是正式的標準,出版目的都是爲了提供信息。
- 由互聯網協會(Internet Society,簡稱ISOC)贊助發行,會交到互聯網工程工作小組(IETF)和互聯網結構委員會(IAB)。文檔可由網站 https://www.ietf.org/ 下載。
什麼是 SHA1
全稱爲 Secure Hash Algorithm 1(安全散列算法1)。是一種密碼散列函數,美國國家安全局設計,並由美國國家標準技術研究所(NIST)發佈爲聯邦數據處理標準(FIPS)。1993年 發佈 SHA0,1995年發佈了SHA1。其設計原理相似於MIT教授 Ronald L. Rivest 所設計的密碼學散列算法 MD4 和 MD5。
SHA1 可以生成一個被稱爲消息摘要的160位(20字節)散列值,散列值通常的呈現形式爲40個十六進制數。主要適用於數字簽名標準 (Digital Signature Standard DSS)裏面定義的數字簽名算法(Digital Signature Algorithm DSA)。
SHA實際上是一系列算法的統稱,分別包括:SHA-1、SHA-224、SHA-256、SHA-384以及SHA-512。
類別 消息長度 分組長度 計算字長 計算步驟 消息摘要長度 SHA-1 小於2^64位 512 32 80 160 SHA-224 小於2^64位 512 32 64 224 SHA-256 小於2^64位 512 32 64 256 SHA-384 小於2^128位 1024 64 80 384 SHA-512 小於2^128位 1024 64 80 512
安全性
2005年二月,王小云、殷益羣及於紅波發表了對完整版SHA-1的攻擊,只需少於269的計算複雜度,就能找到一組碰撞。(利用生日攻擊法找到碰撞需要280的計算複雜度。)
2017年2月23日,CWI Amsterdam與Google宣佈了一個成功的SHA-1碰撞攻擊,發佈了兩份內容不同但SHA-1散列值相同的PDF文件作爲概念證明。
實現原理
When amessage of any length < 2^64 bits is input, the SHA-1 produces a 160-bit output called a message digest. (SHA-1算法輸入報文的最大長度不超過2^64位,產生的輸出是一個160位的報文摘要。)根據算法文檔,算法的實現主要由以下幾部分來組成。
-
RFC3174 中的第二部分,給出了一些術語,總結一下也就下面三點:
- SHA-1算法把輸入消息當做 比特位字符串 來處理
- 輸入是按 512 位(16個字)的分組進行處理的
- 字 等於32位字符串,可以表示爲8個十六進制數字的序列。(A word equals a 32-bit string which may be represented as a sequence of 8 hex digits. )
-
RFC3174 中的第三部分,給出了一些對於 字 的基本運算,總結一下:
- 按位邏輯字操作
- X AND Y = bitwise logical “and” of X and Y.
- X OR Y = bitwise logical “inclusive-or” of X and Y.
- X XOR Y = bitwise logical “exclusive-or” of X and Y.
- NOT X = bitwise logical “complement” of X.
- X+Y定義如下:字 X 和 Y 代表兩個整數 x 和y, 其中
0 <= x < 2^32 且 0 <= y < 2^32
. 令整數z = (x + y) mod 2^32
. 這時候0 <= z < 2^32
。將z轉換成字Z,那麼就是 Z = X + Y。 - 循環左移位操作符
S^n(X)
。X是一個字,n是一個整數,0<=n<=32
。S^n(X) = (X << n) OR (X >> 32-n)
。X << n
定義如下:拋棄最左邊的n位數字,將各個位依次向左移動n位,然後用0填補右邊的n位(最後結果還是32位)。X>>n
是拋棄右邊的n位,將各個位依次向右移動n位,然後在左邊的n位填0。因此可以叫S^n(X)位循環右移位運算。
- 按位邏輯字操作
-
RFC3174 中的第四部分,介紹了 消息填充 。
前面說了,SHA-1算法是以512一包來處理消息的,但是很多情況下,消息長度並不是512的整數倍,這個時候就需要對原始消息進行填充。- 補位
原始消息必須進行補位,以使其長度在對512取模以後的餘數是448。也就是說,(補位後的消息長度)%512 = 448。即使長度已經滿足對512取模後餘數是448,補位也必須要進行。
補位是這樣進行的:先補一個1,然後再補0 ,直到長度滿足對512取模後餘數是448。總而言之,補位是至少補一位,最多補512位。 - 補長度
就是將原始數據的長度補到已經進行了補位操作的消息後面。通常用兩個字(64位)來表示原始消息的長度。如果消息長度不大於2^64,那麼第一個字就是0。
- 補位
-
RFC3174 中的第五部分,給出了算法使用的各種函數和常量 。
在SHA-1中使用了一系列邏輯函數f(0),f(1),…,f(79)。 每個f(t),0 <= t <= 79,對三個32位字B,C,D進行操作,併產生一個32位字作爲輸出。 f(t; B,C,D)定義如下:-
f(t;B,C,D) = (B AND C) OR ((NOT B) AND D) ( 0 <= t <= 19)
-
f(t;B,C,D) = B XOR C XOR D (20 <= t <= 39)
-
f(t;B,C,D) = (B AND C) OR (B AND D) OR (C AND D) (40 <= t <= 59)
-
f(t;B,C,D) = B XOR C XOR D (60 <= t <= 79).
在SHA-1中使用一系列常數字K(0),K(1),…,K(79)。定義如下:
-
K(t) = 5A827999 ( 0 <= t <= 19)
-
K(t) = 6ED9EBA1 (20 <= t <= 39)
-
K(t) = 8F1BBCDC (40 <= t <= 59)
-
K(t) = CA62C1D6 (60 <= t <= 79).
-
-
RFC3174 中的第六部分,給出了算法具體實現方法 。
經過前面的準備,接下來就是計算信息摘要了。SHA1有4輪運算,每一輪包括20個步驟,一共80步,最終產生160位的信息摘要,這160位的摘要存放在5個32位的鏈接變量中。
在SHA1的4論運算中,雖然進行的就具體操作函數不同,但邏輯過程卻是一致的。首先,定義5個變量,假設爲H0、H1、H2、H3、H4,對其分別進行如下操作:
(A)將A左移5爲與 函數的結果求和,再與對應的子明文分組、E以及計算常數求和後的結果賦予H0。
(B)將A的值賦予H1。
(C)將B左移30位,並賦予H2。
(D)將C的值賦予H3。
(E)將D的值賦予H4。
(F)最後將H0、H1、H2、H3、H4的值分別賦予A、B、C、D
源碼
- SHA1.h
#ifndef SHA1_H
#define SHA1_H
#ifdef __cplusplus
extern "C" {
#endif
#define SHA1HANDSOFF
#define LITTLE_ENDIAN
typedef struct
{
unsigned long state[5]; /* 160(5×32)比特的消息摘要(即SHA-1算法要得出的) */
unsigned long count[2]; /* 儲存消息的長度(單位:比特) */
unsigned char buffer[64]; /* 512(64×8)比特(位)的消息塊(由原始消息經處理得出) */
} SHA1_CTX;
void SHA1Init(SHA1_CTX *context);
void SHA1Update(SHA1_CTX *context, unsigned char *data, unsigned int len);
void SHA1Final(unsigned char digest[20], SHA1_CTX *context);
#ifdef __cplusplus
}
#endif
#endif
- SHA1.c
#include <stdio.h>
#include <string.h>
#include "SHA1.h"
typedef union {
unsigned char c[64];
unsigned long l[16];
} CHAR64LONG16;
#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))
/* blk0() and blk() perform the initial expand. */
/* I got the idea of expanding during the round function from SSLeay */
#ifdef LITTLE_ENDIAN
#define blk0(i) (block->l[i] = (rol(block->l[i], 24) & 0xFF00FF00) | (rol(block->l[i], 8) & 0x00FF00FF))
#else
#define blk0(i) block->l[i]
#endif
#define blk(i) (block->l[i & 15] = rol(block->l[(i + 13) & 15] ^ block->l[(i + 8) & 15] ^ block->l[(i + 2) & 15] ^ block->l[i & 15], 1))
/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */
#define R0(v, w, x, y, z, i) \
z += ((w & (x ^ y)) ^ y) + blk0(i) + 0x5A827999 + rol(v, 5); \
w = rol(w, 30);
#define R1(v, w, x, y, z, i) \
z += ((w & (x ^ y)) ^ y) + blk(i) + 0x5A827999 + rol(v, 5); \
w = rol(w, 30);
#define R2(v, w, x, y, z, i) \
z += (w ^ x ^ y) + blk(i) + 0x6ED9EBA1 + rol(v, 5); \
w = rol(w, 30);
#define R3(v, w, x, y, z, i) \
z += (((w | x) & y) | (w & x)) + blk(i) + 0x8F1BBCDC + rol(v, 5); \
w = rol(w, 30);
#define R4(v, w, x, y, z, i) \
z += (w ^ x ^ y) + blk(i) + 0xCA62C1D6 + rol(v, 5); \
w = rol(w, 30);
static void SHA1Transform(unsigned long state[5], unsigned char buffer[64]);
/* Hash a single 512-bit block. This is the core of the algorithm. */
static void SHA1Transform(unsigned long state[5], unsigned char buffer[64])
{
unsigned long a, b, c, d, e;
CHAR64LONG16 *block;
#ifdef SHA1HANDSOFF
static unsigned char workspace[64];
block = (CHAR64LONG16 *)workspace;
memcpy(block, buffer, 64);
#else
block = (CHAR64LONG16 *)buffer;
#endif
/* Copy context-> state[] to working vars */
a = state[0];
b = state[1];
c = state[2];
d = state[3];
e = state[4];
/* 完成的就是RFC文檔中的H0~H4賦值給ABCDE的操作。接下來就是80輪運算的代碼。每20輪爲一組,共分四組 */
/* 第一組比較特殊,使用了R0和R1兩個宏函數,其原因前面已經介紹了。因爲第0~15輪運算和16~79輪運算的時候消息塊M(i)和字塊W(i)的轉換是不一樣的。後面的20~39輪,40~59輪,60~79輪就是依次使用的R2,R3,R4來運算了 */
/* 4 rounds of 20 operations each. Loop unrolled. */
R0(a, b, c, d, e, 0);
R0(e, a, b, c, d, 1);
R0(d, e, a, b, c, 2);
R0(c, d, e, a, b, 3);
R0(b, c, d, e, a, 4);
R0(a, b, c, d, e, 5);
R0(e, a, b, c, d, 6);
R0(d, e, a, b, c, 7);
R0(c, d, e, a, b, 8);
R0(b, c, d, e, a, 9);
R0(a, b, c, d, e, 10);
R0(e, a, b, c, d, 11);
R0(d, e, a, b, c, 12);
R0(c, d, e, a, b, 13);
R0(b, c, d, e, a, 14);
R0(a, b, c, d, e, 15);
R1(e, a, b, c, d, 16);
R1(d, e, a, b, c, 17);
R1(c, d, e, a, b, 18);
R1(b, c, d, e, a, 19);
R2(a, b, c, d, e, 20);
R2(e, a, b, c, d, 21);
R2(d, e, a, b, c, 22);
R2(c, d, e, a, b, 23);
R2(b, c, d, e, a, 24);
R2(a, b, c, d, e, 25);
R2(e, a, b, c, d, 26);
R2(d, e, a, b, c, 27);
R2(c, d, e, a, b, 28);
R2(b, c, d, e, a, 29);
R2(a, b, c, d, e, 30);
R2(e, a, b, c, d, 31);
R2(d, e, a, b, c, 32);
R2(c, d, e, a, b, 33);
R2(b, c, d, e, a, 34);
R2(a, b, c, d, e, 35);
R2(e, a, b, c, d, 36);
R2(d, e, a, b, c, 37);
R2(c, d, e, a, b, 38);
R2(b, c, d, e, a, 39);
R3(a, b, c, d, e, 40);
R3(e, a, b, c, d, 41);
R3(d, e, a, b, c, 42);
R3(c, d, e, a, b, 43);
R3(b, c, d, e, a, 44);
R3(a, b, c, d, e, 45);
R3(e, a, b, c, d, 46);
R3(d, e, a, b, c, 47);
R3(c, d, e, a, b, 48);
R3(b, c, d, e, a, 49);
R3(a, b, c, d, e, 50);
R3(e, a, b, c, d, 51);
R3(d, e, a, b, c, 52);
R3(c, d, e, a, b, 53);
R3(b, c, d, e, a, 54);
R3(a, b, c, d, e, 55);
R3(e, a, b, c, d, 56);
R3(d, e, a, b, c, 57);
R3(c, d, e, a, b, 58);
R3(b, c, d, e, a, 59);
R4(a, b, c, d, e, 60);
R4(e, a, b, c, d, 61);
R4(d, e, a, b, c, 62);
R4(c, d, e, a, b, 63);
R4(b, c, d, e, a, 64);
R4(a, b, c, d, e, 65);
R4(e, a, b, c, d, 66);
R4(d, e, a, b, c, 67);
R4(c, d, e, a, b, 68);
R4(b, c, d, e, a, 69);
R4(a, b, c, d, e, 70);
R4(e, a, b, c, d, 71);
R4(d, e, a, b, c, 72);
R4(c, d, e, a, b, 73);
R4(b, c, d, e, a, 74);
R4(a, b, c, d, e, 75);
R4(e, a, b, c, d, 76);
R4(d, e, a, b, c, 77);
R4(c, d, e, a, b, 78);
R4(b, c, d, e, a, 79);
/* 完成的就是更新緩衝區H0~H4的內容。然後把a~e清空爲0 */
/* Add the working vars back into context.state[] */
state[0] += a;
state[1] += b;
state[2] += c;
state[3] += d;
state[4] += e;
/* Wipe variables */
a = b = c = d = e = 0;
}
/* SHA1Init - Initialize new context */
void SHA1Init(SHA1_CTX *context)
{
/* SHA1 initialization constants */
context->state[0] = 0x67452301;
context->state[1] = 0xEFCDAB89;
context->state[2] = 0x98BADCFE;
context->state[3] = 0x10325476;
context->state[4] = 0xC3D2E1F0;
context->count[0] = context->count[1] = 0;
}
/* Run your data through this. */
void SHA1Update(SHA1_CTX *context, unsigned char *data, unsigned int len)
{
unsigned int i, j;
/* j>>3獲得的就是字節數,j = (j >> 3) & 63得到的就是低6位的值,也就是代表64個字節(512位)長度的消息。,因爲我們每次進行計算都是處理512位的消息數據。 */
j = (context->count[0] >> 3) & 63;
/* context->count[ ]存儲的是消息的長度,超出context->count[0]的存儲範圍的部分存儲在context->count[1]中。len<<3就是len*8的意思,因爲len的單位是字節,而context->count[ ]存儲的長度的單位是位,所以要乘以8。 if ((context->count[0] += len << 3) < j) 的意思就是說如果加上len*8個位,context->count[0]溢出了,那麼就要:context->count[1]++;進位。
len<<3的單位是位,len>>29(len<<3 >>32)表示的就是len中要存儲在context->count[1]中的部分。 */
if ((context->count[0] += len << 3) < (len << 3))
context->count[1]++;
context->count[1] += (len >> 29);
/* 如果j+len的長度大於63個字節,就分開處理,每64個字節處理一次,然後再處理後面的64個字節,重複這個過程;否則就直接將數據附加到buffer末尾 */
if ((j + len) > 63)
{
memcpy(&context->buffer[j], data, (i = 64 - j)); /* i=64-j,然後從data中複製i個字節的數據附加到context->buffer[j]末尾,也就是說給buffer湊成了64個字節 */
SHA1Transform(context->state, context->buffer); /* 執行SHA1Transform()來開始一次消息摘要的計算 */
/* 每64個字節處理一次 */
for (; i + 63 < len; i += 64)
{
SHA1Transform(context->state, &data[i]);
}
j = 0;
}
else
{
i = 0;
}
/* 如果前面的if不成立,那麼也就是說原始數據context->buffer加上新的數據data的長度還不足以湊成64個字節,所以直接附加上data就行了。相當於:memcpy(&context->buffer[j], &data[i], 0);
如果前面的if成立,那麼j是等於0的,而 i 所指向的偏移位置是 (└ len/64┘×64,len)之間。 └ ┘表示向下取整。*/
memcpy(&context->buffer[j], &data[i], len - i);
}
/* Add padding and return the message digest. */
void SHA1Final(unsigned char digest[20], SHA1_CTX *context)
{
unsigned long i, j;
unsigned char finalcount[8];
for (i = 0; i < 8; i++)
{
finalcount[i] = (unsigned char)((context->count[(i >= 4 ? 0 : 1)]
>> ((3 - (i & 3)) * 8)) &
255); /* Endian independent */
}
/* 填充的時候是以字節爲單位的,最少1個字節,最多64個字節。並且第一位要填充1,後面都填充0。所以拿到一個消息我們首先要給他填充一個字節的10 000 000.SHA1Update() 函數就是完成的數據填充(附加)操作 */
SHA1Update(context, (unsigned char *)"\200 ", 1);
/* 循環測試數據模512是否與448同餘。不滿足條件就填充全一個字節0。 */
/* 使用 while ((context->count[0] & 511) != 448) 貌似更合適。但是,504後三位全0,511後三位全1。context->count中存儲的是消息的長度,它的單位是:位。前面我們提到了我們的數據是以字節來存儲的,所以context->count[ ]中的數據肯定是8個倍數,所以後三位肯定是000。所以不管是000&000,還是000&111其結果都是0。 */
while ((context->count[0] & 504) != 448)
{
SHA1Update(context, (unsigned char *)"\0 ", 1);
}
/* 這將觸發SHA1Transform()函數的調用,該函數的功能就是進行運算,得出160位的消息摘要(message digest)並儲存在context-state[ ]中,它是整個SHA-1算法的核心 */
SHA1Update(context, finalcount, 8); /* Should cause a SHA1Transform() */
/* 最後的這步轉換將消息摘要轉換成單字節序列。用代碼來解釋就是:將context-state[5]中儲存的20個字節(5×4字節)的消息摘要取出,將其存儲在20個單字節的數組digest中。並且按大端序存儲(與之前分析context->count[ ]到finalcount[ ]轉換的思路相同) */
for (i = 0; i < 20; i++)
{
digest[i] = (unsigned char)
((context->state[i >> 2] >> ((3 - (i & 3)) * 8)) & 255);
}
/* Wipe variables */
i = j = 0;
memset(context->buffer, 0, 64);
memset(context->state, 0, 20);
memset(context->count, 0, 8);
memset(&finalcount, 0, 8);
#ifdef SHA1HANDSOFF /* make SHA1Transform overwrite it 's own static vars */
SHA1Transform(context->state, context->buffer);
#endif
}
使用方式
在實際使用時,往往需要多次調用循環計算。因爲要計算的內容可能很長,需要分包一次次計算。上面的源碼就是考慮到該種問題而實現的!使用方式如下:
SHA1_CTX ctx;
unsigned char hash[20];
unsigned char abc[] = "abc";
/* 計算前,先初始化 */
SHA1Init(&ctx);
/* 多次調用 SHA1Update 循環計算多個包數據(如果有的話) */
SHA1Update(&ctx, abc, 3);
/* 最後調用 SHA1Final 獲取最終結果 */
SHA1Final(hash, &ctx);
最終,在數組hash
中存放的就是最終計算結果!