Skip to content

Commit ce44581

Browse files
committed
Support legacy
1 parent b5bd3a4 commit ce44581

File tree

7 files changed

+263
-1
lines changed

7 files changed

+263
-1
lines changed

src/main/java/org/codehaus/plexus/components/secdispatcher/DispatcherMeta.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ public Field build() {
104104
}
105105
}
106106

107+
/**
108+
* Option to hide this instance from users, like for migration or legacy purposes.
109+
*/
110+
default boolean isHidden() {
111+
return false;
112+
}
113+
107114
/**
108115
* The name of the dispatcher.
109116
*/

src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ public interface SecDispatcher {
5656
*/
5757
String decrypt(String str) throws SecDispatcherException, IOException;
5858

59+
/**
60+
* Returns {@code true} if passed in string contains "legacy" password (Maven3 kind).
61+
*/
62+
boolean isLegacyPassword(String str);
63+
5964
/**
6065
* Reads the effective configuration, eventually creating new instance if not present.
6166
*

src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.codehaus.plexus.components.secdispatcher.DispatcherMeta;
3232
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
3333
import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
34+
import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher;
3435
import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity;
3536

3637
import static java.util.Objects.requireNonNull;
@@ -139,7 +140,8 @@ public String decrypt(String str) throws SecDispatcherException, IOException {
139140
String bare = cipher.unDecorate(str);
140141
Map<String, String> attr = requireNonNull(stripAttributes(bare));
141142
if (attr.get(DISPATCHER_NAME_ATTR) == null) {
142-
throw new SecDispatcherException("malformed password: no attribute with name");
143+
// TODO: log?
144+
attr.put(DISPATCHER_NAME_ATTR, LegacyDispatcher.NAME);
143145
}
144146
String name = attr.get(DISPATCHER_NAME_ATTR);
145147
Dispatcher dispatcher = dispatchers.get(name);
@@ -150,6 +152,13 @@ public String decrypt(String str) throws SecDispatcherException, IOException {
150152
}
151153
}
152154

155+
@Override
156+
public boolean isLegacyPassword(String str) {
157+
if (!isEncryptedString(str)) return false;
158+
Map<String, String> attr = requireNonNull(stripAttributes(cipher.unDecorate(str)));
159+
return !attr.containsKey(DISPATCHER_NAME_ATTR);
160+
}
161+
153162
@Override
154163
public SettingsSecurity readConfiguration(boolean createIfMissing) throws IOException {
155164
SettingsSecurity configuration = SecUtil.read(configurationFile);
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright (c) 2008 Sonatype, Inc. All rights reserved.
3+
*
4+
* This program is licensed to you under the Apache License Version 2.0,
5+
* and you may not use this file except in compliance with the Apache License Version 2.0.
6+
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7+
*
8+
* Unless required by applicable law or agreed to in writing,
9+
* software distributed under the Apache License Version 2.0 is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12+
*/
13+
14+
package org.codehaus.plexus.components.secdispatcher.internal.dispatchers;
15+
16+
import javax.crypto.Cipher;
17+
import javax.crypto.NoSuchPaddingException;
18+
import javax.crypto.spec.IvParameterSpec;
19+
import javax.crypto.spec.SecretKeySpec;
20+
import javax.inject.Inject;
21+
import javax.inject.Named;
22+
import javax.inject.Singleton;
23+
import javax.xml.xpath.XPathConstants;
24+
import javax.xml.xpath.XPathFactory;
25+
26+
import java.io.InputStream;
27+
import java.nio.file.Files;
28+
import java.nio.file.Path;
29+
import java.nio.file.Paths;
30+
import java.security.InvalidAlgorithmParameterException;
31+
import java.security.InvalidKeyException;
32+
import java.security.MessageDigest;
33+
import java.security.NoSuchAlgorithmException;
34+
import java.util.Base64;
35+
import java.util.Collection;
36+
import java.util.List;
37+
import java.util.Map;
38+
39+
import org.codehaus.plexus.components.cipher.PlexusCipher;
40+
import org.codehaus.plexus.components.cipher.PlexusCipherException;
41+
import org.codehaus.plexus.components.secdispatcher.Dispatcher;
42+
import org.codehaus.plexus.components.secdispatcher.DispatcherMeta;
43+
import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
44+
import org.xml.sax.InputSource;
45+
46+
/**
47+
* This dispatcher is legacy, serves the purpose of migration only. Should not be used.
48+
*/
49+
@Singleton
50+
@Named(LegacyDispatcher.NAME)
51+
public class LegacyDispatcher implements Dispatcher, DispatcherMeta {
52+
public static final String NAME = "legacy";
53+
54+
private static final String MASTER_MASTER_PASSWORD = "settings.security";
55+
56+
private final PlexusCipher plexusCipher;
57+
private final LegacyCipher legacyCipher;
58+
59+
@Inject
60+
public LegacyDispatcher(PlexusCipher plexusCipher) {
61+
this.plexusCipher = plexusCipher;
62+
this.legacyCipher = new LegacyCipher();
63+
}
64+
65+
@Override
66+
public boolean isHidden() {
67+
return true;
68+
}
69+
70+
@Override
71+
public String name() {
72+
return NAME;
73+
}
74+
75+
@Override
76+
public String displayName() {
77+
return "LEGACY (for migration purposes; is hidden)";
78+
}
79+
80+
@Override
81+
public Collection<Field> fields() {
82+
return List.of();
83+
}
84+
85+
@Override
86+
public EncryptPayload encrypt(String str, Map<String, String> attributes, Map<String, String> config)
87+
throws SecDispatcherException {
88+
throw new SecDispatcherException(
89+
NAME + " dispatcher MUST not be used for encryption; is inherently insecure and broken");
90+
}
91+
92+
@Override
93+
public String decrypt(String str, Map<String, String> attributes, Map<String, String> config)
94+
throws SecDispatcherException {
95+
try {
96+
return legacyCipher.decrypt64(str, getMasterPassword());
97+
} catch (PlexusCipherException e) {
98+
throw new SecDispatcherException("Decrypt failed", e);
99+
}
100+
}
101+
102+
private String getMasterPassword() throws SecDispatcherException {
103+
String encryptedMasterPassword = getMasterMasterPasswordFromSettingsSecurityXml();
104+
return legacyCipher.decrypt64(plexusCipher.unDecorate(encryptedMasterPassword), MASTER_MASTER_PASSWORD);
105+
}
106+
107+
private String getMasterMasterPasswordFromSettingsSecurityXml() {
108+
Path xml;
109+
String override = System.getProperty(MASTER_MASTER_PASSWORD);
110+
if (override != null) {
111+
xml = Paths.get(override);
112+
} else {
113+
xml = Paths.get(System.getProperty("user.home"), ".m2", "settings-security.xml");
114+
}
115+
if (Files.exists(xml)) {
116+
try (InputStream is = Files.newInputStream(xml)) {
117+
return (String) XPathFactory.newInstance()
118+
.newXPath()
119+
.evaluate("//master", new InputSource(is), XPathConstants.STRING);
120+
} catch (Exception e) {
121+
// just ignore whatever it is
122+
}
123+
}
124+
throw new SecDispatcherException("Could not locate legacy master password: " + xml);
125+
}
126+
127+
private static final class LegacyCipher {
128+
private static final String STRING_ENCODING = "UTF8";
129+
private static final int SPICE_SIZE = 16;
130+
private static final int SALT_SIZE = 8;
131+
private static final String DIGEST_ALG = "SHA-256";
132+
private static final String KEY_ALG = "AES";
133+
private static final String CIPHER_ALG = "AES/CBC/PKCS5Padding";
134+
135+
private String decrypt64(final String encryptedText, final String password) throws PlexusCipherException {
136+
try {
137+
byte[] allEncryptedBytes = Base64.getDecoder().decode(encryptedText.getBytes());
138+
int totalLen = allEncryptedBytes.length;
139+
byte[] salt = new byte[SALT_SIZE];
140+
System.arraycopy(allEncryptedBytes, 0, salt, 0, SALT_SIZE);
141+
byte padLen = allEncryptedBytes[SALT_SIZE];
142+
byte[] encryptedBytes = new byte[totalLen - SALT_SIZE - 1 - padLen];
143+
System.arraycopy(allEncryptedBytes, SALT_SIZE + 1, encryptedBytes, 0, encryptedBytes.length);
144+
Cipher cipher = createCipher(password.getBytes(STRING_ENCODING), salt, Cipher.DECRYPT_MODE);
145+
byte[] clearBytes = cipher.doFinal(encryptedBytes);
146+
return new String(clearBytes, STRING_ENCODING);
147+
} catch (Exception e) {
148+
throw new PlexusCipherException("Error decrypting", e);
149+
}
150+
}
151+
152+
private Cipher createCipher(final byte[] pwdAsBytes, byte[] salt, final int mode)
153+
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
154+
InvalidAlgorithmParameterException {
155+
MessageDigest _digester = MessageDigest.getInstance(DIGEST_ALG);
156+
byte[] keyAndIv = new byte[SPICE_SIZE * 2];
157+
if (salt == null || salt.length == 0) {
158+
salt = null;
159+
}
160+
byte[] result;
161+
int currentPos = 0;
162+
while (currentPos < keyAndIv.length) {
163+
_digester.update(pwdAsBytes);
164+
if (salt != null) {
165+
_digester.update(salt, 0, 8);
166+
}
167+
result = _digester.digest();
168+
int stillNeed = keyAndIv.length - currentPos;
169+
if (result.length > stillNeed) {
170+
byte[] b = new byte[stillNeed];
171+
System.arraycopy(result, 0, b, 0, b.length);
172+
result = b;
173+
}
174+
System.arraycopy(result, 0, keyAndIv, currentPos, result.length);
175+
currentPos += result.length;
176+
if (currentPos < keyAndIv.length) {
177+
_digester.reset();
178+
_digester.update(result);
179+
}
180+
}
181+
byte[] key = new byte[SPICE_SIZE];
182+
byte[] iv = new byte[SPICE_SIZE];
183+
System.arraycopy(keyAndIv, 0, key, 0, key.length);
184+
System.arraycopy(keyAndIv, key.length, iv, 0, iv.length);
185+
Cipher cipher = Cipher.getInstance(CIPHER_ALG);
186+
cipher.init(mode, new SecretKeySpec(key, KEY_ALG), new IvParameterSpec(iv));
187+
return cipher;
188+
}
189+
}
190+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2008 Sonatype, Inc. All rights reserved.
3+
*
4+
* This program is licensed to you under the Apache License Version 2.0,
5+
* and you may not use this file except in compliance with the Apache License Version 2.0.
6+
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7+
*
8+
* Unless required by applicable law or agreed to in writing,
9+
* software distributed under the Apache License Version 2.0 is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12+
*/
13+
14+
package org.codehaus.plexus.components.secdispatcher.internal.dispatchers;
15+
16+
import java.util.Map;
17+
18+
import org.codehaus.plexus.components.cipher.internal.DefaultPlexusCipher;
19+
import org.junit.jupiter.params.ParameterizedTest;
20+
import org.junit.jupiter.params.provider.ValueSource;
21+
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
24+
public class LegacyDispatcherTest {
25+
/**
26+
* Test values created with Maven 3.9.9.
27+
* <p>
28+
* master password: "masterpassword"
29+
* password: "password"
30+
*/
31+
@ParameterizedTest
32+
@ValueSource(
33+
strings = {
34+
"src/test/legacy/legacy-settings-security-1.xml",
35+
"src/test/legacy/legacy-settings-security-2.xml"
36+
})
37+
void smoke(String xml) {
38+
System.setProperty("settings.security", xml);
39+
LegacyDispatcher legacyDispatcher = new LegacyDispatcher(new DefaultPlexusCipher(Map.of()));
40+
// SecDispatcher "un decorates" the PW
41+
String cleartext = legacyDispatcher.decrypt("L6L/HbmrY+cH+sNkphnq3fguYepTpM04WlIXb8nB1pk=", Map.of(), Map.of());
42+
assertEquals("password", cleartext);
43+
}
44+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<settingsSecurity>
2+
<master>{KDvsYOFLlXgH4LU8tvpzAGg5otiosZXvfdQq0yO86LU=}</master>
3+
</settingsSecurity>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<settingsSecurity>
2+
<relocation>to the moon</relocation>
3+
<master>{KDvsYOFLlXgH4LU8tvpzAGg5otiosZXvfdQq0yO86LU=}</master>
4+
</settingsSecurity>

0 commit comments

Comments
 (0)