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