老项目里关于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