老项目里关于AES埋的坑
起因
不了解上下文的情况下,让帮忙确认一下某个方法是否解密成功,结果解密失败,不清楚具体原因,解密流程和项目里的以前的代码类似,但是不清楚以前对接的场景是啥
原生这么的解密流程是将收到的16进制字符串转成NSData,然后调用YYCategories中的aes256DecryptWithkey方法,初始化向量iv传入的是nil
kCCAlgorithmAES128 实际上会根据密钥长度自动选择 AES-128/192/256
// YYCategories / NSData+YYAdd / AES256解密方法
// (虽然方法名写的是AES256,实际上用的是AES128)
- (NSData *)aes256DecryptWithkey:(NSData *)key iv:(NSData *)iv {
if (key.length != 16 && key.length != 24 && key.length != 32) {
return nil;
}
if (iv.length != 16 && iv.length != 0) {
return nil;
}
NSData *result = nil;
size_t bufferSize = self.length + kCCBlockSizeAES128;
void *buffer = malloc(bufferSize);
if (!buffer) return nil;
size_t encryptedSize = 0;
CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding,
key.bytes,
key.length,
iv.bytes,
self.bytes,
self.length,
buffer,
bufferSize,
&encryptedSize);
if (cryptStatus == kCCSuccess) {
result = [[NSData alloc]initWithBytes:buffer length:encryptedSize];
free(buffer);
return result;
} else {
free(buffer);
return nil;
}
}
后来了解到和前端对接时,有提到这么一段JS的代码
CryptoJS.AES.decrypt('U2FsdGVkX1+PfFsPk+YR/zICHoPmTTo1A2BdIGpi5eI=', 'my-secret-key').toString(CryptoJS.enc.Utf8)
聊了一下发现,前端真的是’my-secret-key’这个key来做加密的,而app里是用以前默认内置的key来解密的,2个key完全不一样
但是试了一下原生这么用’my-secret-key’作为key也解密不出来,细看了一下,貌似传过来的不是16进制字符串,这里应该用base64解码一下,可能和以前的对接场景不同,试了一下,发现base64后解密还是失败的,尴尬了,陷入僵局
了解一下CryptoJS.AES.decrypt这段代码的具体执行流程吧,
- 第一个参数: ‘U2FsdGVkX1+PfFsPk+YR/zICHoPmTTo1A2BdIGpi5eI=’
Base64 编码的密文,开头 U2FsdGVkX1 解码后是 Salted__,这是 OpenSSL 兼容格式的标志,表示这个加密数据包含了 盐 (Salt)。
完整的数据结构通常是:Salted__ + [8字节Salt] + [密文]
加密时使用了盐,并且很可能使用了 EVP_BytesToKey 函数从密码生成密钥和 IV - 第二个参数: ‘my-secret-key’
因为 CryptoJS 的 AES.decrypt 方法在检测到 Salted__ 格式时,会自动使用密码(password)而不是密钥(key)来处理
CryptoJS 会自动提取盐,使用 EVP_BytesToKey 算法(默认 MD5 哈希,1 次迭代)从密码和盐中派生出密钥和 IV,然后进行解密
而原生这么直接把’my-secret-key’当成密钥使用,当然会解密失败
可以用的AES256解密方法
// aes256解密
- (NSString *)aes256Decrypt:(NSString *)ciphertext withPassword:(NSString *)password {
if (ciphertext.length == 0 || password.length == 0) {
return nil;
}
NSData *cipherData = [[NSData alloc] initWithBase64EncodedString:ciphertext options:0];
if (!cipherData) {
return nil;
}
const char saltHeader[] = "Salted__"; // OpenSSL salted prefix
NSData *decrypted = nil;
if (cipherData.length > 16 && memcmp(cipherData.bytes, saltHeader, 8) == 0) {
// OpenSSL salted format: "Salted__" + 8 bytes salt + encrypted
NSData *salt = [cipherData subdataWithRange:NSMakeRange(8, 8)];
NSData *encData = [cipherData subdataWithRange:NSMakeRange(16, cipherData.length - 16)];
// Derive key and IV using OpenSSL EVP_BytesToKey (MD5)
NSMutableData *derived = [NSMutableData data];
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *previous = nil;
while (derived.length < (kCCKeySizeAES256 + kCCBlockSizeAES128)) {
// MD5(previous + password + salt) using one-shot CC_MD5; wrap with pragma to silence deprecation diagnostics
NSMutableData *mdInput = [NSMutableData data];
if (previous) {
[mdInput appendData:previous];
}
[mdInput appendData:passwordData];
[mdInput appendData:salt];
unsigned char md[CC_MD5_DIGEST_LENGTH];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
CC_MD5(mdInput.bytes, (CC_LONG)mdInput.length, md);
#pragma clang diagnostic pop
previous = [NSMutableData dataWithBytes:md length:CC_MD5_DIGEST_LENGTH];
[derived appendBytes:md length:CC_MD5_DIGEST_LENGTH];
}
NSData *key = [derived subdataWithRange:NSMakeRange(0, kCCKeySizeAES256)];
NSData *iv = [derived subdataWithRange:NSMakeRange(kCCKeySizeAES256, kCCBlockSizeAES128)];
size_t outLength = encData.length + kCCBlockSizeAES128;
void *outBuf = malloc(outLength);
size_t numBytesDecrypted = 0;
CCCryptorStatus status = CCCrypt(kCCDecrypt,
kCCAlgorithmAES,
kCCOptionPKCS7Padding,
key.bytes,
key.length,
iv.bytes,
encData.bytes,
encData.length,
outBuf,
outLength,
&numBytesDecrypted);
if (status == kCCSuccess) {
decrypted = [NSData dataWithBytesNoCopy:outBuf length:numBytesDecrypted freeWhenDone:YES];
} else {
free(outBuf);
return nil;
}
} else {
// Fallback: assume raw AES256-CBC with key = SHA256(password), iv = zeros
NSData *encData = cipherData;
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
unsigned char keybuf[CC_SHA256_DIGEST_LENGTH];
CC_SHA256(passwordData.bytes, (CC_LONG)passwordData.length, keybuf);
unsigned char ivZero[kCCBlockSizeAES128];
memset(ivZero, 0, sizeof(ivZero));
size_t outLength = encData.length + kCCBlockSizeAES128;
void *outBuf = malloc(outLength);
size_t numBytesDecrypted = 0;
CCCryptorStatus status = CCCrypt(kCCDecrypt,
kCCAlgorithmAES,
kCCOptionPKCS7Padding,
keybuf,
kCCKeySizeAES256,
ivZero,
encData.bytes,
encData.length,
outBuf,
outLength,
&numBytesDecrypted);
if (status == kCCSuccess) {
decrypted = [NSData dataWithBytesNoCopy:outBuf length:numBytesDecrypted freeWhenDone:YES];
} else {
free(outBuf);
return nil;
}
}
if (!decrypted) return nil;
NSString *plaintext = [[NSString alloc] initWithData:decrypted encoding:NSUTF8StringEncoding];
return plaintext;
}
为什么ECB模式用CBC也解密出来了
后来安卓的哥们和服务端沟通时说用ECB模式,但是iOS这边用以前老的方法也能解密出来,很奇怪,明明老的方法用的是CBC,怎么能解密出来呢
一句话真因
加密方式需要充分沟通呀
Written on September 20, 2025
