最近做跨境支付类项目,安全要求等级比较高。数据加密验签流程比较复杂。先做一个复盘。
工作流程:
- App创建RSA密钥对,将公钥(cPubKey)和IMEI码发送给服务器,私钥(cPriKey)保存本地。
- 服务器根据IMEI也创建RSA密钥对和一个32位随机码(RandKey)将私钥(serverPriKey)和RandKey根据IMEI码保存在服务端。返回给客户端服务器公钥(serverPubKey)和用cPubKey加密的RandKey, 客户端用cPriKey解密RandKey保存。
完成1、2两步后:服务器和客户端双方都保存对方的公钥和自己私钥及RandKey。通过IMEI号做关联。 - 客户端发送请求时,将特定参数用cPriKey签名,将”真正请求参数“用RandKey进行AES256进行加密。
- 服务端接受请求时,用cPubKey对请求中签名进行验签。验签成功,用RandKey解密”真正请求参数“。
- 服务端返回请求时,将特定参数用serverPriKey签名,将”真正返回数据“用RandKey进行AES256进行加密。
- 客户端接受回执时,用serverPubKey对回执中签名进行验签。验签成功,用RandKey解密”真正返回数据“。
注:iOS不能用真正的IMEI详情参考seventhboy的文章
总结:
- 服务端和客户端分别生成密钥对,通过IMEI码进行绑定。保证每个用户和服务器之间的秘钥都是单独对应的。
- 双方都保存对方的公钥和自己私钥及RandKey。(私钥签名、公钥验签,公钥加密、私钥解密)
- 用自己私钥签名,将数据发送对方;收到对方签名过的数据后,用对方共钥验签。
- 用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
-(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格式密钥字符串。