You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by tw...@apache.org on 2023/04/01 16:30:07 UTC

[mina-sshd] branch master updated: Utility method FilePasswordProvider.decode()

This is an automated email from the ASF dual-hosted git repository.

twolf pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git


The following commit(s) were added to refs/heads/master by this push:
     new 5c8e5eabe Utility method FilePasswordProvider.decode()
5c8e5eabe is described below

commit 5c8e5eabe3b25a0bff13d7f272eeeb110c583c64
Author: Thomas Wolf <tw...@apache.org>
AuthorDate: Sat Apr 1 18:03:24 2023 +0200

    Utility method FilePasswordProvider.decode()
    
    Encapsulate the basic decode-with-retries loop in a method. This
    simplifies code using a FilePasswordProvider, which then only has to
    deal with actual decoding given a password, and doesn't have to care
    about the retry logic.
---
 .../common/config/keys/FilePasswordProvider.java   |  58 +++++++++++
 .../openssh/OpenSSHKeyPairResourceParser.java      |  48 ++-------
 .../pem/AbstractPEMResourceKeyPairParser.java      |  65 +++---------
 .../loader/pem/PKCS8PEMResourceKeyPairParser.java  |  56 +++--------
 .../BouncyCastleKeyPairResourceParser.java         |  44 ++------
 .../apache/sshd/putty/AbstractPuttyKeyDecoder.java | 112 +++++++--------------
 6 files changed, 140 insertions(+), 243 deletions(-)

diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
index cab86c72a..3717899a2 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
@@ -20,13 +20,17 @@
 package org.apache.sshd.common.config.keys;
 
 import java.io.IOException;
+import java.net.ProtocolException;
 import java.security.GeneralSecurityException;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.Set;
 
+import javax.security.auth.login.FailedLoginException;
+
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.util.GenericUtils;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
@@ -87,6 +91,60 @@ public interface FilePasswordProvider {
         return ResourceDecodeResult.TERMINATE;
     }
 
+    /**
+     * Something that can produce a result given a password.
+     *
+     * @param <T> type of the result
+     */
+    interface Decoder<T> {
+        T decode(String password) throws IOException, GeneralSecurityException;
+    }
+
+    /**
+     * Obtains the password through {@link #getPassword(SessionContext, NamedResource, int)}, invokes the
+     * {@link Decoder} and then
+     * {@link #handleDecodeAttemptResult(SessionContext, NamedResource, int, String, Exception)} and then returns the
+     * decoded result. If the decoder fails and the {@link ResourceDecodeResult} is {@link ResourceDecodeResult#RETRY},
+     * the whole process is re-tried.
+     *
+     * @param  <T>                      Result type of the decoder
+     * @param  session                  {@link SessionContext}, may be {@code null}
+     * @param  resourceKey              {@link NamedResource} used for error reporting
+     * @param  decoder                  {@link Decoder} to produce the result given a password
+     * @return                          the decoder's result, or {@code null} if none
+     * @throws IOException              if an I/O problem occurs
+     * @throws GeneralSecurityException if the decoder throws it
+     */
+    default <T> T decode(SessionContext session, NamedResource resourceKey, Decoder<? extends T> decoder)
+            throws IOException, GeneralSecurityException {
+        for (int retryCount = 0;; retryCount++) {
+            String pwd = getPassword(session, resourceKey, retryCount);
+            if (GenericUtils.isEmpty(pwd)) {
+                throw new FailedLoginException("No password data for encrypted resource=" + resourceKey);
+            }
+            try {
+                T result = decoder.decode(pwd);
+                handleDecodeAttemptResult(session, resourceKey, retryCount, pwd, null);
+                return result;
+            } catch (IOException | GeneralSecurityException | RuntimeException e) {
+                ResourceDecodeResult result = handleDecodeAttemptResult(session, resourceKey, retryCount, pwd, e);
+                if (result == null) {
+                    throw e;
+                }
+                switch (result) {
+                    case TERMINATE:
+                        throw e;
+                    case RETRY:
+                        continue;
+                    case IGNORE:
+                        return null;
+                    default:
+                        throw new ProtocolException("Unsupported decode attempt result (" + result + ") for " + resourceKey);
+                }
+            }
+        }
+    }
+
     static FilePasswordProvider of(String password) {
         return (session, resource, index) -> password;
     }
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java
index 9e81001c8..09e8d61bc 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java
@@ -23,7 +23,6 @@ import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.StreamCorruptedException;
-import java.net.ProtocolException;
 import java.nio.charset.StandardCharsets;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
@@ -49,7 +48,6 @@ import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.cipher.BuiltinCiphers;
 import org.apache.sshd.common.cipher.CipherFactory;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
-import org.apache.sshd.common.config.keys.FilePasswordProvider.ResourceDecodeResult;
 import org.apache.sshd.common.config.keys.KeyEntryResolver;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder;
@@ -171,45 +169,15 @@ public class OpenSSHKeyPairResourceParser extends AbstractKeyPairResourceParser
         } else {
             encryptedData = KeyEntryResolver.readRLEBytes(stream, MAX_PRIVATE_KEY_DATA_SIZE);
         }
-        for (int retryCount = 0;; retryCount++) {
-            String pwd = passwordProvider.getPassword(session, resourceKey, retryCount);
-            if (GenericUtils.isEmpty(pwd)) {
-                return Collections.emptyList();
-            }
-
-            List<KeyPair> keys;
-            try {
-                byte[] decryptedData = kdfOptions.decodePrivateKeyBytes(session, resourceKey, cipherSpec,
-                        encryptedData, pwd);
-                try (InputStream bais = new ByteArrayInputStream(decryptedData)) {
-                    keys = readPrivateKeys(session, resourceKey, context, publicKeys, passwordProvider, bais);
-                } finally {
-                    Arrays.fill(decryptedData, (byte) 0); // get rid of sensitive data a.s.a.p.
-                }
-            } catch (IOException | GeneralSecurityException | RuntimeException e) {
-                ResourceDecodeResult result = passwordProvider.handleDecodeAttemptResult(session, resourceKey, retryCount, pwd,
-                        e);
-                pwd = null; // get rid of sensitive data a.s.a.p.
-                if (result == null) {
-                    result = ResourceDecodeResult.TERMINATE;
-                }
-
-                switch (result) {
-                    case TERMINATE:
-                        throw e;
-                    case RETRY:
-                        continue;
-                    case IGNORE:
-                        return Collections.emptyList();
-                    default:
-                        throw new ProtocolException("Unsupported decode attempt result (" + result + ") for " + resourceKey);
-                }
+        Collection<KeyPair> keys = passwordProvider.decode(session, resourceKey, pwd -> {
+            byte[] decryptedData = kdfOptions.decodePrivateKeyBytes(session, resourceKey, cipherSpec, encryptedData, pwd);
+            try (InputStream bais = new ByteArrayInputStream(decryptedData)) {
+                return readPrivateKeys(session, resourceKey, context, publicKeys, passwordProvider, bais);
+            } finally {
+                Arrays.fill(decryptedData, (byte) 0);
             }
-
-            passwordProvider.handleDecodeAttemptResult(session, resourceKey, retryCount, pwd, null);
-            pwd = null; // get rid of sensitive data a.s.a.p.
-            return keys;
-        }
+        });
+        return keys == null ? Collections.emptyList() : keys;
     }
 
     protected OpenSSHKdfOptions resolveKdfOptions(
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java
index 6588e85ed..56ad7e3c7 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java
@@ -22,7 +22,6 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.StreamCorruptedException;
-import java.net.ProtocolException;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.NoSuchAlgorithmException;
@@ -34,11 +33,9 @@ import java.util.Map;
 import java.util.TreeMap;
 
 import javax.security.auth.login.CredentialException;
-import javax.security.auth.login.FailedLoginException;
 
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
-import org.apache.sshd.common.config.keys.FilePasswordProvider.ResourceDecodeResult;
 import org.apache.sshd.common.config.keys.loader.AbstractKeyPairResourceParser;
 import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
 import org.apache.sshd.common.config.keys.loader.PrivateKeyEncryptionContext;
@@ -150,56 +147,24 @@ public abstract class AbstractPEMResourceKeyPairParser
                 throw new CredentialException("Missing password provider for encrypted resource=" + resourceKey);
             }
 
-            for (int retryIndex = 0;; retryIndex++) {
-                String password = passwordProvider.getPassword(session, resourceKey, retryIndex);
-                Collection<KeyPair> keys;
+            byte[] encryptedData = KeyPairResourceParser.extractDataBytes(dataLines);
+            String algorithm = algInfo;
+            byte[] iv = initVector;
+            Collection<KeyPair> keys = passwordProvider.decode(session, resourceKey, password -> {
+                PrivateKeyEncryptionContext encContext = new PrivateKeyEncryptionContext(algorithm);
+                encContext.setPassword(password);
+                encContext.setInitVector(iv);
+                byte[] decodedData = GenericUtils.EMPTY_BYTE_ARRAY;
                 try {
-                    if (GenericUtils.isEmpty(password)) {
-                        throw new FailedLoginException("No password data for encrypted resource=" + resourceKey);
-                    }
-
-                    PrivateKeyEncryptionContext encContext = new PrivateKeyEncryptionContext(algInfo);
-                    encContext.setPassword(password);
-                    encContext.setInitVector(initVector);
-
-                    byte[] encryptedData = GenericUtils.EMPTY_BYTE_ARRAY;
-                    byte[] decodedData = GenericUtils.EMPTY_BYTE_ARRAY;
-                    try {
-                        encryptedData = KeyPairResourceParser.extractDataBytes(dataLines);
-                        decodedData = applyPrivateKeyCipher(encryptedData, encContext, false);
-                        try (InputStream bais = new ByteArrayInputStream(decodedData)) {
-                            keys = extractKeyPairs(session, resourceKey, beginMarker, endMarker, passwordProvider, bais,
-                                    headers);
-                        }
-                    } finally {
-                        Arrays.fill(encryptedData, (byte) 0); // get rid of sensitive data a.s.a.p.
-                        Arrays.fill(decodedData, (byte) 0); // get rid of sensitive data a.s.a.p.
-                    }
-                } catch (IOException | GeneralSecurityException | RuntimeException e) {
-                    ResourceDecodeResult result
-                            = passwordProvider.handleDecodeAttemptResult(session, resourceKey, retryIndex, password, e);
-                    password = null; // get rid of sensitive data a.s.a.p.
-                    if (result == null) {
-                        result = ResourceDecodeResult.TERMINATE;
-                    }
-
-                    switch (result) {
-                        case TERMINATE:
-                            throw e;
-                        case RETRY:
-                            continue;
-                        case IGNORE:
-                            return Collections.emptyList();
-                        default:
-                            throw new ProtocolException(
-                                    "Unsupported decode attempt result (" + result + ") for " + resourceKey);
+                    decodedData = applyPrivateKeyCipher(encryptedData, encContext, false);
+                    try (InputStream bais = new ByteArrayInputStream(decodedData)) {
+                        return extractKeyPairs(session, resourceKey, beginMarker, endMarker, passwordProvider, bais, headers);
                     }
+                } finally {
+                    Arrays.fill(decodedData, (byte) 0);
                 }
-
-                passwordProvider.handleDecodeAttemptResult(session, resourceKey, retryIndex, password, null);
-                password = null; // get rid of sensitive data a.s.a.p.
-                return keys;
-            }
+            });
+            return keys == null ? Collections.emptyList() : keys;
         }
 
         return super.extractKeyPairs(session, resourceKey, beginMarker, endMarker, passwordProvider, dataLines, headers);
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java
index df8f6d3b2..fbb5d233f 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java
@@ -21,7 +21,6 @@ package org.apache.sshd.common.config.keys.loader.pem;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.ProtocolException;
 import java.security.GeneralSecurityException;
 import java.security.KeyFactory;
 import java.security.KeyPair;
@@ -36,12 +35,10 @@ import java.util.List;
 import java.util.Map;
 
 import javax.security.auth.login.CredentialException;
-import javax.security.auth.login.FailedLoginException;
 
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.cipher.ECCurves;
 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.session.SessionContext;
 import org.apache.sshd.common.util.GenericUtils;
@@ -83,13 +80,18 @@ public class PKCS8PEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar
             FilePasswordProvider passwordProvider,
             InputStream stream, Map<String, String> headers)
             throws IOException, GeneralSecurityException {
-        byte[] encBytes = IoUtils.toByteArray(stream);
+        byte[] privateBytes = IoUtils.toByteArray(stream);
         if (beginMarker.contains(BEGIN_ENCRYPTED_MARKER)) {
             // RFC 5958 EncryptedPrivateKeyInfo.
-            return decryptKeyPairs(session, resourceKey, passwordProvider, encBytes);
+            return decryptKeyPairs(session, resourceKey, passwordProvider, privateBytes);
+        }
+        PKCS8PrivateKeyInfo pkcs8Info = new PKCS8PrivateKeyInfo(privateBytes);
+        try {
+            return extractKeyPairs(privateBytes, pkcs8Info);
+        } finally {
+            pkcs8Info.clear();
+            Arrays.fill(privateBytes, (byte) 0);
         }
-        PKCS8PrivateKeyInfo pkcs8Info = new PKCS8PrivateKeyInfo(encBytes);
-        return extractKeyPairs(encBytes, pkcs8Info);
     }
 
     public Collection<KeyPair> decryptKeyPairs(
@@ -104,48 +106,22 @@ public class PKCS8PEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar
         // See https://stackoverflow.com/questions/66286457/load-an-encrypted-pcks8-pem-private-key-in-java
         Decryptor decryptor = SecurityUtils.getBouncycastleEncryptedPrivateKeyInfoDecryptor();
 
-        Collection<KeyPair> keyPairs = Collections.emptyList();
-        for (int retryIndex = 0;; retryIndex++) {
-            String password = passwordProvider.getPassword(session, resourceKey, retryIndex);
+        Collection<KeyPair> keyPairs = passwordProvider.decode(session, resourceKey, password -> {
+            char[] key = password.toCharArray();
             try {
-                char[] key = password != null ? password.toCharArray() : null;
-                if (GenericUtils.isEmpty(key)) {
-                    throw new FailedLoginException("No password data for encrypted resource=" + resourceKey);
-                }
                 byte[] decrypted = decryptor.decrypt(encrypted, key);
                 PKCS8PrivateKeyInfo pkcs8Info = new PKCS8PrivateKeyInfo(decrypted);
                 try {
-                    keyPairs = extractKeyPairs(decrypted, pkcs8Info);
+                    return extractKeyPairs(decrypted, pkcs8Info);
                 } finally {
                     pkcs8Info.clear();
-                    Arrays.fill(key, (char) 0);
-                    if (decrypted != null) {
-                        Arrays.fill(decrypted, (byte) 0);
-                    }
-                }
-                passwordProvider.handleDecodeAttemptResult(session, resourceKey, retryIndex, password, null);
-                break;
-            } catch (IOException | GeneralSecurityException | RuntimeException e) {
-                ResourceDecodeResult result = passwordProvider.handleDecodeAttemptResult(session, resourceKey, retryIndex,
-                        password, e);
-                if (result == null) {
-                    result = ResourceDecodeResult.TERMINATE;
-                }
-                switch (result) {
-                    case TERMINATE:
-                        throw e;
-                    case RETRY:
-                        continue;
-                    case IGNORE:
-                        return null;
-                    default:
-                        throw new ProtocolException("Unsupported decode attempt result (" + result + ") for " + resourceKey);
+                    Arrays.fill(decrypted, (byte) 0);
                 }
             } finally {
-                password = null;
+                Arrays.fill(key, (char) 0);
             }
-        }
-        return keyPairs;
+        });
+        return keyPairs == null ? Collections.emptyList() : keyPairs;
     }
 
     public Collection<KeyPair> extractKeyPairs(byte[] encBytes, PKCS8PrivateKeyInfo pkcs8Info)
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleKeyPairResourceParser.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleKeyPairResourceParser.java
index 14a5d4ea2..fb138987e 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleKeyPairResourceParser.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleKeyPairResourceParser.java
@@ -23,7 +23,6 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.net.ProtocolException;
 import java.nio.charset.StandardCharsets;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
@@ -35,14 +34,11 @@ import java.util.List;
 import java.util.Map;
 
 import javax.security.auth.login.CredentialException;
-import javax.security.auth.login.FailedLoginException;
 
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
-import org.apache.sshd.common.config.keys.FilePasswordProvider.ResourceDecodeResult;
 import org.apache.sshd.common.config.keys.loader.AbstractKeyPairResourceParser;
 import org.apache.sshd.common.session.SessionContext;
-import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.security.SecurityProviderRegistrar;
 import org.apache.sshd.common.util.security.SecurityUtils;
@@ -128,40 +124,12 @@ public class BouncyCastleKeyPairResourceParser extends AbstractKeyPairResourcePa
                     throw new CredentialException("Missing password provider for encrypted resource=" + resourceKey);
                 }
 
-                for (int retryIndex = 0;; retryIndex++) {
-                    String password = provider.getPassword(session, resourceKey, retryIndex);
-                    PEMKeyPair decoded;
-                    try {
-                        if (GenericUtils.isEmpty(password)) {
-                            throw new FailedLoginException("No password data for encrypted resource=" + resourceKey);
-                        }
-
-                        JcePEMDecryptorProviderBuilder decryptorBuilder = new JcePEMDecryptorProviderBuilder();
-                        PEMDecryptorProvider pemDecryptor = decryptorBuilder.build(password.toCharArray());
-                        decoded = ((PEMEncryptedKeyPair) o).decryptKeyPair(pemDecryptor);
-                    } catch (IOException | GeneralSecurityException | RuntimeException e) {
-                        ResourceDecodeResult result
-                                = provider.handleDecodeAttemptResult(session, resourceKey, retryIndex, password, e);
-                        if (result == null) {
-                            result = ResourceDecodeResult.TERMINATE;
-                        }
-                        switch (result) {
-                            case TERMINATE:
-                                throw e;
-                            case RETRY:
-                                continue;
-                            case IGNORE:
-                                return null;
-                            default:
-                                throw new ProtocolException(
-                                        "Unsupported decode attempt result (" + result + ") for " + resourceKey);
-                        }
-                    }
-
-                    o = decoded;
-                    provider.handleDecodeAttemptResult(session, resourceKey, retryIndex, password, null);
-                    break;
-                }
+                PEMEncryptedKeyPair encrypted = (PEMEncryptedKeyPair) o;
+                o = provider.decode(session, resourceKey, password -> {
+                    JcePEMDecryptorProviderBuilder decryptorBuilder = new JcePEMDecryptorProviderBuilder();
+                    PEMDecryptorProvider pemDecryptor = decryptorBuilder.build(password.toCharArray());
+                    return encrypted.decryptKeyPair(pemDecryptor);
+                });
             }
 
             if (o instanceof PEMKeyPair) {
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 263a164be..9bc7106d6 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
@@ -23,7 +23,6 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.StreamCorruptedException;
-import java.net.ProtocolException;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.PrivateKey;
@@ -38,11 +37,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 
-import javax.security.auth.login.FailedLoginException;
-
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
-import org.apache.sshd.common.config.keys.FilePasswordProvider.ResourceDecodeResult;
 import org.apache.sshd.common.config.keys.impl.AbstractIdentityResourceLoader;
 import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
 import org.apache.sshd.common.session.SessionContext;
@@ -180,85 +176,51 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends
             String pubData, String prvData, String prvEncryption,
             FilePasswordProvider passwordProvider, Map<String, String> headers)
             throws IOException, GeneralSecurityException {
-        byte[] pubBytes = GenericUtils.EMPTY_BYTE_ARRAY;
-        byte[] prvBytes = GenericUtils.EMPTY_BYTE_ARRAY;
-        try {
-            Decoder b64Decoder = Base64.getDecoder();
-            pubBytes = b64Decoder.decode(pubData);
-            prvBytes = b64Decoder.decode(prvData);
-            if (GenericUtils.isEmpty(prvEncryption)
-                    || NO_PRIVATE_KEY_ENCRYPTION_VALUE.equalsIgnoreCase(prvEncryption)) {
+        Decoder b64Decoder = Base64.getDecoder();
+        byte[] pubBytes = b64Decoder.decode(pubData);
+        byte[] prvBytes = b64Decoder.decode(prvData);
+        if (GenericUtils.isEmpty(prvEncryption) || NO_PRIVATE_KEY_ENCRYPTION_VALUE.equalsIgnoreCase(prvEncryption)) {
+            try {
                 return loadKeyPairs(resourceKey, formatVersion, pubBytes, prvBytes, headers);
+            } finally {
+                Arrays.fill(prvBytes, (byte) 0);
             }
+        }
 
-            // format is "<cipher><bits>-<mode>" - e.g., "aes256-cbc"
-            int pos = prvEncryption.indexOf('-');
-            if (pos <= 0) {
-                throw new StreamCorruptedException("Missing private key encryption mode in " + prvEncryption);
-            }
-
-            String mode = prvEncryption.substring(pos + 1).toUpperCase();
-            String algName = null;
-            int numBits = 0;
-            for (int index = 0; index < pos; index++) {
-                char ch = prvEncryption.charAt(index);
-                if ((ch >= '0') && (ch <= '9')) {
-                    algName = prvEncryption.substring(0, index).toUpperCase();
-                    numBits = Integer.parseInt(prvEncryption.substring(index, pos));
-                    break;
-                }
-            }
+        // format is "<cipher><bits>-<mode>" - e.g., "aes256-cbc"
+        int pos = prvEncryption.indexOf('-');
+        if (pos <= 0) {
+            throw new StreamCorruptedException("Missing private key encryption mode in " + prvEncryption);
+        }
 
-            if (GenericUtils.isEmpty(algName) || (numBits <= 0)) {
-                throw new StreamCorruptedException("Missing private key encryption algorithm details in " + prvEncryption);
+        String mode = prvEncryption.substring(pos + 1).toUpperCase();
+        String algName = null;
+        int numBits = 0;
+        for (int index = 0; index < pos; index++) {
+            char ch = prvEncryption.charAt(index);
+            if ((ch >= '0') && (ch <= '9')) {
+                algName = prvEncryption.substring(0, index).toUpperCase();
+                numBits = Integer.parseInt(prvEncryption.substring(index, pos));
+                break;
             }
+        }
 
-            for (int retryIndex = 0;; retryIndex++) {
-                String password = passwordProvider.getPassword(session, resourceKey, retryIndex);
-
-                Collection<KeyPair> keys;
-                try {
-                    if (GenericUtils.isEmpty(password)) {
-                        throw new FailedLoginException("No password data for encrypted resource=" + resourceKey);
-                    }
-
-                    byte[] decBytes = PuttyKeyPairResourceParser.decodePrivateKeyBytes(
-                            formatVersion, prvBytes, algName, numBits, mode, password, headers);
-                    try {
-                        keys = loadKeyPairs(resourceKey, formatVersion, pubBytes, decBytes, headers);
-                    } finally {
-                        Arrays.fill(decBytes, (byte) 0); // eliminate sensitive data a.s.a.p.
-                    }
-                } catch (IOException | GeneralSecurityException | RuntimeException e) {
-                    ResourceDecodeResult result
-                            = passwordProvider.handleDecodeAttemptResult(session, resourceKey, retryIndex, password, e);
-                    password = null; // get rid of sensitive data a.s.a.p.
-                    if (result == null) {
-                        result = ResourceDecodeResult.TERMINATE;
-                    }
-
-                    password = null; // GC hint - don't keep sensitive data in memory longer than necessary
-                    switch (result) {
-                        case TERMINATE:
-                            throw e;
-                        case RETRY:
-                            continue;
-                        case IGNORE:
-                            return Collections.emptyList();
-                        default:
-                            throw new ProtocolException(
-                                    "Unsupported decode attempt result (" + result + ") for " + resourceKey);
-                    }
-                }
+        if (GenericUtils.isEmpty(algName) || (numBits <= 0)) {
+            throw new StreamCorruptedException("Missing private key encryption algorithm details in " + prvEncryption);
+        }
 
-                passwordProvider.handleDecodeAttemptResult(session, resourceKey, retryIndex, password, null);
-                password = null; // get rid of sensitive data a.s.a.p.
-                return keys;
+        String algorithm = algName;
+        int bits = numBits;
+        Collection<KeyPair> keys = passwordProvider.decode(session, resourceKey, password -> {
+            byte[] decBytes = PuttyKeyPairResourceParser.decodePrivateKeyBytes(formatVersion, prvBytes, algorithm, bits, mode,
+                    password, headers);
+            try {
+                return loadKeyPairs(resourceKey, formatVersion, pubBytes, decBytes, headers);
+            } finally {
+                Arrays.fill(decBytes, (byte) 0); // eliminate sensitive data a.s.a.p.
             }
-        } finally {
-            Arrays.fill(pubBytes, (byte) 0); // eliminate sensitive data a.s.a.p.
-            Arrays.fill(prvBytes, (byte) 0); // eliminate sensitive data a.s.a.p.
-        }
+        });
+        return keys == null ? Collections.emptyList() : keys;
     }
 
     public Collection<KeyPair> loadKeyPairs(