天天看點

Java 中的加密算法: AES

加密算法

加密算法通常分為對稱加密算法和非對稱加密算法:

對稱加密算法(symmetric-key cryptography):加密和解密時使用相同的密鑰。常用的對稱加密算法有 DES、AES。

非對稱加密算法(asymmetric-key cryptography):加密和解密使用不同的密鑰,例如公鑰加密的内容隻能用私鑰解密,是以又稱為公鑰加密算法(public-key cryptography)。使用最廣泛的非對稱加密算法是 RSA 算法。

兩者有不同的使用場景,而且經常會一起搭配起來使用,例如 SSL/TLS 協定就結合了對稱加密算法和非對稱加密算法。

本文主要介紹最常用的對稱加密算法:AES。

AES

AES 全稱 Advanced Encryption Standard,是一種對稱加密算法。AES 的出現主要是用來取代 DES 加密算法,因為 AES 的安全性相對更高。

AES 使用非常廣泛,可以說隻要上網,無論是使用手機 APP 還是 Web 應用,幾乎都離不開 AES 加密算法。因為目前大部分網站,包括手機 APP 後端接口,都已經使用 HTTPS 協定,而 HTTPS 在資料傳輸階段大部分都是使用 AES 對稱加密算法。

在學習 AES 之前,首先要知道以下規則:

AES 是一種區塊加密算法,加密時會将原始資料按大小拆分成一個個區塊進行加密,區塊大小固定為 128 比特(即 16 位元組)

AES 密鑰長度可以是 128、192 或 256 比特(即 16、24 或 32 位元組),密鑰長度越長,安全性越高,而性能也就越低

AES 工作模式

AES加密算法有多種工作模式(mode of operation),如:ECB、CBC、OFB、CFB、CTR、XTS、OCB、GCM。不同的模式參數和加密流程不同,但是核心仍然是 AES 算法。

本文主要介紹 ECB、CBC、GCM 三種模式。

AES 填充方式

由于 AES 是一種區塊加密算法,加密時會将原始資料按大小拆分成一個個 128 比特(即 16 位元組)區塊進行加密,如果需要加密的原始資料不是 16 位元組的整數倍時,就需要對原始資料進行填充,使其達到 16 位元組的整數倍。

常用的填充方式有 PKCS5Padding、ISO10126Padding 等,另外如果能保證待加密的原始資料大小為 16 位元組的整數倍,也可以選擇不填充,即 NoPadding。

Java 中的 AES

Java 中的 javax.crypto.Cipher 類提供加密和解密的功能。

建立一個 Cipher:

Cipher cipher = Cipher.getInstance(“AES/CBC/PKCS5Padding”);

Cipher 類 getInstance 方法需傳遞一個加密算法的名稱作為參數,用來建立對應的 Cipher,其格式為 algorithm/mode/padding,即 算法名稱/工作模式/填充方式,例如 AES/CBC/PKCS5Padding。具體有哪些可選的加密方式,可以參考文檔:

https://docs.oracle.com/javas…

ECB

ECB 全稱為電子密碼本(Electronic codebook),将待加密的資料拆分成塊,并對每個塊進行獨立加密。

ECB 加密

ECB 解密

代碼:

public static byte[] encryptECB(byte[] data, byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {

Cipher cipher = Cipher.getInstance(“AES/ECB/PKCS5Padding”);

cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, “AES”));

byte[] result = cipher.doFinal(data);

return result;

}

public static byte[] decryptECB(byte[] data, byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {

Cipher cipher = Cipher.getInstance(“AES/ECB/PKCS5Padding”);

cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, “AES”));

byte[] result = cipher.doFinal(data);

return result;

}

public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {

String data = “Hello World”; // 待加密的明文

String key = “12345678abcdefgh”; // key 長度隻能是 16、24 或 32 位元組

byte[] ciphertext = encryptECB(data.getBytes(), key.getBytes());
System.out.println("ECB 模式加密結果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));

byte[] plaintext = decryptECB(ciphertext, key.getBytes());
System.out.println("解密結果:" + new String(plaintext));
           

}

由于加密後的密文是二進制格式而非字元串,是以這裡使用了 Base64 編碼方式将其轉換成字元串友善輸出檢視。輸出:

ECB 模式加密結果(Base64):bB0gie8pCE2RBQoIAAIxeA==

解密結果:Hello World

需要注意,AES 密鑰長度隻能是 16、24 或 32 位元組,如果不符合要求則會異常:

java.security.InvalidKeyException: Invalid AES key length

ECB 模式有一個緻命的缺點,由于該模式對每個塊進行獨立加密,會導緻同樣的明文塊被加密成相同的密文塊,相對來說并不是非常安全。下圖就是一個很好的例子:

Java 中的加密算法: AES

CBC

CBC 全稱為密碼分組連結(Cipher-block chaining),它的出現解決 ECB 同樣的明文塊會被加密成相同的密文塊的問題。

CBC 引入了初始向量的概念(IV,Initialization Vector),第一個明文塊先與 IV 進行異或後再加密,後續每個明文塊先與前一個密文塊進行異或後再加密。

Java 中的加密算法: AES

代碼:

public static byte[] encryptCBC(byte[] data, byte[] key, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {

Cipher cipher = Cipher.getInstance(“AES/CBC/PKCS5Padding”);

cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, “AES”), new IvParameterSpec(iv));

byte[] result = cipher.doFinal(data);

return result;

}

public static byte[] decryptCBC(byte[] data, byte[] key, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {

Cipher cipher = Cipher.getInstance(“AES/CBC/PKCS5Padding”);

cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, “AES”), new IvParameterSpec(iv));

byte[] result = cipher.doFinal(data);

return result;

}

public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {

String data = “Hello World”; // 待加密的原文

String key = “12345678abcdefgh”; // key 長度隻能是 16、24 或 32 位元組

String iv = “iviviviviviviviv”; // CBC 模式需要用到初始向量參數

byte[] ciphertext = encryptCBC(data.getBytes(), key.getBytes(), iv.getBytes());
System.out.println("CBC 模式加密結果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));

byte[] plaintext = decryptCBC(ciphertext, key.getBytes(), iv.getBytes());
System.out.println("解密結果:" + new String(plaintext));
           

}

輸出:

CBC 模式加密結果(Base64):K7bSB51+KxfqaMjJOsPAQg==

解密結果:Hello World

由于 CBC 每個明文塊加密都依賴前一個塊的加密結果,是以其主要缺點在于加密過程是串行的,無法被并行化。

GCM

GCM 的全稱是 Galois/Counter Mode,它是一種認證加密(authenticated encryption)算法。它不但提供了加密解密,還提供了資料完整性校驗,防止篡改。

AES-GCM 模式是目前使用最廣泛的模式,可以嘗試抓包看一下目前主流的 https 網站,其中大部分都是基于 GCM 模式。下圖是使用抓包工具 Charles 檢視浏覽器通路 https 網站所使用的加密算法:

可以看到浏覽器一般支援 AES-GCM 和 AES-CBC 模式,最終伺服器選擇使用 AES-GCM。

AES-GCM 認證加密需要用到以下參數:

待加密的明文

密鑰

初始向量 IV

additional authenticated data (AAD)

代碼:

public static byte[] encryptGCM(byte[] data, byte[] key, byte[] iv, byte[] aad) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {

Cipher cipher = Cipher.getInstance(“AES/GCM/NoPadding”);

cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, “AES”), new GCMParameterSpec(128, iv));

cipher.updateAAD(aad);

byte[] result = cipher.doFinal(data);

return result;

}

public static byte[] decryptGCM(byte[] data, byte[] key, byte[] iv, byte[] aad) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {

Cipher cipher = Cipher.getInstance(“AES/GCM/NoPadding”);

cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, “AES”), new GCMParameterSpec(128, iv));

cipher.updateAAD(aad);

byte[] result = cipher.doFinal(data);

return result;

}

public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {

String data = “Hello World”; // 待加密的原文

String key = “12345678abcdefgh”; // key 長度隻能是 16、24 或 32 位元組

String iv = “iviviviviviviviv”;

String aad = “aad”; // AAD 長度無限制,可為空

byte[] ciphertext = encryptGCM(data.getBytes(), key.getBytes(), iv.getBytes(), aad.getBytes());
System.out.println("GCM 模式加密結果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));

byte[] plaintext = decryptGCM(ciphertext, key.getBytes(), iv.getBytes(), aad.getBytes());
System.out.println("解密結果:" + new String(plaintext));
           

}

輸出:

GCM 模式加密結果(Base64):1UxXmFpdUwMnpI7rh0XfmFqtdZSHTbNC/08g

解密結果:Hello World

AES-GCM 是流加密(Stream cipher)算法,是以對應的填充模式為 NoPadding,即無需填充。