---
# System prepended metadata

title: 牛年自強計畫 Week 8 - SpringBoot - Encode & Encrypt & Hash 之於密碼
tags: [Spring Boot]

---

# 牛年自強計畫 Week 8 - SpringBoot - Encode & Encrypt & Hash 之於密碼

## 【前言】
身為工程師，一定對於密碼加密、解密；密文、明文與金鑰的處理不陌生。特別是商用軟體、有會員制的服務，對於處理這些資訊的方式都下足了工夫，怕的就是個資被盜、資訊外洩。

這篇文章就來簡單聊聊，平常在JAVA中處理密碼加密的部分，我們可以怎麼做？

首先我們先搞懂Encode、Encrypt 和 Hash 有什麼不同？

## 【Encode】
對於工程師來說，編碼是完全不陌生的東西，編碼並不會改變資料本身，也不進行任何加密操作，僅僅是採用不同的方式去描述或轉述同一個資料而已。

這就像是數學的 **1** 與國文數字的 **壹**，同樣都表示為單位個數一個，僅是文字與符號差異而已，意思皆相同。

### 【摩斯密碼】
其中最有名的便是摩斯密碼了，下圖取自[維基百科-摩斯密碼](https://en.wikipedia.org/wiki/Morse_code)

![](https://i.imgur.com/NJ0lgl4.png)

摩斯密碼最基本的組成是 **點(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&parameter_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&parameter_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`