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 { @@ -22,4 +26,8 @@ public class AccountResource super(user + "@" + host); } + @Override + public Reader getReader() throws IOException { + return new StringReader(getDetail()); + } } diff --git a/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java b/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java index b3af9615..ea5daf6b 100644 --- a/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java +++ b/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java @@ -16,6 +16,10 @@ package net.schmizz.sshj.userauth.password; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; public class PrivateKeyFileResource extends Resource { @@ -24,4 +28,9 @@ public class PrivateKeyFileResource super(privateKeyFile); } + @Override + public Reader getReader() + throws IOException { + return new InputStreamReader(new FileInputStream(getDetail())); + } } diff --git a/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyStringResource.java b/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyStringResource.java new file mode 100644 index 00000000..6555bec8 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyStringResource.java @@ -0,0 +1,32 @@ +/* + * Copyright 2010, 2011 sshj contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.schmizz.sshj.userauth.password; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + +public class PrivateKeyStringResource extends Resource { + + public PrivateKeyStringResource(String string) { + super(string); + } + + @Override + public Reader getReader() throws IOException { + return new StringReader(getDetail()); + } +} diff --git a/src/main/java/net/schmizz/sshj/userauth/password/Resource.java b/src/main/java/net/schmizz/sshj/userauth/password/Resource.java index 97e4e774..4e7f75ea 100644 --- a/src/main/java/net/schmizz/sshj/userauth/password/Resource.java +++ b/src/main/java/net/schmizz/sshj/userauth/password/Resource.java @@ -15,6 +15,9 @@ */ package net.schmizz.sshj.userauth.password; +import java.io.IOException; +import java.io.Reader; + /** A password-protected resource */ public abstract class Resource { @@ -28,6 +31,9 @@ public abstract class Resource { return detail; } + public abstract Reader getReader() + throws IOException; + @Override public boolean equals(Object o) { if (this == o) diff --git a/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java b/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java index b14b0d8e..144059d7 100644 --- a/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java +++ b/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java @@ -31,6 +31,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.util.Arrays; +import java.util.Scanner; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -113,6 +114,19 @@ public class OpenSSHKeyFileTest { assertEquals(KeyUtil.newDSAPrivateKey(x, p, q, g), dsa.getPrivate()); } + @Test + public void fromString() + throws IOException, GeneralSecurityException { + FileKeyProvider dsa = new OpenSSHKeyFile(); + String privateKey = readFile("src/test/resources/id_dsa"); + String publicKey = readFile("src/test/resources/id_dsa.pub"); + dsa.init(privateKey, publicKey, + PasswordUtils.createOneOff(correctPassphrase)); + assertEquals(dsa.getType(), KeyType.DSA); + assertEquals(KeyUtil.newDSAPublicKey(y, p, q, g), dsa.getPublic()); + assertEquals(KeyUtil.newDSAPrivateKey(x, p, q, g), dsa.getPrivate()); + } + @Before public void setup() throws UnsupportedEncodingException, GeneralSecurityException { @@ -120,4 +134,19 @@ public class OpenSSHKeyFileTest { throw new AssertionError("bouncy castle needed"); } + private String readFile(String pathname) + throws IOException { + + StringBuilder fileContents = new StringBuilder(); + Scanner scanner = new Scanner(new File(pathname)); + String lineSeparator = System.getProperty("line.separator"); + try { + while(scanner.hasNextLine()) { + fileContents.append(scanner.nextLine() + lineSeparator); + } + return fileContents.toString(); + } finally { + scanner.close(); + } + } } \ No newline at end of file