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)
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.
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:
ECCurve_secp256r1()
, ECCurve_prime256v1()
must be used, since basic_utils does not accept the former (error message: ObjectIdentifier secp256r1 is not supported yet). Note that both identifiers name the same curve: prime256v1 aka NIST P-256 are aliases of secp256r1.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).
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!