From b2bded86efd0a8c493892df7692d75f2d54753f5 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 10 Oct 2024 09:46:09 +0200 Subject: [PATCH 01/24] Mvnenc --- .../maven/api/cli/mvnenc/EncryptOptions.java | 14 ++ maven-cli/pom.xml | 4 + .../org/apache/maven/cling/ClingSupport.java | 11 +- .../org/apache/maven/cling/MavenCling.java | 12 ++ .../mvnenc/CommonsCliEncryptOptions.java | 26 +++ .../invoker/mvnenc/DefaultEncryptInvoker.java | 103 ++++++++++-- .../maven/cling/invoker/mvnenc/Goal.java | 26 +++ .../cling/invoker/mvnenc/goals/Encrypt.java | 50 ++++++ .../cling/invoker/mvnenc/goals/InitGoal.java | 151 ++++++++++++++++++ maven-jline/pom.xml | 12 ++ pom.xml | 17 +- 11 files changed, 407 insertions(+), 19 deletions(-) create mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java create mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Encrypt.java create mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java 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 a70e856e3a3e..288fd3624227 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 @@ -59,6 +59,20 @@ public interface EncryptOptions extends Options { @Nonnull Optional dispatcher(); + /** + * Should the operation be forced (ie overwrite existing config, if any). + * + * @return an {@link Optional} containing the boolean value {@code true} if specified, or empty + */ + Optional force(); + + /** + * Should imply "yes" to all questions. + * + * @return an {@link Optional} containing the boolean value {@code true} if specified, or empty + */ + Optional yes(); + /** * Returns the list of encryption goals to be executed. * These goals can include operations like "init", "add-server", "delete-server", etc. diff --git a/maven-cli/pom.xml b/maven-cli/pom.xml index 1ce18d63d50e..8c79d44942d2 100644 --- a/maven-cli/pom.xml +++ b/maven-cli/pom.xml @@ -92,6 +92,10 @@ under the License. + + org.eclipse.sisu + sisu-maven-plugin + org.apache.maven.plugins maven-jar-plugin 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 5ea9ec72dbae..ba65331388fe 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.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 @@ private ClingSupport(ClassWorld classWorld, boolean classWorldManaged) { * The main entry point. */ public int run(String[] args) throws IOException { - MessageUtils.systemInstall(); - MessageUtils.registerShutdownHook(); try (Invoker invoker = createInvoker()) { return invoker.invoke(parseArguments(args)); } catch (ParserException e) { @@ -75,12 +72,8 @@ public int run(String[] args) throws IOException { } 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 b8b204d5f470..691f207fb53d 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.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 MavenCling(ClassWorld classWorld) { 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> createInvoker() { return new DefaultLocalMavenInvoker( 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 c1fdb3e76a48..4f847b628d60 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 @@ -95,6 +95,22 @@ public Optional dispatcher() { return Optional.empty(); } + @Override + public Optional force() { + if (commandLine.hasOption(CLIManager.FORCE)) { + return Optional.of(Boolean.TRUE); + } + return Optional.empty(); + } + + @Override + public Optional yes() { + if (commandLine.hasOption(CLIManager.YES)) { + return Optional.of(Boolean.TRUE); + } + return Optional.empty(); + } + @Override public Optional> goals() { if (!commandLine.getArgList().isEmpty()) { @@ -112,6 +128,8 @@ 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) { @@ -128,6 +146,14 @@ protected void prepareOptions(org.apache.commons.cli.Options options) { .longOpt("dispatcher") .desc("The dispatcher to use for dispatched encryption") .build()); + options.addOption(Option.builder(FORCE) + .longOpt("force") + .desc("Should overwrite without asking any configuration, if exist.") + .build()); + options.addOption(Option.builder(YES) + .longOpt("yes") + .desc("Should imply \"yes\" answer to all questions") + .build()); } } } 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 ff0cb4d05bd6..e1dc37eab9d5 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,26 @@ */ package org.apache.maven.cling.invoker.mvnenc; +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.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +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,13 +47,29 @@ public class DefaultEncryptInvoker extends LookupInvoker implements EncryptInvoker { + @SuppressWarnings("VisibilityModifier") public static class LocalContext extends LookupInvokerContext { protected LocalContext(DefaultEncryptInvoker invoker, EncryptInvokerRequest invokerRequest) { super(invoker, invokerRequest); } - protected SecDispatcher secDispatcher; + public Map goals; + + public List 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()); + } } public DefaultEncryptInvoker(ProtoLookup protoLookup) { @@ -58,14 +88,69 @@ protected LocalContext createContext(EncryptInvokerRequest invokerRequest) { @Override protected void lookup(LocalContext context) { - context.secDispatcher = context.lookup.lookup(SecDispatcher.class); + context.goals = context.lookup.lookupMap(Goal.class); } - 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; + 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!"); + 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 (Terminal terminal = TerminalBuilder.builder().build()) { + 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) { + System.out.println("No goal or multiple goals specified, specify only one goal. Use -h to see help."); + System.out.println("Supported goals are: " + context.goals.keySet()); + return BAD_OPERATION; + } + + Goal goal = context.goals.get( + context.invokerRequest.options().goals().get().get(0)); + + if (goal == null) { + System.out.println("Unknown goal, supported goals are: " + context.goals.keySet()); + return BAD_OPERATION; + } + + return goal.execute(context); + } catch (InterruptedException e) { + System.out.println("Goal canceled by user."); + return CANCELED; + } catch (Exception e) { + context.logger.error(e.getMessage(), e); + return ERROR; + } } } 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 000000000000..d493b289e36c --- /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/Encrypt.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Encrypt.java new file mode 100644 index 000000000000..28903ab07bc0 --- /dev/null +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Encrypt.java @@ -0,0 +1,50 @@ +/* + * 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.cling.invoker.mvnenc.DefaultEncryptInvoker; +import org.apache.maven.cling.invoker.mvnenc.Goal; +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 implements Goal { + private final SecDispatcher secDispatcher; + + @Inject + public Encrypt(SecDispatcher secDispatcher) { + this.secDispatcher = secDispatcher; + } + + @Override + public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception { + String cleartext = context.reader.readLine("Enter the password to encrypt: ", '*'); + System.out.println(secDispatcher.encrypt(cleartext, null)); + return OK; + } +} diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java new file mode 100644 index 000000000000..03d81021d505 --- /dev/null +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java @@ -0,0 +1,151 @@ +/* + * 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.Map; +import java.util.Objects; + +import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker; +import org.apache.maven.cling.invoker.mvnenc.Goal; +import org.codehaus.plexus.components.secdispatcher.Meta; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; +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.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 InitGoal implements Goal { + private final SecDispatcher secDispatcher; + + @Inject + public InitGoal(SecDispatcher secDispatcher) { + this.secDispatcher = secDispatcher; + } + + @Override + public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception { + context.addInHeader(context.style.italic().bold().foreground(Colors.rgbColor("yellow")), "init"); + context.addInHeader(""); + + ConsolePrompt prompt = context.prompt; + boolean force = context.invokerRequest.options().force().orElse(false); + boolean yes = context.invokerRequest.options().yes().orElse(false); + + boolean configExists = secDispatcher.readConfiguration(false) != null; + if (configExists && !force) { + System.out.println("Error: cannot init, configuration exist."); + return BAD_OPERATION; + } + + SettingsSecurity config = secDispatcher.readConfiguration(true); + + Map result = + prompt.prompt(context.header, prompt(prompt).build()); + if (result == null) { + throw new InterruptedException(); + } + config.setDefaultDispatcher(result.get("defaultDispatcher").getResult()); + configureDispatcher( + context, + config, + secDispatcher.availableDispatchers().stream() + .filter(d -> Objects.equals(config.getDefaultDispatcher(), d.name())) + .findFirst() + .orElseThrow()); + + if (yes) { + secDispatcher.writeConfiguration(config); + } else { + ConfirmResult confirm = (ConfirmResult) result.get("confirm"); + if (confirm.getConfirmed() == ConfirmChoice.ConfirmationValue.YES) { + System.out.println("Writing out the configuration..."); + secDispatcher.writeConfiguration(config); + } else { + System.out.println("Values not accepted; not saving configuration."); + return BAD_OPERATION; + } + } + + return OK; + } + + protected PromptBuilder prompt(ConsolePrompt prompt) { + PromptBuilder promptBuilder = prompt.getPromptBuilder(); + dispatcherPrompt(promptBuilder); + promptBuilder + .createConfirmPromp() + .name("confirm") + .message("Are values above correct?") + .defaultValue(ConfirmChoice.ConfirmationValue.YES) + .addPrompt(); + return promptBuilder; + } + + protected void dispatcherPrompt(PromptBuilder promptBuilder) { + ListPromptBuilder listPromptBuilder = promptBuilder + .createListPrompt() + .name("defaultDispatcher") + .message("Which dispatcher you want to use as default?"); + for (Meta meta : secDispatcher.availableDispatchers()) { + listPromptBuilder + .newItem() + .name(meta.name()) + .text(meta.displayName()) + .add(); + } + listPromptBuilder.addPrompt(); + } + + private void configureDispatcher( + DefaultEncryptInvoker.LocalContext context, SettingsSecurity config, Meta dispatcherMeta) throws Exception { + context.addInHeader( + context.style.italic().bold().foreground(Colors.rgbColor("yellow")), + "Configure " + dispatcherMeta.name() + " dispatcher"); + context.addInHeader(""); + PromptBuilder promptBuilder = context.prompt.getPromptBuilder(); + for (Meta.Field fields : dispatcherMeta.fields()) { + + } + cipherPrompt(promptBuilder); + promptBuilder + .createConfirmPromp() + .name("confirm") + .message("Are values above correct?") + .defaultValue(ConfirmChoice.ConfirmationValue.YES) + .addPrompt(); + + Map result = context.prompt.prompt(context.header, promptBuilder.build()); + } +} diff --git a/maven-jline/pom.xml b/maven-jline/pom.xml index 867bc34b41a2..09eb58de33af 100644 --- a/maven-jline/pom.xml +++ b/maven-jline/pom.xml @@ -42,6 +42,18 @@ under the License. org.jline jline-reader + + org.jline + jline-style + + + org.jline + jline-builtins + + + org.jline + jline-console-ui + org.jline jline-terminal-jni diff --git a/pom.xml b/pom.xml index 1526eb14d32b..5f845d1d2a56 100644 --- a/pom.xml +++ b/pom.xml @@ -186,7 +186,7 @@ under the License. 1.4.0 4.0.4 2.0.1 - 3.0.0 + 4.0.0-SNAPSHOT 0.9.0.M3 2.0.16 4.2.2 @@ -475,6 +475,21 @@ under the License. jline-reader ${jlineVersion} + + org.jline + jline-style + ${jlineVersion} + + + org.jline + jline-builtins + ${jlineVersion} + + + org.jline + jline-console-ui + ${jlineVersion} + org.jline jline-terminal-ffm From 2bdb4bd13dc105772abcdb37a4ee657265aefd88 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 10 Oct 2024 13:41:41 +0200 Subject: [PATCH 02/24] More WIP --- apache-maven/src/assembly/component.xml | 1 + .../src/assembly/maven/bin/mvnencDebug | 35 +++ .../src/assembly/maven/bin/mvnencDebug.cmd | 44 ++++ .../maven/api/cli/mvnenc/EncryptOptions.java | 6 +- .../mvnenc/CommonsCliEncryptOptions.java | 25 +- .../invoker/mvnenc/DefaultEncryptInvoker.java | 21 +- .../mvnenc/goals/ConsolePasswordPrompt.java | 66 ++++++ .../cling/invoker/mvnenc/goals/Decrypt.java | 50 ++++ .../cling/invoker/mvnenc/goals/InitGoal.java | 81 +++++-- .../cling/invoker/mvnenc/goals/PinEntry.java | 222 ++++++++++++++++++ .../invoker/mvnenc/goals/PinEntryPrompt.java | 80 +++++++ 11 files changed, 584 insertions(+), 47 deletions(-) create mode 100644 apache-maven/src/assembly/maven/bin/mvnencDebug create mode 100644 apache-maven/src/assembly/maven/bin/mvnencDebug.cmd create mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConsolePasswordPrompt.java create mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Decrypt.java create mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java create mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntryPrompt.java diff --git a/apache-maven/src/assembly/component.xml b/apache-maven/src/assembly/component.xml index 347dee1d5cd1..9a6d61dc5cc5 100644 --- a/apache-maven/src/assembly/component.xml +++ b/apache-maven/src/assembly/component.xml @@ -85,6 +85,7 @@ under the License. mvn mvnenc mvnDebug + mvnencDebug mvnyjp diff --git a/apache-maven/src/assembly/maven/bin/mvnencDebug b/apache-maven/src/assembly/maven/bin/mvnencDebug new file mode 100644 index 000000000000..50b3e6749250 --- /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 000000000000..22a869cd5bd9 --- /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 288fd3624227..49f726a57142 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,15 +36,15 @@ @Experimental public interface EncryptOptions extends Options { /** - * Returns the cipher that the user wants to use for non-dispatched encryption. + * Returns the cipher that the user wants to use for master encryption. * * @return an {@link Optional} containing the cipher string, or empty if not specified */ @Nonnull - Optional cipher(); + Optional masterCipher(); /** - * Returns the master source that the user wants to use for non-dispatched encryption. + * Returns the password source that the user wants to use for master encryption. * * @return an {@link Optional} containing the master source string, or empty if not specified */ 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 4f847b628d60..368db906d9f4 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,9 +72,9 @@ private static CommonsCliEncryptOptions interpolate( } @Override - public Optional cipher() { - if (commandLine.hasOption(CLIManager.CIPHER)) { - return Optional.of(commandLine.getOptionValue(CLIManager.CIPHER)); + public Optional masterCipher() { + if (commandLine.hasOption(CLIManager.MASTER_CIPHER)) { + return Optional.of(commandLine.getOptionValue(CLIManager.MASTER_CIPHER)); } return Optional.empty(); } @@ -125,8 +125,8 @@ public EncryptOptions interpolate(Collection> properties) { } protected static class CLIManager extends CommonsCliOptions.CLIManager { - public static final String CIPHER = "c"; - public static final String MASTER_SOURCE = "m"; + public static final String MASTER_CIPHER = "mc"; + public static final String MASTER_SOURCE = "ms"; public static final String DISPATCHER = "d"; public static final String FORCE = "f"; public static final String YES = "y"; @@ -134,25 +134,28 @@ protected static class CLIManager extends CommonsCliOptions.CLIManager { @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") + options.addOption(Option.builder(MASTER_CIPHER) + .longOpt("master-cipher") + .hasArg() + .desc("The cipher that user wants to use with master dispatcher") .build()); options.addOption(Option.builder(MASTER_SOURCE) .longOpt("master-source") - .desc("The master source that user wants to use for non-dispatched encryption") + .hasArg() + .desc("The master source that user wants to use with master dispatcher") .build()); options.addOption(Option.builder(DISPATCHER) .longOpt("dispatcher") + .hasArg() .desc("The dispatcher to use for dispatched encryption") .build()); options.addOption(Option.builder(FORCE) .longOpt("force") - .desc("Should overwrite without asking any configuration, if exist.") + .desc("Should overwrite without asking any configuration?") .build()); options.addOption(Option.builder(YES) .longOpt("yes") - .desc("Should imply \"yes\" answer to all questions") + .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/DefaultEncryptInvoker.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptInvoker.java index e1dc37eab9d5..c384ca6da151 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 @@ -99,6 +99,9 @@ protected void lookup(LocalContext context) { 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; } @@ -131,17 +134,14 @@ protected int doExecute(LocalContext context) throws Exception { if (context.invokerRequest.options().goals().isEmpty() || context.invokerRequest.options().goals().get().size() != 1) { - System.out.println("No goal or multiple goals specified, specify only one goal. Use -h to see help."); - System.out.println("Supported goals are: " + context.goals.keySet()); - return BAD_OPERATION; + return badGoalsErrorMessage("No goal or multiple goals specified, specify only one goal.", context); } - Goal goal = context.goals.get( - context.invokerRequest.options().goals().get().get(0)); + String goalName = context.invokerRequest.options().goals().get().get(0); + Goal goal = context.goals.get(goalName); if (goal == null) { - System.out.println("Unknown goal, supported goals are: " + context.goals.keySet()); - return BAD_OPERATION; + return badGoalsErrorMessage("Unknown goal: " + goalName, context); } return goal.execute(context); @@ -153,4 +153,11 @@ protected int doExecute(LocalContext context) throws Exception { return ERROR; } } + + 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/goals/ConsolePasswordPrompt.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConsolePasswordPrompt.java new file mode 100644 index 000000000000..1b406ae2f9cd --- /dev/null +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConsolePasswordPrompt.java @@ -0,0 +1,66 @@ +/* + * 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.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.SecDispatcherException; + +@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 configTemplate() { + return Optional.empty(); + } + + @Override + public String handle(String s) throws SecDispatcherException { + if (NAME.equals(s)) { + try { + return prompter.promptForPassword("Enter the master password: "); + } catch (PrompterException e) { + throw new SecDispatcherException("Could not collect the password", e); + } + } + return null; + } +} 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 000000000000..322fe1fe8378 --- /dev/null +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Decrypt.java @@ -0,0 +1,50 @@ +/* + * 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.cling.invoker.mvnenc.DefaultEncryptInvoker; +import org.apache.maven.cling.invoker.mvnenc.Goal; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; + +import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK; + +/** + * The "decrypt" goal. + */ +@Singleton +@Named("decrypt") +public class Decrypt implements Goal { + private final SecDispatcher secDispatcher; + + @Inject + public Decrypt(SecDispatcher secDispatcher) { + this.secDispatcher = secDispatcher; + } + + @Override + public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception { + String encrypted = context.reader.readLine("Enter the password to decrypt: "); + System.out.println(secDispatcher.decrypt(encrypted)); + return OK; + } +} diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java index 03d81021d505..553c6ac16ec1 100644 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java @@ -27,8 +27,10 @@ import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker; import org.apache.maven.cling.invoker.mvnenc.Goal; -import org.codehaus.plexus.components.secdispatcher.Meta; +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; @@ -56,7 +58,7 @@ public InitGoal(SecDispatcher secDispatcher) { @Override public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception { - context.addInHeader(context.style.italic().bold().foreground(Colors.rgbColor("yellow")), "init"); + context.addInHeader(context.style.italic().bold().foreground(Colors.rgbColor("yellow")), "goal: init"); context.addInHeader(""); ConsolePrompt prompt = context.prompt; @@ -65,29 +67,62 @@ public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception boolean configExists = secDispatcher.readConfiguration(false) != null; if (configExists && !force) { - System.out.println("Error: cannot init, configuration exist."); + System.out.println("Error: configuration exist. Use --force if you want to reset existing configuration."); return BAD_OPERATION; } SettingsSecurity config = secDispatcher.readConfiguration(true); - Map result = - prompt.prompt(context.header, prompt(prompt).build()); + // reset config + config.setDefaultDispatcher(null); + config.getConfigurations().clear(); + + Map result = prompt.prompt( + context.header, dispatcherPrompt(prompt.getPromptBuilder()).build()); if (result == null) { throw new InterruptedException(); } config.setDefaultDispatcher(result.get("defaultDispatcher").getResult()); - configureDispatcher( - context, - config, - secDispatcher.availableDispatchers().stream() - .filter(d -> Objects.equals(config.getDefaultDispatcher(), d.name())) - .findFirst() - .orElseThrow()); + + 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(); + } + + 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()); + } + if (!dispatcherConfig.getProperties().isEmpty()) { + config.addConfiguration(dispatcherConfig); + } + } + + System.out.println(); + System.out.println("Values set:"); + System.out.println("defaultDispatcher=" + config.getDefaultDispatcher()); + for (Config c : config.getConfigurations()) { + System.out.println(" dispatcherName=" + c.getName()); + for (ConfigProperty cp : c.getProperties()) { + System.out.println(" " + cp.getName() + "=" + cp.getValue()); + } + } if (yes) { secDispatcher.writeConfiguration(config); } else { + result = prompt.prompt(confirmPrompt(prompt.getPromptBuilder()).build()); ConfirmResult confirm = (ConfirmResult) result.get("confirm"); if (confirm.getConfirmed() == ConfirmChoice.ConfirmationValue.YES) { System.out.println("Writing out the configuration..."); @@ -101,9 +136,7 @@ public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception return OK; } - protected PromptBuilder prompt(ConsolePrompt prompt) { - PromptBuilder promptBuilder = prompt.getPromptBuilder(); - dispatcherPrompt(promptBuilder); + protected PromptBuilder confirmPrompt(PromptBuilder promptBuilder) { promptBuilder .createConfirmPromp() .name("confirm") @@ -113,12 +146,12 @@ protected PromptBuilder prompt(ConsolePrompt prompt) { return promptBuilder; } - protected void dispatcherPrompt(PromptBuilder promptBuilder) { + protected PromptBuilder dispatcherPrompt(PromptBuilder promptBuilder) { ListPromptBuilder listPromptBuilder = promptBuilder .createListPrompt() .name("defaultDispatcher") .message("Which dispatcher you want to use as default?"); - for (Meta meta : secDispatcher.availableDispatchers()) { + for (DispatcherMeta meta : secDispatcher.availableDispatchers()) { listPromptBuilder .newItem() .name(meta.name()) @@ -126,26 +159,22 @@ protected void dispatcherPrompt(PromptBuilder promptBuilder) { .add(); } listPromptBuilder.addPrompt(); + return promptBuilder; } - private void configureDispatcher( - DefaultEncryptInvoker.LocalContext context, SettingsSecurity config, Meta dispatcherMeta) throws Exception { + private PromptBuilder configureDispatcher( + DefaultEncryptInvoker.LocalContext context, DispatcherMeta dispatcherMeta, PromptBuilder promptBuilder) + throws Exception { context.addInHeader( context.style.italic().bold().foreground(Colors.rgbColor("yellow")), "Configure " + dispatcherMeta.name() + " dispatcher"); context.addInHeader(""); - PromptBuilder promptBuilder = context.prompt.getPromptBuilder(); - for (Meta.Field fields : dispatcherMeta.fields()) { - - } - cipherPrompt(promptBuilder); promptBuilder .createConfirmPromp() .name("confirm") .message("Are values above correct?") .defaultValue(ConfirmChoice.ConfirmationValue.YES) .addPrompt(); - - Map result = context.prompt.prompt(context.header, promptBuilder.build()); + return promptBuilder; } } diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java new file mode 100644 index 000000000000..65b6462a25b7 --- /dev/null +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java @@ -0,0 +1,222 @@ +/* + * 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.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 static java.util.Objects.requireNonNull; + +/** + * Inspired by A peek inside pinentry + */ +public class PinEntry { + public enum Outcome { + SUCCESS, + TIMEOUT, + NOT_CONFIRMED, + CANCELED, + FAILED; + } + + public record Result(Outcome outcome, T payload) {} + + private final String cmd; + private final LinkedHashMap commands; + + public PinEntry(String cmd) { + this.cmd = requireNonNull(cmd); + this.commands = new LinkedHashMap<>(); + } + + public PinEntry setKeyInfo(String keyInfo) { + requireNonNull(keyInfo); + commands.put("SETKEYINFO", keyInfo); + commands.put("OPTION", "allow-external-password-cache"); + return this; + } + + public PinEntry setOk(String msg) { + requireNonNull(msg); + commands.put("SETOK", msg); + return this; + } + + public PinEntry setCancel(String msg) { + requireNonNull(msg); + commands.put("SETCANCEL", msg); + return this; + } + + public PinEntry setTitle(String title) { + requireNonNull(title); + commands.put("SETTITLE", title); + return this; + } + + public PinEntry setDescription(String desc) { + requireNonNull(desc); + commands.put("SETDESC", desc); + return this; + } + + public PinEntry setPrompt(String prompt) { + requireNonNull(prompt); + commands.put("SETPROMPT", prompt); + return this; + } + + public PinEntry confirmPin() { + commands.put("SETREPEAT", cmd); + return this; + } + + 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; + } + + public Result getPin() throws IOException { + commands.put("GETPIN", null); + return execute(); + } + + public Result confirm() throws IOException { + commands.put("CONFIRM", null); + return execute(); + } + + 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 lastEntry = commands.entrySet().iterator().next(); + for (Map.Entry entry : commands.entrySet()) { + String cmd; + if (entry.getValue() != null) { + cmd = entry.getKey() + " " + entry.getValue(); + } else { + cmd = entry.getKey(); + } + 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(); + 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(); + 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 { + Result pinResult = new PinEntry("/usr/bin/pinentry-gnome3") + .setTimeout(Duration.ofSeconds(5)) + .setKeyInfo("maven:masterPassword") + .setTitle("Maven Master Password") + .setDescription("Please enter the Maven master password") + .setPrompt("Master password") + .setOk("Her 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("/usr/bin/pinentry-gnome3") + .setTitle("Password confirmation") + .setDescription("Please confirm that the password you entered is correct") + .setPrompt("Is the password '" + pinResult.payload() + "' the one you want?") + .confirm(); + if (confirmResult.outcome() == Outcome.SUCCESS) { + new PinEntry("/usr/bin/pinentry-gnome3") + .setTitle("Password confirmed") + .setDescription("You confirmed your password") + .setPrompt("The password '" + pinResult.payload() + "' is confirmed.") + .confirm(); + } else { + System.out.println(confirmResult); + } + } else { + System.out.println(pinResult); + } + } +} diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntryPrompt.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntryPrompt.java new file mode 100644 index 000000000000..6a8524632e2b --- /dev/null +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntryPrompt.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.goals; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.io.IOException; +import java.time.Duration; +import java.util.Optional; + +import org.codehaus.plexus.components.secdispatcher.MasterSource; +import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta; +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; +import org.codehaus.plexus.components.secdispatcher.internal.sources.PrefixMasterSourceSupport; + +/** + * Inspired by A peek inside pinentry + */ +@Singleton +@Named(PinEntryPrompt.NAME) +public class PinEntryPrompt extends PrefixMasterSourceSupport implements MasterSource, MasterSourceMeta { + public static final String NAME = "pinentry-prompt"; + + public PinEntryPrompt() { + super(NAME + ":"); + } + + @Override + public String description() { + return "Uses selected pinentry application to ask for master password"; + } + + @Override + public Optional configTemplate() { + return Optional.of(NAME + ":" + "$pinentryPath"); + } + + @Override + public String doHandle(String s) throws SecDispatcherException { + try { + PinEntry.Result result = new PinEntry(s) + .setTimeout(Duration.ofSeconds(5)) + .setKeyInfo("maven:masterPassword") + .setTitle("Maven Master Password") + .setDescription("Please enter the Maven master password") + .setPrompt("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); + } + } +} From 2010d5280bd950bb9c9c5d5ed3f6fb9993bb3e0f Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 10 Oct 2024 15:22:37 +0200 Subject: [PATCH 03/24] WIP --- .../org/apache/maven/cling/MavenEncCling.java | 23 ++++++- .../invoker/mvnenc/DefaultEncryptInvoker.java | 16 +++-- .../cling/invoker/mvnenc/goals/InitGoal.java | 66 +++++++++++++++---- .../invoker/mvnenc/goals/PinEntryPrompt.java | 4 +- 4 files changed, 90 insertions(+), 19 deletions(-) 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 488eb9eae44a..72cb51ea2c76 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.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 static int main(String[] args, ClassWorld world) throws IOException { return new MavenEncCling(world).run(args); } + private Terminal terminal; + public MavenEncCling() { super(); } @@ -60,10 +65,24 @@ public MavenEncCling(ClassWorld classWorld) { 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 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/DefaultEncryptInvoker.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptInvoker.java index c384ca6da151..82566bbe3f75 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,6 +18,7 @@ */ package org.apache.maven.cling.invoker.mvnenc; +import java.io.InterruptedIOException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -31,8 +32,8 @@ 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.terminal.TerminalBuilder; import org.jline.utils.AttributedString; import org.jline.utils.AttributedStringBuilder; import org.jline.utils.AttributedStyle; @@ -72,8 +73,11 @@ public void addInHeader(AttributedStyle style, String text) { } } + private final Terminal terminal; + public DefaultEncryptInvoker(ProtoLookup protoLookup) { super(protoLookup); + this.terminal = protoLookup.lookup(Terminal.class); } @Override @@ -113,7 +117,7 @@ protected int doExecute(LocalContext context) throws Exception { context.addInHeader("Tool for secure password management on workstations."); context.addInHeader("This tool is part of Apache Maven 4 distribution."); context.addInHeader(""); - try (Terminal terminal = TerminalBuilder.builder().build()) { + try { Thread executeThread = Thread.currentThread(); terminal.handle(Terminal.Signal.INT, signal -> executeThread.interrupt()); ConsolePrompt.UiConfig config; @@ -145,11 +149,15 @@ protected int doExecute(LocalContext context) throws Exception { } return goal.execute(context); - } catch (InterruptedException e) { + } catch (InterruptedException | InterruptedIOException | UserInterruptException e) { System.out.println("Goal canceled by user."); return CANCELED; } catch (Exception e) { - context.logger.error(e.getMessage(), e); + if (context.invokerRequest.options().showErrors().orElse(false)) { + context.logger.error(e.getMessage(), e); + } else { + context.logger.error(e.getMessage()); + } return ERROR; } } diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java index 553c6ac16ec1..68ae18da4ca9 100644 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java @@ -97,31 +97,38 @@ public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception throw new InterruptedException(); } + ConfirmResult confirm = (ConfirmResult) result.get("confirm"); + if (confirm.getConfirmed() != ConfirmChoice.ConfirmationValue.YES) { + System.out.println("Values not accepted; not saving configuration."); + return BAD_OPERATION; + } + 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); } } - System.out.println(); - System.out.println("Values set:"); - System.out.println("defaultDispatcher=" + config.getDefaultDispatcher()); - for (Config c : config.getConfigurations()) { - System.out.println(" dispatcherName=" + c.getName()); - for (ConfigProperty cp : c.getProperties()) { - System.out.println(" " + cp.getName() + "=" + cp.getValue()); - } - } - 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(confirmPrompt(prompt.getPromptBuilder()).build()); ConfirmResult confirm = (ConfirmResult) result.get("confirm"); if (confirm.getConfirmed() == ConfirmChoice.ConfirmationValue.YES) { @@ -167,8 +174,45 @@ private PromptBuilder configureDispatcher( throws Exception { context.addInHeader( context.style.italic().bold().foreground(Colors.rgbColor("yellow")), - "Configure " + dispatcherMeta.name() + " dispatcher"); + "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(); + } + } + promptBuilder .createConfirmPromp() .name("confirm") diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntryPrompt.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntryPrompt.java index 6a8524632e2b..ff576a6f3331 100644 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntryPrompt.java +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntryPrompt.java @@ -44,7 +44,7 @@ public PinEntryPrompt() { @Override public String description() { - return "Uses selected pinentry application to ask for master password"; + return "Secure PinEntry prompt"; } @Override @@ -56,7 +56,7 @@ public Optional configTemplate() { public String doHandle(String s) throws SecDispatcherException { try { PinEntry.Result result = new PinEntry(s) - .setTimeout(Duration.ofSeconds(5)) + .setTimeout(Duration.ofSeconds(30)) .setKeyInfo("maven:masterPassword") .setTitle("Maven Master Password") .setDescription("Please enter the Maven master password") From 6ae219e795d71325fedd8cfaf94229fd7dafc025 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 10 Oct 2024 15:41:21 +0200 Subject: [PATCH 04/24] Add logging --- .../apache/maven/cling/invoker/mvnenc/goals/PinEntry.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java index 65b6462a25b7..b1408cfd24b2 100644 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java @@ -18,6 +18,9 @@ */ package org.apache.maven.cling.invoker.mvnenc.goals; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; @@ -42,6 +45,7 @@ public enum Outcome { public record Result(Outcome outcome, T payload) {} + private final Logger logger = LoggerFactory.getLogger(getClass()); private final String cmd; private final LinkedHashMap commands; @@ -129,6 +133,7 @@ private Result execute() throws IOException { } else { cmd = entry.getKey(); } + logger.debug("> {}", cmd); writer.write(cmd); writer.newLine(); writer.flush(); @@ -155,6 +160,7 @@ private Result execute() throws IOException { 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); } @@ -163,6 +169,7 @@ private void expectOK(BufferedReader in) throws IOException { private Result lastExpect(BufferedReader in) throws IOException { while (true) { String response = in.readLine(); + logger.debug("< {}", response); if (response.startsWith("#")) { continue; } From 5575195f64bd19d781f70ea606b55a4c7fa28e1b Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 10 Oct 2024 15:42:33 +0200 Subject: [PATCH 05/24] Reformat --- .../apache/maven/cling/invoker/mvnenc/goals/PinEntry.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java index b1408cfd24b2..316d007313b5 100644 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java @@ -18,9 +18,6 @@ */ package org.apache.maven.cling.invoker.mvnenc.goals; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; @@ -29,6 +26,9 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import static java.util.Objects.requireNonNull; /** From 6682949d56862148d4fb00ea960420b136c00749 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 10 Oct 2024 16:32:11 +0200 Subject: [PATCH 06/24] Moved pinetry to sec dispatcher --- .../mvnenc/goals/ConsolePasswordPrompt.java | 3 + .../cling/invoker/mvnenc/goals/PinEntry.java | 229 ------------------ .../invoker/mvnenc/goals/PinEntryPrompt.java | 80 ------ 3 files changed, 3 insertions(+), 309 deletions(-) delete mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java delete mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntryPrompt.java diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConsolePasswordPrompt.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConsolePasswordPrompt.java index 1b406ae2f9cd..9fc03d525fcd 100644 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConsolePasswordPrompt.java +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConsolePasswordPrompt.java @@ -30,6 +30,9 @@ import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta; 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 { diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java deleted file mode 100644 index 316d007313b5..000000000000 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntry.java +++ /dev/null @@ -1,229 +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.apache.maven.cling.invoker.mvnenc.goals; - -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 peek inside pinentry - */ -public class PinEntry { - public enum Outcome { - SUCCESS, - TIMEOUT, - NOT_CONFIRMED, - CANCELED, - FAILED; - } - - public record Result(Outcome outcome, T payload) {} - - private final Logger logger = LoggerFactory.getLogger(getClass()); - private final String cmd; - private final LinkedHashMap commands; - - public PinEntry(String cmd) { - this.cmd = requireNonNull(cmd); - this.commands = new LinkedHashMap<>(); - } - - public PinEntry setKeyInfo(String keyInfo) { - requireNonNull(keyInfo); - commands.put("SETKEYINFO", keyInfo); - commands.put("OPTION", "allow-external-password-cache"); - return this; - } - - public PinEntry setOk(String msg) { - requireNonNull(msg); - commands.put("SETOK", msg); - return this; - } - - public PinEntry setCancel(String msg) { - requireNonNull(msg); - commands.put("SETCANCEL", msg); - return this; - } - - public PinEntry setTitle(String title) { - requireNonNull(title); - commands.put("SETTITLE", title); - return this; - } - - public PinEntry setDescription(String desc) { - requireNonNull(desc); - commands.put("SETDESC", desc); - return this; - } - - public PinEntry setPrompt(String prompt) { - requireNonNull(prompt); - commands.put("SETPROMPT", prompt); - return this; - } - - public PinEntry confirmPin() { - commands.put("SETREPEAT", cmd); - return this; - } - - 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; - } - - public Result getPin() throws IOException { - commands.put("GETPIN", null); - return execute(); - } - - public Result confirm() throws IOException { - commands.put("CONFIRM", null); - return execute(); - } - - 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 lastEntry = commands.entrySet().iterator().next(); - for (Map.Entry 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 { - Result pinResult = new PinEntry("/usr/bin/pinentry-gnome3") - .setTimeout(Duration.ofSeconds(5)) - .setKeyInfo("maven:masterPassword") - .setTitle("Maven Master Password") - .setDescription("Please enter the Maven master password") - .setPrompt("Master password") - .setOk("Her 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("/usr/bin/pinentry-gnome3") - .setTitle("Password confirmation") - .setDescription("Please confirm that the password you entered is correct") - .setPrompt("Is the password '" + pinResult.payload() + "' the one you want?") - .confirm(); - if (confirmResult.outcome() == Outcome.SUCCESS) { - new PinEntry("/usr/bin/pinentry-gnome3") - .setTitle("Password confirmed") - .setDescription("You confirmed your password") - .setPrompt("The password '" + pinResult.payload() + "' is confirmed.") - .confirm(); - } else { - System.out.println(confirmResult); - } - } else { - System.out.println(pinResult); - } - } -} diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntryPrompt.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntryPrompt.java deleted file mode 100644 index ff576a6f3331..000000000000 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/PinEntryPrompt.java +++ /dev/null @@ -1,80 +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.apache.maven.cling.invoker.mvnenc.goals; - -import javax.inject.Named; -import javax.inject.Singleton; - -import java.io.IOException; -import java.time.Duration; -import java.util.Optional; - -import org.codehaus.plexus.components.secdispatcher.MasterSource; -import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta; -import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; -import org.codehaus.plexus.components.secdispatcher.internal.sources.PrefixMasterSourceSupport; - -/** - * Inspired by A peek inside pinentry - */ -@Singleton -@Named(PinEntryPrompt.NAME) -public class PinEntryPrompt extends PrefixMasterSourceSupport implements MasterSource, MasterSourceMeta { - public static final String NAME = "pinentry-prompt"; - - public PinEntryPrompt() { - super(NAME + ":"); - } - - @Override - public String description() { - return "Secure PinEntry prompt"; - } - - @Override - public Optional 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:masterPassword") - .setTitle("Maven Master Password") - .setDescription("Please enter the Maven master password") - .setPrompt("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); - } - } -} From db1f5ff6bece710b890eb684b5106e56f6ba19ca Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 11 Oct 2024 14:39:50 +0200 Subject: [PATCH 07/24] Adopt latest sec-dispatcher changes --- maven-api-impl/pom.xml | 8 ++++ .../impl/MavenSecDispatcherProvider.java | 46 +++++++++++++++++++ .../crypto/DefaultSettingsDecrypter.java | 9 ++-- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/MavenSecDispatcherProvider.java diff --git a/maven-api-impl/pom.xml b/maven-api-impl/pom.xml index 898c823ff057..930e88425b88 100644 --- a/maven-api-impl/pom.xml +++ b/maven-api-impl/pom.xml @@ -107,6 +107,14 @@ under the License. org.apache.maven.resolver maven-resolver-impl + + org.codehaus.plexus + plexus-sec-dispatcher + + + org.codehaus.plexus + plexus-cipher + org.junit.jupiter diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/MavenSecDispatcherProvider.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/MavenSecDispatcherProvider.java new file mode 100644 index 000000000000..80904d62d003 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/MavenSecDispatcherProvider.java @@ -0,0 +1,46 @@ +/* + * 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.internal.impl; + +import java.nio.file.Paths; +import java.util.Map; + +import org.apache.maven.api.Constants; +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Provides; +import org.apache.maven.api.di.Singleton; +import org.codehaus.plexus.components.cipher.PlexusCipher; +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}. + */ +@Named +public class MavenSecDispatcherProvider { + @Singleton + @Provides + public static SecDispatcher secDispatcher(PlexusCipher plexusCipher, Map dispatchers) { + return new DefaultSecDispatcher( + plexusCipher, + dispatchers, + Paths.get(System.getProperty(Constants.MAVEN_USER_CONF), "settings-security.xml")); + } +} 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 3695d6ab3b45..64cc67e875a5 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.Named; import javax.inject.Singleton; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -60,7 +61,7 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { try { server.setPassword(decrypt(server.getPassword())); - } catch (SecDispatcherException e) { + } catch (SecDispatcherException | IOException e) { problems.add(new DefaultSettingsProblem( "Failed to decrypt password for server " + server.getId() + ": " + e.getMessage(), Severity.ERROR, @@ -72,7 +73,7 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { try { server.setPassphrase(decrypt(server.getPassphrase())); - } catch (SecDispatcherException e) { + } catch (SecDispatcherException | IOException e) { problems.add(new DefaultSettingsProblem( "Failed to decrypt passphrase for server " + server.getId() + ": " + e.getMessage(), Severity.ERROR, @@ -90,7 +91,7 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { for (Proxy proxy : request.getProxies()) { try { proxy.setPassword(decrypt(proxy.getPassword())); - } catch (SecDispatcherException e) { + } catch (SecDispatcherException | IOException e) { problems.add(new DefaultSettingsProblem( "Failed to decrypt password for proxy " + proxy.getId() + ": " + e.getMessage(), Severity.ERROR, @@ -106,7 +107,7 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { return new DefaultSettingsDecryptionResult(servers, proxies, problems); } - private String decrypt(String str) throws SecDispatcherException { + private String decrypt(String str) throws SecDispatcherException, IOException { return (str == null) ? null : securityDispatcher.decrypt(str); } } From 8cc2a01481b8bb6a2e404fa55e07bae6c8af73b5 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 11 Oct 2024 15:49:14 +0200 Subject: [PATCH 08/24] Move the component to final place and make it deprecated --- maven-api-impl/pom.xml | 8 ----- .../settings/crypto/MavenSecDispatcher.java | 35 ++++++++++++------- 2 files changed, 23 insertions(+), 20 deletions(-) rename maven-api-impl/src/main/java/org/apache/maven/internal/impl/MavenSecDispatcherProvider.java => maven-settings-builder/src/main/java/org/apache/maven/settings/crypto/MavenSecDispatcher.java (60%) diff --git a/maven-api-impl/pom.xml b/maven-api-impl/pom.xml index 930e88425b88..898c823ff057 100644 --- a/maven-api-impl/pom.xml +++ b/maven-api-impl/pom.xml @@ -107,14 +107,6 @@ under the License. org.apache.maven.resolver maven-resolver-impl - - org.codehaus.plexus - plexus-sec-dispatcher - - - org.codehaus.plexus - plexus-cipher - org.junit.jupiter diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/MavenSecDispatcherProvider.java b/maven-settings-builder/src/main/java/org/apache/maven/settings/crypto/MavenSecDispatcher.java similarity index 60% rename from maven-api-impl/src/main/java/org/apache/maven/internal/impl/MavenSecDispatcherProvider.java rename to maven-settings-builder/src/main/java/org/apache/maven/settings/crypto/MavenSecDispatcher.java index 80904d62d003..a53f89d73ce2 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/MavenSecDispatcherProvider.java +++ b/maven-settings-builder/src/main/java/org/apache/maven/settings/crypto/MavenSecDispatcher.java @@ -16,15 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.internal.impl; +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.apache.maven.api.di.Named; -import org.apache.maven.api.di.Provides; -import org.apache.maven.api.di.Singleton; import org.codehaus.plexus.components.cipher.PlexusCipher; import org.codehaus.plexus.components.secdispatcher.Dispatcher; import org.codehaus.plexus.components.secdispatcher.SecDispatcher; @@ -32,15 +34,24 @@ /** * This class implements "Maven specific" {@link SecDispatcher}. + * + * @deprecated since 4.0.0 */ @Named -public class MavenSecDispatcherProvider { - @Singleton - @Provides - public static SecDispatcher secDispatcher(PlexusCipher plexusCipher, Map dispatchers) { - return new DefaultSecDispatcher( - plexusCipher, - dispatchers, - Paths.get(System.getProperty(Constants.MAVEN_USER_CONF), "settings-security.xml")); +@Singleton +@Deprecated(since = "4.0.0") +public class MavenSecDispatcher extends DefaultSecDispatcher { + @Inject + public MavenSecDispatcher(PlexusCipher cipher, Map dispatchers) { + super(cipher, dispatchers, configurationFile()); + } + + private static Path configurationFile() { + String mavenUserConf = System.getProperty(Constants.MAVEN_USER_CONF); + if (mavenUserConf != null) { + return Paths.get(mavenUserConf, "settings-security.xml"); + } + // this means we are in UT or alike + return Paths.get(System.getProperty("user.home"), ".m2", "settings-security.xml"); } } From 9c79881f4a30259b53bdf5adade8e6ac98f085c2 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 11 Oct 2024 15:56:49 +0200 Subject: [PATCH 09/24] Tune wizard a bit --- .../cling/invoker/mvnenc/goals/InitGoal.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java index 68ae18da4ca9..1f65fa651a68 100644 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java @@ -97,12 +97,6 @@ public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception throw new InterruptedException(); } - ConfirmResult confirm = (ConfirmResult) result.get("confirm"); - if (confirm.getConfirmed() != ConfirmChoice.ConfirmationValue.YES) { - System.out.println("Values not accepted; not saving configuration."); - return BAD_OPERATION; - } - Config dispatcherConfig = new Config(); dispatcherConfig.setName(meta.name()); for (DispatcherMeta.Field field : meta.fields()) { @@ -129,7 +123,8 @@ public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception } } - result = prompt.prompt(confirmPrompt(prompt.getPromptBuilder()).build()); + result = prompt.prompt( + context.header, confirmPrompt(prompt.getPromptBuilder()).build()); ConfirmResult confirm = (ConfirmResult) result.get("confirm"); if (confirm.getConfirmed() == ConfirmChoice.ConfirmationValue.YES) { System.out.println("Writing out the configuration..."); @@ -212,13 +207,6 @@ private PromptBuilder configureDispatcher( .addPrompt(); } } - - promptBuilder - .createConfirmPromp() - .name("confirm") - .message("Are values above correct?") - .defaultValue(ConfirmChoice.ConfirmationValue.YES) - .addPrompt(); return promptBuilder; } } From ee9e0602e964232753c1131b9d863f897e6fa036 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 11 Oct 2024 16:40:07 +0200 Subject: [PATCH 10/24] Allow editing of template values (those having $ in them) --- .../cling/invoker/mvnenc/goals/InitGoal.java | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java index 1f65fa651a68..765962b5eb67 100644 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java @@ -22,6 +22,7 @@ import javax.inject.Named; import javax.inject.Singleton; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -38,6 +39,10 @@ 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; @@ -77,7 +82,7 @@ public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception config.setDefaultDispatcher(null); config.getConfigurations().clear(); - Map result = prompt.prompt( + Map result = prompt.prompt( context.header, dispatcherPrompt(prompt.getPromptBuilder()).build()); if (result == null) { throw new InterruptedException(); @@ -97,6 +102,41 @@ public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception throw new InterruptedException(); } + List> editables = result.entrySet().stream() + .filter(e -> e.getValue().getResult().contains("$")) + .toList(); + if (!editables.isEmpty()) { + context.addInHeader(""); + context.addInHeader("Please customize the editable value:"); + Map editMap; + for (Map.Entry 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 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()) { From f77620761d69ec2258ca078890be5d9937118018 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 11 Oct 2024 16:52:14 +0200 Subject: [PATCH 11/24] Alter filename to not overwrite any existing file --- .../apache/maven/settings/crypto/MavenSecDispatcher.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 index a53f89d73ce2..d78ba17efcb6 100644 --- 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 @@ -41,6 +41,8 @@ @Singleton @Deprecated(since = "4.0.0") public class MavenSecDispatcher extends DefaultSecDispatcher { + private static final String FILE_NAME = "settings-security4.xml"; + @Inject public MavenSecDispatcher(PlexusCipher cipher, Map dispatchers) { super(cipher, dispatchers, configurationFile()); @@ -49,9 +51,9 @@ public MavenSecDispatcher(PlexusCipher cipher, Map dispatche private static Path configurationFile() { String mavenUserConf = System.getProperty(Constants.MAVEN_USER_CONF); if (mavenUserConf != null) { - return Paths.get(mavenUserConf, "settings-security.xml"); + return Paths.get(mavenUserConf, FILE_NAME); } // this means we are in UT or alike - return Paths.get(System.getProperty("user.home"), ".m2", "settings-security.xml"); + return Paths.get(System.getProperty("user.home"), ".m2", FILE_NAME); } } From 685957de4f321bf206e93c8760ca2fa8d2920c5b Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 11 Oct 2024 17:04:40 +0200 Subject: [PATCH 12/24] Drop unused options We REQUIRE interactive for init wizard, and these params made no sense. So let's drop them for now, will return if needed. --- .../maven/api/cli/mvnenc/EncryptOptions.java | 24 ----------- .../mvnenc/CommonsCliEncryptOptions.java | 42 ------------------- 2 files changed, 66 deletions(-) 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 49f726a57142..910d0375eaaf 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 @@ -35,30 +35,6 @@ */ @Experimental public interface EncryptOptions extends Options { - /** - * Returns the cipher that the user wants to use for master encryption. - * - * @return an {@link Optional} containing the cipher string, or empty if not specified - */ - @Nonnull - Optional masterCipher(); - - /** - * Returns the password source that the user wants to use for master encryption. - * - * @return an {@link Optional} containing the master source string, or empty if not specified - */ - @Nonnull - Optional masterSource(); - - /** - * Returns the dispatcher to use for dispatched encryption. - * - * @return an {@link Optional} containing the dispatcher string, or empty if not specified - */ - @Nonnull - Optional dispatcher(); - /** * Should the operation be forced (ie overwrite existing config, if any). * 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 368db906d9f4..fcf964fee85c 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 @@ -71,30 +71,6 @@ private static CommonsCliEncryptOptions interpolate( } } - @Override - public Optional masterCipher() { - if (commandLine.hasOption(CLIManager.MASTER_CIPHER)) { - return Optional.of(commandLine.getOptionValue(CLIManager.MASTER_CIPHER)); - } - return Optional.empty(); - } - - @Override - public Optional masterSource() { - if (commandLine.hasOption(CLIManager.MASTER_SOURCE)) { - return Optional.of(commandLine.getOptionValue(CLIManager.MASTER_SOURCE)); - } - return Optional.empty(); - } - - @Override - public Optional dispatcher() { - if (commandLine.hasOption(CLIManager.DISPATCHER)) { - return Optional.of(commandLine.getOptionValue(CLIManager.DISPATCHER)); - } - return Optional.empty(); - } - @Override public Optional force() { if (commandLine.hasOption(CLIManager.FORCE)) { @@ -125,30 +101,12 @@ public EncryptOptions interpolate(Collection> properties) { } protected static class CLIManager extends CommonsCliOptions.CLIManager { - public static final String MASTER_CIPHER = "mc"; - public static final String MASTER_SOURCE = "ms"; - 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(MASTER_CIPHER) - .longOpt("master-cipher") - .hasArg() - .desc("The cipher that user wants to use with master dispatcher") - .build()); - options.addOption(Option.builder(MASTER_SOURCE) - .longOpt("master-source") - .hasArg() - .desc("The master source that user wants to use with master dispatcher") - .build()); - options.addOption(Option.builder(DISPATCHER) - .longOpt("dispatcher") - .hasArg() - .desc("The dispatcher to use for dispatched encryption") - .build()); options.addOption(Option.builder(FORCE) .longOpt("force") .desc("Should overwrite without asking any configuration?") From f1a2e436345b65b363bfcf16aa23a09405617d3c Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 11 Oct 2024 19:15:52 +0200 Subject: [PATCH 13/24] Multiple things: * init goal obey "hidden" meta (so LEGACY cannot be set as def dispatcher) * settings decrypter create WARN severity problems if legacy pw found * session factory ALWAYS emits problems, unlike in mvn3 when these are logging ONLY in -X mode --- .../cling/invoker/mvnenc/goals/InitGoal.java | 12 ++++--- ...DefaultRepositorySystemSessionFactory.java | 13 +++++--- .../crypto/DefaultSettingsDecrypter.java | 31 +++++++++++++++++++ 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java index 765962b5eb67..0091ba02d16b 100644 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java @@ -194,11 +194,13 @@ protected PromptBuilder dispatcherPrompt(PromptBuilder promptBuilder) { .name("defaultDispatcher") .message("Which dispatcher you want to use as default?"); for (DispatcherMeta meta : secDispatcher.availableDispatchers()) { - listPromptBuilder - .newItem() - .name(meta.name()) - .text(meta.displayName()) - .add(); + if (!meta.isHidden()) { + listPromptBuilder + .newItem() + .name(meta.name()) + .text(meta.displayName()) + .add(); + } } listPromptBuilder.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 f49ef3b727b2..9c214a5da41a 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 SessionBuilder newRepositorySessionBuilder(MavenExecutionRequest request) 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-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 64cc67e875a5..a1f41d474eaa 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 @@ -60,6 +60,15 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { server = server.clone(); try { + if (isLegacy(server.getPassword())) { + problems.add(new DefaultSettingsProblem( + "Legacy/insecurely encrypted password detected for server " + server.getId(), + Severity.WARNING, + "server: " + server.getId(), + -1, + -1, + null)); + } server.setPassword(decrypt(server.getPassword())); } catch (SecDispatcherException | IOException e) { problems.add(new DefaultSettingsProblem( @@ -72,6 +81,15 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { } try { + if (isLegacy(server.getPassphrase())) { + problems.add(new DefaultSettingsProblem( + "Legacy/insecurely encrypted passphrase detected for server " + server.getId(), + Severity.WARNING, + "server: " + server.getId(), + -1, + -1, + null)); + } server.setPassphrase(decrypt(server.getPassphrase())); } catch (SecDispatcherException | IOException e) { problems.add(new DefaultSettingsProblem( @@ -90,6 +108,15 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { for (Proxy proxy : request.getProxies()) { try { + if (isLegacy(proxy.getPassword())) { + problems.add(new DefaultSettingsProblem( + "Legacy/insecurely encrypted password detected for proxy " + proxy.getId(), + Severity.WARNING, + "proxy: " + proxy.getId(), + -1, + -1, + null)); + } proxy.setPassword(decrypt(proxy.getPassword())); } catch (SecDispatcherException | IOException e) { problems.add(new DefaultSettingsProblem( @@ -107,6 +134,10 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { return new DefaultSettingsDecryptionResult(servers, proxies, problems); } + private boolean isLegacy(String str) { + return str != null && securityDispatcher.isLegacyPassword(str); + } + private String decrypt(String str) throws SecDispatcherException, IOException { return (str == null) ? null : securityDispatcher.decrypt(str); } From 0711d395489a8d65524765861c6ef3a07d798de9 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 11 Oct 2024 19:35:50 +0200 Subject: [PATCH 14/24] Tidy up and start "migrate" goal for mvnenc --- .../{goals => }/ConsolePasswordPrompt.java | 2 +- .../mvnenc/goals/ConfiguredGoalSupport.java | 44 +++++++++++++++++ .../cling/invoker/mvnenc/goals/Decrypt.java | 9 ++-- .../cling/invoker/mvnenc/goals/Encrypt.java | 9 ++-- .../invoker/mvnenc/goals/GoalSupport.java | 39 +++++++++++++++ .../mvnenc/goals/{InitGoal.java => Init.java} | 12 ++--- .../cling/invoker/mvnenc/goals/Migrate.java | 47 +++++++++++++++++++ 7 files changed, 141 insertions(+), 21 deletions(-) rename maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/{goals => }/ConsolePasswordPrompt.java (97%) create mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConfiguredGoalSupport.java create mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/GoalSupport.java rename maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/{InitGoal.java => Init.java} (96%) create mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Migrate.java diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConsolePasswordPrompt.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/ConsolePasswordPrompt.java similarity index 97% rename from maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConsolePasswordPrompt.java rename to maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/ConsolePasswordPrompt.java index 9fc03d525fcd..fe43119a74a9 100644 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConsolePasswordPrompt.java +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/ConsolePasswordPrompt.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.cling.invoker.mvnenc.goals; +package org.apache.maven.cling.invoker.mvnenc; import javax.inject.Inject; import javax.inject.Named; 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 000000000000..74f6346d8e37 --- /dev/null +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConfiguredGoalSupport.java @@ -0,0 +1,44 @@ +/* + * 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 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. + */ +public abstract class ConfiguredGoalSupport extends GoalSupport { + protected ConfiguredGoalSupport(SecDispatcher secDispatcher) { + super(secDispatcher); + } + + @Override + public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception { + if (!configExists()) { + context.logger.error("Encryption is not configured, run `mvnenc init` first."); + return ERROR; + } + return doExecute(context); + } + + 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 index 322fe1fe8378..451dbe2b3190 100644 --- 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 @@ -23,7 +23,6 @@ import javax.inject.Singleton; import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker; -import org.apache.maven.cling.invoker.mvnenc.Goal; import org.codehaus.plexus.components.secdispatcher.SecDispatcher; import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK; @@ -33,16 +32,14 @@ */ @Singleton @Named("decrypt") -public class Decrypt implements Goal { - private final SecDispatcher secDispatcher; - +public class Decrypt extends ConfiguredGoalSupport { @Inject public Decrypt(SecDispatcher secDispatcher) { - this.secDispatcher = secDispatcher; + super(secDispatcher); } @Override - public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception { + protected int doExecute(DefaultEncryptInvoker.LocalContext context) throws Exception { String encrypted = context.reader.readLine("Enter the password to decrypt: "); System.out.println(secDispatcher.decrypt(encrypted)); 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 index 28903ab07bc0..0d2a22562bfb 100644 --- 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 @@ -23,7 +23,6 @@ import javax.inject.Singleton; import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker; -import org.apache.maven.cling.invoker.mvnenc.Goal; import org.codehaus.plexus.components.secdispatcher.SecDispatcher; import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK; @@ -33,16 +32,14 @@ */ @Singleton @Named("encrypt") -public class Encrypt implements Goal { - private final SecDispatcher secDispatcher; - +public class Encrypt extends ConfiguredGoalSupport { @Inject public Encrypt(SecDispatcher secDispatcher) { - this.secDispatcher = secDispatcher; + super(secDispatcher); } @Override - public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception { + protected int doExecute(DefaultEncryptInvoker.LocalContext context) throws Exception { String cleartext = context.reader.readLine("Enter the password to encrypt: ", '*'); System.out.println(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 000000000000..5d3313033034 --- /dev/null +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/GoalSupport.java @@ -0,0 +1,39 @@ +/* + * 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.cling.invoker.mvnenc.Goal; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; + +/** + * The support class for goal implementations. + */ +public abstract class GoalSupport implements Goal { + protected final SecDispatcher secDispatcher; + + protected GoalSupport(SecDispatcher secDispatcher) { + 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/InitGoal.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Init.java similarity index 96% rename from maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java rename to maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Init.java index 0091ba02d16b..d9d30f7aa577 100644 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/InitGoal.java +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Init.java @@ -27,7 +27,6 @@ import java.util.Objects; import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker; -import org.apache.maven.cling.invoker.mvnenc.Goal; import org.codehaus.plexus.components.secdispatcher.DispatcherMeta; import org.codehaus.plexus.components.secdispatcher.SecDispatcher; import org.codehaus.plexus.components.secdispatcher.model.Config; @@ -53,12 +52,10 @@ */ @Singleton @Named("init") -public class InitGoal implements Goal { - private final SecDispatcher secDispatcher; - +public class Init extends GoalSupport { @Inject - public InitGoal(SecDispatcher secDispatcher) { - this.secDispatcher = secDispatcher; + public Init(SecDispatcher secDispatcher) { + super(secDispatcher); } @Override @@ -70,8 +67,7 @@ public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception boolean force = context.invokerRequest.options().force().orElse(false); boolean yes = context.invokerRequest.options().yes().orElse(false); - boolean configExists = secDispatcher.readConfiguration(false) != null; - if (configExists && !force) { + if (configExists() && !force) { System.out.println("Error: configuration exist. Use --force if you want to reset existing configuration."); return BAD_OPERATION; } diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Migrate.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Migrate.java new file mode 100644 index 000000000000..a848ed66548c --- /dev/null +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Migrate.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.cling.invoker.mvnenc.DefaultEncryptInvoker; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; + +import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK; + +/** + * The "migrate" goal. + */ +@Singleton +@Named("migrate") +public class Migrate extends ConfiguredGoalSupport { + @Inject + public Migrate(SecDispatcher secDispatcher) { + super(secDispatcher); + } + + @Override + protected int doExecute(DefaultEncryptInvoker.LocalContext context) throws Exception { + // TODO: implement it + context.logger.info("Migration..."); + return OK; + } +} From 7d6d2e670013f03e5712ccf0fc36ed4ec2051d6e Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Sat, 12 Oct 2024 12:49:39 +0200 Subject: [PATCH 15/24] Adopt latest sec dispatcher changes --- .../invoker/mvnenc/ConsolePasswordPrompt.java | 15 ++++- .../mvnenc/goals/ConfiguredGoalSupport.java | 60 +++++++++++++++++-- .../cling/invoker/mvnenc/goals/Decrypt.java | 7 ++- .../cling/invoker/mvnenc/goals/Diag.java | 47 +++++++++++++++ .../cling/invoker/mvnenc/goals/Encrypt.java | 7 ++- .../invoker/mvnenc/goals/GoalSupport.java | 8 ++- .../cling/invoker/mvnenc/goals/Init.java | 14 +++-- .../cling/invoker/mvnenc/goals/Migrate.java | 7 ++- 8 files changed, 143 insertions(+), 22 deletions(-) create mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Diag.java 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 index fe43119a74a9..a3981a789720 100644 --- 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 @@ -22,12 +22,15 @@ 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; /** @@ -56,8 +59,8 @@ public Optional configTemplate() { } @Override - public String handle(String s) throws SecDispatcherException { - if (NAME.equals(s)) { + public String handle(String config) throws SecDispatcherException { + if (NAME.equals(config)) { try { return prompter.promptForPassword("Enter the master password: "); } catch (PrompterException e) { @@ -66,4 +69,12 @@ public String handle(String s) throws SecDispatcherException { } 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/goals/ConfiguredGoalSupport.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConfiguredGoalSupport.java index 74f6346d8e37..5719a816fc3a 100644 --- 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 @@ -18,27 +18,77 @@ */ 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. + * The support class for goal implementations that requires valid/workable config. */ public abstract class ConfiguredGoalSupport extends GoalSupport { - protected ConfiguredGoalSupport(SecDispatcher secDispatcher) { - super(secDispatcher); + protected ConfiguredGoalSupport(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) { + super(messageBuilderFactory, secDispatcher); } @Override public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception { - if (!configExists()) { - context.logger.error("Encryption is not configured, run `mvnenc init` first."); + 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> entry : + response.getReport().entrySet()) { + Consumer 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 index 451dbe2b3190..036c03d0b996 100644 --- 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 @@ -22,6 +22,7 @@ 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; @@ -34,14 +35,14 @@ @Named("decrypt") public class Decrypt extends ConfiguredGoalSupport { @Inject - public Decrypt(SecDispatcher secDispatcher) { - super(secDispatcher); + 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: "); - System.out.println(secDispatcher.decrypt(encrypted)); + logger.info(secDispatcher.decrypt(encrypted)); return OK; } } 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 000000000000..01d4680ac92b --- /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 index 0d2a22562bfb..c17a5bd53ffd 100644 --- 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 @@ -22,6 +22,7 @@ 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; @@ -34,14 +35,14 @@ @Named("encrypt") public class Encrypt extends ConfiguredGoalSupport { @Inject - public Encrypt(SecDispatcher secDispatcher) { - super(secDispatcher); + 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: ", '*'); - System.out.println(secDispatcher.encrypt(cleartext, null)); + 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 index 5d3313033034..8a8b43ebb92a 100644 --- 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 @@ -20,16 +20,22 @@ 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(SecDispatcher secDispatcher) { + protected GoalSupport(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) { + this.messageBuilderFactory = messageBuilderFactory; this.secDispatcher = secDispatcher; } 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 index d9d30f7aa577..cc7ff57c5aaf 100644 --- 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 @@ -26,6 +26,7 @@ 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; @@ -54,8 +55,8 @@ @Named("init") public class Init extends GoalSupport { @Inject - public Init(SecDispatcher secDispatcher) { - super(secDispatcher); + public Init(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) { + super(messageBuilderFactory, secDispatcher); } @Override @@ -68,7 +69,9 @@ public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception boolean yes = context.invokerRequest.options().yes().orElse(false); if (configExists() && !force) { - System.out.println("Error: configuration exist. Use --force if you want to reset existing configuration."); + System.out.println(messageBuilderFactory + .builder() + .error("Error: configuration exist. Use --force if you want to reset existing configuration.")); return BAD_OPERATION; } @@ -163,10 +166,11 @@ public void complete( context.header, confirmPrompt(prompt.getPromptBuilder()).build()); ConfirmResult confirm = (ConfirmResult) result.get("confirm"); if (confirm.getConfirmed() == ConfirmChoice.ConfirmationValue.YES) { - System.out.println("Writing out the configuration..."); + System.out.println(messageBuilderFactory.builder().info("Writing out the configuration...")); secDispatcher.writeConfiguration(config); } else { - System.out.println("Values not accepted; not saving configuration."); + System.out.println( + messageBuilderFactory.builder().warning("Values not accepted; not saving configuration.")); return BAD_OPERATION; } } diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Migrate.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Migrate.java index a848ed66548c..773cc0f4071b 100644 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Migrate.java +++ b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Migrate.java @@ -22,6 +22,7 @@ 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; @@ -34,14 +35,14 @@ @Named("migrate") public class Migrate extends ConfiguredGoalSupport { @Inject - public Migrate(SecDispatcher secDispatcher) { - super(secDispatcher); + public Migrate(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) { + super(messageBuilderFactory, secDispatcher); } @Override protected int doExecute(DefaultEncryptInvoker.LocalContext context) throws Exception { // TODO: implement it - context.logger.info("Migration..."); + logger.info("Migration..."); return OK; } } From ec45cdeb41eeaa3889da21e85955beb0fcabcabe Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Sat, 12 Oct 2024 13:36:15 +0200 Subject: [PATCH 16/24] Drop the migrate goal --- .../cling/invoker/mvnenc/goals/Migrate.java | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Migrate.java diff --git a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Migrate.java b/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Migrate.java deleted file mode 100644 index 773cc0f4071b..000000000000 --- a/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Migrate.java +++ /dev/null @@ -1,48 +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.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 "migrate" goal. - */ -@Singleton -@Named("migrate") -public class Migrate extends ConfiguredGoalSupport { - @Inject - public Migrate(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) { - super(messageBuilderFactory, secDispatcher); - } - - @Override - protected int doExecute(DefaultEncryptInvoker.LocalContext context) throws Exception { - // TODO: implement it - logger.info("Migration..."); - return OK; - } -} From a4059ab0b0bc5e6e5753fbb9aeb426cbd1e3b105 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Sat, 12 Oct 2024 13:49:40 +0200 Subject: [PATCH 17/24] Allow "legacy only" mode --- .../cling/invoker/mvnenc/goals/Init.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) 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 index cc7ff57c5aaf..cd21e4abc2bc 100644 --- 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 @@ -54,6 +54,8 @@ @Singleton @Named("init") public class Init extends GoalSupport { + private static final String NONE = "__none__"; + @Inject public Init(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDispatcher) { super(messageBuilderFactory, secDispatcher); @@ -86,6 +88,15 @@ public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception 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() @@ -166,11 +177,16 @@ public void complete( context.header, confirmPrompt(prompt.getPromptBuilder()).build()); ConfirmResult confirm = (ConfirmResult) result.get("confirm"); if (confirm.getConfirmed() == ConfirmChoice.ConfirmationValue.YES) { - System.out.println(messageBuilderFactory.builder().info("Writing out the configuration...")); + logger.info(messageBuilderFactory + .builder() + .info("Writing out the configuration...") + .build()); secDispatcher.writeConfiguration(config); } else { - System.out.println( - messageBuilderFactory.builder().warning("Values not accepted; not saving configuration.")); + logger.warn(messageBuilderFactory + .builder() + .warning("Values not accepted; not saving configuration.") + .build()); return BAD_OPERATION; } } @@ -193,6 +209,11 @@ protected PromptBuilder dispatcherPrompt(PromptBuilder 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 From 6b9b233ed1bcec4a68ec40bc6a61a213b96f6562 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 14 Oct 2024 13:36:50 +0200 Subject: [PATCH 18/24] Use released sec-dispatcher --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5f845d1d2a56..ac40cd4e8b58 100644 --- a/pom.xml +++ b/pom.xml @@ -186,7 +186,7 @@ under the License. 1.4.0 4.0.4 2.0.1 - 4.0.0-SNAPSHOT + 4.0.0 0.9.0.M3 2.0.16 4.2.2 From d967290bcb9d64127218695e8ddc17b27d0724fc Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 14 Oct 2024 14:06:23 +0200 Subject: [PATCH 19/24] Lower to warn for now --- .../internal/aether/DefaultRepositorySystemSessionFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9c214a5da41a..b816c3cb0be7 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 @@ -218,7 +218,7 @@ public SessionBuilder newRepositorySessionBuilder(MavenExecutionRequest request) if (problem.getSeverity() == SettingsProblem.Severity.WARNING) { logger.warn(problem.getMessage()); } else if (problem.getSeverity() == SettingsProblem.Severity.ERROR) { - logger.error( + logger.warn( problem.getMessage(), request.isShowErrors() ? problem.getException() From db00ad1b064b57912972925ca004472239674568 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 14 Oct 2024 14:35:23 +0200 Subject: [PATCH 20/24] Return error log level, but decrypt only when not placeholder. --- ...DefaultRepositorySystemSessionFactory.java | 2 +- .../crypto/DefaultSettingsDecrypter.java | 93 ++++++++++--------- 2 files changed, 52 insertions(+), 43 deletions(-) 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 b816c3cb0be7..9c214a5da41a 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 @@ -218,7 +218,7 @@ public SessionBuilder newRepositorySessionBuilder(MavenExecutionRequest request) if (problem.getSeverity() == SettingsProblem.Severity.WARNING) { logger.warn(problem.getMessage()); } else if (problem.getSeverity() == SettingsProblem.Severity.ERROR) { - logger.warn( + logger.error( problem.getMessage(), request.isShowErrors() ? problem.getException() 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 a1f41d474eaa..ecd34351775a 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 @@ -59,46 +59,52 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { for (Server server : request.getServers()) { server = server.clone(); - try { - if (isLegacy(server.getPassword())) { + String password = server.getPassword(); + if (password != null && !password.isBlank() && !password.startsWith("${")) { + try { + if (isLegacy(password)) { + problems.add(new DefaultSettingsProblem( + "Legacy/insecurely encrypted password detected for server " + server.getId(), + Severity.WARNING, + "server: " + server.getId(), + -1, + -1, + null)); + } + server.setPassword(decrypt(password)); + } catch (SecDispatcherException | IOException e) { problems.add(new DefaultSettingsProblem( - "Legacy/insecurely encrypted password detected for server " + server.getId(), - Severity.WARNING, + "Failed to decrypt password for server " + server.getId() + ": " + e.getMessage(), + Severity.ERROR, "server: " + server.getId(), -1, -1, - null)); + e)); } - server.setPassword(decrypt(server.getPassword())); - } 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 { - if (isLegacy(server.getPassphrase())) { + String passphrase = server.getPassphrase(); + if (passphrase != null && !passphrase.isBlank() && !passphrase.startsWith("${")) { + try { + if (isLegacy(passphrase)) { + problems.add(new DefaultSettingsProblem( + "Legacy/insecurely encrypted passphrase detected for server " + server.getId(), + Severity.WARNING, + "server: " + server.getId(), + -1, + -1, + null)); + } + server.setPassphrase(decrypt(passphrase)); + } catch (SecDispatcherException | IOException e) { problems.add(new DefaultSettingsProblem( - "Legacy/insecurely encrypted passphrase detected for server " + server.getId(), - Severity.WARNING, + "Failed to decrypt passphrase for server " + server.getId() + ": " + e.getMessage(), + Severity.ERROR, "server: " + server.getId(), -1, -1, - null)); + e)); } - server.setPassphrase(decrypt(server.getPassphrase())); - } 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); @@ -107,25 +113,28 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { List proxies = new ArrayList<>(); for (Proxy proxy : request.getProxies()) { - try { - if (isLegacy(proxy.getPassword())) { + String password = proxy.getPassword(); + if (password != null && !password.isBlank() && !password.startsWith("${")) { + try { + if (isLegacy(password)) { + problems.add(new DefaultSettingsProblem( + "Legacy/insecurely encrypted password detected for proxy " + proxy.getId(), + Severity.WARNING, + "proxy: " + proxy.getId(), + -1, + -1, + null)); + } + proxy.setPassword(decrypt(password)); + } catch (SecDispatcherException | IOException e) { problems.add(new DefaultSettingsProblem( - "Legacy/insecurely encrypted password detected for proxy " + proxy.getId(), - Severity.WARNING, + "Failed to decrypt password for proxy " + proxy.getId() + ": " + e.getMessage(), + Severity.ERROR, "proxy: " + proxy.getId(), -1, -1, - null)); + e)); } - proxy.setPassword(decrypt(proxy.getPassword())); - } 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); From 31c33800ce8d6c0731f84e59cfca5219c3d967bb Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 14 Oct 2024 16:06:13 +0200 Subject: [PATCH 21/24] Use new API --- .../crypto/DefaultSettingsDecrypter.java | 26 +++++++------------ pom.xml | 2 +- 2 files changed, 10 insertions(+), 18 deletions(-) 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 ecd34351775a..59d1c1392120 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 @@ -60,9 +60,9 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { server = server.clone(); String password = server.getPassword(); - if (password != null && !password.isBlank() && !password.startsWith("${")) { + if (securityDispatcher.isAnyEncryptedString(password)) { try { - if (isLegacy(password)) { + if (securityDispatcher.isLegacyEncryptedString(password)) { problems.add(new DefaultSettingsProblem( "Legacy/insecurely encrypted password detected for server " + server.getId(), Severity.WARNING, @@ -71,7 +71,7 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { -1, null)); } - server.setPassword(decrypt(password)); + server.setPassword(securityDispatcher.decrypt(password)); } catch (SecDispatcherException | IOException e) { problems.add(new DefaultSettingsProblem( "Failed to decrypt password for server " + server.getId() + ": " + e.getMessage(), @@ -84,9 +84,9 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { } String passphrase = server.getPassphrase(); - if (passphrase != null && !passphrase.isBlank() && !passphrase.startsWith("${")) { + if (securityDispatcher.isAnyEncryptedString(passphrase)) { try { - if (isLegacy(passphrase)) { + if (securityDispatcher.isLegacyEncryptedString(passphrase)) { problems.add(new DefaultSettingsProblem( "Legacy/insecurely encrypted passphrase detected for server " + server.getId(), Severity.WARNING, @@ -95,7 +95,7 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { -1, null)); } - server.setPassphrase(decrypt(passphrase)); + server.setPassphrase(securityDispatcher.decrypt(passphrase)); } catch (SecDispatcherException | IOException e) { problems.add(new DefaultSettingsProblem( "Failed to decrypt passphrase for server " + server.getId() + ": " + e.getMessage(), @@ -114,9 +114,9 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { for (Proxy proxy : request.getProxies()) { String password = proxy.getPassword(); - if (password != null && !password.isBlank() && !password.startsWith("${")) { + if (securityDispatcher.isAnyEncryptedString(password)) { try { - if (isLegacy(password)) { + if (securityDispatcher.isLegacyEncryptedString(password)) { problems.add(new DefaultSettingsProblem( "Legacy/insecurely encrypted password detected for proxy " + proxy.getId(), Severity.WARNING, @@ -125,7 +125,7 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { -1, null)); } - proxy.setPassword(decrypt(password)); + proxy.setPassword(securityDispatcher.decrypt(password)); } catch (SecDispatcherException | IOException e) { problems.add(new DefaultSettingsProblem( "Failed to decrypt password for proxy " + proxy.getId() + ": " + e.getMessage(), @@ -142,12 +142,4 @@ public SettingsDecryptionResult decrypt(SettingsDecryptionRequest request) { return new DefaultSettingsDecryptionResult(servers, proxies, problems); } - - private boolean isLegacy(String str) { - return str != null && securityDispatcher.isLegacyPassword(str); - } - - private String decrypt(String str) throws SecDispatcherException, IOException { - return (str == null) ? null : securityDispatcher.decrypt(str); - } } diff --git a/pom.xml b/pom.xml index ac40cd4e8b58..e6d0cd951b9f 100644 --- a/pom.xml +++ b/pom.xml @@ -186,7 +186,7 @@ under the License. 1.4.0 4.0.4 2.0.1 - 4.0.0 + 4.0.1-SNAPSHOT 0.9.0.M3 2.0.16 4.2.2 From a57f63202117981f2b6558a7f8d67a40a3cd4248 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 14 Oct 2024 16:12:46 +0200 Subject: [PATCH 22/24] Decrypt can now detect bad input --- .../maven/cling/invoker/mvnenc/goals/Decrypt.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 index 036c03d0b996..6c437c96767b 100644 --- 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 @@ -26,6 +26,7 @@ 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; /** @@ -42,7 +43,12 @@ public Decrypt(MessageBuilderFactory messageBuilderFactory, SecDispatcher secDis @Override protected int doExecute(DefaultEncryptInvoker.LocalContext context) throws Exception { String encrypted = context.reader.readLine("Enter the password to decrypt: "); - logger.info(secDispatcher.decrypt(encrypted)); - return OK; + if (secDispatcher.isAnyEncryptedString(encrypted)) { + logger.info(secDispatcher.decrypt(encrypted)); + return OK; + } else { + logger.error("Malformed encrypted string"); + return BAD_OPERATION; + } } } From 1c4015e27d68e67951cc6ef3da940571aac006b0 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 14 Oct 2024 21:01:55 +0200 Subject: [PATCH 23/24] Update to 4.0.1 --- maven-embedder/pom.xml | 4 ---- maven-settings-builder/pom.xml | 4 ---- .../apache/maven/settings/crypto/MavenSecDispatcher.java | 5 ++--- pom.xml | 9 ++------- 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/maven-embedder/pom.xml b/maven-embedder/pom.xml index a5fb2351e0a8..5a36cf9fea49 100644 --- a/maven-embedder/pom.xml +++ b/maven-embedder/pom.xml @@ -88,10 +88,6 @@ under the License. org.codehaus.plexus plexus-sec-dispatcher - - org.codehaus.plexus - plexus-cipher - org.codehaus.plexus plexus-interpolation diff --git a/maven-settings-builder/pom.xml b/maven-settings-builder/pom.xml index 36533dc3d087..5d442afdd7f7 100644 --- a/maven-settings-builder/pom.xml +++ b/maven-settings-builder/pom.xml @@ -62,10 +62,6 @@ under the License. org.codehaus.plexus plexus-sec-dispatcher - - org.codehaus.plexus - plexus-cipher - javax.inject 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 index d78ba17efcb6..4ac37151d812 100644 --- 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 @@ -27,7 +27,6 @@ import java.util.Map; import org.apache.maven.api.Constants; -import org.codehaus.plexus.components.cipher.PlexusCipher; import org.codehaus.plexus.components.secdispatcher.Dispatcher; import org.codehaus.plexus.components.secdispatcher.SecDispatcher; import org.codehaus.plexus.components.secdispatcher.internal.DefaultSecDispatcher; @@ -44,8 +43,8 @@ public class MavenSecDispatcher extends DefaultSecDispatcher { private static final String FILE_NAME = "settings-security4.xml"; @Inject - public MavenSecDispatcher(PlexusCipher cipher, Map dispatchers) { - super(cipher, dispatchers, configurationFile()); + public MavenSecDispatcher(Map dispatchers) { + super(dispatchers, configurationFile()); } private static Path configurationFile() { diff --git a/pom.xml b/pom.xml index e6d0cd951b9f..6ab282e78282 100644 --- a/pom.xml +++ b/pom.xml @@ -167,7 +167,7 @@ under the License. 3.26.3 9.7.1 1.15.3 - 3.0.0 + 2.8.0 2.8.0 1.9.0 6.0.0 @@ -186,7 +186,7 @@ under the License. 1.4.0 4.0.4 2.0.1 - 4.0.1-SNAPSHOT + 4.0.1 0.9.0.M3 2.0.16 4.2.2 @@ -605,11 +605,6 @@ under the License. plexus-sec-dispatcher ${securityDispatcherVersion} - - org.codehaus.plexus - plexus-cipher - ${cipherVersion} - com.fasterxml.woodstox woodstox-core From ad936b30633c473c21b2fc2ff12e006f1dda4228 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 14 Oct 2024 21:55:58 +0200 Subject: [PATCH 24/24] Remove dupe line --- pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6ab282e78282..e1fddf3ff2ed 100644 --- a/pom.xml +++ b/pom.xml @@ -168,7 +168,6 @@ under the License. 9.7.1 1.15.3 2.8.0 - 2.8.0 1.9.0 6.0.0 33.3.1-jre