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

前言

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

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

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

首先,若是你着急用,而且需求跟我差很少,我也就很少說了demo連接在下面,直接拿去用就好,若是好用,歡迎star,有問題也請直接提交issue,或者留言,我看到就會回覆.github

github.com/JVSFlipped/…算法

直接把JVSRSAHandler文件夾拖進你的工程裏面去
可能會有如下直接問題:
1.找不到頭文件,請在Build Settings -> SearchPatch -> Header Search Patchs 裏面填上對應的文件夾路徑
2.庫衝突,demo中使用的是openssl,據我所知,支付寶也用了這個東西,它的sdk包含了這個,因此須要刪除重複的便可.
3.報錯找不到"沒有添加.pem密鑰文件或者命名不一樣於代碼內名稱",這是我在demo中拋出的異常.中使用了rsa_public_key.pem來讀取公鑰,這個文件能夠問後臺要,也能夠本身生成,這裏不展開講.注意文件的命名必須跟express

importRSAKeyWithType:json

這個方法中的文件名保持一致.數組

接下里我詳細談談我在作這個需求時候踩到的坑和一些注意點:

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文件的問題
這是我在使用過程當中遇到的問題,當時忘了截圖了大概是相似於bash

architecture x86_64:app

的報錯,這個主要是由於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 裏的代碼我也更新了, 請放心使用.

相關文章
相關標籤/搜索