Merge pull request #279 from hierynomus/issue-276

Support for OpenSSH new key file format (fixes #276)
This commit is contained in:
Jeroen van Erp
2016-10-31 09:55:47 +01:00
committed by GitHub
12 changed files with 344 additions and 257 deletions

View File

@@ -23,6 +23,8 @@ import net.schmizz.sshj.common.SSHRuntimeException;
import java.util.Arrays; import java.util.Arrays;
import static net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable.CURVE_ED25519_SHA512;
/** /**
* Our own extension of the EdDSAPublicKey that comes from ECC-25519, as that class does not implement equality. * Our own extension of the EdDSAPublicKey that comes from ECC-25519, as that class does not implement equality.
* The code uses the equality of the keys as an indicator whether they're the same during host key verification. * The code uses the equality of the keys as an indicator whether they're the same during host key verification.
@@ -32,7 +34,7 @@ public class Ed25519PublicKey extends EdDSAPublicKey {
public Ed25519PublicKey(EdDSAPublicKeySpec spec) { public Ed25519PublicKey(EdDSAPublicKeySpec spec) {
super(spec); super(spec);
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("ed25519-sha-512"); EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName(CURVE_ED25519_SHA512);
if (!spec.getParams().getCurve().equals(ed25519.getCurve())) { if (!spec.getParams().getCurve().equals(ed25519.getCurve())) {
throw new SSHRuntimeException("Cannot create Ed25519 Public Key from wrong spec"); throw new SSHRuntimeException("Cannot create Ed25519 Public Key from wrong spec");
} }

View File

@@ -0,0 +1,160 @@
/*
* Copyright (C)2009 - 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 com.hierynomus.sshj.userauth.keyprovider;
import java.io.BufferedReader;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PublicKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import net.schmizz.sshj.common.*;
import net.schmizz.sshj.common.Buffer.PlainBuffer;
import net.schmizz.sshj.userauth.keyprovider.BaseFileKeyProvider;
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
import net.schmizz.sshj.userauth.keyprovider.KeyFormat;
import static net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable.CURVE_ED25519_SHA512;
/**
* Reads a key file in the new OpenSSH format.
* The format is described in the following document: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
*/
public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
private static final Logger logger = LoggerFactory.getLogger(OpenSSHKeyV1KeyFile.class);
private static final String BEGIN = "-----BEGIN ";
private static final String END = "-----END ";
private static final byte[] AUTH_MAGIC = "openssh-key-v1\0".getBytes();
public static final String OPENSSH_PRIVATE_KEY = "OPENSSH PRIVATE KEY-----";
public static class Factory
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
@Override
public FileKeyProvider create() {
return new OpenSSHKeyV1KeyFile();
}
@Override
public String getName() {
return KeyFormat.OpenSSHv1.name();
}
}
@Override
protected KeyPair readKeyPair() throws IOException {
BufferedReader reader = new BufferedReader(resource.getReader());
try {
if (!checkHeader(reader)) {
throw new IOException("This key is not in 'openssh-key-v1' format");
}
String keyFile = readKeyFile(reader);
byte[] decode = Base64.decode(keyFile);
PlainBuffer keyBuffer = new PlainBuffer(decode);
return readDecodedKeyPair(keyBuffer);
} catch (GeneralSecurityException e) {
throw new SSHRuntimeException(e);
} finally {
IOUtils.closeQuietly(reader);
}
}
private KeyPair readDecodedKeyPair(final PlainBuffer keyBuffer) throws IOException, GeneralSecurityException {
byte[] bytes = new byte[AUTH_MAGIC.length];
keyBuffer.readRawBytes(bytes); // byte[] AUTH_MAGIC
if (!ByteArrayUtils.equals(bytes, 0, AUTH_MAGIC, 0, AUTH_MAGIC.length)) {
throw new IOException("This key does not contain the 'openssh-key-v1' format magic header");
}
String cipherName = keyBuffer.readString(); // string ciphername
String kdfName = keyBuffer.readString(); // string kdfname
String kdfOptions = keyBuffer.readString(); // string kdfoptions
int nrKeys = keyBuffer.readUInt32AsInt(); // int number of keys N; Should be 1
if (nrKeys != 1) {
throw new IOException("We don't support having more than 1 key in the file (yet).");
}
PublicKey publicKey = readPublicKey(new PlainBuffer(keyBuffer.readBytes())); // string publickey1
PlainBuffer privateKeyBuffer = new PlainBuffer(keyBuffer.readBytes()); // string (possibly) encrypted, padded list of private keys
if ("none".equals(cipherName)) {
logger.debug("Reading unencrypted keypair");
return readUnencrypted(privateKeyBuffer, publicKey);
} else {
logger.info("Keypair is encrypted with: " + cipherName + ", " + kdfName + ", " + kdfOptions);
throw new IOException("Cannot read encrypted keypair with " + cipherName + " yet.");
}
}
private PublicKey readPublicKey(final PlainBuffer plainBuffer) throws Buffer.BufferException, GeneralSecurityException {
return KeyType.fromString(plainBuffer.readString()).readPubKeyFromBuffer(plainBuffer);
}
private String readKeyFile(final BufferedReader reader) throws IOException {
StringBuilder sb = new StringBuilder();
String line = reader.readLine();
while (!line.startsWith(END)) {
sb.append(line);
line = reader.readLine();
}
return sb.toString();
}
private boolean checkHeader(final BufferedReader reader) throws IOException {
String line = reader.readLine();
while (line != null && !line.startsWith(BEGIN)) {
line = reader.readLine();
}
line = line.substring(BEGIN.length());
return line.startsWith(OPENSSH_PRIVATE_KEY);
}
private KeyPair readUnencrypted(final PlainBuffer keyBuffer, final PublicKey publicKey) throws IOException, GeneralSecurityException {
int privKeyListSize = keyBuffer.available();
if (privKeyListSize % 8 != 0) {
throw new IOException("The private key section must be a multiple of the block size (8)");
}
int checkInt1 = keyBuffer.readUInt32AsInt(); // uint32 checkint1
int checkInt2 = keyBuffer.readUInt32AsInt(); // uint32 checkint2
if (checkInt1 != checkInt2) {
throw new IOException("The checkInts differed, the key was not correctly decoded.");
}
// The private key section contains both the public key and the private key
String keyType = keyBuffer.readString(); // string keytype
logger.info("Read key type: {}", keyType);
byte[] pubKey = keyBuffer.readBytes(); // string publickey (again...)
keyBuffer.readUInt32();
byte[] privKey = new byte[32];
keyBuffer.readRawBytes(privKey); // string privatekey
keyBuffer.readRawBytes(new byte[32]); // string publickey (again...)
String comment = keyBuffer.readString(); // string comment
byte[] padding = new byte[keyBuffer.available()];
keyBuffer.readRawBytes(padding); // char[] padding
for (int i = 0; i < padding.length; i++) {
if ((int) padding[i] != i + 1) {
throw new IOException("Padding of key format contained wrong byte at position: " + i);
}
}
return new KeyPair(publicKey, new EdDSAPrivateKey(new EdDSAPrivateKeySpec(privKey, EdDSANamedCurveTable.getByName(CURVE_ED25519_SHA512))));
}
}

View File

@@ -33,6 +33,7 @@ import net.schmizz.sshj.transport.random.BouncyCastleRandom;
import net.schmizz.sshj.transport.random.JCERandom; import net.schmizz.sshj.transport.random.JCERandom;
import net.schmizz.sshj.transport.random.SingletonRandomFactory; import net.schmizz.sshj.transport.random.SingletonRandomFactory;
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile; import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile;
import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile; import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile;
import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile; import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -113,7 +114,7 @@ public class DefaultConfig
protected void initFileKeyProviderFactories(boolean bouncyCastleRegistered) { protected void initFileKeyProviderFactories(boolean bouncyCastleRegistered) {
if (bouncyCastleRegistered) { if (bouncyCastleRegistered) {
setFileKeyProviderFactories(new PKCS8KeyFile.Factory(), new OpenSSHKeyFile.Factory(), new PuTTYKeyFile.Factory()); setFileKeyProviderFactories(new OpenSSHKeyV1KeyFile.Factory(), new PKCS8KeyFile.Factory(), new OpenSSHKeyFile.Factory(), new PuTTYKeyFile.Factory());
} }
} }

View File

@@ -316,7 +316,7 @@ public class SSHClient
public void authPublickey(String username) public void authPublickey(String username)
throws UserAuthException, TransportException { throws UserAuthException, TransportException {
final String base = System.getProperty("user.home") + File.separator + ".ssh" + File.separator; final String base = System.getProperty("user.home") + File.separator + ".ssh" + File.separator;
authPublickey(username, base + "id_rsa", base + "id_dsa"); authPublickey(username, base + "id_rsa", base + "id_dsa", base + "id_ed25519", base + "id_ecdsa");
} }
/** /**
@@ -524,8 +524,13 @@ public class SSHClient
} }
/** /**
* Creates a {@link KeyProvider} instance from given location on the file system. Currently only PKCS8 format * Creates a {@link KeyProvider} instance from given location on the file system. Currently the following private key files are supported:
* private key files are supported (OpenSSH uses this format). * <ul>
* <li>PKCS8 (OpenSSH uses this format)</li>
* <li>PKCS5</li>
* <li>Putty keyfile</li>
* <li>openssh-key-v1 (New OpenSSH keyfile format)</li>
* </ul>
* <p/> * <p/>
* *
* @param location the location of the key file * @param location the location of the key file

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C)2009 - 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.keyprovider;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.userauth.password.*;
public abstract class BaseFileKeyProvider implements FileKeyProvider {
protected Resource<?> resource;
protected PasswordFinder pwdf;
protected KeyPair kp;
protected KeyType type;
@Override
public void init(Reader location) {
assert location != null;
resource = new PrivateKeyReaderResource(location);
}
@Override
public void init(Reader location, PasswordFinder pwdf) {
init(location);
this.pwdf = pwdf;
}
@Override
public void init(File location) {
assert location != null;
resource = new PrivateKeyFileResource(location.getAbsoluteFile());
}
@Override
public void init(File location, PasswordFinder pwdf) {
init(location);
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;
}
@Override
public PrivateKey getPrivate()
throws IOException {
return kp != null ? kp.getPrivate() : (kp = readKeyPair()).getPrivate();
}
@Override
public PublicKey getPublic()
throws IOException {
return kp != null ? kp.getPublic() : (kp = readKeyPair()).getPublic();
}
@Override
public KeyType getType()
throws IOException {
return type != null ? type : (type = KeyType.fromKey(getPublic()));
}
protected abstract KeyPair readKeyPair() throws IOException;
}

View File

@@ -22,6 +22,7 @@ public enum KeyFormat {
PKCS5, PKCS5,
PKCS8, PKCS8,
OpenSSH, OpenSSH,
OpenSSHv1,
PuTTY, PuTTY,
Unknown Unknown
} }

View File

@@ -18,6 +18,7 @@ package net.schmizz.sshj.userauth.keyprovider;
import net.schmizz.sshj.common.IOUtils; import net.schmizz.sshj.common.IOUtils;
import java.io.*; import java.io.*;
import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile;
public class KeyProviderUtil { public class KeyProviderUtil {
@@ -88,10 +89,12 @@ public class KeyProviderUtil {
private static KeyFormat keyFormatFromHeader(String header, boolean separatePubKey) { private static KeyFormat keyFormatFromHeader(String header, boolean separatePubKey) {
if (header.startsWith("-----BEGIN") && header.endsWith("PRIVATE KEY-----")) { if (header.startsWith("-----BEGIN") && header.endsWith("PRIVATE KEY-----")) {
if (separatePubKey) { if (separatePubKey && header.contains(OpenSSHKeyV1KeyFile.OPENSSH_PRIVATE_KEY)) {
return KeyFormat.OpenSSHv1;
} else if (separatePubKey) {
// Can delay asking for password since have unencrypted pubkey // Can delay asking for password since have unencrypted pubkey
return KeyFormat.OpenSSH; return KeyFormat.OpenSSH;
} else if (header.indexOf("BEGIN PRIVATE KEY") != -1 || header.indexOf("BEGIN ENCRYPTED PRIVATE KEY") != -1) { } else if (header.contains("BEGIN PRIVATE KEY") || header.contains("BEGIN ENCRYPTED PRIVATE KEY")) {
return KeyFormat.PKCS8; return KeyFormat.PKCS8;
} else { } else {
return KeyFormat.PKCS5; return KeyFormat.PKCS5;

View File

@@ -15,28 +15,28 @@
*/ */
package net.schmizz.sshj.userauth.keyprovider; package net.schmizz.sshj.userauth.keyprovider;
import net.schmizz.sshj.common.Base64; import java.io.BufferedReader;
import net.schmizz.sshj.common.IOUtils; import java.io.EOFException;
import net.schmizz.sshj.common.KeyType; import java.io.IOException;
import net.schmizz.sshj.transport.cipher.*;
import net.schmizz.sshj.transport.digest.Digest;
import net.schmizz.sshj.transport.digest.MD5;
import net.schmizz.sshj.userauth.password.*;
import javax.xml.bind.DatatypeConverter;
import java.io.*;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.security.*; import java.security.*;
import java.security.spec.*; import java.security.spec.*;
import java.util.Arrays; import java.util.Arrays;
import javax.xml.bind.DatatypeConverter;
import net.schmizz.sshj.common.Base64;
import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.transport.cipher.*;
import net.schmizz.sshj.transport.digest.Digest;
import net.schmizz.sshj.transport.digest.MD5;
/** /**
* Represents a PKCS5-encoded key file. This is the format typically used by OpenSSH, OpenSSL, Amazon, etc. * Represents a PKCS5-encoded key file. This is the format typically used by OpenSSH, OpenSSL, Amazon, etc.
*/ */
public class PKCS5KeyFile public class PKCS5KeyFile extends BaseFileKeyProvider {
implements FileKeyProvider {
public static class Factory public static class Factory
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> { implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
@@ -74,67 +74,8 @@ public class PKCS5KeyFile
} }
} }
protected PasswordFinder pwdf;
protected Resource<?> resource;
protected KeyPair kp;
protected KeyType type;
protected byte[] data; protected byte[] data;
@Override
public PrivateKey getPrivate()
throws IOException {
return kp != null ? kp.getPrivate() : (kp = readKeyPair()).getPrivate();
}
@Override
public PublicKey getPublic()
throws IOException {
return kp != null ? kp.getPublic() : (kp = readKeyPair()).getPublic();
}
@Override
public KeyType getType()
throws IOException {
return type != null ? type : (type = KeyType.fromKey(getPublic()));
}
@Override
public void init(Reader location) {
assert location != null;
resource = new PrivateKeyReaderResource(location);
}
@Override
public void init(Reader location, PasswordFinder pwdf) {
init(location);
this.pwdf = pwdf;
}
@Override
public void init(File location) {
assert location != null;
resource = new PrivateKeyFileResource(location.getAbsoluteFile());
}
@Override
public void init(File location, PasswordFinder pwdf) {
init(location);
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 KeyPair readKeyPair() protected KeyPair readKeyPair()
throws IOException { throws IOException {

View File

@@ -15,9 +15,8 @@
*/ */
package net.schmizz.sshj.userauth.keyprovider; package net.schmizz.sshj.userauth.keyprovider;
import net.schmizz.sshj.common.IOUtils; import java.io.IOException;
import net.schmizz.sshj.common.KeyType; import java.security.KeyPair;
import net.schmizz.sshj.userauth.password.*;
import org.bouncycastle.openssl.EncryptionException; import org.bouncycastle.openssl.EncryptionException;
import org.bouncycastle.openssl.PEMEncryptedKeyPair; import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMKeyPair;
@@ -27,16 +26,11 @@ import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File; import net.schmizz.sshj.common.IOUtils;
import java.io.IOException; import net.schmizz.sshj.userauth.password.PasswordUtils;
import java.io.Reader;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
/** Represents a PKCS8-encoded key file. This is the format used by OpenSSH and OpenSSL. */ /** Represents a PKCS8-encoded key file. This is the format used by (old-style) OpenSSH and OpenSSL. */
public class PKCS8KeyFile public class PKCS8KeyFile extends BaseFileKeyProvider {
implements FileKeyProvider {
public static class Factory public static class Factory
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> { implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
@@ -53,68 +47,9 @@ public class PKCS8KeyFile
} }
protected final Logger log = LoggerFactory.getLogger(getClass()); protected final Logger log = LoggerFactory.getLogger(getClass());
protected PasswordFinder pwdf;
protected Resource<?> resource;
protected KeyPair kp;
protected KeyType type;
protected char[] passphrase; // for blanking out protected char[] passphrase; // for blanking out
@Override
public PrivateKey getPrivate()
throws IOException {
return kp != null ? kp.getPrivate() : (kp = readKeyPair()).getPrivate();
}
@Override
public PublicKey getPublic()
throws IOException {
return kp != null ? kp.getPublic() : (kp = readKeyPair()).getPublic();
}
@Override
public KeyType getType()
throws IOException {
return type != null ? type : (type = KeyType.fromKey(getPublic()));
}
@Override
public void init(Reader location) {
assert location != null;
resource = new PrivateKeyReaderResource(location);
}
@Override
public void init(Reader location, PasswordFinder pwdf) {
init(location);
this.pwdf = pwdf;
}
@Override
public void init(File location) {
assert location != null;
resource = new PrivateKeyFileResource(location.getAbsoluteFile());
}
@Override
public void init(File location, PasswordFinder pwdf) {
init(location);
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 KeyPair readKeyPair() protected KeyPair readKeyPair()
throws IOException { throws IOException {

View File

@@ -15,21 +15,21 @@
*/ */
package net.schmizz.sshj.userauth.keyprovider; package net.schmizz.sshj.userauth.keyprovider;
import net.schmizz.sshj.common.Base64;
import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.userauth.password.*;
import org.bouncycastle.util.encoders.Hex;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*; import java.io.*;
import java.math.BigInteger; import java.math.BigInteger;
import java.security.*; import java.security.*;
import java.security.spec.*; import java.security.spec.*;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.util.encoders.Hex;
import net.schmizz.sshj.common.Base64;
import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.userauth.password.PasswordUtils;
/** /**
* <h2>Sample PuTTY file format</h2> * <h2>Sample PuTTY file format</h2>
@@ -56,7 +56,7 @@ import java.util.Map;
* *
* @version $Id:$ * @version $Id:$
*/ */
public class PuTTYKeyFile implements FileKeyProvider { public class PuTTYKeyFile extends BaseFileKeyProvider {
public static class Factory public static class Factory
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> { implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
@@ -75,56 +75,6 @@ public class PuTTYKeyFile implements FileKeyProvider {
private byte[] privateKey; private byte[] privateKey;
private byte[] publicKey; private byte[] publicKey;
private KeyPair kp;
protected PasswordFinder pwdf;
protected Resource<?> resource;
@Override
public void init(Reader location) {
this.resource = new PrivateKeyReaderResource(location);
}
public void init(Reader location, PasswordFinder pwdf) {
this.init(location);
this.pwdf = pwdf;
}
@Override
public void init(File location) {
resource = new PrivateKeyFileResource(location.getAbsoluteFile());
}
@Override
public void init(File location, PasswordFinder pwdf) {
this.init(location);
this.pwdf = pwdf;
}
@Override
public void init(String privateKey, String publicKey) {
resource = new PrivateKeyStringResource(privateKey);
}
@Override
public void init(String privateKey, String publicKey, PasswordFinder pwdf) {
init(privateKey, publicKey);
this.pwdf = pwdf;
}
@Override
public PrivateKey getPrivate()
throws IOException {
return kp != null ? kp.getPrivate() : (kp = this.readKeyPair()).getPrivate();
}
@Override
public PublicKey getPublic()
throws IOException {
return kp != null ? kp.getPublic() : (kp = this.readKeyPair()).getPublic();
}
/** /**
* Key type. Either "ssh-rsa" for RSA key, or "ssh-dss" for DSA key. * Key type. Either "ssh-rsa" for RSA key, or "ssh-dss" for DSA key.
*/ */
@@ -150,7 +100,7 @@ public class PuTTYKeyFile implements FileKeyProvider {
protected KeyPair readKeyPair() throws IOException { protected KeyPair readKeyPair() throws IOException {
this.parseKeyPair(); this.parseKeyPair();
if(KeyType.RSA.equals(this.getType())) { if (KeyType.RSA.equals(this.getType())) {
final KeyReader publicKeyReader = new KeyReader(publicKey); final KeyReader publicKeyReader = new KeyReader(publicKey);
publicKeyReader.skip(); // skip this publicKeyReader.skip(); // skip this
// public key exponent // public key exponent
@@ -165,8 +115,7 @@ public class PuTTYKeyFile implements FileKeyProvider {
final KeyFactory factory; final KeyFactory factory;
try { try {
factory = KeyFactory.getInstance("RSA"); factory = KeyFactory.getInstance("RSA");
} } catch (NoSuchAlgorithmException s) {
catch(NoSuchAlgorithmException s) {
throw new IOException(s.getMessage(), s); throw new IOException(s.getMessage(), s);
} }
try { try {
@@ -174,12 +123,11 @@ public class PuTTYKeyFile implements FileKeyProvider {
factory.generatePublic(new RSAPublicKeySpec(n, e)), factory.generatePublic(new RSAPublicKeySpec(n, e)),
factory.generatePrivate(new RSAPrivateKeySpec(n, d)) factory.generatePrivate(new RSAPrivateKeySpec(n, d))
); );
} } catch (InvalidKeySpecException i) {
catch(InvalidKeySpecException i) {
throw new IOException(i.getMessage(), i); throw new IOException(i.getMessage(), i);
} }
} }
if(KeyType.DSA.equals(this.getType())) { if (KeyType.DSA.equals(this.getType())) {
final KeyReader publicKeyReader = new KeyReader(publicKey); final KeyReader publicKeyReader = new KeyReader(publicKey);
publicKeyReader.skip(); // skip this publicKeyReader.skip(); // skip this
BigInteger p = publicKeyReader.readInt(); BigInteger p = publicKeyReader.readInt();
@@ -194,8 +142,7 @@ public class PuTTYKeyFile implements FileKeyProvider {
final KeyFactory factory; final KeyFactory factory;
try { try {
factory = KeyFactory.getInstance("DSA"); factory = KeyFactory.getInstance("DSA");
} } catch (NoSuchAlgorithmException s) {
catch(NoSuchAlgorithmException s) {
throw new IOException(s.getMessage(), s); throw new IOException(s.getMessage(), s);
} }
try { try {
@@ -203,12 +150,10 @@ public class PuTTYKeyFile implements FileKeyProvider {
factory.generatePublic(new DSAPublicKeySpec(y, p, q, g)), factory.generatePublic(new DSAPublicKeySpec(y, p, q, g)),
factory.generatePrivate(new DSAPrivateKeySpec(x, p, q, g)) factory.generatePrivate(new DSAPrivateKeySpec(x, p, q, g))
); );
} } catch (InvalidKeySpecException e) {
catch(InvalidKeySpecException e) {
throw new IOException(e.getMessage(), e); throw new IOException(e.getMessage(), e);
} }
} } else {
else {
throw new IOException(String.format("Unknown key type %s", this.getType())); throw new IOException(String.format("Unknown key type %s", this.getType()));
} }
} }
@@ -219,18 +164,16 @@ public class PuTTYKeyFile implements FileKeyProvider {
try { try {
String headerName = null; String headerName = null;
String line; String line;
while((line = r.readLine()) != null) { while ((line = r.readLine()) != null) {
int idx = line.indexOf(": "); int idx = line.indexOf(": ");
if(idx > 0) { if (idx > 0) {
headerName = line.substring(0, idx); headerName = line.substring(0, idx);
headers.put(headerName, line.substring(idx + 2)); headers.put(headerName, line.substring(idx + 2));
} } else {
else {
String s = payload.get(headerName); String s = payload.get(headerName);
if(s == null) { if (s == null) {
s = line; s = line;
} } else {
else {
// Append to previous line // Append to previous line
s += line; s += line;
} }
@@ -238,29 +181,25 @@ public class PuTTYKeyFile implements FileKeyProvider {
payload.put(headerName, s); payload.put(headerName, s);
} }
} }
} } finally {
finally {
r.close(); r.close();
} }
// Retrieve keys from payload // Retrieve keys from payload
publicKey = Base64.decode(payload.get("Public-Lines")); publicKey = Base64.decode(payload.get("Public-Lines"));
if(this.isEncrypted()) { if (this.isEncrypted()) {
final char[] passphrase; final char[] passphrase;
if(pwdf != null) { if (pwdf != null) {
passphrase = pwdf.reqPassword(resource); passphrase = pwdf.reqPassword(resource);
} } else {
else {
passphrase = "".toCharArray(); passphrase = "".toCharArray();
} }
try { try {
privateKey = this.decrypt(Base64.decode(payload.get("Private-Lines")), new String(passphrase)); privateKey = this.decrypt(Base64.decode(payload.get("Private-Lines")), new String(passphrase));
this.verify(new String(passphrase)); this.verify(new String(passphrase));
} } finally {
finally {
PasswordUtils.blankOut(passphrase); PasswordUtils.blankOut(passphrase);
} }
} } else {
else {
privateKey = Base64.decode(payload.get("Private-Lines")); privateKey = Base64.decode(payload.get("Private-Lines"));
} }
} }
@@ -292,8 +231,7 @@ public class PuTTYKeyFile implements FileKeyProvider {
System.arraycopy(key2, 0, r, 20, 12); System.arraycopy(key2, 0, r, 20, 12);
return r; return r;
} } catch (NoSuchAlgorithmException e) {
catch(NoSuchAlgorithmException e) {
throw new IOException(e.getMessage(), e); throw new IOException(e.getMessage(), e);
} }
} }
@@ -306,7 +244,7 @@ public class PuTTYKeyFile implements FileKeyProvider {
// The key to the MAC is itself a SHA-1 hash of: // The key to the MAC is itself a SHA-1 hash of:
MessageDigest digest = MessageDigest.getInstance("SHA-1"); MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update("putty-private-key-file-mac-key".getBytes()); digest.update("putty-private-key-file-mac-key".getBytes());
if(passphrase != null) { if (passphrase != null) {
digest.update(passphrase.getBytes()); digest.update(passphrase.getBytes());
} }
final byte[] key = digest.digest(); final byte[] key = digest.digest();
@@ -334,11 +272,10 @@ public class PuTTYKeyFile implements FileKeyProvider {
final String encoded = Hex.toHexString(mac.doFinal(out.toByteArray())); final String encoded = Hex.toHexString(mac.doFinal(out.toByteArray()));
final String reference = headers.get("Private-MAC"); final String reference = headers.get("Private-MAC");
if(!encoded.equals(reference)) { if (!encoded.equals(reference)) {
throw new IOException("Invalid passphrase"); throw new IOException("Invalid passphrase");
} }
} } catch (GeneralSecurityException e) {
catch(GeneralSecurityException e) {
throw new IOException(e.getMessage(), e); throw new IOException(e.getMessage(), e);
} }
} }
@@ -355,8 +292,7 @@ public class PuTTYKeyFile implements FileKeyProvider {
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(expanded, 0, 32, "AES"), cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(expanded, 0, 32, "AES"),
new IvParameterSpec(new byte[16])); // initial vector=0 new IvParameterSpec(new byte[16])); // initial vector=0
return cipher.doFinal(key); return cipher.doFinal(key);
} } catch (GeneralSecurityException e) {
catch(GeneralSecurityException e) {
throw new IOException(e.getMessage(), e); throw new IOException(e.getMessage(), e);
} }
} }
@@ -377,14 +313,14 @@ public class PuTTYKeyFile implements FileKeyProvider {
*/ */
public void skip() throws IOException { public void skip() throws IOException {
final int read = di.readInt(); final int read = di.readInt();
if(read != di.skipBytes(read)) { if (read != di.skipBytes(read)) {
throw new IOException(String.format("Failed to skip %d bytes", read)); throw new IOException(String.format("Failed to skip %d bytes", read));
} }
} }
private byte[] read() throws IOException { private byte[] read() throws IOException {
int len = di.readInt(); int len = di.readInt();
if(len <= 0 || len > 513) { if (len <= 0 || len > 513) {
throw new IOException(String.format("Invalid length %d", len)); throw new IOException(String.format("Invalid length %d", len));
} }
byte[] r = new byte[len]; byte[] r = new byte[len];

View File

@@ -15,14 +15,14 @@
*/ */
package com.hierynomus.sshj; package com.hierynomus.sshj;
import net.schmizz.sshj.DefaultConfig; import java.io.File;
import net.schmizz.sshj.SSHClient; import java.io.IOException;
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import java.io.File; import net.schmizz.sshj.DefaultConfig;
import java.io.IOException; import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
@@ -32,8 +32,8 @@ public class IntegrationTest {
public void shouldConnect() throws IOException { public void shouldConnect() throws IOException {
SSHClient sshClient = new SSHClient(new DefaultConfig()); SSHClient sshClient = new SSHClient(new DefaultConfig());
sshClient.addHostKeyVerifier(new OpenSSHKnownHosts(new File("/Users/ajvanerp/.ssh/known_hosts"))); sshClient.addHostKeyVerifier(new OpenSSHKnownHosts(new File("/Users/ajvanerp/.ssh/known_hosts")));
sshClient.connect("172.16.37.129"); sshClient.connect("172.16.37.147");
sshClient.authPassword("jeroen", "jeroen"); sshClient.authPublickey("jeroen");
assertThat("Is connected", sshClient.isAuthenticated()); assertThat("Is connected", sshClient.isAuthenticated());
} }
} }

View File

@@ -18,6 +18,7 @@ package net.schmizz.sshj.keyprovider;
import net.schmizz.sshj.common.KeyType; import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile; import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile;
import net.schmizz.sshj.userauth.password.PasswordFinder; import net.schmizz.sshj.userauth.password.PasswordFinder;
import net.schmizz.sshj.userauth.password.PasswordUtils; import net.schmizz.sshj.userauth.password.PasswordUtils;
import net.schmizz.sshj.userauth.password.Resource; import net.schmizz.sshj.userauth.password.Resource;
@@ -30,11 +31,13 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.Arrays; import java.util.Arrays;
import java.util.Scanner; import java.util.Scanner;
import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -142,7 +145,7 @@ public class OpenSSHKeyFileTest {
@Test @Test
public void shouldHaveCorrectFingerprintForED25519() throws IOException { public void shouldHaveCorrectFingerprintForED25519() throws IOException {
OpenSSHKeyFile keyFile = new OpenSSHKeyFile(); OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile();
keyFile.init(new File("src/test/resources/keytypes/test_ed25519")); keyFile.init(new File("src/test/resources/keytypes/test_ed25519"));
String expected = "256 MD5:d3:5e:40:72:db:08:f1:6d:0c:d7:6d:35:0d:ba:7c:32 root@sshj (ED25519)\n"; String expected = "256 MD5:d3:5e:40:72:db:08:f1:6d:0c:d7:6d:35:0d:ba:7c:32 root@sshj (ED25519)\n";
PublicKey aPublic = keyFile.getPublic(); PublicKey aPublic = keyFile.getPublic();
@@ -150,6 +153,14 @@ public class OpenSSHKeyFileTest {
assertThat(expected, containsString(sshjFingerprintSshjKey)); assertThat(expected, containsString(sshjFingerprintSshjKey));
} }
@Test
public void shouldLoadED25519PrivateKey() throws IOException {
OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile();
keyFile.init(new File("src/test/resources/keytypes/test_ed25519"));
PrivateKey aPrivate = keyFile.getPrivate();
assertThat(aPrivate.getAlgorithm(), equalTo("EdDSA"));
}
@Before @Before
public void setup() public void setup()
throws UnsupportedEncodingException, GeneralSecurityException { throws UnsupportedEncodingException, GeneralSecurityException {
@@ -171,4 +182,4 @@ public class OpenSSHKeyFileTest {
scanner.close(); scanner.close();
} }
} }
} }