diff --git a/GNUmakefile b/GNUmakefile index 0da49e0e..08bcf97f 100755 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,45 +1,46 @@ -Default: all - -TOP=$(realpath .) -include $(TOP)/../DeveloperTools/install/common.mk - -RSRC=rsrc -LIBDIR=$(RSRC)/lib -LIB=$(subst $(SPACE),$(CLN),$(filter %.jar %.zip, $(wildcard $(LIBDIR)/*))) -BUILD=build -SRC=src/main/java -DOCS=docs -CLASSPATH="$(CLASSLIB)$(CLN)$(LIB)$(CLN)$(SRC)" -CWD=$(shell pwd) - -include classes.mk - -CLASS_FILES:=$(foreach class, $(CLASSES), $(BUILD)/$(subst .,/,$(class)).class) -PACKAGES=$(sort $(basename $(CLASSES))) -PACKAGEDIRS=$(subst .,/,$(PACKAGES)) - -all: sshj.jar - -sshj.jar: classes - $(JAR) cvf $@ -C $(BUILD)/ . - -javadocs: - mkdir -p $(DOCS) - $(JAVA_HOME)/bin/javadoc -d $(DOCS) -classpath $(CLASSPATH) $(PACKAGES) - -clean: - rm -rf $(BUILD) - -classes: classdirs $(CLASS_FILES) - -install: all - cp sshj.jar $(TOP)/../jOVAL-Commercial/components/provider/remote/rsrc/lib - cp sshj.jar $(TOP)/../jOVAL-Commercial/components/sdk/dist/3rd-party - -classdirs: $(foreach pkg, $(PACKAGEDIRS), $(BUILD)/$(pkg)/) - -$(BUILD)/%.class: $(SRC)/%.java - $(JAVAC) $(JAVACFLAGS) -d $(BUILD) -classpath $(CLASSPATH) $< - -$(BUILD)/%/: - mkdir -p $(subst PKG,,$@) +Default: all + +TOP=$(realpath .) +include $(TOP)/../DeveloperTools/install/common.mk + +RSRC=rsrc +LIBDIR=$(RSRC)/lib +LIB=$(subst $(SPACE),$(CLN),$(filter %.jar %.zip, $(wildcard $(LIBDIR)/*))) +BUILD=build +SRC=src/main/java +DOCS=docs +CLASSPATH="$(CLASSLIB)$(CLN)$(LIB)$(CLN)$(SRC)" +CWD=$(shell pwd) + +include classes.mk + +CLASS_FILES:=$(foreach class, $(CLASSES), $(BUILD)/$(subst .,/,$(class)).class) +PACKAGES=$(sort $(basename $(CLASSES))) +PACKAGEDIRS=$(subst .,/,$(PACKAGES)) + +all: sshj.jar + +sshj.jar: classes + $(JAR) cvf $@ -C $(BUILD)/ . + +javadocs: + mkdir -p $(DOCS) + $(JAVA_HOME)/bin/javadoc -d $(DOCS) -classpath $(CLASSPATH) $(PACKAGES) + +clean: + rm -rf $(BUILD) + +classes: classdirs $(CLASS_FILES) + +install: all + cp sshj.jar $(TOP)/../jOVAL-Commercial/components/wsmv/winrs/rsrc/lib +# cp sshj.jar $(TOP)/../jOVAL-Commercial/components/provider/remote/rsrc/lib +# cp sshj.jar $(TOP)/../jOVAL-Commercial/components/sdk/dist/3rd-party + +classdirs: $(foreach pkg, $(PACKAGEDIRS), $(BUILD)/$(pkg)/) + +$(BUILD)/%.class: $(SRC)/%.java + $(JAVAC) $(JAVACFLAGS) -d $(BUILD) -classpath $(CLASSPATH) $< + +$(BUILD)/%/: + mkdir -p $(subst PKG,,$@) diff --git a/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java b/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java index b74131eb..78500b59 100644 --- a/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java +++ b/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java @@ -262,6 +262,13 @@ public class OpenSSHKnownHosts } public interface HostEntry { + KeyType getType(); + + String getFingerprint(); + + boolean appliesTo(String host) + throws IOException; + boolean appliesTo(KeyType type, String host) throws IOException; @@ -279,6 +286,22 @@ public class OpenSSHKnownHosts this.comment = comment; } + @Override + public KeyType getType() { + return KeyType.UNKNOWN; + } + + @Override + public String getFingerprint() { + return null; + } + + @Override + public boolean appliesTo(String host) + throws IOException { + return false; + } + @Override public boolean appliesTo(KeyType type, String host) { return false; @@ -308,6 +331,16 @@ public class OpenSSHKnownHosts this.key = key; } + @Override + public KeyType getType() { + return type; + } + + @Override + public String getFingerprint() { + return SecurityUtils.getFingerprint(key); + } + @Override public boolean verify(PublicKey key) throws IOException { @@ -349,6 +382,12 @@ public class OpenSSHKnownHosts return hostnames; } + @Override + public boolean appliesTo(String host) + throws IOException { + return hosts.contains(host); + } + @Override public boolean appliesTo(KeyType type, String host) throws IOException { @@ -377,6 +416,12 @@ public class OpenSSHKnownHosts } } + @Override + public boolean appliesTo(String host) + throws IOException { + return hashedHost.equals(hashHost(host)); + } + @Override public boolean appliesTo(KeyType type, String host) throws IOException { @@ -426,4 +471,4 @@ public class OpenSSHKnownHosts } -} \ No newline at end of file +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java index 097490d0..ac05eb35 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java @@ -15,29 +15,47 @@ */ 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.AES128CBC; +import net.schmizz.sshj.transport.cipher.AES192CBC; +import net.schmizz.sshj.transport.cipher.AES256CBC; +import net.schmizz.sshj.transport.cipher.Cipher; +import net.schmizz.sshj.transport.cipher.NoneCipher; +import net.schmizz.sshj.transport.cipher.TripleDESCBC; +import net.schmizz.sshj.transport.digest.Digest; +import net.schmizz.sshj.transport.digest.MD5; 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.PrivateKeyReaderResource; import net.schmizz.sshj.userauth.password.PrivateKeyStringResource; import net.schmizz.sshj.userauth.password.Resource; -import org.bouncycastle.openssl.EncryptionException; -import org.bouncycastle.openssl.PEMEncryptedKeyPair; -import org.bouncycastle.openssl.PEMKeyPair; -import org.bouncycastle.openssl.PEMParser; -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; -import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedReader; import java.io.File; +import java.io.EOFException; import java.io.IOException; import java.io.Reader; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.security.KeyFactory; import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Arrays; +import javax.xml.bind.DatatypeConverter; /** Represents a PKCS8-encoded key file. This is the format used by OpenSSH and OpenSSL. */ public class PKCS8KeyFile @@ -123,51 +141,172 @@ public class PKCS8KeyFile protected KeyPair readKeyPair() throws IOException { - KeyPair kp = null; + BufferedReader reader = new BufferedReader(resource.getReader()); + try { + String type = null; + String line = null; + Cipher cipher = new NoneCipher(); + StringBuffer sb = new StringBuffer(); + byte[] iv = new byte[0]; // salt + while ((line = reader.readLine()) != null) { + if (line.startsWith("-----BEGIN ") && line.endsWith(" PRIVATE KEY-----")) { + int end = line.length() - 17; + if (end > 11) { + type = line.substring(11, line.length() - 17); + } else { + type = "UNKNOWN"; + } + } else if (line.startsWith("-----END")) { + break; + } else if (type != null) { + if (line.startsWith("Proc-Type: ")) { + if (!"4,ENCRYPTED".equals(line.substring(11))) { + throw new IOException("Unrecognized Proc-Type: " + line.substring(11)); + } + } else if (line.startsWith("DEK-Info: ")) { + int ptr = line.indexOf(","); + if (ptr == -1) { + throw new IOException("Unrecognized DEK-Info: " + line.substring(10)); + } else { + String algorithm = line.substring(10,ptr); + if ("DES-EDE3-CBC".equals(algorithm)) { + cipher = new TripleDESCBC(); + iv = DatatypeConverter.parseHexBinary(line.substring(ptr+1)); + } else if ("AES-128-CBC".equals(algorithm)) { + cipher = new AES128CBC(); + iv = DatatypeConverter.parseHexBinary(line.substring(ptr+1)); + } else if ("AES-192-CBC".equals(algorithm)) { + cipher = new AES192CBC(); + iv = Base64.decode(line.substring(ptr+1)); + } else if ("AES-256-CBC".equals(algorithm)) { + cipher = new AES256CBC(); + iv = Base64.decode(line.substring(ptr+1)); + } else { + throw new IOException("Not a supported algorithm: " + algorithm); + } + } + } else if (line.length() > 0) { + sb.append(line); + } + } + } - for (PEMParser r = null; ; ) { - // while the PasswordFinder tells us we should retry - try { - r = new PEMParser(resource.getReader()); - final Object o = r.readObject(); - - final JcaPEMKeyConverter pemConverter = new JcaPEMKeyConverter(); - pemConverter.setProvider("BC"); - - if (o instanceof PEMEncryptedKeyPair) { - final PEMEncryptedKeyPair encryptedKeyPair = (PEMEncryptedKeyPair) o; - JcePEMDecryptorProviderBuilder decryptorBuilder = new JcePEMDecryptorProviderBuilder(); - decryptorBuilder.setProvider("BC"); - try { - passphrase = pwdf == null ? null : pwdf.reqPassword(resource); - kp = pemConverter.getKeyPair(encryptedKeyPair.decryptKeyPair(decryptorBuilder.build(passphrase))); - } finally { - PasswordUtils.blankOut(passphrase); + byte[] data = Base64.decode(sb.toString()); + if (pwdf != null) { + boolean decrypted = false; + do { + CharBuffer cb = CharBuffer.wrap(pwdf.reqPassword(resource)); + ByteBuffer bb = IOUtils.UTF8.encode(cb); + byte[] passphrase = Arrays.copyOfRange(bb.array(), bb.position(), bb.limit()); + byte[] key = new byte[cipher.getBlockSize()]; + iv = Arrays.copyOfRange(iv, 0, cipher.getIVSize()); + Digest md5 = new MD5(); + md5.init(); + int hsize = md5.getBlockSize(); + byte[] hn = new byte[key.length / hsize * hsize + (key.length % hsize == 0 ? 0 : hsize)]; + byte[] tmp = null; + for (int i=0; i + hsize <= hn.length;) { + if (tmp != null) { + md5.update(tmp, 0, tmp.length); + } + md5.update(passphrase, 0, passphrase.length); + md5.update(iv, 0, iv.length > 8 ? 8 : iv.length); + tmp = md5.digest(); + System.arraycopy(tmp, 0, hn, i, tmp.length); + i += tmp.length; } - } else if (o instanceof PEMKeyPair) { - kp = pemConverter.getKeyPair((PEMKeyPair) o); - } else { - log.debug("Expected PEMEncryptedKeyPair or PEMKeyPair, got: {}", o); - } + System.arraycopy(hn, 0, key, 0, key.length); + cipher.init(Cipher.Mode.Decrypt, key, iv); + cipher.update(data, 0, data.length); + decrypted = 0x30 == data[0]; + Arrays.fill(cb.array(), '\u0000'); + Arrays.fill(bb.array(), (byte) 0); + Arrays.fill(key, (byte) 0); + } while (!decrypted && pwdf.shouldRetry(resource)); + } + if (0x30 != data[0]) { + throw new IOException("Failed to decrypt key"); + } - } catch (EncryptionException e) { - if (pwdf != null && pwdf.shouldRetry(resource)) - continue; - else - throw e; - } finally { - IOUtils.closeQuietly(r); - } - break; - } - - if (kp == null) - throw new IOException("Could not read key pair from: " + resource); - return kp; + ASN1Data asn = new ASN1Data(data); + if ("RSA".equals(type)) { + KeyFactory factory = KeyFactory.getInstance("RSA"); + asn.readNext(); + BigInteger modulus = asn.readNext(); + BigInteger pubExp = asn.readNext(); + BigInteger prvExp = asn.readNext(); + PublicKey pubKey = factory.generatePublic(new RSAPublicKeySpec(modulus, pubExp)); + PrivateKey prvKey = factory.generatePrivate(new RSAPrivateKeySpec(modulus, prvExp)); + return new KeyPair(pubKey, prvKey); + } else if ("DSA".equals(type)) { + KeyFactory factory = KeyFactory.getInstance("DSA"); + asn.readNext(); + BigInteger p = asn.readNext(); + BigInteger q = asn.readNext(); + BigInteger g = asn.readNext(); + BigInteger pub = asn.readNext(); + BigInteger prv = asn.readNext(); + PublicKey pubKey = factory.generatePublic(new DSAPublicKeySpec(pub, p, q, g)); + PrivateKey prvKey = factory.generatePrivate(new DSAPrivateKeySpec(prv, p, q, g)); + return new KeyPair(pubKey, prvKey); + } else { + throw new IOException("Unrecognized key type: " + type); + } + } catch (NoSuchAlgorithmException e) { + throw new IOException(e); + } catch (InvalidKeySpecException e) { + throw new IOException(e); + } finally { + reader.close(); + } } @Override public String toString() { return "PKCS8KeyFile{resource=" + resource + "}"; } -} \ No newline at end of file + + class ASN1Data { + private byte[] buff; + private int index, length; + + ASN1Data(byte[] buff) throws IOException { + this.buff = buff; + index = 0; + if (buff[index++] != (byte)0x30) { + throw new IOException("Not ASN.1 data"); + } + length = buff[index++] & 0xff; + if ((length & 0x80) != 0) { + int counter = length & 0x7f; + length = 0; + while (counter-- > 0) { + length = (length << 8) + (buff[index++] & 0xff); + } + } + if ((index + length) > buff.length) { + throw new IOException("Length mismatch: " + buff.length + " != " + (index + length)); + } + } + + BigInteger readNext() throws IOException { + if (index >= length) { + throw new EOFException(); + } else if (buff[index++] != 0x02) { + throw new IOException("Not an int code: " + Integer.toHexString(0xff & buff[index])); + } + int length = buff[index++] & 0xff; + if ((length & 0x80) != 0) { + int counter = length & 0x7f; + length = 0; + while (counter-- > 0) { + length = (length << 8) + (buff[index++] & 0xff); + } + } + byte[] sequence = new byte[length]; + System.arraycopy(buff, index, sequence, 0, length); + index += length; + return new BigInteger(sequence); + } + } +}