iOS端基於RSA公鑰加密和解密

JVSFlipped發表於2018-03-23

前言

最近在公司專案中被要求使用RSA加密,且要求是全程加解密,期間也是踩了很多的坑,在此做個記錄也算給要使用的朋友一點幫助.注意,具體的RSA加密演算法內容並不在此文的討論範圍之內.本文更多聚焦於使用部分.

我當前的使用場景和環境:

  • 1.移動端(iOS端)只有公鑰,拿不到私鑰,私鑰後臺保留
  • 2.基於base64進行編碼
  • 3.全程加密,即和後臺通訊的時候請求體是一段base64編碼.
  • 4.由於RSA加密機制決定了明文長度不能大於密文長度,所以需要分段加密和解密.
  • 5.使用的金鑰是1024位,要和後臺統一

首先,如果你著急用,並且需求跟我差不多,我也就不多說了demo連結在下面,直接拿去用就好,如果好用,歡迎star,有問題也請直接提交issue,或者留言,我看到就會回覆.

github.com/JVSFlipped/…

直接把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 編碼, 赫然發現我遺留的兩個問題的原因竟然真的跟我預期的一樣, 妹的我昨晚猜德國贏的, 為啥德國踢不過韓國棒子.

  1. 第一個問題: 不明白這裡為什麼是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 裡的程式碼我也更新了, 請放心使用.

相關文章