2

We are porting an app from native Android/iOS to Flutter. We are using AES symmetric encryption in native apps and they are working fine with our device.

Following is the flutter code where we have tried to replicate java implementation.

Here we send client(app) public key and client nonce (random 16 bit) to the device. Device creates an encrypted packet with device public key, client public key and mac id, and send it to app after encrypting it. We decrypt the received packet on app and create a similar packet on our side and we match both of these packets.

In our case, both of them are matching in Flutter (Which suggest that we have correctly completed the encryption process). The problem is, after this process when we try to send next command to device by encrypting the data, device is not able to understand it and replies with junk data.

import 'dart:math';
import 'dart:typed_data';

import 'package:asn1lib/asn1lib.dart';
import 'package:pc_steelcrypt/export.dart';
import 'package:steel_crypt/steel_crypt.dart';
import 'package:collection/collection.dart';

Future<void> startEncryption() async {
  const String SALT_AES_KEY = 'AES-KEY';
  const String SALT_AES_IV = 'AES-IV';
  final Uint8List serverResponse = await sendUnEncryptedCommand(getFirstMessagePayload());
  final Uint8List macId = getMacId(serverResponse);
  final Uint8List saltNonceServer = getSaltNounceServer(serverResponse);
  final Uint8List serverCertificate = getCertificate(serverResponse);

  //Parsing server certificate
  final ASN1Parser p = ASN1Parser(serverCertificate);
  final ASN1Sequence signedCert = p.nextObject() as ASN1Sequence;
  final ASN1Sequence cert = signedCert.elements[0] as ASN1Sequence;
  final ASN1Sequence pubKeyElement = cert.elements[6] as ASN1Sequence;
  final ASN1BitString pubKeyBits = pubKeyElement.elements[1] as ASN1BitString;
  print('pubKeyBits : $pubKeyBits');

  //TODO: Need to authenticate certificate too

  //Client keys generation:
  final ECCurve_secp256r1 secp256r1 = ECCurve_secp256r1();
  final ECPoint Q =
      secp256r1.curve.decodePoint(pubKeyBits.contentBytes().toList());
  final ECPublicKey serverPublicKey = ECPublicKey(Q, secp256r1);

  final ECCurve_secp256r1 _nistp256 = ECCurve_secp256r1();
  final ECKeyGeneratorParameters ecKeyGeneratorParameters =
      ECKeyGeneratorParameters(_nistp256);
  final SecureRandom sr = getSecureRandom();
  final ParametersWithRandom parametersWithRandom =
      ParametersWithRandom(ecKeyGeneratorParameters, sr);
  final ECKeyGenerator ecKeyGenerator = ECKeyGenerator();
  ecKeyGenerator.init(parametersWithRandom);
  final _clientKeyPair = ecKeyGenerator.generateKeyPair();

  final SecureRandom sr2 = getSecureRandom();
  final Uint8List clientNonce = sr2.nextBytes(16);

  final clientPubKeyRaw = getCertificateRaw(_clientKeyPair.publicKey as ECPublicKey);

  final Uint8List msg = Uint8List(80);
  List.copyRange(msg, 0, clientPubKeyRaw, 0, 64);
  List.copyRange(msg, 64, clientNonce, 0, 16);

  final Uint8List signedData = await sendUnEncryptedCommand(msg);

  final secret = serverPublicKey.Q * (_clientKeyPair.privateKey as ECPrivateKey).d;
  Uint8List sharedSecret = secret.getEncoded();
  if (sharedSecret.length == 33) {
    sharedSecret = sharedSecret.sublist(1);
  }

  final Uint8List aesKey = generateAes128BitKeyForBle(sharedSecret,Uint8List.fromList(SALT_AES_KEY.codeUnits), saltNonceServer, clientNonce);
  final Uint8List aesIv = generateAes128BitKeyForBle(sharedSecret,Uint8List.fromList(SALT_AES_IV.codeUnits), saltNonceServer, clientNonce);

  final AesCryptRaw aesEncrypter = AesCryptRaw(padding: PaddingAES.none, key: aesKey);

  final Uint8List serverPubKeyRaw = getCertificateRaw(serverPublicKey);

  final int clientToBeSignedLength = serverPubKeyRaw.length + clientPubKeyRaw.length + macId.length;
  final Uint8List clientToBeSigned = Uint8List(clientToBeSignedLength);
  List.copyRange(clientToBeSigned, 0, serverPubKeyRaw, 0,serverPubKeyRaw.length);
  List.copyRange(clientToBeSigned, serverPubKeyRaw.length, clientPubKeyRaw, 0,clientPubKeyRaw.length);
  List.copyRange(clientToBeSigned, serverPubKeyRaw.length + clientPubKeyRaw.length, macId, 0,macId.length);

  final Digest md = SHA256Digest();
  md.update(clientToBeSigned, 0, clientToBeSigned.length);
  final Uint8List clientHash = Uint8List(md.digestSize);
  md.doFinal(clientHash, 0);

  final Uint8List serverHash = aesEncrypter.ctr.decrypt(enc: signedData, iv: aesIv);

  final Function deepEq = DeepCollectionEquality().equals;
  final bool result = deepEq(serverHash, clientHash) as bool;
  if (result) {
    print('Encryption success');
    //sending encrypted command for the first time to device. Encrypting using aesEncrypter
    sendEncryptedCommand(aesEncrypter, Uint8List.fromList([2,2]));
  } else {
    print('Encryption failure');
  }

}

Uint8List getCertificateRaw(ECPublicKey publicKey){
  final publicKeyRaw = Uint8List(64);
  Uint8List result = publicKey.Q.getEncoded(false);
  result = result.length == 65 ? result.sublist(1) : result;
  List.copyRange(publicKeyRaw, 0, result, 0, result.length);
  return publicKeyRaw;
}

Uint8List generateAes128BitKeyForBle(Uint8List sharedSecret,
    Uint8List salt, Uint8List serverNonce, Uint8List clientNonce) {
  final Digest digester = SHA256Digest();

  digester.update(sharedSecret, 0, sharedSecret.length);
  digester.update(salt, 0, salt.length);
  digester.update(clientNonce, 0, clientNonce.length);
  digester.update(serverNonce, 0, serverNonce.length);

  final Uint8List outBuffer = Uint8List(digester.digestSize);
  digester.doFinal(outBuffer, 0);

  if (outBuffer.length <= 16) {
    //log('Encrypter' ,'digest length is smaller than 16');
    return outBuffer;
  } else {
    final List<int> aesKey = List<int>(16);
    List.copyRange(aesKey, 0, outBuffer.toList(), 0, 16);
    return Uint8List.fromList(aesKey);
  }
}

SecureRandom getSecureRandom() {
  final secureRandom = FortunaRandom();
  final random = Random.secure();
  final List<int> seeds = [];
  for (int i = 0; i < 32; i++) {
    seeds.add(random.nextInt(255));
  }
  secureRandom.seed(KeyParameter(Uint8List.fromList(seeds)));
  return secureRandom;
}

Following is the code snippet from Java/Android used for encryption.

public class AesCtrCipher {

    private Cipher mCipher;

    public void init(byte[] rawKey, byte[] inputVector) {
        if (mCipher == null) {
            SecretKeySpec aesKeySpec = new SecretKeySpec(rawKey, "AES");
            IvParameterSpec ivSpec = new IvParameterSpec(inputVector);
            try {
                mCipher = Cipher.getInstance("AES/CTR/NoPadding");
                mCipher.init(Cipher.ENCRYPT_MODE, aesKeySpec, ivSpec);
            } catch (Exception e) {
                throw new CustomException("Encryption exception : " + e.getMessage(), Encryption);
            }
        }
    }

    public byte[] crypt(byte[] data) {
        try {
            return mCipher.update(data);
        } catch (Exception e) {
            throw new CustomException("Encryption exception : " + e.getMessage(), Encryption);
        }
    }
}

Code to generate keys in Java:

class EncryptionKeyPair {
private static ECParameterSpec nistp256 = new ECParameterSpec(
        new EllipticCurve(
                new ECFieldFp(new BigInteger
                        ("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF",
                                16)),
                new BigInteger
                        ("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC",
                                16),
                new BigInteger
                        ("5ac635d8aa3a93e7bb2ebdb7676RE86bc651d06b45c54c0f63bce3c3e27d2604b",
                                16)),
        new ECPoint(new BigInteger
                ("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A18655D898C296", 16),
                new BigInteger
                        ("3DE342FAFE2A7F9B4AA6EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5",
                                16)),
        new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFECE4FAAFA7179E94F4A9CAB2FC432543", 16),
        1);
public ECPrivateKey mPrivate;
public ECPublicKey mPublic;

public void generateKey() throws IOException {
    KeyPairGenerator kpg;
    try {
        kpg = KeyPairGenerator.getInstance("EC");
        kpg.initialize(nistp256);
        KeyPair pair = kpg.generateKeyPair();
        mPrivate = (ECPrivateKey) pair.getPrivate();
        mPublic = (ECPublicKey) pair.getPublic();
    } catch (NoSuchAlgorithmException e) {
        throw new IOException("No DH keypair generator", e);
    } catch (InvalidAlgorithmParameterException e) {
        throw new IOException("Invalid DH parameters", e);
    }
}
}

Generating shared secret in Java

KeyAgreement ka;
    try {
        ka = KeyAgreement.getInstance("ECDH");
        ka.init((ECPrivateKey) keyPair.mPrivate);
        ka.doPhase(certificate.getPublicKey(), true);
        return ka.generateSecret();
    } catch (Exception e) {
        e.printStackTrace();
    }

Please let me know if any other information is required? Can somebody point out what wrong we are doing?

vijay053
  • 822
  • 3
  • 18
  • 36
  • 2
    It's not clear where you are having a problem. You say that some things match, but others don't. Can you give a simple example of some java code and dart code trying to do the same thing and returning different results (with test vectors)? – Richard Heap Jul 31 '20 at 10:21
  • @RichardHeap I am not very sure about this encryption process, but as per my understanding if deepEq(serverHash, clientHash) as bool; returns true, that means both client and server are following same encryption process and their keys are properly aligned for this session. Since we are generating random keys for each session, I cannot match the flutter output with java output. After completing the encryption process we send [2,3] payload to device after encryption. Device is able to understand the encrypted data received from Java, but not from flutter encrypted data. – vijay053 Jul 31 '20 at 10:34
  • 2
    The Java and Dart code are almost completely unrelated to each other. I'm not certain why you would expect these two functions to return the same results. I assume there's a whole lot more Java code somewhere doing the rest of the things that are in the Dart? – Rob Napier Jul 31 '20 at 13:19
  • 1
    The way you debug this kind of problem, BTW, is to go through each step of the Java code, record its precise (byte-for-byte) input and output. Then make sure your Dart code generates the exact output for that input. Then move to the next step in the process. In crypto, if even one byte is wrong anywhere along the way, the entire output can be garbage, and you generally won't get any errors. Finding where you are computing that wrong byte requires going methodically through the code, line by line and comparing the implementations. – Rob Napier Jul 31 '20 at 13:22
  • If it's working for the first message, and failing for later messages, I would first suspect that the system expects to create one long encrypted stream rather than individually encrypted messages. CTR is often used to maintain a long-running encrypted stream. It looks like your Dart code may start a new session for every message which is more common for block-modes like CBC rather than stream modes like CTR. (But sometimes CTR is used in a block-like way, so that's not certain. It depends on the rest of the Java code.) – Rob Napier Jul 31 '20 at 13:39
  • @vijay053 As Rob says you need to go through this step by step. You don't even need a device to test this. You need two test harnesses: one Dart one Java. Prove that with the same certificate and same random key they produce the same shared secret. (Obviously, create the random key once and feed the same key into both the Java and Dart code.) Having confirmed that the shared secret is the same, prove that the Java and Dart code create the same AES key and IV. Then create a test message and encrypt that - prove once again that the encrypted messages are the same. – Richard Heap Jul 31 '20 at 15:17
  • This may not solve your specific problem as stated, but i have worked with flutter a bit in the past, and if you are trying to port a java-based android app to flutter, the easiest way is probably not to reimplement all of your code in dart. Using the flutter message passing, you can reuse your java code while writing only minimal dart. – byake Jul 31 '20 at 17:52
  • @RobNapier The first two commands we are sending to the device are not encrypted. We start sending encrypted messages only after encryption process is complete (i.e. deepEq(serverHash, clientHash) as bool returns true) – vijay053 Aug 01 '20 at 03:15
  • @RichardHeap Since in the end of the process our serverHash and clientHash are matching, can I infer from this that our shared secret is correct(or equal)? Meanwhile I am trying to pass same key to both Java and Dart and will check each step. – vijay053 Aug 01 '20 at 03:29
  • @vijay053 that would seem to be the case since it's used to decrypt the serverHash, right? – Richard Heap Aug 01 '20 at 03:39
  • @RichardHeap, Yes, we are using it to decrypt the the serverHash. We have checked with the device team, they are not able to decrypt data received from app which was sent after encryption keys were established. – vijay053 Aug 04 '20 at 09:12
  • @RichardHeap I have added additional code for creating keys and generating secret in Java. Can you please check that dart implementation is similar to this java code? – vijay053 Aug 06 '20 at 19:44
  • Did you check the Java compatible ECDH implementation I posted? – Richard Heap Aug 11 '20 at 16:04

0 Answers0