javascriptdartcryptographyaes-gcm

How to implement AES-CCM in Dart


I'm trying to migrate some JavaScript encryption code to a Dart equivalent but I cannot get the same result.

The JavaScript code is using Stanford JavaScript Crypto Library and the Dart version is using AesGcm from Dart Cryptography 2.5.0 (https://pub.dev/packages/cryptography).

The code in JS is

result = sjcl.codec.hex.fromBits(
                sjcl.mode.ccm.encrypt(
                new sjcl.cipher.aes(sjcl.codec.hex.toBits("6dbe8f8c87d58e61c2ec29321f42e9ff")), // prf
                sjcl.codec.hex.toBits("00626c742e332e3132397649356b744455415443"), // plaintext
                sjcl.codec.hex.toBits("101112131415161718191A1B"), // iv
                sjcl.codec.hex.toBits("6465764944"),  // adata
                32));

The Dart I try to implement is

final encrypted = await AesGcm.with256bits().encrypt(
    secretKey: SecretKey(utf8.encode("6dbe8f8c87d58e61c2ec29321f42e9ff")), // prf ?
    utf8.encode("00626c742e332e3132397649356b744455415443"), // plaintext (device_new_id?)
    nonce: utf8.encode("101112131415161718191A1B"), // iv
    aad: utf8.encode("6465764944"), // adata
  );

  // Convert encrypted bytes to hex
  final result= bytesToHex( Uint8List.fromList(encrypted.cipherText));

The result differs but I cannot see why ?

Also tried with hex decode:

I've tried this: 

final encrypted = await AesGcm.with256bits().encrypt(
    secretKey: SecretKey(hex.decode("6dbe8f8c87d58e61c2ec29321f42e9ff")), // prf ?
    hex.decode("00626c742e332e3132397649356b744455415443"), // plaintext (device_new_id?)
    nonce: hex.decode("101112131415161718191A1B"), // iv
    aad: hex.decode("6465764944"), // adata
  );

Gives an exception: "Invalid argument (secretKeyData): Expected 32 bytes, got 16 bytes: Instance of 'SecretKeyData'"

Hope someone can point me in the right direction ?

Best regards Martin


Solution

  • JavaScript and Dart code use different modes, the JavaScript code applies CCM, the Dart code GCM. Although both modes are authenticated encryption and based on CTR the algorithms are different.

    You need a library on the Dart side that supports CCM. One option is PointyCastle. A possible implementation is:

    import 'package:pointycastle/export.dart';
    import 'dart:typed_data';
    import 'package:convert/convert.dart';
    
    ...
    
    Uint8List key = Uint8List.fromList(hex.decode("6dbe8f8c87d58e61c2ec29321f42e9ff"));
    Uint8List nonce = Uint8List.fromList(hex.decode("101112131415161718191A1B"));
    Uint8List aad = Uint8List.fromList(hex.decode("6465764944"));
    Uint8List plaintext = Uint8List.fromList(hex.decode("00626c742e332e3132397649356b744455415443"));
    
    // Encrypt with AES/CCM
    CCMBlockCipher encrypter = CCMBlockCipher(AESEngine());
    AEADParameters paramsEnc = AEADParameters(KeyParameter(key), 32, nonce, aad);
    encrypter.init(true, paramsEnc);
    Uint8List ciphertextTag = encrypter.process(plaintext);
    print(hex.encode(ciphertextTag)); // a2bbe65fcc7d13a8aeef2309055c3e2d24cd2f07bd2265cc - the last 4 bytes are the tag
    
    // Decrypt with AES/CCM
    CCMBlockCipher decrypter = CCMBlockCipher(AESEngine());
    AEADParameters paramsDec = AEADParameters(KeyParameter(key), 32, nonce, aad);
    decrypter.init(false, paramsDec);
    Uint8List decrypted = decrypter.process(ciphertextTag);
    print(hex.encode(decrypted)); // 00626c742e332e3132397649356b744455415443
    

    Like sjcl/JavaScript, PointyCastle/Dart concatenates ciphertext and tag: ciphertext|tag, i.e. the last 4 bytes (corresponding to the explicitly set tag size of 32 bits) are the tag.


    Test: The JavaScript code produces the same ciphertext and tag (for the same input data):

    var result = sjcl.codec.hex.fromBits(
        sjcl.mode.ccm.encrypt(
            new sjcl.cipher.aes(sjcl.codec.hex.toBits("6dbe8f8c87d58e61c2ec29321f42e9ff")), // prf
            sjcl.codec.hex.toBits("00626c742e332e3132397649356b744455415443"), // plaintext
            sjcl.codec.hex.toBits("101112131415161718191A1B"), // iv
            sjcl.codec.hex.toBits("6465764944"),  // adata
            32))
    console.log(result) // a2bbe65fcc7d13a8aeef2309055c3e2d24cd2f07bd2265cc
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/sjcl@1.0.8/sjcl.min.js"></script>


    Security:

    Note that for CCM a minimum tag size of 8 bytes is recommended, s. RFC 3610, sec. 4. Rationale.

    Also, for CCM, using a static IV/nonce is a serious vulnerability (with fixed key). Instead, a random IV/nonce should be generated during encryption, which is passed together with the ciphertext to the decrypting side, usually concatenated (the IV/nonce is not a secret).