From 6801431613593caeb32897582cfc40ad1d4ff1d7 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Tue, 20 Jan 2026 23:46:30 -0800 Subject: [PATCH] X25519: use built-in classes when available With Android API 33+ and also Java 11+ we should be able to use XECPublicKeySpec and its friends without using Tink. Some phones were optimizing Tink code away to make different byte arrays equivalent. --- .../ssh2/crypto/dh/Curve25519Exchange.java | 25 +++- .../crypto/dh/PlatformX25519Provider.java | 126 ++++++++++++++++++ .../ssh2/crypto/dh/TinkX25519Provider.java | 26 ++++ .../ssh2/crypto/dh/X25519Provider.java | 17 +++ .../ssh2/crypto/dh/X25519ProviderFactory.java | 77 +++++++++++ .../crypto/dh/Curve25519ExchangeTest.java | 84 +++++++++--- 6 files changed, 327 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/trilead/ssh2/crypto/dh/PlatformX25519Provider.java create mode 100644 src/main/java/com/trilead/ssh2/crypto/dh/TinkX25519Provider.java create mode 100644 src/main/java/com/trilead/ssh2/crypto/dh/X25519Provider.java create mode 100644 src/main/java/com/trilead/ssh2/crypto/dh/X25519ProviderFactory.java diff --git a/src/main/java/com/trilead/ssh2/crypto/dh/Curve25519Exchange.java b/src/main/java/com/trilead/ssh2/crypto/dh/Curve25519Exchange.java index 01d4ab47..b6c0db9c 100644 --- a/src/main/java/com/trilead/ssh2/crypto/dh/Curve25519Exchange.java +++ b/src/main/java/com/trilead/ssh2/crypto/dh/Curve25519Exchange.java @@ -1,7 +1,5 @@ package com.trilead.ssh2.crypto.dh; -import com.google.crypto.tink.subtle.X25519; - import java.io.IOException; import java.math.BigInteger; import java.security.InvalidKeyException; @@ -14,18 +12,33 @@ public class Curve25519Exchange extends GenericDhExchange { public static final String ALT_NAME = "curve25519-sha256@libssh.org"; public static final int KEY_SIZE = 32; + private final X25519Provider x25519Provider; private byte[] clientPublic; private byte[] clientPrivate; private byte[] serverPublic; public Curve25519Exchange() { + this(X25519ProviderFactory.getProvider()); + } + + public Curve25519Exchange(X25519Provider provider) { super(); + this.x25519Provider = provider; } - /* + /** * Used to test known vectors. */ public Curve25519Exchange(byte[] secret) throws InvalidKeyException { + this(X25519ProviderFactory.getProvider(), secret); + } + + /** + * Used to test known vectors with a specific provider. + */ + public Curve25519Exchange(X25519Provider provider, byte[] secret) throws InvalidKeyException { + super(); + this.x25519Provider = provider; if (secret.length != KEY_SIZE) { throw new AssertionError("secret must be key size"); } @@ -38,9 +51,9 @@ public void init(String name) throws IOException { throw new IOException("Invalid name " + name); } - clientPrivate = X25519.generatePrivateKey(); + clientPrivate = x25519Provider.generatePrivateKey(); try { - clientPublic = X25519.publicFromPrivate(clientPrivate); + clientPublic = x25519Provider.publicFromPrivate(clientPrivate); } catch (InvalidKeyException e) { throw new IOException(e); } @@ -64,7 +77,7 @@ public void setF(byte[] f) throws IOException { } serverPublic = f.clone(); try { - byte[] sharedSecretBytes = X25519.computeSharedSecret(clientPrivate, serverPublic); + byte[] sharedSecretBytes = x25519Provider.computeSharedSecret(clientPrivate, serverPublic); int allBytes = 0; for (int i = 0; i < sharedSecretBytes.length; i++) { allBytes |= sharedSecretBytes[i]; diff --git a/src/main/java/com/trilead/ssh2/crypto/dh/PlatformX25519Provider.java b/src/main/java/com/trilead/ssh2/crypto/dh/PlatformX25519Provider.java new file mode 100644 index 00000000..29aded60 --- /dev/null +++ b/src/main/java/com/trilead/ssh2/crypto/dh/PlatformX25519Provider.java @@ -0,0 +1,126 @@ +package com.trilead.ssh2.crypto.dh; + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.NamedParameterSpec; +import java.security.spec.XECPrivateKeySpec; +import java.security.spec.XECPublicKeySpec; + +import javax.crypto.KeyAgreement; + +/** + * X25519 provider implementation using platform-native APIs (Java 11+/Android API 33+). + */ +public class PlatformX25519Provider implements X25519Provider { + private static final String ALGORITHM = "X25519"; + private static final NamedParameterSpec X25519_SPEC = new NamedParameterSpec(ALGORITHM); + + private final KeyPairGenerator keyPairGenerator; + private final KeyFactory keyFactory; + + public PlatformX25519Provider() { + try { + keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM); + keyFactory = KeyFactory.getInstance(ALGORITHM); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("X25519 not available on this platform", e); + } + } + + @Override + public byte[] generatePrivateKey() { + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + return extractPrivateKeyBytes(keyPair.getPrivate()); + } + + @Override + public byte[] publicFromPrivate(byte[] privateKey) throws InvalidKeyException { + byte[] pubKeyBytes = new byte[KEY_SIZE]; + computePublicFromPrivate(privateKey, pubKeyBytes); + return pubKeyBytes; + } + + private void computePublicFromPrivate(byte[] privateKey, byte[] publicKey) throws InvalidKeyException { + try { + PrivateKey privKey = createPrivateKey(privateKey); + KeyAgreement ka = KeyAgreement.getInstance(ALGORITHM); + ka.init(privKey); + + XECPublicKeySpec basePointSpec = new XECPublicKeySpec(X25519_SPEC, BigInteger.valueOf(9)); + PublicKey basePoint = keyFactory.generatePublic(basePointSpec); + ka.doPhase(basePoint, true); + byte[] result = ka.generateSecret(); + System.arraycopy(result, 0, publicKey, 0, KEY_SIZE); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new InvalidKeyException("X25519 not available", e); + } + } + + @Override + public byte[] computeSharedSecret(byte[] privateKey, byte[] publicKey) throws InvalidKeyException { + try { + PrivateKey privKey = createPrivateKey(privateKey); + PublicKey pubKey = createPublicKey(publicKey); + + KeyAgreement keyAgreement = KeyAgreement.getInstance(ALGORITHM); + keyAgreement.init(privKey); + keyAgreement.doPhase(pubKey, true); + + return keyAgreement.generateSecret(); + } catch (NoSuchAlgorithmException e) { + throw new InvalidKeyException("X25519 not available", e); + } + } + + private PrivateKey createPrivateKey(byte[] keyBytes) throws InvalidKeyException { + try { + XECPrivateKeySpec spec = new XECPrivateKeySpec(X25519_SPEC, keyBytes.clone()); + return keyFactory.generatePrivate(spec); + } catch (InvalidKeySpecException e) { + throw new InvalidKeyException("Invalid private key", e); + } + } + + private PublicKey createPublicKey(byte[] keyBytes) throws InvalidKeyException { + try { + BigInteger u = decodeLittleEndian(keyBytes); + XECPublicKeySpec spec = new XECPublicKeySpec(X25519_SPEC, u); + return keyFactory.generatePublic(spec); + } catch (InvalidKeySpecException e) { + throw new InvalidKeyException("Invalid public key", e); + } + } + + private byte[] extractPrivateKeyBytes(PrivateKey privateKey) { + try { + XECPrivateKeySpec spec = keyFactory.getKeySpec(privateKey, XECPrivateKeySpec.class); + byte[] scalar = spec.getScalar(); + if (scalar == null) { + throw new IllegalStateException("Private key scalar not available"); + } + if (scalar.length == KEY_SIZE) { + return scalar; + } + byte[] padded = new byte[KEY_SIZE]; + System.arraycopy(scalar, 0, padded, KEY_SIZE - scalar.length, scalar.length); + return padded; + } catch (InvalidKeySpecException e) { + throw new IllegalStateException("Failed to extract private key bytes", e); + } + } + + private static BigInteger decodeLittleEndian(byte[] bytes) { + byte[] reversed = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + reversed[i] = bytes[bytes.length - 1 - i]; + } + return new BigInteger(1, reversed); + } +} diff --git a/src/main/java/com/trilead/ssh2/crypto/dh/TinkX25519Provider.java b/src/main/java/com/trilead/ssh2/crypto/dh/TinkX25519Provider.java new file mode 100644 index 00000000..6a3ff27a --- /dev/null +++ b/src/main/java/com/trilead/ssh2/crypto/dh/TinkX25519Provider.java @@ -0,0 +1,26 @@ +package com.trilead.ssh2.crypto.dh; + +import com.google.crypto.tink.subtle.X25519; + +import java.security.InvalidKeyException; + +/** + * X25519 provider implementation using Google Tink. + * This is the fallback implementation for platforms without native X25519 support. + */ +public class TinkX25519Provider implements X25519Provider { + @Override + public byte[] generatePrivateKey() { + return X25519.generatePrivateKey(); + } + + @Override + public byte[] publicFromPrivate(byte[] privateKey) throws InvalidKeyException { + return X25519.publicFromPrivate(privateKey); + } + + @Override + public byte[] computeSharedSecret(byte[] privateKey, byte[] publicKey) throws InvalidKeyException { + return X25519.computeSharedSecret(privateKey, publicKey); + } +} diff --git a/src/main/java/com/trilead/ssh2/crypto/dh/X25519Provider.java b/src/main/java/com/trilead/ssh2/crypto/dh/X25519Provider.java new file mode 100644 index 00000000..9f4697c5 --- /dev/null +++ b/src/main/java/com/trilead/ssh2/crypto/dh/X25519Provider.java @@ -0,0 +1,17 @@ +package com.trilead.ssh2.crypto.dh; + +import java.security.InvalidKeyException; + +/** + * Interface for X25519 key exchange operations. + * Implementations may use different underlying cryptographic libraries. + */ +public interface X25519Provider { + int KEY_SIZE = 32; + + byte[] generatePrivateKey(); + + byte[] publicFromPrivate(byte[] privateKey) throws InvalidKeyException; + + byte[] computeSharedSecret(byte[] privateKey, byte[] publicKey) throws InvalidKeyException; +} diff --git a/src/main/java/com/trilead/ssh2/crypto/dh/X25519ProviderFactory.java b/src/main/java/com/trilead/ssh2/crypto/dh/X25519ProviderFactory.java new file mode 100644 index 00000000..2ffe61dc --- /dev/null +++ b/src/main/java/com/trilead/ssh2/crypto/dh/X25519ProviderFactory.java @@ -0,0 +1,77 @@ +package com.trilead.ssh2.crypto.dh; + +import com.trilead.ssh2.log.Logger; + +import java.security.KeyPairGenerator; + +/** + * Factory for creating X25519Provider instances. + * Automatically selects the platform-native implementation when available (Java 11+/Android API 33+), + * falling back to Tink otherwise. + */ +public class X25519ProviderFactory { + private static final Logger log = Logger.getLogger(X25519ProviderFactory.class); + private static final X25519Provider INSTANCE; + private static final boolean PLATFORM_NATIVE_AVAILABLE; + + static { + X25519Provider provider = null; + boolean platformNative = false; + + if (isPlatformNativeAvailable()) { + try { + provider = createPlatformProvider(); + platformNative = true; + if (log.isEnabled()) { + log.log(20, "Using platform-native X25519 implementation"); + } + } catch (NoClassDefFoundError | Exception e) { + if (log.isEnabled()) { + log.log(20, "Platform X25519 class loading failed, falling back to Tink"); + } + } + } + + if (provider == null) { + provider = new TinkX25519Provider(); + if (log.isEnabled()) { + log.log(20, "Using Tink X25519 implementation"); + } + } + + INSTANCE = provider; + PLATFORM_NATIVE_AVAILABLE = platformNative; + } + + private static boolean isPlatformNativeAvailable() { + try { + KeyPairGenerator.getInstance("X25519"); + return true; + } catch (Exception e) { + return false; + } + } + + private static X25519Provider createPlatformProvider() { + return new PlatformX25519Provider(); + } + + public static X25519Provider getProvider() { + return INSTANCE; + } + + public static boolean isPlatformNative() { + return PLATFORM_NATIVE_AVAILABLE; + } + + public static X25519Provider getTinkProvider() { + return new TinkX25519Provider(); + } + + public static X25519Provider getPlatformProvider() { + if (!PLATFORM_NATIVE_AVAILABLE) { + throw new UnsupportedOperationException("Platform-native X25519 not available"); + } + return new PlatformX25519Provider(); + } +} diff --git a/src/test/java/com/trilead/ssh2/crypto/dh/Curve25519ExchangeTest.java b/src/test/java/com/trilead/ssh2/crypto/dh/Curve25519ExchangeTest.java index 9c674109..42e294b1 100644 --- a/src/test/java/com/trilead/ssh2/crypto/dh/Curve25519ExchangeTest.java +++ b/src/test/java/com/trilead/ssh2/crypto/dh/Curve25519ExchangeTest.java @@ -1,10 +1,11 @@ package com.trilead.ssh2.crypto.dh; -import com.google.crypto.tink.subtle.X25519; - import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.math.BigInteger; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -33,47 +34,86 @@ private static byte[] toByteArray(String s) { return b; } - @Test - public void selfAgreement() throws Exception { - byte[] alicePrivKey = X25519.generatePrivateKey(); - byte[] alicePubKey = X25519.publicFromPrivate(alicePrivKey); + static Stream providers() { + Stream.Builder builder = Stream.builder(); + builder.add(X25519ProviderFactory.getTinkProvider()); + if (X25519ProviderFactory.isPlatformNative()) { + builder.add(X25519ProviderFactory.getPlatformProvider()); + } + return builder.build(); + } + + @ParameterizedTest + @MethodSource("providers") + public void selfAgreement(X25519Provider provider) throws Exception { + byte[] alicePrivKey = provider.generatePrivateKey(); + byte[] alicePubKey = provider.publicFromPrivate(alicePrivKey); - byte[] bobPrivKey = X25519.generatePrivateKey(); - byte[] bobPubKey = X25519.publicFromPrivate(bobPrivKey); + byte[] bobPrivKey = provider.generatePrivateKey(); + byte[] bobPubKey = provider.publicFromPrivate(bobPrivKey); - Curve25519Exchange alice = new Curve25519Exchange(alicePrivKey); + Curve25519Exchange alice = new Curve25519Exchange(provider, alicePrivKey); alice.setF(bobPubKey); - Curve25519Exchange bob = new Curve25519Exchange(bobPrivKey); + Curve25519Exchange bob = new Curve25519Exchange(provider, bobPrivKey); bob.setF(alicePubKey); assertNotNull(alice.sharedSecret); assertEquals(alice.sharedSecret, bob.sharedSecret); } - @Test - public void deriveAlicePublicKey() throws Exception { - byte[] pubKey = X25519.publicFromPrivate(ALICE_PRIVATE); + @ParameterizedTest + @MethodSource("providers") + public void deriveAlicePublicKey(X25519Provider provider) throws Exception { + byte[] pubKey = provider.publicFromPrivate(ALICE_PRIVATE); assertArrayEquals(ALICE_PUBLIC, pubKey); } - @Test - public void deriveBobPublicKey() throws Exception { - byte[] pubKey = X25519.publicFromPrivate(BOB_PRIVATE); + @ParameterizedTest + @MethodSource("providers") + public void deriveBobPublicKey(X25519Provider provider) throws Exception { + byte[] pubKey = provider.publicFromPrivate(BOB_PRIVATE); assertArrayEquals(BOB_PUBLIC, pubKey); } - @Test - public void knownValues_Alice() throws Exception { - Curve25519Exchange ex = new Curve25519Exchange(ALICE_PRIVATE); + @ParameterizedTest + @MethodSource("providers") + public void knownValues_Alice(X25519Provider provider) throws Exception { + Curve25519Exchange ex = new Curve25519Exchange(provider, ALICE_PRIVATE); ex.setF(BOB_PUBLIC); assertEquals(KNOWN_SHARED_SECRET_BI, ex.sharedSecret); } - @Test - public void knownValues_Bob() throws Exception { - Curve25519Exchange ex = new Curve25519Exchange(BOB_PRIVATE); + @ParameterizedTest + @MethodSource("providers") + public void knownValues_Bob(X25519Provider provider) throws Exception { + Curve25519Exchange ex = new Curve25519Exchange(provider, BOB_PRIVATE); ex.setF(ALICE_PUBLIC); assertEquals(KNOWN_SHARED_SECRET_BI, ex.sharedSecret); } + + @Test + public void crossProviderAgreement() throws Exception { + if (!X25519ProviderFactory.isPlatformNative()) { + return; + } + + X25519Provider tink = X25519ProviderFactory.getTinkProvider(); + X25519Provider platform = X25519ProviderFactory.getPlatformProvider(); + + byte[] alicePrivKey = tink.generatePrivateKey(); + byte[] alicePubKey = tink.publicFromPrivate(alicePrivKey); + + byte[] bobPrivKey = platform.generatePrivateKey(); + byte[] bobPubKey = platform.publicFromPrivate(bobPrivKey); + + Curve25519Exchange alice = new Curve25519Exchange(tink, alicePrivKey); + alice.setF(bobPubKey); + + Curve25519Exchange bob = new Curve25519Exchange(platform, bobPrivKey); + bob.setF(alicePubKey); + + assertNotNull(alice.sharedSecret); + assertEquals(alice.sharedSecret, bob.sharedSecret); + } }