常用算法 之三 詳解 SHA1 實現(基於算法的官方原文檔)及源碼詳細註釋

寫在前面

  在之前的工作中,用到了CRC16MD5SHA1 算法,主要用來校驗下發的文件。網上關於這些算法的文章鋪天蓋地,以下內容僅僅是自己在學習時候的一個記錄,一些套話來自於互聯網。下面先來看看 SHA1
   以下算法分析基於 RFC 3174

  1. Request For Comments (RFC),所有關於Internet 的正式標準都是以RFC(Request for Comment )文檔出版。需要注意的是,還有大量的RFC文檔都不是正式的標準,出版目的都是爲了提供信息。
  2. 由互聯網協會(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位的報文摘要。)根據算法文檔,算法的實現主要由以下幾部分來組成。

  1. 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. )
  2. 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<=32S^n(X) = (X << n) OR (X >> 32-n)X << n定義如下:拋棄最左邊的n位數字,將各個位依次向左移動n位,然後用0填補右邊的n位(最後結果還是32位)。X>>n是拋棄右邊的n位,將各個位依次向右移動n位,然後在左邊的n位填0。因此可以叫S^n(X)位循環右移位運算。
  3. RFC3174 中的第四部分,介紹了 消息填充
      前面說了,SHA-1算法是以512一包來處理消息的,但是很多情況下,消息長度並不是512的整數倍,這個時候就需要對原始消息進行填充。

    1. 補位
      原始消息必須進行補位,以使其長度在對512取模以後的餘數是448。也就是說,(補位後的消息長度)%512 = 448。即使長度已經滿足對512取模後餘數是448,補位也必須要進行。
      補位是這樣進行的:先補一個1,然後再補0 ,直到長度滿足對512取模後餘數是448。總而言之,補位是至少補一位,最多補512位。
    2. 補長度
      就是將原始數據的長度補到已經進行了補位操作的消息後面。通常用兩個字(64位)來表示原始消息的長度。如果消息長度不大於2^64,那麼第一個字就是0。
  4. 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).

  5. 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中存放的就是最終計算結果!

附件

  1. SHA1 算法 官方文檔 rfc3174
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章