程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> 用新的高級加密標准(AES)保持你的數據安全

用新的高級加密標准(AES)保持你的數據安全

編輯:關於C#

本文假設你熟悉 C#和位(bit)操作。

摘要

AES(The Advanced Encryption Standard)是美國國家標准與技術研究所用於加密電子數據的規范。它被預期能成為人們公認的加密包括金融、電信和政府數字信息的方法。本文展示了AES的概貌並解析了它使用的算法。包括一個完整的C#實現和加密.NET數據的舉例。在讀完本文後你將能用AES加密、測試 基於AES的軟件並能在你的系統中使用AES加密。

美國國家標准與技術研究所(NIST)在2002年5月26日建立了新的高級數據加密標准(AES)規范。本文中我將提供一個用C#編寫的的能運行的 AES 實現,並詳細解釋到底什麼是 AES 以及編碼是如何工作的。我將向您展示如何用 AES 加密數據並擴展本文給出的代碼來開發一個商業級質量的 AES 類。我 還將解釋怎樣把 AES 結合到你的軟件系統中去和為什麼要這麼做,以及如何測試基於AES的軟件。

注意本文提供的代碼和基於本文的任何其它的實現都在聯邦加密模塊出口控制的適用范圍之內(詳情請參看 Commercial Encryption Export Controls )。

AES 是一個新的可以用於保護電子數據的加密算法。明確地說,AES 是一個迭代的、對稱密鑰分組的密碼,它可以使用128、192和256 位密鑰,並且用 128 位(16字節)分組加密和解密數據。與公共密鑰密碼使用密鑰對不同,對稱密鑰密碼使用相同的密鑰加密和解密數據。通過分組密碼返回的加密數據的位數與輸入數據相同。迭代加密使用一個循環結構,在該循環中重復置換(permutations )和替換(substitutions)輸入數據。Figure 1 顯示了 AES 用192位密鑰對一個16位字節數據塊進行加密和解密的情形。

Figure 1 部分數據

AES算法概述

AES 算法是基於置換和代替的。置換是數據的重新排列,而代替是用一個單元數據替換另一個。AES 使用了幾種不同的技術來實現置換和替換。為了闡明這些技術,讓我們用 Figure 1 所示的數據討論一個具體的 AES 加密例子。下面是你要加密的128位值以及它們對應的索引數組:

00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

192位密鑰的值是:

00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17
0 1 2 3 4 5 6 7 8 9 10 1112 13 14 15 16 17 18 19 20 21 22 23

Figure 2 S-盒( Sbox )

當 AES的構造函數(constructor)被調用時,用於加密方法的兩個表被初始化。第一個表是代替盒稱為S-盒。它是一個16×16的矩陣。S-盒的前五行和前五列如 Figure 2 所示。在幕後,加密例程獲取該密鑰數組並用它來生成一個名為w[]的密鑰調度表,Figure 3 所示。

Figure 3 密鑰調度表(Key Sched)

w[] 最初的 Nk (6) 行被作為種子,用原始密鑰值(0x00 到0x17)。剩余行從種子密鑰來產生。變量 Nk 代表以 32 位字為單位的種子密鑰長度。稍後我分析 AES 實現時你將清楚地看到 w[] 是怎樣產生的。 關鍵是這裡現在有許多密鑰使用而不只是一個。這些新的密鑰被稱為輪密鑰(round keys)以將它們與原始種子密鑰區別開來。

Figure 4 State (態)數組

AES 加密例程開始是拷貝 16 字節的輸入數組到一個名為State (態)的 4×4 字節矩陣中。(參見 Figure 4)。AES 加密算法 取名為Cipher,它操作 State[],其過程描述的偽代碼參見 Figure 5 。

在規范中,加密算法實現的一個預備的處理步驟被稱為AddRoundKey(輪密鑰加)。AddRoundKey 用密鑰調度表中的前四行對 State 矩陣實行一個字節一個字節的異或(XOR)操作,並用輪密鑰表 w[c,r] 異或 輸入 State[r,c]。

舉個例子,如果 State 矩陣的第一行保存的字節是{ 00, 44, 88, cc },第一列密鑰調度表是{ 00, 04, 08, 0c },那麼新的 State[0,2] 值是用 w[2,0]( 0x08 或 0x80 )異或 State[0,2](0x88)的結果:

1 0 0 0 1 0 0 0
0 0 0 0 1 0 0 0 XOR
1 0 0 0 0 0 0 0

AES 算法的主循環對 State 矩陣執行四個不同的操作,在規范中被稱為SubBytes(字節替換)、ShiftRows(行位移變換)、MixColumns(列混合變換)和AddRoundKey。除了每次循環 AddRoundKey 都被調用並使用密鑰調度表的下面四行外,AddRoundKey 與預備處理步驟中的 AddRoundKey 相同。SubBytes 例程是一個代替操作,它將 State 矩陣中的每個字節替換成一個由 Sbox 決定的新字節。比如,如果 State[0,1]的值是 0x40 如果你想找到它的代替者,你取 State[0,1]的值 (0x40) 並讓 x 等於左邊的數字(4)並讓 y 等於右邊的數字(0)。然後你用 x和y 作為索引 進到 Sbox 表中尋找代替值,如 Figure 2 所示。

ShiftRows 是一個置換操作,它將 State 矩陣中的字節向左旋轉。Figure 6 示范了 ShiftRows 如何操作 State[]。State的第0行被向左旋轉0個位置,State的第1行被向左旋轉1個位置,State的第2行被向左旋轉2個位置,而 State的第3行被向左旋轉3個 位置。

Figure 6 對 State 進行 ShiftRows 操作

MixColumns 是一個代替操作,它是理解 AES 算法時最具技巧(或者說是最需要動腦筋的部分)的部分。它用 State 字節列的值進行數學域加和域乘的結果代替每個字節。我將在下一節中 詳細解釋專門的域加和域乘細節。

假設 State[0,1]的值是0x09,並且列1上的其它值分別為0x60,0xe1和0x04,那麼State[0,1]的新值計算如下:

State[0,1] = (State[0,1] * 0x01) + (State[1,1] * 0x02) +(State[2,1] * 0x03) +(State[3,1] * 0x01)
= (0x09 * 0x01) + (0x60 * 0x02) + (0xe1 * 0x03) +(0x04 * 0x01)
= 0x57

此處加法和乘法是專門的數學域操作,而不是平常整數的加法和乘法。

SubBytes、ShiftRows、MixColumns和AddRoundKey 四個操作在一個執行 Nr 次的循環裡被調用,Nr 為給定密鑰大小的輪數減 1。加密算法使用的輪數要麼是10,12,要麼是14,這依賴於種子密鑰長度是128位、192 位還是 256 位。在這個例子中,因為Nr 等於12, 則這四個操作被調用11次。該迭代完成後,在拷貝 State 矩陣到輸出參數前,加密算法調用 SubBytes、ShiftRows和AddRoundKey 後結束。

大致說來,AES 加密算法的核心有四個操作。AddRoundKey 使用從種子密鑰值中生成的輪密鑰代替 4 組字節。SubBytes 替換用一個代替表 替換單個字節。ShiftRows 通過旋轉 4字節行的 4 組字節進行序列置換。MixColumns 用域加和域乘的組合來替換字節。

有限域GF(28)的加法和乘法

正如你所看到的,AES 加密算法使用相當簡單明了的技術來代替和置換,除 MixColumns 例程以外。MixColumns 使用特殊的加法和乘法。AES 所用的加法和乘法是基於數學(譯者注:近世代數)的域論。尤其是 AES 基於有限域GF(28)。

GF(28)由一組從 0x00 到 0xff的256個值組成,加上加法和乘法,因此是(28)。GF代表伽羅瓦域,以發明這一理論的數學家的名字命名。GF(28)的一個特性是一個加法或乘法的操作的結果必須是在{0x00 ... 0xff}這組數中。雖然域論是相當深奧的,但GF(28)加法的最終結果卻很簡單。GF(28) 加法就是異或(XOR)操作。

然而,GF(28)的乘法有點繁難。正如你稍後將在C# 實現中所看到的,AES的加密和解密例程需要知道怎樣只用七個常量 0x01、0x02、0x03、0x09、0x0b、0x0d和0x0e 來相乘。所以我不全面介紹GF(28)的乘法,而只是針對這七種特殊情況進行說明。

在GF(28)中用0x01的乘法是特殊的;它相當於普通算術中用1做乘法並且結果也同樣—任何值乘0x01等於其自身。

現在讓我們看看用0x02做乘法。和加法的情況相同,理論是深奧的,但最終結果十分簡單。只要被乘的值小於0x80,這時乘法的結果就是該值左移1比特位。如果被乘的值大於或等於0x80,這時乘法的結果就是左移1比特位再用值0x1b異或。它防止了“域溢出”並保持乘法的乘積在范圍以內。

一旦你在GF(28)中用0x02建立了加法和乘法,你就可以用任何常量去定義乘法。用0x03做乘法時,你可以將 0x03 分解為2的冪之和。為了用 0x03 乘以任意字節b, 因為0x03 = 0x02 + 0x01,因此:

b * 0x03 = b * (0x02 + 0x01)
     = (b * 0x02) + (b * 0x01)

這是可以行得通的,因為你知道如何用 0x02和0x01 相乘和相加,同哩,用0x0d去乘以任意字節b可以這樣做:

b * 0x0d = b * (0x08 + 0x04 + 0x01)
     = (b * 0x08) + (b * 0x04) + (b * 0x01)
     = (b * 0x02 * 0x02 * 0x02) + (b * 0x02 * 0x02) + (b * 0x01)

在加解密算法中,AES MixColumns 例程的其它乘法遵循大體相同的模式,如下所示:

b * 0x09 = b * (0x08 + 0x01)
     = (b * 0x02 * 0x02 * 0x02) + (b * 0x01)
b * 0x0b = b * (0x08 + 0x02 + 0x01)
     = (b * 0x02 * 0x02 * 0x02) + (b * 0x02) + (b * 0x01)
b * 0x0e = b * (0x08 + 0x04 + 0x02)
     = (b * 0x02 * 0x02 * 0x02) + (b * 0x02 * 0x02) + (b * 0x02)

總之,在GF(28)中,加法是異或操作。其乘法將分解成加法和用0x02做的乘法,而用0x02做的乘法是一個有條件的左移1比特位。AES規范中包括大量 有關GF(28)操作的附加信息。

密鑰擴展

AES加密和解密算法使用了一個由種子密鑰字節數組生成的密鑰調度表。AES規范中稱之為密鑰擴展例程(KeyExpansion)。從本質上講,從一個原始密鑰中生成多重密鑰以代替使用單個密鑰大大增加了比特位的擴散。雖然不是無法抵御的困難,但理解 KeyExpansion 仍是 AES 算法中的一個難點。KeyExpansion 例程高級偽代碼如下所示:

KeyExpansion(byte[] key, byte[][4] w)
{
 copy the seed key into the first rows of w
 for each remaining row of w
 {
  use two of the previous rows to create a new row
 }
}

“用前面兩行來產生一個新行”(“use two of the previous rows to create a new row”)的例程用到了兩個子 例程,RotWord和SubWord 以及一個名為“Rcon”的常數表(作為“輪常數”)。讓我們先來逐個看一下這三東西,然後再回到整個 KeyExpansion的討論中來。

RotWord 例程很簡單。它接受一個4個字節的數組並將它們向左旋轉一個位置。因為輪調度表 w[] 有四列,RotWord 將 w[]的1行左旋。注意 KeyExpansion 使用的這個 RotWord 函數與加密算法使用的 ShiftRows (行位移變換)例程非常相似,只是它 處理的是單行密鑰調度 w[],而不是整個加密狀態表 State[]。

SubWord 例程使用替換表 Sbox 對一給定的一行密鑰調度表 w[] 進行逐字節替換。KeyExpansion 操作中的替換實際上就像在加密算法中的 替換一樣。被代替的輸入字節被分成 (x,y) 對,它被當作進入替換表 Sbox的索引。舉例來說,0x27的代替結果是 x=2和y=7,並且 Sbox[2,7] 返回 0xcc。

KeyExpansion 例程使用一個被稱為輪常數表的數組 Rcon[]。這些常數都是4個字節,每一個與密鑰調度表的某一行相匹配。AES的 KeyExpansion 例程需要11個輪常數。你可以在Figure 7中看到這些常數清單。

每個輪常數的最左邊的字節是GF(28)域中2的冪次方。它的另一個表示方法是其每個值是前一個值乘上0x02,正如前一部分討論 GF(28) 乘法 時所描述的那樣。注意 0x80 × 0x02 = 0x1b 是 0x80 左移1個比特位後緊接著與0x1b 進行異或,如前所述。

現在讓我們更進一步看看 KeyExpansion 內幕中的循環。這裡所用的偽碼比以前更為詳細,這個循環是:

for (row = Nk; row < (4 * Nr+1); ++row)
{
 temp = w[row-1]
 if (row % Nk == 0)
  temp = SubWord(RotWord(temp)) xor Rcon[row/Nk]
 else if (Nk == 8 and row % Nk == 4)
  temp = SubWord(temp)
 w[row] = w[row-Nk] xor temp
}

先不要去看if子句,你將看到密鑰調度表 w[]的每一行都是前面一行與行 Nk 異或的結果(4, 6, 或 8 取決於密鑰的長度)。if條件的第一部分用 SubWord、RotWord 以及與輪常數的異或修改密鑰調度表的每個第4、第6或第8行,取決於是否密鑰的長度是128、192或256位。這個條件的第二部分將修改行 12、20和28 等等——對於256位密鑰而言——每 一個第8行都將添加密鑰調度額外的可變性。

讓我們用本文開頭所舉的例子來考察 KeyExpansion 是如何開始的。種子密鑰是192-bit / 6-word 值:

00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17

密鑰調度字節表 w[]的維數是 4 列並且 Nb × (Nr + 1) 等於4 × (12 + 1),或 52 行。KeyExpansion 將種子密鑰的值拷貝到密鑰調度字節表 w[]的第一行。因為我的種子密鑰是 192 位(24字節),並且 w[] 表總是 4 列,在這種情況下KeyExapansion 將種子密鑰拷貝到 w[]的前面 6 行。現在讓我們看看 KeyExapansion 例程是如何填充密鑰調度表其余部分的。在我的例子裡,第一個被計算的行是第 6 行 ,因為第0-5行已被種子密鑰的值填上了:

temp = w[row-1] = 14 15 16 17

條件 (row % Nk == 0)為真,因此首先 RotWord 子程序被應用:

temp = 15 16 17 14

這時 SubWord 被應用:

temp = 59 47 f0 fa

用 Rcon[row / Nk] = Rcon[6 / 6] = 01 00 00 00 進行異或:

temp = 58 47 f0 fa

這時用 w[row-Nk] = w[6-6] = 00 01 02 03 異或,產生了下面結果:

w[6] = 58 46 f2 f9

密鑰調度表 w[]中其余所有行來重復這個過程本身。

總而言之,AES 加密和解密的一個重要部分就是從最初的種子密鑰中生成多重輪密鑰。這個 KeyExapansion 算法生成一個密鑰調度並 以某種方式進行替代和置換,在這種方式中,加密和解密算法極其相似。

用 C# 編寫 AES 類構造函數

現在我已研究了構成 AES 加密算法的各個成分,我將用 C# 來實現它。官方的 AES 算法規范包含在聯邦信息處理標准出版物197 (Federal Information Processing Standards Publication 197)中。我決定盡可能貼切地以它作為我的實現的基礎,但是我很快發現這個規范更是一個理論文獻而非一個實現的向導。為了將這個官方規范作為資源來使用,我使用的變量名與標准出版物中所用的相同。(即便它們是那麼晦澀,如“Nr”和“W”)。

我的設計使用9個數據成員和一個枚舉類型,如下所示:

public enum KeySize { Bits128, Bits192, Bits256 };
private int Nb;
private int Nk;
private int Nr;
private byte[] key;
private byte[,] Sbox;
private byte[,] iSbox;
private byte[,] w;
private byte[,] Rcon;
private byte[,] State;

因為密鑰長度只能是128位、192位或256位比特,它是非常適於用枚舉類型:

public enum KeySize { Bits128, Bits192, Bits256 };

該規范文檔一般用字節作為基本儲存單元而不是用4字節的字作為兩個重要數據成員的長度。這兩個成員 Nb和Nk 代表 以字為單位的塊長以及以字為單位的密鑰長度。Nr代表輪數。塊長度總是16字節(或這說是 128 位,即為AES的 4個字),因此它可以被聲明為一個常量。密鑰長度 依照枚舉參數 KeySize的值被賦值為4、6 或 8。AES 算法強調通過大量輪數來增加加密數據的復雜性。輪數是10、12或14中的任意一個並且是基於密碼分析學理論的。它直接取決於密鑰長度。

當設計一個類接口時,我喜歡向後來做。我設想從應用程序中調用構造函數和方法。使用這個辦法,我決定象下面這樣來實例化一個 AES 對象:

Aes a = new Aes(the key size, the seed key)

我調用的加密和解密例程如下:

a.Cipher(plainText, cipherText);
a.InvCipher(cipherText, decipheredText);

我選擇少許笨拙的方法來命名 Cipher和InvCipher,因為它們是用在AES 規范文檔中的。這裡是 AES 類構造函數的代碼為:

public Aes(KeySize keySize, byte[] keyBytes)
{
 SetNbNkNr(keySize);
 this.key = new byte[this.Nk * 4]; 
 keyBytes.CopyTo(this.key, 0);
 BuildSbox();
 BuildInvSbox();
 BuildRcon();
 KeyExpansion(); 
}

該構造函數首先調用一個輔助方法 SetNbNkNr 給 Nb、Nk和Nr 賦值,如 Figure 8 所示。如果考慮到效率,你可能將這些代碼直接放入構造函數 以避免方法調用的開銷。

接下來,你必須將傳入構造函數的字節拷貝到類域變量中。密鑰用其它的類域聲明,並且用如下方法獲得它的值:

this.key = new byte[this.Nk * 4];
keyBytes.CopyTo(this.key, 0);

我決定在構造函數中調用私有輔助方法 BuildSbox和BuildInvSbox 來初始化替換表 Sbox[]和iSbox[] 。現在密鑰擴展例程 、Cipher 方法和 InvCipher 方法各自都需要 Sbox[]和iSbox[],因此我本來可以在Cipher和InvCipher 兩個方法中初始化 Sbox[] 並調用 KeyExpansion 方法,但是將它們放入構造函數會代碼結構更加清晰。在Figure 9中 sBox[] 被填充。填充 iSbox[] 代碼 類似。為了可讀性對代碼進行了結構化處理。正如後面你將看到的,還有另外一個可供選擇的令人驚訝的方法為Sbox和iSbox 表提供值。

在構造函數中聲明密鑰調度表 w[]、輪常數表 Rcon[] 和狀態矩陣 State[],並用私有輔助方法來給 Rcon[]和w[] 賦值在我看來似乎是組織它們的最好辦法,但那主要還是個風格問題。置換輪常數表 Rcon的賦值代碼參見 Figure 7。

回想一下,GF(28)中,Rcon[] 每一行左邊的字節都 2的冪,因此這個表可用下面的方法建立:

newVal = prevVal * 0x02;

AES 構造函數在建立完密鑰調度表 w[] 後結束,而 w[] 是在KeyExpansion 方法中完成的(參見 Figure 10)。 其代碼相當簡單。規范文檔使用一個假設的 4-字節的字數據類型。因為C# 沒有那樣的類型,但可以用一個4個字節的數組來模擬。在用 new 操作符為密鑰調度 表 w[] 分配空間後,w[] 最初的 Nk(4, 6, 或 8) 行從被傳遞到構造函數的種子密鑰 key[] 數組中獲值。

this.w[row,0] = this.key[4*row];
this.w[row,1] = this.key[4*row+1];
this.w[row,2] = this.key[4*row+2];
this.w[row,3] = this.key[4*row+3];

兩個字節相互的異或操作在這個代碼中頻頻發生。它需要一些從 byte 到 int的強制類型轉換並轉回到 byte,因為異或操作“^”是不能定義在C#的 byte 類型上,例如:

temp[0] = (byte)( (int)temp[0] ^ (int)this.Rcon[row/Nk,0] );

用來替代:

temp[0] = temp[0] ^ this.Rcon[row/Nk,0];

KeyExpansion 方法有條件地調用私有方法 SubWord和RotWord 以保持同規范命名的一致性。此外,因為在C#中沒有 word類型,我用 4字節數組實現了一個字。SubWord和RotWord的代碼是相當簡單,參見本文附帶的 AesLib 源代碼,它應該很容易理解。

稍微具備有些技巧的部分是在SubWord中查找替代值。回想一下,為了尋找代替值,你將輸入字節分成最左邊的4位比特和最右邊的4位比特。對於一個給定字節,用 >> 操作符右移 4 位將得到 x 索引,並且與0000 1111 進行邏輯與得到 y 值。雖然有些長,但比實際代碼更可讀,我可以象下面這樣:

int x = word[0] >> 4;
int y = word[0] & 0x0f;
byte substitute = this.Sbox[x,y];
result[0] = substitute;

代替我原來用的代碼:

result[0] = this.Sbox[ word[0] >> 4, word[0] & 0x0f ];

總的來說,AES 構造函數接受一個密鑰的長度為128,192 或 256 位和一個字節數組種子密鑰值。構造函數為輸入塊長度,種子密鑰長度 以及加密算法的輪數賦值,並將種子密鑰拷貝到一個名為key的數據成員中。構造函數還創建了四個表:兩個由加密和解密方法使用的替換表,一個輪常數表,和一個輪密鑰的密鑰調度表。

用C#編寫的 AES Cipher 方法

Cipher方法如 Figure 11 所示。它真的非常簡單,因為它分出了大部分的工作給私有方法AddRoundKey, SubBytes, ShiftRows和MixColumns。

Cipher 方法以拷貝明文輸入數組到狀態矩陣 State[] 為開始。最初調用 AddRoundKey 之後,Cipher 方法比總輪數少迭代一次。在最後一輪時,正如規范中所說的那樣,MixColumns 調用被省略了。

AddRoundKey和SubBytes 私有方法的代碼如 Figure 12 所示。AddRoundKey 方法需要知道它處在那一輪,以便它正確引用4行密鑰調度數組 w[]。請注意 State[r,c] 是用 w[c,r] 來異或並不是w[r,c]。SubBytes 方法從輸入字節中提取索引,與KeyExpansion 方法中所用的右移4位和 0x0f 屏蔽技術相同。

ShiftRows 方法的代碼如 Figure 13 所示。回想一下,ShiftRows(可能叫做 RotateRows 更好)將 row[0] 向左旋轉 0 個位置,將 row[1] 向左旋轉 1 位置等等。

把 State[] 拷貝到 temp[] 矩陣之後,然後用下面的這行代碼實現轉換:

this.State[r, (c + r) % Nb ] = temp[r,c];

這裡利用%操作符的優點抱合一行。

MixColumns 方法(Figure 14)用GF(28)加和乘,以字節列中所有其它值的線性組合對每一個字節進行替換。

乘法所用的常量系數基於域論的,並且是0x01, 0x02或 0x03中的任意一個值。給定某一列 c ,其替代式如下:

State[0,c] = 0x02 * State[0,c] +
  0x03 * State[1,c] +
  0x01 * State[2,c] +
  0x01 * State[3,c]

State[1,c] = 0x01 * State[0,c] +
  0x02 * State[1,c] +
  0x03 * State[2,c] +
  0x01 * State[3,c]

State[2,c] = 0x01 * State[0,c] +
  0x01 * State[1,c] +
  0x02 * State[2,c] + 
  0x03 * State[3,c]

State[3,c] = 0x03 * State[0,c] +
  0x01 * State[1,c] +
  0x01 * State[2,c] + 
  0x02 * State[3,c]

這些表達式稍微有些長,因此我決定編寫返回 GF(28)與0x01,0x02和0x03 之乘積的私有輔助函數。這些輔助函數非常短。例如,一個字節 b 被 0x03 域乘的代碼如下:

return (byte) ( (int)gfmultby02(b) ^ (int)b );

正如我前面討論的,被 0x02 乘是所有 GF(28) 乘法的基本操作。我調用了我的 gfmultby02 方法,我改變了使用與規范相同的方法命名慣例,規范上稱此例程為xtime。

Cipher 方法其輸入反復應用四個操作來產生加密的輸出。AddRoundKey 用源於單個原始種子密鑰的多重輪密鑰來替代字節。SubBytes 用某個替換表中的值替代字節。ShiftRows 用移動字節行置換字節,而 MixColumns 用某一列的域加和乘法值來替代字節。

用C#編寫 AES InvCipher 方法

AES 解密算法背後的基本原則很簡單:解密一個加密塊,也就是以反向順序還原(Undo)每個操作。盡管這是基本概念,但仍有幾個細節要處理。

AES規范稱解密例程為InvCipher,而不是 Decipher 或 Decrypt中的一個。這是 AES 背後的數學基礎的反映,它基於可逆的數學操作。

如果你將這個代碼和 Cipher 代碼比較的話,你會看到它比你預期的漂亮很多,但是有兩點例外。首先,在InvCipher 方法中逆方法調用(如 InvSubBytes)順序並不完全與在Cipher 方法中相應調用(如 SubBytes)的逆向順序正好相同。其次,InvCipher 調用的是一個 AddRoundKey 方法而不是 InvAddRoundKey 方法。值得注意的是 InvCipher 算法用密鑰調度表並不是從較高編號的索引處開始向下處理至第0行。

InvSubBytes,InvShiftRows和InvMixColumns 方法的代碼和與之有關的 SubBytes,ShiftRows和 MixColumns 方法的代碼非常接近。InvSubBytes 方法幾乎就是 SubBytes 方法,只是它用逆替換表 iSbox[] 而不是 Sbox[] 表。

正如你可能猜測到的,iSbox[] 就是還原任何被 Sbox[] 處理的對應操作。比如,如果你有字節 b 等於0x20,並在Sbox[]中找到其代替值,你得到 0xb7。如果你在iSbox[]中找到 0xb7的替代值,你便可得到 0x20。

相似地,InvShiftRows 方法還原 ShiftRows 方法—— row[0] 被右移了 0 個位置,row[1] 被右移了 1個位置,row[2] 被右移了 2 個位置,而 row[3] 被右移了 3個位置。

InvMixColumns 方法還原 MixColumns的工作,但沒有用顯而易見的方法。回想一下,MixColumns 用原始字節列中的字節線性組合替換狀態矩陣中的每個字節,並且系數是 0x01,0x02,和 0x03,域論再一次得到應用。它證明逆運算是相似的,只是被 0x09,0x0b,0x0d和0x0e 乘,如下所示:


State[0,c] = 0x0e * State[0,c] +
    0x0b * State[1,c] +
    0x0d * State[2,c] +
       0x09 * State[3,c]
State[1,c] = 0x09 * State[0,c] +
       0x0e * State[1,c] +
       0x0b * State[2,c] +
       0x0d * State[3,c]
State[2,c] = 0x0d * State[0,c] +
       0x09 * State[1,c] +
       0x0e * State[2,c] +
       0x0b * State[3,c]
State[3,c] = 0x0b * State[0,c] +
       0x0d * State[1,c] +
       0x09 * State[2,c] +
       0x0e * State[3,c]

對於MixColumns 方法,我決定專門寫一個輔助函數,而不是內聯展開已經較長的表達式或寫一個普通的乘法輔助函數。讓我向你展示一下我示如何編寫這個任何字節 b 被常數 0x0e (在10進制中的14)乘的函數,像任何數字一樣,數字 14 可以被表示成 2的冪的和,因此,14 等於2 + 4 + 8。並且 4 等於2的平方,8 等於2的立方,你可以將14表示為2 + 22 + 23。記住加法就是 GF(28)中上的異或(^),既然我已經有了 gfmultby02 函數,我可以用它得到我的結果:

return (byte)( (int)gfmultby02(gfmultby02(gfmultby02(b))) ^ /* 23 + */
(int)gfmultby02(gfmultby02(b)) ^ /* 22 + */
(int)gfmultby02(b) ); /* 2 */

用於AES 加密算法的所有的操作都是可逆的,因此解密算法本質上是加密的所有操作的倒轉。

使用 AES 類

用C#實現 AES的特色之一就簡單。看看 Figure 15,它是我用來生成輸出 Figure 1的代碼。聲明了 16 字節 明文輸入硬代碼值和 24 字節(192位)的種子密鑰後,一個 AES 對象被初始化,加密 Cipher 方法 將明文加密成為密文,然後再用 InvCipher 將密文解密。非常清楚和簡單。

因為AES 對象針對字節數組進行處理,你可以輕松地用它處理.NET的其它數據類型。我創建了一個基於Windows的小Demo程序,它接受一個 單純的字符串——有 8 個字符 (16-byte) ,對它進行加密和解密處理。運行畫面如 Figure 16。

Figure 16 加密 Demo 程序

因為加密和解密例程都需要知道用戶定義的密鑰長度,我把它當作一個類范圍的變量來聲明,像這樣:

private Aes.KeySize keysize;

注意種子密鑰並不是由用戶定義的。這個 demo 程序用一個“空密鑰”(null key)作為種子密鑰,通過為構造函數提供一個啞參數 new byte[16] 使得它全部由零字節組成。啞參數的長度是不相關的,因為種子密鑰還是要被初始化為零。空密鑰加密和解密是一個容易和有效的辦法來阻止外界對數據偶然的檢查。在System.Text中的 Encoding.Unicode.GetBytes和Encoding.Unicode.GetString 方法使得將一個.NET 字符串轉換成一個字節數組變得非常容易,反之亦然。

實現選擇

現在讓我們看看本文 AES 實現中出現的一些重要的變量,本文提供的代碼可能出現的擴展,以及針對 AES的密碼分析學攻擊。

和我曾經處理的任何代碼一樣,AES 算法也可以用其它可選的途徑來實現。為什麼這很重要呢?AES 被試圖廣泛應用於各種系統,從只有很少內存容量的智能卡(smart cards)到大型的多處理器主機系統。在許多情況下,性能是關鍵因素,並且有時內存或處理器資源是有限的。事實上,AES的每個例程都能針對非常昂貴的內存資源進行性能優化,反之亦然。比如,為替換表Sbox[] 分配 256 個值看起來好像很簡單直白。但是,這些值是基於GF(28) 理論的,它們都可以用編程方式來生成。逆向替換表和輪常數表也是如此。

可選實現另外一個有趣的可能性是 Cipher和InvCipher 方法所用的 GF(28) 乘法。我的實現代碼是一個被 0x02 乘的基本函數,而後是六個調用 gfmultby02的附加函數。另一個可能性應該是寫一個一般的乘法函數,並用它代替我目前實現的七個單獨函數。另一個極端是你可以用被 0x01, 0x02, 0x03, 0x09, 0x0b, 0x0d和0x0e 乘好的所有 256 個可能的字節值構成的一個完整乘積表。此外,實現 GF(28) 乘法另一途徑是通過在兩個 256 個字節的數組裡查找,通常稱為alog[]和log[],因為它們在GF(28)中基於某些類似對數的方法。

雖然這裡給出的 AES 類完全能用於加密任何形式的.NET數據,你可能考慮想用各種方法擴展它。首先,因為本文的重點在於清楚地解釋 AES,所有 錯誤檢查被剝離掉,以我的經驗,為某個象 AES 這樣的類添加合理數量的錯誤檢查將會產生三倍的代碼量膨脹。因為AES 使用了這麼多的數組,需要做很多索引 邊界檢查。例如,所給出的構造函數甚至都不檢查種子密鑰參數的長度。

你可能還考慮通過添加更多的特性來擴展 AES 類。最明顯的一個地方是添加加密和解密.NET基本數據類型的方法,比如:System.String和System.Int32。更加雄心勃勃的擴展可能會是實現一個 流數據加密類。

AES的安全性怎樣呢?這是一個很難回答的問題,但是一般多數人的意見是:它是目前可獲得的最安全的加密算法。AES 已被列為比任何現今其它加密算法更 安全的一種算法。在理論和實踐基礎上,AES 被認為是“安全的”,因為要破解它的話,唯一有效的方法是強行(brute-force)生成所有可能的密鑰。 如果密鑰長度為256 位,還沒有已知的攻擊可以在一個可接受的時間內破解 AES(即便在當今最快的系統上,它也要花費數年時間)。

注意針對 AES 密碼最可能成功的攻擊來自一個允許時間選擇攻擊的弱實現。攻擊者用不同的密鑰並精確地測量出加密例程所需的時間。如果加密例程被粗心編碼 ,因此執行時間便依賴於密鑰值,它就有可能推導出有關密鑰的信息。在AES中,這種事情最可能發生在MixColumns 例程中,因為有域乘。 針對這種攻擊的兩個安全措施是加入虛指令,以便所以所有乘法都需要相同數量的指令,或者將域乘實現為一個查詢表,就象我前面描述的那樣。

AES 有許多種可能的實現,尤其是是使用查詢表而不是計算。本文提供的 AES 基本類可以被用於加解密任何形式的.NET數據或 被擴展成一個具有更多功能的類。

結束語

新的 AES 將無疑成為加密所有形式電子信息的事實上的標准,取代 DES。AES 加密的數據在某種意義上是牢不可破的,因為沒有已知的密碼分析攻擊可以解密 AES 密文,除非強行遍歷搜索所有可能的 256 位密鑰。

我發現在Microsoft .NET Framework 上實現 AES 類的主要的障礙是官方文檔是以一個數學家的觀點,而不是以一個軟件開發者的觀點來寫的。尤其是該規范假定讀者十分熟悉 GF(28) 域,並省略了幾個正確實現 AES 所必需的關於GF(28) 乘法的關鍵事實。我在本文中試圖努力去掉 AES的神秘面紗,特別是圍繞在GF(28) 域乘法部分的。

以 .NET Framework 庫的形式從 Microsoft 以及第三方供應商處獲得對 AES的廣泛支持只是一個時間問題。然而,處於種種理由,讓本文代碼作為你的技能儲備仍然是有價值的。這個實現尤其簡單,並且是低資源開銷。另外,閱讀並理解源代碼將使你能定制 AES 類且更有效地使用它的任何實現。

在任何軟件設計過程中安全已不再是後顧之憂。AES 是一個重大進步,使用並理解它將大大增加軟件系統的可靠性和安全性。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved