對于RSA加解密來說,一般就是公鑰加密、私鑰解密;或者私鑰加密、公鑰驗簽。并且在iOS的API中同樣也是提供了這兩種形式的方法。
SecKeyEncrypt(加密)
SecKeyDecrypt(解密)
SecKeyRawVerify(驗簽)
但是在做RSA加密的時候,客戶端保存的只會有一個公鑰,有些時候會需要進行公鑰解密。后來查了一些資料最后還是找到了解決方案:通過 openssl 來實現(xiàn)。
RSA_public_encrypt
RSA_private_encrypt
RSA_public_decrypt
RSA_private_decrypt
更新: 在后面的深入了解之后發(fā)現(xiàn),其實iOS原生也能做私鑰加密和公鑰加密。
iOS原生的兩個方法其實說的是加密和解密,并沒有指定是用那個密鑰做加解密。先大致看下RSA加解密的原理:
1.密鑰組成
公鑰 (E,N)
私鑰 (E,D,N)
關(guān)于每個數(shù)是怎么來的這里不介紹,感興趣的可以自己查一下,這里只做加解密的原理解釋。
2.加解密原理
現(xiàn)在已經(jīng)有了密鑰對,那再看下加解密的過程
密文 = 明文E mod N
明文 = 密文D mod N
我們通過一個具體的例子來直觀體驗下,經(jīng)過計算我們現(xiàn)在得到一對具體的密鑰對:
公鑰=(E,N) = (5,323)
私鑰=(D,N) = (29,323)
B = AE mod N = pow(123, 5) % 323 = 225
A = BD mod N = pow(225, 29) % 323 = 123
如果 A(123) 為明文,那上面的過程就是 公鑰加密私鑰解密;
如果 B(225) 為明文,那上面的過程就是 私鑰加密公鑰解密;
換一下順序可能會更清除一點:
A = BD mod N = pow(225, 29) % 323 = 123 (私鑰加密)
B = AE mod N = pow(123, 5) % 323 = 225 (公鑰解密)
這樣一來我們就會發(fā)現(xiàn),其實加解密是同一個方法。那為什么會有加密和解密兩個方法呢?我的理解是:
加密就是,傳入數(shù)據(jù)直接做計算(就像上面的那樣)
解密就是,傳入數(shù)據(jù)直接做計算(還是上面的那樣),不過會根據(jù)填充模式做數(shù)據(jù)處理,把填充的隨機數(shù)剔除掉。
3.填充格式
一般最常用的是 PKCS1。這個填充格式會要求每次加密的數(shù)據(jù)比密鑰長度短11個字節(jié)(keySize - 11),輸出長度跟密鑰長度一致
PS 為隨機填充數(shù),M為明文
00 02 | PS | 00 | M (公鑰加密)
00 01 | PS | 00 | M (私鑰加密)
還有一個是None就是不填充。這個的密文長度可以跟密鑰一樣長,但是如果比密鑰短的話會在明文的前面填充零(0)
0000 | M
| 填充方式 | 最大輸入長度 | 輸出長度 | 填充內(nèi)容 |
|---|---|---|---|
| PKCS1 | keySize - 11 | keySize | 隨機數(shù) |
| none | keySize | keySize | 00 |
4.公鑰解密
現(xiàn)在知道了加密的原理和填充方式,那試一下
- (NSData * )decryptWithPublicKey:(SecKeyRef)publicKeyRef cipherData:(NSData * )cipherData {
size_t keySize = SecKeyGetBlockSize(publicKeyRef) * sizeof(uint8_t);
double totalLength = [cipherData length];
size_t blockSize = keySize;
int blockCount = ceil(totalLength / blockSize);
NSMutableData * plainData = [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)];
unsigned char * plainBuffer = malloc(keySize);
memset(plainBuffer, 0, keySize);
OSStatus status = noErr;
size_t plainBufferSize ;
status = SecKeyDecrypt(publicKeyRef,
kSecPaddingNone, // 解密的時候一定要用 無填充模式,拿到所有的數(shù)據(jù)自行解析
[dataSegment bytes],
dataSegmentRealSize,
plainBuffer,
&plainBufferSize
);
if(status != noErr){
free(plainBuffer);
return nil;
}
NSData * data = [[NSData alloc] initWithBytes:plainBuffer length:plainBufferSize];
NSData * startData = [data subdataWithRange:NSMakeRange(0, 1)];
// 開頭應(yīng)該是 0001 但是原生解出來之后把開頭的 00 忽略了
if ([[startData description] isEqualToString:@"<01>"]) {
Byte flag[] = {0x00};
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)];
}
}
[plainData appendData:data];
free(plainBuffer);
}
return plainData;
}
這里有幾個需要注意的點
1、 填充方式一定要為kSecPaddingNone,因為我們不希望系統(tǒng)幫我們做數(shù)據(jù)的截取,默認(rèn)解出來的所有數(shù)據(jù)都是我們需要的
2、當(dāng)加密的填充模式為PKCS1 ,理論上開頭應(yīng)該是 0001 的,但是NSData 會把開頭的 00 忽略掉。所以開頭就成了01。
3、 當(dāng)加密的填充模式為None, 理論上填充的應(yīng)該都是 0 的,同樣NSData 會把開頭的 00 忽略掉,所以解出來的就是真實的數(shù)據(jù)不用處理。
5.私鑰加密
既然說到公鑰解密就順便說一下私鑰加密,原生這個并沒有這幾的私鑰解密,即使傳入私鑰使用解密的方法也不行。但是可以通過另外一條思路來實現(xiàn)。
在上面的原理介紹中提到加密解密其實用的是同一個方法,所以我們可以使用解密的方法來進行加密。實驗證明是可以的。
但是要注意
1、填充模式要設(shè)置為kSecPaddingNone。因為這個畢竟是一個解密的方法,如果不用kSecPaddingNone內(nèi)部會做數(shù)據(jù)的處理,但是這并不是我們需要的,我們需要的是完整的數(shù)據(jù)。
2、 還因為這里用的是一個解密的方法,所以分段私鑰加密處理之后得到的數(shù)據(jù)長度并不一定等于密鑰的長度,但是在公鑰解密的時候是按密鑰長度進行分段解密的,這樣就是產(chǎn)生異常。處理方案:在私鑰加密之后如果密文長度小于密鑰長度,要在密文前面填充 0 ;
更詳細(xì)的東西可以看Demo。