Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions src/main/java/com/trilead/ssh2/crypto/dh/Curve25519Exchange.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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");
}
Expand All @@ -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);
}
Expand All @@ -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];
Expand Down
126 changes: 126 additions & 0 deletions src/main/java/com/trilead/ssh2/crypto/dh/PlatformX25519Provider.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
26 changes: 26 additions & 0 deletions src/main/java/com/trilead/ssh2/crypto/dh/TinkX25519Provider.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/trilead/ssh2/crypto/dh/X25519Provider.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading
Loading