在 Java 11 中引入了 curve25519 内置实现。由于我对此一无所知,并且最近才发现,因此我使用的是 Signal 的库。这是我切换到 Java 11 的实现之前的代码:
private final Curve25519 CURVE_25519 = Curve25519.getInstance(Curve25519.JAVA);
public Curve25519KeyPair calculateRandomKeyPair() {
return CURVE_25519.generateKeyPair();
}
public byte[] calculateSharedSecret(byte[] publicKey, byte[] privateKey) {
return CURVE_25519.calculateAgreement(publicKey, privateKey);
}
这是我现在的代码:
private final String XDH = "XDH";
private final String CURVE = "X25519";
@SneakyThrows
public KeyPair calculateRandomKeyPair() {
return KeyPairGenerator.getInstance(CURVE).generateKeyPair();
}
@SneakyThrows
public byte[] calculateSharedSecret(byte[] publicKeyBytes, XECPrivateKey privateKey) {
var paramSpec = new NamedParameterSpec(CURVE);
var keyFactory = KeyFactory.getInstance(XDH);
var publicKeySpec = new XECPublicKeySpec(paramSpec, new BigInteger(publicKeyBytes));
var publicKey = keyFactory.generatePublic(publicKeySpec);
var keyAgreement = KeyAgreement.getInstance(XDH);
keyAgreement.init(privateKey);
keyAgreement.doPhase(publicKey, true);
return keyAgreement.generateSecret();
}
显然,第二个实现不起作用,而第一个实现却起作用。最初,我认为我做错了什么,所以我阅读了文档并检查了类似的答案,但由于我没有发现任何有用的信息,我决定进一步挖掘并尝试检查 Signal 的库和 Java 是否生成相同的公钥给定私人的。为此,我写了这个片段:
import org.whispersystems.curve25519.Curve25519;
import sun.security.ec.XECOperations;
import sun.security.ec.XECParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.spec.NamedParameterSpec;
import java.util.Arrays;
private static boolean generateJava11KeyPair() throws InvalidAlgorithmParameterException {
var signalKeyPair = Curve25519.getInstance(Curve25519.JAVA).generateKeyPair();
var signalPublicKey = signalKeyPair.getPublicKey();
var params = XECParameters.get(InvalidAlgorithmParameterException::new, NamedParameterSpec.X25519);
var ops = new XECOperations(params);
var javaPublicKey = ops.computePublic(signalKeyPair.getPrivateKey().clone()).toByteArray();
return Arrays.equals(signalPublicKey, javaPublicKey);
}
(Java实现中用于计算公钥的代码摘自sun.security.ec.XDHKeyPairGenerator)
此方法打印 false,这意味着这两个实现实际上的行为方式不同。此时,我想知道这是一个 Java 错误还是我遗漏了一些东西。预先感谢。
Bernstein 等人为 X25519(和 X448)公钥和私钥定义的编码是无符号固定长度小端,而
BigInteger.toByteArray()
返回并被 ctor BigInteger(byte[])
接受的表示是双补码可变长度大尾数。由于 255 位四舍五入为 32 字节,并且有一个始终为零的备用位(对于 XDH),因此可以忽略符号差异,但其他因素很重要。
JCA 确实使接口类
XECPrivateKey
返回,并且相应的 Spec
接受这些形式,但是对于 XECPublicKey[Spec]
它使用 BigInteger
。它确实对(分别)由 Key.getEncoded()
返回并由 KeyFactory
接受的“X509”和“PKCS8”编码一致使用 Bernstein 形式,但这些元数据仅包含 XDH(或仅 Bernstein XDH) -和-EdDSA)像 X3DH 这样的系统不使用。
AFAICS 您的选择是
Key.getEncoded()
并解析特定于算法的部分,或者相反地构建算法通用结构以作为 X509EncodedKeySpec
传递到 KeyFactory.getInstance("Xblah")
。过去曾针对其他算法询问过第二种方法:“传统”(X9 式)EC——尤其是用于比特币和相关硬币的 secp256k1,它们通常仅使用原始 X9/SECG 数据,没有元数据—— RSA,其中一些系统使用原始 PKCS1 格式(这里更常见的是私钥而不是公钥);如果您愿意,我可以找到一些几乎重复的内容来说明该方法。
几年后我才发现这个问题,但我最终按照我从 dave_thompson_085 得到的建议分叉了 Signal 的存储库: https://github.com/Auties00/curve25519-java