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

cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new f5e54ca6fa [MNG-8285] Implement mvnenc CLI tool (#1793)
f5e54ca6fa is described below

commit f5e54ca6faa5d3025beb202b59640947e42fc336
Author: Tamas Cservenak <[email protected]>
AuthorDate: Mon Oct 14 21:57:22 2024 +0200

    [MNG-8285] Implement mvnenc CLI tool (#1793)
    
    Implements the `mvnenc` tool that is on par with Maven3 master password 
encryption functionality wise, but is _really secure_ unlike Maven3 conterpart. 
On the other hand, _is backward compatible if legacy config is setup_.
    
    Implemented goals: `init`, `encrypt`, `decrypt`, `diag`. Also provides one 
extra "master source" based on Maven infra Prompter: console master password 
prompt.
    
    ---
    
    https://issues.apache.org/jira/browse/MNG-8285
---
 apache-maven/src/assembly/component.xml            |   1 +
 apache-maven/src/assembly/maven/bin/mvnencDebug    |  35 +++
 .../src/assembly/maven/bin/mvnencDebug.cmd         |  44 ++++
 .../maven/api/cli/mvnenc/EncryptOptions.java       |  22 +-
 maven-cli/pom.xml                                  |   4 +
 .../java/org/apache/maven/cling/ClingSupport.java  |  11 +-
 .../java/org/apache/maven/cling/MavenCling.java    |  12 +
 .../java/org/apache/maven/cling/MavenEncCling.java |  23 +-
 .../invoker/mvnenc/CommonsCliEncryptOptions.java   |  41 ++-
 .../invoker/mvnenc/ConsolePasswordPrompt.java      |  80 ++++++
 .../invoker/mvnenc/DefaultEncryptInvoker.java      | 118 ++++++++-
 .../apache/maven/cling/invoker/mvnenc/Goal.java    |  26 ++
 .../mvnenc/goals/ConfiguredGoalSupport.java        |  94 +++++++
 .../maven/cling/invoker/mvnenc/goals/Decrypt.java  |  54 ++++
 .../maven/cling/invoker/mvnenc/goals/Diag.java     |  47 ++++
 .../maven/cling/invoker/mvnenc/goals/Encrypt.java  |  48 ++++
 .../cling/invoker/mvnenc/goals/GoalSupport.java    |  45 ++++
 .../maven/cling/invoker/mvnenc/goals/Init.java     | 275 +++++++++++++++++++++
 .../DefaultRepositorySystemSessionFactory.java     |  13 +-
 maven-embedder/pom.xml                             |   4 -
 maven-jline/pom.xml                                |  12 +
 maven-settings-builder/pom.xml                     |   4 -
 .../settings/crypto/DefaultSettingsDecrypter.java  | 101 +++++---
 .../maven/settings/crypto/MavenSecDispatcher.java  |  58 +++++
 pom.xml                                            |  23 +-
 25 files changed, 1079 insertions(+), 116 deletions(-)

diff --git a/apache-maven/src/assembly/component.xml 
b/apache-maven/src/assembly/component.xml
index 347dee1d5c..9a6d61dc5c 100644
--- a/apache-maven/src/assembly/component.xml
+++ b/apache-maven/src/assembly/component.xml
@@ -85,6 +85,7 @@ under the License.
         <include>mvn</include>
         <include>mvnenc</include>
         <include>mvnDebug</include>
+        <include>mvnencDebug</include>
         <!-- This is so that CI systems can periodically run the profiler -->
         <include>mvnyjp</include>
       </includes>
diff --git a/apache-maven/src/assembly/maven/bin/mvnencDebug 
b/apache-maven/src/assembly/maven/bin/mvnencDebug
new file mode 100644
index 0000000000..50b3e67492
--- /dev/null
+++ b/apache-maven/src/assembly/maven/bin/mvnencDebug
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+# 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.
+
+# -----------------------------------------------------------------------------
+# Apache Maven Debug Script
+#
+# Environment Variable Prerequisites
+#
+#   JAVA_HOME           (Optional) Points to a Java installation.
+#   MAVEN_OPTS          (Optional) Java runtime options used when Maven is 
executed.
+#   MAVEN_SKIP_RC       (Optional) Flag to disable loading of mavenrc files.
+#   MAVEN_DEBUG_ADDRESS (Optional) Set the debug address. Default value is 
localhost:8000
+# -----------------------------------------------------------------------------
+
+MAVEN_DEBUG_OPTS="-Xdebug 
-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=${MAVEN_DEBUG_ADDRESS:-localhost:8000}"
+
+echo Preparing to execute Maven in debug mode
+
+env MAVEN_OPTS="$MAVEN_OPTS" MAVEN_DEBUG_OPTS="$MAVEN_DEBUG_OPTS" "`dirname 
"$0"`/mvnenc" "$@"
diff --git a/apache-maven/src/assembly/maven/bin/mvnencDebug.cmd 
b/apache-maven/src/assembly/maven/bin/mvnencDebug.cmd
new file mode 100644
index 0000000000..22a869cd5b
--- /dev/null
+++ b/apache-maven/src/assembly/maven/bin/mvnencDebug.cmd
@@ -0,0 +1,44 @@
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+
+@REM 
-----------------------------------------------------------------------------
+@REM Apache Maven Debug Script
+@REM
+@REM Environment Variable Prerequisites
+@REM
+@REM   JAVA_HOME           (Optional) Points to a Java installation.
+@REM   MAVEN_BATCH_ECHO    (Optional) Set to 'on' to enable the echoing of the 
batch commands.
+@REM   MAVEN_BATCH_PAUSE   (Optional) set to 'on' to wait for a key stroke 
before ending.
+@REM   MAVEN_OPTS          (Optional) Java runtime options used when Maven is 
executed.
+@REM   MAVEN_SKIP_RC       (Optional) Flag to disable loading of mavenrc files.
+@REM   MAVEN_DEBUG_ADDRESS (Optional) Set the debug address. Default value is 
localhost:8000
+@REM 
-----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%"=="on" echo %MAVEN_BATCH_ECHO%
+
+@setlocal
+
+if "%MAVEN_DEBUG_ADDRESS%"=="" @set MAVEN_DEBUG_ADDRESS=localhost:8000
+
+@set MAVEN_DEBUG_OPTS=-Xdebug 
-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=%MAVEN_DEBUG_ADDRESS%
+
+@call "%~dp0"mvnenc.cmd %*
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptOptions.java
 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptOptions.java
index a70e856e3a..910d0375ea 100644
--- 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptOptions.java
+++ 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptOptions.java
@@ -36,28 +36,18 @@ import org.apache.maven.api.cli.Options;
 @Experimental
 public interface EncryptOptions extends Options {
     /**
-     * Returns the cipher that the user wants to use for non-dispatched 
encryption.
+     * Should the operation be forced (ie overwrite existing config, if any).
      *
-     * @return an {@link Optional} containing the cipher string, or empty if 
not specified
+     * @return an {@link Optional} containing the boolean value {@code true} 
if specified, or empty
      */
-    @Nonnull
-    Optional<String> cipher();
+    Optional<Boolean> force();
 
     /**
-     * Returns the master source that the user wants to use for non-dispatched 
encryption.
+     * Should imply "yes" to all questions.
      *
-     * @return an {@link Optional} containing the master source string, or 
empty if not specified
+     * @return an {@link Optional} containing the boolean value {@code true} 
if specified, or empty
      */
-    @Nonnull
-    Optional<String> masterSource();
-
-    /**
-     * Returns the dispatcher to use for dispatched encryption.
-     *
-     * @return an {@link Optional} containing the dispatcher string, or empty 
if not specified
-     */
-    @Nonnull
-    Optional<String> dispatcher();
+    Optional<Boolean> yes();
 
     /**
      * Returns the list of encryption goals to be executed.
diff --git a/maven-cli/pom.xml b/maven-cli/pom.xml
index 1ce18d63d5..8c79d44942 100644
--- a/maven-cli/pom.xml
+++ b/maven-cli/pom.xml
@@ -92,6 +92,10 @@ under the License.
 
   <build>
     <plugins>
+      <plugin>
+        <groupId>org.eclipse.sisu</groupId>
+        <artifactId>sisu-maven-plugin</artifactId>
+      </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-jar-plugin</artifactId>
diff --git a/maven-cli/src/main/java/org/apache/maven/cling/ClingSupport.java 
b/maven-cli/src/main/java/org/apache/maven/cling/ClingSupport.java
index 5ea9ec72db..ba65331388 100644
--- a/maven-cli/src/main/java/org/apache/maven/cling/ClingSupport.java
+++ b/maven-cli/src/main/java/org/apache/maven/cling/ClingSupport.java
@@ -25,7 +25,6 @@ import org.apache.maven.api.cli.InvokerException;
 import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.Options;
 import org.apache.maven.api.cli.ParserException;
-import org.apache.maven.jline.MessageUtils;
 import org.codehaus.plexus.classworlds.ClassWorld;
 
 import static java.util.Objects.requireNonNull;
@@ -65,8 +64,6 @@ public abstract class ClingSupport<O extends Options, R 
extends InvokerRequest<O
      * The main entry point.
      */
     public int run(String[] args) throws IOException {
-        MessageUtils.systemInstall();
-        MessageUtils.registerShutdownHook();
         try (Invoker<R> invoker = createInvoker()) {
             return invoker.invoke(parseArguments(args));
         } catch (ParserException e) {
@@ -75,12 +72,8 @@ public abstract class ClingSupport<O extends Options, R 
extends InvokerRequest<O
         } catch (InvokerException e) {
             return 1;
         } finally {
-            try {
-                if (classWorldManaged) {
-                    classWorld.close();
-                }
-            } finally {
-                MessageUtils.systemUninstall();
+            if (classWorldManaged) {
+                classWorld.close();
             }
         }
     }
diff --git a/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java 
b/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java
index b8b204d5f4..691f207fb5 100644
--- a/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java
+++ b/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java
@@ -29,6 +29,7 @@ import org.apache.maven.cling.invoker.ProtoLookup;
 import org.apache.maven.cling.invoker.mvn.DefaultMavenParser;
 import org.apache.maven.cling.invoker.mvn.local.DefaultLocalMavenInvoker;
 import org.apache.maven.jline.JLineMessageBuilderFactory;
+import org.apache.maven.jline.MessageUtils;
 import org.codehaus.plexus.classworlds.ClassWorld;
 
 /**
@@ -59,6 +60,17 @@ public class MavenCling extends ClingSupport<MavenOptions, 
MavenInvokerRequest<M
         super(classWorld);
     }
 
+    @Override
+    public int run(String[] args) throws IOException {
+        MessageUtils.systemInstall();
+        MessageUtils.registerShutdownHook();
+        try {
+            return super.run(args);
+        } finally {
+            MessageUtils.systemUninstall();
+        }
+    }
+
     @Override
     protected Invoker<MavenInvokerRequest<MavenOptions>> createInvoker() {
         return new DefaultLocalMavenInvoker(
diff --git a/maven-cli/src/main/java/org/apache/maven/cling/MavenEncCling.java 
b/maven-cli/src/main/java/org/apache/maven/cling/MavenEncCling.java
index 488eb9eae4..72cb51ea2c 100644
--- a/maven-cli/src/main/java/org/apache/maven/cling/MavenEncCling.java
+++ b/maven-cli/src/main/java/org/apache/maven/cling/MavenEncCling.java
@@ -30,7 +30,10 @@ import org.apache.maven.cling.invoker.ProtoLookup;
 import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
 import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptParser;
 import org.apache.maven.jline.JLineMessageBuilderFactory;
+import org.apache.maven.jline.MessageUtils;
 import org.codehaus.plexus.classworlds.ClassWorld;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.TerminalBuilder;
 
 /**
  * Maven encrypt CLI "new-gen".
@@ -52,6 +55,8 @@ public class MavenEncCling extends 
ClingSupport<EncryptOptions, EncryptInvokerRe
         return new MavenEncCling(world).run(args);
     }
 
+    private Terminal terminal;
+
     public MavenEncCling() {
         super();
     }
@@ -60,10 +65,24 @@ public class MavenEncCling extends 
ClingSupport<EncryptOptions, EncryptInvokerRe
         super(classWorld);
     }
 
+    @Override
+    public int run(String[] args) throws IOException {
+        terminal = TerminalBuilder.builder().build();
+        MessageUtils.systemInstall(terminal);
+        MessageUtils.registerShutdownHook();
+        try {
+            return super.run(args);
+        } finally {
+            MessageUtils.systemUninstall();
+        }
+    }
+
     @Override
     protected Invoker<EncryptInvokerRequest> createInvoker() {
-        return new DefaultEncryptInvoker(
-                ProtoLookup.builder().addMapping(ClassWorld.class, 
classWorld).build());
+        return new DefaultEncryptInvoker(ProtoLookup.builder()
+                .addMapping(ClassWorld.class, classWorld)
+                .addMapping(Terminal.class, terminal)
+                .build());
     }
 
     @Override
diff --git 
a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/CommonsCliEncryptOptions.java
 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/CommonsCliEncryptOptions.java
index c1fdb3e76a..fcf964fee8 100644
--- 
a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/CommonsCliEncryptOptions.java
+++ 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/CommonsCliEncryptOptions.java
@@ -72,25 +72,17 @@ public class CommonsCliEncryptOptions extends 
CommonsCliOptions implements Encry
     }
 
     @Override
-    public Optional<String> cipher() {
-        if (commandLine.hasOption(CLIManager.CIPHER)) {
-            return Optional.of(commandLine.getOptionValue(CLIManager.CIPHER));
+    public Optional<Boolean> force() {
+        if (commandLine.hasOption(CLIManager.FORCE)) {
+            return Optional.of(Boolean.TRUE);
         }
         return Optional.empty();
     }
 
     @Override
-    public Optional<String> masterSource() {
-        if (commandLine.hasOption(CLIManager.MASTER_SOURCE)) {
-            return 
Optional.of(commandLine.getOptionValue(CLIManager.MASTER_SOURCE));
-        }
-        return Optional.empty();
-    }
-
-    @Override
-    public Optional<String> dispatcher() {
-        if (commandLine.hasOption(CLIManager.DISPATCHER)) {
-            return 
Optional.of(commandLine.getOptionValue(CLIManager.DISPATCHER));
+    public Optional<Boolean> yes() {
+        if (commandLine.hasOption(CLIManager.YES)) {
+            return Optional.of(Boolean.TRUE);
         }
         return Optional.empty();
     }
@@ -109,24 +101,19 @@ public class CommonsCliEncryptOptions extends 
CommonsCliOptions implements Encry
     }
 
     protected static class CLIManager extends CommonsCliOptions.CLIManager {
-        public static final String CIPHER = "c";
-        public static final String MASTER_SOURCE = "m";
-        public static final String DISPATCHER = "d";
+        public static final String FORCE = "f";
+        public static final String YES = "y";
 
         @Override
         protected void prepareOptions(org.apache.commons.cli.Options options) {
             super.prepareOptions(options);
-            options.addOption(Option.builder(CIPHER)
-                    .longOpt("cipher")
-                    .desc("The cipher that user wants to use for 
non-dispatched encryption")
-                    .build());
-            options.addOption(Option.builder(MASTER_SOURCE)
-                    .longOpt("master-source")
-                    .desc("The master source that user wants to use for 
non-dispatched encryption")
+            options.addOption(Option.builder(FORCE)
+                    .longOpt("force")
+                    .desc("Should overwrite without asking any configuration?")
                     .build());
-            options.addOption(Option.builder(DISPATCHER)
-                    .longOpt("dispatcher")
-                    .desc("The dispatcher to use for dispatched encryption")
+            options.addOption(Option.builder(YES)
+                    .longOpt("yes")
+                    .desc("Should imply user answered \"yes\" to all incoming 
questions?")
                     .build());
         }
     }
diff --git 
a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/ConsolePasswordPrompt.java
 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/ConsolePasswordPrompt.java
new file mode 100644
index 0000000000..a3981a7897
--- /dev/null
+++ 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/ConsolePasswordPrompt.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.cling.invoker.mvnenc;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.maven.api.services.Prompter;
+import org.apache.maven.api.services.PrompterException;
+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;
+
+/**
+ * Trivial master password source using Maven {@link Prompter} service.
+ */
+@Singleton
+@Named(ConsolePasswordPrompt.NAME)
+public class ConsolePasswordPrompt implements MasterSource, MasterSourceMeta {
+    public static final String NAME = "console-prompt";
+
+    private final Prompter prompter;
+
+    @Inject
+    public ConsolePasswordPrompt(Prompter prompter) {
+        this.prompter = prompter;
+    }
+
+    @Override
+    public String description() {
+        return "Secure console password prompt";
+    }
+
+    @Override
+    public Optional<String> configTemplate() {
+        return Optional.empty();
+    }
+
+    @Override
+    public String handle(String config) throws SecDispatcherException {
+        if (NAME.equals(config)) {
+            try {
+                return prompter.promptForPassword("Enter the master password: 
");
+            } catch (PrompterException e) {
+                throw new SecDispatcherException("Could not collect the 
password", e);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SecDispatcher.ValidationResponse validateConfiguration(String 
config) {
+        if (NAME.equals(config)) {
+            return new 
SecDispatcher.ValidationResponse(getClass().getSimpleName(), true, Map.of(), 
List.of());
+        }
+        return null;
+    }
+}
diff --git 
a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptInvoker.java
 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptInvoker.java
index ff0cb4d05b..82566bbe3f 100644
--- 
a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptInvoker.java
+++ 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptInvoker.java
@@ -18,12 +18,27 @@
  */
 package org.apache.maven.cling.invoker.mvnenc;
 
+import java.io.InterruptedIOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
 import org.apache.maven.api.cli.mvnenc.EncryptInvoker;
 import org.apache.maven.api.cli.mvnenc.EncryptInvokerRequest;
 import org.apache.maven.api.cli.mvnenc.EncryptOptions;
+import org.apache.maven.cli.CLIReportingUtils;
 import org.apache.maven.cling.invoker.LookupInvoker;
 import org.apache.maven.cling.invoker.ProtoLookup;
-import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+import org.jline.consoleui.prompt.ConsolePrompt;
+import org.jline.reader.LineReader;
+import org.jline.reader.LineReaderBuilder;
+import org.jline.reader.UserInterruptException;
+import org.jline.terminal.Terminal;
+import org.jline.utils.AttributedString;
+import org.jline.utils.AttributedStringBuilder;
+import org.jline.utils.AttributedStyle;
+import org.jline.utils.Colors;
+import org.jline.utils.OSUtils;
 
 /**
  * Encrypt invoker implementation, when Encrypt CLI is being run. System uses 
ClassWorld launcher, and class world
@@ -33,17 +48,36 @@ public class DefaultEncryptInvoker
         extends LookupInvoker<EncryptOptions, EncryptInvokerRequest, 
DefaultEncryptInvoker.LocalContext>
         implements EncryptInvoker {
 
+    @SuppressWarnings("VisibilityModifier")
     public static class LocalContext
             extends LookupInvokerContext<EncryptOptions, 
EncryptInvokerRequest, DefaultEncryptInvoker.LocalContext> {
         protected LocalContext(DefaultEncryptInvoker invoker, 
EncryptInvokerRequest invokerRequest) {
             super(invoker, invokerRequest);
         }
 
-        protected SecDispatcher secDispatcher;
+        public Map<String, Goal> goals;
+
+        public List<AttributedString> header;
+        public AttributedStyle style;
+        public LineReader reader;
+        public ConsolePrompt prompt;
+
+        public void addInHeader(String text) {
+            addInHeader(AttributedStyle.DEFAULT, text);
+        }
+
+        public void addInHeader(AttributedStyle style, String text) {
+            AttributedStringBuilder asb = new AttributedStringBuilder();
+            asb.style(style).append(text);
+            header.add(asb.toAttributedString());
+        }
     }
 
+    private final Terminal terminal;
+
     public DefaultEncryptInvoker(ProtoLookup protoLookup) {
         super(protoLookup);
+        this.terminal = protoLookup.lookup(Terminal.class);
     }
 
     @Override
@@ -58,14 +92,80 @@ public class DefaultEncryptInvoker
 
     @Override
     protected void lookup(LocalContext context) {
-        context.secDispatcher = context.lookup.lookup(SecDispatcher.class);
+        context.goals = context.lookup.lookupMap(Goal.class);
+    }
+
+    public static final int OK = 0; // OK
+    public static final int ERROR = 1; // "generic" error
+    public static final int BAD_OPERATION = 2; // bad user input or alike
+    public static final int CANCELED = 3; // user canceled
+
+    protected int doExecute(LocalContext context) throws Exception {
+        if (!context.interactive) {
+            System.out.println("This tool works only in interactive mode!");
+            System.out.println("Tool purpose is to configure password 
management on developer workstations.");
+            System.out.println(
+                    "Note: Generated configuration can be moved/copied to 
headless environments, if configured as such.");
+            return BAD_OPERATION;
+        }
+
+        context.header = new ArrayList<>();
+        context.style = new AttributedStyle();
+        context.addInHeader(
+                
context.style.italic().bold().foreground(Colors.rgbColor("green")),
+                "Maven Encryption " + CLIReportingUtils.showVersionMinimal());
+        context.addInHeader("Tool for secure password management on 
workstations.");
+        context.addInHeader("This tool is part of Apache Maven 4 
distribution.");
+        context.addInHeader("");
+        try {
+            Thread executeThread = Thread.currentThread();
+            terminal.handle(Terminal.Signal.INT, signal -> 
executeThread.interrupt());
+            ConsolePrompt.UiConfig config;
+            if (terminal.getType().equals(Terminal.TYPE_DUMB)
+                    || terminal.getType().equals(Terminal.TYPE_DUMB_COLOR)) {
+                System.out.println(terminal.getName() + ": " + 
terminal.getType());
+                throw new IllegalStateException("Dumb terminal detected.\nThis 
tool requires real terminal to work!\n"
+                        + "Note: On Windows Jansi or JNA library must be 
included in classpath.");
+            } else if (OSUtils.IS_WINDOWS) {
+                config = new ConsolePrompt.UiConfig(">", "( )", "(x)", "( )");
+            } else {
+                config = new ConsolePrompt.UiConfig("❯", "◯ ", "◉ ", "◯ ");
+            }
+            config.setCancellableFirstPrompt(true);
+
+            context.reader = 
LineReaderBuilder.builder().terminal(terminal).build();
+            context.prompt = new ConsolePrompt(context.reader, terminal, 
config);
+
+            if (context.invokerRequest.options().goals().isEmpty()
+                    || context.invokerRequest.options().goals().get().size() 
!= 1) {
+                return badGoalsErrorMessage("No goal or multiple goals 
specified, specify only one goal.", context);
+            }
+
+            String goalName = 
context.invokerRequest.options().goals().get().get(0);
+            Goal goal = context.goals.get(goalName);
+
+            if (goal == null) {
+                return badGoalsErrorMessage("Unknown goal: " + goalName, 
context);
+            }
+
+            return goal.execute(context);
+        } catch (InterruptedException | InterruptedIOException | 
UserInterruptException e) {
+            System.out.println("Goal canceled by user.");
+            return CANCELED;
+        } catch (Exception e) {
+            if (context.invokerRequest.options().showErrors().orElse(false)) {
+                context.logger.error(e.getMessage(), e);
+            } else {
+                context.logger.error(e.getMessage());
+            }
+            return ERROR;
+        }
     }
 
-    protected int doExecute(LocalContext localContext) throws Exception {
-        localContext.logger.info("Hello, this is SecDispatcher.");
-        localContext.logger.info("Available Ciphers: " + 
localContext.secDispatcher.availableCiphers());
-        localContext.logger.info("Available Dispatchers: " + 
localContext.secDispatcher.availableDispatchers());
-        // TODO: implement mvnenc
-        return 0;
+    protected int badGoalsErrorMessage(String message, LocalContext context) {
+        System.out.println(message);
+        System.out.println("Supported goals are: " + String.join(", ", 
context.goals.keySet()));
+        System.out.println("Use -h to display help.");
+        return BAD_OPERATION;
     }
 }
diff --git 
a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java
new file mode 100644
index 0000000000..d493b289e3
--- /dev/null
+++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.cling.invoker.mvnenc;
+
+/**
+ * The mvnenc tool goal.
+ */
+public interface Goal {
+    int execute(DefaultEncryptInvoker.LocalContext context) throws Exception;
+}
diff --git 
a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConfiguredGoalSupport.java
 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConfiguredGoalSupport.java
new file mode 100644
index 0000000000..5719a816fc
--- /dev/null
+++ 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConfiguredGoalSupport.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.cling.invoker.mvnenc.goals;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.apache.maven.api.services.MessageBuilderFactory;
+import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+
+import static 
org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.ERROR;
+
+/**
+ * The support class for goal implementations that requires valid/workable 
config.
+ */
+public abstract class ConfiguredGoalSupport extends GoalSupport {
+    protected ConfiguredGoalSupport(MessageBuilderFactory 
messageBuilderFactory, SecDispatcher secDispatcher) {
+        super(messageBuilderFactory, secDispatcher);
+    }
+
+    @Override
+    public int execute(DefaultEncryptInvoker.LocalContext context) throws 
Exception {
+        if (!validateConfiguration()) {
+            logger.error(messageBuilderFactory
+                    .builder()
+                    .error("Maven Encryption is not configured, run `mvnenc 
init` first.")
+                    .build());
+            return ERROR;
+        }
+        return doExecute(context);
+    }
+
+    protected boolean validateConfiguration() {
+        SecDispatcher.ValidationResponse response = 
secDispatcher.validateConfiguration();
+        if (!response.isValid() || logger.isDebugEnabled()) {
+            dumpResponse("", response);
+        }
+        return response.isValid();
+    }
+
+    protected void dumpResponse(String indent, 
SecDispatcher.ValidationResponse response) {
+        logger.info(
+                response.isValid()
+                        ? messageBuilderFactory
+                                .builder()
+                                .success("{}Configuration validation of {}: 
{}")
+                                .build()
+                        : messageBuilderFactory
+                                .builder()
+                                .failure("{}Configuration validation of {}: 
{}")
+                                .build(),
+                indent,
+                response.getSource(),
+                response.isValid() ? "VALID" : "INVALID");
+        for (Map.Entry<SecDispatcher.ValidationResponse.Level, List<String>> 
entry :
+                response.getReport().entrySet()) {
+            Consumer<String> consumer =
+                    s -> 
logger.info(messageBuilderFactory.builder().info(s).build());
+            if (entry.getKey() == 
SecDispatcher.ValidationResponse.Level.ERROR) {
+                consumer = s ->
+                        
logger.error(messageBuilderFactory.builder().error(s).build());
+            } else if (entry.getKey() == 
SecDispatcher.ValidationResponse.Level.WARNING) {
+                consumer = s ->
+                        
logger.warn(messageBuilderFactory.builder().warning(s).build());
+            }
+            for (String line : entry.getValue()) {
+                consumer.accept(indent + "  " + line);
+            }
+        }
+        for (SecDispatcher.ValidationResponse subsystem : 
response.getSubsystems()) {
+            dumpResponse(indent + "  ", subsystem);
+        }
+    }
+
+    protected abstract int doExecute(DefaultEncryptInvoker.LocalContext 
context) throws Exception;
+}
diff --git 
a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Decrypt.java
 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Decrypt.java
new file mode 100644
index 0000000000..6c437c9676
--- /dev/null
+++ 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Decrypt.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.cling.invoker.mvnenc.goals;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.apache.maven.api.services.MessageBuilderFactory;
+import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+
+import static 
org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.BAD_OPERATION;
+import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK;
+
+/**
+ * The "decrypt" goal.
+ */
+@Singleton
+@Named("decrypt")
+public class Decrypt extends ConfiguredGoalSupport {
+    @Inject
+    public Decrypt(MessageBuilderFactory messageBuilderFactory, SecDispatcher 
secDispatcher) {
+        super(messageBuilderFactory, secDispatcher);
+    }
+
+    @Override
+    protected int doExecute(DefaultEncryptInvoker.LocalContext context) throws 
Exception {
+        String encrypted = context.reader.readLine("Enter the password to 
decrypt: ");
+        if (secDispatcher.isAnyEncryptedString(encrypted)) {
+            logger.info(secDispatcher.decrypt(encrypted));
+            return OK;
+        } else {
+            logger.error("Malformed encrypted string");
+            return BAD_OPERATION;
+        }
+    }
+}
diff --git 
a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Diag.java 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Diag.java
new file mode 100644
index 0000000000..01d4680ac9
--- /dev/null
+++ 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Diag.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.cling.invoker.mvnenc.goals;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.apache.maven.api.services.MessageBuilderFactory;
+import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+
+import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK;
+
+/**
+ * The "diag" goal.
+ */
+@Singleton
+@Named("diag")
+public class Diag extends ConfiguredGoalSupport {
+    @Inject
+    public Diag(MessageBuilderFactory messageBuilderFactory, SecDispatcher 
secDispatcher) {
+        super(messageBuilderFactory, secDispatcher);
+    }
+
+    @Override
+    protected int doExecute(DefaultEncryptInvoker.LocalContext context) {
+        dumpResponse("", secDispatcher.validateConfiguration());
+        return OK;
+    }
+}
diff --git 
a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Encrypt.java
 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Encrypt.java
new file mode 100644
index 0000000000..c17a5bd53f
--- /dev/null
+++ 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Encrypt.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.cling.invoker.mvnenc.goals;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.apache.maven.api.services.MessageBuilderFactory;
+import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+
+import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK;
+
+/**
+ * The "encrypt" goal.
+ */
+@Singleton
+@Named("encrypt")
+public class Encrypt extends ConfiguredGoalSupport {
+    @Inject
+    public Encrypt(MessageBuilderFactory messageBuilderFactory, SecDispatcher 
secDispatcher) {
+        super(messageBuilderFactory, secDispatcher);
+    }
+
+    @Override
+    protected int doExecute(DefaultEncryptInvoker.LocalContext context) throws 
Exception {
+        String cleartext = context.reader.readLine("Enter the password to 
encrypt: ", '*');
+        logger.info(secDispatcher.encrypt(cleartext, null));
+        return OK;
+    }
+}
diff --git 
a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/GoalSupport.java
 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/GoalSupport.java
new file mode 100644
index 0000000000..8a8b43ebb9
--- /dev/null
+++ 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/GoalSupport.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.cling.invoker.mvnenc.goals;
+
+import java.io.IOException;
+
+import org.apache.maven.api.services.MessageBuilderFactory;
+import org.apache.maven.cling.invoker.mvnenc.Goal;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The support class for goal implementations.
+ */
+public abstract class GoalSupport implements Goal {
+    protected final Logger logger = LoggerFactory.getLogger(getClass());
+    protected final MessageBuilderFactory messageBuilderFactory;
+    protected final SecDispatcher secDispatcher;
+
+    protected GoalSupport(MessageBuilderFactory messageBuilderFactory, 
SecDispatcher secDispatcher) {
+        this.messageBuilderFactory = messageBuilderFactory;
+        this.secDispatcher = secDispatcher;
+    }
+
+    protected boolean configExists() throws IOException {
+        return secDispatcher.readConfiguration(false) != null;
+    }
+}
diff --git 
a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Init.java 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Init.java
new file mode 100644
index 0000000000..cd21e4abc2
--- /dev/null
+++ 
b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Init.java
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.cling.invoker.mvnenc.goals;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.maven.api.services.MessageBuilderFactory;
+import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
+import org.codehaus.plexus.components.secdispatcher.DispatcherMeta;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+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.jline.consoleui.elements.ConfirmChoice;
+import org.jline.consoleui.prompt.ConfirmResult;
+import org.jline.consoleui.prompt.ConsolePrompt;
+import org.jline.consoleui.prompt.PromptResultItemIF;
+import org.jline.consoleui.prompt.builder.ListPromptBuilder;
+import org.jline.consoleui.prompt.builder.PromptBuilder;
+import org.jline.reader.Candidate;
+import org.jline.reader.Completer;
+import org.jline.reader.LineReader;
+import org.jline.reader.ParsedLine;
+import org.jline.utils.Colors;
+
+import static 
org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.BAD_OPERATION;
+import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK;
+
+/**
+ * The "init" goal.
+ */
+@Singleton
+@Named("init")
+public class Init extends GoalSupport {
+    private static final String NONE = "__none__";
+
+    @Inject
+    public Init(MessageBuilderFactory messageBuilderFactory, SecDispatcher 
secDispatcher) {
+        super(messageBuilderFactory, secDispatcher);
+    }
+
+    @Override
+    public int execute(DefaultEncryptInvoker.LocalContext context) throws 
Exception {
+        
context.addInHeader(context.style.italic().bold().foreground(Colors.rgbColor("yellow")),
 "goal: init");
+        context.addInHeader("");
+
+        ConsolePrompt prompt = context.prompt;
+        boolean force = context.invokerRequest.options().force().orElse(false);
+        boolean yes = context.invokerRequest.options().yes().orElse(false);
+
+        if (configExists() && !force) {
+            System.out.println(messageBuilderFactory
+                    .builder()
+                    .error("Error: configuration exist. Use --force if you 
want to reset existing configuration."));
+            return BAD_OPERATION;
+        }
+
+        SettingsSecurity config = secDispatcher.readConfiguration(true);
+
+        // reset config
+        config.setDefaultDispatcher(null);
+        config.getConfigurations().clear();
+
+        Map<String, PromptResultItemIF> result = prompt.prompt(
+                context.header, 
dispatcherPrompt(prompt.getPromptBuilder()).build());
+        if (result == null) {
+            throw new InterruptedException();
+        }
+        if (NONE.equals(result.get("defaultDispatcher").getResult())) {
+            logger.warn(messageBuilderFactory
+                    .builder()
+                    .warning(
+                            "Maven4 SecDispatcher disabled; Maven3 fallback 
may still work, use `mvnenc diag` to check")
+                    .build());
+            secDispatcher.writeConfiguration(config);
+            return OK;
+        }
+        
config.setDefaultDispatcher(result.get("defaultDispatcher").getResult());
+
+        DispatcherMeta meta = secDispatcher.availableDispatchers().stream()
+                .filter(d -> Objects.equals(config.getDefaultDispatcher(), 
d.name()))
+                .findFirst()
+                .orElseThrow();
+        if (!meta.fields().isEmpty()) {
+            result = prompt.prompt(
+                    context.header,
+                    configureDispatcher(context, meta, 
prompt.getPromptBuilder())
+                            .build());
+            if (result == null) {
+                throw new InterruptedException();
+            }
+
+            List<Map.Entry<String, PromptResultItemIF>> editables = 
result.entrySet().stream()
+                    .filter(e -> e.getValue().getResult().contains("$"))
+                    .toList();
+            if (!editables.isEmpty()) {
+                context.addInHeader("");
+                context.addInHeader("Please customize the editable value:");
+                Map<String, PromptResultItemIF> editMap;
+                for (Map.Entry<String, PromptResultItemIF> editable : 
editables) {
+                    String template = editable.getValue().getResult();
+                    String prefix = template.substring(0, 
template.indexOf("$"));
+                    editMap = prompt.prompt(
+                            context.header,
+                            prompt.getPromptBuilder()
+                                    .createInputPrompt()
+                                    .name("edit")
+                                    .message(template)
+                                    .addCompleter(new Completer() {
+                                        @Override
+                                        public void complete(
+                                                LineReader reader, ParsedLine 
line, List<Candidate> candidates) {
+                                            if 
(!line.line().startsWith(prefix)) {
+                                                candidates.add(
+                                                        new Candidate(prefix, 
prefix, null, null, null, null, false));
+                                            }
+                                        }
+                                    })
+                                    .addPrompt()
+                                    .build());
+                    if (editMap == null) {
+                        throw new InterruptedException();
+                    }
+                    result.put(editable.getKey(), editMap.get("edit"));
+                }
+            }
+
+            Config dispatcherConfig = new Config();
+            dispatcherConfig.setName(meta.name());
+            for (DispatcherMeta.Field field : meta.fields()) {
+                ConfigProperty property = new ConfigProperty();
+                property.setName(field.getKey());
+                property.setValue(result.get(field.getKey()).getResult());
+                dispatcherConfig.addProperty(property);
+            }
+            if (!dispatcherConfig.getProperties().isEmpty()) {
+                config.addConfiguration(dispatcherConfig);
+            }
+        }
+
+        if (yes) {
+            secDispatcher.writeConfiguration(config);
+        } else {
+            context.addInHeader("");
+            context.addInHeader("Values set:");
+            context.addInHeader("defaultDispatcher=" + 
config.getDefaultDispatcher());
+            for (Config c : config.getConfigurations()) {
+                context.addInHeader("  dispatcherName=" + c.getName());
+                for (ConfigProperty cp : c.getProperties()) {
+                    context.addInHeader("    " + cp.getName() + "=" + 
cp.getValue());
+                }
+            }
+
+            result = prompt.prompt(
+                    context.header, 
confirmPrompt(prompt.getPromptBuilder()).build());
+            ConfirmResult confirm = (ConfirmResult) result.get("confirm");
+            if (confirm.getConfirmed() == ConfirmChoice.ConfirmationValue.YES) 
{
+                logger.info(messageBuilderFactory
+                        .builder()
+                        .info("Writing out the configuration...")
+                        .build());
+                secDispatcher.writeConfiguration(config);
+            } else {
+                logger.warn(messageBuilderFactory
+                        .builder()
+                        .warning("Values not accepted; not saving 
configuration.")
+                        .build());
+                return BAD_OPERATION;
+            }
+        }
+
+        return OK;
+    }
+
+    protected PromptBuilder confirmPrompt(PromptBuilder promptBuilder) {
+        promptBuilder
+                .createConfirmPromp()
+                .name("confirm")
+                .message("Are values above correct?")
+                .defaultValue(ConfirmChoice.ConfirmationValue.YES)
+                .addPrompt();
+        return promptBuilder;
+    }
+
+    protected PromptBuilder dispatcherPrompt(PromptBuilder promptBuilder) {
+        ListPromptBuilder listPromptBuilder = promptBuilder
+                .createListPrompt()
+                .name("defaultDispatcher")
+                .message("Which dispatcher you want to use as default?");
+        listPromptBuilder
+                .newItem()
+                .name(NONE)
+                .text("None (disable MavenSecDispatcher)")
+                .add();
+        for (DispatcherMeta meta : secDispatcher.availableDispatchers()) {
+            if (!meta.isHidden()) {
+                listPromptBuilder
+                        .newItem()
+                        .name(meta.name())
+                        .text(meta.displayName())
+                        .add();
+            }
+        }
+        listPromptBuilder.addPrompt();
+        return promptBuilder;
+    }
+
+    private PromptBuilder configureDispatcher(
+            DefaultEncryptInvoker.LocalContext context, DispatcherMeta 
dispatcherMeta, PromptBuilder promptBuilder)
+            throws Exception {
+        context.addInHeader(
+                
context.style.italic().bold().foreground(Colors.rgbColor("yellow")),
+                "Configure " + dispatcherMeta.displayName());
+        context.addInHeader("");
+
+        for (DispatcherMeta.Field field : dispatcherMeta.fields()) {
+            String fieldKey = field.getKey();
+            String fieldDescription = "Configure " + fieldKey + ": " + 
field.getDescription();
+            if (field.getOptions().isPresent()) {
+                // list options
+                ListPromptBuilder listPromptBuilder =
+                        
promptBuilder.createListPrompt().name(fieldKey).message(fieldDescription);
+                for (DispatcherMeta.Field option : field.getOptions().get()) {
+                    listPromptBuilder
+                            .newItem()
+                            .name(
+                                    option.getDefaultValue().isPresent()
+                                            ? option.getDefaultValue().get()
+                                            : option.getKey())
+                            .text(option.getDescription())
+                            .add();
+                }
+                listPromptBuilder.addPrompt();
+            } else if (field.getDefaultValue().isPresent()) {
+                // input w/ def value
+                promptBuilder
+                        .createInputPrompt()
+                        .name(fieldKey)
+                        .message(fieldDescription)
+                        .defaultValue(field.getDefaultValue().get())
+                        .addPrompt();
+            } else {
+                // ? plain input?
+                promptBuilder
+                        .createInputPrompt()
+                        .name(fieldKey)
+                        .message(fieldDescription)
+                        .addPrompt();
+            }
+        }
+        return promptBuilder;
+    }
+}
diff --git 
a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
 
b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
index f49ef3b727..9c214a5da4 100644
--- 
a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
+++ 
b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
@@ -214,10 +214,15 @@ public class DefaultRepositorySystemSessionFactory 
implements RepositorySystemSe
         decrypt.setProxies(request.getProxies());
         decrypt.setServers(request.getServers());
         SettingsDecryptionResult decrypted = 
settingsDecrypter.decrypt(decrypt);
-
-        if (logger.isDebugEnabled()) {
-            for (SettingsProblem problem : decrypted.getProblems()) {
-                logger.debug(problem.getMessage(), problem.getException());
+        for (SettingsProblem problem : decrypted.getProblems()) {
+            if (problem.getSeverity() == SettingsProblem.Severity.WARNING) {
+                logger.warn(problem.getMessage());
+            } else if (problem.getSeverity() == 
SettingsProblem.Severity.ERROR) {
+                logger.error(
+                        problem.getMessage(),
+                        request.isShowErrors()
+                                ? problem.getException()
+                                : problem.getException().getMessage());
             }
         }
 
diff --git a/maven-embedder/pom.xml b/maven-embedder/pom.xml
index a5fb2351e0..5a36cf9fea 100644
--- a/maven-embedder/pom.xml
+++ b/maven-embedder/pom.xml
@@ -88,10 +88,6 @@ under the License.
       <groupId>org.codehaus.plexus</groupId>
       <artifactId>plexus-sec-dispatcher</artifactId>
     </dependency>
-    <dependency>
-      <groupId>org.codehaus.plexus</groupId>
-      <artifactId>plexus-cipher</artifactId>
-    </dependency>
     <dependency>
       <groupId>org.codehaus.plexus</groupId>
       <artifactId>plexus-interpolation</artifactId>
diff --git a/maven-jline/pom.xml b/maven-jline/pom.xml
index 867bc34b41..09eb58de33 100644
--- a/maven-jline/pom.xml
+++ b/maven-jline/pom.xml
@@ -42,6 +42,18 @@ under the License.
       <groupId>org.jline</groupId>
       <artifactId>jline-reader</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.jline</groupId>
+      <artifactId>jline-style</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jline</groupId>
+      <artifactId>jline-builtins</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jline</groupId>
+      <artifactId>jline-console-ui</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.jline</groupId>
       <artifactId>jline-terminal-jni</artifactId>
diff --git a/maven-settings-builder/pom.xml b/maven-settings-builder/pom.xml
index 36533dc3d0..5d442afdd7 100644
--- a/maven-settings-builder/pom.xml
+++ b/maven-settings-builder/pom.xml
@@ -62,10 +62,6 @@ under the License.
       <groupId>org.codehaus.plexus</groupId>
       <artifactId>plexus-sec-dispatcher</artifactId>
     </dependency>
-    <dependency>
-      <groupId>org.codehaus.plexus</groupId>
-      <artifactId>plexus-cipher</artifactId>
-    </dependency>
 
     <dependency>
       <groupId>javax.inject</groupId>
diff --git 
a/maven-settings-builder/src/main/java/org/apache/maven/settings/crypto/DefaultSettingsDecrypter.java
 
b/maven-settings-builder/src/main/java/org/apache/maven/settings/crypto/DefaultSettingsDecrypter.java
index 3695d6ab3b..59d1c13921 100644
--- 
a/maven-settings-builder/src/main/java/org/apache/maven/settings/crypto/DefaultSettingsDecrypter.java
+++ 
b/maven-settings-builder/src/main/java/org/apache/maven/settings/crypto/DefaultSettingsDecrypter.java
@@ -22,6 +22,7 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -58,28 +59,52 @@ public class DefaultSettingsDecrypter implements 
SettingsDecrypter {
         for (Server server : request.getServers()) {
             server = server.clone();
 
-            try {
-                server.setPassword(decrypt(server.getPassword()));
-            } catch (SecDispatcherException e) {
-                problems.add(new DefaultSettingsProblem(
-                        "Failed to decrypt password for server " + 
server.getId() + ": " + e.getMessage(),
-                        Severity.ERROR,
-                        "server: " + server.getId(),
-                        -1,
-                        -1,
-                        e));
+            String password = server.getPassword();
+            if (securityDispatcher.isAnyEncryptedString(password)) {
+                try {
+                    if (securityDispatcher.isLegacyEncryptedString(password)) {
+                        problems.add(new DefaultSettingsProblem(
+                                "Legacy/insecurely encrypted password detected 
for server " + server.getId(),
+                                Severity.WARNING,
+                                "server: " + server.getId(),
+                                -1,
+                                -1,
+                                null));
+                    }
+                    server.setPassword(securityDispatcher.decrypt(password));
+                } catch (SecDispatcherException | IOException e) {
+                    problems.add(new DefaultSettingsProblem(
+                            "Failed to decrypt password for server " + 
server.getId() + ": " + e.getMessage(),
+                            Severity.ERROR,
+                            "server: " + server.getId(),
+                            -1,
+                            -1,
+                            e));
+                }
             }
 
-            try {
-                server.setPassphrase(decrypt(server.getPassphrase()));
-            } catch (SecDispatcherException e) {
-                problems.add(new DefaultSettingsProblem(
-                        "Failed to decrypt passphrase for server " + 
server.getId() + ": " + e.getMessage(),
-                        Severity.ERROR,
-                        "server: " + server.getId(),
-                        -1,
-                        -1,
-                        e));
+            String passphrase = server.getPassphrase();
+            if (securityDispatcher.isAnyEncryptedString(passphrase)) {
+                try {
+                    if 
(securityDispatcher.isLegacyEncryptedString(passphrase)) {
+                        problems.add(new DefaultSettingsProblem(
+                                "Legacy/insecurely encrypted passphrase 
detected for server " + server.getId(),
+                                Severity.WARNING,
+                                "server: " + server.getId(),
+                                -1,
+                                -1,
+                                null));
+                    }
+                    
server.setPassphrase(securityDispatcher.decrypt(passphrase));
+                } catch (SecDispatcherException | IOException e) {
+                    problems.add(new DefaultSettingsProblem(
+                            "Failed to decrypt passphrase for server " + 
server.getId() + ": " + e.getMessage(),
+                            Severity.ERROR,
+                            "server: " + server.getId(),
+                            -1,
+                            -1,
+                            e));
+                }
             }
 
             servers.add(server);
@@ -88,16 +113,28 @@ public class DefaultSettingsDecrypter implements 
SettingsDecrypter {
         List<Proxy> proxies = new ArrayList<>();
 
         for (Proxy proxy : request.getProxies()) {
-            try {
-                proxy.setPassword(decrypt(proxy.getPassword()));
-            } catch (SecDispatcherException e) {
-                problems.add(new DefaultSettingsProblem(
-                        "Failed to decrypt password for proxy " + 
proxy.getId() + ": " + e.getMessage(),
-                        Severity.ERROR,
-                        "proxy: " + proxy.getId(),
-                        -1,
-                        -1,
-                        e));
+            String password = proxy.getPassword();
+            if (securityDispatcher.isAnyEncryptedString(password)) {
+                try {
+                    if (securityDispatcher.isLegacyEncryptedString(password)) {
+                        problems.add(new DefaultSettingsProblem(
+                                "Legacy/insecurely encrypted password detected 
for proxy " + proxy.getId(),
+                                Severity.WARNING,
+                                "proxy: " + proxy.getId(),
+                                -1,
+                                -1,
+                                null));
+                    }
+                    proxy.setPassword(securityDispatcher.decrypt(password));
+                } catch (SecDispatcherException | IOException e) {
+                    problems.add(new DefaultSettingsProblem(
+                            "Failed to decrypt password for proxy " + 
proxy.getId() + ": " + e.getMessage(),
+                            Severity.ERROR,
+                            "proxy: " + proxy.getId(),
+                            -1,
+                            -1,
+                            e));
+                }
             }
 
             proxies.add(proxy);
@@ -105,8 +142,4 @@ public class DefaultSettingsDecrypter implements 
SettingsDecrypter {
 
         return new DefaultSettingsDecryptionResult(servers, proxies, problems);
     }
-
-    private String decrypt(String str) throws SecDispatcherException {
-        return (str == null) ? null : securityDispatcher.decrypt(str);
-    }
 }
diff --git 
a/maven-settings-builder/src/main/java/org/apache/maven/settings/crypto/MavenSecDispatcher.java
 
b/maven-settings-builder/src/main/java/org/apache/maven/settings/crypto/MavenSecDispatcher.java
new file mode 100644
index 0000000000..4ac37151d8
--- /dev/null
+++ 
b/maven-settings-builder/src/main/java/org/apache/maven/settings/crypto/MavenSecDispatcher.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.settings.crypto;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+
+import org.apache.maven.api.Constants;
+import org.codehaus.plexus.components.secdispatcher.Dispatcher;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+import 
org.codehaus.plexus.components.secdispatcher.internal.DefaultSecDispatcher;
+
+/**
+ * This class implements "Maven specific" {@link SecDispatcher}.
+ *
+ * @deprecated since 4.0.0
+ */
+@Named
+@Singleton
+@Deprecated(since = "4.0.0")
+public class MavenSecDispatcher extends DefaultSecDispatcher {
+    private static final String FILE_NAME = "settings-security4.xml";
+
+    @Inject
+    public MavenSecDispatcher(Map<String, Dispatcher> dispatchers) {
+        super(dispatchers, configurationFile());
+    }
+
+    private static Path configurationFile() {
+        String mavenUserConf = System.getProperty(Constants.MAVEN_USER_CONF);
+        if (mavenUserConf != null) {
+            return Paths.get(mavenUserConf, FILE_NAME);
+        }
+        // this means we are in UT or alike
+        return Paths.get(System.getProperty("user.home"), ".m2", FILE_NAME);
+    }
+}
diff --git a/pom.xml b/pom.xml
index 1526eb14d3..e1fddf3ff2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -167,7 +167,6 @@ under the License.
     <assertjVersion>3.26.3</assertjVersion>
     <asmVersion>9.7.1</asmVersion>
     <byteBuddyVersion>1.15.3</byteBuddyVersion>
-    <cipherVersion>3.0.0</cipherVersion>
     <classWorldsVersion>2.8.0</classWorldsVersion>
     <commonsCliVersion>1.9.0</commonsCliVersion>
     <guiceVersion>6.0.0</guiceVersion>
@@ -186,7 +185,7 @@ under the License.
     <plexusTestingVersion>1.4.0</plexusTestingVersion>
     <plexusXmlVersion>4.0.4</plexusXmlVersion>
     <resolverVersion>2.0.1</resolverVersion>
-    <securityDispatcherVersion>3.0.0</securityDispatcherVersion>
+    <securityDispatcherVersion>4.0.1</securityDispatcherVersion>
     <sisuVersion>0.9.0.M3</sisuVersion>
     <slf4jVersion>2.0.16</slf4jVersion>
     <stax2ApiVersion>4.2.2</stax2ApiVersion>
@@ -475,6 +474,21 @@ under the License.
         <artifactId>jline-reader</artifactId>
         <version>${jlineVersion}</version>
       </dependency>
+      <dependency>
+        <groupId>org.jline</groupId>
+        <artifactId>jline-style</artifactId>
+        <version>${jlineVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jline</groupId>
+        <artifactId>jline-builtins</artifactId>
+        <version>${jlineVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jline</groupId>
+        <artifactId>jline-console-ui</artifactId>
+        <version>${jlineVersion}</version>
+      </dependency>
       <dependency>
         <groupId>org.jline</groupId>
         <artifactId>jline-terminal-ffm</artifactId>
@@ -590,11 +604,6 @@ under the License.
         <artifactId>plexus-sec-dispatcher</artifactId>
         <version>${securityDispatcherVersion}</version>
       </dependency>
-      <dependency>
-        <groupId>org.codehaus.plexus</groupId>
-        <artifactId>plexus-cipher</artifactId>
-        <version>${cipherVersion}</version>
-      </dependency>
       <dependency>
         <groupId>com.fasterxml.woodstox</groupId>
         <artifactId>woodstox-core</artifactId>

Reply via email to