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).
This commit is contained in:
Philip Langdale
2011-06-07 12:12:04 -07:00
parent adf44e2dc0
commit 27c60cee60
9 changed files with 186 additions and 17 deletions

View File

@@ -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).
* <p/>
*
* @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

View File

@@ -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);
}

View File

@@ -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.
* <p/>
* 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.
* <p/>
* 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;
}
}

View File

@@ -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());
}
}
}

View File

@@ -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))

View File

@@ -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<String> {
@@ -22,4 +26,8 @@ public class AccountResource
super(user + "@" + host);
}
@Override
public Reader getReader() throws IOException {
return new StringReader(getDetail());
}
}

View File

@@ -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<File> {
@@ -24,4 +28,9 @@ public class PrivateKeyFileResource
super(privateKeyFile);
}
@Override
public Reader getReader()
throws IOException {
return new InputStreamReader(new FileInputStream(getDetail()));
}
}

View File

@@ -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<String> {
public PrivateKeyStringResource(String string) {
super(string);
}
@Override
public Reader getReader() throws IOException {
return new StringReader(getDetail());
}
}

View File

@@ -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<H> {
@@ -28,6 +31,9 @@ public abstract class Resource<H> {
return detail;
}
public abstract Reader getReader()
throws IOException;
@Override
public boolean equals(Object o) {
if (this == o)