
最近在工作上遇到了 RSA 加密的需求,雖然已經有不少業界前輩寫過很棒的 RSA 文章,但我想以初次實作的角度,分享一些讓我感興趣、困惑的小知識。
因此,這篇文章會循序漸進,希望讓你順順地看下去就能略懂一二!
收到的需求
在前端呼叫後端 API 時,為了確保敏感資訊(如個人資料)能在傳遞過程中保持安全,我們可以使用 RSA 加密來保護這些資訊。
- 前端:使用後端提供的 公鑰 (public key) 來加密資料
- 後端:使用 私鑰 (private key) 來解開資料
在實作前,先來了解什麼是 RSA 加密吧~
RSA 是蝦米?
RSA 是一種「非對稱式加密演算法」( asymmeric cryptosystem ),指加密跟解密使用不同的金鑰。
特性是公鑰加密上鎖後,只能用私鑰解鎖。
因此,最大的好處在於「即使傳輸過程中被攔截,由於沒有私鑰,也無法解密出資料內容」。
在研究 RSA 時,也常看到 AES 這個陌生的名詞。
這兩者有什麼關係,又有什麼差異呢?
AES vs. RSA?
一般來說,常見的加密方式分為以下兩種:
AES 加密
- 屬於對稱加密演算法,加密和解密使用相同的密鑰
- 運算速度快、用途廣泛,適合加密大量數據
- 但若密鑰洩漏,所有加密資料都可能被解開
- 在 iOS 可以用 CryptoSwift 套件處理
RSA 加密
- 屬於非對稱加密演算法,加密和解密使用不同的密鑰
- 運算速度較慢,適合加密小型數據
- 在 iOS 可以用 SwiftyRSA 套件處理
當然,我們也可以融合以上兩者的優勢,使用混合式加密!
混合式加密
- 融合非對稱、對稱加密的優點
- 加密大檔案時,可先用 AES 加密,再以 RSA 公鑰加密 AES 金鑰,這樣可以兼顧安全性與效能
查完了資料,後端突然敲了我,並給我了一個 PEM 格式的公鑰。
這又讓我好奇了,什麼是 PEM 格式呢?
PEM (Privacy Enhanced Mail)
提到公鑰私鑰,你可能會看過這個格式~
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1ymUuKnPRw99S1wMbEJt
nKv7lTOyYryYiXAlRjQznFYSaLVa8TACplEKEiLPO2A3aJG9tJf6ObMRiOhKnuGI
E9QJ+ByNOqJZM3mTHm/hAnX0N8d7WCozrJGyXlrb4I/JNkwTFx8DWggT+0rLPTCs
...
-----END PUBLIC KEY-----
這就是常用的 PEM 格式 👆🏻
PEM 格式是一種人類可讀的文字編碼,常用於憑證、公鑰與私鑰的儲存與交換,並可於純文字檔中輕鬆保存與傳輸。
副檔名 .pem
、.crt
、.cer
,裡面都有可能是 PEM 格式,因此只要打開看到 -----BEGIN ...-----
,便能得知它是 PEM!
延伸閱讀: 憑證的格式 PEM 與 DER
iOS 實戰
瞭解基本知識後,準備來實作吧!
在這次的需求中,除了要把「要傳遞的 API 路徑與參數」進行 RSA 加密外,還需要先對 API 路徑與參數做一定的處理。
這些處理的目的是為了降低 API 路徑與參數命名的混亂度,讓後端更好判斷與解密。
具體規則如下:
將 API 進行加密,並放置 header 傳遞資訊。
舉例:RSA(user/getUserInfo?param1=value¶m2=value)
1. route 轉換小寫,去除 "-" 值
a. 格式:route1/route2
b. 舉例:user/getUserInfo
2. parameters 轉換小寫,去除 "-" 值,並用 & 組合
a. 格式:?param=value
b. 舉例:?id=bohsunhsu&password=abcde
1. 導入套件
我們會使用套件 SwiftyRSA 實作 RSA 加密,記得自行導入專案。
2. 取得公鑰
以我的例子,公鑰是由後端提供,而各位在實作前,可以用 Devglan 產出 RSA PEM 格式的公鑰與私鑰。
為求方便,這裡的示範是用參數儲存鑰匙,但現實中請不要這樣做,較佳的方式是放在 Keychain 儲存(但有點偏題故不在此贅述)。
private let testPublicKey = """
-----BEGIN PUBLIC KEY-----
XXXXXXXXXXXXXXXXXXXXXXXXXX
-----END PUBLIC KEY-----
"""
private let testPrivateKey = """
-----BEGIN RSA PRIVATE KEY-----
XXXXXXXXXXXXXXXXXXXXXXXXXX
-----END RSA PRIVATE KEY-----
"""
3. 使用公鑰加密數據
func encryptData(route: String, parameters: [String: String]) -> String {
// 1. route 路徑轉換小寫,去除 "-" 值
let lowercasedRoute = route
.lowercased()
.replacingOccurrences(of: "-", with: "")
// 2. parameters 轉換小寫,去除 "-" 值
let paramString = parameters
.map { "\\($0.key)=\\($0.value)" }
.joined(separator: "&")
.lowercased()
.replacingOccurrences(of: "-", with: "")
// 組合起來準備加密
let stringToEncrypt = "\\(lowercasedRoute)?\\(paramString)"
do {
// 拿出公鑰,將 pem 格式解成一般的 String
let publicKey = try PublicKey(pemEncoded: testPublicKey)
// ClearMessage 代表的是「尚未加密的明文」
// 指定使用 UTF-8 編碼將字符串轉換為二進制數據
let clear = try ClearMessage(string: stringToEncrypt, using: .utf8)
// EncryptedMessage 代表的是 「加密後的數據」,與 ClearMessage 相反
// padding 代表的是加密過程中的填充模式,解決明文長度不符合加密演算法的問題
let encryptedMessage = try clear.encrypted(with: publicKey, padding: .PKCS1)
return encryptedMessage.base64String
} catch {
return error.localizedDescription
}
}
為什麼需要 Padding?
當使用 RSA 時,密文的長度是由公鑰的長度決定的(例如 2048 位元),但明文的長度不一定剛好符合這個長度限制。因此需要 Padding 調整明文的長度,以符合加密演算法的要求。
常見的 Padding 有 PKCS#1、OAEP。
PKCS#1 較普及,但較舊;OAEP 更為安全,但需確認後端是否支援。若前後端都支援,建議使用 OAEP。
4. 使用私鑰解密數據
通常我們不需要做這件事情(由後端解密),但可以解看看是不是跟加密前相同。
func decryptData(readyToDecodeString: String, environment: Environment) -> String {
do {
// 將 pem 格式解成一般的 String
let privateKey = try PrivateKey(pemEncoded: testPrivateKey)
// 「加密後的數據」被 base64 編碼過,因此需要先解開 base64 字串
let encrypted = try EncryptedMessage(base64Encoded: readyToDecodeString)
// 去除 padding,把「加密後的數據」變回「尚未加密的明文」
let clear = try encrypted.decrypted(with: privateKey, padding: .PKCS1)
// 將解密後的明文(clear)轉換為一個 UTF-8 的 String
let string = try clear.string(encoding: .utf8)
// 就可以看到加密前的 String 囉
return string
} catch {
return error.localizedDescription
}
}
這樣就大功告成囉~恭喜(%%%%)!
這篇文章其實就是我的筆記整理,拿到新的需求時,得到解決方法早已不是一件困難的事(AI 世代嘛),但在解決問題的過程中,陸續搞懂每一個名詞、不間斷地質疑並理解、最後內化成自己的知識,還是重要且有趣的!
我可真囉嗦!總之~有任何想法都可以聯繫我,下次見囉~~
參考資料:
https://ithelp.ithome.com.tw/articles/10250721
https://ithelp.ithome.com.tw/articles/10251744
https://blog.csdn.net/weixin_44259720/article/details/110947742
https://www.ssldragon.com/zh/blog/rsa-aes-encryption/