iosobjective-cencryptioncryptographysjcl

Decrypt SJCL cipher text in Objective-C


I am receiving data encrypted with SJCL into an iOS app where I need to decrypt it. The other end is using SJCL with the AES CCM mode which is not supported by the Apple's CommonCrypto so I use the VPCCMCrypt library for this. It is not possible to make any changes to the other side sending the SJCL cipher text.

Here are my methods to decrypt:

+ (NSData *)decryptSjcl:(NSDictionary *)cipher password:(NSString *)password {
    if (cipher == nil || password == nil) {
        return nil;
    }

    int version = [cipher[@"v"] intValue];
    NSString *iv = cipher[@"iv"];
    uint iter = [cipher[@"iter"] unsignedIntValue];
    uint ks = [cipher[@"ks"] unsignedIntValue];
    uint ts = [cipher[@"ts"] unsignedIntValue];
    NSString *mode = cipher[@"mode"];
    NSString *adata = cipher[@"adata"];
    NSString *algorithm = cipher[@"cipher"];
    NSString *salt = cipher[@"salt"];
    NSString *ct = cipher[@"ct"];

    if (version != 1 || ! [@"aes" isEqualToString:algorithm]) {
        return nil;
    }

    NSData *rawIv = [[NSData alloc] initWithBase64EncodedString:iv options:0];
    NSData *rawSalt = [[NSData alloc] initWithBase64EncodedString:salt options:0];
    NSData *rawAdata = [[NSData alloc] initWithBase64EncodedString:adata options:0];
    NSData *cipherData = [[NSData alloc] initWithBase64EncodedString:ct options:0];
    NSData *key;

    NSMutableData *decipheredData = nil;

    if ([@"ccm" isEqualToString:mode]) {
        key = [Cryptor sjclAesKeyForPassword:password salt:rawSalt iterations:iter keySize:ks];
        decipheredData = [Cryptor decryptAesCcmData:cipherData iv:rawIv key:key adata:rawAdata tagSize:ts];
    }

    return decipheredData;
}

SJCL key generation:

+ (NSData *)sjclAesKeyForPassword:(NSString *)password salt:(NSData *)salt iterations:(uint)iterations keySize:(uint)keySizeBits {
    NSMutableData *derivedKey = [NSMutableData dataWithLength:keySizeBits / 8];

    int result = CCKeyDerivationPBKDF(kCCPBKDF2,
            password.UTF8String,
            [password lengthOfBytesUsingEncoding:NSUTF8StringEncoding],
            salt.bytes,
            salt.length,
            kCCPRFHmacAlgSHA256,
            iterations,
            derivedKey.mutableBytes,
            derivedKey.length);

    NSAssert(result == kCCSuccess, @"Unable to create AES key for password: %d", result);

    return derivedKey;
}

AES CCM decryption:

+ (NSMutableData *)decryptAesCcmData:(NSData *)cipherData iv:(NSData *)iv key:(NSData *)key adata:(NSData *)adata tagSize:(uint)tagSizeBits {
    VPCCMCrypt *ccm = [[VPCCMCrypt alloc] initWithKey:key
                                                   iv:iv
                                                adata:adata
                                            tagLength:tagSizeBits / 8];

    [ccm decryptDataWithData:cipherData
               finishedBlock:^(NSData *data) {
                   NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

                   NSLog(@"Decrypted SJCL message: %@", result);
               }
                  errorBlock:^(NSError *error) {
                      NSLog(@"ERROR");
                  }];

    return nil;
}

All the input data from the SJCL is correctly parsed (IV, salt, key size, tag size, PBKDF iterations, cipher text) and decoded to NSData from their Base64 encoded representation. Password is used the same. All data including the created AES key are not nil.

At the end it fails inside the VPCCMCrypt verifying the CCM tag (tags are different). Is there something wrong with the code above? Is there any other iOS/Objective-C/Swift library to decrypt AES CCM or even better the SJCL? I am not interested in a JavaScript wrapper of the SJCL library.

For testing I am using simple encrypted data from the SJCL's demo page.

EDIT:

As said in comments the SJCL send 16 bytes IV instead of max 12 bytes for the CCM mode and then internally clamps it to max 12 bytes when deciphering. Here is the updated decrypt method:

+ (NSData *)decryptSjcl:(NSDictionary *)cipher password:(NSString *)password {
    if (cipher == nil || password == nil) {
        return nil;
    }

    int version = [cipher[@"v"] intValue];
    NSString *iv = cipher[@"iv"];
    uint iter = [cipher[@"iter"] unsignedIntValue];
    uint ks = [cipher[@"ks"] unsignedIntValue];
    uint ts = [cipher[@"ts"] unsignedIntValue];
    NSString *mode = cipher[@"mode"];
    NSString *adata = cipher[@"adata"];
    NSString *algorithm = cipher[@"cipher"];
    NSString *salt = cipher[@"salt"];
    NSString *ct = cipher[@"ct"];

    if (version != 1 || ! [@"aes" isEqualToString:algorithm]) {
        return nil;
    }

    NSMutableData *rawIv = [[NSMutableData alloc] initWithBase64EncodedString:iv options:0];
    NSMutableData *rawSalt = [[NSMutableData alloc] initWithBase64EncodedString:salt options:0];
    NSMutableData *rawAdata = [[NSMutableData alloc] initWithBase64EncodedString:adata options:0];
    NSMutableData *cipherData = [[NSMutableData alloc] initWithBase64EncodedString:ct options:0];
    NSData *key;

    NSData *decipheredData = nil;

    if ([@"ccm" isEqualToString:mode]) {
        key = [Cryptor sjclAesKeyForPassword:password salt:rawSalt iterations:iter keySize:ks];

        // Clamp the SJCL IV - They use a 16 byte IV for CCM mode which is against specification and they do a funky
        // clamping. CCM mode IV should be max 13 bytes long. They almost always clamp it to 13 bytes but save the whole
        // 16 bytes in their JSON container.
        // for (L=2; L<4 && ol >>> 8*L; L++) {}
        // if (L < 15 - ivl) { L = 15-ivl; }
        // iv = w.clamp(iv,8*(15-L));
        int64_t ivl = rawIv.length;
        int64_t ol = cipherData.length - (ts / 8);
        int64_t L = 2;
        for (L = 2; L < 4 && ol >> 8*L; L++) {}
        if (L < 15 - ivl) {
            L = 15 - ivl;
        }
        NSRange subrange = NSMakeRange(0, (NSUInteger)(15 - L));

        decipheredData = [Cryptor decryptAesCcmData:cipherData iv:[rawIv subdataWithRange:subrange] key:key adata:rawAdata tagSize:ts];
    }
    else {
        decipheredData = nil;
    }

    return decipheredData;
}

One last missing thing is verifying the TAG, I am unable to do that. Any ideas?


Solution

  • SJCL doesn't use the whole (128/192/256bit) IV in AES-CCM mode, but the demo page shows it. Try it with less bytes.

    Here you can find how the IV length calculation works: http://bitwiseshiftleft.github.io/sjcl/doc/symbols/src/core_ccm.js.html

    for (L=2; L<4 && ol >>> 8*L; L++) {}
    if (L < 15 - ivl) { L = 15-ivl; }
    iv = w.clamp(iv,8*(15-L));