字節碼轉文本的編碼方式
在計算機中,無論是內存、磁盤、網絡傳輸,涉及到的數據都是以二進制格式來存儲或傳輸的。
每一個二進制位(bit)只能是 0 或 1。二進制位不會單獨存在,而是以 8 個二進制位組成 1 個字節(byte)的方式存在,即 1 byte = 8 bit。
字節碼無法直接轉為可打印的文本字符,有時想通過文本方式配置、存儲、傳輸一段二進制字節碼,比如配置文件、HTML/XML、URL、e-mail 正文、HTTP Header 等僅支持文本的場景下,就需要將二進制字節碼轉為文本字符串。
二進制字節碼轉文本字符有很多種方式,最簡單的方式是直接用 0 和 1 來表示。但是這樣的話,8 個 0/1 字符才能表示 1 個字節,長度太長很不方便。
下面介紹兩種更加緊湊的方式:HEX 編碼和 Base64 編碼。
HEX 編碼
HEX 是 16 進制的編碼方式,所以又稱為 Base16。
如果把一個字節中的二進制數值轉為十六進制,使用 0-9 和 a-e(忽略大小寫)這 16 個字符,那每個字符就可以表示 4 個二進制位(因為 2 的 4 次方等於 16),那麼僅需要兩個可打印字符就可以表示一個字節。
Java 中使用 HEX 編碼(依賴 Apache Commons Codec):
String str = "相對論";
byte[] bytes = str.getBytes("UTF-8");
// Hex 編碼
String encodeString = Hex.encodeHexString(bytes);
System.out.println(encodeString); // 輸出:e79bb8e5afb9e8aeba
// Hex 解碼
byte[] decodeBytes = Hex.decodeHex(encodeString);
System.out.println(new String(decodeBytes, "UTF-8")); // 輸出:相對論
HEX 編碼使用場景非常多。下面介紹幾種常見的使用場景:
RGB 顏色碼
RGB 顏色通常用 HEX 方式表示。如橘紅色可以用 #FF4500 來表示:
.orangered { color: #FF4500; }
RGB 指紅(red)綠(green)藍(blue)三原色,這三種顏色按不同比例疊加後可以得到各式各樣的顏色。三種顏色每種強度取值範圍是 0~255,各需要 1 個字節來表示,共 3 個字節。
用 HEX 編碼的表示某種 RGB 顏色,是一個長度為 6 位的字符串(通常還會加上 # 作為前綴,此時長度是 7 位)。例如 #FF4500 表示紅綠藍三原色的強度分別為 255、69、0。
URL 編碼
由於 URL 中僅允許出現字母、數字和一些特殊符號,當 URL 中有漢字,需要經過 URL 編碼才可以。
例如百度百科"相對論"的頁面 URL 是: https://baike.baidu.com/item/...
其中 %E7%9B%B8%E5%AF%B9%E8%AE%BA 實際上是將 '相對論' 三個字用 UTF-8 編碼後得到 9 個字節,再分別對這 9 個字節使用 HEX 編碼並加上 '%' 前綴得到的結果。
IPv6 地址
由於 IPv4 的地址即將面臨不夠用的問題,取而代之的將會是 IPv6。IPv6 使用了 128 個二進制位的地址,通常會使用 HEX 編碼方式來表示,例如:
2001:0db8:0000:0000:0000:ff00:0042:8329
Base64 編碼
如果覺得 HEX 編碼不夠緊湊,那麼還有更加緊湊的編碼方式:Base64 編碼。
Base64 編碼共使用了 64 個字符來表示二進制位:26 個大寫的 A-Z、26 個小寫的 a-z、10 個數字 0-9、2 個特殊符號 + 和 /。這意味着每個字符可以表示 6 個二進制位,因為 64 等於 2 的 6 次方。
由於每個字節是 8 個二進制位,而 Base64 編碼每個字符表示 6 個二進制位,那麼可以每湊夠 3 個字節(即 24 個二進制位),可將其編碼為 4 個字符。如果被 base64 編碼的原數據字節數不是 3 的倍數,那麼會在末尾補上 1 或 2 個值為 0 的字節,湊到 3 的倍數後再進行 Base64 編碼,編碼後會在末尾添加 1 或 2 個 = 符號,表示補了多少個字節,這個在解碼時會用到。
Java 中使用 Base64 編碼:
String str = "相對論";
byte[] bytes = str.getBytes("UTF-8");
// Base64 編碼
String encodeString = Base64.getEncoder().encodeToString(bytes);
System.out.println(encodeString); // 輸出:55u45a+56K66
// Base64 解碼
byte[] decodeBytes = Base64.getDecoder().decode(encodeString);
System.out.println(new String(decodeBytes, "UTF-8")); // 輸出:相對論
Base64 編碼的使用場景也有很多。例如,由於圖片文件不是文本文件,沒辦法直接寫入到 HTML 中,而將圖片經過 Base64 編碼後的結果是一串文本,可以直接放到 HTML 中:
<img src="data:image/jpg;base64,/9j/4QMZR..." />
需要注意的是,Base64 不是加密算法,有的開發人員把 Base64 當做加密算法來用,這是極其不安全的,因為 Base64 任何人都可以解碼,不需要任何密鑰。
消息摘要算法
消息摘要算法(Message-Digest Algorithm),又稱為密碼散列函數(cryptographic hash function (CHF)),可以將任意長度的字節碼數據通過哈希算法計算出一個固定大小的結果。常用的消息摘要算法有 MD5、SHA-1、SHA-2 系列(包括 SHA-256、SHA-512 等)。
以 MD5 為例,對任意一個數據進行 MD5 運算,結果是一個 128 個二進制位(16 個字節)的哈希值。而我們日常看到的 32 位 MD5 字符串,實際上是對 128 個二進制位的哈希值進行 HEX 編碼後得到的結果。
例如,當使用 MD5 對 "相對論" 這個字符串進行運算,得到一個 32 位字符的 MD5 值,實際上是經過以下 3 個步驟(以下代碼依賴 Apache Commons Codec):
String str = "相對論";
// 1. 將字符串通過 UTF-8 編碼轉為字節數組
byte[] bytes = str.getBytes("UTF-8");
// 2. 對原始數組進行 MD5,得到一個 128 個二進制位(16 個字節)的哈希值
byte[] md5Bytes = DigestUtils.md5(bytes);
// 3. 將 128 位的哈希值 HEX 編碼,得到一個長度為 32 的字符串
String md5Hex = Hex.encodeHexString(md5Bytes);
System.out.println(md5Hex); // 輸出:fa913fb181bc1a69513e3d05a367da49
上面的代碼僅僅是為了更清晰的看到計算一個字符串 MD5 值的整個過程。實際開發中可以使用更加便捷的 API,將上面的 3 個步驟合為 1 步:
String str = "相對論";
// 使用默認的 UTF-8 編碼將字符串轉為字節數組計算 MD5 後再進行 HEX 編碼
String md5Hex = DigestUtils.md5Hex(str);
System.out.println(md5Hex); // 輸出:fa913fb181bc1a69513e3d05a367da49
除此之外,Apache Commons Codec 中的 DigestUtils 還提供了 SHA-1、SHA-256、SHA-384、SHA-512 等消息摘要算法。
消息摘要算法有以下特點:
- 相同的消息通過消息摘要算法計算得到的結果總是相同的。
- 不同的消息通過消息摘要算法計算得到的結果要儘可能保證是不同的。如果兩個不同的數據消息摘要後的結果相同,也就是發生了哈希碰撞,哈希碰撞出現的概率越大,那麼這個消息摘要算法就越不安全。
- 不可逆,無法通過哈希結果反向推算出原始數據。所以,我們一般認為消息摘要算法並不算是加密算法,因為它無法解密。另外,這裏的不可逆是指運算不可逆,但是攻擊者通常會使用窮舉法或彩虹表來找到哈希值對應的原始數據。
下面列舉一些典型的消息摘要算法的使用場景:
- 對用户的登錄密碼使用消息摘要算法得到哈希值後再存儲到數據庫,即使數據庫被黑客攻擊,拿到所有的數據,也很難獲得密碼的原始值。這相對明文存儲密碼來説更加安全。當然,直接使用哈希值存儲也是不安全的,特別是對於一些弱密碼,黑客可以通過彩虹表輕鬆的查到對應的原始值。所以通常不會直接存儲哈希值,而是經過一些處理,例如加鹽、HMAC 等方式。
- 對比兩個文件是否一致,只需要對比兩個文件的消息摘要是否一致即可,無需按字節一個個去對比。例如百度網盤曾經就是用文件的 MD5 來判斷新上傳的文件是否已存在,如果已經存在則不需要重複上傳和存儲,達到節省空間的目的。
- 用於數字簽名(Digital Signature),這個在本文後續會介紹。
在安全性要求比較高的場景下,MD5、SHA-1 目前都已經不建議使用了,現在用的比較多的是 SHA-2 系列算法。
HMAC
HMAC 全稱是散列消息認證碼(Hash-Based Message Authentication Code),它在消息摘要算法的基礎上,加上了一個密鑰(secret key)。
例如 HMAC-SHA256 就是在 SHA-256 算法基礎上加了一個密鑰。以下為代碼示例(依賴 Apache Commons Codec):
String str = "相對論";
String key = "12345678"; // 密鑰
HmacUtils hmacUtils = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, key.getBytes("UTF-8"));
String result = hmacUtils.hmacHex(str.getBytes("UTF-8"));
System.out.println(result); // 輸出:3bd7bbf58159a6d0bff846016b346a617a588fc1e9c43ebbdf38be53d3fc455a
相對於直接使用消息摘要算法,使用 HMAC 優勢在於,它可以對消息進行真實性(authenticity)和完整性(integrity)驗證。
只要密鑰沒有泄露,那麼只有持有密鑰才可以計算和驗證原始數據哈希值。攻擊者在沒有密鑰的前提下,無法發送偽造的消息,也無法篡改消息。
HMAC 可用於接口認證。例如一個暴露在網絡環境中的 HTTP 接口,如果想要對調用方進行認證,可以將密鑰發放給調用方,要求調用方調用接口時,給所有請求參數使用密鑰通過 HMAC 計算一個簽名,被調用方驗證簽名,就可以保證請求參數的真實性和完整性。
另外,HMAC 由於在計算哈希值時添加了密鑰,相對於直接使用消息摘要算法,更加不容易被窮舉法、彩虹表破解,用户密碼經過 HMAC 後保存更加安全。
JWT 中的 HMAC
HMAC 的一個典型的應用場景就是 JWT。JWT 全稱是 JSON Web Token。
傳統的認證方式一般會將認證用户信息保存在服務端,而 JWT 直接將認證用户信息發放給客户端保存。既然 JWT 保存在客户端,那麼任何人都可以偽造或篡改。如何解決這個問題,其中一種方式就是服務端會對 JWT 的 token 使用 HMAC 進行簽名,並將簽名也放在 token 末尾。下次客户端帶上 JWT 請求時,服務端再驗證簽名是否正確。只要密鑰不泄露,就可以保證 token 的真實性和完整性。
JWT token 分為三個部分:
- Header:頭部,指定簽名算法
- Payload:包含 token 主要傳輸的信息,這一部分可以包含用户信息,例如用户名等
-
Signature:簽名,計算方式如下(secret 即密鑰):
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
最終對這三個部分 Base64 編碼後組合為 JWT 的 token:
加密算法
加密算法分為對稱加密算法和非對稱加密算法:
- 對稱加密算法(symmetric-key cryptography):加密和解密時使用相同的密鑰。最常用的是 AES 算法。
- 非對稱加密算法(asymmetric-key cryptography):加密和解密使用不同的密鑰,例如公鑰加密的內容只能用私鑰解密,所以又稱為公鑰加密算法(public-key cryptography)。使用最廣泛的是 RSA 算法。
對稱加密算法
常見的對稱加密算法有 DES、3DES、AES,其中 DES 和 3DES 標準由於安全性問題,已經逐漸被 AES 取代。
AES 有多種工作模式(mode of operation)和填充方式(padding):
- 工作模式:如 ECB、CBC、OFB、CFB、CTR、XTS、OCB、GCM,不同的模式參數和加密流程不同。
- 填充方式:由於 AES 是一種區塊加密(block cipher)算法,加密時會將原始數據按大小拆分成一個個 128 比特(即 16 字節)區塊進行加密,如果需要加密的原始數據不是 16 字節的整數倍時,就需要對原始數據進行填充,使其達到 16 字節的整數倍。常用的填充方式有 PKCS5Padding、ISO10126Padding 等,另外如果能保證待加密的原始數據大小為 16 字節的整數倍,也可以選擇不填充,即 NoPadding。
在實際工作中,需要跨團隊跨語言對數據加密解密,經常出現使用一個語言加密後,另一個語言無法解密的情況。這一般都是兩邊選擇的工作模式和填充方式不一致導致的。
下面的代碼以 ECB 模式結合 PKCS5Padding 填充方式為例,對數據進行加密和解密:
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、25 或 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));
}
輸出:
ECB 模式加密結果(Base64):bB0gie8pCE2RBQoIAAIxeA==
解密結果:Hello World
上面的 ECB 模式雖然簡單易用,但是安全性不高。由於該模式對每個 block 進行獨立加密,會導致同樣的明文塊被加密成相同的密文塊。下圖就是一個很好的例子:
在 CBC 模式中,引入了初始向量(IV,Initialization Vector)的概念,用於解決 ECB 模式的問題。
下面是 CBC 模式結合 PKCS5Padding 填充方式的代碼示例,加密解密時相比 ECB 模式多了一個初始向量 iv 參數:
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、25 或 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
AES 使用非常廣泛,可以説只要上網,無論是使用手機 APP 還是 Web 應用,幾乎都離不開 AES 加密算法。目前大部分網站,包括手機 APP 後端接口,都已經使用 HTTPS 協議,而 HTTPS 在數據傳輸階段大多都是使用 AES 對稱加密算法。
但是,以 AES 為代表的的對稱加密算法面臨一個問題,就是如何安全的傳輸密鑰。網絡中發生數據交換的雙方,需要用同一個密鑰進行加密和解密,密鑰一旦暴露,傳輸的內容就不再安全。密鑰本身如果需要傳輸,如何保證安全?對於這個問題,就需要用到非對稱加密算法。
非對稱加密算法
1977 年,Rivest、Shamir、Adleman 設計了 RSA 非對稱加密算法,並以此獲得了 2002 年的圖靈獎(計算機領域的國際最高獎項,被譽為"計算機界的諾貝爾獎")。至今,RSA 算法一直是最廣為使用的非對稱加密算法。
RSA 有兩個密鑰:公鑰(public key)和私鑰(private key)。
公鑰可以完全公開,任何人都可以獲取到。私鑰是私有的,要保證不能被泄露出去。
公鑰加密的內容,只有私鑰可以解密。私鑰加密的內容,也只有公鑰可以解密。
基於以上規則,RSA 有兩種不同的用法:
- 公鑰加密,私鑰解密:服務端把公鑰公開出去,客户端拿到公鑰,把想要傳輸給服務端的數據通過公鑰加密後傳輸,那麼這個數據只有服務端能夠解密,因為只有服務端擁有私鑰,其他任何中間人即使在傳輸過程中拿到數據,既不能解密,也無法篡改。
-
私鑰簽名,公鑰驗證簽名:內容發佈者將發佈的內容用消息摘要算法(如 SHA-256)計算哈希值,再用私鑰加密哈希值,得到一個簽名,並將簽名加在發佈內容中一起發佈,其他人得到這個內容後,可以用公開的公鑰解密簽名得到哈希值,再對比這個哈希值和內容生成的哈希值是否一致,來保證這份內容沒有被篡改過。
由於只是驗證數據的真實性完整性,所以無需對整個內容進行加密,僅需對內容的哈希值加密即可驗證,所以通常會結合消息摘要算法。例如 SHA256 with RSA 簽名,就是先用 SHA-256 計算出哈希值,再用 RSA 私鑰加密。
上面説到的私鑰加密、公鑰解密只是理論上成立,實際上不會直接這樣用,而是隻用於簽名。因為一段私鑰加密的數據,解密的公鑰是公開的,意味着誰都可以解密,這樣加密就沒有任何意義了。
接下來通過 Java 代碼來體驗一下 RSA 算法。
首先,需要生成一對公鑰和私鑰。下面通過 openssl 命令來生成一對公鑰和私鑰:
# 創建一個 PKCS#8 格式 2048 位的私鑰
openssl genpkey -out private_key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
# 通過私鑰生成公鑰
openssl pkey -in private_key.pem -pubout -out public_key.pem
生成的公鑰和私鑰是 Base64 編碼的文本文件,可以直接用文本編輯器打開。拷貝到下面的代碼中,可以驗證公鑰加密、私鑰解密,以及私鑰簽名、公鑰驗證簽名:
public static void main(String[] args) throws Exception {
String publicKeyBase64 = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0XYlulDsTzDWUb6X66Ia\n" +
"giSn1dKriHvLHYth9hCcaGomdeIQahGnxzE1o76slEyS2HZ164QHqx8Za+LuT6IV\n" +
"yLhU/ZNLWAZABe/sdNEkhti6vSSOdJE43KS4UVADeSgtN+7uXDuVgm35EPWZjkfV\n" +
"5hiRX4nT5ALr1niyi1Ax4BWWyG4qX00n1HzY8MvoyiLdNob71qB+amjUNy9bDhcz\n" +
"CDWtgA/ywOYU5Ec6vMgYfbAXPKGWwo318rS3UH8QtsO8iGcQbZ76q05LNEL8G3fo\n" +
"0Kssj4fjrVGwSsyGztRRMLfGkW/hOPCDj82+D6dGQlGB3gyB7P1xVbkD67FujQA/\n" +
"jwIDAQAB";
String privateKeyBase64 = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDRdiW6UOxPMNZR\n" +
"vpfrohqCJKfV0quIe8sdi2H2EJxoaiZ14hBqEafHMTWjvqyUTJLYdnXrhAerHxlr\n" +
"4u5PohXIuFT9k0tYBkAF7+x00SSG2Lq9JI50kTjcpLhRUAN5KC037u5cO5WCbfkQ\n" +
"9ZmOR9XmGJFfidPkAuvWeLKLUDHgFZbIbipfTSfUfNjwy+jKIt02hvvWoH5qaNQ3\n" +
"L1sOFzMINa2AD/LA5hTkRzq8yBh9sBc8oZbCjfXytLdQfxC2w7yIZxBtnvqrTks0\n" +
"Qvwbd+jQqyyPh+OtUbBKzIbO1FEwt8aRb+E48IOPzb4Pp0ZCUYHeDIHs/XFVuQPr\n" +
"sW6NAD+PAgMBAAECggEABT96joJ8iTdmB0JJOCQlmeElO1w94/uGCUV2vN2JrawL\n" +
"LqTtWFr84ya+e03JsSWCAF5ncfEq6AStdGCJLAGZnh/QMVJBbwEpFXz/ZaXfzmkb\n" +
"tKV31D/XNuABpjfk/mIdT+tymWj8w/nRZbVhlYkDOPKgoc4oOuw/0G3Ru1/VABI+\n" +
"yulNx93A/JNFGk3Bkm4E7jRWyl0BkAqAX2BZkFbXG/u3Jc0eYXrG74JfMH+MEihG\n" +
"GDMSpBKNyX5zWkUT6XxpG82t2erHPWYEoNSoFzAUu+7rZ4ECEXxazAQclEHTkR3r\n" +
"duUZ/XF0GL1WB0GC7+qvV/Z0gxjXuwG9oToFO/0MQQKBgQDu4DuTPWcYwSWY0R1f\n" +
"qZUOuYRwD+5OQnJMIlKAD32QmvYT/jnvigjss5Qf1IUwf1UMynj2FnVF4D7L+kvq\n" +
"O7LzYvHAeDQwZGGt2xWBlqjfhumlfBqfklkkqUiH2A5DvfvtbX/kkiY3n9C+oYZp\n" +
"2ejiOtSC+NqQeB74TluxroEkvwKBgQDgehynybpFl4KkmDhgj++BH5RR+xzXIChb\n" +
"gtIbbspdE1EyXy7Z9iNAJ8PVjHkSwh8iEfAO4EuJFnonF8UNIsWLr3gsKbQytRxR\n" +
"cewqaBhTL54Vgl5dmODNrYjkZva5HHDsCLioYGgljdrj5e/gPSAWBrgT6kI+HypQ\n" +
"/5xyp+KJMQKBgQCMxut1P8eliBa/M+YqvYdR8TVC0bCwwGoZwlR6kiZ+9UQ2zimY\n" +
"qPHPhZmzFI0V4sTdz+lvphahAqIfljftKBezZklxE6Y2KsKCMk4/W+nUKe9Cjpwm\n" +
"FJqih31uSX9Gnw18hH7N1u/c8juUTR8o/LpJsUASm9Q7Nf+SeKODWINVgwKBgDEx\n" +
"UXpLsPBzRYQAf8pZgKkRXJWirC1QtMdpIdY1L0+6Xf7l8QR+9janADmaMSY1OFFl\n" +
"EPCRorwGGvraMKqyRgxYhcNX2E+MdQo8Jv8cFMiWFNSt3zQvvoQUVX2IOuVSIET5\n" +
"nE354pjoP2HWD/1aJ9/r1Qc4PRAUEFfzzDssI27hAoGAOsYKtvW6iRn/WVduIRcy\n" +
"UtBRHHX0U16zGv+I7nOOBIYK5Uan6AjgzG2MfPOBj3cUhMMBDPfVg1cTbonw5Y8F\n" +
"nSO4VLOtqKy0BRxCIUFqltJXUmj1zAJs84IweCBQ3un/OLVUMgE7qGtaIQy2PBsy\n" +
"M8mwuUjo3Fu7l11E2Vgz/qY=";
// Base64 解碼
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64.replace("\n", ""));
byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64.replace("\n", ""));
// 生成 PublicKey(公鑰) 和 PrivateKey(私鑰) 對象
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyBytes));
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
// 待加密、簽名的原文
String data = "Hello World";
// 公鑰加密,私鑰解密
byte[] ciphertext = encrypt(data.getBytes(), publicKey);
System.out.println("RSA 公鑰加密結果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));
byte[] plaintext = decrypt(ciphertext, privateKey);
System.out.println("RSA 私鑰解密結果:" + new String(plaintext));
// 私鑰簽名,公鑰驗證簽名
byte[] signature = sign(data.getBytes(), privateKey);
System.out.println("RSA 私鑰簽名結果(Base64):" + Base64.getEncoder().encodeToString(signature));
boolean verifySuccess = verify(data.getBytes(), signature, publicKey);
System.out.println("RSA 公鑰驗證簽名結果:" + verifySuccess);
}
/**
* 公鑰加密
*/
public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] result = cipher.doFinal(data);
return result;
}
/**
* 私鑰解密
*/
public static byte[] decrypt(byte[] data, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] result = cipher.doFinal(data);
return result;
}
/**
* 私鑰簽名,使用 SHA256 with RSA 簽名
*/
public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(data);
byte[] result = signature.sign();
return result;
}
/**
* 公鑰驗證簽名
*/
public static boolean verify(byte[] data, byte[] sign, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(data);
return signature.verify(sign);
}
輸出:
RSA 公鑰加密結果(Base64):zoY6KM/RdCjAs7upJ9SIwqfXsSn3hAPu/z/ZPHbKgWN6+X0PpyVJVYT8jacEkzB7S2sJe/wLkO2TqXB2gqvL1AuDRgepVlxV2f6Uwx4DxM2/5RE0fAdTiICV5JEEIw81oLix0GGQ7nLjOhJxN9LaTJ2cXtwgR8gUtLtJ0tdWrxSMuN8FHLA45Nv8Ea1EAUQCvfanYZ2L39l++3/zBdg2wYQwCE6XGFnWnayUsGKYjC7JIufnq5f9VDL/kguLKceLmeTHqq31ccRTOQyhuoZjHCsbfXPlW2AT9ejgAcXy7LkXhYCfma50DBM+KUCfC4YrKBg6wKRqdZee90ZPcUKTkw==
RSA 私鑰解密結果:Hello World
RSA 私鑰簽名結果(Base64):AbP5zSV/qvkF8fCseVkEaZMscvznQBUDtO3g0U/FIXVmzeR6WXFwPsMd3cC3oCHtnnqsL/aRQrpW6pHU6EzSJ5w6FgY6kD4kWREq9f8LOnyQm7CoS6CK0tUiAjIgG16rtmS+oPbG+mYaZkLzo1Cpkpz2MzuMMbWNivvXRMbj3wLiXyIMqUefawipvm+GPwrWRxesRot2sGtuZcxtMMZs3NHpJ0CXV/mQlYJWEzIiHUY4mqfqpMDL/djPf9td74ABpjk38O6r1Jt75TLnMvkwRdh7pHBQLZ0Tn/6Vx2cVD2D+sE9BuhinO66B6I0QOGVcl3a5C2whp+85zEovvdGlSg==
RSA 公鑰驗證簽名結果:true
目前隨處可見的 HTTPS 協議,是基於 SSL/TLS 協議的。在 SSL/TLS 協議中,建立加密的傳輸通道前,首先有一個握手過程。在握手過程中,客户端會生成一個隨機值,並使用公鑰加密後傳給服務端。這個隨機值用於生成對稱加密算法的密鑰,僅有服務端的私鑰可以解密,任何第三方都無法解密,這就解決了前面所説到的對稱加密算法密鑰傳輸過程中的安全問題。而握手成功後的通信階段,則使用對稱加密算法進行通信。因為非對稱加密算法更加複雜,相對於對稱加密算法來説效率不高,不適合用來做大量數據的加密解密。
另外,SSL/TLS 中用到的數字證書(digital certificate),為了防止偽造,也會由 CA 機構進行數字簽名。目前大多數 HTTPS 網站使用的數字證書都是使用 SHA256 with RSA 簽名。
例如,在瀏覽器上打開 https://xxgblog.com/ ,點擊地址欄左側的小鎖按鈕,查看網站使用的證書,其數字簽名算法就是 SHA256 with RSA :