diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/EncryptedPEMKeyReader.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/EncryptedPEMKeyReader.java index 20333e10..9a2a9bba 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/EncryptedPEMKeyReader.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/EncryptedPEMKeyReader.java @@ -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); } } 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 671e09fb..1fce269d 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java @@ -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 final 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(); @@ -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 - *

- * 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"); 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/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/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();