From ab0942ef65d0af926b9c45252015652c6a25862a Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 14 Oct 2024 12:38:47 +0200 Subject: [PATCH] 4.x: Unified logic, no more extra logic in SecDispatcher; made self-describable (#75) Hence, moved version to 4.x. Changes: * "master pw" is **really just one (of possibly multiple) dispatchers**, so moved "master password" into dispatchers. Now SecDispatcher is really just a "dispatcher" with one `Dispatcher` implementation OOTB * similarly, old sec behaviour is moved to dispatcher as "legacy" to support mvn3 encrypted passwords (only decrypting) * dispatcher passwords now minimally include "name" attribute (and more, if dispatcher adds it), master dispatcher adds "cipher" as well, this allows future proof working if new Cipher added and user makes it default: non-reencoded passwords will still work with old cipher. * `Dispatcher` and `MasterSource` interfaces have corresponding "meta" interfaces where they can "describe themselves" * added support for pinentry as well * configuration validation now performs "deep validation" and produces useful report * this is no more OOTB ready-to-use JSR330 component. As we know from history, it WAS before, and it provided a "bad" default configuration path that was NEVER used, and forced Maven instead to _always redefine_ the component to make it usable within Maven (to make it use maven expected config). So, it is now the integrator duty to "properly integrate" this component, recommended way is to create JSR330 Provider with it. Also, the "system property override" of configuration path was removed, whatever integrating app needs is doable from it's own Provider implementation. Self describe and validation in action: https://asciinema.org/a/9kmtQWhKJC9elFp3tiDlxpOTE --- pom.xml | 20 +- .../{internal => }/Dispatcher.java | 44 ++- .../secdispatcher/DispatcherMeta.java | 128 ++++++++ .../secdispatcher/MasterSource.java | 42 +++ .../secdispatcher/MasterSourceMeta.java} | 30 +- .../components/secdispatcher/PinEntry.java | 279 ++++++++++++++++++ .../secdispatcher/SecDispatcher.java | 82 +++-- .../internal/DefaultSecDispatcher.java | 279 +++++++++++------- .../internal/MasterPasswordSource.java | 35 --- .../secdispatcher/internal/SecUtil.java | 15 +- .../dispatchers/LegacyDispatcher.java | 230 +++++++++++++++ .../dispatchers/MasterDispatcher.java | 203 +++++++++++++ .../sources/EnvMasterPasswordSource.java | 46 --- .../internal/sources/EnvMasterSource.java | 86 ++++++ ...dSource.java => GpgAgentMasterSource.java} | 58 +++- ...eSupport.java => MasterSourceSupport.java} | 16 +- .../sources/PinEntryMasterSource.java | 109 +++++++ ...rt.java => PrefixMasterSourceSupport.java} | 4 +- .../SystemPropertyMasterPasswordSource.java | 46 --- .../sources/SystemPropertyMasterSource.java | 86 ++++++ src/main/mdo/settings-security.mdo | 11 +- .../internal/DefaultSecDispatcherTest.java | 255 ++++++---------- .../secdispatcher/internal/SecUtilTest.java | 16 +- .../internal/dispatcher/StaticDispatcher.java | 43 --- .../dispatchers/LegacyDispatcherTest.java | 44 +++ .../internal/sources/SourcesTest.java | 52 ++++ .../legacy/legacy-settings-security-1.xml | 3 + .../legacy/legacy-settings-security-2.xml | 4 + 28 files changed, 1752 insertions(+), 514 deletions(-) rename src/main/java/org/codehaus/plexus/components/secdispatcher/{internal => }/Dispatcher.java (51%) create mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/DispatcherMeta.java create mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSource.java rename src/{test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/StaticMasterPasswordSource.java => main/java/org/codehaus/plexus/components/secdispatcher/MasterSourceMeta.java} (51%) create mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/PinEntry.java delete mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/internal/MasterPasswordSource.java create mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcher.java create mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterDispatcher.java delete mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterPasswordSource.java create mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterSource.java rename src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/{GpgAgentMasterPasswordSource.java => GpgAgentMasterSource.java} (66%) rename src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/{MasterPasswordSourceSupport.java => MasterSourceSupport.java} (72%) create mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PinEntryMasterSource.java rename src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/{PrefixMasterPasswordSourceSupport.java => PrefixMasterSourceSupport.java} (90%) delete mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterPasswordSource.java create mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterSource.java delete mode 100644 src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatcher/StaticDispatcher.java create mode 100644 src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcherTest.java create mode 100644 src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SourcesTest.java create mode 100644 src/test/legacy/legacy-settings-security-1.xml create mode 100644 src/test/legacy/legacy-settings-security-2.xml diff --git a/pom.xml b/pom.xml index f42e98f..0e92784 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ plexus-sec-dispatcher - 3.0.1-SNAPSHOT + 4.0.0-SNAPSHOT Plexus Security Dispatcher Component @@ -35,9 +35,16 @@ 17 2024-09-29T15:16:00Z + + 2.0.16 + + org.slf4j + slf4j-api + ${version.slf4j} + org.codehaus.plexus plexus-cipher @@ -62,6 +69,12 @@ junit-jupiter test + + org.slf4j + slf4j-simple + ${version.slf4j} + test + @@ -75,7 +88,7 @@ modello-maven-plugin 2.4.0 - 3.0.0 + 4.0.0 src/main/mdo/settings-security.mdo @@ -96,6 +109,9 @@ org.apache.maven.plugins maven-surefire-plugin + + masterPw + masterPw diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/Dispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/Dispatcher.java similarity index 51% rename from src/main/java/org/codehaus/plexus/components/secdispatcher/internal/Dispatcher.java rename to src/main/java/org/codehaus/plexus/components/secdispatcher/Dispatcher.java index de030a8..059e1e3 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/Dispatcher.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/Dispatcher.java @@ -11,11 +11,11 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ -package org.codehaus.plexus.components.secdispatcher.internal; +package org.codehaus.plexus.components.secdispatcher; import java.util.Map; -import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; +import static java.util.Objects.requireNonNull; /** * Dispatcher. @@ -26,31 +26,51 @@ */ public interface Dispatcher { /** - * Configuration key for masterPassword. It may be present, if SecDispatcher could - * obtain it, but presence is optional. Still, dispatcher may throw and fail the operation - * if it requires it. + * The "encrypt payload" prepared by dispatcher. */ - String CONF_MASTER_PASSWORD = "masterPassword"; + final class EncryptPayload { + private final Map attributes; + private final String encrypted; + + public EncryptPayload(Map attributes, String encrypted) { + this.attributes = requireNonNull(attributes); + this.encrypted = requireNonNull(encrypted); + } + + public Map getAttributes() { + return attributes; + } + + public String getEncrypted() { + return encrypted; + } + } /** - * encrypt given plaintext string + * Encrypt given plaintext string. Implementation must return at least same attributes it got, but may add more + * attributes to returned payload. * - * @param str string to encrypt + * @param str string to encrypt, never {@code null} * @param attributes attributes, never {@code null} * @param config configuration from settings-security.xml, never {@code null} - * @return encrypted string + * @return encrypted string and attributes in {@link EncryptPayload} */ - String encrypt(String str, Map attributes, Map config) + EncryptPayload encrypt(String str, Map attributes, Map config) throws SecDispatcherException; /** - * decrypt given encrypted string + * Decrypt given encrypted string. * - * @param str string to decrypt + * @param str string to decrypt, never {@code null} * @param attributes attributes, never {@code null} * @param config configuration from settings-security.xml, never {@code null} * @return decrypted string */ String decrypt(String str, Map attributes, Map config) throws SecDispatcherException; + + /** + * Validates dispatcher configuration. + */ + SecDispatcher.ValidationResponse validateConfiguration(Map config); } diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/DispatcherMeta.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/DispatcherMeta.java new file mode 100644 index 0000000..219ec46 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/DispatcherMeta.java @@ -0,0 +1,128 @@ +package org.codehaus.plexus.components.secdispatcher; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +/** + * Meta description of dispatcher. + */ +public interface DispatcherMeta { + final class Field { + private final String key; + private final boolean optional; + private final String defaultValue; + private final String description; + private final List options; + + private Field(String key, boolean optional, String defaultValue, String description, List options) { + this.key = requireNonNull(key); + this.optional = optional; + this.defaultValue = defaultValue; + this.description = requireNonNull(description); + this.options = options; + } + + /** + * The key to be used in configuration map for field. + */ + public String getKey() { + return key; + } + + /** + * Is configuration optional? + */ + public boolean isOptional() { + return optional; + } + + /** + * Optional default value of the configuration. + */ + public Optional getDefaultValue() { + return Optional.ofNullable(defaultValue); + } + + /** + * The human description of the configuration. + */ + public String getDescription() { + return description; + } + + /** + * Optional list of options, if this configuration accepts limited values. Each option is represented + * as field, where {@link #getKey()} represents the value to be used, and {@link #displayName()} represents + * the description of option. The {@link #getDefaultValue()}, if present represents the value to be used + * instead of {@link #getKey()}. + */ + public Optional> getOptions() { + return Optional.ofNullable(options); + } + + public static Builder builder(String key) { + return new Builder(key); + } + + public static final class Builder { + private final String key; + private boolean optional; + private String defaultValue; + private String description; + private List options; + + private Builder(String key) { + this.key = requireNonNull(key); + } + + public Builder optional(boolean optional) { + this.optional = optional; + return this; + } + + public Builder defaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + public Builder description(String description) { + this.description = requireNonNull(description); + return this; + } + + public Builder options(List options) { + this.options = requireNonNull(options); + return this; + } + + public Field build() { + return new Field(key, optional, defaultValue, description, options); + } + } + } + + /** + * Option to hide this instance from users, like for migration or legacy purposes. + */ + default boolean isHidden() { + return false; + } + + /** + * The name of the dispatcher. + */ + String name(); + + /** + * Returns the display (human) name of the dispatcher. + */ + String displayName(); + + /** + * Returns the configuration fields of the dispatcher. + */ + Collection fields(); +} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSource.java new file mode 100644 index 0000000..d5a754b --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSource.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2008 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +package org.codehaus.plexus.components.secdispatcher; + +/** + * Source of master password. + */ +public interface MasterSource { + /** + * Handles the config to get master password. Implementation may do one of the following things: + *
    + *
  • if the config cannot be handled by given source, return {@code null}
  • + *
  • otherwise, if master password retrieval based on config was attempted but failed, throw {@link SecDispatcherException}
  • + *
  • happy path: return the master password.
  • + *
+ * + * @param config the source of master password, and opaque string. + * @return the master password, or {@code null} if implementation does not handle this config + * @throws SecDispatcherException If implementation does handle this masterSource, but cannot obtain master password + */ + String handle(String config) throws SecDispatcherException; + + /** + * Validates master source configuration. + *
    + *
  • if the config cannot be handled by given source, return {@code null}
  • + *
  • otherwise, implementation performs validation and returns non-{@code null} validation response
  • + *
+ */ + SecDispatcher.ValidationResponse validateConfiguration(String config); +} diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/StaticMasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSourceMeta.java similarity index 51% rename from src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/StaticMasterPasswordSource.java rename to src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSourceMeta.java index 7ef6d89..4e112c1 100644 --- a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/StaticMasterPasswordSource.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/MasterSourceMeta.java @@ -11,22 +11,22 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ -package org.codehaus.plexus.components.secdispatcher.internal.sources; +package org.codehaus.plexus.components.secdispatcher; -import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; -import org.codehaus.plexus.components.secdispatcher.internal.MasterPasswordSource; +import java.util.Optional; -import static java.util.Objects.requireNonNull; - -public class StaticMasterPasswordSource implements MasterPasswordSource { - private final String masterPassword; - - public StaticMasterPasswordSource(String masterPassword) { - this.masterPassword = requireNonNull(masterPassword); - } +/** + * Source of master password. + */ +public interface MasterSourceMeta { + /** + * String describing what this source does. + */ + String description(); - @Override - public String handle(String masterSource) throws SecDispatcherException { - return masterPassword; - } + /** + * Optional "config template" that may serve as basis to configure this master source. The template cannot be + * "reused" as is as configuration. + */ + Optional configTemplate(); } diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/PinEntry.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/PinEntry.java new file mode 100644 index 0000000..35b8765 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/PinEntry.java @@ -0,0 +1,279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.codehaus.plexus.components.secdispatcher; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.util.Objects.requireNonNull; + +/** + * Inspired by A peek inside pinentry. + * Also look at Pinentry Documentation. + * Finally, source mirror is at gpg/pinentry. + */ +public class PinEntry { + public enum Outcome { + SUCCESS, + TIMEOUT, + NOT_CONFIRMED, + CANCELED, + FAILED; + } + + public record Result(Outcome outcome, String payload) {} + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final String cmd; + private final LinkedHashMap commands; + + /** + * Creates pin entry instance that will use the passed in cmd executable. + */ + public PinEntry(String cmd) { + this.cmd = requireNonNull(cmd); + this.commands = new LinkedHashMap<>(); + } + + /** + * Sets a "stable key handle" for caching purposes. Optional. + */ + public PinEntry setKeyInfo(String keyInfo) { + requireNonNull(keyInfo); + commands.put("OPTION", "allow-external-password-cache"); + commands.put("SETKEYINFO", keyInfo); + return this; + } + + /** + * Sets the OK button label, by default "Ok". + */ + public PinEntry setOk(String msg) { + requireNonNull(msg); + commands.put("SETOK", msg); + return this; + } + + /** + * Sets the CANCEL button label, by default "Cancel". + */ + public PinEntry setCancel(String msg) { + requireNonNull(msg); + commands.put("SETCANCEL", msg); + return this; + } + + /** + * Sets the window title. + */ + public PinEntry setTitle(String title) { + requireNonNull(title); + commands.put("SETTITLE", title); + return this; + } + + /** + * Sets additional test in window. + */ + public PinEntry setDescription(String desc) { + requireNonNull(desc); + commands.put("SETDESC", desc); + return this; + } + + /** + * Sets the prompt. + */ + public PinEntry setPrompt(String prompt) { + requireNonNull(prompt); + commands.put("SETPROMPT", prompt); + return this; + } + + /** + * If set, window will show "Error: xxx", usable for second attempt (ie "bad password"). + */ + public PinEntry setError(String error) { + requireNonNull(error); + commands.put("SETERROR", error); + return this; + } + + /** + * Usable with {@link #getPin()}, window will contain two input fields and will force user to type in same + * input in both fields, ie to "confirm" the pin. + */ + public PinEntry confirmPin() { + commands.put("SETREPEAT", null); + return this; + } + + /** + * Sets the window timeout, if no button pressed and timeout passes, Result will by {@link Outcome#TIMEOUT}. + */ + public PinEntry setTimeout(Duration timeout) { + long seconds = timeout.toSeconds(); + if (seconds < 0) { + throw new IllegalArgumentException("Set timeout is 0 seconds"); + } + commands.put("SETTIMEOUT", String.valueOf(seconds)); + return this; + } + + /** + * Initiates a "get pin" dialogue with input field(s) using previously set options. + */ + public Result getPin() throws IOException { + commands.put("GETPIN", null); + return execute(); + } + + /** + * Initiates a "confirmation" dialogue (no input) using previously set options. + */ + public Result confirm() throws IOException { + commands.put("CONFIRM", null); + return execute(); + } + + /** + * Initiates a "message" dialogue (no input) using previously set options. + */ + public Result message() throws IOException { + commands.put("MESSAGE", null); + return execute(); + } + + private Result execute() throws IOException { + Process process = new ProcessBuilder(cmd).start(); + BufferedReader reader = process.inputReader(); + BufferedWriter writer = process.outputWriter(); + expectOK(process.inputReader()); + Map.Entry 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 { + // check what pinentry apps you have and replace the execName + String cmd = "/usr/bin/pinentry-gnome3"; + Result pinResult = new PinEntry(cmd) + .setTimeout(Duration.ofSeconds(15)) + .setKeyInfo("maven:masterPassword") + .setTitle("Maven Master Password") + .setDescription("Please enter the Maven master password") + .setPrompt("Password") + .setOk("Here you go!") + .setCancel("Uh oh, rather not") + // .confirmPin() (will not let you through if you cannot type same thing twice) + .getPin(); + if (pinResult.outcome() == Outcome.SUCCESS) { + Result confirmResult = new PinEntry(cmd) + .setTitle("Password confirmation") + .setPrompt("Please confirm the password") + .setDescription("Is the password '" + pinResult.payload() + "' the one you want?") + .confirm(); + if (confirmResult.outcome() == Outcome.SUCCESS) { + new PinEntry(cmd) + .setTitle("Password confirmed") + .setPrompt("The password '" + pinResult.payload() + "' is confirmed.") + .setDescription("You confirmed your password") + .message(); + } else { + System.out.println(confirmResult); + } + } else { + System.out.println(pinResult); + } + } +} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java index 04ef9dd..fde9fa9 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java @@ -14,46 +14,35 @@ package org.codehaus.plexus.components.secdispatcher; import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.Set; import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity; /** - * This component decrypts a string, passed to it + * This component decrypts a string, passed to it using various dispatchers. * * @author Oleg Gusakov */ public interface SecDispatcher { /** - * The default path of configuration. - *

- * The character {@code ~} (tilde) may be present as first character ONLY and is - * interpreted as "user.home" system property, and it MUST be followed by path separator. - */ - String DEFAULT_CONFIGURATION = "~/.m2/settings-security.xml"; - - /** - * Java System Property that may be set, to override configuration path. - */ - String SYSTEM_PROPERTY_CONFIGURATION_LOCATION = "settings.security"; - - /** - * Attribute that selects a dispatcher. + * Attribute that selects a dispatcher. If not present in {@link #encrypt(String, Map)} attributes, the + * configured "default dispatcher" is used. * * @see #availableDispatchers() */ String DISPATCHER_NAME_ATTR = "name"; /** - * Returns the set of available dispatcher names, never {@code null}. + * Attribute for version, added by SecDispatcher for possible upgrade path. */ - Set availableDispatchers(); + String DISPATCHER_VERSION_ATTR = "version"; /** - * Returns the set of available ciphers, never {@code null}. + * Returns the set of available dispatcher metadata, never {@code null}. */ - Set availableCiphers(); + Set availableDispatchers(); /** * Encrypt given plaintext string. @@ -63,7 +52,7 @@ public interface SecDispatcher { * @return encrypted string * @throws SecDispatcherException in case of problem */ - String encrypt(String str, Map attr) throws SecDispatcherException; + String encrypt(String str, Map attr) throws SecDispatcherException, IOException; /** * Decrypt given encrypted string. @@ -72,7 +61,12 @@ public interface SecDispatcher { * @return decrypted string * @throws SecDispatcherException in case of problem */ - String decrypt(String str) throws SecDispatcherException; + String decrypt(String str) throws SecDispatcherException, IOException; + + /** + * Returns {@code true} if passed in string contains "legacy" password (Maven3 kind). + */ + boolean isLegacyPassword(String str); /** * Reads the effective configuration, eventually creating new instance if not present. @@ -90,4 +84,50 @@ public interface SecDispatcher { * @throws IOException In case of IO problem */ void writeConfiguration(SettingsSecurity configuration) throws IOException; + + /** + * The validation response. + */ + final class ValidationResponse { + public enum Level { + INFO, + WARNING, + ERROR + }; + + private final String source; + private final boolean valid; + private final Map> report; + private final List subsystems; + + public ValidationResponse( + String source, boolean valid, Map> report, List subsystems) { + this.source = source; + this.valid = valid; + this.report = report; + this.subsystems = subsystems; + } + + public String getSource() { + return source; + } + + public boolean isValid() { + return valid; + } + + public Map> getReport() { + return report; + } + + public List getSubsystems() { + return subsystems; + } + } + + /** + * Performs a "deep validation" and reports the status. If return instance {@link ValidationResponse#isValid()} + * is {@code true}, configuration is usable. + */ + ValidationResponse validateConfiguration(); } diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java index 131c0de..37f4dbd 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java @@ -13,14 +13,13 @@ package org.codehaus.plexus.components.secdispatcher.internal; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; @@ -28,69 +27,110 @@ import org.codehaus.plexus.components.cipher.PlexusCipher; import org.codehaus.plexus.components.cipher.PlexusCipherException; +import org.codehaus.plexus.components.secdispatcher.Dispatcher; +import org.codehaus.plexus.components.secdispatcher.DispatcherMeta; import org.codehaus.plexus.components.secdispatcher.SecDispatcher; import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; +import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher; import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity; import static java.util.Objects.requireNonNull; /** + * Note: this implementation is NOT a JSR330 component. Integrating apps anyway want to customize it (at least + * the name and location of configuration file), so instead as before (providing "bad" configuration file just + * to have one), it is the duty of integrator to wrap and "finish" the implementation in a way it suits the + * integrator. Also, using "globals" like Java System Properties are bad thing, and it is integrator who knows + * what is needed anyway. + *

+ * Recommended way for integration is to create JSR330 {@link javax.inject.Provider}. + * * @author Oleg Gusakov */ -@Singleton -@Named public class DefaultSecDispatcher implements SecDispatcher { public static final String ATTR_START = "["; public static final String ATTR_STOP = "]"; protected final PlexusCipher cipher; - protected final Map masterPasswordSources; protected final Map dispatchers; - protected final String configurationFile; - - @Inject - public DefaultSecDispatcher( - PlexusCipher cipher, - Map masterPasswordSources, - Map dispatchers, - @Named("${configurationFile:-" + DEFAULT_CONFIGURATION + "}") final String configurationFile) { + protected final Path configurationFile; + + public DefaultSecDispatcher(PlexusCipher cipher, Map dispatchers, Path configurationFile) { this.cipher = requireNonNull(cipher); - this.masterPasswordSources = requireNonNull(masterPasswordSources); this.dispatchers = requireNonNull(dispatchers); this.configurationFile = requireNonNull(configurationFile); + + // file may or may not exist, but one thing is certain: it cannot be an exiting directory + if (Files.isDirectory(configurationFile)) { + throw new IllegalArgumentException("configurationFile cannot be a directory"); + } } @Override - public Set availableDispatchers() { - return Set.copyOf(dispatchers.keySet()); + public Set availableDispatchers() { + return Set.copyOf( + dispatchers.entrySet().stream().map(this::dispatcherMeta).collect(Collectors.toSet())); } - @Override - public Set availableCiphers() { - return cipher.availableCiphers(); + private DispatcherMeta dispatcherMeta(Map.Entry dispatcher) { + // sisu components are lazy! + Dispatcher d = dispatcher.getValue(); + if (d instanceof DispatcherMeta meta) { + return meta; + } else { + return new DispatcherMeta() { + @Override + public String name() { + return dispatcher.getKey(); + } + + @Override + public String displayName() { + return dispatcher.getKey() + " (needs manual configuration)"; + } + + @Override + public Collection fields() { + return List.of(); + } + }; + } } @Override - public String encrypt(String str, Map attr) throws SecDispatcherException { + public String encrypt(String str, Map attr) throws SecDispatcherException, IOException { if (isEncryptedString(str)) return str; try { - String res; - if (attr == null || attr.get(DISPATCHER_NAME_ATTR) == null) { - SettingsSecurity sec = getConfiguration(true); - String master = getMasterPassword(sec, true); - res = cipher.encrypt(getMasterCipher(sec), str, master); + if (attr == null) { + attr = new HashMap<>(); } else { - String type = attr.get(DISPATCHER_NAME_ATTR); - Dispatcher dispatcher = dispatchers.get(type); - if (dispatcher == null) throw new SecDispatcherException("no dispatcher for name " + type); - res = ATTR_START - + attr.entrySet().stream() - .map(e -> e.getKey() + "=" + e.getValue()) - .collect(Collectors.joining(",")) - + ATTR_STOP; - res += dispatcher.encrypt(str, attr, prepareDispatcherConfig(type)); + attr = new HashMap<>(attr); + } + if (attr.get(DISPATCHER_NAME_ATTR) == null) { + SettingsSecurity conf = readConfiguration(false); + if (conf == null) { + throw new SecDispatcherException("No configuration found"); + } + String defaultDispatcher = conf.getDefaultDispatcher(); + if (defaultDispatcher == null) { + throw new SecDispatcherException("No defaultDispatcher set in configuration"); + } + attr.put(DISPATCHER_NAME_ATTR, defaultDispatcher); } + String name = attr.get(DISPATCHER_NAME_ATTR); + Dispatcher dispatcher = dispatchers.get(name); + if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name); + Dispatcher.EncryptPayload payload = dispatcher.encrypt(str, attr, prepareDispatcherConfig(name)); + HashMap resultAttributes = new HashMap<>(payload.getAttributes()); + resultAttributes.put(SecDispatcher.DISPATCHER_NAME_ATTR, name); + resultAttributes.put(SecDispatcher.DISPATCHER_VERSION_ATTR, SecUtil.specVersion()); + String res = ATTR_START + + resultAttributes.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(",")) + + ATTR_STOP; + res += payload.getEncrypted(); return cipher.decorate(res); } catch (PlexusCipherException e) { throw new SecDispatcherException(e.getMessage(), e); @@ -98,29 +138,33 @@ public String encrypt(String str, Map attr) throws SecDispatcher } @Override - public String decrypt(String str) throws SecDispatcherException { + public String decrypt(String str) throws SecDispatcherException, IOException { if (!isEncryptedString(str)) return str; try { String bare = cipher.unDecorate(str); - Map attr = stripAttributes(bare); - if (attr == null || attr.get(DISPATCHER_NAME_ATTR) == null) { - SettingsSecurity sec = getConfiguration(true); - String master = getMasterPassword(sec, true); - return cipher.decrypt(getMasterCipher(sec), bare, master); - } else { - String type = attr.get(DISPATCHER_NAME_ATTR); - Dispatcher dispatcher = dispatchers.get(type); - if (dispatcher == null) throw new SecDispatcherException("no dispatcher for name " + type); - return dispatcher.decrypt(strip(bare), attr, prepareDispatcherConfig(type)); + Map attr = requireNonNull(stripAttributes(bare)); + if (isLegacyPassword(str)) { + attr.put(DISPATCHER_NAME_ATTR, LegacyDispatcher.NAME); } + String name = attr.get(DISPATCHER_NAME_ATTR); + Dispatcher dispatcher = dispatchers.get(name); + if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name); + return dispatcher.decrypt(strip(bare), attr, prepareDispatcherConfig(name)); } catch (PlexusCipherException e) { throw new SecDispatcherException(e.getMessage(), e); } } + @Override + public boolean isLegacyPassword(String str) { + if (!isEncryptedString(str)) return false; + Map attr = requireNonNull(stripAttributes(cipher.unDecorate(str))); + return !attr.containsKey(DISPATCHER_NAME_ATTR); + } + @Override public SettingsSecurity readConfiguration(boolean createIfMissing) throws IOException { - SettingsSecurity configuration = SecUtil.read(getConfigurationPath()); + SettingsSecurity configuration = SecUtil.read(configurationFile); if (configuration == null && createIfMissing) { configuration = new SettingsSecurity(); } @@ -130,24 +174,86 @@ public SettingsSecurity readConfiguration(boolean createIfMissing) throws IOExce @Override public void writeConfiguration(SettingsSecurity configuration) throws IOException { requireNonNull(configuration, "configuration is null"); - SecUtil.write(getConfigurationPath(), configuration, true); + SecUtil.write(configurationFile, configuration, true); } - private Map prepareDispatcherConfig(String type) { - HashMap dispatcherConf = new HashMap<>(); - SettingsSecurity sec = getConfiguration(false); - String master = getMasterPassword(sec, false); - if (master != null) { - dispatcherConf.put(Dispatcher.CONF_MASTER_PASSWORD, master); + @Override + public ValidationResponse validateConfiguration() { + HashMap> report = new HashMap<>(); + ArrayList subsystems = new ArrayList<>(); + boolean valid = false; + try { + SettingsSecurity config = readConfiguration(false); + if (config == null) { + report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("No configuration file found on path " + configurationFile); + } else { + report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>()) + .add("Configuration file present on path " + configurationFile); + String defaultDispatcher = config.getDefaultDispatcher(); + if (defaultDispatcher == null) { + report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("No default dispatcher set in configuration"); + } else { + report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>()) + .add("Default dispatcher configured"); + Dispatcher dispatcher = dispatchers.get(defaultDispatcher); + if (dispatcher == null) { + report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Configured default dispatcher not present in system"); + } else { + ValidationResponse dispatcherResponse = + dispatcher.validateConfiguration(prepareDispatcherConfig(defaultDispatcher)); + subsystems.add(dispatcherResponse); + if (!dispatcherResponse.isValid()) { + report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Configured default dispatcher configuration is invalid"); + } else { + valid = true; + report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>()) + .add("Configured default dispatcher configuration is valid"); + } + } + } + } + + // below is legacy check, that does not affect validity of config, is merely informational + Dispatcher legacy = dispatchers.get(LegacyDispatcher.NAME); + if (legacy == null) { + report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>()) + .add("Legacy dispatcher not present in system"); + } else { + report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>()) + .add("Legacy dispatcher present in system"); + ValidationResponse legacyResponse = + legacy.validateConfiguration(prepareDispatcherConfig(LegacyDispatcher.NAME)); + subsystems.add(legacyResponse); + if (!legacyResponse.isValid()) { + report.computeIfAbsent(ValidationResponse.Level.WARNING, k -> new ArrayList<>()) + .add("Legacy dispatcher not operational; transparent fallback not possible"); + } else { + report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>()) + .add("Legacy dispatcher is operational; transparent fallback possible"); + } + } + } catch (IOException e) { + report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add(e.getMessage()); } - Map conf = SecUtil.getConfig(sec, type); + + return new ValidationResponse(getClass().getSimpleName(), valid, report, subsystems); + } + + protected Map prepareDispatcherConfig(String name) throws IOException { + HashMap dispatcherConf = new HashMap<>(); + Map conf = SecUtil.getConfig(SecUtil.read(configurationFile), name); if (conf != null) { dispatcherConf.putAll(conf); } return dispatcherConf; } - private String strip(String str) { + protected String strip(String str) { int start = str.indexOf(ATTR_START); int stop = str.indexOf(ATTR_STOP); if (start != -1 && stop != -1 && stop > start) { @@ -156,7 +262,8 @@ private String strip(String str) { return str; } - private Map stripAttributes(String str) { + protected Map stripAttributes(String str) { + HashMap result = new HashMap<>(); int start = str.indexOf(ATTR_START); int stop = str.indexOf(ATTR_STOP); if (start != -1 && stop != -1 && stop > start) { @@ -164,68 +271,20 @@ private Map stripAttributes(String str) { if (stop == start + 1) return null; String attrs = str.substring(start + 1, stop).trim(); if (attrs.isEmpty()) return null; - Map res = null; StringTokenizer st = new StringTokenizer(attrs, ","); while (st.hasMoreTokens()) { - if (res == null) res = new HashMap<>(st.countTokens()); String pair = st.nextToken(); int pos = pair.indexOf('='); if (pos == -1) throw new SecDispatcherException("Attribute malformed: " + pair); String key = pair.substring(0, pos).trim(); String val = pair.substring(pos + 1).trim(); - res.put(key, val); + result.put(key, val); } - return res; } - return null; + return result; } - private boolean isEncryptedString(String str) { - if (str == null) return false; + protected boolean isEncryptedString(String str) { return cipher.isEncryptedString(str); } - - private Path getConfigurationPath() { - String location = System.getProperty(SYSTEM_PROPERTY_CONFIGURATION_LOCATION, getConfigurationFile()); - location = location.charAt(0) == '~' ? System.getProperty("user.home") + location.substring(1) : location; - return Paths.get(location); - } - - private SettingsSecurity getConfiguration(boolean mandatory) throws SecDispatcherException { - Path path = getConfigurationPath(); - try { - SettingsSecurity sec = SecUtil.read(path); - if (mandatory && sec == null) - throw new SecDispatcherException("Please check that configuration file on path " + path + " exists"); - return sec; - } catch (IOException e) { - throw new SecDispatcherException(e.getMessage(), e); - } - } - - private String getMasterPassword(SettingsSecurity sec, boolean mandatory) throws SecDispatcherException { - if (sec == null && !mandatory) { - return null; - } - requireNonNull(sec, "configuration is null"); - String masterSource = requireNonNull(sec.getMasterSource(), "masterSource is null"); - for (MasterPasswordSource masterPasswordSource : masterPasswordSources.values()) { - String masterPassword = masterPasswordSource.handle(masterSource); - if (masterPassword != null) return masterPassword; - } - if (mandatory) { - throw new SecDispatcherException("master password could not be fetched"); - } else { - return null; - } - } - - private String getMasterCipher(SettingsSecurity sec) throws SecDispatcherException { - requireNonNull(sec, "configuration is null"); - return requireNonNull(sec.getMasterCipher(), "masterCipher is null"); - } - - public String getConfigurationFile() { - return configurationFile; - } } diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/MasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/MasterPasswordSource.java deleted file mode 100644 index e5704fd..0000000 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/MasterPasswordSource.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2008 Sonatype, Inc. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ - -package org.codehaus.plexus.components.secdispatcher.internal; - -import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; - -/** - * Source of master password. - */ -public interface MasterPasswordSource { - /** - * Handles the URI to get master password. Implementation may do one of the following things: - *

    - *
  • if the URI cannot be handled by given source, return {@code null}
  • - *
  • if master password retrieval was attempted, but failed throw {@link SecDispatcherException}
  • - *
  • happy path: return the master password.
  • - *
- * - * @param masterSource the source of master password, and opaque string. - * @return the master password, or {@code null} if implementation does not handle this masterSource - * @throws SecDispatcherException If implementation does handle this masterSource, but cannot obtain it - */ - String handle(String masterSource) throws SecDispatcherException; -} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java index 0338683..5d93c19 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java @@ -48,7 +48,7 @@ public final class SecUtil { private SecUtil() {} /** - * Reads the configuration model up, optionally resolving relocation too. + * Reads the configuration model up, if exists, otherwise returns {@code null}. */ public static SettingsSecurity read(Path configurationFile) throws IOException { requireNonNull(configurationFile, "configurationFile must not be null"); @@ -65,6 +65,9 @@ public static SettingsSecurity read(Path configurationFile) throws IOException { } } + /** + * Returns config with given name, or {@code null} if not exist. + */ public static Map getConfig(SettingsSecurity sec, String name) { if (sec != null && name != null) { List cl = sec.getConfigurations(); @@ -88,6 +91,14 @@ public static Map getConfig(SettingsSecurity sec, String name) { return null; } + public static String specVersion() { + String specVer = SecDispatcher.class.getPackage().getSpecificationVersion(); + if (specVer == null) { + specVer = "test"; // in UT + } + return specVer; + } + private static final boolean IS_WINDOWS = System.getProperty("os.name", "unknown").startsWith("Windows"); @@ -99,7 +110,7 @@ public static void write(Path target, SettingsSecurity configuration, boolean do Path tempFile = parent.resolve(target.getFileName() + "." + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp"); - configuration.setModelVersion(SecDispatcher.class.getPackage().getSpecificationVersion()); + configuration.setModelVersion(specVersion()); configuration.setModelEncoding(StandardCharsets.UTF_8.name()); try { diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcher.java new file mode 100644 index 0000000..74daabe --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcher.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2008 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +package org.codehaus.plexus.components.secdispatcher.internal.dispatchers; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.codehaus.plexus.components.cipher.PlexusCipher; +import org.codehaus.plexus.components.cipher.PlexusCipherException; +import org.codehaus.plexus.components.secdispatcher.Dispatcher; +import org.codehaus.plexus.components.secdispatcher.DispatcherMeta; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; +import org.xml.sax.InputSource; + +/** + * This dispatcher is legacy, serves the purpose of migration only. Should not be used. + */ +@Singleton +@Named(LegacyDispatcher.NAME) +public class LegacyDispatcher implements Dispatcher, DispatcherMeta { + public static final String NAME = "legacy"; + + private static final String MASTER_MASTER_PASSWORD = "settings.security"; + + private final PlexusCipher plexusCipher; + private final LegacyCipher legacyCipher; + + @Inject + public LegacyDispatcher(PlexusCipher plexusCipher) { + this.plexusCipher = plexusCipher; + this.legacyCipher = new LegacyCipher(); + } + + @Override + public boolean isHidden() { + return true; + } + + @Override + public String name() { + return NAME; + } + + @Override + public String displayName() { + return "LEGACY (for migration purposes only; can only decrypt)"; + } + + @Override + public Collection fields() { + return List.of(); + } + + @Override + public EncryptPayload encrypt(String str, Map attributes, Map config) + throws SecDispatcherException { + throw new SecDispatcherException( + NAME + " dispatcher MUST not be used for encryption; is inherently insecure and broken"); + } + + @Override + public String decrypt(String str, Map attributes, Map config) + throws SecDispatcherException { + try { + String masterPassword = getMasterPassword(); + if (masterPassword == null) { + throw new SecDispatcherException("Master password could not be obtained"); + } + return legacyCipher.decrypt64(str, masterPassword); + } catch (PlexusCipherException e) { + throw new SecDispatcherException("Decrypt failed", e); + } + } + + @Override + public SecDispatcher.ValidationResponse validateConfiguration(Map config) { + HashMap> report = new HashMap<>(); + boolean valid = false; + try { + String mpe = getMasterMasterPasswordFromSettingsSecurityXml(); + if (mpe == null) { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Legacy configuration not found or does not contains encrypted master password"); + } else { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>()) + .add("Legacy configuration found with encrypted master password"); + + String mp = getMasterPassword(); + if (mp == null) { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Legacy master password not found"); + } else { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>()) + .add("Legacy master password successfully decrypted"); + valid = true; + } + } + } catch (PlexusCipherException e) { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Legacy master password decryption failed"); + } + return new SecDispatcher.ValidationResponse(getClass().getSimpleName(), valid, report, List.of()); + } + + private String getMasterPassword() throws SecDispatcherException { + String encryptedMasterPassword = getMasterMasterPasswordFromSettingsSecurityXml(); + if (encryptedMasterPassword == null) { + return null; + } + return legacyCipher.decrypt64(plexusCipher.unDecorate(encryptedMasterPassword), MASTER_MASTER_PASSWORD); + } + + private String getMasterMasterPasswordFromSettingsSecurityXml() { + Path xml; + String override = System.getProperty(MASTER_MASTER_PASSWORD); + if (override != null) { + xml = Paths.get(override); + } else { + xml = Paths.get(System.getProperty("user.home"), ".m2", "settings-security.xml"); + } + if (Files.exists(xml)) { + try (InputStream is = Files.newInputStream(xml)) { + return (String) XPathFactory.newInstance() + .newXPath() + .evaluate("//master", new InputSource(is), XPathConstants.STRING); + } catch (Exception e) { + // just ignore whatever it is + } + } + return null; + } + + private static final class LegacyCipher { + private static final String STRING_ENCODING = "UTF8"; + private static final int SPICE_SIZE = 16; + private static final int SALT_SIZE = 8; + private static final String DIGEST_ALG = "SHA-256"; + private static final String KEY_ALG = "AES"; + private static final String CIPHER_ALG = "AES/CBC/PKCS5Padding"; + + private String decrypt64(final String encryptedText, final String password) throws PlexusCipherException { + try { + byte[] allEncryptedBytes = Base64.getDecoder().decode(encryptedText.getBytes()); + int totalLen = allEncryptedBytes.length; + byte[] salt = new byte[SALT_SIZE]; + System.arraycopy(allEncryptedBytes, 0, salt, 0, SALT_SIZE); + byte padLen = allEncryptedBytes[SALT_SIZE]; + byte[] encryptedBytes = new byte[totalLen - SALT_SIZE - 1 - padLen]; + System.arraycopy(allEncryptedBytes, SALT_SIZE + 1, encryptedBytes, 0, encryptedBytes.length); + Cipher cipher = createCipher(password.getBytes(STRING_ENCODING), salt, Cipher.DECRYPT_MODE); + byte[] clearBytes = cipher.doFinal(encryptedBytes); + return new String(clearBytes, STRING_ENCODING); + } catch (Exception e) { + throw new PlexusCipherException("Error decrypting", e); + } + } + + private Cipher createCipher(final byte[] pwdAsBytes, byte[] salt, final int mode) + throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException { + MessageDigest _digester = MessageDigest.getInstance(DIGEST_ALG); + byte[] keyAndIv = new byte[SPICE_SIZE * 2]; + if (salt == null || salt.length == 0) { + salt = null; + } + byte[] result; + int currentPos = 0; + while (currentPos < keyAndIv.length) { + _digester.update(pwdAsBytes); + if (salt != null) { + _digester.update(salt, 0, 8); + } + result = _digester.digest(); + int stillNeed = keyAndIv.length - currentPos; + if (result.length > stillNeed) { + byte[] b = new byte[stillNeed]; + System.arraycopy(result, 0, b, 0, b.length); + result = b; + } + System.arraycopy(result, 0, keyAndIv, currentPos, result.length); + currentPos += result.length; + if (currentPos < keyAndIv.length) { + _digester.reset(); + _digester.update(result); + } + } + byte[] key = new byte[SPICE_SIZE]; + byte[] iv = new byte[SPICE_SIZE]; + System.arraycopy(keyAndIv, 0, key, 0, key.length); + System.arraycopy(keyAndIv, key.length, iv, 0, iv.length); + Cipher cipher = Cipher.getInstance(CIPHER_ALG); + cipher.init(mode, new SecretKeySpec(key, KEY_ALG), new IvParameterSpec(iv)); + return cipher; + } + } +} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterDispatcher.java new file mode 100644 index 0000000..ce1fa46 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterDispatcher.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2008 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +package org.codehaus.plexus.components.secdispatcher.internal.dispatchers; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.codehaus.plexus.components.cipher.PlexusCipher; +import org.codehaus.plexus.components.cipher.PlexusCipherException; +import org.codehaus.plexus.components.secdispatcher.Dispatcher; +import org.codehaus.plexus.components.secdispatcher.DispatcherMeta; +import org.codehaus.plexus.components.secdispatcher.MasterSource; +import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; + +/** + * This dispatcher is logically equivalent (but much more secure) that Maven3 "master password" encryption. + */ +@Singleton +@Named(MasterDispatcher.NAME) +public class MasterDispatcher implements Dispatcher, DispatcherMeta { + public static final String NAME = "master"; + + private static final String CONF_MASTER_CIPHER = "cipher"; + private static final String CONF_MASTER_SOURCE = "source"; + /** + * Attribute holding the Cipher name used to encrypt the password. + */ + private static final String MASTER_CIPHER_ATTR = CONF_MASTER_CIPHER; + + private final PlexusCipher cipher; + protected final Map masterSources; + + @Inject + public MasterDispatcher(PlexusCipher cipher, Map masterSources) { + this.cipher = cipher; + this.masterSources = masterSources; + } + + @Override + public String name() { + return NAME; + } + + @Override + public String displayName() { + return "Master Password Dispatcher"; + } + + @Override + public Collection fields() { + return List.of( + Field.builder(CONF_MASTER_SOURCE) + .optional(false) + .description("Source of the master password") + .options(masterSources.entrySet().stream() + .map(e -> { + MasterSource ms = e.getValue(); + if (ms instanceof MasterSourceMeta m) { + Field.Builder b = + Field.builder(e.getKey()).description(m.description()); + if (m.configTemplate().isPresent()) { + b.defaultValue(m.configTemplate().get()); + } + return b.build(); + } else { + return Field.builder(e.getKey()) + .description(e.getKey() + + "(Field not described, needs manual configuration)") + .build(); + } + }) + .toList()) + .build(), + Field.builder(CONF_MASTER_CIPHER) + .optional(false) + .description("Cipher to use with master password") + .options(cipher.availableCiphers().stream() + .map(c -> Field.builder(c).description(c).build()) + .toList()) + .build()); + } + + @Override + public EncryptPayload encrypt(String str, Map attributes, Map config) + throws SecDispatcherException { + try { + String masterCipher = getMasterCipher(config, true); + String encrypted = cipher.encrypt(masterCipher, str, getMasterPassword(config)); + HashMap attr = new HashMap<>(attributes); + attr.put(MASTER_CIPHER_ATTR, masterCipher); + return new EncryptPayload(attr, encrypted); + } catch (PlexusCipherException e) { + throw new SecDispatcherException("Encrypt failed", e); + } + } + + @Override + public String decrypt(String str, Map attributes, Map config) + throws SecDispatcherException { + try { + String masterCipher = getMasterCipher(attributes, false); + return cipher.decrypt(masterCipher, str, getMasterPassword(config)); + } catch (PlexusCipherException e) { + throw new SecDispatcherException("Decrypt failed", e); + } + } + + @Override + public SecDispatcher.ValidationResponse validateConfiguration(Map config) { + HashMap> report = new HashMap<>(); + ArrayList subsystems = new ArrayList<>(); + boolean valid = false; + String masterCipher = config.get(CONF_MASTER_CIPHER); + if (masterCipher == null) { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Cipher configuration missing"); + } else { + if (!cipher.availableCiphers().contains(masterCipher)) { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Configured Cipher not supported"); + } else { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>()) + .add("Configured Cipher supported"); + } + } + String masterSource = config.get(CONF_MASTER_SOURCE); + if (masterSource == null) { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Source configuration missing"); + } else { + SecDispatcher.ValidationResponse masterSourceResponse = null; + for (MasterSource masterPasswordSource : masterSources.values()) { + masterSourceResponse = masterPasswordSource.validateConfiguration(masterSource); + if (masterSourceResponse != null) { + break; + } + } + if (masterSourceResponse == null) { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Configured Source configuration not handled"); + } else { + subsystems.add(masterSourceResponse); + if (!masterSourceResponse.isValid()) { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Configured Source configuration invalid"); + } else { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>()) + .add("Configured Source configuration valid"); + valid = true; + } + } + } + return new SecDispatcher.ValidationResponse(getClass().getSimpleName(), valid, report, subsystems); + } + + private String getMasterPassword(Map config) throws SecDispatcherException { + String masterSource = config.get(CONF_MASTER_SOURCE); + if (masterSource == null) { + throw new SecDispatcherException("Invalid configuration: Missing configuration " + CONF_MASTER_SOURCE); + } + for (MasterSource masterPasswordSource : masterSources.values()) { + String masterPassword = masterPasswordSource.handle(masterSource); + if (masterPassword != null) return masterPassword; + } + throw new SecDispatcherException("No source handled the given masterSource: " + masterSource); + } + + private String getMasterCipher(Map source, boolean config) throws SecDispatcherException { + if (config) { + String masterCipher = source.get(CONF_MASTER_CIPHER); + if (masterCipher == null) { + throw new SecDispatcherException("Invalid configuration: Missing configuration " + CONF_MASTER_CIPHER); + } + return masterCipher; + } else { + String masterCipher = source.get(MASTER_CIPHER_ATTR); + if (masterCipher == null) { + throw new SecDispatcherException("Malformed attributes: Missing attribute " + MASTER_CIPHER_ATTR); + } + return masterCipher; + } + } +} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterPasswordSource.java deleted file mode 100644 index ede5ce1..0000000 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterPasswordSource.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.codehaus.plexus.components.secdispatcher.internal.sources; - -import javax.inject.Named; -import javax.inject.Singleton; - -import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; - -/** - * Password source that uses env. - */ -@Singleton -@Named(EnvMasterPasswordSource.NAME) -public final class EnvMasterPasswordSource extends PrefixMasterPasswordSourceSupport { - public static final String NAME = "env"; - - public EnvMasterPasswordSource() { - super(NAME + ":"); - } - - @Override - protected String doHandle(String transformed) throws SecDispatcherException { - String value = System.getenv(transformed); - if (value == null) { - throw new SecDispatcherException("Environment variable '" + transformed + "' not found"); - } - return value; - } -} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterSource.java new file mode 100644 index 0000000..431a204 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterSource.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.codehaus.plexus.components.secdispatcher.internal.sources; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; + +/** + * Password source that uses env. + *

+ * Config: {@code env:$ENVIRONMENT_VARIABLE_NAME} + */ +@Singleton +@Named(EnvMasterSource.NAME) +public final class EnvMasterSource extends PrefixMasterSourceSupport implements MasterSourceMeta { + public static final String NAME = "env"; + + public EnvMasterSource() { + super(NAME + ":"); + } + + @Override + public String description() { + return "Environment variable (variable name should be edited)"; + } + + @Override + public Optional configTemplate() { + return Optional.of(NAME + ":$VARIABLE_NAME"); + } + + @Override + protected String doHandle(String transformed) throws SecDispatcherException { + String value = System.getenv(transformed); + if (value == null) { + throw new SecDispatcherException("Environment variable '" + transformed + "' not found"); + } + return value; + } + + @Override + protected SecDispatcher.ValidationResponse doValidateConfiguration(String transformed) { + String value = System.getenv(transformed); + if (value == null) { + return new SecDispatcher.ValidationResponse( + getClass().getSimpleName(), + true, + Map.of( + SecDispatcher.ValidationResponse.Level.WARNING, + List.of("Configured environment variable not exist")), + List.of()); + } else { + return new SecDispatcher.ValidationResponse( + getClass().getSimpleName(), + true, + Map.of( + SecDispatcher.ValidationResponse.Level.INFO, + List.of("Configured environment variable exist")), + List.of()); + } + } +} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterSource.java similarity index 66% rename from src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterPasswordSource.java rename to src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterSource.java index afe2ffa..9a0b486 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterPasswordSource.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterSource.java @@ -29,24 +29,43 @@ import java.net.UnixDomainSocketAddress; import java.nio.channels.Channels; import java.nio.channels.SocketChannel; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; import java.util.HexFormat; +import java.util.List; +import java.util.Optional; +import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; /** * Password source that uses GnuPG Agent. + *

+ * Config: {@code gpg-agent:$agentSocketPath[?non-interactive]} */ @Singleton -@Named(GpgAgentMasterPasswordSource.NAME) -public final class GpgAgentMasterPasswordSource extends PrefixMasterPasswordSourceSupport { +@Named(GpgAgentMasterSource.NAME) +public final class GpgAgentMasterSource extends PrefixMasterSourceSupport implements MasterSourceMeta { public static final String NAME = "gpg-agent"; - public GpgAgentMasterPasswordSource() { + public GpgAgentMasterSource() { super(NAME + ":"); } + @Override + public String description() { + return "GPG Agent (agent socket path should be edited)"; + } + + @Override + public Optional configTemplate() { + return Optional.of(NAME + ":$agentSocketPath"); + } + @Override protected String doHandle(String transformed) throws SecDispatcherException { String extra = ""; @@ -69,6 +88,39 @@ protected String doHandle(String transformed) throws SecDispatcherException { } } + @Override + protected SecDispatcher.ValidationResponse doValidateConfiguration(String transformed) { + HashMap> report = new HashMap<>(); + boolean valid = false; + + String extra = ""; + if (transformed.contains("?")) { + extra = transformed.substring(transformed.indexOf("?")); + transformed = transformed.substring(0, transformed.indexOf("?")); + } + Path socketLocation = Paths.get(transformed); + if (!socketLocation.isAbsolute()) { + socketLocation = Paths.get(System.getProperty("user.home")) + .resolve(socketLocation) + .toAbsolutePath(); + } + if (Files.exists(socketLocation)) { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Unix domain socket for GPG Agent does not exist. Maybe you need to start gpg-agent?"); + } else { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>()) + .add("Unix domain socket for GPG Agent exist"); + valid = true; + } + boolean interactive = !extra.contains("non-interactive"); + if (!interactive) { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.WARNING, k -> new ArrayList<>()) + .add( + "Non-interactive flag found, gpg-agent will not ask for passphrase, it can use only cached ones"); + } + return new SecDispatcher.ValidationResponse(getClass().getSimpleName(), valid, report, List.of()); + } + private String load(Path socketPath, boolean interactive) throws IOException { try (SocketChannel sock = SocketChannel.open(StandardProtocolFamily.UNIX)) { sock.connect(UnixDomainSocketAddress.of(socketPath)); diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterPasswordSourceSupport.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterSourceSupport.java similarity index 72% rename from src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterPasswordSourceSupport.java rename to src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterSourceSupport.java index 7b19876..56be402 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterPasswordSourceSupport.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterSourceSupport.java @@ -21,19 +21,20 @@ import java.util.function.Function; import java.util.function.Predicate; +import org.codehaus.plexus.components.secdispatcher.MasterSource; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; -import org.codehaus.plexus.components.secdispatcher.internal.MasterPasswordSource; import static java.util.Objects.requireNonNull; /** * Master password source support class. */ -public abstract class MasterPasswordSourceSupport implements MasterPasswordSource { +public abstract class MasterSourceSupport implements MasterSource { private final Predicate matcher; private final Function transformer; - public MasterPasswordSourceSupport(Predicate matcher, Function transformer) { + public MasterSourceSupport(Predicate matcher, Function transformer) { this.matcher = requireNonNull(matcher); this.transformer = requireNonNull(transformer); } @@ -47,4 +48,13 @@ public String handle(String masterSource) throws SecDispatcherException { } protected abstract String doHandle(String transformed) throws SecDispatcherException; + + public SecDispatcher.ValidationResponse validateConfiguration(String masterSource) { + if (matcher.test(masterSource)) { + return doValidateConfiguration(transformer.apply(masterSource)); + } + return null; + } + + protected abstract SecDispatcher.ValidationResponse doValidateConfiguration(String transformed); } diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PinEntryMasterSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PinEntryMasterSource.java new file mode 100644 index 0000000..b0f6c0e --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PinEntryMasterSource.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.codehaus.plexus.components.secdispatcher.internal.sources; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +import org.codehaus.plexus.components.secdispatcher.MasterSource; +import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta; +import org.codehaus.plexus.components.secdispatcher.PinEntry; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; + +/** + * Master source using {@link PinEntry} + */ +@Singleton +@Named(PinEntryMasterSource.NAME) +public class PinEntryMasterSource extends PrefixMasterSourceSupport implements MasterSource, MasterSourceMeta { + public static final String NAME = "pinentry-prompt"; + + public PinEntryMasterSource() { + super(NAME + ":"); + } + + @Override + public String description() { + return "Secure PinEntry prompt (pinentry path should be edited)"; + } + + @Override + public Optional configTemplate() { + return Optional.of(NAME + ":$pinentryPath"); + } + + @Override + public String doHandle(String s) throws SecDispatcherException { + try { + PinEntry.Result result = new PinEntry(s) + .setTimeout(Duration.ofSeconds(30)) + .setKeyInfo("Maven: n/masterPassword") + .setTitle("Maven Master Password") + .setDescription("Please enter the Maven master password") + .setPrompt("Maven master password") + .setOk("Ok") + .setCancel("Cancel") + .getPin(); + if (result.outcome() == PinEntry.Outcome.SUCCESS) { + return result.payload(); + } else if (result.outcome() == PinEntry.Outcome.CANCELED) { + throw new SecDispatcherException("User canceled the operation"); + } else if (result.outcome() == PinEntry.Outcome.TIMEOUT) { + throw new SecDispatcherException("Timeout"); + } else { + throw new SecDispatcherException("Failure: " + result.payload()); + } + } catch (IOException e) { + throw new SecDispatcherException("Could not collect the password", e); + } + } + + @Override + protected SecDispatcher.ValidationResponse doValidateConfiguration(String transformed) { + HashMap> report = new HashMap<>(); + boolean valid = false; + + Path pinentry = Paths.get(transformed); + if (!Files.exists(pinentry)) { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Configured pinentry command not found"); + } else { + if (!Files.isExecutable(pinentry)) { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>()) + .add("Configured pinentry command is not executable"); + } else { + report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>()) + .add("Configured pinentry command exists and is executable"); + valid = true; + } + } + return new SecDispatcher.ValidationResponse(getClass().getSimpleName(), valid, report, List.of()); + } +} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterPasswordSourceSupport.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterSourceSupport.java similarity index 90% rename from src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterPasswordSourceSupport.java rename to src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterSourceSupport.java index 3d2d6b3..926ce87 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterPasswordSourceSupport.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterSourceSupport.java @@ -26,8 +26,8 @@ /** * Master password source support class for simple "prefix" use case. */ -public abstract class PrefixMasterPasswordSourceSupport extends MasterPasswordSourceSupport { - public PrefixMasterPasswordSourceSupport(String prefix) { +public abstract class PrefixMasterSourceSupport extends MasterSourceSupport { + public PrefixMasterSourceSupport(String prefix) { super(prefixMatcher(prefix), prefixRemover(prefix)); } diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterPasswordSource.java deleted file mode 100644 index 58b08b8..0000000 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterPasswordSource.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.codehaus.plexus.components.secdispatcher.internal.sources; - -import javax.inject.Named; -import javax.inject.Singleton; - -import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; - -/** - * Password source that uses env. - */ -@Singleton -@Named(SystemPropertyMasterPasswordSource.NAME) -public final class SystemPropertyMasterPasswordSource extends PrefixMasterPasswordSourceSupport { - public static final String NAME = "prop"; - - public SystemPropertyMasterPasswordSource() { - super(NAME + ":"); - } - - @Override - protected String doHandle(String transformed) throws SecDispatcherException { - String value = System.getProperty(transformed); - if (value == null) { - throw new SecDispatcherException("System property '" + transformed + "' not found"); - } - return value; - } -} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterSource.java new file mode 100644 index 0000000..9644bb4 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterSource.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.codehaus.plexus.components.secdispatcher.internal.sources; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; + +/** + * Password source that uses env. + *

+ * Config: {@code system-property:$systemPropertyName} + */ +@Singleton +@Named(SystemPropertyMasterSource.NAME) +public final class SystemPropertyMasterSource extends PrefixMasterSourceSupport implements MasterSourceMeta { + public static final String NAME = "system-property"; + + public SystemPropertyMasterSource() { + super(NAME + ":"); + } + + @Override + public String description() { + return "Java System properties (property name should be edited)"; + } + + @Override + public Optional configTemplate() { + return Optional.of(NAME + ":$systemProperty"); + } + + @Override + protected String doHandle(String transformed) throws SecDispatcherException { + String value = System.getProperty(transformed); + if (value == null) { + throw new SecDispatcherException("System property '" + transformed + "' not found"); + } + return value; + } + + @Override + protected SecDispatcher.ValidationResponse doValidateConfiguration(String transformed) { + String value = System.getProperty(transformed); + if (value == null) { + return new SecDispatcher.ValidationResponse( + getClass().getSimpleName(), + true, + Map.of( + SecDispatcher.ValidationResponse.Level.WARNING, + List.of("Configured Java System Property not exist")), + List.of()); + } else { + return new SecDispatcher.ValidationResponse( + getClass().getSimpleName(), + true, + Map.of( + SecDispatcher.ValidationResponse.Level.INFO, + List.of("Configured Java System Property exist")), + List.of()); + } + } +} diff --git a/src/main/mdo/settings-security.mdo b/src/main/mdo/settings-security.mdo index 336c179..f16b58a 100644 --- a/src/main/mdo/settings-security.mdo +++ b/src/main/mdo/settings-security.mdo @@ -56,18 +56,25 @@ masterSource - 3.0.0+ + 3.0.0/3.0.0 String true The masterSource describes the source of the master password masterCipher - 3.0.0+ + 3.0.0/3.0.0 String true The Cipher to be used for master password + + defaultDispatcher + 4.0.0+ + String + true + The default dispatcher to be used when no dispatcher name provided + configurations 1.0.0+ diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java index 5ecb58d..d1e947b 100644 --- a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java @@ -16,205 +16,132 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Base64; import java.util.Map; -import java.util.Set; import org.codehaus.plexus.components.cipher.internal.AESGCMNoPadding; import org.codehaus.plexus.components.cipher.internal.DefaultPlexusCipher; import org.codehaus.plexus.components.secdispatcher.SecDispatcher; -import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; -import org.codehaus.plexus.components.secdispatcher.internal.dispatcher.StaticDispatcher; -import org.codehaus.plexus.components.secdispatcher.internal.sources.EnvMasterPasswordSource; -import org.codehaus.plexus.components.secdispatcher.internal.sources.GpgAgentMasterPasswordSource; -import org.codehaus.plexus.components.secdispatcher.internal.sources.StaticMasterPasswordSource; -import org.codehaus.plexus.components.secdispatcher.internal.sources.SystemPropertyMasterPasswordSource; +import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher; +import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.MasterDispatcher; +import org.codehaus.plexus.components.secdispatcher.internal.sources.EnvMasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.sources.GpgAgentMasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.sources.SystemPropertyMasterSource; +import org.codehaus.plexus.components.secdispatcher.model.Config; +import org.codehaus.plexus.components.secdispatcher.model.ConfigProperty; import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity; import org.codehaus.plexus.components.secdispatcher.model.io.stax.SecurityConfigurationStaxWriter; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; public class DefaultSecDispatcherTest { - String masterPassword = "masterPw"; - String password = "somePassword"; + private final Path CONFIG_PATH = Paths.get("./target/sec.xml"); - private void saveSec(String masterSource) throws Exception { + private void saveSec(String dispatcher, Map config) throws Exception { SettingsSecurity sec = new SettingsSecurity(); sec.setModelEncoding(StandardCharsets.UTF_8.name()); sec.setModelVersion(SecDispatcher.class.getPackage().getSpecificationVersion()); - sec.setMasterSource(masterSource); - sec.setMasterCipher(AESGCMNoPadding.CIPHER_ALG); - - try (OutputStream fos = Files.newOutputStream(Paths.get("./target/sec.xml"))) { - new SecurityConfigurationStaxWriter().write(fos, sec); + sec.setDefaultDispatcher(dispatcher); + Config conf = new Config(); + conf.setName(dispatcher); + for (Map.Entry entry : config.entrySet()) { + ConfigProperty prop = new ConfigProperty(); + prop.setName(entry.getKey()); + prop.setValue(entry.getValue()); + conf.addProperty(prop); } - System.setProperty(DefaultSecDispatcher.SYSTEM_PROPERTY_CONFIGURATION_LOCATION, "./target/sec.xml"); + sec.getConfigurations().add(conf); + saveSec(sec); } - @BeforeEach - public void prepare() throws Exception { - saveSec("magic:might"); - } + private void saveSec(SettingsSecurity sec) throws Exception { + sec.setModelEncoding(StandardCharsets.UTF_8.name()); + sec.setModelVersion(SecDispatcher.class.getPackage().getSpecificationVersion()); - @Test - void testEncrypt() throws Exception { - DefaultSecDispatcher sd = new DefaultSecDispatcher( - new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())), - Map.of("static", new StaticMasterPasswordSource(masterPassword)), - Map.of(), - DefaultSecDispatcher.DEFAULT_CONFIGURATION); - String enc = sd.encrypt(password, null); - assertNotNull(enc); - String password1 = sd.decrypt(enc); - assertEquals(password, password1); + try (OutputStream fos = Files.newOutputStream(CONFIG_PATH)) { + new SecurityConfigurationStaxWriter().write(fos, sec); + } } @Test - void testDecrypt() throws Exception { - DefaultSecDispatcher sd = new DefaultSecDispatcher( - new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())), - Map.of("static", new StaticMasterPasswordSource(masterPassword)), - Map.of(), - DefaultSecDispatcher.DEFAULT_CONFIGURATION); - String encrypted = sd.encrypt(password, null); - String pass = sd.decrypt(encrypted); - assertNotNull(pass); - assertEquals(password, pass); + void masterWithEnvRoundTrip() throws Exception { + saveSec("master", Map.of("source", "env:MASTER_PASSWORD", "cipher", AESGCMNoPadding.CIPHER_ALG)); + roundtrip(); } @Test - void testDecryptSystemProperty() throws Exception { - System.setProperty("foobar", masterPassword); - saveSec("prop:foobar"); - DefaultSecDispatcher sd = new DefaultSecDispatcher( - new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())), - Map.of( - "prop", - new SystemPropertyMasterPasswordSource(), - "env", - new EnvMasterPasswordSource(), - "gpg", - new GpgAgentMasterPasswordSource()), - Map.of(), - DefaultSecDispatcher.DEFAULT_CONFIGURATION); - String encrypted = sd.encrypt(password, null); - String pass = sd.decrypt(encrypted); - assertNotNull(pass); - assertEquals(password, pass); + void masterWithSystemPropertyRoundTrip() throws Exception { + saveSec("master", Map.of("source", "system-property:masterPassword", "cipher", AESGCMNoPadding.CIPHER_ALG)); + roundtrip(); } @Test - void testDecryptEnv() throws Exception { - saveSec("env:MASTER_PASSWORD"); - DefaultSecDispatcher sd = new DefaultSecDispatcher( - new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())), - Map.of( - "prop", - new SystemPropertyMasterPasswordSource(), - "env", - new EnvMasterPasswordSource(), - "gpg", - new GpgAgentMasterPasswordSource()), - Map.of(), - DefaultSecDispatcher.DEFAULT_CONFIGURATION); - String encrypted = sd.encrypt(password, null); - String pass = sd.decrypt(encrypted); - assertNotNull(pass); - assertEquals(password, pass); + void validate() throws Exception { + saveSec("master", Map.of("source", "system-property:masterPassword", "cipher", AESGCMNoPadding.CIPHER_ALG)); + System.setProperty("settings.security", "src/test/legacy/legacy-settings-security-1.xml"); + + SecDispatcher secDispatcher = construct(); + SecDispatcher.ValidationResponse response = secDispatcher.validateConfiguration(); + assertTrue(response.isValid()); + // secDispatcher + assertEquals(1, response.getReport().size()); + assertEquals(2, response.getSubsystems().size()); + // master dispatcher + assertEquals(1, response.getSubsystems().get(0).getReport().size()); + assertEquals(1, response.getSubsystems().get(0).getSubsystems().size()); + // master source + assertTrue(response.getSubsystems() + .get(0) + .getSubsystems() + .get(0) + .getReport() + .size() + == 1); + assertTrue(response.getSubsystems() + .get(0) + .getSubsystems() + .get(0) + .getSubsystems() + .size() + == 0); } - @Disabled("triggers GPG agent: remove this and type in 'masterPw'") - @Test - void testDecryptGpg() throws Exception { - saveSec("gpg-agent:/run/user/1000/gnupg/S.gpg-agent"); - DefaultSecDispatcher sd = new DefaultSecDispatcher( - new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())), - Map.of( - "prop", - new SystemPropertyMasterPasswordSource(), - "env", - new EnvMasterPasswordSource(), - "gpg", - new GpgAgentMasterPasswordSource()), - Map.of(), - DefaultSecDispatcher.DEFAULT_CONFIGURATION); - String encrypted = sd.encrypt(password, null); + protected void roundtrip() throws Exception { + DefaultSecDispatcher sd = construct(); + + assertEquals(2, sd.availableDispatchers().size()); + String encrypted = sd.encrypt("supersecret", Map.of(SecDispatcher.DISPATCHER_NAME_ATTR, "master", "a", "b")); + // example: + // {[name=master,cipher=AES/GCM/NoPadding,a=b]vvq66pZ7rkvzSPStGTI9q4QDnsmuDwo+LtjraRel2b0XpcGJFdXcYAHAS75HUA6GLpcVtEkmyQ==} + assertTrue(encrypted.startsWith("{") && encrypted.endsWith("}")); + assertTrue(encrypted.contains("name=master")); + assertTrue(encrypted.contains("cipher=" + AESGCMNoPadding.CIPHER_ALG)); + assertTrue(encrypted.contains("version=test")); + assertTrue(encrypted.contains("a=b")); String pass = sd.decrypt(encrypted); - assertNotNull(pass); - assertEquals(password, pass); + assertEquals("supersecret", pass); } - @Test - void testEncryptWithDispatcher() throws Exception { - DefaultSecDispatcher sd = new DefaultSecDispatcher( - new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())), - Map.of("static", new StaticMasterPasswordSource(masterPassword)), - Map.of("magic", new StaticDispatcher("decrypted", "encrypted")), - DefaultSecDispatcher.DEFAULT_CONFIGURATION); - - assertEquals(Set.of("magic"), sd.availableDispatchers()); - String enc = sd.encrypt("whatever", Map.of(SecDispatcher.DISPATCHER_NAME_ATTR, "magic", "a", "b")); - assertNotNull(enc); - assertTrue(enc.contains("encrypted")); - assertTrue(enc.contains(SecDispatcher.DISPATCHER_NAME_ATTR + "=magic")); - String password1 = sd.decrypt(enc); - assertEquals("decrypted", password1); - } - - @Test - void testDecryptWithDispatcher() throws Exception { - DefaultSecDispatcher sd = new DefaultSecDispatcher( - new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())), - Map.of("static", new StaticMasterPasswordSource(masterPassword)), - Map.of("magic", new StaticDispatcher("decrypted", "encrypted")), - DefaultSecDispatcher.DEFAULT_CONFIGURATION); - - assertEquals(Set.of("magic"), sd.availableDispatchers()); - String pass = sd.decrypt("{" + "[a=b," + SecDispatcher.DISPATCHER_NAME_ATTR + "=magic]" - + Base64.getEncoder().encodeToString("whatever".getBytes(StandardCharsets.UTF_8)) + "}"); - assertNotNull(pass); - assertEquals("decrypted", pass); - } - - @Test - void testDecryptWithDispatcherConf() throws Exception { - String bare = Base64.getEncoder().encodeToString("whatever".getBytes(StandardCharsets.UTF_8)); - DefaultSecDispatcher sd = new DefaultSecDispatcher( - new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())), - Map.of("static", new StaticMasterPasswordSource(masterPassword)), - Map.of("magic", new Dispatcher() { - @Override - public String encrypt(String str, Map attributes, Map config) - throws SecDispatcherException { - throw new IllegalStateException("should not be called"); - } - - @Override - public String decrypt(String str, Map attributes, Map config) - throws SecDispatcherException { - assertEquals(bare, str); - assertEquals(2, attributes.size()); - assertEquals("magic", attributes.get(SecDispatcher.DISPATCHER_NAME_ATTR)); - assertEquals("value", attributes.get("key")); - - assertEquals(1, config.size()); - assertEquals(masterPassword, config.get(Dispatcher.CONF_MASTER_PASSWORD)); - - return "magic"; - } - }), - DefaultSecDispatcher.DEFAULT_CONFIGURATION); - - assertEquals(Set.of("magic"), sd.availableDispatchers()); - String pass = sd.decrypt("{" + "[key=value," + SecDispatcher.DISPATCHER_NAME_ATTR + "=magic]" - + Base64.getEncoder().encodeToString("whatever".getBytes(StandardCharsets.UTF_8)) + "}"); - assertNotNull(pass); - assertEquals("magic", pass); + protected DefaultSecDispatcher construct() { + DefaultPlexusCipher dpc = new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())); + return new DefaultSecDispatcher( + dpc, + Map.of( + "master", + new MasterDispatcher( + dpc, + Map.of( + EnvMasterSource.NAME, + new EnvMasterSource(), + SystemPropertyMasterSource.NAME, + new SystemPropertyMasterSource(), + GpgAgentMasterSource.NAME, + new GpgAgentMasterSource())), + "legacy", + new LegacyDispatcher(dpc)), + CONFIG_PATH); } } diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java index f98dccb..7773ad9 100644 --- a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java @@ -41,13 +41,13 @@ public class SecUtilTest { String _propName = "pname"; String _propVal = "pval"; - private void saveSec(String masterSource) throws IOException { - saveSec("./target/sec.xml", masterSource); + private void saveSec(String defaultDispatcher) throws IOException { + saveSec("./target/sec.xml", defaultDispatcher); } - private void saveSec(String path, String masterSource) throws IOException { + private void saveSec(String path, String defaultDispatcher) throws IOException { SettingsSecurity sec = new SettingsSecurity(); - sec.setMasterSource(masterSource); + sec.setDefaultDispatcher(defaultDispatcher); ConfigProperty cp = new ConfigProperty(); cp.setName(_propName); cp.setValue(_propVal); @@ -68,9 +68,9 @@ void readWrite() throws IOException { Path path = Path.of("./target/sec.xml"); SettingsSecurity config = SecUtil.read(path); assertNotNull(config); - assertEquals(SettingsSecurity.class.getPackage().getSpecificationVersion(), config.getModelVersion()); + assertEquals(SecUtil.specVersion(), config.getModelVersion()); assertEquals(StandardCharsets.UTF_8.name(), config.getModelEncoding()); - assertEquals("magic:mighty", config.getMasterSource()); + assertEquals("magic:mighty", config.getDefaultDispatcher()); SecUtil.write(path, config, false); } @@ -79,9 +79,9 @@ void readWriteWithBackup() throws IOException { Path path = Path.of("./target/sec.xml"); SettingsSecurity config = SecUtil.read(path); assertNotNull(config); - assertEquals(SettingsSecurity.class.getPackage().getSpecificationVersion(), config.getModelVersion()); + assertEquals(SecUtil.specVersion(), config.getModelVersion()); assertEquals(StandardCharsets.UTF_8.name(), config.getModelEncoding()); - assertEquals("magic:mighty", config.getMasterSource()); + assertEquals("magic:mighty", config.getDefaultDispatcher()); SecUtil.write(path, config, true); assertTrue(Files.exists(path)); assertTrue(Files.exists(path.getParent().resolve(path.getFileName() + ".bak"))); diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatcher/StaticDispatcher.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatcher/StaticDispatcher.java deleted file mode 100644 index 4088212..0000000 --- a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatcher/StaticDispatcher.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2008 Sonatype, Inc. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ - -package org.codehaus.plexus.components.secdispatcher.internal.dispatcher; - -import java.util.Map; - -import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; -import org.codehaus.plexus.components.secdispatcher.internal.Dispatcher; - -import static java.util.Objects.requireNonNull; - -public class StaticDispatcher implements Dispatcher { - private final String decrypted; - private final String encrypted; - - public StaticDispatcher(String decrypted, String encrypted) { - this.decrypted = requireNonNull(decrypted); - this.encrypted = requireNonNull(encrypted); - } - - @Override - public String encrypt(String str, Map attributes, Map config) - throws SecDispatcherException { - return encrypted; - } - - @Override - public String decrypt(String str, Map attributes, Map config) - throws SecDispatcherException { - return decrypted; - } -} diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcherTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcherTest.java new file mode 100644 index 0000000..f02e758 --- /dev/null +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcherTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2008 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +package org.codehaus.plexus.components.secdispatcher.internal.dispatchers; + +import java.util.Map; + +import org.codehaus.plexus.components.cipher.internal.DefaultPlexusCipher; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class LegacyDispatcherTest { + /** + * Test values created with Maven 3.9.9. + *

+ * master password: "masterpassword" + * password: "password" + */ + @ParameterizedTest + @ValueSource( + strings = { + "src/test/legacy/legacy-settings-security-1.xml", + "src/test/legacy/legacy-settings-security-2.xml" + }) + void smoke(String xml) { + System.setProperty("settings.security", xml); + LegacyDispatcher legacyDispatcher = new LegacyDispatcher(new DefaultPlexusCipher(Map.of())); + // SecDispatcher "un decorates" the PW + String cleartext = legacyDispatcher.decrypt("L6L/HbmrY+cH+sNkphnq3fguYepTpM04WlIXb8nB1pk=", Map.of(), Map.of()); + assertEquals("password", cleartext); + } +} diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SourcesTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SourcesTest.java new file mode 100644 index 0000000..76bdde5 --- /dev/null +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SourcesTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2008 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +package org.codehaus.plexus.components.secdispatcher.internal.sources; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * surefire plugin set system property and env. + */ +public class SourcesTest { + @Test + void systemProperty() { + SystemPropertyMasterSource source = new SystemPropertyMasterSource(); + assertEquals("masterPw", source.handle("system-property:masterPassword")); + } + + @Test + void env() { + EnvMasterSource source = new EnvMasterSource(); + assertEquals("masterPw", source.handle("env:MASTER_PASSWORD")); + } + + @Disabled("enable and type in 'masterPw'") + @Test + void gpgAgent() { + GpgAgentMasterSource source = new GpgAgentMasterSource(); + // you may adjust path, this is Fedora40 WS. Ubuntu does `.gpg/S.gpg-agent` + assertEquals("masterPw", source.handle("gpg-agent:/run/user/1000/gnupg/S.gpg-agent")); + } + + @Disabled("enable and type in 'masterPw'") + @Test + void pinEntry() { + PinEntryMasterSource source = new PinEntryMasterSource(); + // ypu may adjust path, this is Fedora40 WS + gnome + assertEquals("masterPw", source.handle("pinentry-prompt:/usr/bin/pinentry-gnome3")); + } +} diff --git a/src/test/legacy/legacy-settings-security-1.xml b/src/test/legacy/legacy-settings-security-1.xml new file mode 100644 index 0000000..eb4ff1e --- /dev/null +++ b/src/test/legacy/legacy-settings-security-1.xml @@ -0,0 +1,3 @@ + + {KDvsYOFLlXgH4LU8tvpzAGg5otiosZXvfdQq0yO86LU=} + diff --git a/src/test/legacy/legacy-settings-security-2.xml b/src/test/legacy/legacy-settings-security-2.xml new file mode 100644 index 0000000..0f7b33d --- /dev/null +++ b/src/test/legacy/legacy-settings-security-2.xml @@ -0,0 +1,4 @@ + + to the moon + {KDvsYOFLlXgH4LU8tvpzAGg5otiosZXvfdQq0yO86LU=} +