# 牛年自強計畫 Week 8 - SpringBoot - Encode & Encrypt & Hash 之於密碼
## 【前言】
身為工程師,一定對於密碼加密、解密;密文、明文與金鑰的處理不陌生。特別是商用軟體、有會員制的服務,對於處理這些資訊的方式都下足了工夫,怕的就是個資被盜、資訊外洩。
這篇文章就來簡單聊聊,平常在JAVA中處理密碼加密的部分,我們可以怎麼做?
首先我們先搞懂Encode、Encrypt 和 Hash 有什麼不同?
## 【Encode】
對於工程師來說,編碼是完全不陌生的東西,編碼並不會改變資料本身,也不進行任何加密操作,僅僅是採用不同的方式去描述或轉述同一個資料而已。
這就像是數學的 **1** 與國文數字的 **壹**,同樣都表示為單位個數一個,僅是文字與符號差異而已,意思皆相同。
### 【摩斯密碼】
其中最有名的便是摩斯密碼了,下圖取自[維基百科-摩斯密碼](https://en.wikipedia.org/wiki/Morse_code)

摩斯密碼最基本的組成是 **點(Dot)** 跟 **線(Dash)**,以及用來分隔的空格,以此組成一個詞彙、一段訊息等。
而實際應用中,字元與符號之間的轉換不一定跟圖片中相同,可以根據兩人之間的協議去更動,摩斯密碼因其簡單的原理且應用延伸十分廣泛,經常被應用於軍事防衛、航港環境、甚至科技電影的情節中。
```
舉例:
.... . .-.. .-.. --- .-- --- .-. .-.. -..
上述各符號以空格切分之後,可轉譯成 HELLOWORLD
```
**點(Dot)** 跟 **線(Dash)**、空格有可能被替換成手勢動作、聲音、時間經過等。
延伸類型過多,點到為止,有興趣的可自行深究。
### 【URI】
工程常見的對於 URI 的 encode、decode 處理,這是因為 URI 無法支援多種符號所致,因此針對較為特殊的符號必須先將其轉為 URI 可接受的格式規範,待接收後再將其轉回,避免中間過程傳輸出錯 or 亂碼。
詳細可參考 [w3School HTML URL Encoding Reference](https://www.w3schools.com/tags/ref_urlencode.ASP)
```java=
String url = "http://www.test.tw?parameter_1=check¶meter_2>2";
System.out.println("Origin Url: " + url);
try{
String encodeURL = URLEncoder.encode(url, "UTF-8");
System.out.println("URL Encode: " + encodeURL);
}catch(Exception e){
e.printStackTrace();
}
```
```
Origin Url: http://www.test.tw?parameter_1=check¶meter_2>2
URL Encode: http%3A%2F%2Fwww.test.tw%3Fparameter_1%3Dcheck%26parameter_2%3E2
```
### 【Base64】
一種可將二進位字元轉成 ASCII 的方法,但並非加密的方法,因此其非常容易破解,基本用於將過度赤裸的訊息轉譯為無法一眼辨別程度的文字串,從而達到隱藏訊息的目的。常應用在MIME 電子郵件、隱藏 URL 參數等場景。
```java=
/*** Old Version Way ***/
String secretMessage = "Message for Base64 Encoding";
System.out.println("Origin Message: " + secretMessage);
try{
BASE64Encoder encoder = new BASE64Encoder();
String encodedSecretMessage = encoder.encode(secretMessage.getBytes(StandardCharsets.UTF_8));
System.out.println("Message Encoded: " + encodedSecretMessage);
BASE64Decoder decoder = new BASE64Decoder();
String decodedSecretMessage = new String(decoder.decodeBuffer(encodedSecretMessage), StandardCharsets.UTF_8);
System.out.println("Message Decoded: " + decodedSecretMessage);
}catch(Exception e){
e.printStackTrace();
}
/*** New Version Way ***/
String secretMessage = "Message for Base64 Encoding";
System.out.println("Origin Message: " + secretMessage);
try{
Base64.Encoder encoder = Base64.getEncoder();
String encodedSecretMessage = encoder.encodeToString(secretMessage.getBytes(StandardCharsets.UTF_8));
System.out.println("Message Encoded: " + encodedSecretMessage);
Base64.Decoder decoder = Base64.getDecoder();
String decodedSecretMessage = new String(decoder.decode(encodedSecretMessage), StandardCharsets.UTF_8);
System.out.println("Message Decoded: " + decodedSecretMessage);
}catch(Exception e){
e.printStackTrace();
}
```
```
Origin Message: Message for Base64 Encoding
Message Encoded: TWVzc2FnZSBmb3IgQmFzZTY0IEVuY29kaW5n
Message Decoded: Message for Base64 Encoding
```
:::success
## 【Encode 之於密碼】
基本上對於密碼的保護,不會用 Encode 的方式處理,但這樣的說法並不實際,還是有不少公司行號甚至小型個人企業的系統採用最簡單的 Base64 方式作密碼處理,這樣的保護力完全不夠,一旦發生盜用問題,法律層面、系統革新成本都將會造成一定程度的傷害,對於密碼的處理,最起碼要用 Hash 的方式並只作配對驗證而不儲存原值,這在下面會提到。
> 雖然有些小企業老闆會說:「等事情發生在說。」or「我們也不是大企業,不會被攻擊。」
> ORZ...
:::
## 【Hash】
接續介紹 Hash,是一種將目標文字轉為 **相同長度** 的 **不可逆** 的 **雜湊文字** 的技術。
由於其不可逆的方式,因此成為大部分中型企業軟體的密碼保護方法首選,重視資安的公司甚至會多加幾道功夫在密碼保護上。
常見的方式有:
- **MD5**
> 2015年被破解
- **SHA-0**
> SHA-1 前身,發布後不久就被NSA撤回,2年後 SHA-1 問世
- **SHA-1**
> 2017年被荷蘭密碼學研究小組CWI和Google破解
- **SHA-2** (SHA-256, SHA-512, etc...)
> 隨著電腦發展出現越來越多漏洞,但 SHA-512 目前仍是 JAVA 中優秀的 Hash 方法
- **SHA-3** (SHA3-256, SHA3-512, etc...)
> 因應 MD5 以及 SHA-0, SHA-1 被攻破,而發展出的 Hash 新方法
對照表:
| Hash | Speed 處理速度 | Shortness 結果長度 | Duplicated 重複機率 | Security 安全性 |
| ---- | ------------- | ---------------- | --------------------- | -------------- |
| MD5 | 最快 | 16 bytes | 1.47 \* 10^-29^ | 低 |
| SHA-1 | 比 MD5 慢 20% | 20 bytes | 1 \* 10^-45^ | 中 |
| SHA-2 | 比 MD5 慢 60% | 32 bytes | 4.3 \* 10^-60^ | 高 |
參考資料來源: [Ariva - MD5: The broken algorithm](https://www.avira.com/en/blog/md5-the-broken-algorithm)
舉例:
```
password
經由 md5 轉換後,得到下面這段訊息
5f4dcc3b5aa765d61d8327deb882cf99
而這段訊息幾乎無法被反轉回去
```
其他近年著名的 Hash 方法還包含了 **PBKDF2**、**BCrypt** 和 **SCrypt** 等,詳細需讀者自行深研。在此不細說,不然就要寫好幾大篇文章了...
```java=
/*** MD5 ***/
String message = "Message for Hash";
System.out.println("Origin Message: " + message);
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(message.getBytes(StandardCharsets.UTF_8));
byte[] digest = md.digest();
String hashMessage = DatatypeConverter.printHexBinary(digest).toUpperCase();
System.out.println("MD5 Hash Message: " + hashMessage);
}catch(Exception e){
e.printStackTrace();
}
/*** SHA-512 ***/
String message = "Message for Hash";
System.out.println("Origin Message: " + message);
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
md.update(message.getBytes(StandardCharsets.UTF_8));
byte[] digest = md.digest();
String hashMessage = DatatypeConverter.printHexBinary(digest).toUpperCase();
System.out.println("SHA-512 Hash Message: " + hashMessage);
}catch(Exception e){
e.printStackTrace();
}
/*** PBKDF2 ***/
String message = "Message for Hash";
byte [] salt = "message for Salt.".getBytes(StandardCharsets.UTF_8);
System.out.println("Origin Message: " + message);
System.out.println("Salt: " + salt);
KeySpec spec = new PBEKeySpec(message.toCharArray(), salt, 65536, 128);
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte [] digest = factory.generateSecret(spec).getEncoded();
String hashMessage = DatatypeConverter.printHexBinary(digest).toUpperCase();
System.out.println("PBKDF2 Hash: " + hashMessage);
}catch(Exception e){
e.printStackTrace();
}
/*** Bcrypt ***/
String message = "Message for Hash";
String hash = BCrypt.hashpw(message, BCrypt.gensalt(11));
System.out.println("BCrypt Hash Message: " + hash);
Boolean b = BCrypt.checkpw(message, hash);
System.out.println(b);
```
```
Origin Message: Message for Hash
MD5 Hash Message: 0BE271FAD77B0F3B8A003536311AF785
SHA-512 Hash Message: FF1A6C369B0DB0E7130B8DF98E11636DAAF82385CC397BD5CBDB814296925FB9DD5B633B006829DFFBC2FEAEACCF0400A742A4684C859973965672806F8A2B5C
Salt: [B@7ce6a65d
PBKDF2 Hash: 410438F6CAA3C4C359D59B47ED1D2F3A
BCrypt Hash Message: $2a$11$JFnPmb7t76Y/MPSxECbk9eSXWplrRATXqVOIiB86FfMGGMLtKe6w.
```
:::success
## 【Hash 之於密碼】
基本上不少中小企業以往都採用 MD5 的方式進行密碼的處理,可惜的是,作為著名例子的 MD5 方法,在近代超級運算電腦的窮舉淫威下,也幾乎被破解了。
破解方法並不是透過方法的反推,而是透過大量輸入不同的字詞後取得的結果去配對。但後續有許多承著 MD5 的原理繼續發展的方法誕生,MD5 可以說功成身退,為其在電腦發展史中添上了一筆。
然而這並不代表 MD5 就完全不能用了,畢竟窮舉的方式被破解,但另外一個前提是你要取得這段訊息才能作配對,因此原先就已經作多層密碼保護的公司,考慮到其改變處理法的成本效益,可能暫時會續用 MD5 一段時間才更換,或是直接採用類似於 MD5 的新方法代替。
使用 Hash 作為保護密碼的額外搭配方式之一,就是只保留轉換後的值,而不保留原值,這樣做法可以讓公司內部員工避嫌,減少內部盜用的可能,對於客戶端的保護也會更好一些。
:::
## 【Encrypt】
接續介紹 Encrypt 即加密,將目標文字轉換為 **不盡相同長度** 的 **可逆** 的 **文字**。
加密的方法不盡相同,主要分作兩大類:
- **對稱密鑰加密**
- 傳送方與接收方擁有相同的密鑰,能針對同一份訊息進行加密與解密動作
- **非對稱金鑰加密**
- 接收方有公開密鑰與私有密鑰,將公開密鑰交予傳送方給訊息進行加密後,被加密的訊息無法被公開密鑰解密,只有接收方的私有密鑰可以進行正確的解密
通常來說,**非對稱加密**的保護力比**對稱加密**還要好,但是針對巨量資料的處理上,耗費的時間卻可能是**對稱加密**的數倍,因此並非完全都只能使用一種加密方式,而是需要針對資料型態與數量去決定。
而無論是哪一種方法,皆會有一組(或多組)用於加密解密的 KEY 存在,而實際應用的加密方法又非常多種多樣,因此各挑一個代表分享。
> 口語上我們會將欲加密的資訊稱為**明文**,加密後的稱為**密文**
### 【AES】
對稱加密的經典代表,鑒於 DES 系列被證實為不安全加密方法後,AES 便逐步取代 DES 的使用。
根據密鑰長度不同,AES 會將二進位文檔切分為長度同密鑰的區塊,並將其進行加密數次再做結合:
| Way | Key Length | Hash Length |
| ---- | --------- | ----------- |
| AES-128 | 16 | 32 |
| AES-192 | 24 | 48 |
| AES-256 | 32 | 64 |
是目前業界十分常用的加密方法,缺點就是 Key 如果遺失,那救回資料也是難如登天ORZ
以上是非常簡化的說法,實際上要理解絕對不容易。
詳細的 AES 加密邏輯推演,可參考
[iT邦幫忙-羊小咩著-Day 20. 對稱式加密演算法 - 大家都愛用的 AES](https://ithelp.ithome.com.tw/articles/10249488)
清楚、明白的優質好文章!!
```java=
try {
String secretMessage = "Message for Encrypt.";
System.out.println("Origin Message: " + secretMessage);
/*** Set Key ***/
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(256);
Key key = generator.generateKey();
/*** do Encrypt ***/
Cipher encryptCipher = Cipher.getInstance("AES");
encryptCipher.init(Cipher.ENCRYPT_MODE, key);
byte [] secretMessageBytes = secretMessage.getBytes(StandardCharsets.UTF_8);
byte [] encryptedMessageBytes = encryptCipher.doFinal(secretMessageBytes);
System.out.println("AES Encrypt: " + encryptedMessageBytes);
/*** do Decrypt ***/
Cipher decryptCipher = Cipher.getInstance("AES");
decryptCipher.init(Cipher.DECRYPT_MODE, key);
byte[] decryptedMessageBytes = decryptCipher.doFinal(encryptedMessageBytes);
String decryptedMessage = new String(decryptedMessageBytes, StandardCharsets.UTF_8);
System.out.println("AES Decrypt: " + decryptedMessage);
}catch(Exception e){
e.printStackTrace();
}
```
```
Origin Message: Message for Encrypt.
AES Encrypt: [B@37e547da
AES Decrypt: Message for Encrypt.
```
### 【RSA】
非對稱加密的經典代表,因其加密為數值運算,因此加密前會把明文轉成二進位數進行運算。而其 Key 的處理目前支援 1024 bits, 2048 bits 及以上,因 768 bits 被破解關係,推薦至少使用到 1024 bits 以上,目前沒有一個有效的破解法能夠解開 1024 bits 以上的 RSA 加密。
詳細的 AES 加密邏輯推演,可參考
[iT邦幫忙-羊小咩著-Day 23. 非對稱式加密演算法 - RSA (觀念篇)](https://ithelp.ithome.com.tw/articles/10250721)
清楚、明白的優質好文章.Too !!
```java=
try {
/*** build Generator with 2048bits key ***/
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
KeyPair pair = generator.generateKeyPair();
/*** build public and private keys ***/
PrivateKey privateKey = pair.getPrivate();
PublicKey publicKey = pair.getPublic();
/*** build a file to store public key ***/
FileOutputStream fos = new FileOutputStream("public.key");
fos.write(publicKey.getEncoded());
File publicKeyFile = new File("public.key");
byte[] publicKeyBytes = Files.readAllBytes(publicKeyFile.toPath());
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
keyFactory.generatePublic(publicKeySpec);
/*** build message ***/
String secretMessage = "Message for Encrypt.";
System.out.println("Origin Message: " + secretMessage);
/*** do Encrypt ***/
Cipher encryptCipher = Cipher.getInstance("RSA");
encryptCipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] secretMessageBytes = secretMessage.getBytes(StandardCharsets.UTF_8);
byte[] encryptedMessageBytes = encryptCipher.doFinal(secretMessageBytes);
System.out.println("RSA Encrypt: " + encryptedMessageBytes);
/*** do Decrypt ***/
Cipher decryptCipher = Cipher.getInstance("RSA");
decryptCipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedMessageBytes = decryptCipher.doFinal(encryptedMessageBytes);
String decryptedMessage = new String(decryptedMessageBytes, StandardCharsets.UTF_8);
System.out.println("RSA Decrypt: " + decryptedMessage);
}catch(Exception e){
e.printStackTrace();
}
```
```
Origin Message: Message for Encrypt.
RSA Encrypt: [B@358c99f5
RSA Decrypt: Message for Encrypt.
```
:::success
## 【Encrypt 之於密碼】
Encrypt 對於密碼來說可能不是最好的,但應是其中一道該有的做法,原因在 Hash 提到過,伺服端盡可能不去存取客戶真實密碼,而是改用 Hash 後的訊息替代,但如今透過窮舉的方式破解 Hash 過的訊息太過容易,因此針對這部分就必須要使用 Encrypt 方式處理,也就是先 Hash 再加密的方式,部分大型企業甚至會有好幾層方法交互應用。
:::
## 【結語】
稍微整理了近幾年碰到這三類東西的一點經驗,寫了篇筆記在這裡,避免日後忘記,總結三者有各自的功用,依照需求選用才能達到預期的保護效果~
[GitHub 實作分享](https://github.com/moom50302/LearningPratice/tree/main/src/main/java/Encrypt)
首頁 [Kai 個人技術 Hackmd](/2G-RoB0QTrKzkftH2uLueA)
###### tags: `Spring Boot`