javaflutterdartecdh

ECDH Compatible between Dart pointycastle and Java 17


I want to communicate between dart and java with ECDH (secp256r1 algorithm) .
this is my Dart Code :

import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:pointycastle/export.dart';

class EcdhHandler {
  late final AsymmetricKeyPair _keyPair;
  late final ECDomainParameters _domainParameters;

  EcdhHandler.init() {
    _domainParameters = ECDomainParameters(ECCurve_secp256r1().domainName);
    var ecParams = ECKeyGeneratorParameters(_domainParameters);
    var params = ParametersWithRandom<ECKeyGeneratorParameters>(ecParams, getSecureRandom());

    var keyGenerator = ECKeyGenerator();
    keyGenerator.init(params);
    _keyPair = keyGenerator.generateKeyPair();
  }

  Uint8List getPublicKey() {
    var publicKey = _keyPair.publicKey as ECPublicKey;
    var publicPoint = publicKey.Q!.getEncoded(false);
    return publicPoint;
  }

  ECPublicKey bytesToPublicKey(Uint8List bytes) {
    var domainParams = (_keyPair.publicKey as ECPublicKey).parameters;
    var curve = domainParams!.curve;
    var point = curve.decodePoint(bytes);
    return ECPublicKey(point, domainParams);
  }

  BigInt makeSharedSecret(ECPublicKey remotePublicKey) {
    var agreement = ECDHBasicAgreement();
    var privateKey = _keyPair.privateKey as ECPrivateKey;
    agreement.init(privateKey);
    return agreement.calculateAgreement(remotePublicKey);
  }
  SecureRandom getSecureRandom() {
    var secureRandom = FortunaRandom();
    var random = Random.secure();
    List<int> seeds = [];
    for (int i = 0; i < 32; i++) {
      seeds.add(random.nextInt(255));
    }
    secureRandom.seed(KeyParameter(Uint8List.fromList(seeds)));
    return secureRandom;
  }

}

and this Java code :

import javax.crypto.KeyAgreement;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;

public class ECDHHandler {
    private PublicKey publicKey;
    private KeyAgreement keyAgreement;
    private byte[] sharedSecret;

    public ECDHHandler() {
        makeKeyExchangeParams();
    }

    private void makeKeyExchangeParams() {
        try {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
            ECGenParameterSpec secp256r1 = new ECGenParameterSpec("secp256r1");
            kpg.initialize(secp256r1);
            KeyPair kp = kpg.generateKeyPair();
            keyAgreement = KeyAgreement.getInstance("ECDH");
            keyAgreement.init(kp.getPrivate());
            publicKey = kp.getPublic();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void initializeSharedSecret(PublicKey publickey) {
        try {
            keyAgreement.doPhase(publickey, true);
            sharedSecret = keyAgreement.generateSecret();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        }
    }

    public PublicKey convertByteArrayToPublicKey(byte[] bytes) {
        try {
            KeyFactory kf = KeyFactory.getInstance("EC");
            X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(bytes);
            return kf.generatePublic(pkSpec);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

I generate public key with pointycastle on dart application and send that to Java .
My problem :
method convertByteArrayToPublicKey on java can not parse byte codes to ecdh public key !

I always receive :

java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException: DerInputStream.getLength(): lengthTag=20, too big.
pointycastle: ^3.7.3 

Dart SDK version: 3.0.2 (stable) (Tue May 23 08:26:58 2023 +0000) on "linux_x64"

java version "17.0.4" 2022-07-19 LTS
Java(TM) SE Runtime Environment GraalVM EE 22.2.0 (build 17.0.4+11-LTS-jvmci-22.2-b05)
Java HotSpot(TM) 64-Bit Server VM GraalVM EE 22.2.0 (build 17.0.4+11-LTS-jvmci-22.2-b05, mixed mode, sharing)

Solution

  • Both sides use different key formats: getPublicKey() in the Dart code returns the public key in raw, uncompressed format. convertByteArrayToPublicKey() in the Java code requires the key in X.509/SPKI format, ASN.1/DER encoded.

    The different formats are the reason for your problem. The fix is to use identical formats. The adaptation can be done on the Dart side (export in X.509/SPKI format), on the Java side (import of a raw, uncompressed key) or via a key conversion tool.

    In the following the adaption is done on the Dart side. For import/export of keys e.g. the Dart package basic_utils can be used. The following solution is based on the code you posted.

    Part 1: Key export

    Key (generation via init() and) export can be implemented on the Dart side as follows:

    import 'package:basic_utils/basic_utils.dart';
    import 'package:pointycastle/export.dart';
    ...
    var ecdh = EcdhHandler.init();
    var x509pemFromDart = CryptoUtils.encodeEcPublicKeyToPem(ecdh.publicKey);
    var sec1pemFromDart = CryptoUtils.encodeEcPrivateKeyToPem(ecdh.privateKey);
    ...
    class EcdhHandler {  
      ...  
      ECPublicKey get publicKey {
        return _keyPair.publicKey as ECPublicKey;
      }
      ECPrivateKey get privateKey {
        return _keyPair.privateKey as ECPrivateKey;
      }
      EcdhHandler.init() {
        _domainParameters = ECDomainParameters(ECCurve_prime256v1().domainName);
        ...
    

    Here the following has to be considered:

    Key (generation and) export on the Java side can be implemented as follows:

    KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
    ECGenParameterSpec secp256r1 = new ECGenParameterSpec("secp256r1");
    kpg.initialize(secp256r1);
    KeyPair kp = kpg.generateKeyPair();
    String x509DerFromJava = Base64.getEncoder().encodeToString(kp.getPublic().getEncoded());
    String pkcs8DerFromJava = Base64.getEncoder().encodeToString(kp.getPrivate().getEncoded());
    

    The public key is exported in X.509/SPKI format, DER encoded, the private key in PKCS#8 format, DER encoded (the PKCS#8 format was chosen because it is supported by JCA/JCE by default).


    Part 2: Key import and key agreement

    Key import and key agreement on the Dart side can be implemented as follows (the keys used here were generated with the code from the first part):

    var x509DerFromJava = base64.decode("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXcXjAIwp17z2hHuKe+yd1xD6VnX8cFjmaibxeMnTVNQCilTaW+M2WkcAdNTDVJSGA9EYNpfJgHrw1G8055Ml4w==");
    var sec1pemFromDart = """-----BEGIN EC PRIVATE KEY-----
    MHcCAQEEIGW/gmCOh3bWEKFhuX+bbHpMmfP7YKQA7KS6Ddfqx658oAoGCCqGSM49
    AwEHoUQDQgAE7ux8syhN5jc7IOQDQgjr7Lgc9QiY2lD9O+3vA9ly+HaxD5H9Ihy6
    AM5ATMlZurtxz4V9FrOie+Nmhydu8TnMow==
    -----END EC PRIVATE KEY-----""";
    var x509FromJava = CryptoUtils.ecPublicKeyFromDerBytes(x509DerFromJava);
    var sec1FromDart = CryptoUtils.ecPrivateKeyFromPem(sec1pemFromDart);
    
    var agreement = ECDHBasicAgreement();
    agreement.init(sec1FromDart);
    var sharedSecret = agreement.calculateAgreement(x509FromJava);
    print(sharedSecret.toRadixString(16)); // f50ba48e19b4c3ddf35f23d24c27b335ef3f3fe61bc5a52d069262e83e653f6a
    

    and on the Java side: Here the public key must be converted from PEM to DER. This is relatively simple: header, footer and line breaks have to be removed and the rest has to be Base64 decoded. Alternatively, BouncyCastle can be used to import the PEM key directly:

    String x509PemFromDart = """
    -----BEGIN PUBLIC KEY-----
    MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7ux8syhN5jc7IOQDQgjr7Lgc9QiY
    2lD9O+3vA9ly+HaxD5H9Ihy6AM5ATMlZurtxz4V9FrOie+Nmhydu8TnMow==
    -----END PUBLIC KEY-----""";
    byte[] x509DerFromDart = Base64.getDecoder().decode(x509PemFromDart.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace("\n" , ""));
    PublicKey x509FromDart = convertByteArrayToPublicKey(x509DerFromDart);
    byte[] pkcs8DerFromJava = Base64.getDecoder().decode("MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCCl52mYjL52FtHM0s3J+9BHReocDl4Dmo0hoAHr9LxVqA==");
    PrivateKey pkcs8FromJava = KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(pkcs8DerFromJava));
    
    KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
    keyAgreement.init(pkcs8FromJava);
    keyAgreement.doPhase(x509FromDart, true);
    byte[] sharedSecret = keyAgreement.generateSecret();
    System.out.println(HexFormat.of().formatHex(sharedSecret)); // f50ba48e19b4c3ddf35f23d24c27b335ef3f3fe61bc5a52d069262e83e653f6a
    

    Test result: On both sides the same shared secret is generated!