Merge pull request #16 from cloudera/forUpstream

Adding support for public key authentication from strings
This commit is contained in:
Shikhar Bhushan
2011-06-08 11:45:32 -07:00
10 changed files with 215 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)

View File

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