diff --git a/build.gradle b/build.gradle index e3248dbe..a5365f89 100644 --- a/build.gradle +++ b/build.gradle @@ -47,14 +47,13 @@ compileJava { configurations.implementation.transitive = false def bouncycastleVersion = "1.80" -def sshdVersion = "2.14.0" +def sshdVersion = "2.15.0" dependencies { - implementation "org.slf4j:slf4j-api:2.0.16" + implementation "org.slf4j:slf4j-api:2.0.17" implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" implementation "com.hierynomus:asn-one:0.6.0" - implementation "net.i2p.crypto:eddsa:0.3.0" } license { @@ -90,16 +89,15 @@ testing { configureEach { useJUnitJupiter() dependencies { - implementation "org.slf4j:slf4j-api:2.0.16" - implementation "org.spockframework:spock-core:2.4-M5-groovy-4.0" - implementation "org.mockito:mockito-core:5.15.2" - implementation "org.assertj:assertj-core:3.24.2" + implementation "org.slf4j:slf4j-api:2.0.17" + implementation 'org.spockframework:spock-core:2.3-groovy-3.0' + implementation "org.mockito:mockito-core:5.16.1" + implementation "org.assertj:assertj-core:3.27.3" implementation "ru.vyarus:spock-junit5:1.2.0" implementation "org.apache.sshd:sshd-core:$sshdVersion" implementation "org.apache.sshd:sshd-sftp:$sshdVersion" implementation "org.apache.sshd:sshd-scp:$sshdVersion" - implementation "ch.qos.logback:logback-classic:1.3.15" - implementation 'org.glassfish.grizzly:grizzly-http-server:3.0.1' + implementation "ch.qos.logback:logback-classic:1.5.18" } targets { @@ -134,8 +132,8 @@ testing { integrationTest(JvmTestSuite) { dependencies { implementation project() - implementation 'org.testcontainers:testcontainers:1.20.4' - implementation 'org.testcontainers:junit-jupiter:1.20.4' + implementation platform('org.testcontainers:testcontainers-bom:1.20.6') + implementation 'org.testcontainers:junit-jupiter' } sources { @@ -182,8 +180,6 @@ jar { instruction "Import-Package", "!net.schmizz.*" instruction "Import-Package", "!com.hierynomus.sshj.*" instruction "Import-Package", "javax.crypto*" - instruction "Import-Package", "!net.i2p.crypto.eddsa.math" - instruction "Import-Package", "net.i2p*" instruction "Import-Package", "com.jcraft.jzlib*;version=\"[1.1,2)\";resolution:=optional" instruction "Import-Package", "org.slf4j*;version=\"[1.7,5)\"" instruction "Import-Package", "org.bouncycastle*;resolution:=optional" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e18bc253..37f853b1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/itest/java/com/hierynomus/sshj/sftp/FileWriteTest.java b/src/itest/java/com/hierynomus/sshj/sftp/FileWriteTest.java index b1ba7ea7..0d7519b9 100644 --- a/src/itest/java/com/hierynomus/sshj/sftp/FileWriteTest.java +++ b/src/itest/java/com/hierynomus/sshj/sftp/FileWriteTest.java @@ -21,7 +21,6 @@ import java.util.EnumSet; import org.junit.jupiter.api.Test; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.shaded.org.bouncycastle.util.Arrays; import com.hierynomus.sshj.SshdContainer; @@ -31,11 +30,12 @@ import net.schmizz.sshj.sftp.RemoteFile; import net.schmizz.sshj.sftp.SFTPClient; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; @Testcontainers public class FileWriteTest { @Container - private static SshdContainer sshd = new SshdContainer(); + private static final SshdContainer sshd = new SshdContainer(); @Test public void shouldAppendToFile_GH390() throws Throwable { @@ -63,8 +63,14 @@ public class FileWriteTest { try (RemoteFile read = sftp.open(file, EnumSet.of(OpenMode.READ))) { byte[] readBytes = new byte[initialText.length + appendText.length]; read.read(0, readBytes, 0, readBytes.length); - assertThat(Arrays.copyOfRange(readBytes, 0, initialText.length)).isEqualTo(initialText); - assertThat(Arrays.copyOfRange(readBytes, initialText.length, initialText.length + appendText.length)).isEqualTo(appendText); + + final byte[] expectedInitialText = new byte[initialText.length]; + System.arraycopy(readBytes, 0, expectedInitialText, 0, expectedInitialText.length); + assertArrayEquals(expectedInitialText, initialText); + + final byte[] expectedAppendText = new byte[appendText.length]; + System.arraycopy(readBytes, initialText.length, expectedAppendText, 0, expectedAppendText.length); + assertArrayEquals(expectedAppendText, appendText); } } diff --git a/src/itest/java/com/hierynomus/sshj/transport/kex/StrictKeyExchangeTest.java b/src/itest/java/com/hierynomus/sshj/transport/kex/StrictKeyExchangeTest.java index 9d207c0e..229f1b3d 100644 --- a/src/itest/java/com/hierynomus/sshj/transport/kex/StrictKeyExchangeTest.java +++ b/src/itest/java/com/hierynomus/sshj/transport/kex/StrictKeyExchangeTest.java @@ -145,7 +145,7 @@ class StrictKeyExchangeTest { @Override protected void doKeepAlive() throws TransportException { - conn.getTransport().write(new SSHPacket(Message.IGNORE)); + conn.getTransport().write(new SSHPacket(Message.IGNORE).putString("")); } } diff --git a/src/main/java/com/hierynomus/sshj/common/KeyDecryptionFailedException.java b/src/main/java/com/hierynomus/sshj/common/KeyDecryptionFailedException.java index eae5fc16..a8bc8cff 100644 --- a/src/main/java/com/hierynomus/sshj/common/KeyDecryptionFailedException.java +++ b/src/main/java/com/hierynomus/sshj/common/KeyDecryptionFailedException.java @@ -26,8 +26,12 @@ public class KeyDecryptionFailedException extends IOException { public static final String MESSAGE = "Decryption of the key failed. A supplied passphrase may be incorrect."; - public KeyDecryptionFailedException() { - super(MESSAGE); + public KeyDecryptionFailedException(final String message) { + super(message); + } + + public KeyDecryptionFailedException(final String message, final Throwable cause) { + super(message, cause); } public KeyDecryptionFailedException(IOException cause) { diff --git a/src/main/java/com/hierynomus/sshj/signature/Ed25519PublicKey.java b/src/main/java/com/hierynomus/sshj/signature/Ed25519PublicKey.java deleted file mode 100644 index f26b580c..00000000 --- a/src/main/java/com/hierynomus/sshj/signature/Ed25519PublicKey.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.signature; - -import net.i2p.crypto.eddsa.EdDSAPublicKey; -import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec; -import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; -import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; -import net.schmizz.sshj.common.SSHRuntimeException; - -import java.util.Arrays; - -/** - * 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. - */ -@SuppressWarnings("serial") -public class Ed25519PublicKey extends EdDSAPublicKey { - - public Ed25519PublicKey(EdDSAPublicKeySpec spec) { - super(spec); - - EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519"); - if (!spec.getParams().getCurve().equals(ed25519.getCurve())) { - throw new SSHRuntimeException("Cannot create Ed25519 Public Key from wrong spec"); - } - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof Ed25519PublicKey)) { - return false; - } - - Ed25519PublicKey otherKey = (Ed25519PublicKey) other; - return Arrays.equals(getAbyte(), otherKey.getAbyte()); - } - - @Override - public int hashCode() { - return getA().hashCode(); - } -} diff --git a/src/main/java/com/hierynomus/sshj/signature/SignatureEdDSA.java b/src/main/java/com/hierynomus/sshj/signature/SignatureEdDSA.java index 5f460b33..01622606 100644 --- a/src/main/java/com/hierynomus/sshj/signature/SignatureEdDSA.java +++ b/src/main/java/com/hierynomus/sshj/signature/SignatureEdDSA.java @@ -15,14 +15,14 @@ */ package com.hierynomus.sshj.signature; -import net.i2p.crypto.eddsa.EdDSAEngine; import net.schmizz.sshj.common.KeyType; import net.schmizz.sshj.common.SSHRuntimeException; +import net.schmizz.sshj.common.SecurityUtils; import net.schmizz.sshj.signature.AbstractSignature; import net.schmizz.sshj.signature.Signature; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.security.SignatureException; public class SignatureEdDSA extends AbstractSignature { @@ -43,11 +43,11 @@ public class SignatureEdDSA extends AbstractSignature { super(getEngine(), KeyType.ED25519.toString()); } - private static EdDSAEngine getEngine() { + private static java.security.Signature getEngine() { try { - return new EdDSAEngine(MessageDigest.getInstance("SHA-512")); - } catch (NoSuchAlgorithmException e) { - throw new SSHRuntimeException(e); + return SecurityUtils.getSignature("Ed25519"); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new SSHRuntimeException("Ed25519 Signatures not supported", e); } } diff --git a/src/main/java/com/hierynomus/sshj/transport/IdentificationStringParser.java b/src/main/java/com/hierynomus/sshj/transport/IdentificationStringParser.java index 95de38dc..f6a21e92 100644 --- a/src/main/java/com/hierynomus/sshj/transport/IdentificationStringParser.java +++ b/src/main/java/com/hierynomus/sshj/transport/IdentificationStringParser.java @@ -28,7 +28,7 @@ public class IdentificationStringParser { private final Logger log; private final Buffer.PlainBuffer buffer; - private byte[] EXPECTED_START_BYTES = new byte[] {'S', 'S', 'H', '-'}; + private final byte[] EXPECTED_START_BYTES = new byte[] {'S', 'S', 'H', '-'}; public IdentificationStringParser(Buffer.PlainBuffer buffer) { this(buffer, LoggerFactory.DEFAULT); diff --git a/src/main/java/com/hierynomus/sshj/transport/cipher/BlockCiphers.java b/src/main/java/com/hierynomus/sshj/transport/cipher/BlockCiphers.java index 99cf09f3..e8731d21 100644 --- a/src/main/java/com/hierynomus/sshj/transport/cipher/BlockCiphers.java +++ b/src/main/java/com/hierynomus/sshj/transport/cipher/BlockCiphers.java @@ -121,11 +121,11 @@ public class BlockCiphers { public static class Factory implements net.schmizz.sshj.common.Factory.Named { - private int keysize; - private String cipher; - private String mode; - private String name; - private int ivsize; + private final int keysize; + private final String cipher; + private final String mode; + private final String name; + private final int ivsize; /** * @param ivsize diff --git a/src/main/java/com/hierynomus/sshj/transport/cipher/GcmCiphers.java b/src/main/java/com/hierynomus/sshj/transport/cipher/GcmCiphers.java index 2a288516..414b8af8 100644 --- a/src/main/java/com/hierynomus/sshj/transport/cipher/GcmCiphers.java +++ b/src/main/java/com/hierynomus/sshj/transport/cipher/GcmCiphers.java @@ -33,12 +33,12 @@ public class GcmCiphers { public static class Factory implements net.schmizz.sshj.common.Factory.Named { - private int keysize; - private int authSize; - private String cipher; - private String mode; - private String name; - private int ivsize; + private final int keysize; + private final int authSize; + private final String cipher; + private final String mode; + private final String name; + private final int ivsize; /** * @param ivsize diff --git a/src/main/java/com/hierynomus/sshj/transport/cipher/StreamCiphers.java b/src/main/java/com/hierynomus/sshj/transport/cipher/StreamCiphers.java index 55d37f0f..1c92c732 100644 --- a/src/main/java/com/hierynomus/sshj/transport/cipher/StreamCiphers.java +++ b/src/main/java/com/hierynomus/sshj/transport/cipher/StreamCiphers.java @@ -40,10 +40,10 @@ public class StreamCiphers { public static class Factory implements net.schmizz.sshj.common.Factory.Named { - private int keysize; - private String cipher; - private String mode; - private String name; + private final int keysize; + private final String cipher; + private final String mode; + private final String name; /** * @param keysize The keysize used in bits. diff --git a/src/main/java/com/hierynomus/sshj/transport/kex/DHG.java b/src/main/java/com/hierynomus/sshj/transport/kex/DHG.java index dbb97092..4c7f3e20 100644 --- a/src/main/java/com/hierynomus/sshj/transport/kex/DHG.java +++ b/src/main/java/com/hierynomus/sshj/transport/kex/DHG.java @@ -28,8 +28,8 @@ import java.security.GeneralSecurityException; * */ public class DHG extends AbstractDHG { - private BigInteger group; - private BigInteger generator; + private final BigInteger group; + private final BigInteger generator; public DHG(BigInteger group, BigInteger generator, Digest digest) { super(new DH(), digest); diff --git a/src/main/java/com/hierynomus/sshj/transport/kex/DHGroups.java b/src/main/java/com/hierynomus/sshj/transport/kex/DHGroups.java index abff0149..6387f0a7 100644 --- a/src/main/java/com/hierynomus/sshj/transport/kex/DHGroups.java +++ b/src/main/java/com/hierynomus/sshj/transport/kex/DHGroups.java @@ -68,10 +68,10 @@ public class DHGroups { public static class Factory implements net.schmizz.sshj.common.Factory.Named { - private String name; - private BigInteger group; - private BigInteger generator; - private Factory.Named digestFactory; + private final String name; + private final BigInteger group; + private final BigInteger generator; + private final Factory.Named digestFactory; public Factory(String name, BigInteger group, BigInteger generator, Named digestFactory) { this.name = name; diff --git a/src/main/java/com/hierynomus/sshj/transport/mac/Macs.java b/src/main/java/com/hierynomus/sshj/transport/mac/Macs.java index 9383b351..37b210a0 100644 --- a/src/main/java/com/hierynomus/sshj/transport/mac/Macs.java +++ b/src/main/java/com/hierynomus/sshj/transport/mac/Macs.java @@ -71,10 +71,10 @@ public class Macs { public static class Factory implements net.schmizz.sshj.common.Factory.Named { - private String name; - private String algorithm; - private int bSize; - private int defBSize; + private final String name; + private final String algorithm; + private final int bSize; + private final int defBSize; private final boolean etm; public Factory(String name, String algorithm, int bSize, int defBSize, boolean etm) { diff --git a/src/main/java/com/hierynomus/sshj/transport/verification/KnownHostMatchers.java b/src/main/java/com/hierynomus/sshj/transport/verification/KnownHostMatchers.java index cb664563..cf16cb51 100644 --- a/src/main/java/com/hierynomus/sshj/transport/verification/KnownHostMatchers.java +++ b/src/main/java/com/hierynomus/sshj/transport/verification/KnownHostMatchers.java @@ -17,11 +17,11 @@ package com.hierynomus.sshj.transport.verification; import net.schmizz.sshj.common.Base64DecodingException; import net.schmizz.sshj.common.Base64Decoder; -import net.schmizz.sshj.common.IOUtils; import net.schmizz.sshj.common.SSHException; import net.schmizz.sshj.transport.mac.MAC; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; import java.util.List; @@ -57,7 +57,7 @@ public class KnownHostMatchers { } private static class EquiHostMatcher implements HostMatcher { - private String host; + private final String host; public EquiHostMatcher(String host) { this.host = host; @@ -96,7 +96,7 @@ public class KnownHostMatchers { private String hashHost(String host) throws IOException, Base64DecodingException { sha1.init(getSaltyBytes()); - return "|1|" + salt + "|" + Base64.getEncoder().encodeToString(sha1.doFinal(host.getBytes(IOUtils.UTF8))); + return "|1|" + salt + "|" + Base64.getEncoder().encodeToString(sha1.doFinal(host.getBytes(StandardCharsets.UTF_8))); } private byte[] getSaltyBytes() throws IOException, Base64DecodingException { diff --git a/src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyFileUtil.java b/src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyFileUtil.java index 94802c41..d033f608 100644 --- a/src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyFileUtil.java +++ b/src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyFileUtil.java @@ -47,8 +47,7 @@ public class OpenSSHKeyFileUtil { * @param publicKey Public key accessible through a {@code Reader} */ public static ParsedPubKey initPubKey(Reader publicKey) throws IOException { - final BufferedReader br = new BufferedReader(publicKey); - try { + try (BufferedReader br = new BufferedReader(publicKey)) { String keydata; while ((keydata = br.readLine()) != null) { keydata = keydata.trim(); @@ -68,8 +67,6 @@ public class OpenSSHKeyFileUtil { throw new IOException("Public key file is blank"); } catch (Base64DecodingException err) { throw new IOException("Public key decoding failed", err); - } finally { - br.close(); } } diff --git a/src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyV1KeyFile.java b/src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyV1KeyFile.java index 3fc30ba8..568649a3 100644 --- a/src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyV1KeyFile.java +++ b/src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyV1KeyFile.java @@ -21,9 +21,6 @@ import com.hierynomus.sshj.transport.cipher.BlockCiphers; import com.hierynomus.sshj.transport.cipher.ChachaPolyCiphers; import com.hierynomus.sshj.transport.cipher.GcmCiphers; import com.hierynomus.sshj.userauth.keyprovider.bcrypt.BCrypt; -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.transport.cipher.Cipher; @@ -351,8 +348,14 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider { keyBuffer.readUInt32(); // length of privatekey+publickey byte[] privKey = new byte[32]; keyBuffer.readRawBytes(privKey); // string privatekey - keyBuffer.readRawBytes(new byte[32]); // string publickey (again...) - kp = new KeyPair(publicKey, new EdDSAPrivateKey(new EdDSAPrivateKeySpec(privKey, EdDSANamedCurveTable.getByName("Ed25519")))); + + final byte[] pubKey = new byte[32]; + keyBuffer.readRawBytes(pubKey); // string publickey (again...) + + final PrivateKey edPrivateKey = Ed25519KeyFactory.getPrivateKey(privKey); + final PublicKey edPublicKey = Ed25519KeyFactory.getPublicKey(pubKey); + + kp = new KeyPair(edPublicKey, edPrivateKey); break; case RSA: final RSAPrivateCrtKeySpec rsaPrivateCrtKeySpec = readRsaPrivateKeySpec(keyBuffer); diff --git a/src/main/java/net/schmizz/keepalive/Heartbeater.java b/src/main/java/net/schmizz/keepalive/Heartbeater.java index e18ea997..e6dbb535 100644 --- a/src/main/java/net/schmizz/keepalive/Heartbeater.java +++ b/src/main/java/net/schmizz/keepalive/Heartbeater.java @@ -29,6 +29,6 @@ final class Heartbeater @Override protected void doKeepAlive() throws TransportException { - conn.getTransport().write(new SSHPacket(Message.IGNORE)); + conn.getTransport().write(new SSHPacket(Message.IGNORE).putString("")); } } diff --git a/src/main/java/net/schmizz/sshj/SSHClient.java b/src/main/java/net/schmizz/sshj/SSHClient.java index 78b91c5f..792038c5 100644 --- a/src/main/java/net/schmizz/sshj/SSHClient.java +++ b/src/main/java/net/schmizz/sshj/SSHClient.java @@ -59,6 +59,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.util.*; @@ -126,7 +127,7 @@ public class SSHClient private final List forwarders = new ArrayList(); /** character set of the remote machine */ - protected Charset remoteCharset = IOUtils.UTF8; + protected Charset remoteCharset = StandardCharsets.UTF_8; /** Default constructor. Initializes this object using {@link DefaultConfig}. */ public SSHClient() { @@ -765,7 +766,7 @@ public class SSHClient * remote character set or {@code null} for default */ public void setRemoteCharset(Charset remoteCharset) { - this.remoteCharset = remoteCharset != null ? remoteCharset : IOUtils.UTF8; + this.remoteCharset = remoteCharset != null ? remoteCharset : StandardCharsets.UTF_8; } @Override diff --git a/src/main/java/net/schmizz/sshj/common/Buffer.java b/src/main/java/net/schmizz/sshj/common/Buffer.java index a47ddd37..13f0b5be 100644 --- a/src/main/java/net/schmizz/sshj/common/Buffer.java +++ b/src/main/java/net/schmizz/sshj/common/Buffer.java @@ -17,6 +17,7 @@ package net.schmizz.sshj.common; import java.math.BigInteger; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.PublicKey; import java.util.Arrays; @@ -428,7 +429,7 @@ public class Buffer> { */ public String readString() throws BufferException { - return readString(IOUtils.UTF8); + return readString(StandardCharsets.UTF_8); } /** @@ -454,7 +455,7 @@ public class Buffer> { } public T putString(String string) { - return putString(string, IOUtils.UTF8); + return putString(string, StandardCharsets.UTF_8); } /** diff --git a/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java b/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java index 265943a3..d9c16b7c 100644 --- a/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java +++ b/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java @@ -17,8 +17,8 @@ package net.schmizz.sshj.common; import java.nio.ByteBuffer; import java.nio.CharBuffer; -import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; +import java.nio.charset.StandardCharsets; import java.util.Arrays; /** Utility functions for byte arrays. */ @@ -141,7 +141,7 @@ public class ByteArrayUtils { * @return UTF-8 bytes of the string */ public static byte[] encodeSensitiveStringToUtf8(char[] str) { - CharsetEncoder charsetEncoder = Charset.forName("UTF-8").newEncoder(); + CharsetEncoder charsetEncoder = StandardCharsets.UTF_8.newEncoder(); ByteBuffer utf8Buffer = ByteBuffer.allocate((int) (str.length * charsetEncoder.maxBytesPerChar())); assert utf8Buffer.hasArray(); charsetEncoder.encode(CharBuffer.wrap(str), utf8Buffer, true); diff --git a/src/main/java/net/schmizz/sshj/common/Ed25519KeyFactory.java b/src/main/java/net/schmizz/sshj/common/Ed25519KeyFactory.java new file mode 100644 index 00000000..5ca77130 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/Ed25519KeyFactory.java @@ -0,0 +1,90 @@ +/* + * 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.common; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.Objects; + +/** + * Factory for generating Edwards-curve 25519 Public and Private Keys + */ +public class Ed25519KeyFactory { + private static final int KEY_LENGTH = 32; + + private static final String KEY_ALGORITHM = "Ed25519"; + + private static final byte[] ED25519_PKCS8_PRIVATE_KEY_HEADER = Base64.getDecoder().decode("MC4CAQEwBQYDK2VwBCIEIA"); + + private static final byte[] ED25519_PKCS8_PUBLIC_KEY_HEADER = Base64.getDecoder().decode("MCowBQYDK2VwAyEA"); + + private static final int PRIVATE_KEY_ENCODED_LENGTH = 48; + + private static final int PUBLIC_KEY_ENCODED_LENGTH = 44; + + private Ed25519KeyFactory() { + + } + + /** + * Get Edwards-curve Private Key for private key binary + * + * @param privateKeyBinary Private Key byte array consisting of 32 bytes + * @return Edwards-curve 25519 Private Key + * @throws GeneralSecurityException Thrown on failure to generate Private Key + */ + public static PrivateKey getPrivateKey(final byte[] privateKeyBinary) throws GeneralSecurityException { + Objects.requireNonNull(privateKeyBinary, "Private Key byte array required"); + if (privateKeyBinary.length == KEY_LENGTH) { + final byte[] privateKeyEncoded = new byte[PRIVATE_KEY_ENCODED_LENGTH]; + System.arraycopy(ED25519_PKCS8_PRIVATE_KEY_HEADER, 0, privateKeyEncoded, 0, ED25519_PKCS8_PRIVATE_KEY_HEADER.length); + System.arraycopy(privateKeyBinary, 0, privateKeyEncoded, ED25519_PKCS8_PRIVATE_KEY_HEADER.length, KEY_LENGTH); + final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyEncoded); + + final KeyFactory keyFactory = SecurityUtils.getKeyFactory(KEY_ALGORITHM); + return keyFactory.generatePrivate(keySpec); + } else { + throw new IllegalArgumentException("Key length of 32 bytes required"); + } + } + + /** + * Get Edwards-curve Public Key for public key binary + * + * @param publicKeyBinary Public Key byte array consisting of 32 bytes + * @return Edwards-curve 25519 Public Key + * @throws GeneralSecurityException Thrown on failure to generate Public Key + */ + public static PublicKey getPublicKey(final byte[] publicKeyBinary) throws GeneralSecurityException { + Objects.requireNonNull(publicKeyBinary, "Public Key byte array required"); + if (publicKeyBinary.length == KEY_LENGTH) { + final byte[] publicKeyEncoded = new byte[PUBLIC_KEY_ENCODED_LENGTH]; + System.arraycopy(ED25519_PKCS8_PUBLIC_KEY_HEADER, 0, publicKeyEncoded, 0, ED25519_PKCS8_PUBLIC_KEY_HEADER.length); + System.arraycopy(publicKeyBinary, 0, publicKeyEncoded, ED25519_PKCS8_PUBLIC_KEY_HEADER.length, KEY_LENGTH); + final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyEncoded); + + final KeyFactory keyFactory = SecurityUtils.getKeyFactory(KEY_ALGORITHM); + return keyFactory.generatePublic(keySpec); + } else { + throw new IllegalArgumentException("Key length of 32 bytes required"); + } + } +} diff --git a/src/main/java/net/schmizz/sshj/common/IOUtils.java b/src/main/java/net/schmizz/sshj/common/IOUtils.java index 8c39bf91..8d905826 100644 --- a/src/main/java/net/schmizz/sshj/common/IOUtils.java +++ b/src/main/java/net/schmizz/sshj/common/IOUtils.java @@ -19,12 +19,9 @@ import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.Charset; public class IOUtils { - public static final Charset UTF8 = Charset.forName("UTF-8"); - public static void closeQuietly(Closeable... closeables) { closeQuietly(LoggerFactory.DEFAULT, closeables); } diff --git a/src/main/java/net/schmizz/sshj/common/KeyType.java b/src/main/java/net/schmizz/sshj/common/KeyType.java index b34ebbe4..79a3801e 100644 --- a/src/main/java/net/schmizz/sshj/common/KeyType.java +++ b/src/main/java/net/schmizz/sshj/common/KeyType.java @@ -16,13 +16,8 @@ package net.schmizz.sshj.common; import com.hierynomus.sshj.common.KeyAlgorithm; -import com.hierynomus.sshj.signature.Ed25519PublicKey; import com.hierynomus.sshj.signature.SignatureEdDSA; import com.hierynomus.sshj.userauth.certificate.Certificate; -import net.i2p.crypto.eddsa.EdDSAPublicKey; -import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec; -import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; -import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; import net.schmizz.sshj.common.Buffer.BufferException; import net.schmizz.sshj.signature.Signature; import net.schmizz.sshj.signature.SignatureDSA; @@ -178,20 +173,16 @@ public enum KeyType { public PublicKey readPubKeyFromBuffer(Buffer buf) throws GeneralSecurityException { try { final int keyLen = buf.readUInt32AsInt(); - final byte[] p = new byte[keyLen]; - buf.readRawBytes(p); + final byte[] publicKeyBinary = new byte[keyLen]; + buf.readRawBytes(publicKeyBinary); if (log.isDebugEnabled()) { log.debug(String.format("Key algo: %s, Key curve: 25519, Key Len: %s\np: %s", sType, keyLen, - Arrays.toString(p)) + Arrays.toString(publicKeyBinary)) ); } - - EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519"); - EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(p, ed25519); - return new Ed25519PublicKey(publicSpec); - + return Ed25519KeyFactory.getPublicKey(publicKeyBinary); } catch (Buffer.BufferException be) { throw new SSHRuntimeException(be); } @@ -199,13 +190,17 @@ public enum KeyType { @Override protected void writePubKeyContentsIntoBuffer(PublicKey pk, Buffer buf) { - EdDSAPublicKey key = (EdDSAPublicKey) pk; - buf.putBytes(key.getAbyte()); + final byte[] encoded = pk.getEncoded(); + final int keyLength = 32; + final int headerLength = encoded.length - keyLength; + final byte[] encodedPublicKey = new byte[keyLength]; + System.arraycopy(encoded, headerLength, encodedPublicKey, 0, keyLength); + buf.putBytes(encodedPublicKey); } @Override protected boolean isMyType(Key key) { - return "EdDSA".equals(key.getAlgorithm()); + return "EdDSA".equals(key.getAlgorithm()) || "Ed25519".equals(key.getAlgorithm()); } }, diff --git a/src/main/java/net/schmizz/sshj/common/SecurityUtils.java b/src/main/java/net/schmizz/sshj/common/SecurityUtils.java index 2492b256..4a800078 100644 --- a/src/main/java/net/schmizz/sshj/common/SecurityUtils.java +++ b/src/main/java/net/schmizz/sshj/common/SecurityUtils.java @@ -286,8 +286,8 @@ public class SecurityUtils { if (securityProvider == null && registerBouncyCastle == null) { LOG.info("BouncyCastle not registered, using the default JCE provider"); } else if (securityProvider == null) { - LOG.error("Failed to register BouncyCastle as the defaut JCE provider"); - throw new SSHRuntimeException("Failed to register BouncyCastle as the defaut JCE provider"); + LOG.error("Failed to register BouncyCastle as the default JCE provider"); + throw new SSHRuntimeException("Failed to register BouncyCastle as the default JCE provider"); } } registrationDone = true; diff --git a/src/main/java/net/schmizz/sshj/connection/channel/AbstractChannel.java b/src/main/java/net/schmizz/sshj/connection/channel/AbstractChannel.java index 0ea8fae4..5dd5728e 100644 --- a/src/main/java/net/schmizz/sshj/connection/channel/AbstractChannel.java +++ b/src/main/java/net/schmizz/sshj/connection/channel/AbstractChannel.java @@ -27,6 +27,7 @@ import org.slf4j.Logger; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.TimeUnit; @@ -90,7 +91,7 @@ public abstract class AbstractChannel this.log = loggerFactory.getLogger(getClass()); this.trans = conn.getTransport(); - this.remoteCharset = remoteCharset != null ? remoteCharset : IOUtils.UTF8; + this.remoteCharset = remoteCharset != null ? remoteCharset : StandardCharsets.UTF_8; id = conn.nextID(); lwin = new Window.Local(conn.getWindowSize(), conn.getMaxPacketSize(), loggerFactory); diff --git a/src/main/java/net/schmizz/sshj/connection/channel/ChannelOutputStream.java b/src/main/java/net/schmizz/sshj/connection/channel/ChannelOutputStream.java index 29701b18..43584b84 100644 --- a/src/main/java/net/schmizz/sshj/connection/channel/ChannelOutputStream.java +++ b/src/main/java/net/schmizz/sshj/connection/channel/ChannelOutputStream.java @@ -37,7 +37,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti private final DataBuffer buffer = new DataBuffer(); private final byte[] b = new byte[1]; - private AtomicBoolean closed; + private final AtomicBoolean closed; private SSHException error; private final class DataBuffer { diff --git a/src/main/java/net/schmizz/sshj/sftp/RandomAccessRemoteFile.java b/src/main/java/net/schmizz/sshj/sftp/RandomAccessRemoteFile.java index 3ee6a882..6abf7f3d 100644 --- a/src/main/java/net/schmizz/sshj/sftp/RandomAccessRemoteFile.java +++ b/src/main/java/net/schmizz/sshj/sftp/RandomAccessRemoteFile.java @@ -317,13 +317,10 @@ public class RandomAccessRemoteFile @Override public void writeUTF(String str) throws IOException { - final DataOutputStream dos = new DataOutputStream(rf.new RemoteFileOutputStream(fp)); - try { + try (DataOutputStream dos = new DataOutputStream(rf.new RemoteFileOutputStream(fp));) { dos.writeUTF(str); - } finally { - dos.close(); + fp += dos.size(); } - fp += dos.size(); } } diff --git a/src/main/java/net/schmizz/sshj/sftp/SFTPEngine.java b/src/main/java/net/schmizz/sshj/sftp/SFTPEngine.java index eeac5d8a..974dbf30 100644 --- a/src/main/java/net/schmizz/sshj/sftp/SFTPEngine.java +++ b/src/main/java/net/schmizz/sshj/sftp/SFTPEngine.java @@ -17,7 +17,6 @@ package net.schmizz.sshj.sftp; import com.hierynomus.sshj.common.ThreadNameProvider; import net.schmizz.concurrent.Promise; -import net.schmizz.sshj.common.IOUtils; import net.schmizz.sshj.common.LoggerFactory; import net.schmizz.sshj.common.SSHException; import net.schmizz.sshj.connection.channel.direct.Session; @@ -28,6 +27,7 @@ import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; @@ -373,7 +373,7 @@ public class SFTPEngine /** Using UTF-8 */ protected static String readSingleName(Response res) throws IOException { - return readSingleName(res, IOUtils.UTF8); + return readSingleName(res, StandardCharsets.UTF_8); } /** Using any character set */ diff --git a/src/main/java/net/schmizz/sshj/sftp/SFTPFileTransfer.java b/src/main/java/net/schmizz/sshj/sftp/SFTPFileTransfer.java index bef6c694..0ab2568f 100644 --- a/src/main/java/net/schmizz/sshj/sftp/SFTPFileTransfer.java +++ b/src/main/java/net/schmizz/sshj/sftp/SFTPFileTransfer.java @@ -52,7 +52,7 @@ public class SFTPFileTransfer throws IOException { upload(source, dest, 0); } - + @Override public void upload(String source, String dest, long byteOffset) throws IOException { @@ -64,7 +64,7 @@ public class SFTPFileTransfer throws IOException { download(source, dest, 0); } - + @Override public void download(String source, String dest, long byteOffset) throws IOException { @@ -75,7 +75,7 @@ public class SFTPFileTransfer public void upload(LocalSourceFile localFile, String remotePath) throws IOException { upload(localFile, remotePath, 0); } - + @Override public void upload(LocalSourceFile localFile, String remotePath, long byteOffset) throws IOException { new Uploader(localFile, remotePath).upload(getTransferListener(), byteOffset); @@ -85,7 +85,7 @@ public class SFTPFileTransfer public void download(String source, LocalDestFile dest) throws IOException { download(source, dest, 0); } - + @Override public void download(String source, LocalDestFile dest, long byteOffset) throws IOException { final PathComponents pathComponents = engine.getPathHelper().getComponents(source); @@ -140,12 +140,9 @@ public class SFTPFileTransfer final LocalDestFile local) throws IOException { final LocalDestFile adjusted = local.getTargetDirectory(remote.getName()); - final RemoteDirectory rd = engine.openDir(remote.getPath()); - try { + try (RemoteDirectory rd = engine.openDir(remote.getPath())) { for (RemoteResourceInfo rri : rd.scan(getDownloadFilter())) download(listener, rri, adjusted.getChild(rri.getName()), 0); // not supporting individual byte offsets for these files - } finally { - rd.close(); } return adjusted; } @@ -156,23 +153,16 @@ public class SFTPFileTransfer final long byteOffset) throws IOException { final LocalDestFile adjusted = local.getTargetFile(remote.getName()); - final RemoteFile rf = engine.open(remote.getPath()); - try { + try (RemoteFile rf = engine.open(remote.getPath())) { log.debug("Attempting to download {} with offset={}", remote.getPath(), byteOffset); - final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16, byteOffset); - final OutputStream os = adjusted.getOutputStream(byteOffset != 0); - try { + try (RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16, byteOffset); + OutputStream os = adjusted.getOutputStream(byteOffset != 0)) { new StreamCopier(rfis, os, engine.getLoggerFactory()) .bufSize(engine.getSubsystem().getLocalMaxPacketSize()) .keepFlushing(false) .listener(listener) .copy(); - } finally { - rfis.close(); - os.close(); } - } finally { - rf.close(); } return adjusted; } @@ -266,7 +256,7 @@ public class SFTPFileTransfer // Starting at some offset, append modes = EnumSet.of(OpenMode.WRITE, OpenMode.APPEND); } - + log.debug("Attempting to upload {} with offset={}", local.getName(), byteOffset); rf = engine.open(adjusted, modes); fis = local.getInputStream(); diff --git a/src/main/java/net/schmizz/sshj/signature/SignatureECDSA.java b/src/main/java/net/schmizz/sshj/signature/SignatureECDSA.java index d1d5d5a1..190f7f1b 100644 --- a/src/main/java/net/schmizz/sshj/signature/SignatureECDSA.java +++ b/src/main/java/net/schmizz/sshj/signature/SignatureECDSA.java @@ -77,7 +77,7 @@ public class SignatureECDSA extends AbstractSignatureDSA { } - private String keyTypeName; + private final String keyTypeName; public SignatureECDSA(String algorithm, String keyTypeName) { super(algorithm, keyTypeName); diff --git a/src/main/java/net/schmizz/sshj/signature/SignatureRSA.java b/src/main/java/net/schmizz/sshj/signature/SignatureRSA.java index e9695ac4..ad8275dc 100644 --- a/src/main/java/net/schmizz/sshj/signature/SignatureRSA.java +++ b/src/main/java/net/schmizz/sshj/signature/SignatureRSA.java @@ -87,7 +87,7 @@ public class SignatureRSA } - private KeyType keyType; + private final KeyType keyType; public SignatureRSA(String algorithm, KeyType keyType, String name) { diff --git a/src/main/java/net/schmizz/sshj/transport/TransportImpl.java b/src/main/java/net/schmizz/sshj/transport/TransportImpl.java index e5738da9..85c11b6d 100644 --- a/src/main/java/net/schmizz/sshj/transport/TransportImpl.java +++ b/src/main/java/net/schmizz/sshj/transport/TransportImpl.java @@ -33,6 +33,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -207,7 +208,7 @@ public final class TransportImpl */ private void sendClientIdent() throws IOException { log.info("Client identity string: {}", clientID); - connInfo.out.write((clientID + "\r\n").getBytes(IOUtils.UTF8)); + connInfo.out.write((clientID + "\r\n").getBytes(StandardCharsets.UTF_8)); connInfo.out.flush(); } diff --git a/src/main/java/net/schmizz/sshj/transport/kex/ECDHNistP.java b/src/main/java/net/schmizz/sshj/transport/kex/ECDHNistP.java index be836680..56081dd0 100644 --- a/src/main/java/net/schmizz/sshj/transport/kex/ECDHNistP.java +++ b/src/main/java/net/schmizz/sshj/transport/kex/ECDHNistP.java @@ -25,7 +25,7 @@ import java.security.spec.ECGenParameterSpec; public class ECDHNistP extends AbstractDHG { - private String curve; + private final String curve; /** Named factory for ECDHNistP key exchange */ public static class Factory521 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 a5821908..c887def8 100644 --- a/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java +++ b/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java @@ -31,6 +31,7 @@ import java.io.FileWriter; import java.io.IOException; import java.io.Reader; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.PublicKey; import java.security.spec.RSAPublicKeySpec; @@ -186,12 +187,9 @@ public class OpenSSHKnownHosts public void write() throws IOException { - final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(khFile)); - try { + try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(khFile))) { for (KnownHostEntry entry : entries) - bos.write((entry.getLine() + LS).getBytes(IOUtils.UTF8)); - } finally { - bos.close(); + bos.write((entry.getLine() + LS).getBytes(StandardCharsets.UTF_8)); } } @@ -477,7 +475,7 @@ public class OpenSSHKnownHosts } public static class BadHostEntry implements KnownHostEntry { - private String line; + private final String line; public BadHostEntry(String line) { this.line = line; diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/EncryptedPEMKeyReader.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/EncryptedPEMKeyReader.java new file mode 100644 index 00000000..9a2a9bba --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/EncryptedPEMKeyReader.java @@ -0,0 +1,149 @@ +/* + * 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 com.hierynomus.sshj.common.KeyDecryptionFailedException; +import net.schmizz.sshj.common.ByteArrayUtils; +import net.schmizz.sshj.userauth.password.PasswordFinder; +import net.schmizz.sshj.userauth.password.PasswordUtils; +import net.schmizz.sshj.userauth.password.Resource; +import org.bouncycastle.openssl.PEMDecryptor; +import org.bouncycastle.openssl.PEMDecryptorProvider; +import org.bouncycastle.openssl.PEMException; +import org.bouncycastle.openssl.bc.BcPEMDecryptorProvider; +import org.bouncycastle.operator.OperatorCreationException; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * PEM Key Reader implementation supporting historical password-based encryption from OpenSSL EVP_BytesToKey + */ +class EncryptedPEMKeyReader extends StandardPEMKeyReader { + private static final String PROC_TYPE_ENCRYPTED_HEADER = "Proc-Type: 4,ENCRYPTED"; + + private static final Pattern DEK_INFO_PATTERN = Pattern.compile("^DEK-Info: ([A-Z0-9\\-]+),([A-F0-9]{16,32})$"); + + private static final int DEK_INFO_ALGORITHM_GROUP = 1; + + private static final int DEK_INFO_IV_GROUP = 2; + + private final PasswordFinder passwordFinder; + + private final Resource resource; + + EncryptedPEMKeyReader(final PasswordFinder passwordFinder, final Resource resource) { + this.passwordFinder = Objects.requireNonNull(passwordFinder, "Password Finder required"); + this.resource = Objects.requireNonNull(resource, "Resource required"); + } + + @Override + public PEMKey readPemKey(final BufferedReader bufferedReader) throws IOException { + final PEMKey pemKey = super.readPemKey(bufferedReader); + final List headers = pemKey.getHeaders(); + + final PEMKey processedPemKey; + if (isEncrypted(headers)) { + processedPemKey = readEncryptedPemKey(pemKey); + } else { + processedPemKey = pemKey; + } + + return processedPemKey; + } + + private boolean isEncrypted(final List headers) { + return headers.contains(PROC_TYPE_ENCRYPTED_HEADER); + } + + private PEMKey readEncryptedPemKey(final PEMKey pemKey) throws IOException { + final List headers = pemKey.getHeaders(); + final DataEncryptionKeyInfo dataEncryptionKeyInfo = getDataEncryptionKeyInfo(headers); + final byte[] pemKeyBody = pemKey.getBody(); + + byte[] decryptedPemKeyBody = null; + char[] password = passwordFinder.reqPassword(resource); + while (password != null) { + try { + decryptedPemKeyBody = getDecryptedPemKeyBody(password, pemKeyBody, dataEncryptionKeyInfo); + break; + } catch (final KeyDecryptionFailedException e) { + if (passwordFinder.shouldRetry(resource)) { + password = passwordFinder.reqPassword(resource); + } else { + throw e; + } + } + } + + if (decryptedPemKeyBody == null) { + throw new KeyDecryptionFailedException("PEM Key password-based decryption failed"); + } + + return new PEMKey(pemKey.getPemKeyType(), headers, decryptedPemKeyBody); + } + + private byte[] getDecryptedPemKeyBody(final char[] password, final byte[] pemKeyBody, final DataEncryptionKeyInfo dataEncryptionKeyInfo) throws IOException { + final String algorithm = dataEncryptionKeyInfo.algorithm; + try { + final PEMDecryptorProvider pemDecryptorProvider = new BcPEMDecryptorProvider(password); + final PEMDecryptor pemDecryptor = pemDecryptorProvider.get(algorithm); + final byte[] initializationVector = dataEncryptionKeyInfo.initializationVector; + return pemDecryptor.decrypt(pemKeyBody, initializationVector); + } catch (final OperatorCreationException e) { + throw new IOException(String.format("PEM decryption support not found for algorithm [%s]", algorithm), e); + } catch (final PEMException e) { + throw new KeyDecryptionFailedException(String.format("PEM Key decryption failed for algorithm [%s]", algorithm), e); + } finally { + PasswordUtils.blankOut(password); + } + } + + private DataEncryptionKeyInfo getDataEncryptionKeyInfo(final List headers) throws IOException { + DataEncryptionKeyInfo dataEncryptionKeyInfo = null; + + for (final String header : headers) { + final Matcher matcher = DEK_INFO_PATTERN.matcher(header); + if (matcher.matches()) { + final String algorithm = matcher.group(DEK_INFO_ALGORITHM_GROUP); + final String initializationVectorGroup = matcher.group(DEK_INFO_IV_GROUP); + final byte[] initializationVector = ByteArrayUtils.parseHex(initializationVectorGroup); + dataEncryptionKeyInfo = new DataEncryptionKeyInfo(algorithm, initializationVector); + } + } + + if (dataEncryptionKeyInfo == null) { + throw new IOException("Data Encryption Key Information header [DEK-Info] not found"); + } + + return dataEncryptionKeyInfo; + } + + private static class DataEncryptionKeyInfo { + private final String algorithm; + + private final byte[] initializationVector; + + private DataEncryptionKeyInfo(final String algorithm, final byte[] initializationVector) { + this.algorithm = algorithm; + this.initializationVector = initializationVector; + } + } +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PEMKey.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PEMKey.java new file mode 100644 index 00000000..0ba51baa --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PEMKey.java @@ -0,0 +1,75 @@ +/* + * 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.util.List; +import java.util.Objects; + +/** + * PEM Key container with identified Key Type and decoded body + */ +public class PEMKey { + private final PEMKeyType pemKeyType; + + private final List headers; + + private final byte[] body; + + PEMKey(final PEMKeyType pemKeyType, final List headers, final byte[] body) { + this.pemKeyType = Objects.requireNonNull(pemKeyType, "PEM Key Type required"); + this.headers = Objects.requireNonNull(headers, "Headers required"); + this.body = Objects.requireNonNull(body, "Body required"); + } + + PEMKeyType getPemKeyType() { + return pemKeyType; + } + + List getHeaders() { + return headers; + } + + byte[] getBody() { + return body.clone(); + } + + public enum PEMKeyType { + /** RFC 3279 Section 2.3.2 */ + DSA("-----BEGIN DSA PRIVATE KEY-----"), + + /** RFC 5915 Section 3 */ + EC("-----BEGIN EC PRIVATE KEY-----"), + + /** RFC 8017 Appendix 1.2 */ + RSA("-----BEGIN RSA PRIVATE KEY-----"), + + /** RFC 5208 Section 5 */ + PKCS8("-----BEGIN PRIVATE KEY-----"), + + /** RFC 5208 Section 6 */ + PKCS8_ENCRYPTED("-----BEGIN ENCRYPTED PRIVATE KEY-----"); + + private final String header; + + PEMKeyType(final String header) { + this.header = header; + } + + String getHeader() { + return header; + } + } +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/KeyPairConverter.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PEMKeyReader.java similarity index 59% rename from src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/KeyPairConverter.java rename to src/main/java/net/schmizz/sshj/userauth/keyprovider/PEMKeyReader.java index dec183ee..1c8c31e3 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/KeyPairConverter.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PEMKeyReader.java @@ -13,23 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.schmizz.sshj.userauth.keyprovider.pkcs; - -import org.bouncycastle.openssl.PEMKeyPair; +package net.schmizz.sshj.userauth.keyprovider; +import java.io.BufferedReader; import java.io.IOException; /** - * Converter from typed object to PEM Key Pair - * @param Object Type + * Abstraction for parsing and returning PEM Keys */ -public interface KeyPairConverter { +interface PEMKeyReader { /** - * Get PEM Key Pair from typed object + * Read PEM Key from buffered reader * - * @param object Typed Object - * @return PEM Key Pair - * @throws IOException Thrown on conversion failures + * @param bufferedReader Buffered Reader containing lines from resource reader + * @return PEM Key + * @throws IOException Thrown on failure to read PEM Key from resources */ - PEMKeyPair getKeyPair(T object) throws IOException; + PEMKey readPemKey(BufferedReader bufferedReader) throws IOException; } 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 300e4aa1..0322b68d 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java @@ -15,37 +15,66 @@ */ package net.schmizz.sshj.userauth.keyprovider; +import com.hierynomus.asn1.ASN1InputStream; +import com.hierynomus.asn1.encodingrules.der.DERDecoder; +import com.hierynomus.asn1.types.ASN1Tag; +import com.hierynomus.asn1.types.constructed.ASN1Sequence; +import com.hierynomus.asn1.types.constructed.ASN1TaggedObject; +import com.hierynomus.asn1.types.primitive.ASN1Integer; +import com.hierynomus.asn1.types.primitive.ASN1ObjectIdentifier; +import com.hierynomus.asn1.types.string.ASN1BitString; +import com.hierynomus.asn1.types.string.ASN1OctetString; +import com.hierynomus.sshj.common.KeyAlgorithm; import com.hierynomus.sshj.common.KeyDecryptionFailedException; -import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.ECDSACurve; +import net.schmizz.sshj.common.ECDSAKeyFactory; import net.schmizz.sshj.common.SecurityUtils; -import net.schmizz.sshj.userauth.keyprovider.pkcs.KeyPairConverter; -import net.schmizz.sshj.userauth.keyprovider.pkcs.PrivateKeyInfoKeyPairConverter; import net.schmizz.sshj.userauth.password.PasswordUtils; -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -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.JceOpenSSLPKCS8DecryptorProviderBuilder; -import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; -import org.bouncycastle.operator.InputDecryptorProvider; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; -import org.bouncycastle.pkcs.PKCSException; +import net.schmizz.sshj.userauth.keyprovider.PEMKey.PEMKeyType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.io.BufferedReader; import java.io.IOException; +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.ECField; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPublicKeySpec; /** * Key File implementation supporting PEM-encoded PKCS8 and PKCS1 formats with or without password-based encryption */ public class PKCS8KeyFile extends BaseFileKeyProvider { + /** Bouncy Castle class for detecting support of historical OpenSSL password-based decryption */ + private static final String BOUNCY_CASTLE_CLASS = "org.bouncycastle.openssl.PEMDecryptor"; - public static class Factory - implements net.schmizz.sshj.common.Factory.Named { + private static final boolean HISTORICAL_DECRYPTION_SUPPORTED = isHistoricalDecryptionSupported(); + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + public static class Factory implements net.schmizz.sshj.common.Factory.Named { @Override public FileKeyProvider create() { @@ -58,58 +87,47 @@ public class PKCS8KeyFile extends BaseFileKeyProvider { } } - protected final Logger log = LoggerFactory.getLogger(getClass()); + @Override + protected KeyPair readKeyPair() throws IOException { + final PEMKeyReader pemKeyReader; - protected KeyPairConverter privateKeyInfoKeyPairConverter = new PrivateKeyInfoKeyPairConverter(); - - protected KeyPair readKeyPair() - throws IOException { - KeyPair kp = null; - - 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(); - if (SecurityUtils.getSecurityProvider() != null) { - pemConverter.setProvider(SecurityUtils.getSecurityProvider()); - } - - if (o instanceof PEMEncryptedKeyPair) { - final PEMEncryptedKeyPair encryptedKeyPair = (PEMEncryptedKeyPair) o; - final PEMKeyPair pemKeyPair = readEncryptedKeyPair(encryptedKeyPair); - kp = pemConverter.getKeyPair(pemKeyPair); - } else if (o instanceof PEMKeyPair) { - kp = pemConverter.getKeyPair((PEMKeyPair) o); - } else if (o instanceof PrivateKeyInfo) { - final PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) o; - final PEMKeyPair pemKeyPair = privateKeyInfoKeyPairConverter.getKeyPair(privateKeyInfo); - kp = pemConverter.getKeyPair(pemKeyPair); - } else if (o instanceof PKCS8EncryptedPrivateKeyInfo) { - final PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = (PKCS8EncryptedPrivateKeyInfo) o; - final PrivateKeyInfo privateKeyInfo = readEncryptedPrivateKeyInfo(encryptedPrivateKeyInfo); - final PEMKeyPair pemKeyPair = privateKeyInfoKeyPairConverter.getKeyPair(privateKeyInfo); - kp = pemConverter.getKeyPair(pemKeyPair); - } else { - log.warn("Unexpected PKCS8 PEM Object [{}]", o); - } - - } catch (EncryptionException e) { - if (pwdf != null && pwdf.shouldRetry(resource)) - continue; - else - throw new KeyDecryptionFailedException(e); - } finally { - IOUtils.closeQuietly(r); + if (HISTORICAL_DECRYPTION_SUPPORTED) { + if (pwdf == null) { + pemKeyReader = new StandardPEMKeyReader(); + } else { + pemKeyReader = new EncryptedPEMKeyReader(pwdf, resource); } - break; + } else { + pemKeyReader = new StandardPEMKeyReader(); } - if (kp == null) - throw new IOException("Could not read key pair from: " + resource); - return kp; + try (BufferedReader bufferedReader = new BufferedReader(resource.getReader())) { + final PEMKey pemKey = pemKeyReader.readPemKey(bufferedReader); + return readKeyPair(pemKey); + } + } + + private KeyPair readKeyPair(final PEMKey pemKey) throws IOException { + final KeyPair keyPair; + + final PEMKeyType pemKeyType = pemKey.getPemKeyType(); + final byte[] pemKeyBody = pemKey.getBody(); + + if (PEMKeyType.DSA == pemKeyType) { + keyPair = readDsaKeyPair(pemKeyBody); + } else if (PEMKeyType.EC == pemKeyType) { + keyPair = readEcKeyPair(pemKeyBody); + } else if (PEMKeyType.PKCS8 == pemKeyType) { + keyPair = getPkcs8KeyPair(pemKeyBody); + } else if (PEMKeyType.PKCS8_ENCRYPTED == pemKeyType) { + keyPair = readEncryptedPkcs8KeyPair(pemKeyBody); + } else if (PEMKeyType.RSA == pemKeyType) { + keyPair = readRsaKeyPair(pemKeyBody); + } else { + throw new IOException(String.format("PEM Key Type [%s] not supported", pemKeyType)); + } + + return keyPair; } @Override @@ -117,36 +135,304 @@ public class PKCS8KeyFile extends BaseFileKeyProvider { return "PKCS8KeyFile{resource=" + resource + "}"; } - private PEMKeyPair readEncryptedKeyPair(final PEMEncryptedKeyPair encryptedKeyPair) throws IOException { - final JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder(); - if (SecurityUtils.getSecurityProvider() != null) { - builder.setProvider(SecurityUtils.getSecurityProvider()); - } - char[] passphrase = null; - try { - passphrase = pwdf == null ? null : pwdf.reqPassword(resource); - return encryptedKeyPair.decryptKeyPair(builder.build(passphrase)); - } finally { - PasswordUtils.blankOut(passphrase); + private KeyPair readDsaKeyPair(final byte[] pemKeyBody) throws IOException { + try (ASN1InputStream inputStream = new ASN1InputStream(new DERDecoder(), pemKeyBody)) { + final ASN1Sequence sequence = inputStream.readObject(); + + final BigInteger p = getBigInteger(sequence, 1); + final BigInteger q = getBigInteger(sequence, 2); + final BigInteger g = getBigInteger(sequence, 3); + + final BigInteger y = getBigInteger(sequence, 4); + final BigInteger x = getBigInteger(sequence, 5); + + final DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g); + final DSAPublicKeySpec publicKeySpec = new DSAPublicKeySpec(y, p, q, g); + + final KeyFactory keyFactory = SecurityUtils.getKeyFactory(KeyAlgorithm.DSA); + final PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + final PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); + return new KeyPair(publicKey, privateKey); + } catch (final Exception e) { + throw new IOException("PEM Key [DSA] processing failed", e); } } - private PrivateKeyInfo readEncryptedPrivateKeyInfo(final PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo) throws EncryptionException { - final JceOpenSSLPKCS8DecryptorProviderBuilder builder = new JceOpenSSLPKCS8DecryptorProviderBuilder(); - if (SecurityUtils.getSecurityProvider() != null) { - builder.setProvider(SecurityUtils.getSecurityProvider()); + private KeyPair readRsaKeyPair(final byte[] pemKeyBody) throws IOException { + try (ASN1InputStream inputStream = new ASN1InputStream(new DERDecoder(), pemKeyBody)) { + final ASN1Sequence sequence = inputStream.readObject(); + final BigInteger modulus = getBigInteger(sequence, 1); + final BigInteger publicExponent = getBigInteger(sequence, 2); + final BigInteger privateExponent = getBigInteger(sequence, 3); + final BigInteger prime1 = getBigInteger(sequence, 4); + final BigInteger prime2 = getBigInteger(sequence, 5); + final BigInteger exponent1 = getBigInteger(sequence, 6); + final BigInteger exponent2 = getBigInteger(sequence, 7); + final BigInteger coefficient = getBigInteger(sequence, 8); + + final RSAPrivateCrtKeySpec privateKeySpec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent, prime1, prime2, exponent1, exponent2, coefficient); + final KeyFactory keyFactory = SecurityUtils.getKeyFactory(KeyAlgorithm.RSA); + final PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); + + final RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(modulus, publicExponent); + final PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + + return new KeyPair(publicKey, privateKey); + } catch (final Exception e) { + throw new IOException("PEM Key [RSA] processing failed", e); } - char[] passphrase = null; + } + + private KeyPair readEcKeyPair(final byte[] pemKeyBody) throws IOException { + try (ASN1InputStream inputStream = new ASN1InputStream(new DERDecoder(), pemKeyBody)) { + final ASN1Sequence sequence = inputStream.readObject(); + + final ASN1TaggedObject taggedObjectParameters = (ASN1TaggedObject) sequence.get(2); + final ASN1ObjectIdentifier objectIdentifier = (ASN1ObjectIdentifier) taggedObjectParameters.getObject(); + final String objectId = objectIdentifier.getValue(); + final ECNamedCurveObjectIdentifier ecNamedCurveObjectIdentifier = getEcNamedCurve(objectId); + + final ASN1OctetString privateKeyOctetString = (ASN1OctetString) sequence.get(1); + final BigInteger privateKeyInteger = new BigInteger(1, privateKeyOctetString.getValue()); + final ECPrivateKey privateKey = (ECPrivateKey) ECDSAKeyFactory.getPrivateKey(privateKeyInteger, ecNamedCurveObjectIdentifier.ecdsaCurve); + final ECParameterSpec ecParameterSpec = privateKey.getParams(); + + final ASN1TaggedObject taggedBitString = (ASN1TaggedObject) sequence.get(3); + final ASN1BitString publicKeyBitString = (ASN1BitString) taggedBitString.getObject(); + final byte[] bitString = publicKeyBitString.getValueBytes(); + final PublicKey publicKey = getEcPublicKey(bitString, ecParameterSpec); + return new KeyPair(publicKey, privateKey); + } catch (final Exception e) { + throw new IOException("PEM Key [EC] processing failed", e); + } + } + + private ECNamedCurveObjectIdentifier getEcNamedCurve(final String objectId) { + ECNamedCurveObjectIdentifier objectIdentifierFound = null; + + for (final ECNamedCurveObjectIdentifier objectIdentifier : ECNamedCurveObjectIdentifier.values()) { + if (objectIdentifier.objectId.equals(objectId)) { + objectIdentifierFound = objectIdentifier; + } + } + + if (objectIdentifierFound == null) { + throw new IllegalArgumentException(String.format("ECDSA Key Algorithm [%s] not supported", objectId)); + } + + return objectIdentifierFound; + } + + private KeyPair readEncryptedPkcs8KeyPair(final byte[] pemKeyBody) throws IOException { + if (pwdf == null) { + throw new KeyDecryptionFailedException("Password not provided for encrypted PKCS8 key"); + } + + KeyPair keyPair = null; try { - passphrase = pwdf == null ? null : pwdf.reqPassword(resource); - final InputDecryptorProvider inputDecryptorProvider = builder.build(passphrase); - return encryptedPrivateKeyInfo.decryptPrivateKeyInfo(inputDecryptorProvider); - } catch (final OperatorCreationException e) { - throw new EncryptionException("Loading Password for Encrypted Private Key Failed", e); - } catch (final PKCSException e) { - throw new EncryptionException("Reading Encrypted Private Key Failed", e); + char[] password = pwdf.reqPassword(resource); + while (password != null) { + try { + final PKCS8EncodedKeySpec encodedKeySpec = getPkcs8DecryptedKeySpec(password, pemKeyBody); + keyPair = getPkcs8KeyPair(encodedKeySpec.getEncoded()); + break; + } catch (final KeyDecryptionFailedException e) { + if (pwdf.shouldRetry(resource)) { + password = pwdf.reqPassword(resource); + } else { + throw e; + } + } + } + } catch (final GeneralSecurityException e) { + throw new IOException("PEM Key [PKCS8] processing failed", e); + } + + if (keyPair == null) { + throw new KeyDecryptionFailedException("PEM Key [PKCS8] decryption failed"); + } + + return keyPair; + } + + private PKCS8EncodedKeySpec getPkcs8DecryptedKeySpec(final char[] password, final byte[] encoded) throws IOException, GeneralSecurityException { + try { + final EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(encoded); + final AlgorithmParameters algorithmParameters = encryptedPrivateKeyInfo.getAlgParameters(); + final String secretKeyAlgorithm = algorithmParameters.toString(); + final SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(secretKeyAlgorithm); + final PBEKeySpec secretKeySpec = new PBEKeySpec(password); + final SecretKey secretKey = secretKeyFactory.generateSecret(secretKeySpec); + final Cipher cipher = Cipher.getInstance(secretKeyAlgorithm); + cipher.init(Cipher.DECRYPT_MODE, secretKey, algorithmParameters); + + try { + return encryptedPrivateKeyInfo.getKeySpec(cipher); + } catch (final GeneralSecurityException e) { + throw new KeyDecryptionFailedException(String.format("PKCS8 Key Decryption failed for algorithm [%s]", secretKeyAlgorithm), e); + } } finally { - PasswordUtils.blankOut(passphrase); + PasswordUtils.blankOut(password); + } + } + + private KeyPair getPkcs8KeyPair(final byte[] encoded) throws IOException { + try (ASN1InputStream inputStream = new ASN1InputStream(new DERDecoder(), encoded)) { + final ASN1Sequence sequence = inputStream.readObject(); + + final ASN1Sequence privateKeyAlgorithmSequence = (ASN1Sequence) sequence.get(1); + final ASN1ObjectIdentifier privateKeyAlgorithm = (ASN1ObjectIdentifier) privateKeyAlgorithmSequence.get(0); + final String privateKeyAlgorithmObjectId = privateKeyAlgorithm.getValue(); + final KeyAlgorithmObjectIdentifier keyAlgorithmObjectIdentifier = getKeyAlgorithmObjectIdentifier(privateKeyAlgorithmObjectId); + + return getPkcs8KeyPair(keyAlgorithmObjectIdentifier, encoded); + } catch (final Exception e) { + throw new IOException("PEM Key [PKCS8] processing failed", e); + } + } + + private KeyPair getPkcs8KeyPair(final KeyAlgorithmObjectIdentifier objectIdentifier, final byte[] privateKeyInfo) throws GeneralSecurityException { + final PublicKey publicKey; + + final PrivateKey privateKey = getPkcs8PrivateKey(objectIdentifier, privateKeyInfo); + + if (privateKey instanceof RSAPrivateCrtKey) { + final RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey) privateKey; + final BigInteger modulus = rsaPrivateKey.getModulus(); + final BigInteger publicExponent = rsaPrivateKey.getPublicExponent(); + final RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(modulus, publicExponent); + final KeyFactory keyFactory = SecurityUtils.getKeyFactory(privateKey.getAlgorithm()); + publicKey = keyFactory.generatePublic(publicKeySpec); + } else if (privateKey instanceof DSAPrivateKey) { + final DSAPrivateKey dsaPrivateKey = (DSAPrivateKey) privateKey; + final DSAParams dsaParams = dsaPrivateKey.getParams(); + final BigInteger p = dsaParams.getP(); + final BigInteger g = dsaParams.getG(); + final BigInteger q = dsaParams.getQ(); + final BigInteger x = dsaPrivateKey.getX(); + final BigInteger y = g.modPow(x, p); + final DSAPublicKeySpec publicKeySpec = new DSAPublicKeySpec(y, p, q, g); + final KeyFactory keyFactory = SecurityUtils.getKeyFactory(privateKey.getAlgorithm()); + publicKey = keyFactory.generatePublic(publicKeySpec); + } else if (privateKey instanceof ECPrivateKey) { + final ECPrivateKey ecPrivateKey = (ECPrivateKey) privateKey; + final ECParameterSpec ecParameterSpec = ecPrivateKey.getParams(); + + // Read ECDSA Public Key from ASN.1 + try (ASN1InputStream inputStream = new ASN1InputStream(new DERDecoder(), privateKeyInfo)) { + final ASN1Sequence sequence = inputStream.readObject(); + final ASN1OctetString keyOctetString = (ASN1OctetString) sequence.get(2); + final byte[] keyBytes = keyOctetString.getValue(); + try (ASN1InputStream keyInputStream = new ASN1InputStream(new DERDecoder(), keyBytes)) { + final ASN1Sequence keySequence = keyInputStream.readObject(); + final ASN1TaggedObject taggedObject = (ASN1TaggedObject) keySequence.get(2); + final ASN1BitString publicKeyBitString = taggedObject.getObject(ASN1Tag.BIT_STRING); + final byte[] bitString = publicKeyBitString.getValueBytes(); + + publicKey = getEcPublicKey(bitString, ecParameterSpec); + } + } catch (final IOException e) { + throw new GeneralSecurityException("ECDSA Private Key Info parsing failed", e); + } + } else { + throw new GeneralSecurityException(String.format("PEM Key [PKCS8] algorithm [%s] Key Pair derivation not supported", privateKey.getAlgorithm())); + } + + return new KeyPair(publicKey, privateKey); + } + + private PrivateKey getPkcs8PrivateKey(final KeyAlgorithmObjectIdentifier objectIdentifier, final byte[] privateKeyInfo) throws GeneralSecurityException { + final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyInfo); + final KeyFactory keyFactory = SecurityUtils.getKeyFactory(objectIdentifier.name()); + return keyFactory.generatePrivate(keySpec); + } + + private PublicKey getEcPublicKey(final byte[] bitString, final ECParameterSpec ecParameterSpec) throws GeneralSecurityException { + final EllipticCurve ellipticCurve = ecParameterSpec.getCurve(); + final ECField ecField = ellipticCurve.getField(); + final int fieldSize = (ecField.getFieldSize() + 7) / 8; + final int publicKeyPointSize = fieldSize * 2; + + final byte[] x = new byte[fieldSize]; + final byte[] y = new byte[fieldSize]; + + final int pointOffset = bitString.length - publicKeyPointSize; + + System.arraycopy(bitString, pointOffset, x, 0, x.length); + System.arraycopy(bitString, pointOffset + y.length, y, 0, y.length); + + final BigInteger pointX = new BigInteger(1, x); + final BigInteger pointY = new BigInteger(1, y); + final ECPoint point = new ECPoint(pointX, pointY); + final ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(point, ecParameterSpec); + + final KeyFactory keyFactory = SecurityUtils.getKeyFactory(KeyAlgorithm.EC_KEYSTORE); + return keyFactory.generatePublic(publicKeySpec); + } + + private KeyAlgorithmObjectIdentifier getKeyAlgorithmObjectIdentifier(final String objectId) { + KeyAlgorithmObjectIdentifier keyAlgorithmObjectIdentifier = null; + + for (final KeyAlgorithmObjectIdentifier objectIdentifier : KeyAlgorithmObjectIdentifier.values()) { + if (objectIdentifier.getObjectId().equals(objectId)) { + keyAlgorithmObjectIdentifier = objectIdentifier; + } + } + + if (keyAlgorithmObjectIdentifier == null) { + throw new IllegalArgumentException(String.format("PKCS8 Private Key Algorithm [%s] not supported", objectId)); + } + + return keyAlgorithmObjectIdentifier; + } + + private BigInteger getBigInteger(final ASN1Sequence sequence, final int index) { + final ASN1Integer integer = (ASN1Integer) sequence.get(index); + return integer.getValue(); + } + + private static boolean isHistoricalDecryptionSupported() { + try { + // Support requires Bouncy Castle library for OpenSSL password-based decryption + Class.forName(BOUNCY_CASTLE_CLASS); + return true; + } catch (final Exception e) { + return false; + } + } + + private enum ECNamedCurveObjectIdentifier { + SECP256R1("1.2.840.10045.3.1.7", ECDSACurve.SECP256R1), + + SECP384R1("1.3.132.0.34", ECDSACurve.SECP384R1), + + SECP521R1("1.3.132.0.35", ECDSACurve.SECP521R1); + + private final String objectId; + + private final ECDSACurve ecdsaCurve; + + ECNamedCurveObjectIdentifier(final String objectId, final ECDSACurve ecdsaCurve) { + this.objectId = objectId; + this.ecdsaCurve = ecdsaCurve; + } + } + + private enum KeyAlgorithmObjectIdentifier { + DSA("1.2.840.10040.4.1"), + + EC("1.2.840.10045.2.1"), + + RSA("1.2.840.113549.1.1.1"); + + private final String objectId; + + KeyAlgorithmObjectIdentifier(final String objectId) { + this.objectId = objectId; + } + + String getObjectId() { + return objectId; } } } diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java index e2486d66..1fce269d 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java @@ -16,20 +16,12 @@ package net.schmizz.sshj.userauth.keyprovider; import com.hierynomus.sshj.common.KeyAlgorithm; -import net.i2p.crypto.eddsa.EdDSAPrivateKey; -import net.i2p.crypto.eddsa.EdDSAPublicKey; -import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec; -import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; -import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec; -import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; import net.schmizz.sshj.common.*; import net.schmizz.sshj.userauth.password.PasswordUtils; -import org.bouncycastle.crypto.generators.Argon2BytesGenerator; -import org.bouncycastle.crypto.params.Argon2Parameters; -import org.bouncycastle.util.encoders.Hex; import javax.crypto.Cipher; import javax.crypto.Mac; +import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.io.*; @@ -81,6 +73,8 @@ public class PuTTYKeyFile extends BaseFileKeyProvider { } } + private static final String KEY_DERIVATION_HEADER = "Key-Derivation"; + private Integer keyFileVersion; private byte[] privateKey; private byte[] publicKey; @@ -107,12 +101,12 @@ public class PuTTYKeyFile extends BaseFileKeyProvider { throw new IOException(String.format("Unsupported encryption: %s", encryption)); } - private Map payload = new HashMap(); + private final Map payload = new HashMap<>(); /** * For each line that looks like "Xyz: vvv", it will be stored in this map. */ - private final Map headers = new HashMap(); + private final Map headers = new HashMap<>(); protected KeyPair readKeyPair() throws IOException { this.parseKeyPair(); @@ -165,10 +159,17 @@ public class PuTTYKeyFile extends BaseFileKeyProvider { } } if (KeyType.ED25519.equals(keyType)) { - EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519"); - EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(publicKeyReader.readBytes(), ed25519); - EdDSAPrivateKeySpec privateSpec = new EdDSAPrivateKeySpec(privateKeyReader.readBytes(), ed25519); - return new KeyPair(new EdDSAPublicKey(publicSpec), new EdDSAPrivateKey(privateSpec)); + try { + final byte[] publicKeyEncoded = publicKeyReader.readBytes(); + final PublicKey edPublicKey = Ed25519KeyFactory.getPublicKey(publicKeyEncoded); + + final byte[] privateKeyEncoded = privateKeyReader.readBytes(); + final PrivateKey edPrivateKey = Ed25519KeyFactory.getPrivateKey(privateKeyEncoded); + + return new KeyPair(edPublicKey, edPrivateKey); + } catch (final GeneralSecurityException e) { + throw new IOException("Reading Ed25519 Keys failed", e); + } } final ECDSACurve ecdsaCurve; switch (keyType) { @@ -200,9 +201,8 @@ public class PuTTYKeyFile extends BaseFileKeyProvider { protected void parseKeyPair() throws IOException { this.keyFileVersion = null; - BufferedReader r = new BufferedReader(resource.getReader()); // Parse the text into headers and payloads - try { + try (BufferedReader r = new BufferedReader(resource.getReader())) { String headerName = null; String line; while ((line = r.readLine()) != null) { @@ -225,8 +225,6 @@ public class PuTTYKeyFile extends BaseFileKeyProvider { payload.put(headerName, s); } } - } finally { - r.close(); } if (this.keyFileVersion == null) { throw new IOException("Invalid key file format: missing \"PuTTY-User-Key-File-?\" entry"); @@ -263,99 +261,43 @@ public class PuTTYKeyFile extends BaseFileKeyProvider { } /** - * Converts a passphrase into a key, by following the convention that PuTTY - * uses. Only PuTTY v1/v2 key files - *

- * This is used to decrypt the private key when it's encrypted. + * Initialize Java Cipher for decryption using Secret Key derived from passphrase according to PuTTY Key Version */ - private void initCipher(final char[] passphrase, Cipher cipher) throws IOException, InvalidAlgorithmParameterException, InvalidKeyException { - // The field Key-Derivation has been introduced with Putty v3 key file format - // For v3 the algorithms are "Argon2i" "Argon2d" and "Argon2id" - String kdfAlgorithm = headers.get("Key-Derivation"); - if (kdfAlgorithm != null) { - kdfAlgorithm = kdfAlgorithm.toLowerCase(); - byte[] keyData = this.argon2(kdfAlgorithm, passphrase); - if (keyData == null) { - throw new IOException(String.format("Unsupported key derivation function: %s", kdfAlgorithm)); - } - byte[] key = new byte[32]; - byte[] iv = new byte[16]; - byte[] tag = new byte[32]; // Hmac key - System.arraycopy(keyData, 0, key, 0, 32); - System.arraycopy(keyData, 32, iv, 0, 16); - System.arraycopy(keyData, 48, tag, 0, 32); - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), - new IvParameterSpec(iv)); - verifyHmac = tag; - return; - } + private void initCipher(final char[] passphrase, final Cipher cipher) throws InvalidAlgorithmParameterException, InvalidKeyException { + final String keyDerivationHeader = headers.get(KEY_DERIVATION_HEADER); - // Key file format v1 + v2 - try { - MessageDigest digest = MessageDigest.getInstance("SHA-1"); + final SecretKey secretKey; + final IvParameterSpec ivParameterSpec; - // The encryption key is derived from the passphrase by means of a succession of - // SHA-1 hashes. - byte[] encodedPassphrase = PasswordUtils.toByteArray(passphrase); - - // Sequence number 0 - digest.update(new byte[]{0, 0, 0, 0}); - digest.update(encodedPassphrase); - byte[] key1 = digest.digest(); - - // Sequence number 1 - digest.update(new byte[]{0, 0, 0, 1}); - digest.update(encodedPassphrase); - byte[] key2 = digest.digest(); - - Arrays.fill(encodedPassphrase, (byte) 0); - - byte[] expanded = new byte[32]; - System.arraycopy(key1, 0, expanded, 0, 20); - System.arraycopy(key2, 0, expanded, 20, 12); - - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(expanded, 0, 32, "AES"), - new IvParameterSpec(new byte[16])); // initial vector=0 - - } catch (NoSuchAlgorithmException e) { - throw new IOException(e.getMessage(), e); - } - } - - /** - * Uses BouncyCastle Argon2 implementation - */ - private byte[] argon2(String algorithm, final char[] passphrase) throws IOException { - int type; - if ("argon2i".equals(algorithm)) { - type = Argon2Parameters.ARGON2_i; - } else if ("argon2d".equals(algorithm)) { - type = Argon2Parameters.ARGON2_d; - } else if ("argon2id".equals(algorithm)) { - type = Argon2Parameters.ARGON2_id; + if (keyDerivationHeader == null) { + // Key Version 1 and 2 with historical key derivation + final PuTTYSecretKeyDerivationFunction keyDerivationFunction = new V1PuTTYSecretKeyDerivationFunction(); + secretKey = keyDerivationFunction.deriveSecretKey(passphrase); + ivParameterSpec = new IvParameterSpec(new byte[16]); } else { - return null; - } - byte[] salt = Hex.decode(headers.get("Argon2-Salt")); - int iterations = Integer.parseInt(headers.get("Argon2-Passes")); - int memory = Integer.parseInt(headers.get("Argon2-Memory")); - int parallelism = Integer.parseInt(headers.get("Argon2-Parallelism")); + // Key Version 3 with Argon2 key derivation + final PuTTYSecretKeyDerivationFunction keyDerivationFunction = new V3PuTTYSecretKeyDerivationFunction(headers); + final SecretKey derivedSecretKey = keyDerivationFunction.deriveSecretKey(passphrase); + final byte[] derivedSecretKeyEncoded = derivedSecretKey.getEncoded(); - Argon2Parameters a2p = new Argon2Parameters.Builder(type) - .withVersion(Argon2Parameters.ARGON2_VERSION_13) - .withIterations(iterations) - .withMemoryAsKB(memory) - .withParallelism(parallelism) - .withSalt(salt).build(); + // Set Secret Key from first 32 bytes + final byte[] secretKeyEncoded = new byte[32]; + System.arraycopy(derivedSecretKeyEncoded, 0, secretKeyEncoded, 0, secretKeyEncoded.length); + secretKey = new SecretKeySpec(secretKeyEncoded, derivedSecretKey.getAlgorithm()); - Argon2BytesGenerator generator = new Argon2BytesGenerator(); - generator.init(a2p); - byte[] output = new byte[80]; - int bytes = generator.generateBytes(passphrase, output); - if (bytes != output.length) { - throw new IOException("Failed to generate key via Argon2"); + // Set IV from next 16 bytes + final byte[] iv = new byte[16]; + System.arraycopy(derivedSecretKeyEncoded, secretKeyEncoded.length, iv, 0, iv.length); + ivParameterSpec = new IvParameterSpec(iv); + + // Set HMAC Tag from next 32 bytes + final byte[] tag = new byte[32]; + final int tagSourcePosition = secretKeyEncoded.length + iv.length; + System.arraycopy(derivedSecretKeyEncoded, tagSourcePosition, tag, 0, tag.length); + verifyHmac = tag; } - return output; + + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec); } /** @@ -382,7 +324,7 @@ public class PuTTYKeyFile extends BaseFileKeyProvider { data.writeInt(privateKey.length); data.write(privateKey); - final String encoded = Hex.toHexString(mac.doFinal(out.toByteArray())); + final String encoded = ByteArrayUtils.toHex(mac.doFinal(out.toByteArray())); final String reference = headers.get("Private-MAC"); if (!encoded.equals(reference)) { throw new IOException("Invalid passphrase"); diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYSecretKeyDerivationFunction.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYSecretKeyDerivationFunction.java new file mode 100644 index 00000000..0088902c --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYSecretKeyDerivationFunction.java @@ -0,0 +1,31 @@ +/* + * 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 javax.crypto.SecretKey; + +/** + * Abstraction for deriving the Secret Key for decrypting PuTTY Key Files + */ +interface PuTTYSecretKeyDerivationFunction { + /** + * Derive Secret Key from provided passphrase characters + * + * @param passphrase Passphrase characters required + * @return Derived Secret Key + */ + SecretKey deriveSecretKey(char[] passphrase); +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/StandardPEMKeyReader.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/StandardPEMKeyReader.java new file mode 100644 index 00000000..077ad903 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/StandardPEMKeyReader.java @@ -0,0 +1,113 @@ +/* + * 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.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Objects; + +/** + * Standard implementation of PEM Key Reader supporting Base64 decoding without decryption + */ +class StandardPEMKeyReader implements PEMKeyReader { + private static final String HEADER_DELIMITER = "-----BEGIN"; + + private static final String FOOTER_DELIMITER = "-----END"; + + private static final char PEM_HEADER_DELIMITER = ':'; + + private static final int CHARACTER_NOT_FOUND = -1; + + private static final String HEADER_NOT_FOUND = "header not found"; + + private static final Base64.Decoder bodyDecoder = Base64.getDecoder(); + + /** + * Read PEM Key from Buffered Reader + * + * @param bufferedReader Buffered Reader containing lines from resource reader + * @return PEM Key + * @throws IOException Thrown on failure to read or decode PEM Key + */ + @Override + public PEMKey readPemKey(final BufferedReader bufferedReader) throws IOException { + Objects.requireNonNull(bufferedReader, "Reader required"); + final PEMKey.PEMKeyType pemKeyType = findPemKeyType(bufferedReader); + return readPemKeyBody(pemKeyType, bufferedReader); + } + + private PEMKey.PEMKeyType findPemKeyType(final BufferedReader bufferedReader) throws IOException { + PEMKey.PEMKeyType pemKeyTypeFound = null; + + String header = HEADER_NOT_FOUND; + String line = bufferedReader.readLine(); + readLoop: while (line != null) { + if (line.startsWith(HEADER_DELIMITER)) { + header = line; + for (final PEMKey.PEMKeyType pemKeyType : PEMKey.PEMKeyType.values()) { + if (pemKeyType.getHeader().equals(line)) { + pemKeyTypeFound = pemKeyType; + break readLoop; + } + } + } + + line = bufferedReader.readLine(); + } + + if (pemKeyTypeFound == null) { + throw new IOException(String.format("Supported PEM Key Type not found for header [%s]", header)); + } + + return pemKeyTypeFound; + } + + private PEMKey readPemKeyBody(final PEMKey.PEMKeyType pemKeyType, final BufferedReader bufferedReader) throws IOException { + final StringBuilder builder = new StringBuilder(); + + final List headers = new ArrayList<>(); + + String line = bufferedReader.readLine(); + while (line != null) { + if (line.startsWith(FOOTER_DELIMITER)) { + break; + } + + if (line.indexOf(PEM_HEADER_DELIMITER) > CHARACTER_NOT_FOUND) { + headers.add(line); + } else if (!line.isEmpty()) { + builder.append(line); + } + + line = bufferedReader.readLine(); + } + + final String pemKeyBody = builder.toString(); + final byte[] pemKeyBodyDecoded = getPemKeyBodyDecoded(pemKeyBody); + return new PEMKey(pemKeyType, headers, pemKeyBodyDecoded); + } + + private byte[] getPemKeyBodyDecoded(final String pemKeyBodyEncoded) throws IOException { + try { + return bodyDecoder.decode(pemKeyBodyEncoded); + } catch (final IllegalArgumentException e) { + throw new IOException("Base64 decoding of PEM Key failed", e); + } + } +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/V1PuTTYSecretKeyDerivationFunction.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/V1PuTTYSecretKeyDerivationFunction.java new file mode 100644 index 00000000..3448013d --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/V1PuTTYSecretKeyDerivationFunction.java @@ -0,0 +1,76 @@ +/* + * 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 net.schmizz.sshj.common.SecurityUtils; +import net.schmizz.sshj.userauth.password.PasswordUtils; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.Arrays; +import java.util.Objects; + +/** + * PuTTY Key Derivation Function supporting Version 1 and 2 Key files with historical SHA-1 key derivation + */ +class V1PuTTYSecretKeyDerivationFunction implements PuTTYSecretKeyDerivationFunction { + private static final String SECRET_KEY_ALGORITHM = "AES"; + + private static final String DIGEST_ALGORITHM = "SHA-1"; + + /** + * Derive Secret Key from provided passphrase characters + * + * @param passphrase Passphrase characters required + * @return Derived Secret Key + */ + public SecretKey deriveSecretKey(char[] passphrase) { + Objects.requireNonNull(passphrase, "Passphrase required"); + + final MessageDigest digest = getMessageDigest(); + final byte[] encodedPassphrase = PasswordUtils.toByteArray(passphrase); + + // Sequence number 0 + digest.update(new byte[]{0, 0, 0, 0}); + digest.update(encodedPassphrase); + final byte[] key1 = digest.digest(); + + // Sequence number 1 + digest.update(new byte[]{0, 0, 0, 1}); + digest.update(encodedPassphrase); + final byte[] key2 = digest.digest(); + + Arrays.fill(encodedPassphrase, (byte) 0); + + final byte[] secretKeyEncoded = new byte[32]; + System.arraycopy(key1, 0, secretKeyEncoded, 0, 20); + System.arraycopy(key2, 0, secretKeyEncoded, 20, 12); + + return new SecretKeySpec(secretKeyEncoded, SECRET_KEY_ALGORITHM); + } + + private MessageDigest getMessageDigest() { + try { + return SecurityUtils.getMessageDigest(DIGEST_ALGORITHM); + } catch (final NoSuchAlgorithmException | NoSuchProviderException e) { + final String message = String.format("Message Digest Algorithm [%s] not supported", DIGEST_ALGORITHM); + throw new IllegalStateException(message, e); + } + } +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/V3PuTTYSecretKeyDerivationFunction.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/V3PuTTYSecretKeyDerivationFunction.java new file mode 100644 index 00000000..111b6316 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/V3PuTTYSecretKeyDerivationFunction.java @@ -0,0 +1,98 @@ +/* + * 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 net.schmizz.sshj.common.ByteArrayUtils; +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.bouncycastle.crypto.params.Argon2Parameters; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.util.Map; +import java.util.Objects; + +/** + * PuTTY Key Derivation Function supporting Version 3 Key files with Argon2 Key Derivation using Bouncy Castle + */ +class V3PuTTYSecretKeyDerivationFunction implements PuTTYSecretKeyDerivationFunction { + private static final String SECRET_KEY_ALGORITHM = "AES"; + + private static final int KEY_LENGTH = 80; + + private final Map headers; + + V3PuTTYSecretKeyDerivationFunction(final Map headers) { + this.headers = Objects.requireNonNull(headers, "Headers required"); + } + + /** + * Derive Secret Key from provided passphrase characters + * + * @param passphrase Passphrase characters required + * @return Derived Secret Key + */ + public SecretKey deriveSecretKey(char[] passphrase) { + Objects.requireNonNull(passphrase, "Passphrase required"); + + final Argon2Parameters parameters = getParameters(); + final Argon2BytesGenerator generator = new Argon2BytesGenerator(); + generator.init(parameters); + + final byte[] secretKeyEncoded = new byte[KEY_LENGTH]; + final int bytesGenerated = generator.generateBytes(passphrase, secretKeyEncoded); + if (KEY_LENGTH == bytesGenerated) { + return new SecretKeySpec(secretKeyEncoded, SECRET_KEY_ALGORITHM); + } else { + final String message = String.format("Argon2 bytes generated [%d] not expected", bytesGenerated); + throw new IllegalStateException(message); + } + } + + private Argon2Parameters getParameters() { + final int algorithmType = getAlgorithmType(); + + final byte[] salt = ByteArrayUtils.parseHex(headers.get("Argon2-Salt")); + final int iterations = Integer.parseInt(headers.get("Argon2-Passes")); + final int memory = Integer.parseInt(headers.get("Argon2-Memory")); + final int parallelism = Integer.parseInt(headers.get("Argon2-Parallelism")); + + return new Argon2Parameters.Builder(algorithmType) + .withVersion(Argon2Parameters.ARGON2_VERSION_13) + .withIterations(iterations) + .withMemoryAsKB(memory) + .withParallelism(parallelism) + .withSalt(salt) + .build(); + } + + private int getAlgorithmType() { + final String algorithm = headers.get("Key-Derivation"); + + final int algorithmType; + if ("argon2i".equalsIgnoreCase(algorithm)) { + algorithmType = Argon2Parameters.ARGON2_i; + } else if ("argon2d".equalsIgnoreCase(algorithm)) { + algorithmType = Argon2Parameters.ARGON2_d; + } else if ("argon2id".equalsIgnoreCase(algorithm)) { + algorithmType = Argon2Parameters.ARGON2_id; + } else { + final String message = String.format("Key-Derivation [%s] not supported", algorithm); + throw new IllegalArgumentException(message); + } + + return algorithmType; + } +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/DSAPrivateKeyInfoKeyPairConverter.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/DSAPrivateKeyInfoKeyPairConverter.java deleted file mode 100644 index 03b36bc1..00000000 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/DSAPrivateKeyInfoKeyPairConverter.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.pkcs; - -import org.bouncycastle.asn1.ASN1Integer; -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.ASN1Sequence; -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -import org.bouncycastle.asn1.x509.AlgorithmIdentifier; -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; -import org.bouncycastle.crypto.params.DSAParameters; -import org.bouncycastle.openssl.PEMKeyPair; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.math.BigInteger; -import java.util.Objects; - -/** - * Key Pair Converter from DSA Private Key Information to PEM Key Pair - */ -class DSAPrivateKeyInfoKeyPairConverter implements KeyPairConverter { - private static final Logger logger = LoggerFactory.getLogger(DSAPrivateKeyInfoKeyPairConverter.class); - - private static final int P_INDEX = 0; - - private static final int Q_INDEX = 1; - - private static final int G_INDEX = 2; - - /** - * Get PEM Key Pair calculating DSA Public Key from DSA Private Key Information - * - * @param privateKeyInfo DSA Private Key Information - * @return PEM Key Pair - * @throws IOException Thrown on Public Key parsing failures - */ - @Override - public PEMKeyPair getKeyPair(final PrivateKeyInfo privateKeyInfo) throws IOException { - Objects.requireNonNull(privateKeyInfo, "Private Key Info required"); - final AlgorithmIdentifier algorithmIdentifier = privateKeyInfo.getPrivateKeyAlgorithm(); - final ASN1ObjectIdentifier algorithm = algorithmIdentifier.getAlgorithm(); - if (X9ObjectIdentifiers.id_dsa.equals(algorithm)) { - logger.debug("DSA Algorithm Found [{}]", algorithm); - } else { - throw new IllegalArgumentException(String.format("DSA Algorithm OID required [%s]", algorithm)); - } - final ASN1Integer encodedPublicKey = getEncodedPublicKey(privateKeyInfo); - final SubjectPublicKeyInfo subjectPublicKeyInfo = new SubjectPublicKeyInfo(algorithmIdentifier, encodedPublicKey); - return new PEMKeyPair(subjectPublicKeyInfo, privateKeyInfo); - } - - /** - * Get ASN.1 Encoded Public Key calculated according to RFC 6979 Section 2.2 - * - * @param privateKeyInfo DSA Private Key Information - * @return ASN.1 Encoded DSA Public Key - * @throws IOException Thrown on failures parsing private key - */ - private ASN1Integer getEncodedPublicKey(final PrivateKeyInfo privateKeyInfo) throws IOException { - final ASN1Integer privateKey = ASN1Integer.getInstance(privateKeyInfo.parsePrivateKey()); - final AlgorithmIdentifier algorithmIdentifier = privateKeyInfo.getPrivateKeyAlgorithm(); - final DSAParameters dsaParameters = getDsaParameters(algorithmIdentifier); - final BigInteger publicKey = dsaParameters.getG().modPow(privateKey.getValue(), dsaParameters.getP()); - return new ASN1Integer(publicKey); - } - - private DSAParameters getDsaParameters(final AlgorithmIdentifier algorithmIdentifier) { - final ASN1Sequence sequence = ASN1Sequence.getInstance(algorithmIdentifier.getParameters()); - final ASN1Integer p = ASN1Integer.getInstance(sequence.getObjectAt(P_INDEX)); - final ASN1Integer q = ASN1Integer.getInstance(sequence.getObjectAt(Q_INDEX)); - final ASN1Integer g = ASN1Integer.getInstance(sequence.getObjectAt(G_INDEX)); - return new DSAParameters(p.getValue(), q.getValue(), g.getValue()); - } -} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/ECDSAPrivateKeyInfoKeyPairConverter.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/ECDSAPrivateKeyInfoKeyPairConverter.java deleted file mode 100644 index 4d7cfd48..00000000 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/ECDSAPrivateKeyInfoKeyPairConverter.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.pkcs; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -import org.bouncycastle.asn1.sec.ECPrivateKey; -import org.bouncycastle.asn1.x509.AlgorithmIdentifier; -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.asn1.x9.X9ECParameters; -import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; -import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; -import org.bouncycastle.math.ec.ECMultiplier; -import org.bouncycastle.math.ec.ECPoint; -import org.bouncycastle.math.ec.FixedPointCombMultiplier; -import org.bouncycastle.openssl.PEMKeyPair; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.math.BigInteger; -import java.util.Objects; - -/** - * Key Pair Converter from ECDSA Private Key Information to PEM Key Pair - */ -class ECDSAPrivateKeyInfoKeyPairConverter implements KeyPairConverter { - private static final Logger logger = LoggerFactory.getLogger(ECDSAPrivateKeyInfoKeyPairConverter.class); - - private static final boolean POINT_COMPRESSED = false; - - /** - * Get PEM Key Pair calculating ECDSA Public Key from ECDSA Private Key Information - * - * @param privateKeyInfo ECDSA Private Key Information - * @return PEM Key Pair - * @throws IOException Thrown on Public Key parsing failures - */ - @Override - public PEMKeyPair getKeyPair(final PrivateKeyInfo privateKeyInfo) throws IOException { - Objects.requireNonNull(privateKeyInfo, "Private Key Info required"); - final AlgorithmIdentifier algorithmIdentifier = privateKeyInfo.getPrivateKeyAlgorithm(); - final ASN1ObjectIdentifier algorithm = algorithmIdentifier.getAlgorithm(); - if (X9ObjectIdentifiers.id_ecPublicKey.equals(algorithm)) { - logger.debug("ECDSA Algorithm Found [{}]", algorithm); - } else { - throw new IllegalArgumentException(String.format("ECDSA Algorithm OID required [%s]", algorithm)); - } - final byte[] encodedPublicKey = getEncodedPublicKey(privateKeyInfo); - final SubjectPublicKeyInfo subjectPublicKeyInfo = new SubjectPublicKeyInfo(algorithmIdentifier, encodedPublicKey); - return new PEMKeyPair(subjectPublicKeyInfo, privateKeyInfo); - } - - /** - * Get Encoded Elliptic Curve Public Key calculated according to RFC 6979 Section 2.2 - * - * @param privateKeyInfo ECDSA Private Key Information - * @return Encoded Elliptic Curve Public Key - * @throws IOException Thrown on failures parsing private key - */ - private byte[] getEncodedPublicKey(final PrivateKeyInfo privateKeyInfo) throws IOException { - final X9ECParameters parameters = getParameters(privateKeyInfo.getPrivateKeyAlgorithm()); - final ECPrivateKey ecPrivateKey = ECPrivateKey.getInstance(privateKeyInfo.parsePrivateKey()); - final ECPoint publicKey = getPublicKey(parameters, ecPrivateKey.getKey()); - return publicKey.getEncoded(POINT_COMPRESSED); - } - - private X9ECParameters getParameters(final AlgorithmIdentifier algorithmIdentifier) { - final ASN1ObjectIdentifier encodedParameters = ASN1ObjectIdentifier.getInstance(algorithmIdentifier.getParameters()); - return ECUtil.getNamedCurveByOid(encodedParameters); - } - - private ECPoint getPublicKey(final X9ECParameters parameters, final BigInteger privateKey) { - final ECMultiplier multiplier = new FixedPointCombMultiplier(); - return multiplier.multiply(parameters.getG(), privateKey); - } -} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/PrivateKeyInfoKeyPairConverter.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/PrivateKeyInfoKeyPairConverter.java deleted file mode 100644 index 04527607..00000000 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/PrivateKeyInfoKeyPairConverter.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.pkcs; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -import org.bouncycastle.asn1.x509.AlgorithmIdentifier; -import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; -import org.bouncycastle.openssl.PEMKeyPair; - -import java.io.IOException; -import java.util.Objects; - -/** - * Key Pair Converter for Private Key Information using known Algorithm Object Identifiers - */ -public class PrivateKeyInfoKeyPairConverter implements KeyPairConverter { - private DSAPrivateKeyInfoKeyPairConverter dsaPrivateKeyInfoKeyPairConverter = new DSAPrivateKeyInfoKeyPairConverter(); - - private ECDSAPrivateKeyInfoKeyPairConverter ecdsaPrivateKeyInfoKeyPairConverter = new ECDSAPrivateKeyInfoKeyPairConverter(); - - private RSAPrivateKeyInfoKeyPairConverter rsaPrivateKeyInfoKeyPairConverter = new RSAPrivateKeyInfoKeyPairConverter(); - - /** - * Get PEM Key Pair delegating to configured converters based on Algorithm Object Identifier - * - * @param privateKeyInfo Private Key Information - * @return PEM Key Pair - * @throws IOException Thrown on conversion failures - */ - @Override - public PEMKeyPair getKeyPair(final PrivateKeyInfo privateKeyInfo) throws IOException { - Objects.requireNonNull(privateKeyInfo, "Private Key Info required"); - final AlgorithmIdentifier algorithmIdentifier = privateKeyInfo.getPrivateKeyAlgorithm(); - final ASN1ObjectIdentifier algorithm = algorithmIdentifier.getAlgorithm(); - - if (PKCSObjectIdentifiers.rsaEncryption.equals(algorithm)) { - return rsaPrivateKeyInfoKeyPairConverter.getKeyPair(privateKeyInfo); - } else if (X9ObjectIdentifiers.id_ecPublicKey.equals(algorithm)) { - return ecdsaPrivateKeyInfoKeyPairConverter.getKeyPair(privateKeyInfo); - } else if (X9ObjectIdentifiers.id_dsa.equals(algorithm)) { - return dsaPrivateKeyInfoKeyPairConverter.getKeyPair(privateKeyInfo); - } else { - throw new IllegalArgumentException(String.format("Unsupported Algorithm [%s]", algorithm)); - } - } -} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/RSAPrivateKeyInfoKeyPairConverter.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/RSAPrivateKeyInfoKeyPairConverter.java deleted file mode 100644 index 01b77cae..00000000 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/RSAPrivateKeyInfoKeyPairConverter.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.pkcs; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -import org.bouncycastle.asn1.pkcs.RSAPrivateKey; -import org.bouncycastle.asn1.pkcs.RSAPublicKey; -import org.bouncycastle.asn1.x509.AlgorithmIdentifier; -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.openssl.PEMKeyPair; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.Objects; - -/** - * Key Pair Converter from RSA Private Key Information to PEM Key Pair - */ -class RSAPrivateKeyInfoKeyPairConverter implements KeyPairConverter { - private static final Logger logger = LoggerFactory.getLogger(RSAPrivateKeyInfoKeyPairConverter.class); - - /** - * Get PEM Key Pair parsing RSA Public Key attributes from RSA Private Key Information - * - * @param privateKeyInfo RSA Private Key Information - * @return PEM Key Pair - * @throws IOException Thrown on Public Key parsing failures - */ - @Override - public PEMKeyPair getKeyPair(final PrivateKeyInfo privateKeyInfo) throws IOException { - Objects.requireNonNull(privateKeyInfo, "Private Key Info required"); - final AlgorithmIdentifier algorithmIdentifier = privateKeyInfo.getPrivateKeyAlgorithm(); - final ASN1ObjectIdentifier algorithm = algorithmIdentifier.getAlgorithm(); - if (PKCSObjectIdentifiers.rsaEncryption.equals(algorithm)) { - logger.debug("RSA Algorithm Found [{}]", algorithm); - } else { - throw new IllegalArgumentException(String.format("RSA Algorithm OID required [%s]", algorithm)); - } - - final RSAPublicKey rsaPublicKey = getRsaPublicKey(privateKeyInfo); - final SubjectPublicKeyInfo subjectPublicKeyInfo = new SubjectPublicKeyInfo(algorithmIdentifier, rsaPublicKey); - return new PEMKeyPair(subjectPublicKeyInfo, privateKeyInfo); - } - - private RSAPublicKey getRsaPublicKey(final PrivateKeyInfo privateKeyInfo) throws IOException { - final RSAPrivateKey rsaPrivateKey = RSAPrivateKey.getInstance(privateKeyInfo.parsePrivateKey()); - return new RSAPublicKey(rsaPrivateKey.getModulus(), rsaPrivateKey.getPublicExponent()); - } -} diff --git a/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java b/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java index 601a6870..14d9cef6 100644 --- a/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java +++ b/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java @@ -16,6 +16,7 @@ package net.schmizz.sshj.userauth.password; import java.io.*; +import java.nio.charset.StandardCharsets; public class PrivateKeyFileResource extends Resource { @@ -27,6 +28,6 @@ public class PrivateKeyFileResource @Override public Reader getReader() throws IOException { - return new InputStreamReader(new FileInputStream(getDetail()), "UTF-8"); + return new InputStreamReader(new FileInputStream(getDetail()), StandardCharsets.UTF_8); } } diff --git a/src/main/java/net/schmizz/sshj/xfer/scp/SCPEngine.java b/src/main/java/net/schmizz/sshj/xfer/scp/SCPEngine.java index a3a175c4..000c9b0f 100644 --- a/src/main/java/net/schmizz/sshj/xfer/scp/SCPEngine.java +++ b/src/main/java/net/schmizz/sshj/xfer/scp/SCPEngine.java @@ -29,6 +29,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; /** @see SCP Protocol */ class SCPEngine { @@ -128,7 +129,7 @@ class SCPEngine { baos.write(x); } } - final String msg = baos.toString(IOUtils.UTF8.displayName()); + final String msg = baos.toString(StandardCharsets.UTF_8.displayName()); log.debug("Read message: `{}`", msg); return msg; } diff --git a/src/main/java/net/schmizz/sshj/xfer/scp/ScpCommandLine.java b/src/main/java/net/schmizz/sshj/xfer/scp/ScpCommandLine.java index 4a9d3246..75b70297 100644 --- a/src/main/java/net/schmizz/sshj/xfer/scp/ScpCommandLine.java +++ b/src/main/java/net/schmizz/sshj/xfer/scp/ScpCommandLine.java @@ -71,7 +71,7 @@ public class ScpCommandLine { } } - private LinkedHashMap arguments = new LinkedHashMap(); + private final LinkedHashMap arguments = new LinkedHashMap(); private String path; ScpCommandLine() { diff --git a/src/test/java/com/hierynomus/sshj/connection/channel/forwarded/LocalPortForwarderTest.java b/src/test/java/com/hierynomus/sshj/connection/channel/forwarded/LocalPortForwarderTest.java index b001a98c..db2df51a 100644 --- a/src/test/java/com/hierynomus/sshj/connection/channel/forwarded/LocalPortForwarderTest.java +++ b/src/test/java/com/hierynomus/sshj/connection/channel/forwarded/LocalPortForwarderTest.java @@ -17,7 +17,6 @@ package com.hierynomus.sshj.connection.channel.forwarded; import com.hierynomus.sshj.test.HttpServer; import com.hierynomus.sshj.test.SshServerExtension; -import com.hierynomus.sshj.test.util.FileUtil; import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.connection.channel.direct.LocalPortForwarder; import net.schmizz.sshj.connection.channel.direct.Parameters; @@ -29,13 +28,10 @@ import org.junit.jupiter.api.extension.RegisterExtension; import java.io.*; import java.net.*; -import java.nio.file.Files; import static org.junit.jupiter.api.Assertions.assertEquals; public class LocalPortForwarderTest { - private static final String LOCALHOST_URL = "http://127.0.0.1:8080"; - @RegisterExtension public SshServerExtension fixture = new SshServerExtension(); @@ -43,21 +39,19 @@ public class LocalPortForwarderTest { public HttpServer httpServer = new HttpServer(); @BeforeEach - public void setUp() throws IOException { + public void setUp() { fixture.getServer().setForwardingFilter(new AcceptAllForwardingFilter()); - File file = Files.createFile(httpServer.getDocRoot().toPath().resolve("index.html")).toFile(); - FileUtil.writeToFile(file, "

Hi!

"); } @Test public void shouldHaveWorkingHttpServer() throws IOException { - assertEquals(200, httpGet()); + assertEquals(HttpURLConnection.HTTP_NOT_FOUND, httpGet()); } @Test public void shouldHaveHttpServerThatClosesConnectionAfterResponse() throws IOException { // Just to check that the test server does close connections before we try through the forwarder... - httpGetAndAssertConnectionClosedByServer(8080); + httpGetAndAssertConnectionClosedByServer(httpServer.getServerUrl().getPort()); } @Test @@ -68,7 +62,8 @@ public class LocalPortForwarderTest { ServerSocket serverSocket = new ServerSocket(); serverSocket.setReuseAddress(true); serverSocket.bind(new InetSocketAddress("0.0.0.0", 12345)); - LocalPortForwarder localPortForwarder = sshClient.newLocalPortForwarder(new Parameters("0.0.0.0", 12345, "localhost", 8080), serverSocket); + final int serverPort = httpServer.getServerUrl().getPort(); + LocalPortForwarder localPortForwarder = sshClient.newLocalPortForwarder(new Parameters("0.0.0.0", 12345, "localhost", serverPort), serverSocket); new Thread(() -> { try { localPortForwarder.listen(); @@ -90,7 +85,7 @@ public class LocalPortForwarderTest { // It returns 400 Bad Request because it's missing a bunch of info, but the HTTP response doesn't matter, we just want to test the connection closing. OutputStream outputStream = socket.getOutputStream(); PrintWriter writer = new PrintWriter(outputStream); - writer.println("GET / HTTP/1.1"); + writer.println("GET / HTTP/1.1\r\n"); writer.println(""); writer.flush(); @@ -111,7 +106,7 @@ public class LocalPortForwarderTest { } private int httpGet() throws IOException { - final URL url = new URL(LOCALHOST_URL); + final URL url = httpServer.getServerUrl().toURL(); final HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.setConnectTimeout(3000); urlConnection.setRequestMethod("GET"); diff --git a/src/test/java/com/hierynomus/sshj/connection/channel/forwarded/RemotePortForwarderTest.java b/src/test/java/com/hierynomus/sshj/connection/channel/forwarded/RemotePortForwarderTest.java index b07f2d2c..8a1da440 100644 --- a/src/test/java/com/hierynomus/sshj/connection/channel/forwarded/RemotePortForwarderTest.java +++ b/src/test/java/com/hierynomus/sshj/connection/channel/forwarded/RemotePortForwarderTest.java @@ -17,7 +17,6 @@ package com.hierynomus.sshj.connection.channel.forwarded; import com.hierynomus.sshj.test.HttpServer; import com.hierynomus.sshj.test.SshServerExtension; -import com.hierynomus.sshj.test.util.FileUtil; import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.connection.ConnectionException; import net.schmizz.sshj.connection.channel.forwarded.RemotePortForwarder; @@ -27,20 +26,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; import java.net.InetSocketAddress; +import java.net.URI; import java.net.URL; -import java.nio.file.Files; import static org.junit.jupiter.api.Assertions.assertEquals; public class RemotePortForwarderTest { private static final PortRange RANGE = new PortRange(9000, 9999); private static final String LOCALHOST = "127.0.0.1"; - private static final String LOCALHOST_URL_FORMAT = "http://127.0.0.1:%d"; - private static final InetSocketAddress HTTP_SERVER_SOCKET_ADDR = new InetSocketAddress(LOCALHOST, 8080); + private static final String URL_FORMAT = "http://%s:%d"; @RegisterExtension public SshServerExtension fixture = new SshServerExtension(); @@ -49,21 +46,21 @@ public class RemotePortForwarderTest { public HttpServer httpServer = new HttpServer(); @BeforeEach - public void setUp() throws IOException { + public void setUp() { fixture.getServer().setForwardingFilter(new AcceptAllForwardingFilter()); - File file = Files.createFile(httpServer.getDocRoot().toPath().resolve("index.html")).toFile(); - FileUtil.writeToFile(file, "

Hi!

"); } @Test public void shouldHaveWorkingHttpServer() throws IOException { - assertEquals(200, httpGet(8080)); + final URI serverUrl = httpServer.getServerUrl(); + + assertEquals(HttpURLConnection.HTTP_NOT_FOUND, httpGet(serverUrl.getHost(), serverUrl.getPort())); } @Test public void shouldDynamicallyForwardPortForLocalhost() throws IOException { SSHClient sshClient = getFixtureClient(); - RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", new SinglePort(0)); + RemotePortForwarder.Forward bind = forwardPort(sshClient, LOCALHOST, new SinglePort(0)); assertHttpGetSuccess(bind); } @@ -84,7 +81,7 @@ public class RemotePortForwarderTest { @Test public void shouldForwardPortForLocalhost() throws IOException { SSHClient sshClient = getFixtureClient(); - RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", RANGE); + RemotePortForwarder.Forward bind = forwardPort(sshClient, LOCALHOST, RANGE); assertHttpGetSuccess(bind); } @@ -103,17 +100,22 @@ public class RemotePortForwarderTest { } private void assertHttpGetSuccess(final RemotePortForwarder.Forward bind) throws IOException { - assertEquals(200, httpGet(bind.getPort())); + final String bindAddress = bind.getAddress(); + final String address = bindAddress.isEmpty() ? LOCALHOST : bindAddress; + final int port = bind.getPort(); + assertEquals(HttpURLConnection.HTTP_NOT_FOUND, httpGet(address, port)); } private RemotePortForwarder.Forward forwardPort(SSHClient sshClient, String address, PortRange portRange) throws IOException { while (true) { + final URI serverUrl = httpServer.getServerUrl(); + final InetSocketAddress serverAddress = new InetSocketAddress(serverUrl.getHost(), serverUrl.getPort()); try { return sshClient.getRemotePortForwarder().bind( // where the server should listen new RemotePortForwarder.Forward(address, portRange.nextPort()), // what we do with incoming connections that are forwarded to us - new SocketForwardingConnectListener(HTTP_SERVER_SOCKET_ADDR)); + new SocketForwardingConnectListener(serverAddress)); } catch (ConnectionException ce) { if (!portRange.hasNext()) { throw ce; @@ -122,8 +124,8 @@ public class RemotePortForwarderTest { } } - private int httpGet(final int port) throws IOException { - final URL url = new URL(String.format(LOCALHOST_URL_FORMAT, port)); + private int httpGet(final String address, final int port) throws IOException { + final URL url = new URL(String.format(URL_FORMAT, address, port)); final HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.setConnectTimeout(3000); urlConnection.setRequestMethod("GET"); diff --git a/src/test/java/com/hierynomus/sshj/test/HttpServer.java b/src/test/java/com/hierynomus/sshj/test/HttpServer.java index 259235b7..ed7b725a 100644 --- a/src/test/java/com/hierynomus/sshj/test/HttpServer.java +++ b/src/test/java/com/hierynomus/sshj/test/HttpServer.java @@ -19,42 +19,36 @@ import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; -import java.io.File; -import java.nio.file.Files; +import java.net.InetSocketAddress; +import java.net.URI; /** * Can be used to setup a test HTTP server */ public class HttpServer implements BeforeEachCallback, AfterEachCallback { - private org.glassfish.grizzly.http.server.HttpServer httpServer; + private static final String BIND_ADDRESS = "127.0.0.1"; - - private File docRoot ; + private com.sun.net.httpserver.HttpServer httpServer; @Override - public void afterEach(ExtensionContext context) throws Exception { + public void afterEach(ExtensionContext context) { try { - httpServer.shutdownNow(); - } catch (Exception e) {} - try { - docRoot.delete(); - } catch (Exception e) {} - + httpServer.stop(0); + } catch (Exception ignored) {} } @Override public void beforeEach(ExtensionContext context) throws Exception { - docRoot = Files.createTempDirectory("sshj").toFile(); - httpServer = org.glassfish.grizzly.http.server.HttpServer.createSimpleServer(docRoot.getAbsolutePath()); + httpServer = com.sun.net.httpserver.HttpServer.create(); + final InetSocketAddress socketAddress = new InetSocketAddress(BIND_ADDRESS, 0); + httpServer.bind(socketAddress, 10); httpServer.start(); } - public org.glassfish.grizzly.http.server.HttpServer getHttpServer() { - return httpServer; - } - - public File getDocRoot() { - return docRoot; + public URI getServerUrl() { + final InetSocketAddress bindAddress = httpServer.getAddress(); + final String serverUrl = String.format("http://%s:%d", BIND_ADDRESS, bindAddress.getPort()); + return URI.create(serverUrl); } } diff --git a/src/test/java/com/hierynomus/sshj/test/util/FileUtil.java b/src/test/java/com/hierynomus/sshj/test/util/FileUtil.java index a0767e12..b3b66d4a 100644 --- a/src/test/java/com/hierynomus/sshj/test/util/FileUtil.java +++ b/src/test/java/com/hierynomus/sshj/test/util/FileUtil.java @@ -18,6 +18,7 @@ package com.hierynomus.sshj.test.util; import net.schmizz.sshj.common.IOUtils; import java.io.*; +import java.nio.charset.StandardCharsets; public class FileUtil { @@ -34,12 +35,12 @@ public class FileUtil { FileInputStream fileInputStream = new FileInputStream(f); try { ByteArrayOutputStream byteArrayOutputStream = IOUtils.readFully(fileInputStream); - return byteArrayOutputStream.toString(IOUtils.UTF8.displayName()); + return byteArrayOutputStream.toString(StandardCharsets.UTF_8.displayName()); } finally { IOUtils.closeQuietly(fileInputStream); } } - + public static boolean compareFileContents(File f1, File f2) throws IOException { return readFromFile(f1).equals(readFromFile(f2)); } diff --git a/src/test/java/com/hierynomus/sshj/transport/GcmCipherDecryptSshPacketTest.java b/src/test/java/com/hierynomus/sshj/transport/GcmCipherDecryptSshPacketTest.java index 60995cb4..5d78bd53 100644 --- a/src/test/java/com/hierynomus/sshj/transport/GcmCipherDecryptSshPacketTest.java +++ b/src/test/java/com/hierynomus/sshj/transport/GcmCipherDecryptSshPacketTest.java @@ -19,13 +19,10 @@ import com.hierynomus.sshj.transport.cipher.GcmCiphers; import net.schmizz.sshj.common.IOUtils; import net.schmizz.sshj.common.SSHPacket; import net.schmizz.sshj.transport.cipher.Cipher; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.security.Security; import java.util.Arrays; import java.util.stream.Stream; @@ -42,11 +39,6 @@ public class GcmCipherDecryptSshPacketTest { return Stream.of(Arguments.of("mina-sshd", 3), Arguments.of("openssh", 4)); } - @BeforeAll - public static void setupBeforeClass() { - Security.addProvider(new BouncyCastleProvider()); - } - @ParameterizedTest @MethodSource("sets") public void testDecryptPacket(String ssh, int nr) throws Exception { diff --git a/src/test/java/com/hierynomus/sshj/transport/verification/OpenSSHKnownHostsTest.java b/src/test/java/com/hierynomus/sshj/transport/verification/OpenSSHKnownHostsTest.java index 8bfebdae..fcd286a3 100644 --- a/src/test/java/com/hierynomus/sshj/transport/verification/OpenSSHKnownHostsTest.java +++ b/src/test/java/com/hierynomus/sshj/transport/verification/OpenSSHKnownHostsTest.java @@ -16,10 +16,8 @@ package com.hierynomus.sshj.transport.verification; import net.schmizz.sshj.common.Buffer; -import net.schmizz.sshj.common.SecurityUtils; import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts; import net.schmizz.sshj.util.KeyUtil; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; @@ -41,11 +39,6 @@ public class OpenSSHKnownHostsTest { @TempDir public File tempDir; - @BeforeAll - public static void setup() { - SecurityUtils.registerSecurityProvider("org.bouncycastle.jce.provider.BouncyCastleProvider"); - } - @Test public void shouldParseAndVerifyHashedHostEntry() throws Exception { File knownHosts = knownHosts( diff --git a/src/test/java/net/schmizz/keepalive/HeartbeaterTest.java b/src/test/java/net/schmizz/keepalive/HeartbeaterTest.java new file mode 100644 index 00000000..38eadc65 --- /dev/null +++ b/src/test/java/net/schmizz/keepalive/HeartbeaterTest.java @@ -0,0 +1,51 @@ +/* + * 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.keepalive; + +import net.schmizz.sshj.DefaultConfig; +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.connection.ConnectionImpl; +import net.schmizz.sshj.transport.Transport; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class HeartbeaterTest { + + @Test + void ignoreMessageContainsData() throws Exception { + Transport transport = mock(Transport.class); + when(transport.getConfig()).thenReturn(new DefaultConfig()); + ArgumentCaptor sshPacketCaptor = ArgumentCaptor.forClass(SSHPacket.class); + when(transport.write(sshPacketCaptor.capture())).thenReturn(0L); + ConnectionImpl connection = new ConnectionImpl(transport, KeepAliveProvider.HEARTBEAT); + + KeepAlive heartbeater = connection.getKeepAlive(); + assertThat(heartbeater).isInstanceOf(Heartbeater.class); + + heartbeater.doKeepAlive(); + + SSHPacket sshPacket = sshPacketCaptor.getValue(); + assertThat(sshPacket.readMessageID()).isEqualTo(Message.IGNORE); + assertThat(sshPacket.readBytes()).isNotNull(); + } + +} diff --git a/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java b/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java index 3268e73c..b19beef7 100644 --- a/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java +++ b/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java @@ -222,7 +222,7 @@ public class OpenSSHKeyFileTest { OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile(); keyFile.init(new File("src/test/resources/keytypes/test_ed25519")); PrivateKey aPrivate = keyFile.getPrivate(); - assertThat(aPrivate.getAlgorithm(), equalTo("EdDSA")); + assertThat(aPrivate.getAlgorithm(), equalTo("Ed25519")); } @Test @@ -343,7 +343,7 @@ public class OpenSSHKeyFileTest { WipeTrackingPasswordFinder pwf = new WipeTrackingPasswordFinder(password, withRetry); keyFile.init(new File(key), pwf); PrivateKey aPrivate = keyFile.getPrivate(); - assertThat(aPrivate.getAlgorithm(), equalTo("EdDSA")); + assertThat(aPrivate.getAlgorithm(), equalTo("Ed25519")); pwf.assertWiped(); } diff --git a/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java b/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java index 48a03de1..95f2dac0 100644 --- a/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java +++ b/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java @@ -126,8 +126,8 @@ public class PKCS8KeyFileTest { public void testPkcs8Ecdsa() throws IOException { final PKCS8KeyFile provider = new PKCS8KeyFile(); provider.init(getFile("pkcs8-ecdsa")); - assertEquals(KeyAlgorithm.ECDSA, provider.getPublic().getAlgorithm()); - assertEquals(KeyAlgorithm.ECDSA, provider.getPrivate().getAlgorithm()); + assertEquals(KeyAlgorithm.EC_KEYSTORE, provider.getPublic().getAlgorithm()); + assertEquals(KeyAlgorithm.EC_KEYSTORE, provider.getPrivate().getAlgorithm()); } @Test diff --git a/src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java b/src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java index 1c582296..7e25c0a9 100644 --- a/src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java +++ b/src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java @@ -489,6 +489,8 @@ public class PuTTYKeyFileTest { key.init(new StringReader(v3_rsa_argon2d), new UnitTestPasswordFinder("changeit")); assertNotNull(key.getPrivate()); assertNotNull(key.getPublic()); + assertEquals(3, key.getKeyFileVersion()); + OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile(); referenceKey.init(new File("src/test/resources/keytypes/test_rsa_putty_priv.openssh2")); RSAPrivateKey loadedPrivate = (RSAPrivateKey) key.getPrivate(); diff --git a/src/test/java/net/schmizz/sshj/signature/SignatureDSATest.java b/src/test/java/net/schmizz/sshj/signature/SignatureDSATest.java index 758dcdba..10be4853 100644 --- a/src/test/java/net/schmizz/sshj/signature/SignatureDSATest.java +++ b/src/test/java/net/schmizz/sshj/signature/SignatureDSATest.java @@ -17,11 +17,11 @@ package net.schmizz.sshj.signature; import com.hierynomus.sshj.common.KeyAlgorithm; import net.schmizz.sshj.common.Buffer; -import net.schmizz.sshj.common.IOUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.spec.DSAPrivateKeySpec; @@ -47,7 +47,7 @@ public class SignatureDSATest { BigInteger q = new BigInteger(new byte[] { 0, -105, 96, 80, -113, 21, 35, 11, -52, -78, -110, -71, -126, -94, -21, -124, 11, -16, 88, 28, -11 }); BigInteger g = new BigInteger(new byte[] { 0, -9, -31, -96, -123, -42, -101, 61, -34, -53, -68, -85, 92, 54, -72, 87, -71, 121, -108, -81, -69, -6, 58, -22, -126, -7, 87, 76, 11, 61, 7, -126, 103, 81, 89, 87, -114, -70, -44, 89, 79, -26, 113, 7, 16, -127, -128, -76, 73, 22, 113, 35, -24, 76, 40, 22, 19, -73, -49, 9, 50, -116, -56, -90, -31, 60, 22, 122, -117, 84, 124, -115, 40, -32, -93, -82, 30, 43, -77, -90, 117, -111, 110, -93, 127, 11, -6, 33, 53, 98, -15, -5, 98, 122, 1, 36, 59, -52, -92, -15, -66, -88, 81, -112, -119, -88, -125, -33, -31, 90, -27, -97, 6, -110, -117, 102, 94, -128, 123, 85, 37, 100, 1, 76, 59, -2, -49, 73, 42 }); - byte[] data = "The Magic Words are Squeamish Ossifrage".getBytes(IOUtils.UTF8); + byte[] data = "The Magic Words are Squeamish Ossifrage".getBytes(StandardCharsets.UTF_8); // A previously signed and verified signature using the data and DSA key parameters above. byte[] dataSig = new byte[] { 0, 0, 0, 7, 115, 115, 104, 45, 100, 115, 115, 0, 0, 0, 40, 40, -71, 33, 105, -89, -107, 8, 26, -13, -90, 73, -103, 105, 112, 7, -59, -66, 46, 85, -27, 20, 82, 22, -113, -75, -86, -121, -42, -73, 78, 66, 93, -34, 39, -50, -93, 27, -5, 37, -92 }; diff --git a/src/test/java/net/schmizz/sshj/transport/DecoderDecryptGcmCipherSshPacketTest.java b/src/test/java/net/schmizz/sshj/transport/DecoderDecryptGcmCipherSshPacketTest.java index c1c6a0f6..a41b9e9d 100644 --- a/src/test/java/net/schmizz/sshj/transport/DecoderDecryptGcmCipherSshPacketTest.java +++ b/src/test/java/net/schmizz/sshj/transport/DecoderDecryptGcmCipherSshPacketTest.java @@ -22,12 +22,10 @@ import net.schmizz.sshj.common.LoggerFactory; import net.schmizz.sshj.common.SSHException; import net.schmizz.sshj.common.SSHPacket; import net.schmizz.sshj.transport.cipher.Cipher; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.security.SecureRandom; -import java.security.Security; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.mockito.ArgumentMatchers.any; @@ -49,7 +47,6 @@ public class DecoderDecryptGcmCipherSshPacketTest { @BeforeEach public void setUp() throws Exception { - Security.addProvider(new BouncyCastleProvider()); ClassLoader classLoader = DecoderDecryptGcmCipherSshPacketTest.class.getClassLoader(); iv = IOUtils.readFully(classLoader.getResourceAsStream("ssh-packets/gcm/mina-sshd/s2c.iv.bin" )).toByteArray(); key = IOUtils.readFully(classLoader.getResourceAsStream("ssh-packets/gcm/mina-sshd/s2c.key.bin" )).toByteArray(); diff --git a/src/test/java/net/schmizz/sshj/transport/mac/BaseMacTest.java b/src/test/java/net/schmizz/sshj/transport/mac/BaseMacTest.java index 2989624c..43421f36 100644 --- a/src/test/java/net/schmizz/sshj/transport/mac/BaseMacTest.java +++ b/src/test/java/net/schmizz/sshj/transport/mac/BaseMacTest.java @@ -17,10 +17,11 @@ package net.schmizz.sshj.transport.mac; import com.hierynomus.sshj.transport.mac.Macs; import net.schmizz.sshj.common.SSHRuntimeException; -import org.bouncycastle.util.encoders.Hex; +import org.apache.sshd.common.util.buffer.BufferUtils; import org.junit.jupiter.api.Test; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -28,9 +29,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; public class BaseMacTest { - private static final Charset CHARSET = Charset.forName("US-ASCII"); + private static final Charset CHARSET = StandardCharsets.US_ASCII; private static final byte[] PLAIN_TEXT = "Hello World".getBytes(CHARSET); - private static final String EXPECTED_HMAC = "24ddeed57ad91465c5b59dce74ef73778bfb0cb9"; + private static final String EXPECTED_HMAC = "24 dd ee d5 7a d9 14 65 c5 b5 9d ce 74 ef 73 77 8b fb 0c b9"; private static final String KEY = "et1Quo5ooCie6theel8i"; @Test @@ -38,7 +39,7 @@ public class BaseMacTest { BaseMAC hmac = Macs.HMACSHA1().create(); hmac.init((KEY + "foo").getBytes(CHARSET)); hmac.update(PLAIN_TEXT); - assertThat(Hex.toHexString(hmac.doFinal()), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal()), is(EXPECTED_HMAC)); } @Test @@ -54,7 +55,7 @@ public class BaseMacTest { public void testUpdateWithDoFinal() { BaseMAC hmac = initHmac(); hmac.update(PLAIN_TEXT); - assertThat(Hex.toHexString(hmac.doFinal()), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal()), is(EXPECTED_HMAC)); } @Test @@ -67,13 +68,13 @@ public class BaseMacTest { // update with the range from the second to penultimate byte hmac.update(plainText, 1, PLAIN_TEXT.length); - assertThat(Hex.toHexString(hmac.doFinal()), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal()), is(EXPECTED_HMAC)); } @Test public void testDoFinalWithInput() { BaseMAC hmac = initHmac(); - assertThat(Hex.toHexString(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); } @Test @@ -82,7 +83,7 @@ public class BaseMacTest { byte[] resultBuf = new byte[20]; hmac.update(PLAIN_TEXT); hmac.doFinal(resultBuf, 0); - assertThat(Hex.toHexString(resultBuf), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(resultBuf), is(EXPECTED_HMAC)); } private BaseMAC initHmac() { diff --git a/src/test/java/net/schmizz/sshj/transport/mac/HMACMD596Test.java b/src/test/java/net/schmizz/sshj/transport/mac/HMACMD596Test.java index 4d87fcab..f1074544 100644 --- a/src/test/java/net/schmizz/sshj/transport/mac/HMACMD596Test.java +++ b/src/test/java/net/schmizz/sshj/transport/mac/HMACMD596Test.java @@ -16,30 +16,31 @@ package net.schmizz.sshj.transport.mac; import com.hierynomus.sshj.transport.mac.Macs; -import org.bouncycastle.util.encoders.Hex; +import org.apache.sshd.common.util.buffer.BufferUtils; import org.junit.jupiter.api.Test; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class HMACMD596Test { - private static final Charset CHARSET = Charset.forName("US-ASCII"); + private static final Charset CHARSET = StandardCharsets.US_ASCII; private static final byte[] PLAIN_TEXT = "Hello World".getBytes(CHARSET); - private static final String EXPECTED_HMAC = "dff33c507463f9cf088a5ce8"; + private static final String EXPECTED_HMAC = "df f3 3c 50 74 63 f9 cf 08 8a 5c e8"; @Test public void testUpdateWithDoFinal() { BaseMAC hmac = initHmac(); hmac.update(PLAIN_TEXT); - assertThat(Hex.toHexString(hmac.doFinal()), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal()), is(EXPECTED_HMAC)); } @Test public void testDoFinalWithInput() { BaseMAC hmac = initHmac(); - assertThat(Hex.toHexString(hmac.doFinal(PLAIN_TEXT)), + assertThat(BufferUtils.toHex(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); } @@ -49,7 +50,7 @@ public class HMACMD596Test { byte[] resultBuf = new byte[12]; hmac.update(PLAIN_TEXT); hmac.doFinal(resultBuf, 0); - assertThat(Hex.toHexString(resultBuf), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(resultBuf), is(EXPECTED_HMAC)); } private BaseMAC initHmac() { diff --git a/src/test/java/net/schmizz/sshj/transport/mac/HMACMD5Test.java b/src/test/java/net/schmizz/sshj/transport/mac/HMACMD5Test.java index 0179eea6..8df1839f 100644 --- a/src/test/java/net/schmizz/sshj/transport/mac/HMACMD5Test.java +++ b/src/test/java/net/schmizz/sshj/transport/mac/HMACMD5Test.java @@ -16,30 +16,31 @@ package net.schmizz.sshj.transport.mac; import com.hierynomus.sshj.transport.mac.Macs; -import org.bouncycastle.util.encoders.Hex; +import org.apache.sshd.common.util.buffer.BufferUtils; import org.junit.jupiter.api.Test; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class HMACMD5Test { - private static final Charset CHARSET = Charset.forName("US-ASCII"); + private static final Charset CHARSET = StandardCharsets.US_ASCII; private static final byte[] PLAIN_TEXT = "Hello World".getBytes(CHARSET); - private static final String EXPECTED_HMAC = "dff33c507463f9cf088a5ce8d969c386"; + private static final String EXPECTED_HMAC = "df f3 3c 50 74 63 f9 cf 08 8a 5c e8 d9 69 c3 86"; @Test public void testUpdateWithDoFinal() { BaseMAC hmac = initHmac(); hmac.update(PLAIN_TEXT); - assertThat(Hex.toHexString(hmac.doFinal()), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal()), is(EXPECTED_HMAC)); } @Test public void testDoFinalWithInput() { BaseMAC hmac = initHmac(); - assertThat(Hex.toHexString(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); } @Test @@ -48,7 +49,7 @@ public class HMACMD5Test { byte[] resultBuf = new byte[16]; hmac.update(PLAIN_TEXT); hmac.doFinal(resultBuf, 0); - assertThat(Hex.toHexString(resultBuf), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(resultBuf), is(EXPECTED_HMAC)); } private BaseMAC initHmac() { diff --git a/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA196Test.java b/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA196Test.java index b45913de..c01abc0f 100644 --- a/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA196Test.java +++ b/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA196Test.java @@ -16,30 +16,31 @@ package net.schmizz.sshj.transport.mac; import com.hierynomus.sshj.transport.mac.Macs; -import org.bouncycastle.util.encoders.Hex; +import org.apache.sshd.common.util.buffer.BufferUtils; import org.junit.jupiter.api.Test; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class HMACSHA196Test { - private static final Charset CHARSET = Charset.forName("US-ASCII"); + private static final Charset CHARSET = StandardCharsets.US_ASCII; private static final byte[] PLAIN_TEXT = "Hello World".getBytes(CHARSET); - private static final String EXPECTED_HMAC = "24ddeed57ad91465c5b59dce"; + private static final String EXPECTED_HMAC = "24 dd ee d5 7a d9 14 65 c5 b5 9d ce"; @Test public void testUpdateWithDoFinal() { BaseMAC hmac = initHmac(); hmac.update(PLAIN_TEXT); - assertThat(Hex.toHexString(hmac.doFinal()), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal()), is(EXPECTED_HMAC)); } @Test public void testDoFinalWithInput() { BaseMAC hmac = initHmac(); - assertThat(Hex.toHexString(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); } @Test @@ -48,7 +49,7 @@ public class HMACSHA196Test { byte[] resultBuf = new byte[12]; hmac.update(PLAIN_TEXT); hmac.doFinal(resultBuf, 0); - assertThat(Hex.toHexString(resultBuf), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(resultBuf), is(EXPECTED_HMAC)); } private BaseMAC initHmac() { diff --git a/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA1Test.java b/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA1Test.java index ac1d4bc2..291458db 100644 --- a/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA1Test.java +++ b/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA1Test.java @@ -16,30 +16,31 @@ package net.schmizz.sshj.transport.mac; import com.hierynomus.sshj.transport.mac.Macs; -import org.bouncycastle.util.encoders.Hex; +import org.apache.sshd.common.util.buffer.BufferUtils; import org.junit.jupiter.api.Test; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class HMACSHA1Test { - private static final Charset CHARSET = Charset.forName("US-ASCII"); + private static final Charset CHARSET = StandardCharsets.US_ASCII; private static final byte[] PLAIN_TEXT = "Hello World".getBytes(CHARSET); - private static final String EXPECTED_HMAC = "24ddeed57ad91465c5b59dce74ef73778bfb0cb9"; + private static final String EXPECTED_HMAC = "24 dd ee d5 7a d9 14 65 c5 b5 9d ce 74 ef 73 77 8b fb 0c b9"; @Test public void testUpdateWithDoFinal() { BaseMAC hmac = initHmac(); hmac.update(PLAIN_TEXT); - assertThat(Hex.toHexString(hmac.doFinal()), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal()), is(EXPECTED_HMAC)); } @Test public void testDoFinalWithInput() { BaseMAC hmac = initHmac(); - assertThat(Hex.toHexString(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); } @Test @@ -48,7 +49,7 @@ public class HMACSHA1Test { byte[] resultBuf = new byte[20]; hmac.update(PLAIN_TEXT); hmac.doFinal(resultBuf, 0); - assertThat(Hex.toHexString(resultBuf), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(resultBuf), is(EXPECTED_HMAC)); } private BaseMAC initHmac() { diff --git a/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA2256Test.java b/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA2256Test.java index de7f43cd..0bbd4bc0 100644 --- a/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA2256Test.java +++ b/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA2256Test.java @@ -16,30 +16,31 @@ package net.schmizz.sshj.transport.mac; import com.hierynomus.sshj.transport.mac.Macs; -import org.bouncycastle.util.encoders.Hex; +import org.apache.sshd.common.util.buffer.BufferUtils; import org.junit.jupiter.api.Test; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class HMACSHA2256Test { - private static final Charset CHARSET = Charset.forName("US-ASCII"); + private static final Charset CHARSET = StandardCharsets.US_ASCII; private static final byte[] PLAIN_TEXT = "Hello World".getBytes(CHARSET); - private static final String EXPECTED_HMAC = "eb2207b2df36c7485f46d1be30418bc44e8134b4fdaabbe16d71f56ab24fce88"; + private static final String EXPECTED_HMAC = "eb 22 07 b2 df 36 c7 48 5f 46 d1 be 30 41 8b c4 4e 81 34 b4 fd aa bb e1 6d 71 f5 6a b2 4f ce 88"; @Test public void testUpdateWithDoFinal() { BaseMAC hmac = initHmac(); hmac.update(PLAIN_TEXT); - assertThat(Hex.toHexString(hmac.doFinal()), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal()), is(EXPECTED_HMAC)); } @Test public void testDoFinalWithInput() { BaseMAC hmac = initHmac(); - assertThat(Hex.toHexString(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); } @Test @@ -48,7 +49,7 @@ public class HMACSHA2256Test { byte[] resultBuf = new byte[32]; hmac.update(PLAIN_TEXT); hmac.doFinal(resultBuf, 0); - assertThat(Hex.toHexString(resultBuf), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(resultBuf), is(EXPECTED_HMAC)); } private BaseMAC initHmac() { diff --git a/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA2512Test.java b/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA2512Test.java index 2e0c7d8a..d080d6af 100644 --- a/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA2512Test.java +++ b/src/test/java/net/schmizz/sshj/transport/mac/HMACSHA2512Test.java @@ -16,30 +16,31 @@ package net.schmizz.sshj.transport.mac; import com.hierynomus.sshj.transport.mac.Macs; -import org.bouncycastle.util.encoders.Hex; +import org.apache.sshd.common.util.buffer.BufferUtils; import org.junit.jupiter.api.Test; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class HMACSHA2512Test { - private static final Charset CHARSET = Charset.forName("US-ASCII"); + private static final Charset CHARSET = StandardCharsets.US_ASCII; private static final byte[] PLAIN_TEXT = "Hello World".getBytes(CHARSET); - private static final String EXPECTED_HMAC = "28929cffc903039ef18cbc9cea6fd5f1420763af297a470d731236ed1f5a4c61d64dfccf6529265205bec932f2f7850c8ae4de1dc1a5259dc5b1fd85d8e62c04"; + private static final String EXPECTED_HMAC = "28 92 9c ff c9 03 03 9e f1 8c bc 9c ea 6f d5 f1 42 07 63 af 29 7a 47 0d 73 12 36 ed 1f 5a 4c 61 d6 4d fc cf 65 29 26 52 05 be c9 32 f2 f7 85 0c 8a e4 de 1d c1 a5 25 9d c5 b1 fd 85 d8 e6 2c 04"; @Test public void testUpdateWithDoFinal() { BaseMAC hmac = initHmac(); hmac.update(PLAIN_TEXT); - assertThat(Hex.toHexString(hmac.doFinal()), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal()), is(EXPECTED_HMAC)); } @Test public void testDoFinalWithInput() { BaseMAC hmac = initHmac(); - assertThat(Hex.toHexString(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(hmac.doFinal(PLAIN_TEXT)), is(EXPECTED_HMAC)); } @Test @@ -48,7 +49,7 @@ public class HMACSHA2512Test { byte[] resultBuf = new byte[64]; hmac.update(PLAIN_TEXT); hmac.doFinal(resultBuf, 0); - assertThat(Hex.toHexString(resultBuf), is(EXPECTED_HMAC)); + assertThat(BufferUtils.toHex(resultBuf), is(EXPECTED_HMAC)); } private BaseMAC initHmac() { diff --git a/src/test/java/net/schmizz/sshj/util/BufferTest.java b/src/test/java/net/schmizz/sshj/util/BufferTest.java index 9b9b2de1..17341249 100644 --- a/src/test/java/net/schmizz/sshj/util/BufferTest.java +++ b/src/test/java/net/schmizz/sshj/util/BufferTest.java @@ -31,12 +31,12 @@ package net.schmizz.sshj.util; import net.schmizz.sshj.common.Buffer; -import net.schmizz.sshj.common.IOUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.UnsupportedEncodingException; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import static org.junit.jupiter.api.Assertions.*; @@ -51,7 +51,7 @@ public class BufferTest { public void setUp() throws UnsupportedEncodingException, GeneralSecurityException { // for position test - byte[] data = "Hello".getBytes(IOUtils.UTF8); + byte[] data = "Hello".getBytes(StandardCharsets.UTF_8); posBuf = new Buffer.PlainBuffer(data); handyBuf = new Buffer.PlainBuffer(); } diff --git a/src/test/java/net/schmizz/sshj/util/gss/BogusGSSContext.java b/src/test/java/net/schmizz/sshj/util/gss/BogusGSSContext.java index ee361a31..b14e46e0 100644 --- a/src/test/java/net/schmizz/sshj/util/gss/BogusGSSContext.java +++ b/src/test/java/net/schmizz/sshj/util/gss/BogusGSSContext.java @@ -15,13 +15,13 @@ */ package net.schmizz.sshj.util.gss; -import net.schmizz.sshj.common.IOUtils; import org.ietf.jgss.*; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import static net.schmizz.sshj.util.gss.BogusGSSManager.unavailable; @@ -34,7 +34,7 @@ public class BogusGSSContext private static final byte[] MIC = fromString("LGTM"); private static byte[] fromString(String s) { - return s.getBytes(IOUtils.UTF8); + return s.getBytes(StandardCharsets.UTF_8); } private boolean initialized = false;