Dart pointycastle和Java 17之间的ECDH兼容

huwehgph  于 2023-06-19  发布在  Java
关注(0)|答案(1)|浏览(191)

我想用ECDH(secp 256 r1算法)在dart和java之间进行通信。
这是我的Dart代码:

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;
  }

}

这段Java代码:

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;
    }
}

我在dart应用程序上用pointycastle生成公钥并将其发送到Java。

我的问题

java上的方法convertByteArrayToPublicKey无法解析字节码到ecdh公钥!
我总是收到:

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)
q0qdq0h2

q0qdq0h21#

双方使用不同的密钥格式:Dart代码中的getPublicKey()以原始的未压缩格式返回公钥。Java代码中的convertByteArrayToPublicKey()需要X.509/SPKI格式的密钥,采用ASN.1/DER编码。
不同的格式是您的问题的原因。解决办法是使用相同的格式。可以在Dart端(以X.509/SPKI格式导出)、Java端(导入原始的未压缩密钥)或通过密钥转换工具进行调整。
在下文中,在Dart侧进行适配。用于密钥的导入/导出,例如可以使用Dart软件包basic_utils。以下解决方案基于您发布的代码。

第一部分:密钥导出

密钥(通过init()和)导出可以在Dart端实现如下:

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);
    ...

这里必须考虑以下几点:

  • 必须使用ECCurve_prime256v1()而不是ECCurve_secp256r1(),因为 basic_utils 不接受前者(错误消息:* 目前还不支持ObjectIdentifier secp 256 r1 )。请注意,这两个标识符命名相同的曲线: prime 256 v1 * aka NIST P-256 是 * secp 256 r1 * 的别名。
  • 除了公钥外,还导出了私钥,以便在此测试中更容易分离密钥生成和密钥协商。
  • 公钥以X.509/SPKI格式导出,PEM编码,私钥以SEC 1格式导出,PEM编码(选择SEC 1格式是因为 basic_utils 支持它)。

Java端的密钥(生成和)导出可以实现如下:

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());

公钥以X.509/SPKI格式导出,DER编码,私钥以PKCS#8格式导出,DER编码(选择PKCS#8格式是因为JCA/JCE默认支持该格式)。

第二部分:密钥导入和密钥协议

Dart端的密钥导入和密钥协商可以如下实现(这里使用的密钥是用第一部分的代码生成的):

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

在Java方面:这里,公钥必须从PEM转换为DER。这是相对简单的:header、footer和line breaks必须删除,其余部分必须进行Base64解码。或者,BouncyCastle也可以用于直接导入PEM密钥:

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

测试结果:在双方上,生成相同的共享秘密!

相关问题