Java實現AES加密演算法
最近惡補了一些關於加密演算法的知識,然後用程式語言的來實現
AES簡介
高階加密標準(AES,Advanced Encryption Standard)為最常見的 對稱加密演算法 (微信小程式加密傳輸就是用這個加密演算法的)。 對稱加密演算法也就是加密和解密用相同的金鑰 ,具體的加密流程如下圖:

下面簡單介紹下各個部分的作用與意義:
- 明文P
沒有經過加密的資料
- 金鑰K
用來加密明文的密碼, 在對稱加密演算法中,加密與解密的金鑰是相同的 ,金鑰為接收方與傳送方協商產生,但不可以直接在網路上傳輸,否則會導致金鑰洩露,通常是通過非對稱加密演算法加密金鑰,然後再通過網路傳輸給對方,或者直接面對面商量金鑰。金鑰絕對不能洩露,否則會被攻擊者還原金鑰,竊取資料
- AES加密函式
設AES加密函式為E,則 C = E(K,P)
.其中P為明文,K為金鑰,C為密文。也就是說,把明文P和金鑰K作為加密函式的引數輸入,則加密函式E會輸出密文C
- 密文C
經過加密函式處理後的資料
- AES解密函式
設AES解密函式為D,則 P = D()
.其中C為密文,K為金鑰,P為明文。也就是說,把密文C和金鑰K作為解密函式的引數輸入,則解密函式會輸出明文P
這裡簡單解釋下 對稱加密演算法 和 非對稱加密演算法
- 對稱加密演算法
加密和解密使用的金鑰是相同的,這種加密方式加密 速度非常快 ,適合經常傳送資料的場合。 缺點是金鑰的傳輸比較麻煩
- 非對稱加密演算法
加密和解密使用的金鑰是不同的,這種加密方式是用數學定理或者公式構造的,通常加密解密的 速度比較慢 ,適合偶爾傳送資料的場合。 優點是金鑰傳輸方便 。常見的非對稱加密演算法為RSA、ECC和EIGamal
實際中,一般是通過RSA加密AES的金鑰,傳輸到接收方,接收方解密得到AES金鑰
AES的基本結構
AES為分組密碼,分組密碼也就是把明文分成一組一組的,每組長度相等,每次加密一組資料,直到加密完所有組。在AES標準規範中,分組長度只能是128位,也就是說,每個分組為16個位元組。金鑰的長度可以使用128位、192位或256位。金鑰的長度不同,推薦加密輪數也不同。如下表所示:
AES | 金鑰長度(32bit) | 分組長度(32bit) | 加密輪數 |
---|---|---|---|
AES-128 | 4 | 4 | 10 |
AES-192 | 6 | 4 | 12 |
AES-256 | 8 | 4 | 14 |
上面說到,AES的加密公式為 C = E(K,P)
,在加密函式E種,會執行一個輪函式,並且執行n(n為加密輪數)次這個輪函式,這個輪函式的前n-1次執行的操作是一樣的,只有第n次有所不同
AES的處理單位是位元組,128位的輸入明文分組P和輸入金鑰K都被分成16位元組,分別記為$P = P_0,P_1,...,P_{15}$和$K = K_0,K_1,...,K_{15}$。如明文分組為P=abcdefghijklmnop,其中字元a對應$P_0$,p對應$P_{15}$。一般地,明文分組用位元組為單位地正方形矩陣描述,稱為狀態矩陣。在演算法地每一輪中,狀態矩陣地內容不斷髮生變化,最後的結果作為密文輸出。該矩陣中位元組地排列順序為從上到下、從左至右依次排列,如下圖所示:

現在假設明文分組P=abcdefghijklmnop,則對應上面生成地狀態矩陣圖如下:

上圖中,0x61為a字元的十六進位制表示,可以看到,明文經過AES加密後,已經面目全非了
類似地,128位金鑰也是用以位元組為單位的矩陣表示,矩陣的每一列被稱為1個32為位元字。通過金鑰編排函式可以將該金鑰矩陣擴充套件成一個44字組成的序列W[0],W[1],...,W[43]。該序列的前4個元素W[0],W[1],W[2],W[3]是原始金鑰,用於加密運算中的初始金鑰。後面40個字分為10組,每組4個字(128bit)分別用於10輪加密運算中的輪金鑰加,如下圖所示:

上圖中,設K=abcdefghijklmnop,則$K_0=a,K_{15}=p$,w[0]$=K_0k_1K_2K_3=abcd$
AES整體的結構如下圖所示,其中W[0,3]是指W[0]、W[1]、W[2]和W[3]串聯組成的128位金鑰。加密的第一輪到第9輪的輪函式一樣,包括4個操作,位元組代換、行位移、列混合和輪金鑰加。最後一輪迭代不執行列混合。另外,在第一輪迭代之前,先將明文和原始金鑰進行一次異或加密操作

上圖也展示了AES的解密過程,解密過程仍為10輪,每一輪的操作是加密操作的逆操作。由於AES的4個輪操作都是可逆的,因此,解密操作的每一輪就是順序執行逆行移位、逆位元組代換、輪金鑰加和逆列混合。同加密操作類似,最後一輪不執行逆列混合,在第1輪解密之前,要執行1次金鑰加操作
下面分別介紹AES中一輪的4個操作階段,這4分操作階段使輸入位得到充分的混淆
位元組代換
1.位元組代換操作
AES的位元組代換其實就是一個簡單的查表操作。AES定義了一個S盒和一個逆S盒
AES的S盒:

把該位元組的高4位作為行值,低4位作為列值,取出S盒或者逆S盒中對應的行的元素作為輸出。例如,加密時,輸出的位元組S1為0x12,則查S盒的第0x01行和0x02列,得到值0xc9,然後替換S1原有的0x12為0xc9。狀態矩陣經位元組代換後的圖如下:

2.位元組代換逆操作
逆位元組代換也就是查逆S盒來變換,逆S盒如下:

行位移
1.行位移變換
行移位是一個簡單的左迴圈移位操作。當金鑰長度為128位元時,狀態矩陣的第0行左移0位元組,第1行左移1位元組,第2行左移2位元組,第3行左移3位元組,如下圖所示:

2.行位移逆變換
行移位的逆變換是將狀態矩陣中的每一行執行相反的移位操作,例如AES-128中,狀態矩陣的第0行右移0位元組,第1行右移1位元組,第2行右移2位元組,第3行右移3位元組
列混合
1.列混合預算
列混合變換是通過矩陣相乘來實現的,經行移位後的狀態矩陣與固定的矩陣相乘,得到混淆後的狀態矩陣,如下圖的公式所示:

狀態矩陣中的第j列(0 ≤j≤3)的列混合可以表示為:
$$
S'_{0,j} = (2 S_{0,j}) \oplus (3 S_{1,j}) \oplus S_{2,j} \oplus S_{3,j}
S'_{1,j} = S_{0,j} \oplus (2 S_{1,j}) \oplus (3 S_{2,j}) \oplus S_{3,j}
S'_{2,j} = S_{0,j} \oplus S_{1,j} \oplus (2 S_{2,j}) \oplus (3 S_{3,j})
S'_{3,j} = (3 S_{0,j}) \oplus S_{1,j} \oplus S_{2,j} \oplus (2 S_{3,j})
$$
2.列混合逆運算
逆向列混合變換可由下圖的矩陣乘法定義:

可以驗證,逆變換矩陣同正變換矩陣的乘積恰好為單位矩陣
輪金鑰加
輪金鑰加是將128位輪金鑰$K_i$同狀態矩陣中的資料進行逐位異或操作,如下圖所示。其中,金鑰$K_i$中每個字W[4i],W[4i+1],W[4i+2],W[4i+3]為32位位元字,包含4個位元組,他們的生成演算法下面在下面介紹。輪金鑰加過程可以看成是字逐位異或的結果,也可以看成位元組級別或者位級別的操作。也就是說,可以看成S0 S1 S2 S3 組成的32位字與W[4i]的異或運算

輪金鑰加的逆運算同正向的輪金鑰加運算完全一致,這是因為異或的逆操作是其自身。輪金鑰加非常簡單,但卻能夠影響S陣列中的每一位
金鑰擴充套件
這個4*4矩陣的每一列的4個位元組組成一個字,矩陣4列的4個字依次命名為W[0]、W[1]、W[2]和W[3],它們構成一個以字為單位的陣列W。例如,設金鑰K=abcdefghijklmnop,則$K_0 = a,K_1 = b,K_2 = c,K_3 = d$,W[0] = abcd
接著,對W陣列擴充40個新列,構成總共44列的擴充套件金鑰陣列。新列以如下的遞迴方式產生:
W[i]=W[i-4]⨁W[i-1] W[i]=W[i-4]⨁T(W[i-1])
其中,函式T由3部分組成:字迴圈、位元組代換和輪常量異或,這3部分的作用分別如下:
- 字迴圈:將1個字中的4個位元組迴圈左移1個位元組。即將輸入字[b0, b1, b2, b3]變換成[b1,b2,b3,b0]
- 位元組代換:對字迴圈的結果使用S盒進行位元組代換
- 輪常量異或:將前兩步的結果同輪常量Rcon[j]進行異或,其中j表示輪數
輪常量Rcon[j]是一個字,其值見下表:
j | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
Rcon[j] | 01 00 00 00 | 02 00 00 00 | 04 00 00 00 | 08 00 00 00 | 10 00 00 00 |
j | 6 | 7 | 8 | 9 | 10 |
Rcon[j] | 20 00 00 00 | 40 00 00 00 | 80 00 00 00 | 1B 00 00 00 | 36 00 00 00 |
舉個例子,設初始的128為金鑰為: 3C A1 0B 21 57 F0 19 16 90 2E 13 80 AC C1 07 BD
,那麼4個初始值為
W[0] = 3C A1 0B 21 W[1] = 57 F0 19 16 W[2] = 90 2E 13 80 W[3] = AC C1 07 BD
下面求擴充套件的第1輪的子金鑰(W[4],W[5],W[6],W[7])
由於4是4的倍數,所以: W[4] = W[0] ⨁ T(W[3])
,T(W[3])的計算步驟如下:
- 迴圈地將W[3]的元素移位:
AC C1 07 BD
變成C1 07 BD AC
- 將
C1 07 BD AC
作為S盒的輸入,輸出為78 C5 7A 91
- 將
78 C5 7A 91
與第一輪輪常量Rcon[1]進行異或運算,將得到79 C5 7A 91
,因此,T(W[3]) = 79 C5 7A 91
,故W[4] = 3C A1 0B 21 ⨁ 79 C5 7A 91 = 45 64 71 B0
其餘3個子金鑰段的計算如下:
W[5] = W[1] ⨁ W[4] = 57 F0 19 16 ⨁ 45 64 71 B0 = 12 94 68 A6 W[6] = W[2] ⨁ W[5] =90 2E 13 80 ⨁ 12 94 68 A6 = 82 BA 7B 26 W[7] = W[3] ⨁ W[6] = AC C1 07 BD ⨁ 82 BA 7B 26 = 2E 7B 7C 9B
所以,第一輪的金鑰為 45 64 71 B0 12 94 68 A6 82 BA 7B 26 2E 7B 7C 9B
Java實現AES演算法
由於Java有自帶的函式,因此可以直接呼叫
首先生成金鑰,金鑰是 SecretKey
型別的物件
static final String ALGORITHM = "AES"; public static SecretKey generateKey() throws NoSuchAlgorithmException { // 生成金鑰 KeyGenerator secretGenerator = KeyGenerator.getInstance(ALGORITHM); SecureRandom secureRandom = new SecureRandom(); secretGenerator.init(secureRandom); SecretKey secretKey = secretGenerator.generateKey(); return secretKey; }
首先建立金鑰生成器 KeyGenerator
物件,產生的是AES演算法的金鑰,因此傳入引數AES。這裡使用 安全的隨機數 SecureRandom
作為引數傳入金鑰生成器的 init()
函式中,最後呼叫金鑰生成器的 generateKey()
方法產生金鑰物件 secretKey
然後實現加密方法和解密方法
static Charset charset = Charset.forName("UTF-8"); public static byte[] encrypt(String content, SecretKey secretKey) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { // 加密 return aes(content.getBytes(charset),Cipher.ENCRYPT_MODE,secretKey); } public static String decrypt(byte[] contentArray, SecretKey secretKey) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { // 解密 byte[] result =aes(contentArray,Cipher.DECRYPT_MODE,secretKey); return new String(result,charset); }
加密方法傳入的是需要加密的明文 content
以及金鑰;解密方法傳入的是密文 contentArray
以及金鑰
最後實現aes函式
private static byte[] aes(byte[] contentArray, int mode, SecretKey secretKey) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(mode, secretKey); byte[] result = cipher.doFinal(contentArray); return result; }
完整程式碼如下:
import java.nio.charset.Charset; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; public class Aes { static final String ALGORITHM = "AES"; public static SecretKey generateKey() throws NoSuchAlgorithmException { // 生成金鑰 KeyGenerator secretGenerator = KeyGenerator.getInstance(ALGORITHM); SecureRandom secureRandom = new SecureRandom(); secretGenerator.init(secureRandom); SecretKey secretKey = secretGenerator.generateKey(); return secretKey; } static Charset charset = Charset.forName("UTF-8"); public static byte[] encrypt(String content, SecretKey secretKey) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { // 加密 return aes(content.getBytes(charset),Cipher.ENCRYPT_MODE,secretKey); } public static String decrypt(byte[] contentArray, SecretKey secretKey) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { // 解密 byte[] result =aes(contentArray,Cipher.DECRYPT_MODE,secretKey); return new String(result,charset); } private static byte[] aes(byte[] contentArray, int mode, SecretKey secretKey) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(mode, secretKey); byte[] result = cipher.doFinal(contentArray); return result; } public static void main(String[] args) { String content = "你好,我很喜歡加密演算法"; SecretKey secretKey; try { long timeStart = System.currentTimeMillis(); secretKey = generateKey(); byte[] encryptResult = encrypt(content, secretKey); long timeEnd = System.currentTimeMillis(); System.out.println("加密後的結果為:" + new String(encryptResult,charset)); String decryptResult = decrypt(encryptResult,secretKey); System.out.println("解密後的結果為:" + decryptResult); System.out.println("加密用時:" + (timeEnd - timeStart)); } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException e) { e.printStackTrace(); } } }
執行GIF圖如下所示:
