前言
最近在公司專案中被要求使用RSA加密,且要求是全程加解密,期間也是踩了很多的坑,在此做個記錄也算給要使用的朋友一點幫助.注意,具體的RSA加密演算法內容並不在此文的討論範圍之內.本文更多聚焦於使用部分.
我當前的使用場景和環境:
- 1.移動端(iOS端)只有公鑰,拿不到私鑰,私鑰後臺保留
- 2.基於base64進行編碼
- 3.全程加密,即和後臺通訊的時候請求體是一段base64編碼.
- 4.由於RSA加密機制決定了明文長度不能大於密文長度,所以需要分段加密和解密.
- 5.使用的金鑰是1024位,要和後臺統一
首先,如果你著急用,並且需求跟我差不多,我也就不多說了demo連結在下面,直接拿去用就好,如果好用,歡迎star,有問題也請直接提交issue,或者留言,我看到就會回覆.
直接把JVSRSAHandler資料夾拖進你的工程裡面去
可能會有以下直接問題:
1.找不到標頭檔案,請在Build Settings -> SearchPatch -> Header Search Patchs 裡面填上對應的資料夾路徑
2.庫衝突,demo中使用的是openssl,據我所知,支付寶也用了這個東西,它的sdk包含了這個,所以需要刪除重複的即可.
3.報錯找不到"沒有新增.pem金鑰檔案或者命名不同於程式碼內名稱",這是我在demo中丟擲的異常.中使用了rsa_public_key.pem來讀取公鑰,這個檔案可以問後臺要,也可以自己生成,這裡不展開講.注意檔案的命名必須跟
importRSAKeyWithType:
這個方法中的檔名保持一致.
接下里我詳細談談我在做這個需求時候踩到的坑和一些注意點:
1.網上有很多公鑰加密私鑰解密的,我找了很久都沒找到合適的公鑰解密的解決方案,各位不要去找後臺要私鑰啊,這牽扯到RSA的加密機制,即使用的策略是非對稱加密,即客戶端使用公鑰,後臺使用私鑰,公鑰加密的內容只有私鑰能解開,這樣即使客戶端的公鑰被竊取了(實際上設計是公開公鑰的),只要私鑰妥善得保管在後臺,公鑰加密資料的安全就能得到保證.
2.要確定幾個重要引數,我在demo中有註釋
//RSA演算法填充型別,前後臺要統一
static NSInteger kRSAPaddingType = RSA_PKCS1_PADDING;
//解密長度,前後臺要統一
static NSInteger kDecryptionLength = 128;
//RSA公鑰檔名
static NSString *kPublicKeyFile = @"rsa_public_key";
//加密長度,前後臺要統一
static NSInteger kEncryptionLength = 117;
//RSA金鑰檔名,目前沒有此類呼叫,後續可能會新增
static NSString *kPrivateKeyFile = @"rsa_private_key";
複製程式碼
3.有關openssl檔案的問題
這是我在使用過程中遇到的問題,當時忘了截圖了大概是類似於
architecture x86_64:
的報錯,這個主要是因為openssl資料夾中lib下的.a庫太老,不支援最新的iOS系統,我提供的demo中應該沒有這個問題(因為我弄的是比較新的,具體方法這裡不展開講了,跟這裡沒啥關係).
4.有關分段加密的問題
由於RSA限制明文長度不能長於密文長度,所以資料過長就需要分段加密,就是說分段加密然後base64編碼然後再拼接起來.
獲取公鑰
不管加密還是解密都需要提前獲取公鑰
//獲取key
- (BOOL)importRSAKeyWithType:(KeyType)type
{
FILE *file;
NSString *keyName = type == KeyTypePublic?kPublicKeyFile:kPrivateKeyFile;
NSString *keyPath = [[NSBundle mainBundle] pathForResource:keyName ofType:@"pem"];
file = fopen([keyPath UTF8String], "rb");
if (NULL != file)
{
if (type == KeyTypePublic)
{
_rsa = PEM_read_RSA_PUBKEY(file, NULL, NULL, NULL);
assert(_rsa != NULL);
}
else
{
_rsa = PEM_read_RSAPrivateKey(file, NULL, NULL, NULL);
assert(_rsa != NULL);
}
fclose(file);
return (_rsa != NULL) ? YES : NO;
}
NSException* exception = [NSException exceptionWithName:@"讀取金鑰失敗!" reason:@"沒有新增.pem金鑰檔案或者命名不同於程式碼內名稱" userInfo:nil];
@throw exception;
return NO;
}
複製程式碼
加密過程
JVSRSAHandler提供了加密方法將字典轉換並基於RSA加密後再base64編碼獲得字串的方法
//加密字典
- (NSString *)encryptDictionary:(NSDictionary*)dict WithRSAKeyType:(KeyType)keyType
{
//將字典轉成json字串
NSString *jsonString = [self conversionDictionary:dict];
//轉成UTF8Data
NSData *UTF8Data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
//加密過程
NSData *RSAEncryptData = [self encryptionData:UTF8Data WithRSAKeyType:keyType];
//轉成base64的string
NSString *encryptString = [RSAEncryptData base64EncodedString];
return encryptString;
}
複製程式碼
加密方法,這裡主要是分段過程:
//加密方法,這裡主要是分段內容
- (NSData *)encryptionData:(NSData *)expressData WithRSAKeyType:(KeyType)keyType
{
if (expressData && [expressData length]) {
//計劃分段加密長度
NSInteger planSubLength = kEncryptionLength;
//資料總長度
NSInteger sumLength = [expressData length];
//分段數
NSInteger blockCount = sumLength/planSubLength + ((sumLength%planSubLength)?1:0);
//總的資料,存放解密後的資料
NSMutableData *sumData = [[NSMutableData alloc ] initWithCapacity:0];
for(int i = 0;i < blockCount; i++)
{
//實際分段長度,注意最後一段不夠的問題
int relSubLength = (int)MIN(planSubLength, sumLength - i*planSubLength);
//定義放置待加密資料的陣列, 因為要按kDecryptionLength(128)進行拼接, 所以長度為 128
unsigned char expressArr[kDecryptionLength];
//C函式方法,將陣列初始化置空
bzero(expressArr, sizeof(expressArr));
//在expressArr中放入目標要加密的資料
memcpy(expressArr, [[expressData subdataWithRange:NSMakeRange(i*planSubLength, relSubLength)] bytes], relSubLength);
//定義存放加密後資料的陣列,因為明文長度不得大於密文長度,所以這裡的長度為計劃長度(密文,較長)
unsigned char encryptedArr[planSubLength];
//同上,將陣列初始化置空
bzero(encryptedArr, sizeof(encryptedArr));
//加密expressArr中的資料並放入encryptedArr陣列中
[self encryptFrom:expressArr length:(int)relSubLength to:encryptedArr WithKeyType:keyType];
int k=0;
// 拼接
for(int j = 0;j< 128;j++)
{
if(encryptedArr[j] != '\0')
{
k = j+1;
}
}
// base64 解碼時候, 長度必須為 4 的倍數
if(k%4 != 0){
k = ((int)(k/4) + 1)*4;
}
//拼接加密後資料
[sumData appendData:[NSData dataWithBytes:encryptedArr length:k]];
}
return sumData;
}
return nil;
}
複製程式碼
真正的加密部分
//加密部分
- (NSInteger)encryptFrom:(const unsigned char *)expressArr length:(int)length to:(unsigned char *)encryptedArr WithKeyType:(KeyType)keyType
{
//匯入檔案中金鑰
if (![self importRSAKeyWithType:keyType])
return 0;
if (expressArr != NULL && encryptedArr != NULL) {
NSInteger status;
switch (keyType) {
case KeyTypePrivate:{
//私鑰加密
status = RSA_private_encrypt(length, expressArr,encryptedArr, _rsa, (int)kRSAPaddingType);
}
break;
default:{
//公鑰加密
status = RSA_public_encrypt(length,expressArr,encryptedArr, _rsa, (int)kRSAPaddingType);
}
break;
}
return status;
}
return -1;
}
複製程式碼
解密過程
JVSRSAHandler提供瞭解密方法將後臺給的base64字串轉化成字典
//解密字串
- (NSDictionary *)decryptString:(NSString *)encryptedString WithRSAKeyType:(KeyType)keyType
{
//將要解密的字串base64解碼
NSData *encryptedData = [[NSData alloc] initWithBase64EncodedString:encryptedString options:NSDataBase64DecodingIgnoreUnknownCharacters];
//解密過程
NSData *jsonData = [self decryptData:encryptedData WithRSAKeyType:keyType];
//將data轉成string
NSString *josnString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
//轉成字典輸出
NSDictionary *dict = [self dictionaryWithJsonString:josnString];
return dict;
}
複製程式碼
解密方法,這裡主要是分段過程:
//解密方法(主要是分段)
- (NSData *)decryptData:(NSData *)encryptedData WithRSAKeyType:(KeyType)keyType
{
if (encryptedData && [encryptedData length]) {
//計劃解密長度
NSInteger planSubLength = kDecryptionLength;
//資料總長度
NSInteger sumLength = [encryptedData length];
//分段數
NSInteger blockCount = sumLength/planSubLength + ((sumLength%planSubLength)?1:0);
//存放解密後的資料
NSMutableData *sumData = [[NSMutableData alloc ] initWithCapacity:0];
for(int i = 0;i < blockCount; i++)
{
//實際分段的長度,注意最後一段不夠的情況
int realSubLength = (int)MIN(planSubLength, sumLength - i*planSubLength);
//定義存放待解密資料的陣列encryptedArr(密文,較長)
unsigned char encryptedArr[planSubLength];
//C函式,初始化置空encryptedArr陣列
bzero(encryptedArr, sizeof(encryptedArr));
//將待解密的data資料存放入encryptedArr陣列中
memcpy(encryptedArr, [[encryptedData subdataWithRange:NSMakeRange(i*planSubLength, realSubLength)] bytes], realSubLength);
//定義存放解密出來的資料的陣列expressArr(明文,較短)
unsigned char expressArr[realSubLength];
//初始化置空expressArr陣列
bzero(expressArr, sizeof(expressArr));
//解密encryptedArr中的資料並存入expressArr中
[self decryptFrom:encryptedArr length:realSubLength to:expressArr WithKeyType:keyType];
int k=0;
// 拼接
for(int j = 0;j< planSubLength;j++)
{
if(expressArr[j] != '\0')
{
k = j+1;
}
}
//拼接解密出來的資料
[sumData appendData:[NSData dataWithBytes:expressArr length:k]];
}
return sumData;
}
return nil;
}
複製程式碼
真正的解密部分
//解密方法
- (NSInteger)decryptFrom:(const unsigned char *)encryptedArr length:(int)length to:(unsigned char *)expressArr WithKeyType:(KeyType)keyType
{
//獲取金鑰
if (![self importRSAKeyWithType:keyType])
return -1;
if (encryptedArr != NULL && expressArr != NULL) {
int status;
switch (keyType) {
case KeyTypePrivate:{
//私鑰解密
status = (int)RSA_private_decrypt(length, encryptedArr,expressArr, _rsa, (int)kRSAPaddingType);
}
break;
default:{
//公鑰解密
status = RSA_public_decrypt(length, encryptedArr, expressArr, _rsa, (int)kRSAPaddingType);
}
break;
}
return status;
}
return -1;
}
複製程式碼
其他的字串轉字典互轉的部分我就不詳細碼出來了,demo裡面都有,注意留意控制檯列印的資訊,會有測試加密出來的密文儲存的txt文件路徑以及解密密文出來的字典列印.
總結
簡單來說,在這個過程中JVSHandler做了以下事情:
加密過程:字典 -> 字串(UTF-8) -> Data(UTF-8) -> Data(分段) -> Data(加密) -> 字串(base64)
解密過程:字串(base64) -> Data(base64解碼) -> Data(分段) -> Data(解密) -> 字串(UTF-8) -> 字典
目前存在的問題(已解決, 請往後看)
- 1.0版本的時候加密不穩定,後來新版本新增了程式碼基本解決了這個問題,但是這部分程式碼我是在網上找來的,具體為啥我目前也很懵(emmmmmmm...)
//不明白這裡為什麼是128,按理說128會越界的,因為定義的時候陣列長度只有117
for(int j = 0;j< 128;j++)
{
if(encryptedArr[j] != '\0')
{
k = j+1;
}
}
//同樣不明白這裡的操作含義,去掉的話加密成功率降低很多
if(k%4 != 0){
k = ((int)(k/4) + 1)*4;
}
複製程式碼
我的疑惑點在哪我已經寫在上面了,希望有大神可以賜教.我目前懷疑是RSA本身需要或是編碼規則需要,目前還沒時間仔細去研究,後續如果搞明白了我會補充的.具體的RSA加解密演算法過程之後有時間也會研究下,有必要也會做出一份這樣的記錄文件分享出來.
解決之前的問題
我們的 app 已經上線很久了, 使用這個輪子也一直沒出過啥問題, 也就一直沒動力解決之前心中的疑惑, 最近有時間有重頭梳理了下這個 RSA 加解密, 和 Base64 編碼, 赫然發現我遺留的兩個問題的原因竟然真的跟我預期的一樣, 妹的我昨晚猜德國贏的, 為啥德國踢不過韓國棒子.
- 第一個問題: 不明白這裡為什麼是128,按理說128會越界的,因為定義的時候陣列長度只有117?
for(int j = 0;j< 128;j++)
{
if(encryptedArr[j] != '\0')
{
k = j+1;
}
}
複製程式碼
這真是個先有雞還是先有蛋的問題, 因為這個陣列本身長度就不應該定義為 117, 117 是加密長度沒有錯, 但是是指對 data 的加密長度, 陣列本身是用來存資料的, 而 RSA 是非對稱加密, 解密的時候長度是128, 所以拼接的時候是按128來拼接的, 所以陣列的長度應該是解密長度128而不是加密長度117, 而坑爹的 C 語言陣列越界又不報錯, 資料拼接起來之後還能正常解密, 我就沒往下細想這個問題. 現在看來還是自己當時考慮不夠周全. 2. 第二個問題: 這個操作是啥意思
if(k%4 != 0){
k = ((int)(k/4) + 1)*4;
}
複製程式碼
這裡牽扯到 base64 編碼問題, 解碼base64時必須要是4的倍數個, 但編碼時,字元數無所謂. 這裡操作的目的是保證起來的是4的倍數個.
後記
這個輪子穩定性很高了, 至少我嘗試過加密很多較多和較為複雜的資料, 而且公司專案目前已經使用三四個月了, 目前沒有發現有加解密失敗的情況, 文章和 demo 裡的程式碼我也更新了, 請放心使用.