【问题标题】:Equivalent of spongycastle encryption for ios等效于 ios 的 spongycastle 加密
【发布时间】:2013-04-29 03:32:05
【问题描述】:

这让我很难过——下面的代码使用 SpongyCastle 的 Android 加密/解密——我正在尝试实现 iOS 的跨平台加密/解密。

以下代码(来自Android)使用提供的盐和密码(来自Android)工作,AES 128bit CBC和PKCS7Padding,盐存储在mysql数据库中,密码由最终用户提供,以下代码是改编自kelhoer的这个答案。

我使用 AES128bit 的原因是 AES256 在 iOS 4+ 中不可用,它是在 iOS5+ 中引入的,并且不得不使用openssl 生成派生密钥和初始化向量 (iv),它是冒险得知 Apple 拒绝与 openssl 库静态链接的应用程序。

由于该平台基于 iOS 4.2+,使用 bundling and statically linking the openssl 库似乎有点过头了,最好使用 CommonCryptor 库。

这是带有 Spongycastle 代码的 Android 版本:

private static void encrypt(InputStream fin, 
    OutputStream fout, 
    String password, 
    byte[] bSalt) {
    try {
        PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(
            new SHA256Digest()
            );
        char[] passwordChars = password.toCharArray();
        final byte[] pkcs12PasswordBytes = 
            PBEParametersGenerator.PKCS12PasswordToBytes(passwordChars);
        pGen.init(pkcs12PasswordBytes, bSalt, ITERATIONS);
        CBCBlockCipher aesCBC = new CBCBlockCipher(new AESEngine());
        ParametersWithIV aesCBCParams = 
            (ParametersWithIV) pGen.generateDerivedParameters(128, 128);
        aesCBC.init(true, aesCBCParams);
        PaddedBufferedBlockCipher aesCipher = 
            new PaddedBufferedBlockCipher(aesCBC, new PKCS7Padding());
        aesCipher.init(true, aesCBCParams);
        byte[] buf = new byte[BUF_SIZE];
        // Read in the decrypted bytes and write the cleartext to out
        int numRead = 0;
        while ((numRead = fin.read(buf)) >= 0) {
            if (numRead == 1024) {
                byte[] plainTemp = new byte[
                    aesCipher.getUpdateOutputSize(numRead)];
                int offset = 
                    aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
                final byte[] plain = new byte[offset];
                System.arraycopy(plainTemp, 0, plain, 0, plain.length);
                fout.write(plain, 0, plain.length);
            } else {
                byte[] plainTemp = new byte[aesCipher.getOutputSize(numRead)];
                int offset = 
                    aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
                int last = aesCipher.doFinal(plainTemp, offset);
                final byte[] plain = new byte[offset + last];
                System.arraycopy(plainTemp, 0, plain, 0, plain.length);
                fout.write(plain, 0, plain.length);
            }
        }
        fout.close();
        fin.close();
    } catch (Exception e) {
        e.printStackTrace();
    }

}

private static void decrypt(InputStream fin, 
    OutputStream fout, 
    String password, 
    byte[] bSalt) {
    try {
        PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(
            new SHA256Digest()
            );
        char[] passwordChars = password.toCharArray();
        final byte[] pkcs12PasswordBytes = 
            PBEParametersGenerator.PKCS12PasswordToBytes(passwordChars);
        pGen.init(pkcs12PasswordBytes, bSalt, ITERATIONS);
        CBCBlockCipher aesCBC = new CBCBlockCipher(new AESEngine());
        ParametersWithIV aesCBCParams = 
            (ParametersWithIV) pGen.generateDerivedParameters(128, 128);
        aesCBC.init(false, aesCBCParams);
        PaddedBufferedBlockCipher aesCipher = 
            new PaddedBufferedBlockCipher(aesCBC, new PKCS7Padding());
        aesCipher.init(false, aesCBCParams);
        byte[] buf = new byte[BUF_SIZE];
        // Read in the decrypted bytes and write the cleartext to out
        int numRead = 0;
        while ((numRead = fin.read(buf)) >= 0) {
            if (numRead == 1024) {
                byte[] plainTemp = new byte[
                    aesCipher.getUpdateOutputSize(numRead)];
                int offset = 
                    aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
                // int last = aesCipher.doFinal(plainTemp, offset);
                final byte[] plain = new byte[offset];
                System.arraycopy(plainTemp, 0, plain, 0, plain.length);
                fout.write(plain, 0, plain.length);
            } else {
                byte[] plainTemp = new byte[
                    aesCipher.getOutputSize(numRead)];
                int offset = 
                    aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
                int last = aesCipher.doFinal(plainTemp, offset);
                final byte[] plain = new byte[offset + last];
                System.arraycopy(plainTemp, 0, plain, 0, plain.length);
                fout.write(plain, 0, plain.length);
            }
        }
        fout.close();
        fin.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

但是在 iOS 4.2(使用 XCode)下,我无法弄清楚如何做等效的,

这是我在Objective C下尝试过的,目的是从Android端解密存储在mysql数据库中的数据,以进行测试:

+(NSData*) decrypt:(NSData*)cipherData 
    userPassword:(NSString*)argPassword 
    genSalt:(NSData*)argPtrSalt{

    size_t szPlainBufLen = cipherData.length + (kCCBlockSizeAES128);
    uint8_t *ptrPlainBuf = malloc(szPlainBufLen);
    //
    const unsigned char *ptrPasswd = 
        (const unsigned char*)[argPassword 
            cStringUsingEncoding:NSASCIIStringEncoding];
    int ptrPasswdLen = strlen(ptrPasswd);
    //
    NSString *ptrSaltStr = [[NSString alloc]
        initWithData:argPtrSalt 
        encoding:NSASCIIStringEncoding];

    const unsigned char *ptrSalt = 
        (const unsigned char *)[ptrSaltStr UTF8String];
    NSString *ptrCipherStr = 
        [[NSString alloc]initWithData:cipherData 
            encoding:NSASCIIStringEncoding];
    unsigned char *ptrCipher = (unsigned char *)[ptrCipherStr UTF8String];
    unsigned char key[kCCKeySizeAES128];
    unsigned char iv[kCCKeySizeAES128];
    //
    //int     EVP_BytesToKey(const EVP_CIPHER *type,const EVP_MD *md,
    //const unsigned char *salt, const unsigned char *data,
    //int datal, int count, unsigned char *key,unsigned char *iv);
    int i = EVP_BytesToKey(EVP_aes_128_cbc(), 
                       EVP_sha256(), 
                       ptrSalt, 
                       ptrPasswd, 
                       ptrPasswdLen, 
                       PBKDF2_ITERATIONS, 
                       key, 
                       iv);
    NSAssert(i == kCCKeySizeAES128, 
        @"Unable to generate key for AES");
    //
    size_t cipherLen = [cipherData length];
    size_t outlength = 0;
    //
    CCCryptorStatus resultCCStatus = CCCrypt(kCCDecrypt,
                                             kCCAlgorithmAES128,
                                             kCCOptionPKCS7Padding,
                                             key,
                                             kCCBlockSizeAES128,
                                             iv,
                                             ptrCipher,
                                             cipherLen,
                                             ptrPlainBuf,
                                             szPlainBufLen,
                                             &outlength);
    NSAssert(resultCCStatus == kCCSuccess, 
        @"Unable to perform PBE AES128bit decryption: %d", errno);
    NSData *ns_dta_PlainData = nil;

    if (resultCCStatus == kCCSuccess){
        ns_dta_PlainData = 
        [NSData dataWithBytesNoCopy:ptrPlainBuf length:outlength];
    }else{
        return nil;
    }
    return ns_dta_PlainData;
}

已提供数据和用户密码,并从CCCrypt 获得返回码为-4304,表示解码不成功和错误。

我认为编码方案可能会抛弃 CommonCryptor 的解密路由,因此转换为 NSASCIIStringEncoding 的方式很冗长。

Salt 与密码数据一起存储,长度为 32 字节。

请记住,我在这方面缺少什么,我在密码学方面很薄弱。

【问题讨论】:

  • 如果您想要兼容性,我认为您在尝试在 iOS 和 android 之间使用不同的代码库时会遇到麻烦。为什么不直接找到一个 C/C++ AES 实现并将其编译到两个平台上的代码库中?

标签: android ios commoncrypto spongycastle


【解决方案1】:

是的,我不得不放弃 Android 端的加密算法,这是一个挑战,以找到一个跨平台兼容的算法。

我已经阅读了很多关于 Rob Napier's RNCryptor 的内容,在谷歌搜索了一个 Android 等效项后,我在其中找到了 JNCryptor,我冒险并在 iOS 端使用了 RNCryptor。

github 上分叉了 JNCryptor 代码,以添加能够指定自定义设置的增强功能,并为旧版本的 Android 使用 SpongyCastle。从那时起,两个平台都能够互换加密/解密。

我增强 JNCryptor 的原因是 PKDBF2 函数的迭代次数太高 - 10,000 并且是默认值(因为代码将在旧手机上运行 - 它卡住了 - 如果你有双核/四核,那就太好了!),并且需要覆盖迭代计数以更“可忍受” - 1,000。 RNCryptor 可以使用自定义设置。

感谢 Rob Napier 和 Duncan Jones 的工作!

【讨论】:

  • 您好,我也想实现在 Android 平台上使用 Spongycastle 开发的相同功能。我应该为此使用 RNCryptor 吗?提前感谢您帮助我解决这个问题。
【解决方案2】:

我冒昧地编写了在 Android 端使用的 PKCS12Parameters generator 的直接端口,此标头的要点在上面。

实现也是直接复制,如发现here,密码,被转换为等效的PKCS12 - unicode,big-endian,最后有两个额外的零。

生成器生成派生密钥和 iv 通过执行迭代次数,在本例中为 1000,就像在 Android 端一样,使用 SHA256 摘要,最终生成的密钥和 iv 是然后用作CCCryptorCreate的参数。

使用以下代码示例也不起作用,它在调用 CCCryptorFinal 时以 -4304 结尾

代码摘录如图:

#define ITERATIONS 1000

PKCS12ParametersGenerator *pGen = [[PKCS12ParametersGenerator alloc]
        init:argPassword 
        saltedHash:argPtrSalt 
        iterCount:ITERATIONS 
        keySize:128 
        initVectSize:128]; 
//
[pGen generateDerivedParameters];
//
CCCryptorRef decryptor = NULL;
// Create and Initialize the crypto reference.
CCCryptorStatus ccStatus = CCCryptorCreate(kCCDecrypt,
                           kCCAlgorithmAES128,
                           kCCOptionPKCS7Padding,
                           pGen.derivedKey.bytes,
                           kCCKeySizeAES128,
                           pGen.derivedIV.bytes,
                           &decryptor
                           );
NSAssert(ccStatus == kCCSuccess, 
    @"Unable to initialise decryptor!");
//
size_t szPlainBufLen = cipherData.length + (kCCBlockSizeAES128);

// Calculate byte block alignment for all calls through to and including final.
size_t szPtrPlainBufSize = CCCryptorGetOutputLength(decryptor, szPlainBufLen, true);
uint8_t *ptrPlainBuf = calloc(szPtrPlainBufSize, sizeof(uint8_t));
//
// Set up initial size.
size_t remainingBytes = szPtrPlainBufSize;
uint8_t *ptr = ptrPlainBuf;
size_t movedBytes = 0;
size_t totalBytesWritten = 0;

// Actually perform the encryption or decryption.
ccStatus = CCCryptorUpdate(decryptor,
                           (const void *) cipherData.bytes,
                           szPtrPlainBufSize,
                           ptr,
                           remainingBytes,
                           &movedBytes
                           );
NSAssert(ccStatus == kCCSuccess, 
    @"Unable to update decryptor! Error: %d", ccStatus);
ptr += movedBytes;
remainingBytes -= movedBytes;
totalBytesWritten += movedBytes;
//
// Finalize everything to the output buffer.
CCCryptorStatus resultCCStatus = CCCryptorFinal(decryptor,
                          ptr,
                          remainingBytes,
                          &movedBytes
                          );

totalBytesWritten += movedBytes;

if(decryptor) {
    (void) CCCryptorRelease(decryptor);
    decryptor = NULL;
}

NSAssert(resultCCStatus == kCCSuccess, 
    @"Unable to perform PBE AES128bit decryption: %d", resultCCStatus);

有趣的是,解密有效,如果我在CCCryptorCreate 的开头用kCCOptionPKCS7Padding 替换0x0000,则对CCCryptorFinal 的最终调用返回0,即没有填充。唉,数据不是我所期望的,不管什么时候“不起作用”,仍然完全乱码。

它在某个地方失败了,所以如果有人对如何实现等效有更好的想法,我会很高兴听到其他意见。

要么更改 Android 端的机制以使其“跨平台”与 iPhone 兼容,要么寻求替代加密解决方案以在两端兼容,但代价是双方的加密都较弱用于使数据交换可移植的平台的侧面。

提供的输入数据:

  • Base64 编码密码,盐和密码用':'分隔tnNhKyJ2vvrUzAmtQV5q9uEwzzAH63sTKtLf4pOQylw=:qTBluA+aNeFnEUfkUFUEVgNYrdz7enn5W1n4Q9uBKYmFfJeSCcbsfziErsa4EU9Cz/pO0KE4WE1QdqRcvSXthQ==
  • 提供的密码是f00b4r
  • 原始字符串为The quick brown fox jumped over the lazy dog and ran away

【讨论】: