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=} +