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
9 changes: 9 additions & 0 deletions src/main/java/com/trilead/ssh2/crypto/OpenSSHKeyEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.ECPoint;
import java.security.spec.InvalidKeySpecException;
Expand Down Expand Up @@ -648,6 +649,14 @@ public static String exportOpenSSH(PrivateKey privateKey, PublicKey publicKey, S
throws InvalidKeyException {
if (privateKey instanceof RSAPrivateCrtKey && publicKey instanceof RSAPublicKey) {
return exportOpenSSHRSA((RSAPrivateCrtKey) privateKey, (RSAPublicKey) publicKey, comment, passphrase);
} else if (privateKey instanceof RSAPrivateKey && publicKey instanceof RSAPublicKey) {
// Handle non-CRT RSA keys (e.g., from Conscrypt's OpenSSLRSAPrivateKey)
try {
RSAPrivateCrtKey crtKey = PEMEncoder.convertToRSAPrivateCrtKey((RSAPrivateKey) privateKey);
return exportOpenSSHRSA(crtKey, (RSAPublicKey) publicKey, comment, passphrase);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new InvalidKeyException("Failed to convert RSA key to CRT format", e);
}
} else if (privateKey instanceof DSAPrivateKey && publicKey instanceof DSAPublicKey) {
return exportOpenSSHDSA((DSAPrivateKey) privateKey, (DSAPublicKey) publicKey, comment, passphrase);
} else if (privateKey instanceof ECPrivateKey && publicKey instanceof ECPublicKey) {
Expand Down
92 changes: 91 additions & 1 deletion src/main/java/com/trilead/ssh2/crypto/PEMEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.trilead.ssh2.crypto.cipher.BlockCipher;
import com.trilead.ssh2.crypto.cipher.DES;
import com.trilead.ssh2.crypto.cipher.DESede;
import com.trilead.ssh2.crypto.keys.Ed25519PrivateKey;
import com.trilead.ssh2.signature.ECDSASHA2Verify;

import java.io.IOException;
Expand All @@ -15,10 +16,14 @@
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.KeyFactory;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.ECFieldFp;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.util.Locale;
Expand Down Expand Up @@ -203,7 +208,7 @@ public static String encodePrivateKey(PrivateKey privateKey, String password)
/**
* Encode private key to PEM format with auto-detected key type and specified algorithm.
*
* @param privateKey private key (RSA, DSA, or EC)
* @param privateKey private key (RSA, DSA, EC, or Ed25519)
* @param password password for encryption, or null for unencrypted
* @param algorithm encryption algorithm
* @return PEM-encoded private key
Expand All @@ -214,15 +219,100 @@ public static String encodePrivateKey(PrivateKey privateKey, String password, St
throws IOException, InvalidKeyException {
if (privateKey instanceof RSAPrivateCrtKey) {
return encodeRSAPrivateKey((RSAPrivateCrtKey) privateKey, password, algorithm);
} else if (privateKey instanceof RSAPrivateKey) {
// Handle non-CRT RSA keys (e.g., from Conscrypt's OpenSSLRSAPrivateKey)
try {
RSAPrivateCrtKey crtKey = convertToRSAPrivateCrtKey((RSAPrivateKey) privateKey);
return encodeRSAPrivateKey(crtKey, password, algorithm);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new InvalidKeyException("Failed to convert RSA key to CRT format", e);
}
} else if (privateKey instanceof DSAPrivateKey) {
return encodeDSAPrivateKey((DSAPrivateKey) privateKey, password, algorithm);
} else if (privateKey instanceof ECPrivateKey) {
return encodeECPrivateKey((ECPrivateKey) privateKey, password, algorithm);
} else if (privateKey instanceof Ed25519PrivateKey) {
return encodeEd25519PrivateKey((Ed25519PrivateKey) privateKey, password, algorithm);
} else {
throw new InvalidKeyException("Unsupported key type: " + privateKey.getClass().getName());
}
}

/**
* Encode Ed25519 private key to PEM format (PKCS#8).
*
* @param privateKey Ed25519 private key
* @param password password for encryption, or null for unencrypted
* @return PEM-encoded private key
* @throws IOException if encoding fails
*/
public static String encodeEd25519PrivateKey(Ed25519PrivateKey privateKey, String password) throws IOException {
return encodeEd25519PrivateKey(privateKey, password, DEFAULT_ENCRYPTION);
}

/**
* Encode Ed25519 private key to PEM format (PKCS#8) with specified encryption algorithm.
*
* @param privateKey Ed25519 private key
* @param password password for encryption, or null for unencrypted
* @param algorithm encryption algorithm
* @return PEM-encoded private key
* @throws IOException if encoding fails
*/
public static String encodeEd25519PrivateKey(Ed25519PrivateKey privateKey, String password, String algorithm)
throws IOException {
// Ed25519 uses PKCS#8 format (BEGIN PRIVATE KEY)
byte[] encoded = privateKey.getEncoded();
return formatPEM("PRIVATE KEY", encoded, password, algorithm);
}

/**
* Converts an RSAPrivateKey to RSAPrivateCrtKey by parsing the PKCS#8 encoded form.
* This is needed for keys from providers like Conscrypt that don't implement RSAPrivateCrtKey.
*
* @param privateKey The RSA private key to convert
* @return The RSAPrivateCrtKey with CRT parameters
* @throws InvalidKeySpecException if the key cannot be parsed
* @throws NoSuchAlgorithmException if RSA algorithm is not available
*/
static RSAPrivateCrtKey convertToRSAPrivateCrtKey(RSAPrivateKey privateKey)
throws InvalidKeySpecException, NoSuchAlgorithmException {
byte[] encoded = privateKey.getEncoded();
try {
SimpleDERReader reader = new SimpleDERReader(encoded);
reader.resetInput(reader.readSequenceAsByteArray());
if (!reader.readInt().equals(BigInteger.ZERO)) {
throw new InvalidKeySpecException("PKCS#8 is not version 0");
}

reader.readSequenceAsByteArray(); // OID sequence
reader.resetInput(reader.readOctetString()); // RSA key bytes
reader.resetInput(reader.readSequenceAsByteArray()); // RSA key sequence

if (!reader.readInt().equals(BigInteger.ZERO)) {
throw new InvalidKeySpecException("RSA key is not version 0");
}

BigInteger modulus = reader.readInt();
BigInteger publicExponent = reader.readInt();
BigInteger privateExponent = reader.readInt();
BigInteger primeP = reader.readInt();
BigInteger primeQ = reader.readInt();
BigInteger primeExponentP = reader.readInt();
BigInteger primeExponentQ = reader.readInt();
BigInteger crtCoefficient = reader.readInt();

RSAPrivateCrtKeySpec spec = new RSAPrivateCrtKeySpec(
modulus, publicExponent, privateExponent,
primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient);

KeyFactory kf = KeyFactory.getInstance("RSA");
return (RSAPrivateCrtKey) kf.generatePrivate(spec);
} catch (IOException e) {
throw new InvalidKeySpecException("Could not parse RSA key", e);
}
}

private static String formatPEM(String type, byte[] data, String password, String algorithm) throws IOException {
byte[] encodedData = data;

Expand Down
99 changes: 99 additions & 0 deletions src/test/java/com/trilead/ssh2/crypto/OpenSSHKeyEncoderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.ECGenParameterSpec;

Expand Down Expand Up @@ -309,4 +311,101 @@ public void testDecodeGoldenEd25519AndReencode() throws Exception {
assertEquals(kp.getPublic(), decoded.getPublic());
assertEquals(kp.getPrivate(), decoded.getPrivate());
}

/**
* Tests that non-CRT RSA keys (like Conscrypt's OpenSSLRSAPrivateKey) can be exported.
* This simulates the scenario where an RSAPrivateKey does not implement RSAPrivateCrtKey.
*/
@Test
public void testExportOpenSSHWithNonCrtRSAKey() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair original = kpg.generateKeyPair();

// Wrap the RSA private key to simulate a non-CRT key (like OpenSSLRSAPrivateKey)
RSAPrivateKey nonCrtKey = new NonCrtRSAPrivateKeyWrapper((RSAPrivateCrtKey) original.getPrivate());

// Verify our wrapper is not an instance of RSAPrivateCrtKey
assertTrue(nonCrtKey instanceof RSAPrivateKey);
assertTrue(!(nonCrtKey instanceof RSAPrivateCrtKey));

// Export using the generic method which should handle non-CRT keys
String exported = OpenSSHKeyEncoder.exportOpenSSH(
nonCrtKey,
original.getPublic(),
"test-non-crt");

assertNotNull(exported);
assertTrue(exported.contains("-----BEGIN OPENSSH PRIVATE KEY-----"));
assertTrue(exported.contains("-----END OPENSSH PRIVATE KEY-----"));

// Verify round-trip
KeyPair decoded = PEMDecoder.decode(exported.toCharArray(), null);

assertEquals(original.getPublic(), decoded.getPublic());
assertEquals(original.getPrivate(), decoded.getPrivate());
}

/**
* Tests that non-CRT RSA keys can be exported with encryption.
*/
@Test
public void testExportOpenSSHWithNonCrtRSAKeyEncrypted() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair original = kpg.generateKeyPair();

RSAPrivateKey nonCrtKey = new NonCrtRSAPrivateKeyWrapper((RSAPrivateCrtKey) original.getPrivate());

String exported = OpenSSHKeyEncoder.exportOpenSSH(
nonCrtKey,
original.getPublic(),
"test-non-crt",
"testpassword");

assertNotNull(exported);
assertTrue(exported.contains("-----BEGIN OPENSSH PRIVATE KEY-----"));

KeyPair decoded = PEMDecoder.decode(exported.toCharArray(), "testpassword");

assertEquals(original.getPublic(), decoded.getPublic());
assertEquals(original.getPrivate(), decoded.getPrivate());
}

/**
* A wrapper that implements RSAPrivateKey but NOT RSAPrivateCrtKey.
* This simulates keys from providers like Conscrypt's OpenSSLRSAPrivateKey.
*/
private static class NonCrtRSAPrivateKeyWrapper implements RSAPrivateKey {
private final RSAPrivateCrtKey delegate;

NonCrtRSAPrivateKeyWrapper(RSAPrivateCrtKey delegate) {
this.delegate = delegate;
}

@Override
public BigInteger getPrivateExponent() {
return delegate.getPrivateExponent();
}

@Override
public String getAlgorithm() {
return delegate.getAlgorithm();
}

@Override
public String getFormat() {
return delegate.getFormat();
}

@Override
public byte[] getEncoded() {
return delegate.getEncoded();
}

@Override
public BigInteger getModulus() {
return delegate.getModulus();
}
}
}
Loading
Loading