1

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
Topaco
  • 40,594
  • 4
  • 35
  • 62
ddd
  • 63
  • 1
  • 9
  • "without success". I assume it means it runs but the result is rejected by the API server. – Codo Aug 04 '22 at 10:38
  • Yeah, it runs but the result is rejected by the API with error ` {"state":{"code":407,"message":"Invalid Request Params"}}` – ddd Aug 04 '22 at 10:44
  • Several things can be wrong: You are certainly using the wrong padding. How the key is generated is not specified; so `Key.fromUtf8()` might be incorrect. The sepc says to take the HMAC from base64 encoded data but it uses the binary data. The spec doesn't say what the key for the HMAC is. It's surprising that the resulting JSON is Base64 encoded again (but if the spec says so it's probably correct). – Codo Aug 04 '22 at 10:48
  • 1
    This is the Dart code from your last question (meanwhile deleted), which is a port of a more extensive Java code that is poorly specified by your description in this question. You should post the code instead (taking into account the criticized points from the last question). – Topaco Aug 04 '22 at 11:21
  • I posted the Java example – ddd Aug 04 '22 at 11:34
  • What does not work the encryption or the data integrity check? – Topaco Aug 04 '22 at 11:40
  • The data integrity check – ddd Aug 04 '22 at 11:45

1 Answers1

4

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.
Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Thanks for the code! Here is my test data: PlainText: "{\"coupon_key\":\"382236526272\",\"location_id\":\"2\",\"device_key\":\"test_1234\"}" Key: 33a485cb146e1153c69b588c671ab474 – ddd Aug 05 '22 at 09:22
  • @ddd - Why the test data? I mean, do you have a question? – Topaco Aug 05 '22 at 09:36
  • You asked me in the previous comments for the testData. – ddd Aug 05 '22 at 10:29
  • @ddd - Since I needed the test data for my answer, I already created some myself. You can find it in the middle part of my answer. I have added your test data to your question. Note that you need to add a static IV to your test data to compare the ciphertexts. – Topaco Aug 05 '22 at 10:54