diff --git a/src/main/java/net/schmizz/sshj/SSHClient.java b/src/main/java/net/schmizz/sshj/SSHClient.java index 525ab0f5..dde11f1c 100644 --- a/src/main/java/net/schmizz/sshj/SSHClient.java +++ b/src/main/java/net/schmizz/sshj/SSHClient.java @@ -50,6 +50,7 @@ import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; import net.schmizz.sshj.userauth.keyprovider.KeyPairWrapper; import net.schmizz.sshj.userauth.keyprovider.KeyProvider; import net.schmizz.sshj.userauth.keyprovider.KeyProviderUtil; +import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile; import net.schmizz.sshj.userauth.method.AuthKeyboardInteractive; import net.schmizz.sshj.userauth.method.AuthMethod; import net.schmizz.sshj.userauth.method.AuthPassword; @@ -64,6 +65,8 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; import java.net.SocketAddress; import java.security.KeyPair; import java.security.PublicKey; @@ -501,6 +504,33 @@ public class SSHClient return loadKeys(location, passphrase.toCharArray()); } + /** + * Creates a {@link KeyProvider} instance from passed strings. Currently only PKCS8 format + * private key files are supported (OpenSSH uses this format). + *
+ * + * @param privateKey the private key as a string + * @param publicKey the public key as a string if it's not included with the private key + * @param passwordFinder the {@link PasswordFinder} that can supply the passphrase for decryption (may be {@code + * null} in case keyfile is not encrypted) + * + * @return the key provider ready for use in authentication + * + * @throws SSHException if there was no suitable key provider available for the file format; typically because + * BouncyCastle is not in the classpath + * @throws IOException if the key file format is not known, etc. + */ + public KeyProvider loadKeys(String privateKey, String publicKey, PasswordFinder passwordFinder) + throws IOException { + final FileKeyProvider.Format format = KeyProviderUtil.detectKeyFileFormat(privateKey, publicKey != null); + final FileKeyProvider fkp = Factory.Named.Util.create(trans.getConfig().getFileKeyProviderFactories(), format + .toString()); + if (fkp == null) + throw new SSHException("No provider available for " + format + " key file"); + fkp.init(privateKey, publicKey, passwordFinder); + return fkp; + } + /** * Attempts loading the user's {@code known_hosts} file from the default locations, i.e. {@code ~/.ssh/known_hosts} * and {@code ~/.ssh/known_hosts2} on most platforms. Adds the resulting {@link OpenSSHKnownHosts} object as a host diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/FileKeyProvider.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/FileKeyProvider.java index b4cb885a..cd2075bf 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/FileKeyProvider.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/FileKeyProvider.java @@ -33,4 +33,7 @@ public interface FileKeyProvider void init(File location, PasswordFinder pwdf); + void init(String privateKey, String publicKey); + + void init(String privateKey, String publicKey, PasswordFinder pwdf); } diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java index 6aa643b1..3ece9411 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java @@ -21,6 +21,8 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; public class KeyProviderUtil { @@ -37,13 +39,50 @@ public class KeyProviderUtil { */ public static FileKeyProvider.Format detectKeyFileFormat(File location) throws IOException { - BufferedReader br = new BufferedReader(new FileReader(location)); + return detectKeyFileFormat(new FileReader(location), + new File(location + ".pub").exists()); + } + + /** + * Attempts to detect how a key file is encoded. + * + * Return values are consistent with the {@code NamedFactory} implementations in the {@code keyprovider} package. + * + * @param privateKey Private key stored in a string + * @param separatePubKey Is the public key stored separately from the private key + * + * @return name of the key file format + * + * @throws java.io.IOException + */ + public static FileKeyProvider.Format detectKeyFileFormat(String privateKey, + boolean separatePubKey) + throws IOException { + return detectKeyFileFormat(new StringReader(privateKey), separatePubKey); + } + + /** + * Attempts to detect how a key file is encoded. + * + * Return values are consistent with the {@code NamedFactory} implementations in the {@code keyprovider} package. + * + * @param privateKey Private key accessible through a {@code Reader} + * @param separatePubKey Is the public key stored separately from the private key + * + * @return name of the key file format + * + * @throws java.io.IOException + */ + private static FileKeyProvider.Format detectKeyFileFormat(Reader privateKey, + boolean separatePubKey) + throws IOException { + BufferedReader br = new BufferedReader(privateKey); String firstLine = br.readLine(); IOUtils.closeQuietly(br); if (firstLine == null) throw new IOException("Empty file"); if (firstLine.startsWith("-----BEGIN") && firstLine.endsWith("PRIVATE KEY-----")) - if (new File(location + ".pub").exists()) + if (separatePubKey) // Can delay asking for password since have unencrypted pubkey return FileKeyProvider.Format.OpenSSH; else @@ -54,5 +93,4 @@ public class KeyProviderUtil { */ return FileKeyProvider.Format.Unknown; } - } diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/OpenSSHKeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/OpenSSHKeyFile.java index 6f75699d..cd18aa1e 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/OpenSSHKeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/OpenSSHKeyFile.java @@ -23,8 +23,11 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; import java.security.PublicKey; + /** * Represents an OpenSSH identity that consists of a PKCS8-encoded private key file and an unencrypted public key file * of the same name with the {@code ".pub"} extension. This allows to delay requesting of the passphrase until the @@ -62,18 +65,7 @@ public class OpenSSHKeyFile final File f = new File(location + ".pub"); if (f.exists()) try { - final BufferedReader br = new BufferedReader(new FileReader(f)); - try { - final String keydata = br.readLine(); - if (keydata != null) { - String[] parts = keydata.split(" "); - assert parts.length >= 2; - type = KeyType.fromString(parts[0]); - pubKey = new Buffer.PlainBuffer(Base64.decode(parts[1])).readPublicKey(); - } - } finally { - br.close(); - } + initPubKey(new FileReader(f)); } catch (IOException e) { // let super provide both public & private key log.warn("Error reading public key file: {}", e.toString()); @@ -81,4 +73,36 @@ public class OpenSSHKeyFile super.init(location); } + @Override + public void init(String privateKey, String publicKey) { + if (publicKey != null) { + initPubKey(new StringReader(publicKey)); + } + super.init(privateKey, null); + } + + /** + * Read and store the separate public key provided alongside the private key + * + * @param publicKey Public key accessible through a {@code Reader} + */ + private void initPubKey(Reader publicKey) { + try { + final BufferedReader br = new BufferedReader(publicKey); + try { + final String keydata = br.readLine(); + if (keydata != null) { + String[] parts = keydata.split(" "); + assert parts.length >= 2; + type = KeyType.fromString(parts[0]); + pubKey = new Buffer.PlainBuffer(Base64.decode(parts[1])).readPublicKey(); + } + } finally { + br.close(); + } + } catch (IOException e) { + // let super provide both public & private key + log.warn("Error reading public key: {}", e.toString()); + } + } } diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java index 45a1b775..b34758bf 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java @@ -20,6 +20,9 @@ import net.schmizz.sshj.common.KeyType; import net.schmizz.sshj.userauth.password.PasswordFinder; import net.schmizz.sshj.userauth.password.PasswordUtils; import net.schmizz.sshj.userauth.password.PrivateKeyFileResource; +import net.schmizz.sshj.userauth.password.PrivateKeyStringResource; +import net.schmizz.sshj.userauth.password.Resource; + import org.bouncycastle.openssl.EncryptionException; import org.bouncycastle.openssl.PEMReader; import org.slf4j.Logger; @@ -29,6 +32,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; @@ -52,7 +57,8 @@ public class PKCS8KeyFile protected final Logger log = LoggerFactory.getLogger(getClass()); protected PasswordFinder pwdf; - protected PrivateKeyFileResource resource; + @SuppressWarnings("unchecked") + protected Resource resource; protected KeyPair kp; protected KeyType type; @@ -89,6 +95,19 @@ public class PKCS8KeyFile this.pwdf = pwdf; } + @Override + public void init(String privateKey, String publicKey) { + assert privateKey != null; + assert publicKey == null; + resource = new PrivateKeyStringResource(privateKey); + } + + @Override + public void init(String privateKey, String publicKey, PasswordFinder pwdf) { + init(privateKey, publicKey); + this.pwdf = pwdf; + } + protected org.bouncycastle.openssl.PasswordFinder makeBouncyPasswordFinder() { if (pwdf == null) return null; @@ -111,7 +130,7 @@ public class PKCS8KeyFile for (; ;) { // while the PasswordFinder tells us we should retry try { - r = new PEMReader(new InputStreamReader(new FileInputStream(resource.getDetail())), pFinder); + r = new PEMReader(resource.getReader(), pFinder); o = r.readObject(); } catch (EncryptionException e) { if (pwdf.shouldRetry(resource)) diff --git a/src/main/java/net/schmizz/sshj/userauth/password/AccountResource.java b/src/main/java/net/schmizz/sshj/userauth/password/AccountResource.java index 6a25a742..82620896 100644 --- a/src/main/java/net/schmizz/sshj/userauth/password/AccountResource.java +++ b/src/main/java/net/schmizz/sshj/userauth/password/AccountResource.java @@ -15,6 +15,10 @@ */ package net.schmizz.sshj.userauth.password; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + public class AccountResource extends Resource