iOS使用OpenSSL进行RSA加密、验签的心得

最近做跨境支付类项目,安全要求等级比较高。数据加密验签流程比较复杂。先做一个复盘。

工作流程:
  1. App创建RSA密钥对,将公钥(cPubKey)和IMEI码发送给服务器,私钥(cPriKey)保存本地。
  2. 服务器根据IMEI也创建RSA密钥对和一个32位随机码(RandKey)将私钥(serverPriKey)和RandKey根据IMEI码保存在服务端。返回给客户端服务器公钥(serverPubKey)和用cPubKey加密的RandKey, 客户端用cPriKey解密RandKey保存。
    完成1、2两步后:服务器和客户端双方都保存对方的公钥和自己私钥及RandKey。通过IMEI号做关联。
  3. 客户端发送请求时,将特定参数用cPriKey签名,将”真正请求参数“用RandKey进行AES256进行加密。
  4. 服务端接受请求时,用cPubKey对请求中签名进行验签。验签成功,用RandKey解密”真正请求参数“。
  5. 服务端返回请求时,将特定参数用serverPriKey签名,将”真正返回数据“用RandKey进行AES256进行加密。
  6. 客户端接受回执时,用serverPubKey对回执中签名进行验签。验签成功,用RandKey解密”真正返回数据“。

注:iOS不能用真正的IMEI详情参考seventhboy的文章

总结:
  1. 服务端和客户端分别生成密钥对,通过IMEI码进行绑定。保证每个用户和服务器之间的秘钥都是单独对应的。
  2. 双方都保存对方的公钥和自己私钥及RandKey。(私钥签名、公钥验签,公钥加密、私钥解密)
  3. 用自己私钥签名,将数据发送对方;收到对方签名过的数据后,用对方共钥验签。
  4. 用RandKey进行AES256进行加密核心数据。非对称加密效率低,加密内容短。所以要用aes这样的对称加密来加密data部数据。
核心代码

参考了XPorter的文章和其他几篇类似文章。

#pragma mark ---生成密钥对
+ (BOOL)generateRSAKeyPairWithKeySize:(int)keySize publicKey:(RSA **)publicKey privateKey:(RSA **)privateKey{
    if (keySize == 512 || keySize == 1024 || keySize == 2048) {
        /* 产生RSA密钥 */
        RSA *rsa = RSA_new();
        BIGNUM* e = BN_new();
        /* 设置随机数长度 */
        BN_set_word(e, 65537);
        /* 生成RSA密钥对 RSA_generate_key_ex()新版本方法 */
        RSA_generate_key_ex(rsa, keySize, e, NULL);
        if (rsa) {
            *publicKey = RSAPublicKey_dup(rsa);
            *privateKey = RSAPrivateKey_dup(rsa);
            return YES;
        }
    }
    return NO;
}

此方法有一定的失败概率,用下面方法保证成功

//生成本地密钥对
        while (1) {
            if ([LPXRSATool generateRSAKeyPairWithKeySize:2048 publicKey:&_publicKey privateKey:&_privateKey]) {
                
                self.publicKeyBase64 = [LPXRSATool base64EncodedStringKey:_publicKey isPubkey:YES];
                self.privateKeyBase64 = [LPXRSATool base64EncodedStringKey:_privateKey isPubkey:NO];
                NSLog(@"\n私钥:\n%@",_privateKeyBase64);
                NSLog(@"\n公钥:\n%@",_publicKeyBase64);
                if (_privateKeyBase64 && _publicKeyBase64) {
                    [ud setObject:_publicKeyBase64 forKey:cBase64_PubKey];
                    [ud setObject:_privateKeyBase64 forKey:cBase64_PriKey];
                    
                    break;
                }
            }
        }
//
//  LPXRSATool.h
//  OTTPAY
//
//  Created by Lipengxuan on 2019/1/5.
//  Copyright © 2019 Lipengxuan. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <openssl/rsa.h>

typedef enum {
    Rsa_PKCS1_PADDING       =   RSA_PKCS1_PADDING,
    Rsa_SSLV23_PADDING      =   RSA_SSLV23_PADDING,
    Rsa_NO_PADDING          =   RSA_NO_PADDING,
    Rsa_PKCS1_OAEP_PADDING  =   RSA_PKCS1_OAEP_PADDING,
    Rsa_X931_PADDING        =   RSA_X931_PADDING,
    /* EVP_PKEY_ only */
    Rsa_PKCS1_PSS_PADDING   =   RSA_PKCS1_PSS_PADDING,
    Rsa_PKCS1_PADDING_SIZE  =   RSA_PKCS1_PADDING_SIZE,
}RsaPaddingType;

NS_ASSUME_NONNULL_BEGIN

@interface LPXRSATool : NSObject
    
+ (BOOL)generateRSAKeyPairWithKeySize:(int)keySize publicKey:(RSA **)publicKey privateKey:(RSA **)privateKey;
    
+ (RSA *)rsaFromBase64:(NSString *)base64Key isPubkey:(BOOL)isPubkey;
    
#pragma mark ---密钥格式转换
+ (RSA *)rsaFromPEM:(NSString *)KeyPEM isPubkey:(BOOL)isPubkey;
+ (NSString *)base64EncodedStringKey:(RSA *)rsaKey isPubkey:(BOOL)isPubkey;
+(NSString *)PEMKeyFromBase64:(NSString *)base64Key isPubkey:(BOOL)isPubkey;

#pragma mark ---加解密
+ (NSData *)encryptWithPublicKey:(RSA *)publicKey plainData:(NSData *)plainData padding:(RsaPaddingType)padding;
+ (NSData *)decryptWithPrivateKey:(RSA *)privateKey cipherData:(NSData *)cipherData padding:(RsaPaddingType)padding;
+ (NSData *)encryptWithPrivateRSA:(RSA *)privateKey plainData:(NSData *)plainData padding:(RsaPaddingType)padding;
+ (NSData *)decryptWithPublicKey:(RSA *)publicKey cipherData:(NSData *)cipherData padding:(RsaPaddingType)padding;
@end

NS_ASSUME_NONNULL_END

//
//  LPXRSATool.m
//  OTTPAY
//
//  Created by Lipengxuan on 2019/1/5.
//  Copyright © 2019 Lipengxuan. All rights reserved.
//

#import "LPXRSATool.h"
#import <openssl/pem.h>

@implementation LPXRSATool
    
#pragma mark ---生成密钥对
+ (BOOL)generateRSAKeyPairWithKeySize:(int)keySize publicKey:(RSA **)publicKey privateKey:(RSA **)privateKey{
    if (keySize == 512 || keySize == 1024 || keySize == 2048) {
        /* 产生RSA密钥 */
        RSA *rsa = RSA_new();
        BIGNUM* e = BN_new();
        /* 设置随机数长度 */
        BN_set_word(e, 65537);
        /* 生成RSA密钥对 RSA_generate_key_ex()新版本方法 */
        RSA_generate_key_ex(rsa, keySize, e, NULL);
        if (rsa) {
            *publicKey = RSAPublicKey_dup(rsa);
            *privateKey = RSAPrivateKey_dup(rsa);
            return YES;
        }
    }
    return NO;
}
+ (NSString *)base64EncodedStringKey:(RSA *)rsaKey isPubkey:(BOOL)isPubkey{
    if (!rsaKey) {
        return nil;
    }
    BIO *bio = BIO_new(BIO_s_mem());
    
    if (isPubkey) {
        PEM_write_bio_RSA_PUBKEY(bio, rsaKey);
    }else{
        //此方法生成的是pkcs1格式的,IOS中需要pkcs8格式的,因此通过PEM_write_bio_PrivateKey 方法生成
        // PEM_write_bio_RSAPrivateKey(bio, rsaKey, NULL, NULL, 0, NULL, NULL);
        EVP_PKEY* key = NULL;
        key = EVP_PKEY_new();
        EVP_PKEY_assign_RSA(key, rsaKey);
        PEM_write_bio_PrivateKey(bio, key, NULL, NULL, 0, NULL, NULL);
    }
    
    BUF_MEM *bptr;
    BIO_get_mem_ptr(bio, &bptr);
    BIO_set_close(bio, BIO_NOCLOSE); /* So BIO_free() leaves BUF_MEM alone */
    BIO_free(bio);
    NSString *res = [NSString stringWithUTF8String:bptr->data];
    //将PEM格式转换为base64格式
    return [self base64EncodedStringFromPEM:res];
}

+ (NSString *)base64EncodedStringFromPEM:(NSString *)PEMFormat{
    return [[[PEMFormat componentsSeparatedByString:@"-----"] objectAtIndex:2] stringByReplacingOccurrencesOfString:@"\n" withString:@""];
}
+(NSString *)PEMKeyFromBase64:(NSString *)base64Key isPubkey:(BOOL)isPubkey{
    NSMutableString *result = [NSMutableString string];
    if (isPubkey) {
        [result appendString:@"-----BEGIN PUBLIC KEY-----\n"];
    }else{
        [result appendString:@"-----BEGIN RSA PRIVATE KEY-----\n"];
    }
    int count = 0;
    for (int i = 0; i < [base64Key length]; ++i) {
        unichar c = [base64Key characterAtIndex:i];
        if (c == '\n' || c == '\r') {
            continue;
        }
        [result appendFormat:@"%c", c];
        if (++count == 64) {
            [result appendString:@"\n"];
            count = 0;
        }
    }
    if (isPubkey) {
        [result appendString:@"\n-----END PUBLIC KEY-----"];
    }else{
        [result appendString:@"\n-----END RSA PRIVATE KEY-----"];
    }
    return result;
}
+ (RSA *)rsaFromBase64:(NSString *)base64Key isPubkey:(BOOL)isPubkey{
    NSString *result = [self PEMKeyFromBase64:base64Key isPubkey:isPubkey];
    return [self rsaFromPEM:result isPubkey:isPubkey];
}

#pragma mark ---密钥格式转换
+ (RSA *)rsaFromPEM:(NSString *)KeyPEM isPubkey:(BOOL)isPubkey{
    const char *buffer = [KeyPEM UTF8String];
    BIO *keyBio = BIO_new_mem_buf(buffer, (int)strlen(buffer));
    RSA *rsa;
    if (isPubkey) {
        rsa = PEM_read_bio_RSA_PUBKEY(keyBio, NULL, NULL, NULL);
    }else{
        rsa = PEM_read_bio_RSAPrivateKey(keyBio, NULL, NULL, NULL);
    }
    BIO_free_all(keyBio);
    return rsa;
}


    

#pragma mark ---加解密
+ (NSData *)encryptWithPublicKey:(RSA *)publicKey plainData:(NSData *)plainData padding:(RsaPaddingType)padding{
    int paddingSize = 0;
    if (padding == Rsa_PKCS1_PADDING) {
        paddingSize = Rsa_PKCS1_PADDING_SIZE;
    }
    
    int publicRSALength = RSA_size(publicKey);
    double totalLength = [plainData length];
    int blockSize = publicRSALength - paddingSize;
    int blockCount = ceil(totalLength / blockSize);
    size_t publicEncryptSize = publicRSALength;
    NSMutableData *encryptDate = [NSMutableData data];
    for (int i = 0; i < blockCount; i++) {
        NSUInteger loc = i * blockSize;
        int dataSegmentRealSize = MIN(blockSize, totalLength - loc);
        NSData *dataSegment = [plainData subdataWithRange:NSMakeRange(loc, dataSegmentRealSize)];
        char *publicEncrypt = malloc(publicRSALength);
        memset(publicEncrypt, 0, publicRSALength);
        const unsigned char *str = [dataSegment bytes];
        int r = RSA_public_encrypt(dataSegmentRealSize,str,(unsigned char*)publicEncrypt,publicKey,padding);
        if (r < 0) {
            free(publicEncrypt);
            return nil;
        }
        NSData *encryptData = [[NSData alloc] initWithBytes:publicEncrypt length:publicEncryptSize];
        [encryptDate appendData:encryptData];
        
        free(publicEncrypt);
    }
    return encryptDate;
}
    
+ (NSData *)decryptWithPrivateKey:(RSA *)privateKey cipherData:(NSData *)cipherData padding:(RsaPaddingType)padding{
    
    if (!privateKey) {
        return nil;
    }
    if (!cipherData) {
        return nil;
    }
    int privateRSALenght = RSA_size(privateKey);
    double totalLength = [cipherData length];
    int blockSize = privateRSALenght;
    int blockCount = ceil(totalLength / blockSize);
    NSMutableData *decrypeData = [NSMutableData data];
    for (int i = 0; i < blockCount; i++) {
        NSUInteger loc = i * blockSize;
        long dataSegmentRealSize = MIN(blockSize, totalLength - loc);
        NSData *dataSegment = [cipherData subdataWithRange:NSMakeRange(loc, dataSegmentRealSize)];
        const unsigned char *str = [dataSegment bytes];
        unsigned char *privateDecrypt = malloc(privateRSALenght);
        memset(privateDecrypt, 0, privateRSALenght);
        int ret = RSA_private_decrypt(privateRSALenght,str,privateDecrypt,privateKey,padding);
        if(ret >=0){
            NSData *data = [[NSData alloc] initWithBytes:privateDecrypt length:ret];
            [decrypeData appendData:data];
        }
        free(privateDecrypt);
    }
    
    return decrypeData;
}
    
+ (NSData *)encryptWithPrivateRSA:(RSA *)privateKey plainData:(NSData *)plainData padding:(RsaPaddingType)padding{
    
    if (!privateKey) {
        return nil;
    }
    if (!plainData) {
        return nil;
    }
    int paddingSize = 0;
    if (padding == Rsa_PKCS1_PADDING) {
        paddingSize = Rsa_PKCS1_PADDING_SIZE;
    }
    
    int privateRSALength = RSA_size(privateKey);
    double totalLength = [plainData length];
    int blockSize = privateRSALength - paddingSize;
    int blockCount = ceil(totalLength / blockSize);
    size_t privateEncryptSize = privateRSALength;
    NSMutableData *encryptDate = [NSMutableData data];
    for (int i = 0; i < blockCount; i++) {
        NSUInteger loc = i * blockSize;
        int dataSegmentRealSize = MIN(blockSize, totalLength - loc);
        NSData *dataSegment = [plainData subdataWithRange:NSMakeRange(loc, dataSegmentRealSize)];
        char *privateEncrypt = malloc(privateRSALength);
        memset(privateEncrypt, 0, privateRSALength);
        const unsigned char *str = [dataSegment bytes];
        int r = RSA_private_encrypt(dataSegmentRealSize,str,(unsigned char*)privateEncrypt,privateKey,padding);
        if (r < 0) {
            free(privateEncrypt);
            return nil;
        }
        
        NSData *encryptData = [[NSData alloc] initWithBytes:privateEncrypt length:privateEncryptSize];
        [encryptDate appendData:encryptData];
        
        free(privateEncrypt);
    }
    return encryptDate;
    
}
    
+ (NSData *)decryptWithPublicKey:(RSA *)publicKey cipherData:(NSData *)cipherData padding:(RsaPaddingType)padding{
    if (!publicKey) {
        return nil;
    }
    if (!cipherData) {
        return nil;
    }
    
    int publicRSALenght = RSA_size(publicKey);
    double totalLength = [cipherData length];
    int blockSize = publicRSALenght;
    int blockCount = ceil(totalLength / blockSize);
    NSMutableData *decrypeData = [NSMutableData data];
    for (int i = 0; i < blockCount; i++) {
        NSUInteger loc = i * blockSize;
        long dataSegmentRealSize = MIN(blockSize, totalLength - loc);
        NSData *dataSegment = [cipherData subdataWithRange:NSMakeRange(loc, dataSegmentRealSize)];
        const unsigned char *str = [dataSegment bytes];
        unsigned char *publicDecrypt = malloc(publicRSALenght);
        memset(publicDecrypt, 0, publicRSALenght);
        int ret = RSA_public_decrypt(publicRSALenght,str,publicDecrypt,publicKey,padding);
        if(ret < 0){
            free(publicDecrypt);
            return nil ;
        }
        NSData *data = [[NSData alloc] initWithBytes:publicDecrypt length:ret];
        if (padding == Rsa_NO_PADDING) {
            Byte flag[] = {0x00};
            NSData *startData = [data subdataWithRange:NSMakeRange(0, 1)];
            if ([[startData description] isEqualToString:@"<00>"]) {
                NSRange startRange = [data rangeOfData:[NSData dataWithBytes:flag length:1] options:NSDataSearchBackwards range:NSMakeRange(0, data.length)];
                NSUInteger s = startRange.location + startRange.length;
                if (startRange.location != NSNotFound && s < data.length) {
                    data = [data subdataWithRange:NSMakeRange(s, data.length - s)];
                }
            }
        }
        [decrypeData appendData:data];
        
        free(publicDecrypt);
    }
    return decrypeData;
}

@end

验签参考HustBroventure的文章

-(HBRSAHandler *)handler{
    if (!_handler ) {
        if ( _severPubKey) {
            self.handler = [HBRSAHandler new];
            //导入客户端私钥用于签名
            // importKeyWithType: andkeyString: 要求导入的keystr是PEM格式。否则不能正确生成(RSA*)证书
            NSString *privatePEMKey = [LPXRSATool PEMKeyFromBase64:_privateKeyBase64 isPubkey:NO];
            [_handler importKeyWithType:KeyTypePrivate andkeyString:privatePEMKey];
            
            //导入服务端公钥用于验签
            // importKeyWithType: andkeyString: 要求导入的keystr是PEM格式。否则不能正确生成(RSA*)证书
            NSString *severPubPEMKey = [LPXRSATool PEMKeyFromBase64:_severPubKey isPubkey:YES];
            
            [_handler importKeyWithType:KeyTypePublic andkeyString:severPubPEMKey];
            return _handler;
        }else{
            AppLog(@"签名handler 获取失败");
            return nil;
        }
    }
    return _handler;
    
}

客户端发送请求时,将特定参数用cPriKey签名,将”真正请求参数“用RandKey进行AES256进行加密。

        //data字段内容进行AES加密,再二进制转十六进制(bin2hex)
        NSString *aesData = [MyCommonCrypto AES256EncryptWithContent:[NSString jsonStrFromDictionary:params] andKey:dataManger.randKey];
        [p setObject:[NSString hexStringFormBase64String:aesData] forKey:@"data"];
        
        //请求参数签名
        NSString *sortStr_p = [NSString sortDictionary:p];
        NSString* signStr_p = [dataManger.handler signString:sortStr_p];
        [p setObject:[NSString hexStringFormBase64String:signStr_p] forKey:@"sign"];
        
NSString *sortStr_r = [NSString sortDictionary:reDic];
NSString* signStr_r = result[@"sign"];
//服务端的signStr是签名后的data转16进制字符串,反向signStr转data
NSData *signData_r = [NSString convertHexStrToData:signStr_r];
//verifyString:withSign: 方法的sing参数是data的base64格式
NSString *signBase64str = [signData_r base64EncodedStringWithOptions:0];
                        
if ([dataManger.handler verifyString:sortStr_r withSign:signBase64str]) {
     AppLog(@"验签成功")
     // 将16进制字符串转为NSData
     NSData *resData = [NSString convertHexStrToData:responseData];
     NSData *deData = [MyCommonCrypto AES256DecryptWithContent:resData andKey:dataManger.randKey];
     NSString *base64String = [[NSString alloc]initWithData:deData encoding:NSUTF8StringEncoding];
     NSDictionary *resDic = [NSString  dictinaryFromJsonStr:base64String];
     finshed(YES,resDic);
}else{
     AppLog(@"验签失败")
     finshed(NO,nil);
}
总结:

过程中遇到不少的坑,特别是和服务端互相验签,由于RSA签名后的数据OC是NSData形式、Java是byte[]形式。也就是数据流。HTTP传输过程中是不能用直接用这种形式的。一般第三方封装的方法会把数据转换为字符串形式。我们服务端用的是16进制字符串的形式,oc这边是base64形式。
故:请求时把签名好的base64String转换成hexString。回执验签时把待验签字符串从hexString转换成base64String。

更新:

开发过程中可能会遇到
PEM_read_bio_RSAPrivateKey() return NULL
PEM_read_bio_RSA_PUBKEY() return NULL
需要注意,这里bio中data需要是PEM格式密钥字符串。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,874评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,102评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,676评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,911评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,937评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,935评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,860评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,660评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,113评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,363评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,506评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,238评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,861评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,486评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,674评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,513评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,426评论 2 352

推荐阅读更多精彩内容