mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 15:20:54 +03:00
Merge pull request #279 from hierynomus/issue-276
Support for OpenSSH new key file format (fixes #276)
This commit is contained in:
@@ -23,6 +23,8 @@ import net.schmizz.sshj.common.SSHRuntimeException;
|
||||
|
||||
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.
|
||||
* 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) {
|
||||
super(spec);
|
||||
|
||||
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("ed25519-sha-512");
|
||||
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName(CURVE_ED25519_SHA512);
|
||||
if (!spec.getParams().getCurve().equals(ed25519.getCurve())) {
|
||||
throw new SSHRuntimeException("Cannot create Ed25519 Public Key from wrong spec");
|
||||
}
|
||||
|
||||
@@ -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))));
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import net.schmizz.sshj.transport.random.BouncyCastleRandom;
|
||||
import net.schmizz.sshj.transport.random.JCERandom;
|
||||
import net.schmizz.sshj.transport.random.SingletonRandomFactory;
|
||||
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.PuTTYKeyFile;
|
||||
import org.slf4j.Logger;
|
||||
@@ -113,7 +114,7 @@ public class DefaultConfig
|
||||
|
||||
protected void initFileKeyProviderFactories(boolean 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -316,7 +316,7 @@ public class SSHClient
|
||||
public void authPublickey(String username)
|
||||
throws UserAuthException, TransportException {
|
||||
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
|
||||
* private key files are supported (OpenSSH uses this format).
|
||||
* Creates a {@link KeyProvider} instance from given location on the file system. Currently the following private key files are supported:
|
||||
* <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/>
|
||||
*
|
||||
* @param location the location of the key 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;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public enum KeyFormat {
|
||||
PKCS5,
|
||||
PKCS8,
|
||||
OpenSSH,
|
||||
OpenSSHv1,
|
||||
PuTTY,
|
||||
Unknown
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package net.schmizz.sshj.userauth.keyprovider;
|
||||
import net.schmizz.sshj.common.IOUtils;
|
||||
|
||||
import java.io.*;
|
||||
import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile;
|
||||
|
||||
public class KeyProviderUtil {
|
||||
|
||||
@@ -88,10 +89,12 @@ public class KeyProviderUtil {
|
||||
|
||||
private static KeyFormat keyFormatFromHeader(String header, boolean separatePubKey) {
|
||||
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
|
||||
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;
|
||||
} else {
|
||||
return KeyFormat.PKCS5;
|
||||
|
||||
@@ -15,28 +15,28 @@
|
||||
*/
|
||||
package net.schmizz.sshj.userauth.keyprovider;
|
||||
|
||||
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;
|
||||
import net.schmizz.sshj.userauth.password.*;
|
||||
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
import java.io.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.security.*;
|
||||
import java.security.spec.*;
|
||||
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.
|
||||
*/
|
||||
public class PKCS5KeyFile
|
||||
implements FileKeyProvider {
|
||||
public class PKCS5KeyFile extends BaseFileKeyProvider {
|
||||
|
||||
public static class Factory
|
||||
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;
|
||||
|
||||
@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()
|
||||
throws IOException {
|
||||
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
package net.schmizz.sshj.userauth.keyprovider;
|
||||
|
||||
import net.schmizz.sshj.common.IOUtils;
|
||||
import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.userauth.password.*;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import org.bouncycastle.openssl.EncryptionException;
|
||||
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
@@ -27,16 +26,11 @@ import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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.IOUtils;
|
||||
import net.schmizz.sshj.userauth.password.PasswordUtils;
|
||||
|
||||
/** Represents a PKCS8-encoded key file. This is the format used by OpenSSH and OpenSSL. */
|
||||
public class PKCS8KeyFile
|
||||
implements FileKeyProvider {
|
||||
/** Represents a PKCS8-encoded key file. This is the format used by (old-style) OpenSSH and OpenSSL. */
|
||||
public class PKCS8KeyFile extends BaseFileKeyProvider {
|
||||
|
||||
public static class Factory
|
||||
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
|
||||
@@ -53,68 +47,9 @@ public class PKCS8KeyFile
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@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()
|
||||
throws IOException {
|
||||
|
||||
@@ -15,21 +15,21 @@
|
||||
*/
|
||||
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.math.BigInteger;
|
||||
import java.security.*;
|
||||
import java.security.spec.*;
|
||||
import java.util.HashMap;
|
||||
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>
|
||||
@@ -56,7 +56,7 @@ import java.util.Map;
|
||||
*
|
||||
* @version $Id:$
|
||||
*/
|
||||
public class PuTTYKeyFile implements FileKeyProvider {
|
||||
public class PuTTYKeyFile extends BaseFileKeyProvider {
|
||||
|
||||
public static class Factory
|
||||
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
|
||||
@@ -75,56 +75,6 @@ public class PuTTYKeyFile implements FileKeyProvider {
|
||||
private byte[] privateKey;
|
||||
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.
|
||||
*/
|
||||
@@ -150,7 +100,7 @@ public class PuTTYKeyFile implements FileKeyProvider {
|
||||
|
||||
protected KeyPair readKeyPair() throws IOException {
|
||||
this.parseKeyPair();
|
||||
if(KeyType.RSA.equals(this.getType())) {
|
||||
if (KeyType.RSA.equals(this.getType())) {
|
||||
final KeyReader publicKeyReader = new KeyReader(publicKey);
|
||||
publicKeyReader.skip(); // skip this
|
||||
// public key exponent
|
||||
@@ -165,8 +115,7 @@ public class PuTTYKeyFile implements FileKeyProvider {
|
||||
final KeyFactory factory;
|
||||
try {
|
||||
factory = KeyFactory.getInstance("RSA");
|
||||
}
|
||||
catch(NoSuchAlgorithmException s) {
|
||||
} catch (NoSuchAlgorithmException s) {
|
||||
throw new IOException(s.getMessage(), s);
|
||||
}
|
||||
try {
|
||||
@@ -174,12 +123,11 @@ public class PuTTYKeyFile implements FileKeyProvider {
|
||||
factory.generatePublic(new RSAPublicKeySpec(n, e)),
|
||||
factory.generatePrivate(new RSAPrivateKeySpec(n, d))
|
||||
);
|
||||
}
|
||||
catch(InvalidKeySpecException i) {
|
||||
} catch (InvalidKeySpecException 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);
|
||||
publicKeyReader.skip(); // skip this
|
||||
BigInteger p = publicKeyReader.readInt();
|
||||
@@ -194,8 +142,7 @@ public class PuTTYKeyFile implements FileKeyProvider {
|
||||
final KeyFactory factory;
|
||||
try {
|
||||
factory = KeyFactory.getInstance("DSA");
|
||||
}
|
||||
catch(NoSuchAlgorithmException s) {
|
||||
} catch (NoSuchAlgorithmException s) {
|
||||
throw new IOException(s.getMessage(), s);
|
||||
}
|
||||
try {
|
||||
@@ -203,12 +150,10 @@ public class PuTTYKeyFile implements FileKeyProvider {
|
||||
factory.generatePublic(new DSAPublicKeySpec(y, p, q, g)),
|
||||
factory.generatePrivate(new DSAPrivateKeySpec(x, p, q, g))
|
||||
);
|
||||
}
|
||||
catch(InvalidKeySpecException e) {
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IOException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new IOException(String.format("Unknown key type %s", this.getType()));
|
||||
}
|
||||
}
|
||||
@@ -219,18 +164,16 @@ public class PuTTYKeyFile implements FileKeyProvider {
|
||||
try {
|
||||
String headerName = null;
|
||||
String line;
|
||||
while((line = r.readLine()) != null) {
|
||||
while ((line = r.readLine()) != null) {
|
||||
int idx = line.indexOf(": ");
|
||||
if(idx > 0) {
|
||||
if (idx > 0) {
|
||||
headerName = line.substring(0, idx);
|
||||
headers.put(headerName, line.substring(idx + 2));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
String s = payload.get(headerName);
|
||||
if(s == null) {
|
||||
if (s == null) {
|
||||
s = line;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// Append to previous line
|
||||
s += line;
|
||||
}
|
||||
@@ -238,29 +181,25 @@ public class PuTTYKeyFile implements FileKeyProvider {
|
||||
payload.put(headerName, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
r.close();
|
||||
}
|
||||
// Retrieve keys from payload
|
||||
publicKey = Base64.decode(payload.get("Public-Lines"));
|
||||
if(this.isEncrypted()) {
|
||||
if (this.isEncrypted()) {
|
||||
final char[] passphrase;
|
||||
if(pwdf != null) {
|
||||
if (pwdf != null) {
|
||||
passphrase = pwdf.reqPassword(resource);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
passphrase = "".toCharArray();
|
||||
}
|
||||
try {
|
||||
privateKey = this.decrypt(Base64.decode(payload.get("Private-Lines")), new String(passphrase));
|
||||
this.verify(new String(passphrase));
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
PasswordUtils.blankOut(passphrase);
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
privateKey = Base64.decode(payload.get("Private-Lines"));
|
||||
}
|
||||
}
|
||||
@@ -292,8 +231,7 @@ public class PuTTYKeyFile implements FileKeyProvider {
|
||||
System.arraycopy(key2, 0, r, 20, 12);
|
||||
|
||||
return r;
|
||||
}
|
||||
catch(NoSuchAlgorithmException e) {
|
||||
} catch (NoSuchAlgorithmException 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:
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
digest.update("putty-private-key-file-mac-key".getBytes());
|
||||
if(passphrase != null) {
|
||||
if (passphrase != null) {
|
||||
digest.update(passphrase.getBytes());
|
||||
}
|
||||
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 reference = headers.get("Private-MAC");
|
||||
if(!encoded.equals(reference)) {
|
||||
if (!encoded.equals(reference)) {
|
||||
throw new IOException("Invalid passphrase");
|
||||
}
|
||||
}
|
||||
catch(GeneralSecurityException e) {
|
||||
} catch (GeneralSecurityException 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"),
|
||||
new IvParameterSpec(new byte[16])); // initial vector=0
|
||||
return cipher.doFinal(key);
|
||||
}
|
||||
catch(GeneralSecurityException e) {
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IOException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
@@ -377,14 +313,14 @@ public class PuTTYKeyFile implements FileKeyProvider {
|
||||
*/
|
||||
public void skip() throws IOException {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] read() throws IOException {
|
||||
int len = di.readInt();
|
||||
if(len <= 0 || len > 513) {
|
||||
if (len <= 0 || len > 513) {
|
||||
throw new IOException(String.format("Invalid length %d", len));
|
||||
}
|
||||
byte[] r = new byte[len];
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
*/
|
||||
package com.hierynomus.sshj;
|
||||
|
||||
import net.schmizz.sshj.DefaultConfig;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import net.schmizz.sshj.DefaultConfig;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
@@ -32,8 +32,8 @@ public class IntegrationTest {
|
||||
public void shouldConnect() throws IOException {
|
||||
SSHClient sshClient = new SSHClient(new DefaultConfig());
|
||||
sshClient.addHostKeyVerifier(new OpenSSHKnownHosts(new File("/Users/ajvanerp/.ssh/known_hosts")));
|
||||
sshClient.connect("172.16.37.129");
|
||||
sshClient.authPassword("jeroen", "jeroen");
|
||||
sshClient.connect("172.16.37.147");
|
||||
sshClient.authPublickey("jeroen");
|
||||
assertThat("Is connected", sshClient.isAuthenticated());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package net.schmizz.sshj.keyprovider;
|
||||
import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
|
||||
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.PasswordUtils;
|
||||
import net.schmizz.sshj.userauth.password.Resource;
|
||||
@@ -30,11 +31,13 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.Scanner;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -142,7 +145,7 @@ public class OpenSSHKeyFileTest {
|
||||
|
||||
@Test
|
||||
public void shouldHaveCorrectFingerprintForED25519() throws IOException {
|
||||
OpenSSHKeyFile keyFile = new OpenSSHKeyFile();
|
||||
OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile();
|
||||
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";
|
||||
PublicKey aPublic = keyFile.getPublic();
|
||||
@@ -150,6 +153,14 @@ public class OpenSSHKeyFileTest {
|
||||
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
|
||||
public void setup()
|
||||
throws UnsupportedEncodingException, GeneralSecurityException {
|
||||
|
||||
Reference in New Issue
Block a user