This is an automated email from the ASF dual-hosted git repository. lgoldstein pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit a1d767f3595f3974c37cc747825207cd47756a67 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Fri Feb 18 12:11:31 2022 +0200 [SSHD-1247] Added support for Argon2id encrypted PUTTY keys --- CHANGES.md | 1 + assembly/src/main/legal/notices.xml | 2 +- .../apache/sshd/putty/AbstractPuttyKeyDecoder.java | 38 ++++-- .../org/apache/sshd/putty/DSSPuttyKeyDecoder.java | 3 +- .../apache/sshd/putty/ECDSAPuttyKeyDecoder.java | 3 +- .../apache/sshd/putty/EdDSAPuttyKeyDecoder.java | 3 +- .../sshd/putty/PuttyKeyPairResourceParser.java | 127 ++++++++++++++++++--- .../org/apache/sshd/putty/RSAPuttyKeyDecoder.java | 3 +- .../sshd/putty/AbstractPuttyTestSupport.java | 100 ++++++++++++++++ .../org/apache/sshd/putty/PuttyKeyUtilsTest.java | 41 +------ .../apache/sshd/putty/PuttySpecialKeysTest.java | 59 ++++++++++ ...ialKeysTest-ssh-rsa-argon2id-KeyPair-123456.ppk | 31 +++++ 12 files changed, 342 insertions(+), 69 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fb283e5..857da14 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -70,5 +70,6 @@ Was originally in *HostConfigEntry*. * [SSHD-1244](https://issues.apache.org/jira/browse/SSHD-1244) Fixed channel window adjustment handling of large UINT32 values * [SSHD-1244](https://issues.apache.org/jira/browse/SSHD-1244) Re-defined channel identifiers as `long` rather than `int` to align with protocol UINT32 definition * [SSHD-1246](https://issues.apache.org/jira/browse/SSHD-1246) Added SshKeyDumpMain utility +* [SSHD-1247](https://issues.apache.org/jira/browse/SSHD-1247) Added support for Argon2id encrypted PUTTY keys diff --git a/assembly/src/main/legal/notices.xml b/assembly/src/main/legal/notices.xml index 6a60df2..f6c3fba 100644 --- a/assembly/src/main/legal/notices.xml +++ b/assembly/src/main/legal/notices.xml @@ -149,7 +149,7 @@ <licenses> <license> <name>The Apache Software License, Version 2.0</name> - <url>https://github.com/spring-projects/spring-integration/blob/master/src/dist/license.txt</url> + <url>https://github.com/spring-projects/spring-integration/blob/main/LICENSE.txt</url> </license> </licenses> </project> diff --git a/sshd-putty/src/main/java/org/apache/sshd/putty/AbstractPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/putty/AbstractPuttyKeyDecoder.java index 7331c0e..263a164 100644 --- a/sshd-putty/src/main/java/org/apache/sshd/putty/AbstractPuttyKeyDecoder.java +++ b/sshd-putty/src/main/java/org/apache/sshd/putty/AbstractPuttyKeyDecoder.java @@ -28,6 +28,7 @@ import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; import java.util.Arrays; import java.util.Base64; import java.util.Base64.Decoder; @@ -96,6 +97,7 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends List<String> prvLines = Collections.emptyList(); Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); String prvEncryption = null; + int formatVersion = -1; for (int index = 0, numLines = lines.size(); index < numLines; index++) { String l = lines.get(index); l = GenericUtils.trimToEmpty(l); @@ -107,6 +109,16 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends String hdrName = l.substring(0, pos).trim(); String hdrValue = l.substring(pos + 1).trim(); headers.put(hdrName, hdrValue); + if (hdrName.startsWith(KEY_FILE_HEADER_PREFIX)) { + String versionValue = hdrName.substring(KEY_FILE_HEADER_PREFIX.length()); + int fileVersion = Integer.parseInt(versionValue); + if ((formatVersion >= 0) && (fileVersion != formatVersion)) { + throw new InvalidKeySpecException( + "Inconsistent key file version specification: " + formatVersion + " and " + fileVersion); + } + formatVersion = fileVersion; + } + switch (hdrName) { case ENCRYPTION_HEADER: if (prvEncryption != null) { @@ -126,7 +138,7 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends } } - return loadKeyPairs(session, resourceKey, pubLines, prvLines, prvEncryption, passwordProvider, headers); + return loadKeyPairs(session, resourceKey, formatVersion, pubLines, prvLines, prvEncryption, passwordProvider, headers); } public static List<String> extractDataLines( @@ -154,17 +166,17 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends } public Collection<KeyPair> loadKeyPairs( - SessionContext session, NamedResource resourceKey, + SessionContext session, NamedResource resourceKey, int formatVersion, List<String> pubLines, List<String> prvLines, String prvEncryption, FilePasswordProvider passwordProvider, Map<String, String> headers) throws IOException, GeneralSecurityException { - return loadKeyPairs(session, resourceKey, + return loadKeyPairs(session, resourceKey, formatVersion, KeyPairResourceParser.joinDataLines(pubLines), KeyPairResourceParser.joinDataLines(prvLines), prvEncryption, passwordProvider, headers); } public Collection<KeyPair> loadKeyPairs( - SessionContext session, NamedResource resourceKey, + SessionContext session, NamedResource resourceKey, int formatVersion, String pubData, String prvData, String prvEncryption, FilePasswordProvider passwordProvider, Map<String, String> headers) throws IOException, GeneralSecurityException { @@ -176,7 +188,7 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends prvBytes = b64Decoder.decode(prvData); if (GenericUtils.isEmpty(prvEncryption) || NO_PRIVATE_KEY_ENCRYPTION_VALUE.equalsIgnoreCase(prvEncryption)) { - return loadKeyPairs(resourceKey, pubBytes, prvBytes, headers); + return loadKeyPairs(resourceKey, formatVersion, pubBytes, prvBytes, headers); } // format is "<cipher><bits>-<mode>" - e.g., "aes256-cbc" @@ -211,9 +223,9 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends } byte[] decBytes = PuttyKeyPairResourceParser.decodePrivateKeyBytes( - prvBytes, algName, numBits, mode, password); + formatVersion, prvBytes, algName, numBits, mode, password, headers); try { - keys = loadKeyPairs(resourceKey, pubBytes, decBytes, headers); + keys = loadKeyPairs(resourceKey, formatVersion, pubBytes, decBytes, headers); } finally { Arrays.fill(decBytes, (byte) 0); // eliminate sensitive data a.s.a.p. } @@ -250,28 +262,30 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends } public Collection<KeyPair> loadKeyPairs( - NamedResource resourceKey, byte[] pubData, byte[] prvData, Map<String, String> headers) + NamedResource resourceKey, int formatVersion, byte[] pubData, byte[] prvData, Map<String, String> headers) throws IOException, GeneralSecurityException { ValidateUtils.checkNotNullAndNotEmpty(pubData, "No public key data in %s", resourceKey); ValidateUtils.checkNotNullAndNotEmpty(prvData, "No private key data in %s", resourceKey); try (InputStream pubStream = new ByteArrayInputStream(pubData); InputStream prvStream = new ByteArrayInputStream(prvData)) { - return loadKeyPairs(resourceKey, pubStream, prvStream, headers); + return loadKeyPairs(resourceKey, formatVersion, pubStream, prvStream, headers); } } public Collection<KeyPair> loadKeyPairs( - NamedResource resourceKey, InputStream pubData, InputStream prvData, Map<String, String> headers) + NamedResource resourceKey, int formatVersion, + InputStream pubData, InputStream prvData, Map<String, String> headers) throws IOException, GeneralSecurityException { try (PuttyKeyReader pubReader = new PuttyKeyReader(ValidateUtils.checkNotNull(pubData, "No public key data in %s", resourceKey)); PuttyKeyReader prvReader = new PuttyKeyReader(ValidateUtils.checkNotNull(prvData, "No private key data in %s", resourceKey))) { - return loadKeyPairs(resourceKey, pubReader, prvReader, headers); + return loadKeyPairs(resourceKey, formatVersion, pubReader, prvReader, headers); } } public abstract Collection<KeyPair> loadKeyPairs( - NamedResource resourceKey, PuttyKeyReader pubReader, PuttyKeyReader prvReader, Map<String, String> headers) + NamedResource resourceKey, int formatVersion, PuttyKeyReader pubReader, PuttyKeyReader prvReader, + Map<String, String> headers) throws IOException, GeneralSecurityException; } diff --git a/sshd-putty/src/main/java/org/apache/sshd/putty/DSSPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/putty/DSSPuttyKeyDecoder.java index ebc342a..04426cd 100644 --- a/sshd-putty/src/main/java/org/apache/sshd/putty/DSSPuttyKeyDecoder.java +++ b/sshd-putty/src/main/java/org/apache/sshd/putty/DSSPuttyKeyDecoder.java @@ -51,7 +51,8 @@ public class DSSPuttyKeyDecoder extends AbstractPuttyKeyDecoder<DSAPublicKey, DS @Override public Collection<KeyPair> loadKeyPairs( - NamedResource resourceKey, PuttyKeyReader pubReader, PuttyKeyReader prvReader, Map<String, String> headers) + NamedResource resourceKey, int formatVersion, PuttyKeyReader pubReader, PuttyKeyReader prvReader, + Map<String, String> headers) throws IOException, GeneralSecurityException { pubReader.skip(); // skip version diff --git a/sshd-putty/src/main/java/org/apache/sshd/putty/ECDSAPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/putty/ECDSAPuttyKeyDecoder.java index ce7a298..48bc921 100644 --- a/sshd-putty/src/main/java/org/apache/sshd/putty/ECDSAPuttyKeyDecoder.java +++ b/sshd-putty/src/main/java/org/apache/sshd/putty/ECDSAPuttyKeyDecoder.java @@ -57,7 +57,8 @@ public class ECDSAPuttyKeyDecoder extends AbstractPuttyKeyDecoder<ECPublicKey, E @Override public Collection<KeyPair> loadKeyPairs( - NamedResource resourceKey, PuttyKeyReader pubReader, PuttyKeyReader prvReader, Map<String, String> headers) + NamedResource resourceKey, int formatVersion, PuttyKeyReader pubReader, PuttyKeyReader prvReader, + Map<String, String> headers) throws IOException, GeneralSecurityException { if (!SecurityUtils.isECCSupported()) { throw new NoSuchAlgorithmException("ECC not supported for " + resourceKey); diff --git a/sshd-putty/src/main/java/org/apache/sshd/putty/EdDSAPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/putty/EdDSAPuttyKeyDecoder.java index 1bef3a3..c427002 100644 --- a/sshd-putty/src/main/java/org/apache/sshd/putty/EdDSAPuttyKeyDecoder.java +++ b/sshd-putty/src/main/java/org/apache/sshd/putty/EdDSAPuttyKeyDecoder.java @@ -50,7 +50,8 @@ public class EdDSAPuttyKeyDecoder extends AbstractPuttyKeyDecoder<EdDSAPublicKey @Override public Collection<KeyPair> loadKeyPairs( - NamedResource resourceKey, PuttyKeyReader pubReader, PuttyKeyReader prvReader, Map<String, String> headers) + NamedResource resourceKey, int formatVersion, PuttyKeyReader pubReader, PuttyKeyReader prvReader, + Map<String, String> headers) throws IOException, GeneralSecurityException { if (!SecurityUtils.isEDDSACurveSupported()) { throw new NoSuchAlgorithmException(SecurityUtils.EDDSA + " provider not supported for " + resourceKey); diff --git a/sshd-putty/src/main/java/org/apache/sshd/putty/PuttyKeyPairResourceParser.java b/sshd-putty/src/main/java/org/apache/sshd/putty/PuttyKeyPairResourceParser.java index ae57afa..912695c 100644 --- a/sshd-putty/src/main/java/org/apache/sshd/putty/PuttyKeyPairResourceParser.java +++ b/sshd-putty/src/main/java/org/apache/sshd/putty/PuttyKeyPairResourceParser.java @@ -31,6 +31,7 @@ import java.security.spec.InvalidKeySpecException; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import javax.crypto.Cipher; @@ -42,8 +43,12 @@ import org.apache.sshd.common.config.keys.IdentityResourceLoader; import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser; import org.apache.sshd.common.digest.BuiltinDigests; import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.MapEntryUtils; import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; import org.apache.sshd.common.util.security.SecurityUtils; +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.bouncycastle.crypto.params.Argon2Parameters; //CHECKSTYLE:OFF /** @@ -95,7 +100,7 @@ import org.apache.sshd.common.util.security.SecurityUtils; //CHECKSTYLE:ON public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends PrivateKey> extends IdentityResourceLoader<PUB, PRV>, KeyPairResourceParser { - String KEY_FILE_HEADER_PREFIX = "PuTTY-User-Key-File"; + String KEY_FILE_HEADER_PREFIX = "PuTTY-User-Key-File-"; String PUBLIC_LINES_HEADER = "Public-Lines"; String PRIVATE_LINES_HEADER = "Private-Lines"; String PPK_FILE_SUFFIX = ".ppk"; @@ -111,6 +116,9 @@ public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends P */ String NO_PRIVATE_KEY_ENCRYPTION_VALUE = "none"; + /** PUTTY key v3 MAC key length */ + int FORMAT_3_MAC_KEY_LENGTH = 32; + @Override default boolean canExtractKeyPairs(NamedResource resourceKey, List<String> lines) throws IOException, GeneralSecurityException { @@ -131,7 +139,8 @@ public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends P } static byte[] decodePrivateKeyBytes( - byte[] prvBytes, String algName, int numBits, String algMode, String password) + int formatVersion, byte[] prvBytes, String algName, int numBits, String algMode, String password, + Map<String, String> headers) throws GeneralSecurityException { Objects.requireNonNull(prvBytes, "No encrypted key bytes"); ValidateUtils.checkNotNullAndNotEmpty(algName, "No encryption algorithm", GenericUtils.EMPTY_OBJECT_ARRAY); @@ -143,8 +152,13 @@ public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends P throw new NoSuchAlgorithmException("decodePrivateKeyBytes(" + algName + "-" + numBits + "-" + algMode + ") N/A"); } + if ((numBits != 128) && (numBits != 192) && (numBits != 256)) { + throw new InvalidKeySpecException("Requested key size (" + numBits + ") is not supported"); + } + byte[] initVector = new byte[16]; - byte[] keyValue = toEncryptionKey(password); + byte[] keyValue = new byte[numBits / Byte.SIZE]; + decodeEncryptionKey(formatVersion, password, initVector, keyValue, headers); try { return decodePrivateKeyBytes(prvBytes, algName, algMode, numBits, initVector, keyValue); } finally { @@ -175,28 +189,111 @@ public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends P } /** - * Converts a pass-phrase into a key, by following the convention that PuTTY uses. Used to decrypt the private key + * Converts a pass-phrase into a key, by following the conventions that PuTTY uses. Used to decrypt the private key * when it's encrypted. - * - * @param passphrase the Password to be used as seed for the key - ignored if {@code null}/empty - * @return The encryption key bytes - {@code null/empty} if no pass-phrase + * + * @param formatVersion The file format version + * @param passphrase The Password to be used as seed for the key - ignored if {@code null}/empty + * @param iv Initialization vector to be populated if necessary + * @param key Key to be populated + * @param headers Any extra headers found in the PPK file that might be used for KDF + * @throws GeneralSecurityException If cannot derive the key bytes from the password + */ + static void decodeEncryptionKey( + int formatVersion, String passphrase, byte[] iv, byte[] key, Map<String, String> headers) + throws GeneralSecurityException { + String keyDerivationType = getStringHeaderValue(headers, "Key-Derivation"); + if (GenericUtils.isBlank(keyDerivationType)) { + deriveFormat2EncryptionKey(passphrase, iv, key); + } else if ("Argon2id".equalsIgnoreCase(keyDerivationType) + || "Argon2i".equalsIgnoreCase(keyDerivationType) + || "Argon2d".equalsIgnoreCase(keyDerivationType)) { + deriveFormat3EncryptionKey(passphrase, keyDerivationType, iv, key, headers); + } else { + throw new NoSuchAlgorithmException("Unsupported KDF method: " + keyDerivationType); + } + } + + static void deriveFormat3EncryptionKey( + String passphrase, String keyDerivationType, byte[] iv, byte[] key, Map<String, String> headers) + throws GeneralSecurityException { + ValidateUtils.checkNotNullAndNotEmpty(headers, "Mising file headers for KDF purposes"); + Objects.requireNonNull(passphrase, "No passphrase provded"); + + int parallelism = getIntegerHeaderValue(headers, "Argon2-Parallelism"); + int iterations = getIntegerHeaderValue(headers, "Argon2-Passes"); + int memory = getIntegerHeaderValue(headers, "Argon2-Memory"); + byte[] salt = ValidateUtils.checkNotNullAndNotEmpty( + getHexArrayHeaderValue(headers, "Argon2-Salt"), "No Argon2 salt value provided"); + byte[] hashValue = new byte[key.length + iv.length + FORMAT_3_MAC_KEY_LENGTH]; + byte[] passBytes = passphrase.getBytes(StandardCharsets.UTF_8); + try { + Argon2Parameters.Builder builder; + if ("Argon2id".equalsIgnoreCase(keyDerivationType)) { + builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id); + } else if ("Argon2i".equalsIgnoreCase(keyDerivationType)) { + builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i); + } else if ("Argon2d".equalsIgnoreCase(keyDerivationType)) { + builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i); + } else { + throw new NoSuchAlgorithmException("Unsupported key derivation type: " + keyDerivationType); + } + Argon2Parameters params = builder + .withSalt(salt) + .withParallelism(parallelism) + .withMemoryAsKB(memory) + .withIterations(iterations) + .build(); + Argon2BytesGenerator generator = new Argon2BytesGenerator(); + generator.init(params); + generator.generateBytes(passBytes, hashValue); + } finally { + Arrays.fill(passBytes, (byte) 0); // eliminate sensitive data a.s.a.p. + } + + try { + System.arraycopy(hashValue, 0, key, 0, key.length); + System.arraycopy(hashValue, key.length, iv, 0, iv.length); + } finally { + Arrays.fill(hashValue, (byte) 0); // eliminate sensitive data a.s.a.p. + } + } + + static String getStringHeaderValue(Map<String, String> headers, String key) { + return MapEntryUtils.isEmpty(headers) ? null : headers.get(key); + } + + static byte[] getHexArrayHeaderValue(Map<String, String> headers, String key) { + String value = getStringHeaderValue(headers, key); + return BufferUtils.decodeHex(BufferUtils.EMPTY_HEX_SEPARATOR, value); + } + + static int getIntegerHeaderValue(Map<String, String> headers, String key) { + String value + = ValidateUtils.checkNotNullAndNotEmpty(getStringHeaderValue(headers, key), "Missing %s header value", key); + return Integer.parseInt(value); + } + + /** + * Uses the "legacy" KDF via SHA-1 + * + * @param passphrase The Password to be used as seed for the key - ignored if {@code null}/empty + * @param iv Initialization vector to be populated if necessary + * @param key Key to be populated * @throws GeneralSecurityException If cannot retrieve SHA-1 digest * @see <A HREF= * "http://security.stackexchange.com/questions/71341/how-does-putty-derive-the-encryption-key-in-its-ppk-format"> * How does Putty derive the encryption key in its .ppk format ?</A> */ - static byte[] toEncryptionKey(String passphrase) throws GeneralSecurityException { - if (GenericUtils.isEmpty(passphrase)) { - return GenericUtils.EMPTY_BYTE_ARRAY; - } + static void deriveFormat2EncryptionKey(String passphrase, byte[] iv, byte[] key) throws GeneralSecurityException { + Objects.requireNonNull(passphrase, "No passphrase provded"); byte[] passBytes = passphrase.getBytes(StandardCharsets.UTF_8); try { MessageDigest hash = SecurityUtils.getMessageDigest(BuiltinDigests.sha1.getAlgorithm()); byte[] stateValue = { 0, 0, 0, 0 }; - byte[] keyValue = new byte[32]; try { - for (int i = 0, remLen = keyValue.length; i < 2; i++) { + for (int i = 0, remLen = key.length; remLen > 0; i++) { hash.reset(); // just making sure stateValue[3] = (byte) i; @@ -205,7 +302,7 @@ public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends P byte[] digest = hash.digest(); try { - System.arraycopy(digest, 0, keyValue, i * 20, Math.min(20, remLen)); + System.arraycopy(digest, 0, key, i * 20, Math.min(20, remLen)); } finally { Arrays.fill(digest, (byte) 0); // eliminate sensitive data a.s.a.p. } @@ -215,7 +312,7 @@ public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends P Arrays.fill(stateValue, (byte) 0); // eliminate sensitive data a.s.a.p. } - return keyValue; + Arrays.fill(iv, (byte) 0); } finally { Arrays.fill(passBytes, (byte) 0); // eliminate sensitive data a.s.a.p. } diff --git a/sshd-putty/src/main/java/org/apache/sshd/putty/RSAPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/putty/RSAPuttyKeyDecoder.java index 998b9bd..6950ef2 100644 --- a/sshd-putty/src/main/java/org/apache/sshd/putty/RSAPuttyKeyDecoder.java +++ b/sshd-putty/src/main/java/org/apache/sshd/putty/RSAPuttyKeyDecoder.java @@ -52,7 +52,8 @@ public class RSAPuttyKeyDecoder extends AbstractPuttyKeyDecoder<RSAPublicKey, RS @Override public Collection<KeyPair> loadKeyPairs( - NamedResource resourceKey, PuttyKeyReader pubReader, PuttyKeyReader prvReader, Map<String, String> headers) + NamedResource resourceKey, int formatVersion, PuttyKeyReader pubReader, PuttyKeyReader prvReader, + Map<String, String> headers) throws IOException, GeneralSecurityException { pubReader.skip(); // skip version diff --git a/sshd-putty/src/test/java/org/apache/sshd/putty/AbstractPuttyTestSupport.java b/sshd-putty/src/test/java/org/apache/sshd/putty/AbstractPuttyTestSupport.java new file mode 100644 index 0000000..ed61230 --- /dev/null +++ b/sshd-putty/src/test/java/org/apache/sshd/putty/AbstractPuttyTestSupport.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.putty; + +import java.io.IOException; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Collection; + +import org.apache.sshd.common.cipher.BuiltinCiphers; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder; +import org.apache.sshd.common.config.keys.loader.openssh.OpenSSHKeyPairResourceParser; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.util.test.JUnitTestSupport; +import org.junit.Assume; +import org.junit.AssumptionViolatedException; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public abstract class AbstractPuttyTestSupport extends JUnitTestSupport { + protected AbstractPuttyTestSupport() { + super(); + } + + protected KeyPair testDecodeEncryptedPuttyKeyFile( + String encryptedFile, boolean okIfMissing, String password, String keyType) + throws IOException, GeneralSecurityException { + PuttyKeyPairResourceParser<?, ?> parser = PuttyKeyUtils.BY_KEY_TYPE.get(keyType); + assertNotNull("No parser found for key type=" + keyType, parser); + return testDecodeEncryptedPuttyKeyFile(encryptedFile, okIfMissing, password, parser, keyType); + } + + protected KeyPair testDecodeEncryptedPuttyKeyFile( + String encryptedFile, boolean okIfMissing, String password, PuttyKeyPairResourceParser<?, ?> parser, String keyType) + throws IOException, GeneralSecurityException { + Assume.assumeTrue(BuiltinCiphers.aes256cbc.getTransformation() + " N/A", BuiltinCiphers.aes256cbc.isSupported()); + + URL url = getClass().getResource(encryptedFile); + if (url == null) { + if (okIfMissing) { + throw new AssumptionViolatedException("Skip non-existent encrypted file: " + encryptedFile); + } + + fail("Missing test resource: " + encryptedFile); + } + + Collection<KeyPair> keys = parser.loadKeyPairs(null, url, (s, r, index) -> password); + assertEquals("Mismatched loaded keys count from " + encryptedFile, 1, GenericUtils.size(keys)); + + return assertLoadedKeyPair(encryptedFile, GenericUtils.head(keys), keyType); + } + + //////////////////////////////////////////////////////////////////////////////////// + + public static KeyPair assertLoadedKeyPair(String prefix, KeyPair kp, String keyType) throws GeneralSecurityException { + assertNotNull(prefix + ": no key pair loaded", kp); + + PublicKey pubKey = kp.getPublic(); + assertNotNull(prefix + ": no public key loaded", pubKey); + assertEquals(prefix + ": mismatched public key type", keyType, KeyUtils.getKeyType(pubKey)); + + PrivateKey prvKey = kp.getPrivate(); + assertNotNull(prefix + ": no private key loaded", prvKey); + assertEquals(prefix + ": mismatched private key type", keyType, KeyUtils.getKeyType(prvKey)); + + @SuppressWarnings("rawtypes") + PrivateKeyEntryDecoder decoder = OpenSSHKeyPairResourceParser.getPrivateKeyEntryDecoder(prvKey); + assertNotNull("No private key decoder", decoder); + + if (decoder.isPublicKeyRecoverySupported()) { + @SuppressWarnings("unchecked") + PublicKey recKey = decoder.recoverPublicKey(prvKey); + assertKeyEquals("Mismatched recovered public key", pubKey, recKey); + } + + return kp; + } +} diff --git a/sshd-putty/src/test/java/org/apache/sshd/putty/PuttyKeyUtilsTest.java b/sshd-putty/src/test/java/org/apache/sshd/putty/PuttyKeyUtilsTest.java index 25dd011..24803c9 100644 --- a/sshd-putty/src/test/java/org/apache/sshd/putty/PuttyKeyUtilsTest.java +++ b/sshd-putty/src/test/java/org/apache/sshd/putty/PuttyKeyUtilsTest.java @@ -23,8 +23,6 @@ import java.io.IOException; import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; import java.util.Collection; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -33,15 +31,11 @@ import org.apache.sshd.common.NamedResource; import org.apache.sshd.common.cipher.BuiltinCiphers; import org.apache.sshd.common.config.keys.FilePasswordProvider; import org.apache.sshd.common.config.keys.FilePasswordProvider.ResourceDecodeResult; -import org.apache.sshd.common.config.keys.KeyUtils; -import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder; -import org.apache.sshd.common.config.keys.loader.openssh.OpenSSHKeyPairResourceParser; import org.apache.sshd.common.session.SessionContext; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.functors.UnaryEquator; import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory; -import org.apache.sshd.util.test.JUnitTestSupport; import org.apache.sshd.util.test.NoIoTestCase; import org.junit.Assume; import org.junit.FixMethodOrder; @@ -61,7 +55,7 @@ import org.mockito.Mockito; @RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class) @Category({ NoIoTestCase.class }) -public class PuttyKeyUtilsTest extends JUnitTestSupport { +public class PuttyKeyUtilsTest extends AbstractPuttyTestSupport { public static final String PASSWORD = "super secret passphrase"; private final String keyType; @@ -125,16 +119,7 @@ public class PuttyKeyUtilsTest extends JUnitTestSupport { @Test public void testDecodeEncryptedPuttyKeyFile() throws IOException, GeneralSecurityException { - Assume.assumeTrue(BuiltinCiphers.aes256cbc.getTransformation() + " N/A", BuiltinCiphers.aes256cbc.isSupported()); - - URL url = getClass().getResource(encryptedFile); - Assume.assumeTrue("Skip non-existent encrypted file: " + encryptedFile, url != null); - assertNotNull("Missing test resource: " + encryptedFile, url); - - Collection<KeyPair> keys = parser.loadKeyPairs(null, url, (s, r, index) -> PASSWORD); - assertEquals("Mismatched loaded keys count from " + encryptedFile, 1, GenericUtils.size(keys)); - - assertLoadedKeyPair(encryptedFile, GenericUtils.head(keys)); + testDecodeEncryptedPuttyKeyFile(encryptedFile, true, PASSWORD, parser, keyType); } @Test @@ -219,25 +204,7 @@ public class PuttyKeyUtilsTest extends JUnitTestSupport { } } - private void assertLoadedKeyPair(String prefix, KeyPair kp) throws GeneralSecurityException { - assertNotNull(prefix + ": no key pair loaded", kp); - - PublicKey pubKey = kp.getPublic(); - assertNotNull(prefix + ": no public key loaded", pubKey); - assertEquals(prefix + ": mismatched public key type", keyType, KeyUtils.getKeyType(pubKey)); - - PrivateKey prvKey = kp.getPrivate(); - assertNotNull(prefix + ": no private key loaded", prvKey); - assertEquals(prefix + ": mismatched private key type", keyType, KeyUtils.getKeyType(prvKey)); - - @SuppressWarnings("rawtypes") - PrivateKeyEntryDecoder decoder = OpenSSHKeyPairResourceParser.getPrivateKeyEntryDecoder(prvKey); - assertNotNull("No private key decoder", decoder); - - if (decoder.isPublicKeyRecoverySupported()) { - @SuppressWarnings("unchecked") - PublicKey recKey = decoder.recoverPublicKey(prvKey); - assertKeyEquals("Mismatched recovered public key", pubKey, recKey); - } + private KeyPair assertLoadedKeyPair(String prefix, KeyPair kp) throws GeneralSecurityException { + return assertLoadedKeyPair(prefix, kp, keyType); } } diff --git a/sshd-putty/src/test/java/org/apache/sshd/putty/PuttySpecialKeysTest.java b/sshd-putty/src/test/java/org/apache/sshd/putty/PuttySpecialKeysTest.java new file mode 100644 index 0000000..35f8455 --- /dev/null +++ b/sshd-putty/src/test/java/org/apache/sshd/putty/PuttySpecialKeysTest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.putty; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; + +import org.apache.sshd.common.util.security.SecurityUtils; +import org.apache.sshd.util.test.NoIoTestCase; +import org.junit.Assume; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Category({ NoIoTestCase.class }) +public class PuttySpecialKeysTest extends AbstractPuttyTestSupport { + public PuttySpecialKeysTest() { + super(); + } + + @Test // SSHD-1247 + public void testArgon2KeyDerivation() throws Exception { + Assume.assumeTrue("BC provider available", SecurityUtils.isBouncyCastleRegistered()); + testDecodeSpecialEncryptedPuttyKeyFile("ssh-rsa", "argon2id", "123456"); + } + + protected KeyPair testDecodeSpecialEncryptedPuttyKeyFile( + String keyType, String flavor, String password) + throws IOException, GeneralSecurityException { + return testDecodeEncryptedPuttyKeyFile( + getClass().getSimpleName() + "-" + keyType + + "-" + flavor + "-" + KeyPair.class.getSimpleName() + + "-" + password + PuttyKeyPairResourceParser.PPK_FILE_SUFFIX, + false, password, keyType); + } +} diff --git a/sshd-putty/src/test/resources/org/apache/sshd/putty/PuttySpecialKeysTest-ssh-rsa-argon2id-KeyPair-123456.ppk b/sshd-putty/src/test/resources/org/apache/sshd/putty/PuttySpecialKeysTest-ssh-rsa-argon2id-KeyPair-123456.ppk new file mode 100644 index 0000000..7758fdc --- /dev/null +++ b/sshd-putty/src/test/resources/org/apache/sshd/putty/PuttySpecialKeysTest-ssh-rsa-argon2id-KeyPair-123456.ppk @@ -0,0 +1,31 @@ +PuTTY-User-Key-File-3: ssh-rsa +Encryption: aes256-cbc +Comment: rsa-key-20220217 +Public-Lines: 6 +AAAAB3NzaC1yc2EAAAADAQABAAABAQDJqngLGC4VEPxQECFimieG28bimImxJG4N +w6lZ8FHjQELXrd/yByHrVLBUecOSMtx8meozoWwyOaKwf3MgEm0bU8VHLG45kTfP +X5bbAu/V+5JAC7IDsxPB8ULPk2A7K0+whSF3v08Qrsaamw9ZH/IP1prBDyLRBaYD +539e6NkvHhmIPmpfzf7ahmRMC2+9i7f0RxG2tGtFRA5tIMLROfczU+TQImNLWZLF +dHII80TyhRXXruX1DuZGBR8s2hiazH4gY+cL/YUBvmZMLF2VYlVq/0peWqgcdEGQ +MPwlPmbpqYnN1DevLQiVU8k/ztrp3mBFigdX+P6IgYgfGkeZvvp1 +Key-Derivation: Argon2id +Argon2-Memory: 8192 +Argon2-Passes: 5 +Argon2-Parallelism: 1 +Argon2-Salt: 6fdbdf2093b7697f46686e31506c1564 +Private-Lines: 14 +acARqLeh0XtN0WAdOlN4G3gSDI7y8w1WvxI2ykZnh1y9J86iYo+uDpsIgdGkxNK7 +swCoogl1kqYIOFe+yOsmAeynIBwyPd42KoE4er1EqKMfyiVa5Gy9wEzTawYCnK7E +aLmjUiJHU6kkRraD0QUuo1enfbu/xGqicRSVcVLYPR9jgQ5FN3+1lDHCo7bpj5up +j/ErTcFmIEmpWLBbi8gF/qU3WLi3YgiFTT6tBY3f9PlehJzcMv0DqsRqE9/F9+Oc +ZYq+/iTb95UkwfYvmKkWLqJY5Wf+it+WkGOjj5d1ti2LrBZlSd7VVaToGU5VXUzR +TG3ZzRr7yAM6PkkINzyYcGnZNz6NnYfqq4y9QD1cAzJa+00+ngavgwybKqWwV8Rm +RGn0RSBXsIDAsSwuj0rEax7VYKAfKuGJHdmK9FdJ/oEEPxQ+DmRCG368GhhWlLAY +joUl6H45qhHUNF6+a0kPK9geZZR1pbhEqeKeSTHZUbwAxkTIZPX61F9GCYNkvN5o +tVdIXXF2SgiNUdtYuKmGdNx6rOmCs2dWeH5kboQsh52gtzFpSgIEiFq/n32cG5od +mYufM7WsWlkasIXlQV8lpd2elp5ob9gOJP5P3CGzyvtLb2myEy8HPcRBHXr7qtyT +MEPTcjD5axegXLCCIk7FDTVKtl2oxTH9htGQYEaGG+3Yh7qh8lHT/aLrZkGW2tLJ +AvMHrDlWyUaBdo13yJL6ywx35wBcMDPrjU/GeHT05sShEjy7CRv3vmyEYHqJNYCC +hjFo1N2wVe18Z4pnbjDKDviZiCy1WfDkl/KidDMCD5HGo3mRZC4l3/LajrtWygmc +0rgdX1UueHVlKcakgcJX/dLoQZvFHAc6h8GhmEYMJFmJAzITholR0aMkYaToFJPe +Private-MAC: 7dcaa183900b3b60c1b90c689b2f4f283334c30d97abea1155320a4247ff42c8