iOS端基于RSA公钥加密和解密

5,602 阅读10分钟

前言

最近在公司项目中被要求使用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 里的代码我也更新了, 请放心使用.