From 27c60cee60cd12a9591157735ece0ca42e9e23f1 Mon Sep 17 00:00:00 2001 From: Philip Langdale Date: Tue, 7 Jun 2011 12:12:04 -0700 Subject: [PATCH] Add support for public key authentication with keys as strings. Currently, only keys as file locations are supported. This change adds support for keys as strings. Significant changes are: 1) Introduction of a new Resource type for keys as strings. 2) Initialization of a key provider with two strings (private and public keys) Leaving the public key null is equivalent to not having a .pub file. 3) Obtaining the reader for the resource is refactored into the resource itself to avoid requiring knowledge of the type outside the resource. The loadKeys and authPublickey convenience methods are not duplicated for the string based loading as we currently don't need them but they could be if desired (although method signature collisions will be a problem). --- src/main/java/net/schmizz/sshj/SSHClient.java | 30 ++++++++++++ .../userauth/keyprovider/FileKeyProvider.java | 3 ++ .../userauth/keyprovider/KeyProviderUtil.java | 44 +++++++++++++++-- .../userauth/keyprovider/OpenSSHKeyFile.java | 48 ++++++++++++++----- .../userauth/keyprovider/PKCS8KeyFile.java | 23 ++++++++- .../userauth/password/AccountResource.java | 8 ++++ .../password/PrivateKeyFileResource.java | 9 ++++ .../password/PrivateKeyStringResource.java | 32 +++++++++++++ .../sshj/userauth/password/Resource.java | 6 +++ 9 files changed, 186 insertions(+), 17 deletions(-) create mode 100644 src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyStringResource.java 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)