Compare commits

...

10 Commits

Author SHA1 Message Date
Jeroen van Erp
1398b190ec Prepare release 0.39.0
Some checks failed
Build SSHJ / Build with Java 11 (push) Has been cancelled
Build SSHJ / Integration test (push) Has been cancelled
2025-05-13 11:33:00 +02:00
Jeroen van Erp
acad163e50 Reduce code duplication
Some checks failed
Build SSHJ / Build with Java 11 (push) Has been cancelled
Build SSHJ / Integration test (push) Has been cancelled
Signed-off-by: Jeroen van Erp <jeroen@hierynomus.com>
2025-04-22 14:36:09 +02:00
Jeroen van Erp
8e9e644bb8 Merge branch 'master' into pr/uttamgupta/976
Signed-off-by: Jeroen van Erp <jeroen@hierynomus.com>
2025-04-22 14:35:35 +02:00
David Handermann
857d56a679 Replaced Grizzly HTTP Server with Java HTTP Server (#1010)
Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2025-04-22 11:59:58 +02:00
David Handermann
0e4a8f675f Upgraded SSHD from 2.14.0 to 2.15.0 (#1009) 2025-04-22 09:53:03 +02:00
Dmitry Sulman
95aab0088e Upgrade dependencies (#1007)
Some checks failed
Build SSHJ / Build with Java 11 (push) Has been cancelled
Build SSHJ / Integration test (push) Has been cancelled
- Upgrade Gradle to 8.13
- Upgrade SLF4J to 2.0.17
- Upgrade Mockito to 5.16.1
- Upgrade AssertJ to 3.27.3
- Upgrade Logback to 1.5.18
- Upgrade Testcontainers to 1.20.6

Signed-off-by: Dmitry Sulman <dmitry.sulman@gmail.com>
2025-03-24 14:10:44 +01:00
David Handermann
e390394e3b Refactored PuTTY Secret Key Derivation (#1003)
Some checks failed
Build SSHJ / Build with Java 11 (push) Has been cancelled
Build SSHJ / Integration test (push) Has been cancelled
- Added KeyDerivationFunction interface for PuTTY Key Files
- Moved Argon2 Key Derivation to Version 3 implementation class to separate Bouncy Castle dependency references
- Replaced Bouncy Castle Hex references with ByteArrayUtils

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2025-03-19 22:40:00 +01:00
Simon Legner
995de2da99 Make private fields final (#1005) 2025-03-19 22:18:17 +01:00
Jeroen van Erp
c3236a7405 Merge branch 'master' into pr/uttamgupta/976 2025-02-18 13:27:31 +01:00
Uttam Gupta
bfa82b4e44 Issue-973 (part1) - Removing direct dependency on BouncyCastle library and using JCE.
- This is first change towards removing direct dependency on BouncyCastle.
- Removed the imports org.bouncycastle.crypto.prng.RandomGenerator and
  org.bouncycastle.crypto.prng.VMPCRandomGenerator from the file
  BouncyCastleRandom.java, eliminating the direct dependency on BouncyCastle.
  Now using JCE with a provider, allowing SecureRandom to utilize the BC provider.
- Added a new class, BouncyCastleFipsRandom, similar to BouncyCastleRandom.java,
  which leverages the BCFIPS provider to create SecureRandom instances.
- Upgraded gradle plugin, groovy and Mockito versions to ensure compatibility with Java 21.
  from org.spockframework:spock-core:2.3-groovy-3.0 to org.spockframework:spock-core:2.4-M5-groovy-4.0
  from org.mockito:mockito-core:4.11.0 to org.mockito:mockito-core:5.15.2
  from gradle-8.2-bin.zip to gradle-8.11-bin.zip
 Testing: I have run gradle clean build with java 11 and Java 21 and works.
Test Gradle Test Executor 2; Executed: 470/469/0
✓ Test Gradle Test Run :test; Executed: 470/469/0
2025-01-13 14:54:54 -08:00
31 changed files with 476 additions and 257 deletions

View File

@@ -110,6 +110,18 @@ Issue tracker: https://github.com/hierynomus/sshj/issues
Fork away!
== Release history
SSHJ 0.39.0 (2024-02-20)::
* Upgraded dependencies
* Remove hard dependencies on BouncyCastle, making it optional.
* Merged https://github.com/hierynomus/sshj/pull/993[#993]: Remove EDDSA dependency
* Merged https://github.com/hierynomus/sshj/pull/959[#959]: Improve Curve25519 public key handling
* Merged https://github.com/hierynomus/sshj/pull/911[#911]: Fix for bad packet received with heartbeat enabled
* Merged https://github.com/hierynomus/sshj/pull/926[#926]: Close session when closing SFTP client
* Merged https://github.com/hierynomus/sshj/pull/928[#928]: Improve file-listing performance
* Merged https://github.com/hierynomus/sshj/pull/934[#934]: Don't send keep-alive before KEX done
* Merged https://github.com/hierynomus/sshj/pull/936[#936]: Improve Base64 decoding error handling
* Merged https://github.com/hierynomus/sshj/pull/925[#925]: Allow passing connected sockets
* Merged https://github.com/hierynomus/sshj/pull/922[#922]: Fix bug in known_hosts parsing
SSHJ 0.38.0 (2024-01-02)::
* Mitigated CVE-2023-48795 - Terrapin
* Merged https://github.com/hierynomus/sshj/pull/917[#917]: Implement OpenSSH strict key exchange extension

View File

@@ -47,10 +47,10 @@ 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"
@@ -89,16 +89,15 @@ testing {
configureEach {
useJUnitJupiter()
dependencies {
implementation "org.slf4j:slf4j-api:2.0.16"
implementation "org.slf4j:slf4j-api:2.0.17"
implementation 'org.spockframework:spock-core:2.3-groovy-3.0'
implementation "org.mockito:mockito-core:4.11.0"
implementation "org.assertj:assertj-core:3.24.2"
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 {
@@ -133,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 {

View File

@@ -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

View File

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

View File

@@ -121,11 +121,11 @@ public class BlockCiphers {
public static class Factory
implements net.schmizz.sshj.common.Factory.Named<Cipher> {
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

View File

@@ -33,12 +33,12 @@ public class GcmCiphers {
public static class Factory
implements net.schmizz.sshj.common.Factory.Named<Cipher> {
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

View File

@@ -40,10 +40,10 @@ public class StreamCiphers {
public static class Factory
implements net.schmizz.sshj.common.Factory.Named<Cipher> {
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.

View File

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

View File

@@ -68,10 +68,10 @@ public class DHGroups {
public static class Factory
implements net.schmizz.sshj.common.Factory.Named<KeyExchange> {
private String name;
private BigInteger group;
private BigInteger generator;
private Factory.Named<Digest> digestFactory;
private final String name;
private final BigInteger group;
private final BigInteger generator;
private final Factory.Named<Digest> digestFactory;
public Factory(String name, BigInteger group, BigInteger generator, Named<Digest> digestFactory) {
this.name = name;

View File

@@ -71,10 +71,10 @@ public class Macs {
public static class Factory implements net.schmizz.sshj.common.Factory.Named<MAC> {
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) {

View File

@@ -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;

View File

@@ -59,7 +59,8 @@ import java.util.Properties;
* net.schmizz.sshj.transport.mac.HMACMD596}</li>
* <li>{@link net.schmizz.sshj.ConfigImpl#setCompressionFactories Compression}: {@link net.schmizz.sshj.transport.compression.NoneCompression}</li>
* <li>{@link net.schmizz.sshj.ConfigImpl#setKeyAlgorithms KeyAlgorithm}: {@link net.schmizz.sshj.signature.SignatureRSA}, {@link net.schmizz.sshj.signature.SignatureDSA}</li>
* <li>{@link net.schmizz.sshj.ConfigImpl#setRandomFactory PRNG}: {@link net.schmizz.sshj.transport.random.BouncyCastleRandom}* or {@link net.schmizz.sshj.transport.random.JCERandom}</li>
* <li>{@link net.schmizz.sshj.ConfigImpl#setRandomFactory BC}: {@link net.schmizz.sshj.transport.random.BouncyCastleRandom}* or {@link net.schmizz.sshj.transport.random.JCERandom}</li>
* <li>{@link net.schmizz.sshj.ConfigImpl#setRandomFactory BCFIPS}: {@link net.schmizz.sshj.transport.random.BouncyCastleFipsRandom}* or {@link net.schmizz.sshj.transport.random.JCERandom}</li>
* <li>{@link net.schmizz.sshj.ConfigImpl#setFileKeyProviderFactories Key file support}: {@link net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile}*, {@link
* net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile}*</li>
* <li>{@link net.schmizz.sshj.ConfigImpl#setVersion Client version}: {@code "NET_3_0"}</li>

View File

@@ -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 {

View File

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

View File

@@ -87,7 +87,7 @@ public class SignatureRSA
}
private KeyType keyType;
private final KeyType keyType;
public SignatureRSA(String algorithm, KeyType keyType, String name) {

View File

@@ -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

View File

@@ -0,0 +1,39 @@
/*
* 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.transport.random;
/**
* BouncyCastle <code>Random</code>. This pseudo random number generator uses BouncyCastle fips.
* The JRE random will be used when creating a new generator to add some random data to the seed.
*/
public class BouncyCastleFipsRandom extends SecureRandomProvider {
/** Named factory for the BouncyCastle <code>Random</code> */
public static class Factory
implements net.schmizz.sshj.common.Factory<Random> {
@Override
public Random create() {
return new BouncyCastleFipsRandom();
}
}
public BouncyCastleFipsRandom() {
super("DEFAULT", "BCFIPS");
}
}

View File

@@ -13,27 +13,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* 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.transport.random;
import org.bouncycastle.crypto.prng.RandomGenerator;
import org.bouncycastle.crypto.prng.VMPCRandomGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.SecureRandom;
/**
* BouncyCastle <code>Random</code>. This pseudo random number generator uses the a very fast PRNG from BouncyCastle.
* The JRE random will be used when creating a new generator to add some random data to the seed.
*/
public class BouncyCastleRandom
implements Random {
private static final Logger logger = LoggerFactory.getLogger(BouncyCastleRandom.class);
* BouncyCastle <code>Random</code>. This pseudo random number generator uses BouncyCastle non fips.
* The JRE random will be used when creating a new generator to add some random data to the seed.
*/
public class BouncyCastleRandom extends SecureRandomProvider {
/** Named factory for the BouncyCastle <code>Random</code> */
public static class Factory
implements net.schmizz.sshj.common.Factory<Random> {
implements net.schmizz.sshj.common.Factory<Random> {
@Override
public Random create() {
@@ -42,24 +47,7 @@ public class BouncyCastleRandom
}
private final RandomGenerator random = new VMPCRandomGenerator();
public BouncyCastleRandom() {
logger.info("Generating random seed from SecureRandom.");
long t = System.currentTimeMillis();
byte[] seed = new SecureRandom().generateSeed(8);
logger.debug("Creating random seed took {} ms", System.currentTimeMillis() - t);
random.addSeedMaterial(seed);
super("DEFAULT", "BC");
}
@Override
public void fill(byte[] bytes, int start, int len) {
random.nextBytes(bytes, start, len);
}
@Override
public void fill(byte[] bytes) {
random.nextBytes(bytes);
}
}

View File

@@ -15,16 +15,11 @@
*/
package net.schmizz.sshj.transport.random;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.SecureRandom;
/** A {@link Random} implementation using the built-in {@link SecureRandom} PRNG. */
public class JCERandom
implements Random {
private static final Logger logger = LoggerFactory.getLogger(JCERandom.class);
public class JCERandom extends SecureRandomProvider {
/** Named factory for the JCE {@link Random} */
public static class Factory
implements net.schmizz.sshj.common.Factory.Named<Random> {
@@ -41,39 +36,7 @@ public class JCERandom
}
private byte[] tmp = new byte[16];
private final SecureRandom random;
JCERandom() {
logger.info("Creating new SecureRandom.");
long t = System.currentTimeMillis();
random = new SecureRandom();
logger.debug("Random creation took {} ms", System.currentTimeMillis() - t);
}
/**
* Fill the given byte-array with random bytes from this PRNG.
*
* @param foo the byte-array
* @param start the offset to start at
* @param len the number of bytes to fill
*/
@Override
public synchronized void fill(byte[] foo, int start, int len) {
if (start == 0 && len == foo.length) {
random.nextBytes(foo);
} else {
synchronized (this) {
if (len > tmp.length)
tmp = new byte[len];
random.nextBytes(tmp);
System.arraycopy(tmp, 0, foo, start, len);
}
}
}
@Override
public void fill(final byte[] bytes) {
random.nextBytes(bytes);
super();
}
}

View File

@@ -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.transport.random;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SecureRandomProvider implements Random{
private static final Logger logger = LoggerFactory.getLogger(SecureRandomProvider.class);
private byte[] tmp = new byte[16];
private SecureRandom random;
protected SecureRandomProvider() {
this.random = newRandom();
}
protected SecureRandomProvider(String algorithm, String provider) {
this.random = newRandom(algorithm, provider);
}
private static SecureRandom newRandom() {
return new SecureRandom();
}
private static SecureRandom newRandom(String algorithm, String provider) {
logger.info("Generating random seed from SecureRandom of {}.", provider);
long t = System.currentTimeMillis();
try {
// Use SecureRandom with the provider
return SecureRandom.getInstance(algorithm, provider);
} catch (NoSuchProviderException e) {
throw new RuntimeException(String.format("%s provider is not in the classpath", provider), e);
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to initialize SecureRandom with %s provider", provider), e);
} finally {
logger.debug("Creating random seed took {} ms", System.currentTimeMillis() - t);
}
}
@Override
public synchronized void fill(byte[] bytes, int start, int len) {
if (start == 0 && len == bytes.length) {
random.nextBytes(bytes);
} else {
synchronized (this) {
if (len > tmp.length) tmp = new byte[len];
random.nextBytes(tmp);
System.arraycopy(tmp, 0, bytes, start, len);
}
}
}
@Override
public void fill(byte[] bytes) {
random.nextBytes(bytes);
}
}

View File

@@ -475,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;

View File

@@ -16,6 +16,7 @@
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;
@@ -24,7 +25,6 @@ import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMException;
import org.bouncycastle.openssl.bc.BcPEMDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.util.encoders.Hex;
import java.io.BufferedReader;
import java.io.IOException;
@@ -124,7 +124,7 @@ class EncryptedPEMKeyReader extends StandardPEMKeyReader {
if (matcher.matches()) {
final String algorithm = matcher.group(DEK_INFO_ALGORITHM_GROUP);
final String initializationVectorGroup = matcher.group(DEK_INFO_IV_GROUP);
final byte[] initializationVector = Hex.decode(initializationVectorGroup);
final byte[] initializationVector = ByteArrayUtils.parseHex(initializationVectorGroup);
dataEncryptionKeyInfo = new DataEncryptionKeyInfo(algorithm, initializationVector);
}
}

View File

@@ -18,12 +18,10 @@ package net.schmizz.sshj.userauth.keyprovider;
import com.hierynomus.sshj.common.KeyAlgorithm;
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.*;
@@ -75,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;
@@ -101,12 +101,12 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
throw new IOException(String.format("Unsupported encryption: %s", encryption));
}
private Map<String, String> payload = new HashMap<String, String>();
private final Map<String, String> payload = new HashMap<>();
/**
* For each line that looks like "Xyz: vvv", it will be stored in this map.
*/
private final Map<String, String> headers = new HashMap<String, String>();
private final Map<String, String> headers = new HashMap<>();
protected KeyPair readKeyPair() throws IOException {
this.parseKeyPair();
@@ -261,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
* <p><p/>
* 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);
}
/**
@@ -380,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");

View File

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

View File

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

View File

@@ -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<String, String> headers;
V3PuTTYSecretKeyDerivationFunction(final Map<String, String> 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;
}
}

View File

@@ -71,7 +71,7 @@ public class ScpCommandLine {
}
}
private LinkedHashMap<Arg, String> arguments = new LinkedHashMap<Arg, String>();
private final LinkedHashMap<Arg, String> arguments = new LinkedHashMap<Arg, String>();
private String path;
ScpCommandLine() {

View File

@@ -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, "<html><head/><body><h1>Hi!</h1></body></html>");
}
@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");

View File

@@ -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, "<html><head/><body><h1>Hi!</h1></body></html>");
}
@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");

View File

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

View File

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