| /* |
| * 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.util.Collection; |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| 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.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.junit.jupiter.api.Assumptions; |
| import org.junit.jupiter.api.MethodOrderer.MethodName; |
| import org.junit.jupiter.api.Tag; |
| import org.junit.jupiter.api.TestMethodOrder; |
| import org.junit.jupiter.params.ParameterizedTest; |
| import org.junit.jupiter.params.provider.MethodSource; |
| import org.mockito.Mockito; |
| |
| import static org.junit.jupiter.api.Assertions.assertEquals; |
| import static org.junit.jupiter.api.Assertions.assertFalse; |
| import static org.junit.jupiter.api.Assertions.assertNotNull; |
| import static org.junit.jupiter.api.Assertions.assertSame; |
| import static org.junit.jupiter.api.Assertions.assertTrue; |
| |
| /** |
| * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> |
| */ |
| @TestMethodOrder(MethodName.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests |
| @Tag("NoIoTestCase") |
| public class PuttyKeyUtilsTest extends AbstractPuttyTestSupport { |
| public static final String PASSWORD = "super secret passphrase"; |
| |
| private String keyType; |
| private String regularFile; |
| private String encryptedFile; |
| private PuttyKeyPairResourceParser<?, ?> parser; |
| |
| public void initPuttyKeyUtilsTest(String keyType) { |
| this.keyType = keyType; |
| this.parser = PuttyKeyUtils.BY_KEY_TYPE.get(keyType); |
| this.regularFile = getClass().getSimpleName() |
| + "-" + keyType + "-" + KeyPair.class.getSimpleName() |
| + PuttyKeyPairResourceParser.PPK_FILE_SUFFIX; |
| this.encryptedFile = PASSWORD.replace(' ', '-') + "-AES-256-CBC" |
| + "-" + keyType + "-" + KeyPair.class.getSimpleName() |
| + PuttyKeyPairResourceParser.PPK_FILE_SUFFIX; |
| } |
| |
| public static List<Object[]> parameters() { |
| return parameterize(PuttyKeyUtils.BY_KEY_TYPE.keySet()); |
| } |
| |
| @MethodSource("parameters") |
| @ParameterizedTest(name = "{0}") |
| public void canDecodePuttyKeyFile(String keyType) throws IOException, GeneralSecurityException { |
| initPuttyKeyUtilsTest(keyType); |
| for (String resource : new String[] { regularFile, encryptedFile }) { |
| URL url = getClass().getResource(resource); |
| if (UnaryEquator.isSameReference(regularFile, resource)) { |
| assertNotNull(url, "Missing test resource: " + resource); |
| } else { |
| if (url == null) { |
| outputDebugMessage("Skip non-existing encrypted file: %s", resource); |
| continue; |
| } |
| } |
| |
| List<String> lines = IoUtils.readAllLines(url); |
| NamedResource resourceKey = NamedResource.ofName(resource); |
| assertTrue(parser.canExtractKeyPairs(resourceKey, lines), resource + " - can extract key pair"); |
| |
| for (PuttyKeyPairResourceParser<?, ?> other : PuttyKeyUtils.BY_KEY_TYPE.values()) { |
| if (parser == other) { |
| continue; |
| } |
| |
| assertFalse(other.canExtractKeyPairs(resourceKey, lines), |
| other.getClass().getSimpleName() + "/" + resource + " - unexpected extraction capability"); |
| } |
| } |
| } |
| |
| @MethodSource("parameters") |
| @ParameterizedTest(name = "{0}") |
| public void decodePuttyKeyFile(String keyType) throws IOException, GeneralSecurityException { |
| initPuttyKeyUtilsTest(keyType); |
| URL url = getClass().getResource(regularFile); |
| assertNotNull(url, "Missing test resource: " + regularFile); |
| |
| Collection<KeyPair> keys = parser.loadKeyPairs(null, url, null); |
| assertEquals(1, GenericUtils.size(keys), "Mismatched loaded keys count from " + regularFile); |
| assertLoadedKeyPair(regularFile, GenericUtils.head(keys)); |
| } |
| |
| @MethodSource("parameters") |
| @ParameterizedTest(name = "{0}") |
| public void decodeEncryptedPuttyKeyFile(String keyType) throws IOException, GeneralSecurityException { |
| initPuttyKeyUtilsTest(keyType); |
| testDecodeEncryptedPuttyKeyFile(encryptedFile, true, PASSWORD, parser, keyType); |
| } |
| |
| @MethodSource("parameters") |
| @ParameterizedTest(name = "{0}") |
| public void decideEncryptedFileWithRetries(String keyType) throws IOException, GeneralSecurityException { |
| initPuttyKeyUtilsTest(keyType); |
| Assumptions.assumeTrue(BuiltinCiphers.aes256cbc.isSupported(), BuiltinCiphers.aes256cbc.getTransformation() + " N/A"); |
| |
| URL url = getClass().getResource(encryptedFile); |
| Assumptions.assumeTrue(url != null, "Skip non-existent encrypted file: " + encryptedFile); |
| assertNotNull(url, "Missing test resource: " + encryptedFile); |
| |
| int maxRetries = 3; |
| SessionContext mockSession = Mockito.mock(SessionContext.class); |
| for (ResourceDecodeResult result : ResourceDecodeResult.values()) { |
| AtomicInteger retriesCount = new AtomicInteger(0); |
| FilePasswordProvider provider = new FilePasswordProvider() { |
| @Override |
| public String getPassword(SessionContext session, NamedResource resourceKey, int retryIndex) |
| throws IOException { |
| assertSame(mockSession, session, "Mismatched session context"); |
| |
| switch (result) { |
| case IGNORE: |
| case TERMINATE: |
| assertEquals(0, retryIndex, "Mismatched retries invocation count"); |
| assertEquals(retryIndex, retriesCount.get(), "Mismatched retries tracking count"); |
| return "qwertyuiop123456!@#$%^"; |
| case RETRY: { |
| int count = retriesCount.incrementAndGet(); |
| assertEquals(retryIndex + 1, count, "Mismatched retries count"); |
| if (count == maxRetries) { |
| return PASSWORD; |
| } else { |
| return "retry #" + count; |
| } |
| } |
| default: |
| throw new UnsupportedOperationException("Unknown decode result type: " + result); |
| } |
| } |
| |
| @Override |
| public ResourceDecodeResult handleDecodeAttemptResult( |
| SessionContext session, NamedResource resourceKey, int retryIndex, String password, Exception err) |
| throws IOException, GeneralSecurityException { |
| assertSame(mockSession, session, "Mismatched session context"); |
| if (err == null) { |
| return null; |
| } |
| |
| if (result == ResourceDecodeResult.RETRY) { |
| if (retriesCount.get() >= maxRetries) { |
| return ResourceDecodeResult.TERMINATE; |
| } |
| } |
| |
| return result; |
| } |
| |
| @Override |
| public String toString() { |
| return FilePasswordProvider.class.getSimpleName() + "[" + result + "]"; |
| } |
| }; |
| |
| try { |
| Collection<KeyPair> keys = parser.loadKeyPairs(mockSession, url, provider); |
| if (result == ResourceDecodeResult.IGNORE) { |
| assertEquals(0, GenericUtils.size(keys), "Unexpected loaded keys count from " + encryptedFile); |
| assertEquals(0, retriesCount.get(), "Mismatched " + result + " retries count"); |
| } else { |
| assertEquals(1, GenericUtils.size(keys), "Mismatched loaded keys count from " + encryptedFile); |
| assertEquals(maxRetries, retriesCount.get(), "Mismatched " + result + " retries count"); |
| assertLoadedKeyPair(encryptedFile, GenericUtils.head(keys)); |
| } |
| } catch (IOException | GeneralSecurityException | RuntimeException e) { |
| if (result != ResourceDecodeResult.TERMINATE) { |
| throw e; |
| } |
| |
| assertEquals(0, retriesCount.get(), "Mismatched " + result + " retries count"); |
| } |
| } |
| } |
| |
| private KeyPair assertLoadedKeyPair(String prefix, KeyPair kp) throws GeneralSecurityException { |
| return assertLoadedKeyPair(prefix, kp, keyType); |
| } |
| } |