I am trying to send an encrypted request to a specific API in dart, but without success - I don't have any experience with the Dart language.

This are the requirements:

  • The JSON to be sent is encrypted as follows: "AES/CBC/ZeroBytePadding", IV is generated according to SHA1PRNG with a length of 16 bytes.
  • The encrypted bytes are Base64 encoded. This results in the encryptedJson.
  • The hmac is generated from base64 encoded IV and the encryptedJson with "HmacSHA256".
  • A json will be generated: {"value":encryptedJson,"iv":initialisationVector,"mac":hmac}
  • This json will be base64 encoded and sent as an encrypted payload.

Can anyone help me? Thanks in advance!

This is the Dart Code so far.

import 'dart:convert';
import 'dart:core';
import 'package:crypto/crypto.dart' as crypto;
import 'package:encrypt/encrypt.dart' as enc;

String encrypt(String string) {
    // json encryption
    final enc.Key key = enc.Key.fromUtf8(env.get('password'));
    final enc.IV iv = enc.IV.fromSecureRandom(IV_LENGTH);
    final enc.Encrypter encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc));
    final encryptedJson = encrypter.encrypt(string, iv: iv);
    final String IVBase64String = base64.encode(iv.bytes);

    print('encrypted JSON: '+encryptedJson.base64);
    print('decrypted JSON: '+encrypter.decrypt(encryptedJson, iv: iv));

    crypto.Hmac hmacSha256 = new crypto.Hmac(crypto.sha256, key.bytes);
    crypto.Digest sha256Result = hmacSha256.convert(iv.bytes + encryptedJson.bytes);

    print('data: ' + encryptedJson.base64);
    print('iv: ' + IVBase64String);
    print('hmac: ' + sha256Result.toString());

    // Payload
    final encryptedText = "{\"value\":\""+encryptedJson.base64+"\",\"iv\":\""+IVBase64String+"\",\"mac\":\""+sha256Result.toString()+"\"}";

    print('final: ' + jsonEncode(encryptedText));
    return base64.encode(utf8.encode(encryptedText));
  }

This is the JavaExample

import java.io.UnsupportedEncodingException;
import java.util.Base64;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class ApiJavaSample
{
    private final Cipher cipher;
    private final SecretKeySpec key;
    private static final String TAG = "AESCrypt";
    private static final int IV_LENGTH = 16;
    private String cypher_mode = "AES/CBC/NoPadding";
    private String cypher_mode_iv = "SHA1PRNG";

    public static void main (String[] args)
    {
        try{
            System.out.println("encrypting");
            ApiJavaSample test = new ApiJavaSample("password");
            String encryptedString = test.encrypt("{\"coupon_key\":\"011205358365\",\"location_id\":\"2\",\"device_key\":\"test_1234\"}");
            System.out.println("encrpyted");
            System.out.println(encryptedString);
        }
        catch(Exception e)
        {
            System.out.println(e);
        }
    }

    public ApiJavaSample(String password) throws Exception
    {
        // hash password with SHA-256 and crop the output to 128-bit for key
        //MessageDigest digest = MessageDigest.getInstance("SHA-256");
        //digest.Updater(password.getBytes("UTF-8"));
        byte[] keyBytes = password.getBytes();

        cipher = Cipher.getInstance(cypher_mode);
        key = new SecretKeySpec(keyBytes, "AES");
    }

     private String hmacDigest(String msg, String algo)
    {
        String digest = null;
        try
        {
            //SecretKeySpec key = new SecretKeySpec((keyString).getBytes("UTF-8"), algo);
            Mac mac = Mac.getInstance(algo);
            mac.init(key);

            byte[] bytes = mac.doFinal(msg.getBytes("UTF-8"));

            StringBuilder hash = new StringBuilder();
            for (int i = 0; i < bytes.length; i++)
            {
                String hex = Integer.toHexString(0xFF & bytes[i]);
                if (hex.length() == 1)
                {
                    hash.append('0');
                }
                hash.append(hex);
            }
            digest = hash.toString();
        }
        catch (UnsupportedEncodingException | InvalidKeyException e)
        {
            e.printStackTrace();
        }
        catch (NoSuchAlgorithmException e)
        {
            e.printStackTrace();
        }
        return digest;
    }

    public String encrypt(String plainText) throws Exception
    {

        byte[] iv_bytes = generateIv();
        AlgorithmParameterSpec spec = new IvParameterSpec(iv_bytes);
        cipher.init(Cipher.ENCRYPT_MODE, key, spec);
        int blockSize = cipher.getBlockSize();

        while (plainText.length() % blockSize != 0) {
            plainText += "\0";
        }

        byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
        String encryptedText = Base64.getEncoder().encodeToString(encrypted);

        String iv_base64_string = Base64.getEncoder().encodeToString(iv_bytes);

        String mac = hmacDigest(iv_base64_string + encryptedText.trim(), "HmacSHA256");

        //JSONObject encryptedJson = new JSONObject();

        //encryptedJson.put("value", encryptedText.trim());
        //encryptedJson.put("iv", iv_base64_string);
        //encryptedJson.put("mac", mac);

        String base64Encrypt = "{\"value\":\""+encryptedText.trim()+"\",\"iv\":\""+iv_base64_string+"\",\"mac\":\""+mac+"\"}";

        return Base64.getEncoder().encodeToString(base64Encrypt.getBytes());
    }

    private byte[] generateIv() throws NoSuchAlgorithmException
    {
        SecureRandom random = SecureRandom.getInstance(cypher_mode_iv);
        byte[] iv = new byte[IV_LENGTH];
        random.nextBytes(iv);

        return iv;
    }
}

Here is my test data:

Plaintext:

"{\"coupon_key\":\"382236526272\",\"location_id\":\"2\",\"device_key\":\"test_1234\"}"

Key:

33a485cb146e1153c69b588c671ab474


Solution 1: Topaco

The following has to be changed/optimized in the Dart code:

  • The Java code uses Zero padding. PointyCastle and the encrypt package (a PointyCastle wrapper) do not support Zero padding (to my knowledge). A possible approach for the Dart code is to disable the default PKCS#7 padding in combination with a custom implementation for Zero padding.
  • The Java code applies the Base64 encoded data for the HMAC, while the Dart code uses the raw data. This has to be changed.
  • The Base64 encoding of the IV is obtained more efficiently with iv.base64.

Thus, the code is to be changed as follows:

import 'package:crypto/crypto.dart' as crypto;
import 'package:encrypt/encrypt.dart' as enc;
import 'package:convert/convert.dart';
import 'dart:typed_data';
import 'dart:convert';

String encrypt(String string) {

  final enc.Key key = enc.Key.fromUtf8(env.get('password')); // Valid AES key           
  final enc.IV iv = enc.IV.fromSecureRandom(IV_LENGTH);      // IV_LENGTH = 16          

  final dataPadded = pad(Uint8List.fromList(utf8.encode(string)), 16);
  final enc.Encrypter encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc, padding: null));
  final encryptedJson = encrypter.encryptBytes(dataPadded, iv: iv);

  crypto.Hmac hmacSha256 = crypto.Hmac(crypto.sha256, key.bytes);
  crypto.Digest sha256Result = hmacSha256.convert(utf8.encode(iv.base64 + encryptedJson.base64));

  final encryptedText = "{\"value\":\""+encryptedJson.base64+"\",\"iv\":\""+iv.base64+"\",\"mac\":\""+sha256Result.toString()+"\"}";
  return base64.encode(utf8.encode(encryptedText));
}

Uint8List pad(Uint8List plaintext, int blockSize){
  int padLength = (blockSize - (plaintext.lengthInBytes % blockSize)) % blockSize;
  if (padLength != 0) {
    BytesBuilder bb = BytesBuilder();
    Uint8List padding = Uint8List(padLength);
    bb.add(plaintext);
    bb.add(padding);
    return bb.toBytes();
  }
  else {
    return plaintext;
  }
}

Test (using a static IV to allow comparison between the ciphertexts of the two codes):

Key:        enc.Key.fromUtf8("5432109876543210")
IV:         enc.IV.fromUtf8("0123456789012345")
Plaintext:  "{\"coupon_key\":\"011205358365\",\"location_id\":\"2\",\"device_key\":\"test_1234\"}"  
Result:     eyJ2YWx1ZSI6InNRTjJ0OWc5ZWY2RzdNV2RsOFB3emlXSlQwclNxUWJ2ZnN0eCtpMmNtSTQyUXJjUGRNV0JLbTlRZ2kxdmM0dElna2NOZEJsOVpEM0JlYTFPZ1kxaHNSeklSbHM1TnlaN0s1T2NqMTEzdkdvPSIsIml2IjoiTURFeU16UTFOamM0T1RBeE1qTTBOUT09IiwibWFjIjoiMzkwYzlhMzAxMjAxYjc1MWUxNjBhM2JlZTdmZGU5YzE5ZDY0MzJlNTBjOTJhNTg0ODBhMTJkNTYyNWRkYWMyNSJ9

After the changes, both codes return the above result for the above input data.


Security:

  • Typically, an AES key is a randomly generated byte sequence and not a string. If the key is to be derived from a passphrase/string, a reliable key derivation like PBKDF2 is to be used.
  • Zero padding is unreliable, so the reliable PKCS#7 padding that most libraries use by default should be applied. If the Java code had used PKCS#7 padding, porting would have been easier.
  • For encoding/decoding the charset should be specified (e.g. getBytes(StandardCharsets.UTF_8)), otherwise the default encoding will be used (which might not be wanted).
  • Using the same key for encryption and integrity checking for AES/HMAC is not a pressing security issue, but should be avoided as a preventive measure, see here.
  • The code is partially inefficient, e.g. when concatenating the Base64 encoded data instead of the raw data to determine the HMAC.