diff --git a/pom.xml b/pom.xml
index f42e98f..0e92784 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
   </parent>
 
   <artifactId>plexus-sec-dispatcher</artifactId>
-  <version>3.0.1-SNAPSHOT</version>
+  <version>4.0.0-SNAPSHOT</version>
 
   <name>Plexus Security Dispatcher Component</name>
 
@@ -35,9 +35,16 @@
   <properties>
     <javaVersion>17</javaVersion>
     <project.build.outputTimestamp>2024-09-29T15:16:00Z</project.build.outputTimestamp>
+
+    <version.slf4j>2.0.16</version.slf4j>
   </properties>
 
   <dependencies>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <version>${version.slf4j}</version>
+    </dependency>
     <dependency>
       <groupId>org.codehaus.plexus</groupId>
       <artifactId>plexus-cipher</artifactId>
@@ -62,6 +69,12 @@
       <artifactId>junit-jupiter</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <version>${version.slf4j}</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
@@ -75,7 +88,7 @@
         <artifactId>modello-maven-plugin</artifactId>
         <version>2.4.0</version>
         <configuration>
-          <version>3.0.0</version>
+          <version>4.0.0</version>
           <models>
             <model>src/main/mdo/settings-security.mdo</model>
           </models>
@@ -96,6 +109,9 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-plugin</artifactId>
         <configuration>
+          <systemPropertyVariables>
+            <masterPassword>masterPw</masterPassword>
+          </systemPropertyVariables>
           <environmentVariables>
             <MASTER_PASSWORD>masterPw</MASTER_PASSWORD>
           </environmentVariables>
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/Dispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/Dispatcher.java
similarity index 51%
rename from src/main/java/org/codehaus/plexus/components/secdispatcher/internal/Dispatcher.java
rename to src/main/java/org/codehaus/plexus/components/secdispatcher/Dispatcher.java
index de030a8..059e1e3 100644
--- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/Dispatcher.java
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/Dispatcher.java
@@ -11,11 +11,11 @@
  * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
  */
 
-package org.codehaus.plexus.components.secdispatcher.internal;
+package org.codehaus.plexus.components.secdispatcher;
 
 import java.util.Map;
 
-import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
+import static java.util.Objects.requireNonNull;
 
 /**
  * Dispatcher.
@@ -26,31 +26,51 @@
  */
 public interface Dispatcher {
     /**
-     * Configuration key for masterPassword. It may be present, if SecDispatcher could
-     * obtain it, but presence is optional. Still, dispatcher may throw and fail the operation
-     * if it requires it.
+     * The "encrypt payload" prepared by dispatcher.
      */
-    String CONF_MASTER_PASSWORD = "masterPassword";
+    final class EncryptPayload {
+        private final Map<String, String> attributes;
+        private final String encrypted;
+
+        public EncryptPayload(Map<String, String> attributes, String encrypted) {
+            this.attributes = requireNonNull(attributes);
+            this.encrypted = requireNonNull(encrypted);
+        }
+
+        public Map<String, String> getAttributes() {
+            return attributes;
+        }
+
+        public String getEncrypted() {
+            return encrypted;
+        }
+    }
 
     /**
-     * encrypt given plaintext string
+     * Encrypt given plaintext string. Implementation must return at least same attributes it got, but may add more
+     * attributes to returned payload.
      *
-     * @param str string to encrypt
+     * @param str string to encrypt, never {@code null}
      * @param attributes attributes, never {@code null}
      * @param config configuration from settings-security.xml, never {@code null}
-     * @return encrypted string
+     * @return encrypted string and attributes in {@link EncryptPayload}
      */
-    String encrypt(String str, Map<String, String> attributes, Map<String, String> config)
+    EncryptPayload encrypt(String str, Map<String, String> attributes, Map<String, String> config)
             throws SecDispatcherException;
 
     /**
-     * decrypt given encrypted string
+     * Decrypt given encrypted string.
      *
-     * @param str string to decrypt
+     * @param str string to decrypt, never {@code null}
      * @param attributes attributes, never {@code null}
      * @param config configuration from settings-security.xml, never {@code null}
      * @return decrypted string
      */
     String decrypt(String str, Map<String, String> attributes, Map<String, String> config)
             throws SecDispatcherException;
+
+    /**
+     * Validates dispatcher configuration.
+     */
+    SecDispatcher.ValidationResponse validateConfiguration(Map<String, String> config);
 }
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/DispatcherMeta.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/DispatcherMeta.java
new file mode 100644
index 0000000..219ec46
--- /dev/null
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/DispatcherMeta.java
@@ -0,0 +1,128 @@
+package org.codehaus.plexus.components.secdispatcher;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Meta description of dispatcher.
+ */
+public interface DispatcherMeta {
+    final class Field {
+        private final String key;
+        private final boolean optional;
+        private final String defaultValue;
+        private final String description;
+        private final List<Field> options;
+
+        private Field(String key, boolean optional, String defaultValue, String description, List<Field> options) {
+            this.key = requireNonNull(key);
+            this.optional = optional;
+            this.defaultValue = defaultValue;
+            this.description = requireNonNull(description);
+            this.options = options;
+        }
+
+        /**
+         * The key to be used in configuration map for field.
+         */
+        public String getKey() {
+            return key;
+        }
+
+        /**
+         * Is configuration optional?
+         */
+        public boolean isOptional() {
+            return optional;
+        }
+
+        /**
+         * Optional default value of the configuration.
+         */
+        public Optional<String> getDefaultValue() {
+            return Optional.ofNullable(defaultValue);
+        }
+
+        /**
+         * The human description of the configuration.
+         */
+        public String getDescription() {
+            return description;
+        }
+
+        /**
+         * Optional list of options, if this configuration accepts limited values. Each option is represented
+         * as field, where {@link #getKey()} represents the value to be used, and {@link #displayName()} represents
+         * the description of option. The {@link #getDefaultValue()}, if present represents the value to be used
+         * instead of {@link #getKey()}.
+         */
+        public Optional<List<Field>> getOptions() {
+            return Optional.ofNullable(options);
+        }
+
+        public static Builder builder(String key) {
+            return new Builder(key);
+        }
+
+        public static final class Builder {
+            private final String key;
+            private boolean optional;
+            private String defaultValue;
+            private String description;
+            private List<Field> options;
+
+            private Builder(String key) {
+                this.key = requireNonNull(key);
+            }
+
+            public Builder optional(boolean optional) {
+                this.optional = optional;
+                return this;
+            }
+
+            public Builder defaultValue(String defaultValue) {
+                this.defaultValue = defaultValue;
+                return this;
+            }
+
+            public Builder description(String description) {
+                this.description = requireNonNull(description);
+                return this;
+            }
+
+            public Builder options(List<Field> options) {
+                this.options = requireNonNull(options);
+                return this;
+            }
+
+            public Field build() {
+                return new Field(key, optional, defaultValue, description, options);
+            }
+        }
+    }
+
+    /**
+     * Option to hide this instance from users, like for migration or legacy purposes.
+     */
+    default boolean isHidden() {
+        return false;
+    }
+
+    /**
+     * The name of the dispatcher.
+     */
+    String name();
+
+    /**
+     * Returns the display (human) name of the dispatcher.
+     */
+    String displayName();
+
+    /**
+     * Returns the configuration fields of the dispatcher.
+     */
+    Collection<Field> fields();
+}
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSource.java
new file mode 100644
index 0000000..d5a754b
--- /dev/null
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSource.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2008 Sonatype, Inc. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package org.codehaus.plexus.components.secdispatcher;
+
+/**
+ * Source of master password.
+ */
+public interface MasterSource {
+    /**
+     * Handles the config to get master password. Implementation may do one of the following things:
+     * <ul>
+     *     <li>if the config cannot be handled by given source, return {@code null}</li>
+     *     <li>otherwise, if master password retrieval based on config was attempted but failed, throw {@link SecDispatcherException}</li>
+     *     <li>happy path: return the master password.</li>
+     * </ul>
+     *
+     * @param config the source of master password, and opaque string.
+     * @return the master password, or {@code null} if implementation does not handle this config
+     * @throws SecDispatcherException If implementation does handle this masterSource, but cannot obtain master password
+     */
+    String handle(String config) throws SecDispatcherException;
+
+    /**
+     * Validates master source configuration.
+     * <ul>
+     *     <li>if the config cannot be handled by given source, return {@code null}</li>
+     *     <li>otherwise, implementation performs validation and returns non-{@code null} validation response</li>
+     * </ul>
+     */
+    SecDispatcher.ValidationResponse validateConfiguration(String config);
+}
diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/StaticMasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSourceMeta.java
similarity index 51%
rename from src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/StaticMasterPasswordSource.java
rename to src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSourceMeta.java
index 7ef6d89..4e112c1 100644
--- a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/StaticMasterPasswordSource.java
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSourceMeta.java
@@ -11,22 +11,22 @@
  * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
  */
 
-package org.codehaus.plexus.components.secdispatcher.internal.sources;
+package org.codehaus.plexus.components.secdispatcher;
 
-import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
-import org.codehaus.plexus.components.secdispatcher.internal.MasterPasswordSource;
+import java.util.Optional;
 
-import static java.util.Objects.requireNonNull;
-
-public class StaticMasterPasswordSource implements MasterPasswordSource {
-    private final String masterPassword;
-
-    public StaticMasterPasswordSource(String masterPassword) {
-        this.masterPassword = requireNonNull(masterPassword);
-    }
+/**
+ * Source of master password.
+ */
+public interface MasterSourceMeta {
+    /**
+     * String describing what this source does.
+     */
+    String description();
 
-    @Override
-    public String handle(String masterSource) throws SecDispatcherException {
-        return masterPassword;
-    }
+    /**
+     * Optional "config template" that may serve as basis to configure this master source. The template cannot be
+     * "reused" as is as configuration.
+     */
+    Optional<String> configTemplate();
 }
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/PinEntry.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/PinEntry.java
new file mode 100644
index 0000000..35b8765
--- /dev/null
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/PinEntry.java
@@ -0,0 +1,279 @@
+/*
+ * 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.codehaus.plexus.components.secdispatcher;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Inspired by <a href="https://velvetcache.org/2023/03/26/a-peek-inside-pinentry/">A peek inside pinentry</a>.
+ * Also look at <a href="https://gorbe.io/posts/gnupg/pinentry/documentation/">Pinentry Documentation</a>.
+ * Finally, source mirror is at <a href="https://github.com/gpg/pinentry">gpg/pinentry</a>.
+ */
+public class PinEntry {
+    public enum Outcome {
+        SUCCESS,
+        TIMEOUT,
+        NOT_CONFIRMED,
+        CANCELED,
+        FAILED;
+    }
+
+    public record Result(Outcome outcome, String payload) {}
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+    private final String cmd;
+    private final LinkedHashMap<String, String> commands;
+
+    /**
+     * Creates pin entry instance that will use the passed in cmd executable.
+     */
+    public PinEntry(String cmd) {
+        this.cmd = requireNonNull(cmd);
+        this.commands = new LinkedHashMap<>();
+    }
+
+    /**
+     * Sets a "stable key handle" for caching purposes. Optional.
+     */
+    public PinEntry setKeyInfo(String keyInfo) {
+        requireNonNull(keyInfo);
+        commands.put("OPTION", "allow-external-password-cache");
+        commands.put("SETKEYINFO", keyInfo);
+        return this;
+    }
+
+    /**
+     * Sets the OK button label, by default "Ok".
+     */
+    public PinEntry setOk(String msg) {
+        requireNonNull(msg);
+        commands.put("SETOK", msg);
+        return this;
+    }
+
+    /**
+     * Sets the CANCEL button label, by default "Cancel".
+     */
+    public PinEntry setCancel(String msg) {
+        requireNonNull(msg);
+        commands.put("SETCANCEL", msg);
+        return this;
+    }
+
+    /**
+     * Sets the window title.
+     */
+    public PinEntry setTitle(String title) {
+        requireNonNull(title);
+        commands.put("SETTITLE", title);
+        return this;
+    }
+
+    /**
+     * Sets additional test in window.
+     */
+    public PinEntry setDescription(String desc) {
+        requireNonNull(desc);
+        commands.put("SETDESC", desc);
+        return this;
+    }
+
+    /**
+     * Sets the prompt.
+     */
+    public PinEntry setPrompt(String prompt) {
+        requireNonNull(prompt);
+        commands.put("SETPROMPT", prompt);
+        return this;
+    }
+
+    /**
+     * If set, window will show "Error: xxx", usable for second attempt (ie "bad password").
+     */
+    public PinEntry setError(String error) {
+        requireNonNull(error);
+        commands.put("SETERROR", error);
+        return this;
+    }
+
+    /**
+     * Usable with {@link #getPin()}, window will contain two input fields and will force user to type in same
+     * input in both fields, ie to "confirm" the pin.
+     */
+    public PinEntry confirmPin() {
+        commands.put("SETREPEAT", null);
+        return this;
+    }
+
+    /**
+     * Sets the window timeout, if no button pressed and timeout passes, Result will by {@link Outcome#TIMEOUT}.
+     */
+    public PinEntry setTimeout(Duration timeout) {
+        long seconds = timeout.toSeconds();
+        if (seconds < 0) {
+            throw new IllegalArgumentException("Set timeout is 0 seconds");
+        }
+        commands.put("SETTIMEOUT", String.valueOf(seconds));
+        return this;
+    }
+
+    /**
+     * Initiates a "get pin" dialogue with input field(s) using previously set options.
+     */
+    public Result getPin() throws IOException {
+        commands.put("GETPIN", null);
+        return execute();
+    }
+
+    /**
+     * Initiates a "confirmation" dialogue (no input) using previously set options.
+     */
+    public Result confirm() throws IOException {
+        commands.put("CONFIRM", null);
+        return execute();
+    }
+
+    /**
+     * Initiates a "message" dialogue (no input) using previously set options.
+     */
+    public Result message() throws IOException {
+        commands.put("MESSAGE", null);
+        return execute();
+    }
+
+    private Result execute() throws IOException {
+        Process process = new ProcessBuilder(cmd).start();
+        BufferedReader reader = process.inputReader();
+        BufferedWriter writer = process.outputWriter();
+        expectOK(process.inputReader());
+        Map.Entry<String, String> lastEntry = commands.entrySet().iterator().next();
+        for (Map.Entry<String, String> entry : commands.entrySet()) {
+            String cmd;
+            if (entry.getValue() != null) {
+                cmd = entry.getKey() + " " + entry.getValue();
+            } else {
+                cmd = entry.getKey();
+            }
+            logger.debug("> {}", cmd);
+            writer.write(cmd);
+            writer.newLine();
+            writer.flush();
+            if (entry != lastEntry) {
+                expectOK(reader);
+            }
+        }
+        Result result = lastExpect(reader);
+        writer.write("BYE");
+        writer.newLine();
+        writer.flush();
+        try {
+            process.waitFor(5, TimeUnit.SECONDS);
+            int exitCode = process.exitValue();
+            if (exitCode != 0) {
+                return new Result(Outcome.FAILED, "Exit code: " + exitCode);
+            } else {
+                return result;
+            }
+        } catch (Exception e) {
+            return new Result(Outcome.FAILED, e.getMessage());
+        }
+    }
+
+    private void expectOK(BufferedReader in) throws IOException {
+        String response = in.readLine();
+        logger.debug("< {}", response);
+        if (!response.startsWith("OK")) {
+            throw new IOException("Expected OK but got this instead: " + response);
+        }
+    }
+
+    private Result lastExpect(BufferedReader in) throws IOException {
+        while (true) {
+            String response = in.readLine();
+            logger.debug("< {}", response);
+            if (response.startsWith("#")) {
+                continue;
+            }
+            if (response.startsWith("S")) {
+                continue;
+            }
+            if (response.startsWith("ERR")) {
+                if (response.contains("83886142")) {
+                    return new Result(Outcome.TIMEOUT, response);
+                }
+                if (response.contains("83886179")) {
+                    return new Result(Outcome.CANCELED, response);
+                }
+                if (response.contains("83886194")) {
+                    return new Result(Outcome.NOT_CONFIRMED, response);
+                }
+            }
+            if (response.startsWith("D")) {
+                return new Result(Outcome.SUCCESS, response.substring(2));
+            }
+            if (response.startsWith("OK")) {
+                return new Result(Outcome.SUCCESS, response);
+            }
+        }
+    }
+
+    public static void main(String[] args) throws IOException {
+        // check what pinentry apps you have and replace the execName
+        String cmd = "/usr/bin/pinentry-gnome3";
+        Result pinResult = new PinEntry(cmd)
+                .setTimeout(Duration.ofSeconds(15))
+                .setKeyInfo("maven:masterPassword")
+                .setTitle("Maven Master Password")
+                .setDescription("Please enter the Maven master password")
+                .setPrompt("Password")
+                .setOk("Here you go!")
+                .setCancel("Uh oh, rather not")
+                // .confirmPin() (will not let you through if you cannot type same thing twice)
+                .getPin();
+        if (pinResult.outcome() == Outcome.SUCCESS) {
+            Result confirmResult = new PinEntry(cmd)
+                    .setTitle("Password confirmation")
+                    .setPrompt("Please confirm the password")
+                    .setDescription("Is the password '" + pinResult.payload() + "' the one you want?")
+                    .confirm();
+            if (confirmResult.outcome() == Outcome.SUCCESS) {
+                new PinEntry(cmd)
+                        .setTitle("Password confirmed")
+                        .setPrompt("The password '" + pinResult.payload() + "' is confirmed.")
+                        .setDescription("You confirmed your password")
+                        .message();
+            } else {
+                System.out.println(confirmResult);
+            }
+        } else {
+            System.out.println(pinResult);
+        }
+    }
+}
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java
index 04ef9dd..fde9fa9 100644
--- a/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java
@@ -14,46 +14,35 @@
 package org.codehaus.plexus.components.secdispatcher;
 
 import java.io.IOException;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity;
 
 /**
- * This component decrypts a string, passed to it
+ * This component decrypts a string, passed to it using various dispatchers.
  *
  * @author Oleg Gusakov
  */
 public interface SecDispatcher {
     /**
-     * The default path of configuration.
-     * <p>
-     * The character {@code ~} (tilde) may be present as first character ONLY and is
-     * interpreted as "user.home" system property, and it MUST be followed by path separator.
-     */
-    String DEFAULT_CONFIGURATION = "~/.m2/settings-security.xml";
-
-    /**
-     * Java System Property that may be set, to override configuration path.
-     */
-    String SYSTEM_PROPERTY_CONFIGURATION_LOCATION = "settings.security";
-
-    /**
-     * Attribute that selects a dispatcher.
+     * Attribute that selects a dispatcher. If not present in {@link #encrypt(String, Map)} attributes, the
+     * configured "default dispatcher" is used.
      *
      * @see #availableDispatchers()
      */
     String DISPATCHER_NAME_ATTR = "name";
 
     /**
-     * Returns the set of available dispatcher names, never {@code null}.
+     * Attribute for version, added by SecDispatcher for possible upgrade path.
      */
-    Set<String> availableDispatchers();
+    String DISPATCHER_VERSION_ATTR = "version";
 
     /**
-     * Returns the set of available ciphers, never {@code null}.
+     * Returns the set of available dispatcher metadata, never {@code null}.
      */
-    Set<String> availableCiphers();
+    Set<DispatcherMeta> availableDispatchers();
 
     /**
      * Encrypt given plaintext string.
@@ -63,7 +52,7 @@ public interface SecDispatcher {
      * @return encrypted string
      * @throws SecDispatcherException in case of problem
      */
-    String encrypt(String str, Map<String, String> attr) throws SecDispatcherException;
+    String encrypt(String str, Map<String, String> attr) throws SecDispatcherException, IOException;
 
     /**
      * Decrypt given encrypted string.
@@ -72,7 +61,12 @@ public interface SecDispatcher {
      * @return decrypted string
      * @throws SecDispatcherException in case of problem
      */
-    String decrypt(String str) throws SecDispatcherException;
+    String decrypt(String str) throws SecDispatcherException, IOException;
+
+    /**
+     * Returns {@code true} if passed in string contains "legacy" password (Maven3 kind).
+     */
+    boolean isLegacyPassword(String str);
 
     /**
      * Reads the effective configuration, eventually creating new instance if not present.
@@ -90,4 +84,50 @@ public interface SecDispatcher {
      * @throws IOException In case of IO problem
      */
     void writeConfiguration(SettingsSecurity configuration) throws IOException;
+
+    /**
+     * The validation response.
+     */
+    final class ValidationResponse {
+        public enum Level {
+            INFO,
+            WARNING,
+            ERROR
+        };
+
+        private final String source;
+        private final boolean valid;
+        private final Map<Level, List<String>> report;
+        private final List<ValidationResponse> subsystems;
+
+        public ValidationResponse(
+                String source, boolean valid, Map<Level, List<String>> report, List<ValidationResponse> subsystems) {
+            this.source = source;
+            this.valid = valid;
+            this.report = report;
+            this.subsystems = subsystems;
+        }
+
+        public String getSource() {
+            return source;
+        }
+
+        public boolean isValid() {
+            return valid;
+        }
+
+        public Map<Level, List<String>> getReport() {
+            return report;
+        }
+
+        public List<ValidationResponse> getSubsystems() {
+            return subsystems;
+        }
+    }
+
+    /**
+     * Performs a "deep validation" and reports the status. If return instance {@link ValidationResponse#isValid()}
+     * is {@code true}, configuration is usable.
+     */
+    ValidationResponse validateConfiguration();
 }
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java
index 131c0de..37f4dbd 100644
--- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java
@@ -13,14 +13,13 @@
 
 package org.codehaus.plexus.components.secdispatcher.internal;
 
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
 import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.StringTokenizer;
@@ -28,69 +27,110 @@
 
 import org.codehaus.plexus.components.cipher.PlexusCipher;
 import org.codehaus.plexus.components.cipher.PlexusCipherException;
+import org.codehaus.plexus.components.secdispatcher.Dispatcher;
+import org.codehaus.plexus.components.secdispatcher.DispatcherMeta;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
+import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher;
 import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity;
 
 import static java.util.Objects.requireNonNull;
 
 /**
+ * Note: this implementation is NOT a JSR330 component. Integrating apps anyway want to customize it (at least
+ * the name and location of configuration file), so instead as before (providing "bad" configuration file just
+ * to have one), it is the duty of integrator to wrap and "finish" the implementation in a way it suits the
+ * integrator. Also, using "globals" like Java System Properties are bad thing, and it is integrator who knows
+ * what is needed anyway.
+ * <p>
+ * Recommended way for integration is to create JSR330 {@link javax.inject.Provider}.
+ *
  * @author Oleg Gusakov
  */
-@Singleton
-@Named
 public class DefaultSecDispatcher implements SecDispatcher {
     public static final String ATTR_START = "[";
     public static final String ATTR_STOP = "]";
 
     protected final PlexusCipher cipher;
-    protected final Map<String, MasterPasswordSource> masterPasswordSources;
     protected final Map<String, Dispatcher> dispatchers;
-    protected final String configurationFile;
-
-    @Inject
-    public DefaultSecDispatcher(
-            PlexusCipher cipher,
-            Map<String, MasterPasswordSource> masterPasswordSources,
-            Map<String, Dispatcher> dispatchers,
-            @Named("${configurationFile:-" + DEFAULT_CONFIGURATION + "}") final String configurationFile) {
+    protected final Path configurationFile;
+
+    public DefaultSecDispatcher(PlexusCipher cipher, Map<String, Dispatcher> dispatchers, Path configurationFile) {
         this.cipher = requireNonNull(cipher);
-        this.masterPasswordSources = requireNonNull(masterPasswordSources);
         this.dispatchers = requireNonNull(dispatchers);
         this.configurationFile = requireNonNull(configurationFile);
+
+        // file may or may not exist, but one thing is certain: it cannot be an exiting directory
+        if (Files.isDirectory(configurationFile)) {
+            throw new IllegalArgumentException("configurationFile cannot be a directory");
+        }
     }
 
     @Override
-    public Set<String> availableDispatchers() {
-        return Set.copyOf(dispatchers.keySet());
+    public Set<DispatcherMeta> availableDispatchers() {
+        return Set.copyOf(
+                dispatchers.entrySet().stream().map(this::dispatcherMeta).collect(Collectors.toSet()));
     }
 
-    @Override
-    public Set<String> availableCiphers() {
-        return cipher.availableCiphers();
+    private DispatcherMeta dispatcherMeta(Map.Entry<String, Dispatcher> dispatcher) {
+        // sisu components are lazy!
+        Dispatcher d = dispatcher.getValue();
+        if (d instanceof DispatcherMeta meta) {
+            return meta;
+        } else {
+            return new DispatcherMeta() {
+                @Override
+                public String name() {
+                    return dispatcher.getKey();
+                }
+
+                @Override
+                public String displayName() {
+                    return dispatcher.getKey() + " (needs manual configuration)";
+                }
+
+                @Override
+                public Collection<Field> fields() {
+                    return List.of();
+                }
+            };
+        }
     }
 
     @Override
-    public String encrypt(String str, Map<String, String> attr) throws SecDispatcherException {
+    public String encrypt(String str, Map<String, String> attr) throws SecDispatcherException, IOException {
         if (isEncryptedString(str)) return str;
 
         try {
-            String res;
-            if (attr == null || attr.get(DISPATCHER_NAME_ATTR) == null) {
-                SettingsSecurity sec = getConfiguration(true);
-                String master = getMasterPassword(sec, true);
-                res = cipher.encrypt(getMasterCipher(sec), str, master);
+            if (attr == null) {
+                attr = new HashMap<>();
             } else {
-                String type = attr.get(DISPATCHER_NAME_ATTR);
-                Dispatcher dispatcher = dispatchers.get(type);
-                if (dispatcher == null) throw new SecDispatcherException("no dispatcher for name " + type);
-                res = ATTR_START
-                        + attr.entrySet().stream()
-                                .map(e -> e.getKey() + "=" + e.getValue())
-                                .collect(Collectors.joining(","))
-                        + ATTR_STOP;
-                res += dispatcher.encrypt(str, attr, prepareDispatcherConfig(type));
+                attr = new HashMap<>(attr);
+            }
+            if (attr.get(DISPATCHER_NAME_ATTR) == null) {
+                SettingsSecurity conf = readConfiguration(false);
+                if (conf == null) {
+                    throw new SecDispatcherException("No configuration found");
+                }
+                String defaultDispatcher = conf.getDefaultDispatcher();
+                if (defaultDispatcher == null) {
+                    throw new SecDispatcherException("No defaultDispatcher set in configuration");
+                }
+                attr.put(DISPATCHER_NAME_ATTR, defaultDispatcher);
             }
+            String name = attr.get(DISPATCHER_NAME_ATTR);
+            Dispatcher dispatcher = dispatchers.get(name);
+            if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name);
+            Dispatcher.EncryptPayload payload = dispatcher.encrypt(str, attr, prepareDispatcherConfig(name));
+            HashMap<String, String> resultAttributes = new HashMap<>(payload.getAttributes());
+            resultAttributes.put(SecDispatcher.DISPATCHER_NAME_ATTR, name);
+            resultAttributes.put(SecDispatcher.DISPATCHER_VERSION_ATTR, SecUtil.specVersion());
+            String res = ATTR_START
+                    + resultAttributes.entrySet().stream()
+                            .map(e -> e.getKey() + "=" + e.getValue())
+                            .collect(Collectors.joining(","))
+                    + ATTR_STOP;
+            res += payload.getEncrypted();
             return cipher.decorate(res);
         } catch (PlexusCipherException e) {
             throw new SecDispatcherException(e.getMessage(), e);
@@ -98,29 +138,33 @@ public String encrypt(String str, Map<String, String> attr) throws SecDispatcher
     }
 
     @Override
-    public String decrypt(String str) throws SecDispatcherException {
+    public String decrypt(String str) throws SecDispatcherException, IOException {
         if (!isEncryptedString(str)) return str;
         try {
             String bare = cipher.unDecorate(str);
-            Map<String, String> attr = stripAttributes(bare);
-            if (attr == null || attr.get(DISPATCHER_NAME_ATTR) == null) {
-                SettingsSecurity sec = getConfiguration(true);
-                String master = getMasterPassword(sec, true);
-                return cipher.decrypt(getMasterCipher(sec), bare, master);
-            } else {
-                String type = attr.get(DISPATCHER_NAME_ATTR);
-                Dispatcher dispatcher = dispatchers.get(type);
-                if (dispatcher == null) throw new SecDispatcherException("no dispatcher for name " + type);
-                return dispatcher.decrypt(strip(bare), attr, prepareDispatcherConfig(type));
+            Map<String, String> attr = requireNonNull(stripAttributes(bare));
+            if (isLegacyPassword(str)) {
+                attr.put(DISPATCHER_NAME_ATTR, LegacyDispatcher.NAME);
             }
+            String name = attr.get(DISPATCHER_NAME_ATTR);
+            Dispatcher dispatcher = dispatchers.get(name);
+            if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name);
+            return dispatcher.decrypt(strip(bare), attr, prepareDispatcherConfig(name));
         } catch (PlexusCipherException e) {
             throw new SecDispatcherException(e.getMessage(), e);
         }
     }
 
+    @Override
+    public boolean isLegacyPassword(String str) {
+        if (!isEncryptedString(str)) return false;
+        Map<String, String> attr = requireNonNull(stripAttributes(cipher.unDecorate(str)));
+        return !attr.containsKey(DISPATCHER_NAME_ATTR);
+    }
+
     @Override
     public SettingsSecurity readConfiguration(boolean createIfMissing) throws IOException {
-        SettingsSecurity configuration = SecUtil.read(getConfigurationPath());
+        SettingsSecurity configuration = SecUtil.read(configurationFile);
         if (configuration == null && createIfMissing) {
             configuration = new SettingsSecurity();
         }
@@ -130,24 +174,86 @@ public SettingsSecurity readConfiguration(boolean createIfMissing) throws IOExce
     @Override
     public void writeConfiguration(SettingsSecurity configuration) throws IOException {
         requireNonNull(configuration, "configuration is null");
-        SecUtil.write(getConfigurationPath(), configuration, true);
+        SecUtil.write(configurationFile, configuration, true);
     }
 
-    private Map<String, String> prepareDispatcherConfig(String type) {
-        HashMap<String, String> dispatcherConf = new HashMap<>();
-        SettingsSecurity sec = getConfiguration(false);
-        String master = getMasterPassword(sec, false);
-        if (master != null) {
-            dispatcherConf.put(Dispatcher.CONF_MASTER_PASSWORD, master);
+    @Override
+    public ValidationResponse validateConfiguration() {
+        HashMap<ValidationResponse.Level, List<String>> report = new HashMap<>();
+        ArrayList<ValidationResponse> subsystems = new ArrayList<>();
+        boolean valid = false;
+        try {
+            SettingsSecurity config = readConfiguration(false);
+            if (config == null) {
+                report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                        .add("No configuration file found on path " + configurationFile);
+            } else {
+                report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>())
+                        .add("Configuration file present on path " + configurationFile);
+                String defaultDispatcher = config.getDefaultDispatcher();
+                if (defaultDispatcher == null) {
+                    report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                            .add("No default dispatcher set in configuration");
+                } else {
+                    report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>())
+                            .add("Default dispatcher configured");
+                    Dispatcher dispatcher = dispatchers.get(defaultDispatcher);
+                    if (dispatcher == null) {
+                        report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                                .add("Configured default dispatcher not present in system");
+                    } else {
+                        ValidationResponse dispatcherResponse =
+                                dispatcher.validateConfiguration(prepareDispatcherConfig(defaultDispatcher));
+                        subsystems.add(dispatcherResponse);
+                        if (!dispatcherResponse.isValid()) {
+                            report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                                    .add("Configured default dispatcher configuration is invalid");
+                        } else {
+                            valid = true;
+                            report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>())
+                                    .add("Configured default dispatcher configuration is valid");
+                        }
+                    }
+                }
+            }
+
+            // below is legacy check, that does not affect validity of config, is merely informational
+            Dispatcher legacy = dispatchers.get(LegacyDispatcher.NAME);
+            if (legacy == null) {
+                report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>())
+                        .add("Legacy dispatcher not present in system");
+            } else {
+                report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>())
+                        .add("Legacy dispatcher present in system");
+                ValidationResponse legacyResponse =
+                        legacy.validateConfiguration(prepareDispatcherConfig(LegacyDispatcher.NAME));
+                subsystems.add(legacyResponse);
+                if (!legacyResponse.isValid()) {
+                    report.computeIfAbsent(ValidationResponse.Level.WARNING, k -> new ArrayList<>())
+                            .add("Legacy dispatcher not operational; transparent fallback not possible");
+                } else {
+                    report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>())
+                            .add("Legacy dispatcher is operational; transparent fallback possible");
+                }
+            }
+        } catch (IOException e) {
+            report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                    .add(e.getMessage());
         }
-        Map<String, String> conf = SecUtil.getConfig(sec, type);
+
+        return new ValidationResponse(getClass().getSimpleName(), valid, report, subsystems);
+    }
+
+    protected Map<String, String> prepareDispatcherConfig(String name) throws IOException {
+        HashMap<String, String> dispatcherConf = new HashMap<>();
+        Map<String, String> conf = SecUtil.getConfig(SecUtil.read(configurationFile), name);
         if (conf != null) {
             dispatcherConf.putAll(conf);
         }
         return dispatcherConf;
     }
 
-    private String strip(String str) {
+    protected String strip(String str) {
         int start = str.indexOf(ATTR_START);
         int stop = str.indexOf(ATTR_STOP);
         if (start != -1 && stop != -1 && stop > start) {
@@ -156,7 +262,8 @@ private String strip(String str) {
         return str;
     }
 
-    private Map<String, String> stripAttributes(String str) {
+    protected Map<String, String> stripAttributes(String str) {
+        HashMap<String, String> result = new HashMap<>();
         int start = str.indexOf(ATTR_START);
         int stop = str.indexOf(ATTR_STOP);
         if (start != -1 && stop != -1 && stop > start) {
@@ -164,68 +271,20 @@ private Map<String, String> stripAttributes(String str) {
             if (stop == start + 1) return null;
             String attrs = str.substring(start + 1, stop).trim();
             if (attrs.isEmpty()) return null;
-            Map<String, String> res = null;
             StringTokenizer st = new StringTokenizer(attrs, ",");
             while (st.hasMoreTokens()) {
-                if (res == null) res = new HashMap<>(st.countTokens());
                 String pair = st.nextToken();
                 int pos = pair.indexOf('=');
                 if (pos == -1) throw new SecDispatcherException("Attribute malformed: " + pair);
                 String key = pair.substring(0, pos).trim();
                 String val = pair.substring(pos + 1).trim();
-                res.put(key, val);
+                result.put(key, val);
             }
-            return res;
         }
-        return null;
+        return result;
     }
 
-    private boolean isEncryptedString(String str) {
-        if (str == null) return false;
+    protected boolean isEncryptedString(String str) {
         return cipher.isEncryptedString(str);
     }
-
-    private Path getConfigurationPath() {
-        String location = System.getProperty(SYSTEM_PROPERTY_CONFIGURATION_LOCATION, getConfigurationFile());
-        location = location.charAt(0) == '~' ? System.getProperty("user.home") + location.substring(1) : location;
-        return Paths.get(location);
-    }
-
-    private SettingsSecurity getConfiguration(boolean mandatory) throws SecDispatcherException {
-        Path path = getConfigurationPath();
-        try {
-            SettingsSecurity sec = SecUtil.read(path);
-            if (mandatory && sec == null)
-                throw new SecDispatcherException("Please check that configuration file on path " + path + " exists");
-            return sec;
-        } catch (IOException e) {
-            throw new SecDispatcherException(e.getMessage(), e);
-        }
-    }
-
-    private String getMasterPassword(SettingsSecurity sec, boolean mandatory) throws SecDispatcherException {
-        if (sec == null && !mandatory) {
-            return null;
-        }
-        requireNonNull(sec, "configuration is null");
-        String masterSource = requireNonNull(sec.getMasterSource(), "masterSource is null");
-        for (MasterPasswordSource masterPasswordSource : masterPasswordSources.values()) {
-            String masterPassword = masterPasswordSource.handle(masterSource);
-            if (masterPassword != null) return masterPassword;
-        }
-        if (mandatory) {
-            throw new SecDispatcherException("master password could not be fetched");
-        } else {
-            return null;
-        }
-    }
-
-    private String getMasterCipher(SettingsSecurity sec) throws SecDispatcherException {
-        requireNonNull(sec, "configuration is null");
-        return requireNonNull(sec.getMasterCipher(), "masterCipher is null");
-    }
-
-    public String getConfigurationFile() {
-        return configurationFile;
-    }
 }
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/MasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/MasterPasswordSource.java
deleted file mode 100644
index e5704fd..0000000
--- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/MasterPasswordSource.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (c) 2008 Sonatype, Inc. All rights reserved.
- *
- * This program is licensed to you under the Apache License Version 2.0,
- * and you may not use this file except in compliance with the Apache License Version 2.0.
- * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the Apache License Version 2.0 is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
- */
-
-package org.codehaus.plexus.components.secdispatcher.internal;
-
-import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
-
-/**
- * Source of master password.
- */
-public interface MasterPasswordSource {
-    /**
-     * Handles the URI to get master password. Implementation may do one of the following things:
-     * <ul>
-     *     <li>if the URI cannot be handled by given source, return {@code null}</li>
-     *     <li>if master password retrieval was attempted, but failed throw {@link SecDispatcherException}</li>
-     *     <li>happy path: return the master password.</li>
-     * </ul>
-     *
-     * @param masterSource the source of master password, and opaque string.
-     * @return the master password, or {@code null} if implementation does not handle this masterSource
-     * @throws SecDispatcherException If implementation does handle this masterSource, but cannot obtain it
-     */
-    String handle(String masterSource) throws SecDispatcherException;
-}
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java
index 0338683..5d93c19 100644
--- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java
@@ -48,7 +48,7 @@ public final class SecUtil {
     private SecUtil() {}
 
     /**
-     * Reads the configuration model up, optionally resolving relocation too.
+     * Reads the configuration model up, if exists, otherwise returns {@code null}.
      */
     public static SettingsSecurity read(Path configurationFile) throws IOException {
         requireNonNull(configurationFile, "configurationFile must not be null");
@@ -65,6 +65,9 @@ public static SettingsSecurity read(Path configurationFile) throws IOException {
         }
     }
 
+    /**
+     * Returns config with given name, or {@code null} if not exist.
+     */
     public static Map<String, String> getConfig(SettingsSecurity sec, String name) {
         if (sec != null && name != null) {
             List<Config> cl = sec.getConfigurations();
@@ -88,6 +91,14 @@ public static Map<String, String> getConfig(SettingsSecurity sec, String name) {
         return null;
     }
 
+    public static String specVersion() {
+        String specVer = SecDispatcher.class.getPackage().getSpecificationVersion();
+        if (specVer == null) {
+            specVer = "test"; // in UT
+        }
+        return specVer;
+    }
+
     private static final boolean IS_WINDOWS =
             System.getProperty("os.name", "unknown").startsWith("Windows");
 
@@ -99,7 +110,7 @@ public static void write(Path target, SettingsSecurity configuration, boolean do
         Path tempFile = parent.resolve(target.getFileName() + "."
                 + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
 
-        configuration.setModelVersion(SecDispatcher.class.getPackage().getSpecificationVersion());
+        configuration.setModelVersion(specVersion());
         configuration.setModelEncoding(StandardCharsets.UTF_8.name());
 
         try {
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcher.java
new file mode 100644
index 0000000..74daabe
--- /dev/null
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcher.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (c) 2008 Sonatype, Inc. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package org.codehaus.plexus.components.secdispatcher.internal.dispatchers;
+
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathFactory;
+
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.codehaus.plexus.components.cipher.PlexusCipher;
+import org.codehaus.plexus.components.cipher.PlexusCipherException;
+import org.codehaus.plexus.components.secdispatcher.Dispatcher;
+import org.codehaus.plexus.components.secdispatcher.DispatcherMeta;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
+import org.xml.sax.InputSource;
+
+/**
+ * This dispatcher is legacy, serves the purpose of migration only. Should not be used.
+ */
+@Singleton
+@Named(LegacyDispatcher.NAME)
+public class LegacyDispatcher implements Dispatcher, DispatcherMeta {
+    public static final String NAME = "legacy";
+
+    private static final String MASTER_MASTER_PASSWORD = "settings.security";
+
+    private final PlexusCipher plexusCipher;
+    private final LegacyCipher legacyCipher;
+
+    @Inject
+    public LegacyDispatcher(PlexusCipher plexusCipher) {
+        this.plexusCipher = plexusCipher;
+        this.legacyCipher = new LegacyCipher();
+    }
+
+    @Override
+    public boolean isHidden() {
+        return true;
+    }
+
+    @Override
+    public String name() {
+        return NAME;
+    }
+
+    @Override
+    public String displayName() {
+        return "LEGACY (for migration purposes only; can only decrypt)";
+    }
+
+    @Override
+    public Collection<Field> fields() {
+        return List.of();
+    }
+
+    @Override
+    public EncryptPayload encrypt(String str, Map<String, String> attributes, Map<String, String> config)
+            throws SecDispatcherException {
+        throw new SecDispatcherException(
+                NAME + " dispatcher MUST not be used for encryption; is inherently insecure and broken");
+    }
+
+    @Override
+    public String decrypt(String str, Map<String, String> attributes, Map<String, String> config)
+            throws SecDispatcherException {
+        try {
+            String masterPassword = getMasterPassword();
+            if (masterPassword == null) {
+                throw new SecDispatcherException("Master password could not be obtained");
+            }
+            return legacyCipher.decrypt64(str, masterPassword);
+        } catch (PlexusCipherException e) {
+            throw new SecDispatcherException("Decrypt failed", e);
+        }
+    }
+
+    @Override
+    public SecDispatcher.ValidationResponse validateConfiguration(Map<String, String> config) {
+        HashMap<SecDispatcher.ValidationResponse.Level, List<String>> report = new HashMap<>();
+        boolean valid = false;
+        try {
+            String mpe = getMasterMasterPasswordFromSettingsSecurityXml();
+            if (mpe == null) {
+                report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                        .add("Legacy configuration not found or does not contains encrypted master password");
+            } else {
+                report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>())
+                        .add("Legacy configuration found with encrypted master password");
+
+                String mp = getMasterPassword();
+                if (mp == null) {
+                    report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                            .add("Legacy master password not found");
+                } else {
+                    report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>())
+                            .add("Legacy master password successfully decrypted");
+                    valid = true;
+                }
+            }
+        } catch (PlexusCipherException e) {
+            report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                    .add("Legacy master password decryption failed");
+        }
+        return new SecDispatcher.ValidationResponse(getClass().getSimpleName(), valid, report, List.of());
+    }
+
+    private String getMasterPassword() throws SecDispatcherException {
+        String encryptedMasterPassword = getMasterMasterPasswordFromSettingsSecurityXml();
+        if (encryptedMasterPassword == null) {
+            return null;
+        }
+        return legacyCipher.decrypt64(plexusCipher.unDecorate(encryptedMasterPassword), MASTER_MASTER_PASSWORD);
+    }
+
+    private String getMasterMasterPasswordFromSettingsSecurityXml() {
+        Path xml;
+        String override = System.getProperty(MASTER_MASTER_PASSWORD);
+        if (override != null) {
+            xml = Paths.get(override);
+        } else {
+            xml = Paths.get(System.getProperty("user.home"), ".m2", "settings-security.xml");
+        }
+        if (Files.exists(xml)) {
+            try (InputStream is = Files.newInputStream(xml)) {
+                return (String) XPathFactory.newInstance()
+                        .newXPath()
+                        .evaluate("//master", new InputSource(is), XPathConstants.STRING);
+            } catch (Exception e) {
+                // just ignore whatever it is
+            }
+        }
+        return null;
+    }
+
+    private static final class LegacyCipher {
+        private static final String STRING_ENCODING = "UTF8";
+        private static final int SPICE_SIZE = 16;
+        private static final int SALT_SIZE = 8;
+        private static final String DIGEST_ALG = "SHA-256";
+        private static final String KEY_ALG = "AES";
+        private static final String CIPHER_ALG = "AES/CBC/PKCS5Padding";
+
+        private String decrypt64(final String encryptedText, final String password) throws PlexusCipherException {
+            try {
+                byte[] allEncryptedBytes = Base64.getDecoder().decode(encryptedText.getBytes());
+                int totalLen = allEncryptedBytes.length;
+                byte[] salt = new byte[SALT_SIZE];
+                System.arraycopy(allEncryptedBytes, 0, salt, 0, SALT_SIZE);
+                byte padLen = allEncryptedBytes[SALT_SIZE];
+                byte[] encryptedBytes = new byte[totalLen - SALT_SIZE - 1 - padLen];
+                System.arraycopy(allEncryptedBytes, SALT_SIZE + 1, encryptedBytes, 0, encryptedBytes.length);
+                Cipher cipher = createCipher(password.getBytes(STRING_ENCODING), salt, Cipher.DECRYPT_MODE);
+                byte[] clearBytes = cipher.doFinal(encryptedBytes);
+                return new String(clearBytes, STRING_ENCODING);
+            } catch (Exception e) {
+                throw new PlexusCipherException("Error decrypting", e);
+            }
+        }
+
+        private Cipher createCipher(final byte[] pwdAsBytes, byte[] salt, final int mode)
+                throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
+                        InvalidAlgorithmParameterException {
+            MessageDigest _digester = MessageDigest.getInstance(DIGEST_ALG);
+            byte[] keyAndIv = new byte[SPICE_SIZE * 2];
+            if (salt == null || salt.length == 0) {
+                salt = null;
+            }
+            byte[] result;
+            int currentPos = 0;
+            while (currentPos < keyAndIv.length) {
+                _digester.update(pwdAsBytes);
+                if (salt != null) {
+                    _digester.update(salt, 0, 8);
+                }
+                result = _digester.digest();
+                int stillNeed = keyAndIv.length - currentPos;
+                if (result.length > stillNeed) {
+                    byte[] b = new byte[stillNeed];
+                    System.arraycopy(result, 0, b, 0, b.length);
+                    result = b;
+                }
+                System.arraycopy(result, 0, keyAndIv, currentPos, result.length);
+                currentPos += result.length;
+                if (currentPos < keyAndIv.length) {
+                    _digester.reset();
+                    _digester.update(result);
+                }
+            }
+            byte[] key = new byte[SPICE_SIZE];
+            byte[] iv = new byte[SPICE_SIZE];
+            System.arraycopy(keyAndIv, 0, key, 0, key.length);
+            System.arraycopy(keyAndIv, key.length, iv, 0, iv.length);
+            Cipher cipher = Cipher.getInstance(CIPHER_ALG);
+            cipher.init(mode, new SecretKeySpec(key, KEY_ALG), new IvParameterSpec(iv));
+            return cipher;
+        }
+    }
+}
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterDispatcher.java
new file mode 100644
index 0000000..ce1fa46
--- /dev/null
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterDispatcher.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2008 Sonatype, Inc. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package org.codehaus.plexus.components.secdispatcher.internal.dispatchers;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.codehaus.plexus.components.cipher.PlexusCipher;
+import org.codehaus.plexus.components.cipher.PlexusCipherException;
+import org.codehaus.plexus.components.secdispatcher.Dispatcher;
+import org.codehaus.plexus.components.secdispatcher.DispatcherMeta;
+import org.codehaus.plexus.components.secdispatcher.MasterSource;
+import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
+
+/**
+ * This dispatcher is logically equivalent (but much more secure) that Maven3 "master password" encryption.
+ */
+@Singleton
+@Named(MasterDispatcher.NAME)
+public class MasterDispatcher implements Dispatcher, DispatcherMeta {
+    public static final String NAME = "master";
+
+    private static final String CONF_MASTER_CIPHER = "cipher";
+    private static final String CONF_MASTER_SOURCE = "source";
+    /**
+     * Attribute holding the Cipher name used to encrypt the password.
+     */
+    private static final String MASTER_CIPHER_ATTR = CONF_MASTER_CIPHER;
+
+    private final PlexusCipher cipher;
+    protected final Map<String, MasterSource> masterSources;
+
+    @Inject
+    public MasterDispatcher(PlexusCipher cipher, Map<String, MasterSource> masterSources) {
+        this.cipher = cipher;
+        this.masterSources = masterSources;
+    }
+
+    @Override
+    public String name() {
+        return NAME;
+    }
+
+    @Override
+    public String displayName() {
+        return "Master Password Dispatcher";
+    }
+
+    @Override
+    public Collection<Field> fields() {
+        return List.of(
+                Field.builder(CONF_MASTER_SOURCE)
+                        .optional(false)
+                        .description("Source of the master password")
+                        .options(masterSources.entrySet().stream()
+                                .map(e -> {
+                                    MasterSource ms = e.getValue();
+                                    if (ms instanceof MasterSourceMeta m) {
+                                        Field.Builder b =
+                                                Field.builder(e.getKey()).description(m.description());
+                                        if (m.configTemplate().isPresent()) {
+                                            b.defaultValue(m.configTemplate().get());
+                                        }
+                                        return b.build();
+                                    } else {
+                                        return Field.builder(e.getKey())
+                                                .description(e.getKey()
+                                                        + "(Field not described, needs manual configuration)")
+                                                .build();
+                                    }
+                                })
+                                .toList())
+                        .build(),
+                Field.builder(CONF_MASTER_CIPHER)
+                        .optional(false)
+                        .description("Cipher to use with master password")
+                        .options(cipher.availableCiphers().stream()
+                                .map(c -> Field.builder(c).description(c).build())
+                                .toList())
+                        .build());
+    }
+
+    @Override
+    public EncryptPayload encrypt(String str, Map<String, String> attributes, Map<String, String> config)
+            throws SecDispatcherException {
+        try {
+            String masterCipher = getMasterCipher(config, true);
+            String encrypted = cipher.encrypt(masterCipher, str, getMasterPassword(config));
+            HashMap<String, String> attr = new HashMap<>(attributes);
+            attr.put(MASTER_CIPHER_ATTR, masterCipher);
+            return new EncryptPayload(attr, encrypted);
+        } catch (PlexusCipherException e) {
+            throw new SecDispatcherException("Encrypt failed", e);
+        }
+    }
+
+    @Override
+    public String decrypt(String str, Map<String, String> attributes, Map<String, String> config)
+            throws SecDispatcherException {
+        try {
+            String masterCipher = getMasterCipher(attributes, false);
+            return cipher.decrypt(masterCipher, str, getMasterPassword(config));
+        } catch (PlexusCipherException e) {
+            throw new SecDispatcherException("Decrypt failed", e);
+        }
+    }
+
+    @Override
+    public SecDispatcher.ValidationResponse validateConfiguration(Map<String, String> config) {
+        HashMap<SecDispatcher.ValidationResponse.Level, List<String>> report = new HashMap<>();
+        ArrayList<SecDispatcher.ValidationResponse> subsystems = new ArrayList<>();
+        boolean valid = false;
+        String masterCipher = config.get(CONF_MASTER_CIPHER);
+        if (masterCipher == null) {
+            report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                    .add("Cipher configuration missing");
+        } else {
+            if (!cipher.availableCiphers().contains(masterCipher)) {
+                report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                        .add("Configured Cipher not supported");
+            } else {
+                report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>())
+                        .add("Configured Cipher supported");
+            }
+        }
+        String masterSource = config.get(CONF_MASTER_SOURCE);
+        if (masterSource == null) {
+            report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                    .add("Source configuration missing");
+        } else {
+            SecDispatcher.ValidationResponse masterSourceResponse = null;
+            for (MasterSource masterPasswordSource : masterSources.values()) {
+                masterSourceResponse = masterPasswordSource.validateConfiguration(masterSource);
+                if (masterSourceResponse != null) {
+                    break;
+                }
+            }
+            if (masterSourceResponse == null) {
+                report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                        .add("Configured Source configuration not handled");
+            } else {
+                subsystems.add(masterSourceResponse);
+                if (!masterSourceResponse.isValid()) {
+                    report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                            .add("Configured Source configuration invalid");
+                } else {
+                    report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>())
+                            .add("Configured Source configuration valid");
+                    valid = true;
+                }
+            }
+        }
+        return new SecDispatcher.ValidationResponse(getClass().getSimpleName(), valid, report, subsystems);
+    }
+
+    private String getMasterPassword(Map<String, String> config) throws SecDispatcherException {
+        String masterSource = config.get(CONF_MASTER_SOURCE);
+        if (masterSource == null) {
+            throw new SecDispatcherException("Invalid configuration: Missing configuration " + CONF_MASTER_SOURCE);
+        }
+        for (MasterSource masterPasswordSource : masterSources.values()) {
+            String masterPassword = masterPasswordSource.handle(masterSource);
+            if (masterPassword != null) return masterPassword;
+        }
+        throw new SecDispatcherException("No source handled the given masterSource: " + masterSource);
+    }
+
+    private String getMasterCipher(Map<String, String> source, boolean config) throws SecDispatcherException {
+        if (config) {
+            String masterCipher = source.get(CONF_MASTER_CIPHER);
+            if (masterCipher == null) {
+                throw new SecDispatcherException("Invalid configuration: Missing configuration " + CONF_MASTER_CIPHER);
+            }
+            return masterCipher;
+        } else {
+            String masterCipher = source.get(MASTER_CIPHER_ATTR);
+            if (masterCipher == null) {
+                throw new SecDispatcherException("Malformed attributes: Missing attribute " + MASTER_CIPHER_ATTR);
+            }
+            return masterCipher;
+        }
+    }
+}
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterPasswordSource.java
deleted file mode 100644
index ede5ce1..0000000
--- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterPasswordSource.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.codehaus.plexus.components.secdispatcher.internal.sources;
-
-import javax.inject.Named;
-import javax.inject.Singleton;
-
-import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
-
-/**
- * Password source that uses env.
- */
-@Singleton
-@Named(EnvMasterPasswordSource.NAME)
-public final class EnvMasterPasswordSource extends PrefixMasterPasswordSourceSupport {
-    public static final String NAME = "env";
-
-    public EnvMasterPasswordSource() {
-        super(NAME + ":");
-    }
-
-    @Override
-    protected String doHandle(String transformed) throws SecDispatcherException {
-        String value = System.getenv(transformed);
-        if (value == null) {
-            throw new SecDispatcherException("Environment variable '" + transformed + "' not found");
-        }
-        return value;
-    }
-}
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterSource.java
new file mode 100644
index 0000000..431a204
--- /dev/null
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterSource.java
@@ -0,0 +1,86 @@
+/*
+ * 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.codehaus.plexus.components.secdispatcher.internal.sources;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
+
+/**
+ * Password source that uses env.
+ * <p>
+ * Config: {@code env:$ENVIRONMENT_VARIABLE_NAME}
+ */
+@Singleton
+@Named(EnvMasterSource.NAME)
+public final class EnvMasterSource extends PrefixMasterSourceSupport implements MasterSourceMeta {
+    public static final String NAME = "env";
+
+    public EnvMasterSource() {
+        super(NAME + ":");
+    }
+
+    @Override
+    public String description() {
+        return "Environment variable (variable name should be edited)";
+    }
+
+    @Override
+    public Optional<String> configTemplate() {
+        return Optional.of(NAME + ":$VARIABLE_NAME");
+    }
+
+    @Override
+    protected String doHandle(String transformed) throws SecDispatcherException {
+        String value = System.getenv(transformed);
+        if (value == null) {
+            throw new SecDispatcherException("Environment variable '" + transformed + "' not found");
+        }
+        return value;
+    }
+
+    @Override
+    protected SecDispatcher.ValidationResponse doValidateConfiguration(String transformed) {
+        String value = System.getenv(transformed);
+        if (value == null) {
+            return new SecDispatcher.ValidationResponse(
+                    getClass().getSimpleName(),
+                    true,
+                    Map.of(
+                            SecDispatcher.ValidationResponse.Level.WARNING,
+                            List.of("Configured environment variable not exist")),
+                    List.of());
+        } else {
+            return new SecDispatcher.ValidationResponse(
+                    getClass().getSimpleName(),
+                    true,
+                    Map.of(
+                            SecDispatcher.ValidationResponse.Level.INFO,
+                            List.of("Configured environment variable exist")),
+                    List.of());
+        }
+    }
+}
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterSource.java
similarity index 66%
rename from src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterPasswordSource.java
rename to src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterSource.java
index afe2ffa..9a0b486 100644
--- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterPasswordSource.java
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterSource.java
@@ -29,24 +29,43 @@
 import java.net.UnixDomainSocketAddress;
 import java.nio.channels.Channels;
 import java.nio.channels.SocketChannel;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HexFormat;
+import java.util.List;
+import java.util.Optional;
 
+import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
 
 /**
  * Password source that uses GnuPG Agent.
+ * <p>
+ * Config: {@code gpg-agent:$agentSocketPath[?non-interactive]}
  */
 @Singleton
-@Named(GpgAgentMasterPasswordSource.NAME)
-public final class GpgAgentMasterPasswordSource extends PrefixMasterPasswordSourceSupport {
+@Named(GpgAgentMasterSource.NAME)
+public final class GpgAgentMasterSource extends PrefixMasterSourceSupport implements MasterSourceMeta {
     public static final String NAME = "gpg-agent";
 
-    public GpgAgentMasterPasswordSource() {
+    public GpgAgentMasterSource() {
         super(NAME + ":");
     }
 
+    @Override
+    public String description() {
+        return "GPG Agent (agent socket path should be edited)";
+    }
+
+    @Override
+    public Optional<String> configTemplate() {
+        return Optional.of(NAME + ":$agentSocketPath");
+    }
+
     @Override
     protected String doHandle(String transformed) throws SecDispatcherException {
         String extra = "";
@@ -69,6 +88,39 @@ protected String doHandle(String transformed) throws SecDispatcherException {
         }
     }
 
+    @Override
+    protected SecDispatcher.ValidationResponse doValidateConfiguration(String transformed) {
+        HashMap<SecDispatcher.ValidationResponse.Level, List<String>> report = new HashMap<>();
+        boolean valid = false;
+
+        String extra = "";
+        if (transformed.contains("?")) {
+            extra = transformed.substring(transformed.indexOf("?"));
+            transformed = transformed.substring(0, transformed.indexOf("?"));
+        }
+        Path socketLocation = Paths.get(transformed);
+        if (!socketLocation.isAbsolute()) {
+            socketLocation = Paths.get(System.getProperty("user.home"))
+                    .resolve(socketLocation)
+                    .toAbsolutePath();
+        }
+        if (Files.exists(socketLocation)) {
+            report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                    .add("Unix domain socket for GPG Agent does not exist. Maybe you need to start gpg-agent?");
+        } else {
+            report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>())
+                    .add("Unix domain socket for GPG Agent exist");
+            valid = true;
+        }
+        boolean interactive = !extra.contains("non-interactive");
+        if (!interactive) {
+            report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.WARNING, k -> new ArrayList<>())
+                    .add(
+                            "Non-interactive flag found, gpg-agent will not ask for passphrase, it can use only cached ones");
+        }
+        return new SecDispatcher.ValidationResponse(getClass().getSimpleName(), valid, report, List.of());
+    }
+
     private String load(Path socketPath, boolean interactive) throws IOException {
         try (SocketChannel sock = SocketChannel.open(StandardProtocolFamily.UNIX)) {
             sock.connect(UnixDomainSocketAddress.of(socketPath));
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterPasswordSourceSupport.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterSourceSupport.java
similarity index 72%
rename from src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterPasswordSourceSupport.java
rename to src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterSourceSupport.java
index 7b19876..56be402 100644
--- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterPasswordSourceSupport.java
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterSourceSupport.java
@@ -21,19 +21,20 @@
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import org.codehaus.plexus.components.secdispatcher.MasterSource;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
-import org.codehaus.plexus.components.secdispatcher.internal.MasterPasswordSource;
 
 import static java.util.Objects.requireNonNull;
 
 /**
  * Master password source support class.
  */
-public abstract class MasterPasswordSourceSupport implements MasterPasswordSource {
+public abstract class MasterSourceSupport implements MasterSource {
     private final Predicate<String> matcher;
     private final Function<String, String> transformer;
 
-    public MasterPasswordSourceSupport(Predicate<String> matcher, Function<String, String> transformer) {
+    public MasterSourceSupport(Predicate<String> matcher, Function<String, String> transformer) {
         this.matcher = requireNonNull(matcher);
         this.transformer = requireNonNull(transformer);
     }
@@ -47,4 +48,13 @@ public String handle(String masterSource) throws SecDispatcherException {
     }
 
     protected abstract String doHandle(String transformed) throws SecDispatcherException;
+
+    public SecDispatcher.ValidationResponse validateConfiguration(String masterSource) {
+        if (matcher.test(masterSource)) {
+            return doValidateConfiguration(transformer.apply(masterSource));
+        }
+        return null;
+    }
+
+    protected abstract SecDispatcher.ValidationResponse doValidateConfiguration(String transformed);
 }
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PinEntryMasterSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PinEntryMasterSource.java
new file mode 100644
index 0000000..b0f6c0e
--- /dev/null
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PinEntryMasterSource.java
@@ -0,0 +1,109 @@
+/*
+ * 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.codehaus.plexus.components.secdispatcher.internal.sources;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+
+import org.codehaus.plexus.components.secdispatcher.MasterSource;
+import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta;
+import org.codehaus.plexus.components.secdispatcher.PinEntry;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
+
+/**
+ * Master source using {@link PinEntry}
+ */
+@Singleton
+@Named(PinEntryMasterSource.NAME)
+public class PinEntryMasterSource extends PrefixMasterSourceSupport implements MasterSource, MasterSourceMeta {
+    public static final String NAME = "pinentry-prompt";
+
+    public PinEntryMasterSource() {
+        super(NAME + ":");
+    }
+
+    @Override
+    public String description() {
+        return "Secure PinEntry prompt (pinentry path should be edited)";
+    }
+
+    @Override
+    public Optional<String> configTemplate() {
+        return Optional.of(NAME + ":$pinentryPath");
+    }
+
+    @Override
+    public String doHandle(String s) throws SecDispatcherException {
+        try {
+            PinEntry.Result result = new PinEntry(s)
+                    .setTimeout(Duration.ofSeconds(30))
+                    .setKeyInfo("Maven: n/masterPassword")
+                    .setTitle("Maven Master Password")
+                    .setDescription("Please enter the Maven master password")
+                    .setPrompt("Maven master password")
+                    .setOk("Ok")
+                    .setCancel("Cancel")
+                    .getPin();
+            if (result.outcome() == PinEntry.Outcome.SUCCESS) {
+                return result.payload();
+            } else if (result.outcome() == PinEntry.Outcome.CANCELED) {
+                throw new SecDispatcherException("User canceled the operation");
+            } else if (result.outcome() == PinEntry.Outcome.TIMEOUT) {
+                throw new SecDispatcherException("Timeout");
+            } else {
+                throw new SecDispatcherException("Failure: " + result.payload());
+            }
+        } catch (IOException e) {
+            throw new SecDispatcherException("Could not collect the password", e);
+        }
+    }
+
+    @Override
+    protected SecDispatcher.ValidationResponse doValidateConfiguration(String transformed) {
+        HashMap<SecDispatcher.ValidationResponse.Level, List<String>> report = new HashMap<>();
+        boolean valid = false;
+
+        Path pinentry = Paths.get(transformed);
+        if (!Files.exists(pinentry)) {
+            report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                    .add("Configured pinentry command not found");
+        } else {
+            if (!Files.isExecutable(pinentry)) {
+                report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>())
+                        .add("Configured pinentry command is not executable");
+            } else {
+                report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>())
+                        .add("Configured pinentry command exists and is executable");
+                valid = true;
+            }
+        }
+        return new SecDispatcher.ValidationResponse(getClass().getSimpleName(), valid, report, List.of());
+    }
+}
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterPasswordSourceSupport.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterSourceSupport.java
similarity index 90%
rename from src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterPasswordSourceSupport.java
rename to src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterSourceSupport.java
index 3d2d6b3..926ce87 100644
--- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterPasswordSourceSupport.java
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterSourceSupport.java
@@ -26,8 +26,8 @@
 /**
  * Master password source support class for simple "prefix" use case.
  */
-public abstract class PrefixMasterPasswordSourceSupport extends MasterPasswordSourceSupport {
-    public PrefixMasterPasswordSourceSupport(String prefix) {
+public abstract class PrefixMasterSourceSupport extends MasterSourceSupport {
+    public PrefixMasterSourceSupport(String prefix) {
         super(prefixMatcher(prefix), prefixRemover(prefix));
     }
 
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterPasswordSource.java
deleted file mode 100644
index 58b08b8..0000000
--- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterPasswordSource.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.codehaus.plexus.components.secdispatcher.internal.sources;
-
-import javax.inject.Named;
-import javax.inject.Singleton;
-
-import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
-
-/**
- * Password source that uses env.
- */
-@Singleton
-@Named(SystemPropertyMasterPasswordSource.NAME)
-public final class SystemPropertyMasterPasswordSource extends PrefixMasterPasswordSourceSupport {
-    public static final String NAME = "prop";
-
-    public SystemPropertyMasterPasswordSource() {
-        super(NAME + ":");
-    }
-
-    @Override
-    protected String doHandle(String transformed) throws SecDispatcherException {
-        String value = System.getProperty(transformed);
-        if (value == null) {
-            throw new SecDispatcherException("System property '" + transformed + "' not found");
-        }
-        return value;
-    }
-}
diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterSource.java
new file mode 100644
index 0000000..9644bb4
--- /dev/null
+++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterSource.java
@@ -0,0 +1,86 @@
+/*
+ * 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.codehaus.plexus.components.secdispatcher.internal.sources;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
+
+/**
+ * Password source that uses env.
+ * <p>
+ * Config: {@code system-property:$systemPropertyName}
+ */
+@Singleton
+@Named(SystemPropertyMasterSource.NAME)
+public final class SystemPropertyMasterSource extends PrefixMasterSourceSupport implements MasterSourceMeta {
+    public static final String NAME = "system-property";
+
+    public SystemPropertyMasterSource() {
+        super(NAME + ":");
+    }
+
+    @Override
+    public String description() {
+        return "Java System properties (property name should be edited)";
+    }
+
+    @Override
+    public Optional<String> configTemplate() {
+        return Optional.of(NAME + ":$systemProperty");
+    }
+
+    @Override
+    protected String doHandle(String transformed) throws SecDispatcherException {
+        String value = System.getProperty(transformed);
+        if (value == null) {
+            throw new SecDispatcherException("System property '" + transformed + "' not found");
+        }
+        return value;
+    }
+
+    @Override
+    protected SecDispatcher.ValidationResponse doValidateConfiguration(String transformed) {
+        String value = System.getProperty(transformed);
+        if (value == null) {
+            return new SecDispatcher.ValidationResponse(
+                    getClass().getSimpleName(),
+                    true,
+                    Map.of(
+                            SecDispatcher.ValidationResponse.Level.WARNING,
+                            List.of("Configured Java System Property not exist")),
+                    List.of());
+        } else {
+            return new SecDispatcher.ValidationResponse(
+                    getClass().getSimpleName(),
+                    true,
+                    Map.of(
+                            SecDispatcher.ValidationResponse.Level.INFO,
+                            List.of("Configured Java System Property exist")),
+                    List.of());
+        }
+    }
+}
diff --git a/src/main/mdo/settings-security.mdo b/src/main/mdo/settings-security.mdo
index 336c179..f16b58a 100644
--- a/src/main/mdo/settings-security.mdo
+++ b/src/main/mdo/settings-security.mdo
@@ -56,18 +56,25 @@
         </field>
         <field>
           <name>masterSource</name>
-          <version>3.0.0+</version>
+          <version>3.0.0/3.0.0</version>
           <type>String</type>
           <required>true</required>
           <description>The masterSource describes the source of the master password</description>
         </field>
         <field>
           <name>masterCipher</name>
-          <version>3.0.0+</version>
+          <version>3.0.0/3.0.0</version>
           <type>String</type>
           <required>true</required>
           <description>The Cipher to be used for master password</description>
         </field>
+        <field>
+          <name>defaultDispatcher</name>
+          <version>4.0.0+</version>
+          <type>String</type>
+          <required>true</required>
+          <description>The default dispatcher to be used when no dispatcher name provided</description>
+        </field>
         <field>
           <name>configurations</name>
           <version>1.0.0+</version>
diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java
index 5ecb58d..d1e947b 100644
--- a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java
+++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java
@@ -16,205 +16,132 @@
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.Base64;
 import java.util.Map;
-import java.util.Set;
 
 import org.codehaus.plexus.components.cipher.internal.AESGCMNoPadding;
 import org.codehaus.plexus.components.cipher.internal.DefaultPlexusCipher;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
-import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
-import org.codehaus.plexus.components.secdispatcher.internal.dispatcher.StaticDispatcher;
-import org.codehaus.plexus.components.secdispatcher.internal.sources.EnvMasterPasswordSource;
-import org.codehaus.plexus.components.secdispatcher.internal.sources.GpgAgentMasterPasswordSource;
-import org.codehaus.plexus.components.secdispatcher.internal.sources.StaticMasterPasswordSource;
-import org.codehaus.plexus.components.secdispatcher.internal.sources.SystemPropertyMasterPasswordSource;
+import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher;
+import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.MasterDispatcher;
+import org.codehaus.plexus.components.secdispatcher.internal.sources.EnvMasterSource;
+import org.codehaus.plexus.components.secdispatcher.internal.sources.GpgAgentMasterSource;
+import org.codehaus.plexus.components.secdispatcher.internal.sources.SystemPropertyMasterSource;
+import org.codehaus.plexus.components.secdispatcher.model.Config;
+import org.codehaus.plexus.components.secdispatcher.model.ConfigProperty;
 import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity;
 import org.codehaus.plexus.components.secdispatcher.model.io.stax.SecurityConfigurationStaxWriter;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class DefaultSecDispatcherTest {
-    String masterPassword = "masterPw";
-    String password = "somePassword";
+    private final Path CONFIG_PATH = Paths.get("./target/sec.xml");
 
-    private void saveSec(String masterSource) throws Exception {
+    private void saveSec(String dispatcher, Map<String, String> config) throws Exception {
         SettingsSecurity sec = new SettingsSecurity();
         sec.setModelEncoding(StandardCharsets.UTF_8.name());
         sec.setModelVersion(SecDispatcher.class.getPackage().getSpecificationVersion());
-        sec.setMasterSource(masterSource);
-        sec.setMasterCipher(AESGCMNoPadding.CIPHER_ALG);
-
-        try (OutputStream fos = Files.newOutputStream(Paths.get("./target/sec.xml"))) {
-            new SecurityConfigurationStaxWriter().write(fos, sec);
+        sec.setDefaultDispatcher(dispatcher);
+        Config conf = new Config();
+        conf.setName(dispatcher);
+        for (Map.Entry<String, String> entry : config.entrySet()) {
+            ConfigProperty prop = new ConfigProperty();
+            prop.setName(entry.getKey());
+            prop.setValue(entry.getValue());
+            conf.addProperty(prop);
         }
-        System.setProperty(DefaultSecDispatcher.SYSTEM_PROPERTY_CONFIGURATION_LOCATION, "./target/sec.xml");
+        sec.getConfigurations().add(conf);
+        saveSec(sec);
     }
 
-    @BeforeEach
-    public void prepare() throws Exception {
-        saveSec("magic:might");
-    }
+    private void saveSec(SettingsSecurity sec) throws Exception {
+        sec.setModelEncoding(StandardCharsets.UTF_8.name());
+        sec.setModelVersion(SecDispatcher.class.getPackage().getSpecificationVersion());
 
-    @Test
-    void testEncrypt() throws Exception {
-        DefaultSecDispatcher sd = new DefaultSecDispatcher(
-                new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())),
-                Map.of("static", new StaticMasterPasswordSource(masterPassword)),
-                Map.of(),
-                DefaultSecDispatcher.DEFAULT_CONFIGURATION);
-        String enc = sd.encrypt(password, null);
-        assertNotNull(enc);
-        String password1 = sd.decrypt(enc);
-        assertEquals(password, password1);
+        try (OutputStream fos = Files.newOutputStream(CONFIG_PATH)) {
+            new SecurityConfigurationStaxWriter().write(fos, sec);
+        }
     }
 
     @Test
-    void testDecrypt() throws Exception {
-        DefaultSecDispatcher sd = new DefaultSecDispatcher(
-                new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())),
-                Map.of("static", new StaticMasterPasswordSource(masterPassword)),
-                Map.of(),
-                DefaultSecDispatcher.DEFAULT_CONFIGURATION);
-        String encrypted = sd.encrypt(password, null);
-        String pass = sd.decrypt(encrypted);
-        assertNotNull(pass);
-        assertEquals(password, pass);
+    void masterWithEnvRoundTrip() throws Exception {
+        saveSec("master", Map.of("source", "env:MASTER_PASSWORD", "cipher", AESGCMNoPadding.CIPHER_ALG));
+        roundtrip();
     }
 
     @Test
-    void testDecryptSystemProperty() throws Exception {
-        System.setProperty("foobar", masterPassword);
-        saveSec("prop:foobar");
-        DefaultSecDispatcher sd = new DefaultSecDispatcher(
-                new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())),
-                Map.of(
-                        "prop",
-                        new SystemPropertyMasterPasswordSource(),
-                        "env",
-                        new EnvMasterPasswordSource(),
-                        "gpg",
-                        new GpgAgentMasterPasswordSource()),
-                Map.of(),
-                DefaultSecDispatcher.DEFAULT_CONFIGURATION);
-        String encrypted = sd.encrypt(password, null);
-        String pass = sd.decrypt(encrypted);
-        assertNotNull(pass);
-        assertEquals(password, pass);
+    void masterWithSystemPropertyRoundTrip() throws Exception {
+        saveSec("master", Map.of("source", "system-property:masterPassword", "cipher", AESGCMNoPadding.CIPHER_ALG));
+        roundtrip();
     }
 
     @Test
-    void testDecryptEnv() throws Exception {
-        saveSec("env:MASTER_PASSWORD");
-        DefaultSecDispatcher sd = new DefaultSecDispatcher(
-                new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())),
-                Map.of(
-                        "prop",
-                        new SystemPropertyMasterPasswordSource(),
-                        "env",
-                        new EnvMasterPasswordSource(),
-                        "gpg",
-                        new GpgAgentMasterPasswordSource()),
-                Map.of(),
-                DefaultSecDispatcher.DEFAULT_CONFIGURATION);
-        String encrypted = sd.encrypt(password, null);
-        String pass = sd.decrypt(encrypted);
-        assertNotNull(pass);
-        assertEquals(password, pass);
+    void validate() throws Exception {
+        saveSec("master", Map.of("source", "system-property:masterPassword", "cipher", AESGCMNoPadding.CIPHER_ALG));
+        System.setProperty("settings.security", "src/test/legacy/legacy-settings-security-1.xml");
+
+        SecDispatcher secDispatcher = construct();
+        SecDispatcher.ValidationResponse response = secDispatcher.validateConfiguration();
+        assertTrue(response.isValid());
+        // secDispatcher
+        assertEquals(1, response.getReport().size());
+        assertEquals(2, response.getSubsystems().size());
+        // master dispatcher
+        assertEquals(1, response.getSubsystems().get(0).getReport().size());
+        assertEquals(1, response.getSubsystems().get(0).getSubsystems().size());
+        // master source
+        assertTrue(response.getSubsystems()
+                        .get(0)
+                        .getSubsystems()
+                        .get(0)
+                        .getReport()
+                        .size()
+                == 1);
+        assertTrue(response.getSubsystems()
+                        .get(0)
+                        .getSubsystems()
+                        .get(0)
+                        .getSubsystems()
+                        .size()
+                == 0);
     }
 
-    @Disabled("triggers GPG agent: remove this and type in 'masterPw'")
-    @Test
-    void testDecryptGpg() throws Exception {
-        saveSec("gpg-agent:/run/user/1000/gnupg/S.gpg-agent");
-        DefaultSecDispatcher sd = new DefaultSecDispatcher(
-                new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())),
-                Map.of(
-                        "prop",
-                        new SystemPropertyMasterPasswordSource(),
-                        "env",
-                        new EnvMasterPasswordSource(),
-                        "gpg",
-                        new GpgAgentMasterPasswordSource()),
-                Map.of(),
-                DefaultSecDispatcher.DEFAULT_CONFIGURATION);
-        String encrypted = sd.encrypt(password, null);
+    protected void roundtrip() throws Exception {
+        DefaultSecDispatcher sd = construct();
+
+        assertEquals(2, sd.availableDispatchers().size());
+        String encrypted = sd.encrypt("supersecret", Map.of(SecDispatcher.DISPATCHER_NAME_ATTR, "master", "a", "b"));
+        // example:
+        // {[name=master,cipher=AES/GCM/NoPadding,a=b]vvq66pZ7rkvzSPStGTI9q4QDnsmuDwo+LtjraRel2b0XpcGJFdXcYAHAS75HUA6GLpcVtEkmyQ==}
+        assertTrue(encrypted.startsWith("{") && encrypted.endsWith("}"));
+        assertTrue(encrypted.contains("name=master"));
+        assertTrue(encrypted.contains("cipher=" + AESGCMNoPadding.CIPHER_ALG));
+        assertTrue(encrypted.contains("version=test"));
+        assertTrue(encrypted.contains("a=b"));
         String pass = sd.decrypt(encrypted);
-        assertNotNull(pass);
-        assertEquals(password, pass);
+        assertEquals("supersecret", pass);
     }
 
-    @Test
-    void testEncryptWithDispatcher() throws Exception {
-        DefaultSecDispatcher sd = new DefaultSecDispatcher(
-                new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())),
-                Map.of("static", new StaticMasterPasswordSource(masterPassword)),
-                Map.of("magic", new StaticDispatcher("decrypted", "encrypted")),
-                DefaultSecDispatcher.DEFAULT_CONFIGURATION);
-
-        assertEquals(Set.of("magic"), sd.availableDispatchers());
-        String enc = sd.encrypt("whatever", Map.of(SecDispatcher.DISPATCHER_NAME_ATTR, "magic", "a", "b"));
-        assertNotNull(enc);
-        assertTrue(enc.contains("encrypted"));
-        assertTrue(enc.contains(SecDispatcher.DISPATCHER_NAME_ATTR + "=magic"));
-        String password1 = sd.decrypt(enc);
-        assertEquals("decrypted", password1);
-    }
-
-    @Test
-    void testDecryptWithDispatcher() throws Exception {
-        DefaultSecDispatcher sd = new DefaultSecDispatcher(
-                new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())),
-                Map.of("static", new StaticMasterPasswordSource(masterPassword)),
-                Map.of("magic", new StaticDispatcher("decrypted", "encrypted")),
-                DefaultSecDispatcher.DEFAULT_CONFIGURATION);
-
-        assertEquals(Set.of("magic"), sd.availableDispatchers());
-        String pass = sd.decrypt("{" + "[a=b," + SecDispatcher.DISPATCHER_NAME_ATTR + "=magic]"
-                + Base64.getEncoder().encodeToString("whatever".getBytes(StandardCharsets.UTF_8)) + "}");
-        assertNotNull(pass);
-        assertEquals("decrypted", pass);
-    }
-
-    @Test
-    void testDecryptWithDispatcherConf() throws Exception {
-        String bare = Base64.getEncoder().encodeToString("whatever".getBytes(StandardCharsets.UTF_8));
-        DefaultSecDispatcher sd = new DefaultSecDispatcher(
-                new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())),
-                Map.of("static", new StaticMasterPasswordSource(masterPassword)),
-                Map.of("magic", new Dispatcher() {
-                    @Override
-                    public String encrypt(String str, Map<String, String> attributes, Map<String, String> config)
-                            throws SecDispatcherException {
-                        throw new IllegalStateException("should not be called");
-                    }
-
-                    @Override
-                    public String decrypt(String str, Map<String, String> attributes, Map<String, String> config)
-                            throws SecDispatcherException {
-                        assertEquals(bare, str);
-                        assertEquals(2, attributes.size());
-                        assertEquals("magic", attributes.get(SecDispatcher.DISPATCHER_NAME_ATTR));
-                        assertEquals("value", attributes.get("key"));
-
-                        assertEquals(1, config.size());
-                        assertEquals(masterPassword, config.get(Dispatcher.CONF_MASTER_PASSWORD));
-
-                        return "magic";
-                    }
-                }),
-                DefaultSecDispatcher.DEFAULT_CONFIGURATION);
-
-        assertEquals(Set.of("magic"), sd.availableDispatchers());
-        String pass = sd.decrypt("{" + "[key=value," + SecDispatcher.DISPATCHER_NAME_ATTR + "=magic]"
-                + Base64.getEncoder().encodeToString("whatever".getBytes(StandardCharsets.UTF_8)) + "}");
-        assertNotNull(pass);
-        assertEquals("magic", pass);
+    protected DefaultSecDispatcher construct() {
+        DefaultPlexusCipher dpc = new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding()));
+        return new DefaultSecDispatcher(
+                dpc,
+                Map.of(
+                        "master",
+                        new MasterDispatcher(
+                                dpc,
+                                Map.of(
+                                        EnvMasterSource.NAME,
+                                        new EnvMasterSource(),
+                                        SystemPropertyMasterSource.NAME,
+                                        new SystemPropertyMasterSource(),
+                                        GpgAgentMasterSource.NAME,
+                                        new GpgAgentMasterSource())),
+                        "legacy",
+                        new LegacyDispatcher(dpc)),
+                CONFIG_PATH);
     }
 }
diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java
index f98dccb..7773ad9 100644
--- a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java
+++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java
@@ -41,13 +41,13 @@ public class SecUtilTest {
     String _propName = "pname";
     String _propVal = "pval";
 
-    private void saveSec(String masterSource) throws IOException {
-        saveSec("./target/sec.xml", masterSource);
+    private void saveSec(String defaultDispatcher) throws IOException {
+        saveSec("./target/sec.xml", defaultDispatcher);
     }
 
-    private void saveSec(String path, String masterSource) throws IOException {
+    private void saveSec(String path, String defaultDispatcher) throws IOException {
         SettingsSecurity sec = new SettingsSecurity();
-        sec.setMasterSource(masterSource);
+        sec.setDefaultDispatcher(defaultDispatcher);
         ConfigProperty cp = new ConfigProperty();
         cp.setName(_propName);
         cp.setValue(_propVal);
@@ -68,9 +68,9 @@ void readWrite() throws IOException {
         Path path = Path.of("./target/sec.xml");
         SettingsSecurity config = SecUtil.read(path);
         assertNotNull(config);
-        assertEquals(SettingsSecurity.class.getPackage().getSpecificationVersion(), config.getModelVersion());
+        assertEquals(SecUtil.specVersion(), config.getModelVersion());
         assertEquals(StandardCharsets.UTF_8.name(), config.getModelEncoding());
-        assertEquals("magic:mighty", config.getMasterSource());
+        assertEquals("magic:mighty", config.getDefaultDispatcher());
         SecUtil.write(path, config, false);
     }
 
@@ -79,9 +79,9 @@ void readWriteWithBackup() throws IOException {
         Path path = Path.of("./target/sec.xml");
         SettingsSecurity config = SecUtil.read(path);
         assertNotNull(config);
-        assertEquals(SettingsSecurity.class.getPackage().getSpecificationVersion(), config.getModelVersion());
+        assertEquals(SecUtil.specVersion(), config.getModelVersion());
         assertEquals(StandardCharsets.UTF_8.name(), config.getModelEncoding());
-        assertEquals("magic:mighty", config.getMasterSource());
+        assertEquals("magic:mighty", config.getDefaultDispatcher());
         SecUtil.write(path, config, true);
         assertTrue(Files.exists(path));
         assertTrue(Files.exists(path.getParent().resolve(path.getFileName() + ".bak")));
diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatcher/StaticDispatcher.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatcher/StaticDispatcher.java
deleted file mode 100644
index 4088212..0000000
--- a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatcher/StaticDispatcher.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (c) 2008 Sonatype, Inc. All rights reserved.
- *
- * This program is licensed to you under the Apache License Version 2.0,
- * and you may not use this file except in compliance with the Apache License Version 2.0.
- * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the Apache License Version 2.0 is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
- */
-
-package org.codehaus.plexus.components.secdispatcher.internal.dispatcher;
-
-import java.util.Map;
-
-import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
-import org.codehaus.plexus.components.secdispatcher.internal.Dispatcher;
-
-import static java.util.Objects.requireNonNull;
-
-public class StaticDispatcher implements Dispatcher {
-    private final String decrypted;
-    private final String encrypted;
-
-    public StaticDispatcher(String decrypted, String encrypted) {
-        this.decrypted = requireNonNull(decrypted);
-        this.encrypted = requireNonNull(encrypted);
-    }
-
-    @Override
-    public String encrypt(String str, Map<String, String> attributes, Map<String, String> config)
-            throws SecDispatcherException {
-        return encrypted;
-    }
-
-    @Override
-    public String decrypt(String str, Map<String, String> attributes, Map<String, String> config)
-            throws SecDispatcherException {
-        return decrypted;
-    }
-}
diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcherTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcherTest.java
new file mode 100644
index 0000000..f02e758
--- /dev/null
+++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcherTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2008 Sonatype, Inc. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package org.codehaus.plexus.components.secdispatcher.internal.dispatchers;
+
+import java.util.Map;
+
+import org.codehaus.plexus.components.cipher.internal.DefaultPlexusCipher;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class LegacyDispatcherTest {
+    /**
+     * Test values created with Maven 3.9.9.
+     * <p>
+     * master password: "masterpassword"
+     * password: "password"
+     */
+    @ParameterizedTest
+    @ValueSource(
+            strings = {
+                "src/test/legacy/legacy-settings-security-1.xml",
+                "src/test/legacy/legacy-settings-security-2.xml"
+            })
+    void smoke(String xml) {
+        System.setProperty("settings.security", xml);
+        LegacyDispatcher legacyDispatcher = new LegacyDispatcher(new DefaultPlexusCipher(Map.of()));
+        // SecDispatcher "un decorates" the PW
+        String cleartext = legacyDispatcher.decrypt("L6L/HbmrY+cH+sNkphnq3fguYepTpM04WlIXb8nB1pk=", Map.of(), Map.of());
+        assertEquals("password", cleartext);
+    }
+}
diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SourcesTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SourcesTest.java
new file mode 100644
index 0000000..76bdde5
--- /dev/null
+++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SourcesTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2008 Sonatype, Inc. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package org.codehaus.plexus.components.secdispatcher.internal.sources;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * surefire plugin set system property and env.
+ */
+public class SourcesTest {
+    @Test
+    void systemProperty() {
+        SystemPropertyMasterSource source = new SystemPropertyMasterSource();
+        assertEquals("masterPw", source.handle("system-property:masterPassword"));
+    }
+
+    @Test
+    void env() {
+        EnvMasterSource source = new EnvMasterSource();
+        assertEquals("masterPw", source.handle("env:MASTER_PASSWORD"));
+    }
+
+    @Disabled("enable and type in 'masterPw'")
+    @Test
+    void gpgAgent() {
+        GpgAgentMasterSource source = new GpgAgentMasterSource();
+        // you may adjust path, this is Fedora40 WS. Ubuntu does `.gpg/S.gpg-agent`
+        assertEquals("masterPw", source.handle("gpg-agent:/run/user/1000/gnupg/S.gpg-agent"));
+    }
+
+    @Disabled("enable and type in 'masterPw'")
+    @Test
+    void pinEntry() {
+        PinEntryMasterSource source = new PinEntryMasterSource();
+        // ypu may adjust path, this is Fedora40 WS + gnome
+        assertEquals("masterPw", source.handle("pinentry-prompt:/usr/bin/pinentry-gnome3"));
+    }
+}
diff --git a/src/test/legacy/legacy-settings-security-1.xml b/src/test/legacy/legacy-settings-security-1.xml
new file mode 100644
index 0000000..eb4ff1e
--- /dev/null
+++ b/src/test/legacy/legacy-settings-security-1.xml
@@ -0,0 +1,3 @@
+<settingsSecurity>
+    <master>{KDvsYOFLlXgH4LU8tvpzAGg5otiosZXvfdQq0yO86LU=}</master>
+</settingsSecurity>
diff --git a/src/test/legacy/legacy-settings-security-2.xml b/src/test/legacy/legacy-settings-security-2.xml
new file mode 100644
index 0000000..0f7b33d
--- /dev/null
+++ b/src/test/legacy/legacy-settings-security-2.xml
@@ -0,0 +1,4 @@
+<settingsSecurity>
+    <relocation>to the moon</relocation>
+    <master>{KDvsYOFLlXgH4LU8tvpzAGg5otiosZXvfdQq0yO86LU=}</master>
+</settingsSecurity>