前言
首先,希望大家耐心點,這個加密我也是弄很久才出來的,辛辛苦苦整理的博客,介紹大概概念就進入正題!
上面是基本介紹,后面是個人經歷的問題及解決方案
,希望你們耐心看,不要踩我進過的坑啦!祝你們都能順利解決問題??
1. 介紹
1.1 AES是什么?
高級加密標準(英語:Advanced Encryption Standard,縮寫:AES),在密碼學中又稱 Rijndael 加密法。AES 是一個對稱分組密碼
算法,旨在取代 DES 成為廣泛使用的標準。
1.2 AES詳解
AES 根據使用密碼長度有三種方案以應對不同的場景要求,分別是 AES-128
、AES-192
和 AES-256
。加密模式有四種,分別是 ECB
(Elecyronic Code Book,電子密碼本)、CBC
(Cipher Block Chaining,加密塊鏈)、CFB
(Cipher FeedBack Mode,加密反饋)、OFB
(Output FeedBack,輸出反饋)。
需要和后臺統一四個東西:
秘鑰長度
、加密模式
、填充方式
、初始向量
(也稱偏移量,ECB模式不需要)。
定義中說到 AES 是一個對稱分組密碼算法,加密原理如圖:
1.3 實現原理和比較
這個就比較深入啦,有興趣的自行查看~
- 實現原理
- 比較
1.4 模式和填充選擇
算法/模式/填充 | 16字節加密后數據長度 | 不滿16字節加密后長度 |
---|---|---|
AES/CBC/NoPadding | 16 | 不支持 |
AES/CBC/PKCS5Padding | 32 | 16 |
AES/CBC/ISO10126Padding | 32 | 16 |
AES/CFB/NoPadding | 16 | 原始數據長度 |
AES/CFB/PKCS5Padding | 32 | 16 |
AES/CFB/ISO10126Padding | 32 | 16 |
AES/ECB/NoPadding | 16 | 不支持 |
AES/ECB/PKCS5Padding | 32 | 16 |
AES/ECB/ISO10126Padding | 32 | 16 |
AES/OFB/NoPadding | 16 | 原始數據長度 |
AES/OFB/PKCS5Padding | 32 | 16 |
AES/OFB/ISO10126Padding | 32 | 16 |
AES/PCBC/NoPadding | 16 | 不支持 |
AES/PCBC/PKCS5Padding | 32 | 16 |
AES/PCBC/ISO10126Padding | 32 | 16 |
PKCS7Padding VS PKCS5Padding:
PKCS5Padding
的 blocksize 為8字節,而 PKCS7Padding
的 blocksize 可以為1到255字節。
需要注意點:
1. iOS只支持PKCS7Padding
填充方式;Java 支持PKCS5Padding
但不支持PKCS7Padding
,不過不要擔心,上面說的區別我也不懂,實際中倒是一樣;
2. node.js 在 AES 加密上和其他語言有略不同,它系統自帶方法對 Key 進行過 MD5 處理。
2. 經驗總結
2.1 加密模式和填充方式的確定
首先,一定要確認使用的加密模式和填充方式!!!因為我接到任務的時候郵件里只給了 key
和 iv
,沒有說清楚,而 iOS
默認的是 CBC
模式,Android
是 CFB
模式一下就 OK 了,我調了半天不行,看了 Android
的代碼才醒悟過來。。。。。。
我的項目中,后臺使用的是
AES/CFB/PKCS7Padding
,Android 使用的是AES/CFB/PKCS5Padding
。
2.2 填充方式的選擇
其次,填充方式的選擇:按照上面來看,我使用 AES/CFB/PKCS7Padding
就可以了哈。
然而,iOS 有的加密方法,只有 CCCryptorCreateWithMode
可以設置除了默認的 CBC 、ECB 之外的其他模式,所以就用它啦,其方法如下:
CCCryptorStatus status = CCCryptorCreateWithMode(operation,
kCCModeCFB,
kCCAlgorithmAES,
ccPKCS7Padding,
iv,
key,
keyStr.length,
NULL,
0,
0,
0,
&cryptor);
這里的 padding
除了 ccPKCS7Padding
,還有 ccNoPadding
不填充兩種選擇。我試了兩個的加密結果是一樣的,使用 ccPKCS7Padding
并沒有自動填充。(可能其他模式可以,總不能有這個還不能用吧。但是我沒有查到 AES/CFB/PKCS7Padding
為什么不填充,如果小伙伴知道還請告知哦!)
所以我就先試試
ccNoPadding
不填充模式。舉例如下:
原字符串:@"hello中國"
原數據 Data:<68656c6c 6fe4b8ad e59bbd>
Java 的PKCS5Padding
方式加密后的字符串:QReiy/Ddik50cXQ=
iOS 的ccNoPadding
加密后的字符串:QReiy/DRrn92WV8=
加密結果當然不一致,下面對 Java 加密字符串解密后進行分析:
iOS 的
ccNoPadding
對 Java 加密字符串解密后 data:<68656c6c 6fe4b8ad e59bbd05 05050505>
iOS 的ccNoPadding
對加密字符串解密后 string:@"hello中國\x05\x05\x05\x05\x05"(這里要注意解密后的字符串在控制臺輸出是沒問題的,但是實際是有多余的,如圖:)
可以看出來,因為一個是PKCS5Padding
,一個是ccNoPadding
會有填充模式上導致的數據差異。相信看過上面幾篇文章的應該明白了。
如果 Java 加密的填充方式也是用
ccNoPadding
,那么解出來的就不會有多余填充了。也就是說應該三方都保持同樣的ccNoPadding
填充方式,我和 Android 測試過(注意 Android 那邊應該是NoPadding
書寫方式)。
但是后臺那邊說多個地方都已經使用 PKCS7Padding
。當然解決辦法還是有的~~~
2.3 選錯填充方式的補救
對此,只能我這邊采取措施和 Android 、后臺保持一致了。
也就是解密后臺給的數據的時,截掉多余的填充;加密傳輸時,加密后,補充需要填充的數據。這里主要是對 NSData
的操作。
注意:加解密的步驟(ase64、URL Encode、有的還有字符串替換)不同公司可能采取方式不同,要對接好。
2.4 代碼思路
在我的項目中,對 NSData 進行填充補位和刪除,我們需要了解 PKCS7Padding
的填充方式:
需要填充的字節長度 = (塊長度 - (數據長度 % 塊長度))
假定塊長度為 8,數據長度為 3,則填充字節數等于 5。
原數據為: FF FF FF
填充結果: FF FF FF 05 05 05 05 05
假定塊長度為 8,數據長度為 8,則填充字節數等于 8。
原數據為: FF FF FF FF FF FF FF FF
填充結果: FF FF FF FF FF FF FF FF 08 08 08 08 08 08 08 08
差多少補多少,不差就補一個塊。
當然我采坑的過程中沒那么順利 ,在此想提醒大家這種不熟悉的任務一定要多批量復雜數據,不能簡簡單單測試簡單少量數據就行了,早發現早解決。
該處理已在項目中使用上線,使用正常,沒有問題。
2.5 加密方法的實現
2.5.1 NSData 擴展實現加密解密
NSData+EHIExtension.h:
#import <Foundation/Foundation.h>
#include <CommonCrypto/CommonCrypto.h>
interface NSData (EHIExtension)
/** AES解密:CFB模式 */
- (NSData *)aes256ByCFBModeWithOperation:(CCOperation)operation key:(NSString *)keyStr iv:(NSString *)ivStr;
end
NSData+EHIExtension.m:
#import "NSData+EHIExtension.h"
/** AES加密位數 */
static NSInteger const kEHIAESMode = 16;
@implementation NSData (EHIExtension)
/** AES解密:CFB模式 */
- (NSData *)aes256ByCFBModeWithOperation:(CCOperation)operation key:(NSString *)keyStr iv:(NSString *)ivStr {
NSData *originData = self;
if (operation == kCCEncrypt) {
// 加密:位數不夠的補全
originData = [self fullData:originData mode:kEHIAESMode];
}
const char *iv = [[ivStr dataUsingEncoding:NSUTF8StringEncoding] bytes];
const char *key = [[keyStr dataUsingEncoding:NSUTF8StringEncoding] bytes];
// 加密/解密
CCCryptorRef cryptor = NULL;
CCCryptorStatus status = CCCryptorCreateWithMode(operation,
kCCModeCFB,
kCCAlgorithmAES,
ccNoPadding,
iv,
key,
keyStr.length,
NULL,
0,
0,
0,
&cryptor);
if (status != kCCSuccess) {
NSLog(@"AES加密/解密失敗 error: %@", @(status));
return nil;
}
// 輸出加密/解密數據
NSUInteger inputLength = originData.length;
char *outData = malloc(inputLength);
memset(outData, 0, inputLength);
size_t outLength = 0;
CCCryptorUpdate(cryptor, originData.bytes, inputLength, outData, inputLength, &outLength);
NSData *resultData = [NSData dataWithBytes:outData length:outLength];
CCCryptorRelease(cryptor);
free(outData);
if (operation == kCCDecrypt) {
// 解密:位數多的刪除
resultData = [self deleteData:resultData mode:kEHIAESMode];
}
return resultData;
}
/** 加密:位數不夠的補全
補位規則:1.length=13,補5位05
2.length=16,補16位ff */
- (NSData *)fullData:(NSData *)originData mode:(NSUInteger)mode {
NSMutableData *tmpData = [[NSMutableData alloc] initWithData:originData];
// 確定要補全的個數
NSUInteger shouldLength = mode * ((tmpData.length / mode) + 1);
NSUInteger diffLength = shouldLength - tmpData.length;
uint8_t *bytes = malloc(sizeof(*bytes) * diffLength);
for (NSUInteger i = 0; i < diffLength; i++) {
// 補全缺失的部分
bytes[i] = diffLength;
}
[tmpData appendBytes:bytes length:diffLength];
return tmpData;
}
/** 解密:位數多的刪除
刪位規則:最后一位數字在1-16之間,且連續n位相同n數字 */
- (NSData *)deleteData:(NSData *)originData mode:(NSUInteger)mode {
NSMutableData *tmpData = [[NSMutableData alloc] initWithData:originData];
Byte *bytes = (Byte *)tmpData.bytes;
Byte lastNo = bytes[tmpData.length - 1];
if (lastNo >= 1 && lastNo <= mode) {
NSUInteger count = 0;
// 確定多余的部分正確性
for (NSUInteger i = tmpData.length - lastNo; i < tmpData.length; i++) {
if (lastNo == bytes[i]) {
count ++;
}
}
if (count == lastNo) {
// 截取正常的部分
NSRange replaceRange = NSMakeRange(0, tmpData.length - lastNo);
return [tmpData subdataWithRange:replaceRange];
}
}
return originData;
}
2.5.2 NSString 擴展實現使用過程
NSString+EHIAES.h:
#import <Foundation/Foundation.h>
interface NSString (EHIAES)
/** AES解密 */
- (NSString *)aes256Decrypt;
/** AES加密 */
- (NSString *)aes256Encrypt;
@end
NSString+EHIAES.m:
#import "NSString+EHIAES.h"
#import "NSString+YYAdd.h"
#import "NSData+YYAdd.h"
#import "NSData+EHIExtension.h"
#import "GTMBase64.h"
/** AES加密:key */
static NSString * const kAESKey = @""; // 32位
/** AES加密:iv */
static NSString * const kAESIv = @""; // 16位
@implementation NSString (EHIAES)
/** AES解密 */
- (NSString *)ehi_aes256Decrypt {
// 1.Base64 Decode
NSData *base64DecodeData = [NSData dataWithBase64EncodedString:self];
// 1.Aes256 解密
NSData *decodeData = [base64DecodeData aes256ByCFBModeWithOperation:kCCDecrypt key:kAESKey iv:kAESIv];
NSString *decodeStr = [[NSString alloc] initWithData:decodeData encoding:NSUTF8StringEncoding];
if ([NSString isNilOrEmpty:decodeStr]) {
// 解密失敗
return nil;
}
return decodeStr;
}
/** AES加密 */
- (NSString *)ehi_aes256Encrypt {
NSData *originData = [self dataUsingEncoding:NSUTF8StringEncoding];
// 1.Aes256 加密
NSData *encodeData = [originData aes256ByCFBModeWithOperation:kCCEncrypt key:kAESKey iv:kAESIv];
// 2.Base64 Encode
NSString *base64EncodeStr = [encodeData base64EncodedString];
return base64EncodeStr;
}
@end