diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 09feae4..571e41b 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -23,6 +23,8 @@ jobs: build: name: Build it uses: codehaus-plexus/.github/.github/workflows/maven.yml@master + with: + jdk-matrix: '[ "23", "21", "17" ]' deploy: name: Deploy diff --git a/pom.xml b/pom.xml index dcb3c0f..af06ec5 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ plexus-sec-dispatcher - 2.1.0-SNAPSHOT + 3.0.0-SNAPSHOT Plexus Security Dispatcher Component @@ -33,31 +33,30 @@ + 17 2023-05-22T22:22:22Z - - org.codehaus.plexus - plexus-xml - 3.0.1 - - - org.codehaus.plexus - plexus-utils - 4.0.2 - org.codehaus.plexus plexus-cipher - 2.0 + 3.0.0 javax.inject javax.inject 1 + provided + + org.eclipse.sisu + org.eclipse.sisu.inject + ${sisuMavenPluginVersion} + provided + + org.junit.jupiter junit-jupiter @@ -76,7 +75,7 @@ modello-maven-plugin 2.4.0 - 1.0.0 + 3.0.0 src/main/mdo/settings-security.mdo @@ -86,12 +85,22 @@ standard java - xpp3-reader - xpp3-writer + xsd + stax-reader + stax-writer + + org.apache.maven.plugins + maven-surefire-plugin + + + masterPw + + + diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java new file mode 100644 index 0000000..4e88a46 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java @@ -0,0 +1,68 @@ +/* + * 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; + +import java.util.Map; +import java.util.Set; + +/** + * This component decrypts a string, passed to it + * + * @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". + */ + 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. + * + * @see #availableDispatchers() + */ + String DISPATCHER_NAME_ATTR = "name"; + + /** + * Returns the set of available dispatcher names, never {@code null}. + */ + Set availableDispatchers(); + + /** + * encrypt given plaintext string + * + * @param str the plaintext to encrypt + * @param attr the attributes, may be {@code null} + * @return encrypted string + * @throws SecDispatcherException in case of problem + */ + String encrypt(String str, Map attr) throws SecDispatcherException; + + /** + * decrypt given encrypted string + * + * @param str the encrypted string + * @return plaintext string + * @throws SecDispatcherException in case of problem + */ + String decrypt(String str) throws SecDispatcherException; +} diff --git a/src/main/java/org/sonatype/plexus/components/sec/dispatcher/SecDispatcherException.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcherException.java similarity index 82% rename from src/main/java/org/sonatype/plexus/components/sec/dispatcher/SecDispatcherException.java rename to src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcherException.java index c7b8ad1..a83c70e 100644 --- a/src/main/java/org/sonatype/plexus/components/sec/dispatcher/SecDispatcherException.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcherException.java @@ -11,17 +11,13 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ -package org.sonatype.plexus.components.sec.dispatcher; +package org.codehaus.plexus.components.secdispatcher; -public class SecDispatcherException extends Exception { +public class SecDispatcherException extends RuntimeException { public SecDispatcherException(String message) { super(message); } - public SecDispatcherException(Throwable cause) { - super(cause); - } - public SecDispatcherException(String message, Throwable cause) { super(message, cause); } 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 new file mode 100644 index 0000000..25dd6fc --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java @@ -0,0 +1,200 @@ +/* + * 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 javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.stream.Collectors; + +import org.codehaus.plexus.components.cipher.PlexusCipher; +import org.codehaus.plexus.components.cipher.PlexusCipherException; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; +import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity; + +import static java.util.Objects.requireNonNull; + +/** + * @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) { + this.cipher = requireNonNull(cipher); + this.masterPasswordSources = requireNonNull(masterPasswordSources); + this.dispatchers = requireNonNull(dispatchers); + this.configurationFile = requireNonNull(configurationFile); + } + + @Override + public Set availableDispatchers() { + return Set.copyOf(dispatchers.keySet()); + } + + @Override + public String encrypt(String str, Map attr) throws SecDispatcherException { + 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); + } 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)); + } + return cipher.decorate(res); + } catch (PlexusCipherException e) { + throw new SecDispatcherException(e.getMessage(), e); + } + } + + @Override + public String decrypt(String str) throws SecDispatcherException { + 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)); + } + } catch (PlexusCipherException e) { + throw new SecDispatcherException(e.getMessage(), e); + } + } + + 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); + } + Map conf = SecUtil.getConfig(sec, type); + if (conf != null) { + dispatcherConf.putAll(conf); + } + return dispatcherConf; + } + + private String strip(String str) { + int start = str.indexOf(ATTR_START); + int stop = str.indexOf(ATTR_STOP); + if (start != -1 && stop != -1 && stop > start) { + return str.substring(stop + 1); + } + return str; + } + + private Map stripAttributes(String str) { + int start = str.indexOf(ATTR_START); + int stop = str.indexOf(ATTR_STOP); + if (start != -1 && stop != -1 && stop > start) { + if (start != 0) throw new SecDispatcherException("Attributes can be prefix only"); + 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); + } + return res; + } + return null; + } + + private boolean isEncryptedString(String str) { + if (str == null) return false; + return cipher.isEncryptedString(str); + } + + private SettingsSecurity getConfiguration(boolean mandatory) throws SecDispatcherException { + String location = System.getProperty(SYSTEM_PROPERTY_CONFIGURATION_LOCATION, getConfigurationFile()); + location = location.charAt(0) == '~' ? System.getProperty("user.home") + location.substring(1) : location; + SettingsSecurity sec = SecUtil.read(location, true); + if (mandatory && sec == null) + throw new SecDispatcherException("Please check that configuration file on path " + location + " exists"); + + return sec; + } + + 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/Dispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/Dispatcher.java new file mode 100644 index 0000000..de030a8 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/Dispatcher.java @@ -0,0 +1,56 @@ +/* + * 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 java.util.Map; + +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; + +/** + * Dispatcher. + * + * @author Oleg Gusakov + * @version $Id$ + * + */ +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. + */ + String CONF_MASTER_PASSWORD = "masterPassword"; + + /** + * encrypt given plaintext string + * + * @param str string to encrypt + * @param attributes attributes, never {@code null} + * @param config configuration from settings-security.xml, never {@code null} + * @return encrypted string + */ + String encrypt(String str, Map attributes, Map config) + throws SecDispatcherException; + + /** + * decrypt given encrypted string + * + * @param str string to decrypt + * @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; +} 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 new file mode 100644 index 0000000..e5704fd --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/MasterPasswordSource.java @@ -0,0 +1,35 @@ +/* + * 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/sonatype/plexus/components/sec/dispatcher/SecUtil.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java similarity index 59% rename from src/main/java/org/sonatype/plexus/components/sec/dispatcher/SecUtil.java rename to src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java index ccd32da..ebf400e 100644 --- a/src/main/java/org/sonatype/plexus/components/sec/dispatcher/SecUtil.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtil.java @@ -11,21 +11,27 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ -package org.sonatype.plexus.components.sec.dispatcher; +package org.codehaus.plexus.components.secdispatcher.internal; + +import javax.xml.stream.XMLStreamException; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.sonatype.plexus.components.sec.dispatcher.model.Config; -import org.sonatype.plexus.components.sec.dispatcher.model.ConfigProperty; -import org.sonatype.plexus.components.sec.dispatcher.model.SettingsSecurity; -import org.sonatype.plexus.components.sec.dispatcher.model.io.xpp3.SecurityConfigurationXpp3Reader; +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; +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.SecurityConfigurationStaxReader; + +import static java.util.Objects.requireNonNull; /** * @@ -43,68 +49,57 @@ public class SecUtil { public static SettingsSecurity read(String location, boolean cycle) throws SecDispatcherException { if (location == null) throw new SecDispatcherException("location to read from is null"); - SettingsSecurity sec; - try { try (InputStream in = toStream(location)) { - sec = new SecurityConfigurationXpp3Reader().read(in); + sec = new SecurityConfigurationStaxReader().read(in); } - if (cycle && sec.getRelocation() != null) return read(sec.getRelocation(), true); - return sec; - } catch (Exception e) { - throw new SecDispatcherException(e); + } catch (NoSuchFileException e) { + return null; + } catch (IOException e) { + throw new SecDispatcherException("IO Problem", e); + } catch (XMLStreamException e) { + throw new SecDispatcherException("Parsing error", e); } } - // --------------------------------------------------------------------------------------------------------------- - private static InputStream toStream(String resource) throws IOException { - if (resource == null) return null; + private static InputStream toStream(String resource) throws IOException { + requireNonNull(resource, "resource is null"); int ind = resource.indexOf(PROTOCOL_DELIM); - if (ind > 1) { String protocol = resource.substring(0, ind); resource = resource.substring(ind + PROTOCOL_DELIM_LEN); - for (String p : URL_PROTOCOLS) { if (protocol.regionMatches(true, 0, p, 0, p.length())) { return new URL(p + PROTOCOL_DELIM + resource).openStream(); } } } - return Files.newInputStream(Paths.get(resource)); } - // --------------------------------------------------------------------------------------------------------------- - public static Map getConfig(SettingsSecurity sec, String name) { - if (name == null) return null; - List cl = sec.getConfigurations(); - - if (cl == null || cl.isEmpty()) return null; - - for (Config cf : cl) { - if (!name.equals(cf.getName())) { - continue; - } - - List pl = cf.getProperties(); - - if (pl == null || pl.isEmpty()) { - return null; - } - - Map res = new HashMap<>(pl.size()); - - for (ConfigProperty p : pl) { - res.put(p.getName(), p.getValue()); + public static Map getConfig(SettingsSecurity sec, String name) { + if (sec != null && name != null) { + List cl = sec.getConfigurations(); + if (!cl.isEmpty()) { + for (Config cf : cl) { + if (!name.equals(cf.getName())) { + continue; + } + List pl = cf.getProperties(); + if (pl.isEmpty()) { + break; + } + Map res = new HashMap<>(pl.size()); + for (ConfigProperty p : pl) { + res.put(p.getName(), p.getValue()); + } + return res; + } } - - return res; } - return null; } } 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 new file mode 100644 index 0000000..ede5ce1 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/EnvMasterPasswordSource.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.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/GpgAgentMasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterPasswordSource.java new file mode 100644 index 0000000..afe2ffa --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GpgAgentMasterPasswordSource.java @@ -0,0 +1,124 @@ +/* + * 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.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.channels.Channels; +import java.nio.channels.SocketChannel; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HexFormat; + +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; + +/** + * Password source that uses GnuPG Agent. + */ +@Singleton +@Named(GpgAgentMasterPasswordSource.NAME) +public final class GpgAgentMasterPasswordSource extends PrefixMasterPasswordSourceSupport { + public static final String NAME = "gpg-agent"; + + public GpgAgentMasterPasswordSource() { + super(NAME + ":"); + } + + @Override + protected String doHandle(String transformed) throws SecDispatcherException { + String extra = ""; + if (transformed.contains("?")) { + extra = transformed.substring(transformed.indexOf("?")); + transformed = transformed.substring(0, transformed.indexOf("?")); + } + String socketLocation = transformed; + boolean interactive = !extra.contains("non-interactive"); + try { + Path socketLocationPath = Paths.get(socketLocation); + if (!socketLocationPath.isAbsolute()) { + socketLocationPath = Paths.get(System.getProperty("user.home")) + .resolve(socketLocationPath) + .toAbsolutePath(); + } + return load(socketLocationPath, interactive); + } catch (IOException e) { + throw new SecDispatcherException(e.getMessage(), e); + } + } + + private String load(Path socketPath, boolean interactive) throws IOException { + try (SocketChannel sock = SocketChannel.open(StandardProtocolFamily.UNIX)) { + sock.connect(UnixDomainSocketAddress.of(socketPath)); + try (BufferedReader in = new BufferedReader(new InputStreamReader(Channels.newInputStream(sock))); + OutputStream os = Channels.newOutputStream(sock)) { + + expectOK(in); + String display = System.getenv("DISPLAY"); + if (display != null) { + os.write(("OPTION display=" + display + "\n").getBytes()); + os.flush(); + expectOK(in); + } + String term = System.getenv("TERM"); + if (term != null) { + os.write(("OPTION ttytype=" + term + "\n").getBytes()); + os.flush(); + expectOK(in); + } + // https://unix.stackexchange.com/questions/71135/how-can-i-find-out-what-keys-gpg-agent-has-cached-like-how-ssh-add-l-shows-yo + String instruction = "GET_PASSPHRASE " + + (!interactive ? "--no-ask " : "") + + "plexus:secDispatcherMasterPassword" + + " " + + "X " + + "Maven+Master+Password " + + "Please+enter+your+Maven+master+password" + + "+to+use+it+for+decrypting+Maven+Settings\n"; + os.write((instruction).getBytes()); + os.flush(); + return mayExpectOK(in); + } + } + } + + private void expectOK(BufferedReader in) throws IOException { + String response = in.readLine(); + if (!response.startsWith("OK")) { + throw new IOException("Expected OK but got this instead: " + response); + } + } + + private String mayExpectOK(BufferedReader in) throws IOException { + String response = in.readLine(); + if (response.startsWith("ERR")) { + return null; + } else if (!response.startsWith("OK")) { + throw new IOException("Expected OK/ERR but got this instead: " + response); + } + return new String(HexFormat.of() + .parseHex(response.substring(Math.min(response.length(), 3)).trim())); + } +} 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/MasterPasswordSourceSupport.java new file mode 100644 index 0000000..7b19876 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MasterPasswordSourceSupport.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.codehaus.plexus.components.secdispatcher.internal.sources; + +import java.util.function.Function; +import java.util.function.Predicate; + +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 { + private final Predicate matcher; + private final Function transformer; + + public MasterPasswordSourceSupport(Predicate matcher, Function transformer) { + this.matcher = requireNonNull(matcher); + this.transformer = requireNonNull(transformer); + } + + @Override + public String handle(String masterSource) throws SecDispatcherException { + if (matcher.test(masterSource)) { + return doHandle(transformer.apply(masterSource)); + } + return null; + } + + protected abstract String doHandle(String transformed) throws SecDispatcherException; +} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MatchingMasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MatchingMasterPasswordSource.java new file mode 100644 index 0000000..438f49c --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MatchingMasterPasswordSource.java @@ -0,0 +1,39 @@ +/* + * 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 java.util.function.Predicate; + +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; +import org.codehaus.plexus.components.secdispatcher.internal.MasterPasswordSource; + +import static java.util.Objects.requireNonNull; + +public class MatchingMasterPasswordSource implements MasterPasswordSource { + private final Predicate matcher; + private final MasterPasswordSource masterPasswordSource; + + public MatchingMasterPasswordSource(Predicate matcher, MasterPasswordSource masterPasswordSource) { + this.matcher = requireNonNull(matcher); + this.masterPasswordSource = requireNonNull(masterPasswordSource); + } + + @Override + public String handle(String masterSource) throws SecDispatcherException { + if (matcher.test(masterSource)) { + return masterPasswordSource.handle(masterSource); + } + return null; + } +} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MemoizingMasterPasswordSource.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MemoizingMasterPasswordSource.java new file mode 100644 index 0000000..5460062 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/MemoizingMasterPasswordSource.java @@ -0,0 +1,36 @@ +/* + * 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 java.util.concurrent.ConcurrentHashMap; + +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; +import org.codehaus.plexus.components.secdispatcher.internal.MasterPasswordSource; + +import static java.util.Objects.requireNonNull; + +public class MemoizingMasterPasswordSource implements MasterPasswordSource { + private final MasterPasswordSource masterPasswordSource; + private final ConcurrentHashMap memo; + + public MemoizingMasterPasswordSource(MasterPasswordSource masterPasswordSource) { + this.masterPasswordSource = requireNonNull(masterPasswordSource); + this.memo = new ConcurrentHashMap<>(); + } + + @Override + public String handle(String masterSource) throws SecDispatcherException { + return memo.computeIfAbsent(masterSource, k -> masterPasswordSource.handle(masterSource)); + } +} 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/PrefixMasterPasswordSourceSupport.java new file mode 100644 index 0000000..3d2d6b3 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/PrefixMasterPasswordSourceSupport.java @@ -0,0 +1,43 @@ +/* + * 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 java.util.function.Function; +import java.util.function.Predicate; + +import static java.util.Objects.requireNonNull; + +/** + * Master password source support class for simple "prefix" use case. + */ +public abstract class PrefixMasterPasswordSourceSupport extends MasterPasswordSourceSupport { + public PrefixMasterPasswordSourceSupport(String prefix) { + super(prefixMatcher(prefix), prefixRemover(prefix)); + } + + private static Predicate prefixMatcher(String prefix) { + requireNonNull(prefix, "prefix cannot be null"); + return s -> s != null && s.startsWith(prefix); + } + + private static Function prefixRemover(String prefix) { + requireNonNull(prefix, "prefix cannot be null"); + return s -> s.substring(prefix.length()); + } +} diff --git a/src/main/java/org/sonatype/plexus/components/sec/dispatcher/SecDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/StaticMasterPasswordSource.java similarity index 51% rename from src/main/java/org/sonatype/plexus/components/sec/dispatcher/SecDispatcher.java rename to src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/StaticMasterPasswordSource.java index 1557a69..7ef6d89 100644 --- a/src/main/java/org/sonatype/plexus/components/sec/dispatcher/SecDispatcher.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/StaticMasterPasswordSource.java @@ -11,24 +11,22 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ -package org.sonatype.plexus.components.sec.dispatcher; +package org.codehaus.plexus.components.secdispatcher.internal.sources; -/** - * This component decrypts a string, passed to it - * - * @author Oleg Gusakov - */ -public interface SecDispatcher { - String[] SYSTEM_PROPERTY_MASTER_PASSWORD = new String[] {"settings.master.password", "settings-master-password"}; +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; +import org.codehaus.plexus.components.secdispatcher.internal.MasterPasswordSource; + +import static java.util.Objects.requireNonNull; + +public class StaticMasterPasswordSource implements MasterPasswordSource { + private final String masterPassword; - String[] SYSTEM_PROPERTY_SERVER_PASSWORD = new String[] {"settings.server.password", "settings-server-password"}; + public StaticMasterPasswordSource(String masterPassword) { + this.masterPassword = requireNonNull(masterPassword); + } - /** - * decrypt given encrypted string - * - * @param str - * @return decrypted string - * @throws SecDispatcherException - */ - String decrypt(String str) throws SecDispatcherException; + @Override + public String handle(String masterSource) throws SecDispatcherException { + return masterPassword; + } } 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 new file mode 100644 index 0000000..58b08b8 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/SystemPropertyMasterPasswordSource.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.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/sonatype/plexus/components/sec/dispatcher/DefaultSecDispatcher.java b/src/main/java/org/sonatype/plexus/components/sec/dispatcher/DefaultSecDispatcher.java deleted file mode 100644 index 2421787..0000000 --- a/src/main/java/org/sonatype/plexus/components/sec/dispatcher/DefaultSecDispatcher.java +++ /dev/null @@ -1,290 +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.sonatype.plexus.components.sec.dispatcher; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.util.HashMap; -import java.util.Map; -import java.util.StringTokenizer; - -import org.sonatype.plexus.components.cipher.DefaultPlexusCipher; -import org.sonatype.plexus.components.cipher.PlexusCipher; -import org.sonatype.plexus.components.cipher.PlexusCipherException; -import org.sonatype.plexus.components.sec.dispatcher.model.SettingsSecurity; - -/** - * @author Oleg Gusakov - */ -@Singleton -@Named -public class DefaultSecDispatcher implements SecDispatcher { - private static final String DEFAULT_CONFIGURATION = "~/.settings-security.xml"; - - public static final String SYSTEM_PROPERTY_SEC_LOCATION = "settings.security"; - - public static final String TYPE_ATTR = "type"; - - public static final char ATTR_START = '['; - - public static final char ATTR_STOP = ']'; - - /** - * DefaultHandler - */ - protected final PlexusCipher _cipher; - - /** - * All available dispatchers - */ - protected final Map _decryptors; - - /** - * Configuration file - */ - protected String _configurationFile; - - @Inject - public DefaultSecDispatcher( - final PlexusCipher _cipher, - final Map _decryptors, - @Named("${_configurationFile:-" + DEFAULT_CONFIGURATION + "}") final String _configurationFile) { - this._cipher = _cipher; - this._decryptors = _decryptors; - this._configurationFile = _configurationFile; - } - - /** - * Ctor to be used in tests and other simplified cases (no decryptors and config). - */ - public DefaultSecDispatcher(final PlexusCipher _cipher) { - this(_cipher, new HashMap<>(), DEFAULT_CONFIGURATION); - } - - // --------------------------------------------------------------- - - @Override - public String decrypt(String str) throws SecDispatcherException { - if (!isEncryptedString(str)) return str; - - String bare; - - try { - bare = _cipher.unDecorate(str); - } catch (PlexusCipherException e1) { - throw new SecDispatcherException(e1); - } - - try { - Map attr = stripAttributes(bare); - - String res; - - SettingsSecurity sec = getSec(); - - if (attr == null || attr.get("type") == null) { - String master = getMaster(sec); - - res = _cipher.decrypt(bare, master); - } else { - String type = attr.get(TYPE_ATTR); - - if (_decryptors == null) - throw new SecDispatcherException( - "plexus container did not supply any required dispatchers - cannot lookup " + type); - - Map conf = SecUtil.getConfig(sec, type); - - PasswordDecryptor dispatcher = _decryptors.get(type); - - if (dispatcher == null) throw new SecDispatcherException("no dispatcher for hint " + type); - - String pass = attr == null ? bare : strip(bare); - - return dispatcher.decrypt(pass, attr, conf); - } - - return res; - } catch (Exception e) { - throw new SecDispatcherException(e); - } - } - - private String strip(String str) { - int pos = str.indexOf(ATTR_STOP); - - if (pos == str.length()) return null; - - if (pos != -1) return str.substring(pos + 1); - - return str; - } - - private Map stripAttributes(String str) { - int start = str.indexOf(ATTR_START); - int stop = str.indexOf(ATTR_STOP); - if (start != -1 && stop != -1 && stop > start) { - if (stop == start + 1) return null; - - String attrs = str.substring(start + 1, stop).trim(); - - if (attrs.length() < 1) 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) continue; - - String key = pair.substring(0, pos).trim(); - - if (pos == pair.length()) { - res.put(key, null); - continue; - } - - String val = pair.substring(pos + 1); - - res.put(key, val.trim()); - } - - return res; - } - - return null; - } - - // ---------------------------------------------------------------------------- - - private boolean isEncryptedString(String str) { - if (str == null) return false; - - return _cipher.isEncryptedString(str); - } - - // ---------------------------------------------------------------------------- - - private SettingsSecurity getSec() throws SecDispatcherException { - String location = System.getProperty(SYSTEM_PROPERTY_SEC_LOCATION, getConfigurationFile()); - String realLocation = - location.charAt(0) == '~' ? System.getProperty("user.home") + location.substring(1) : location; - - SettingsSecurity sec = SecUtil.read(realLocation, true); - - if (sec == null) - throw new SecDispatcherException( - "cannot retrieve master password. Please check that " + realLocation + " exists and has data"); - - return sec; - } - - // ---------------------------------------------------------------------------- - - private String getMaster(SettingsSecurity sec) throws SecDispatcherException { - String master = sec.getMaster(); - - if (master == null) throw new SecDispatcherException("master password is not set"); - - try { - return _cipher.decryptDecorated(master, SYSTEM_PROPERTY_SEC_LOCATION); - } catch (PlexusCipherException e) { - throw new SecDispatcherException(e); - } - } - // --------------------------------------------------------------- - public String getConfigurationFile() { - return _configurationFile; - } - - public void setConfigurationFile(String file) { - _configurationFile = file; - } - - // --------------------------------------------------------------- - - private static boolean propertyExists(String[] values, String[] av) { - if (values != null) { - for (String item : values) { - String p = System.getProperty(item); - - if (p != null) { - return true; - } - } - - if (av != null) - for (String value : values) - for (String s : av) { - if (("--" + value).equals(s)) { - return true; - } - } - } - - return false; - } - - private static void usage() { - System.out.println("usage: java -jar ...jar [-m|-p]\n-m: encrypt master password\n-p: encrypt password"); - } - - // --------------------------------------------------------------- - - public static void main(String[] args) throws Exception { - if (args == null || args.length < 1) { - usage(); - return; - } - - if ("-m".equals(args[0]) || propertyExists(SYSTEM_PROPERTY_MASTER_PASSWORD, args)) show(true); - else if ("-p".equals(args[0]) || propertyExists(SYSTEM_PROPERTY_SERVER_PASSWORD, args)) show(false); - else usage(); - } - - // --------------------------------------------------------------- - - private static void show(boolean showMaster) throws Exception { - if (showMaster) System.out.print("\nsettings master password\n"); - else System.out.print("\nsettings server password\n"); - - System.out.print("enter password: "); - - BufferedReader r = new BufferedReader(new InputStreamReader(System.in)); - - String pass = r.readLine(); - - System.out.println("\n"); - - DefaultPlexusCipher dc = new DefaultPlexusCipher(); - DefaultSecDispatcher dd = new DefaultSecDispatcher(dc); - - if (showMaster) - System.out.println(dc.encryptAndDecorate(pass, DefaultSecDispatcher.SYSTEM_PROPERTY_SEC_LOCATION)); - else { - SettingsSecurity sec = dd.getSec(); - System.out.println(dc.encryptAndDecorate(pass, dd.getMaster(sec))); - } - } -} diff --git a/src/main/java/org/sonatype/plexus/components/sec/dispatcher/PasswordDecryptor.java b/src/main/java/org/sonatype/plexus/components/sec/dispatcher/PasswordDecryptor.java deleted file mode 100644 index 2d6a434..0000000 --- a/src/main/java/org/sonatype/plexus/components/sec/dispatcher/PasswordDecryptor.java +++ /dev/null @@ -1,37 +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.sonatype.plexus.components.sec.dispatcher; - -import java.util.Map; - -/** - * - * - * @author Oleg Gusakov - * @version $Id$ - * - */ -public interface PasswordDecryptor { - /** - * decrypt given encrypted string - * - * @param str - string to decrypt - * @param attributes - string attributes - * @param config - configuration from settings-security.xml, if any - * @return decrypted string - * - * @throws SecDispatcherException - */ - String decrypt(String str, Map attributes, Map config) throws SecDispatcherException; -} diff --git a/src/main/mdo/settings-security.mdo b/src/main/mdo/settings-security.mdo index 436af7e..3702210 100644 --- a/src/main/mdo/settings-security.mdo +++ b/src/main/mdo/settings-security.mdo @@ -13,7 +13,8 @@ */ --> - + settings-security SecurityConfiguration @@ -22,7 +23,7 @@ package - org.sonatype.plexus.components.sec.dispatcher.model + org.codehaus.plexus.components.secdispatcher.model @@ -30,26 +31,47 @@ SettingsSecurity - 1.0.0 + 1.0.0+ master - 1.0.0 + 1.0.0/2.1.0 String encrypted master password + + modelVersion + 3.0.0+ + String + The version of the model + + + + masterSource + 3.0.0+ + String + The URI describing the source of the master password + + + + masterCipher + 3.0.0+ + String + The Cipher to be used + + relocation - 1.0.0 + 1.0.0+ String reference to the location of the security file configurations - 1.0.0 + 1.0.0+ named configurations Config @@ -62,7 +84,7 @@ Config - 1.0.0 + 1.0.0+ Named configuration @@ -70,13 +92,13 @@ name String true - 1.0.0 + 1.0.0+ name of this configuration properties - 1.0.0 + 1.0.0+ properties ConfigProperty @@ -89,7 +111,7 @@ ConfigProperty - 1.0.0 + 1.0.0+ generic property - name/value pair @@ -98,7 +120,7 @@ name String true - 1.0.0 + 1.0.0+ name of this property @@ -106,7 +128,7 @@ value String true - 1.0.0 + 1.0.0+ value of this property 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 new file mode 100644 index 0000000..5ecb58d --- /dev/null +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java @@ -0,0 +1,220 @@ +/* + * 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 java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +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.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 void saveSec(String masterSource) 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); + } + System.setProperty(DefaultSecDispatcher.SYSTEM_PROPERTY_CONFIGURATION_LOCATION, "./target/sec.xml"); + } + + @BeforeEach + public void prepare() throws Exception { + saveSec("magic:might"); + } + + @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); + } + + @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); + } + + @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); + } + + @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); + } + + @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); + String pass = sd.decrypt(encrypted); + assertNotNull(pass); + assertEquals(password, 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); + } +} diff --git a/src/test/java/org/sonatype/plexus/components/sec/dispatcher/SecUtilTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java similarity index 58% rename from src/test/java/org/sonatype/plexus/components/sec/dispatcher/SecUtilTest.java rename to src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java index be9dc58..8afc4f1 100644 --- a/src/test/java/org/sonatype/plexus/components/sec/dispatcher/SecUtilTest.java +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/SecUtilTest.java @@ -11,18 +11,19 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ -package org.sonatype.plexus.components.sec.dispatcher; +package org.codehaus.plexus.components.secdispatcher.internal; -import java.io.FileWriter; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Map; +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.Test; -import org.sonatype.plexus.components.cipher.DefaultPlexusCipher; -import org.sonatype.plexus.components.sec.dispatcher.model.Config; -import org.sonatype.plexus.components.sec.dispatcher.model.ConfigProperty; -import org.sonatype.plexus.components.sec.dispatcher.model.SettingsSecurity; -import org.sonatype.plexus.components.sec.dispatcher.model.io.xpp3.SecurityConfigurationXpp3Writer; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -35,32 +36,15 @@ * */ public class SecUtilTest { - String _pw = "{1wQaa6S/o8MH7FnaTNL53XmhT5O0SEGXQi3gC49o6OY=}"; - - String _clear = "testtest"; - - String _encrypted = "{BteqUEnqHecHM7MZfnj9FwLcYbdInWxou1C929Txa0A=}"; - String _confName = "cname"; - String _propName = "pname"; - String _propVal = "pval"; - @BeforeEach - public void prepare() throws Exception { - System.setProperty(DefaultSecDispatcher.SYSTEM_PROPERTY_SEC_LOCATION, "./target/sec.xml"); - - // DefaultPlexusCipher c = new DefaultPlexusCipher(); - // System.out.println(_clear+" -> "+c.encrypt( _clear, "testtest" )); - + private void saveSec(String masterSource) throws Exception { SettingsSecurity sec = new SettingsSecurity(); - sec.setRelocation("./target/sec1.xml"); - new SecurityConfigurationXpp3Writer().write(new FileWriter("./target/sec.xml"), sec); - sec.setRelocation(null); - sec.setMaster(_pw); + sec.setMasterSource(masterSource); ConfigProperty cp = new ConfigProperty(); cp.setName(_propName); @@ -72,34 +56,30 @@ public void prepare() throws Exception { sec.addConfiguration(conf); - new SecurityConfigurationXpp3Writer().write(new FileWriter("./target/sec1.xml"), sec); + try (OutputStream fos = Files.newOutputStream(Paths.get("./target/sec1.xml"))) { + new SecurityConfigurationStaxWriter().write(fos, sec); + } + } + + @BeforeEach + public void prepare() throws Exception { + System.setProperty(DefaultSecDispatcher.SYSTEM_PROPERTY_CONFIGURATION_LOCATION, "./target/sec.xml"); + SettingsSecurity sec = new SettingsSecurity(); + sec.setRelocation("./target/sec1.xml"); + try (OutputStream fos = Files.newOutputStream(Paths.get("./target/sec.xml"))) { + new SecurityConfigurationStaxWriter().write(fos, sec); + } + saveSec("magic:mighty"); } @Test - void testRead() throws Exception { + void testReadWithRelocation() throws Exception { SettingsSecurity sec = SecUtil.read("./target/sec.xml", true); - assertNotNull(sec); - - assertEquals(_pw, sec.getMaster()); - + assertEquals("magic:mighty", sec.getMasterSource()); Map conf = SecUtil.getConfig(sec, _confName); - assertNotNull(conf); - assertNotNull(conf.get(_propName)); - assertEquals(_propVal, conf.get(_propName)); } - - @Test - void testDecrypt() throws Exception { - DefaultSecDispatcher sd = new DefaultSecDispatcher(new DefaultPlexusCipher()); - - String pass = sd.decrypt(_encrypted); - - assertNotNull(pass); - - assertEquals(_clear, pass); - } } 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 new file mode 100644 index 0000000..4088212 --- /dev/null +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatcher/StaticDispatcher.java @@ -0,0 +1,43 @@ +/* + * 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/resources/test-sec.xml b/src/test/resources/test-sec.xml deleted file mode 100644 index b452cb4..0000000 --- a/src/test/resources/test-sec.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - {CFxZA99+BAZVLyBgcmvfLZciAWn31QdSjVpSXodH13MAkHoTl8JPLGpt1rFTh07dnKoNiOUh92sash3p0PXbKi2NhY3sxvmVXnlCf+Vdz38uaZBQ7L0ebNt+YhpsyUE33iKqMwZt4oWr1acD3mpIufk2godfNP2nKGO2ufIFfIbqO4mGMWQ5VIQ=} - \ No newline at end of file