Compare commits

...

2 Commits

Author SHA1 Message Date
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
20 changed files with 278 additions and 127 deletions

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

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

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

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