Skip to content

Commit f0183e1

Browse files
authored
Ingest plexus-cipher (#78)
Ingest plexus-cipher, add improvements to "detection" of encrypted strings.
1 parent 85a46a6 commit f0183e1

File tree

12 files changed

+429
-119
lines changed

12 files changed

+429
-119
lines changed

pom.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@
4545
<artifactId>slf4j-api</artifactId>
4646
<version>${version.slf4j}</version>
4747
</dependency>
48-
<dependency>
49-
<groupId>org.codehaus.plexus</groupId>
50-
<artifactId>plexus-cipher</artifactId>
51-
<version>3.0.0</version>
52-
</dependency>
5348

5449
<dependency>
5550
<groupId>javax.inject</groupId>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
*/
19+
20+
package org.codehaus.plexus.components.secdispatcher;
21+
22+
/**
23+
* Cipher interface.
24+
*
25+
* @since 4.0.1
26+
*/
27+
public interface Cipher {
28+
/**
29+
* Encrypts the clear text data with password and returns result. No argument is allowed to be {@code null}.
30+
*
31+
* @throws CipherException if encryption failed (is unexpected to happen, as it would mean that Java Runtime
32+
* lacks some Crypto elements).
33+
*/
34+
String encrypt(final String clearText, final String password) throws CipherException;
35+
36+
/**
37+
* Decrypts the encrypted text with password and returns clear text result. No argument is allowed to be {@code null}.
38+
*
39+
* @throws CipherException if decryption failed. It may happen as with {@link #encrypt(String, String)} due Java
40+
* Runtime lacking some Crypto elements (less likely). Most likely decrypt will fail due wrong provided password
41+
* or maybe corrupted encrypted text.
42+
*/
43+
String decrypt(final String encryptedText, final String password) throws CipherException;
44+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
package org.codehaus.plexus.components.secdispatcher;
14+
15+
/**
16+
* Exception thrown by {@link Cipher}.
17+
*
18+
* @since 4.0.1
19+
*/
20+
public class CipherException extends SecDispatcherException {
21+
public CipherException(String message) {
22+
super(message);
23+
}
24+
25+
public CipherException(String message, Throwable cause) {
26+
super(message, cause);
27+
}
28+
}

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,23 @@ public interface SecDispatcher {
6464
String decrypt(String str) throws SecDispatcherException, IOException;
6565

6666
/**
67-
* Returns {@code true} if passed in string contains "legacy" password (Maven3 kind).
67+
* Returns {@code true} if passed in string adheres to "encrypted string" format (current or legacy).
68+
*
69+
* @since 4.0.1
70+
*/
71+
default boolean isAnyEncryptedString(String str) {
72+
return isEncryptedString(str) || isLegacyEncryptedString(str);
73+
}
74+
75+
/**
76+
* Returns {@code true} if passed in string adheres "encrypted string" format.
77+
*/
78+
boolean isEncryptedString(String str);
79+
80+
/**
81+
* Returns {@code true} if passed in string adheres to "legacy encrypted string" format.
6882
*/
69-
boolean isLegacyPassword(String str);
83+
boolean isLegacyEncryptedString(String str);
7084

7185
/**
7286
* Reads the effective configuration, eventually creating new instance if not present.

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

Lines changed: 77 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525
import java.util.StringTokenizer;
2626
import java.util.stream.Collectors;
2727

28-
import org.codehaus.plexus.components.cipher.PlexusCipher;
29-
import org.codehaus.plexus.components.cipher.PlexusCipherException;
3028
import org.codehaus.plexus.components.secdispatcher.Dispatcher;
3129
import org.codehaus.plexus.components.secdispatcher.DispatcherMeta;
3230
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
@@ -48,15 +46,15 @@
4846
* @author Oleg Gusakov
4947
*/
5048
public class DefaultSecDispatcher implements SecDispatcher {
49+
public static final String SHIELD_BEGIN = "{";
50+
public static final String SHIELD_END = "}";
5151
public static final String ATTR_START = "[";
5252
public static final String ATTR_STOP = "]";
5353

54-
protected final PlexusCipher cipher;
5554
protected final Map<String, Dispatcher> dispatchers;
5655
protected final Path configurationFile;
5756

58-
public DefaultSecDispatcher(PlexusCipher cipher, Map<String, Dispatcher> dispatchers, Path configurationFile) {
59-
this.cipher = requireNonNull(cipher);
57+
public DefaultSecDispatcher(Map<String, Dispatcher> dispatchers, Path configurationFile) {
6058
this.dispatchers = requireNonNull(dispatchers);
6159
this.configurationFile = requireNonNull(configurationFile);
6260

@@ -100,66 +98,90 @@ public Collection<Field> fields() {
10098
@Override
10199
public String encrypt(String str, Map<String, String> attr) throws SecDispatcherException, IOException {
102100
if (isEncryptedString(str)) return str;
103-
104-
try {
105-
if (attr == null) {
106-
attr = new HashMap<>();
107-
} else {
108-
attr = new HashMap<>(attr);
101+
if (attr == null) {
102+
attr = new HashMap<>();
103+
} else {
104+
attr = new HashMap<>(attr);
105+
}
106+
if (attr.get(DISPATCHER_NAME_ATTR) == null) {
107+
SettingsSecurity conf = readConfiguration(false);
108+
if (conf == null) {
109+
throw new SecDispatcherException("No configuration found");
109110
}
110-
if (attr.get(DISPATCHER_NAME_ATTR) == null) {
111-
SettingsSecurity conf = readConfiguration(false);
112-
if (conf == null) {
113-
throw new SecDispatcherException("No configuration found");
114-
}
115-
String defaultDispatcher = conf.getDefaultDispatcher();
116-
if (defaultDispatcher == null) {
117-
throw new SecDispatcherException("No defaultDispatcher set in configuration");
118-
}
119-
attr.put(DISPATCHER_NAME_ATTR, defaultDispatcher);
111+
String defaultDispatcher = conf.getDefaultDispatcher();
112+
if (defaultDispatcher == null) {
113+
throw new SecDispatcherException("No defaultDispatcher set in configuration");
120114
}
121-
String name = attr.get(DISPATCHER_NAME_ATTR);
122-
Dispatcher dispatcher = dispatchers.get(name);
123-
if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name);
124-
Dispatcher.EncryptPayload payload = dispatcher.encrypt(str, attr, prepareDispatcherConfig(name));
125-
HashMap<String, String> resultAttributes = new HashMap<>(payload.getAttributes());
126-
resultAttributes.put(SecDispatcher.DISPATCHER_NAME_ATTR, name);
127-
resultAttributes.put(SecDispatcher.DISPATCHER_VERSION_ATTR, SecUtil.specVersion());
128-
String res = ATTR_START
129-
+ resultAttributes.entrySet().stream()
130-
.map(e -> e.getKey() + "=" + e.getValue())
131-
.collect(Collectors.joining(","))
132-
+ ATTR_STOP;
133-
res += payload.getEncrypted();
134-
return cipher.decorate(res);
135-
} catch (PlexusCipherException e) {
136-
throw new SecDispatcherException(e.getMessage(), e);
115+
attr.put(DISPATCHER_NAME_ATTR, defaultDispatcher);
137116
}
117+
String name = attr.get(DISPATCHER_NAME_ATTR);
118+
Dispatcher dispatcher = dispatchers.get(name);
119+
if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name);
120+
Dispatcher.EncryptPayload payload = dispatcher.encrypt(str, attr, prepareDispatcherConfig(name));
121+
HashMap<String, String> resultAttributes = new HashMap<>(payload.getAttributes());
122+
resultAttributes.put(SecDispatcher.DISPATCHER_NAME_ATTR, name);
123+
resultAttributes.put(SecDispatcher.DISPATCHER_VERSION_ATTR, SecUtil.specVersion());
124+
return SHIELD_BEGIN
125+
+ ATTR_START
126+
+ resultAttributes.entrySet().stream()
127+
.map(e -> e.getKey() + "=" + e.getValue())
128+
.collect(Collectors.joining(","))
129+
+ ATTR_STOP
130+
+ payload.getEncrypted()
131+
+ SHIELD_END;
138132
}
139133

140134
@Override
141135
public String decrypt(String str) throws SecDispatcherException, IOException {
142136
if (!isEncryptedString(str)) return str;
143-
try {
144-
String bare = cipher.unDecorate(str);
145-
Map<String, String> attr = requireNonNull(stripAttributes(bare));
146-
if (isLegacyPassword(str)) {
147-
attr.put(DISPATCHER_NAME_ATTR, LegacyDispatcher.NAME);
148-
}
149-
String name = attr.get(DISPATCHER_NAME_ATTR);
150-
Dispatcher dispatcher = dispatchers.get(name);
151-
if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name);
152-
return dispatcher.decrypt(strip(bare), attr, prepareDispatcherConfig(name));
153-
} catch (PlexusCipherException e) {
154-
throw new SecDispatcherException(e.getMessage(), e);
137+
String bare = unDecorate(str);
138+
Map<String, String> attr = requireNonNull(stripAttributes(bare));
139+
if (isLegacyEncryptedString(str)) {
140+
attr.put(DISPATCHER_NAME_ATTR, LegacyDispatcher.NAME);
155141
}
142+
String name = attr.get(DISPATCHER_NAME_ATTR);
143+
Dispatcher dispatcher = dispatchers.get(name);
144+
if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name);
145+
return dispatcher.decrypt(strip(bare), attr, prepareDispatcherConfig(name));
156146
}
157147

148+
/**
149+
* <ul>
150+
* <li>Current: {[name=master,cipher=AES/GCM/NoPadding,version=4.0]vvq66pZ7rkvzSPStGTI9q4QDnsmuDwo+LtjraRel2b0XpcGJFdXcYAHAS75HUA6GLpcVtEkmyQ==}</li>
151+
* </ul>
152+
*/
158153
@Override
159-
public boolean isLegacyPassword(String str) {
160-
if (!isEncryptedString(str)) return false;
161-
Map<String, String> attr = requireNonNull(stripAttributes(cipher.unDecorate(str)));
162-
return !attr.containsKey(DISPATCHER_NAME_ATTR);
154+
public boolean isEncryptedString(String str) {
155+
boolean looksLike = str != null
156+
&& !str.isBlank()
157+
&& str.startsWith(SHIELD_BEGIN)
158+
&& str.endsWith(SHIELD_END)
159+
&& !unDecorate(str).contains(SHIELD_BEGIN)
160+
&& !unDecorate(str).contains(SHIELD_END);
161+
if (looksLike) {
162+
Map<String, String> attributes = stripAttributes(unDecorate(str));
163+
return attributes.containsKey(DISPATCHER_NAME_ATTR) && attributes.containsKey(DISPATCHER_VERSION_ATTR);
164+
}
165+
return false;
166+
}
167+
168+
/**
169+
* <ul>
170+
* <li>Legacy: {jSMOWnoPFgsHVpMvz5VrIt5kRbzGpI8u+9EF1iFQyJQ=}</li>
171+
* </ul>
172+
*/
173+
@Override
174+
public boolean isLegacyEncryptedString(String str) {
175+
boolean looksLike = str != null
176+
&& !str.isBlank()
177+
&& str.startsWith(SHIELD_BEGIN)
178+
&& str.endsWith(SHIELD_END)
179+
&& !unDecorate(str).contains(SHIELD_BEGIN)
180+
&& !unDecorate(str).contains(SHIELD_END);
181+
if (looksLike) {
182+
return stripAttributes(unDecorate(str)).isEmpty();
183+
}
184+
return false;
163185
}
164186

165187
@Override
@@ -284,7 +306,7 @@ protected Map<String, String> stripAttributes(String str) {
284306
return result;
285307
}
286308

287-
protected boolean isEncryptedString(String str) {
288-
return cipher.isEncryptedString(str);
309+
protected String unDecorate(String str) {
310+
return str.substring(SHIELD_BEGIN.length(), str.length() - SHIELD_END.length());
289311
}
290312
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
*/
19+
20+
package org.codehaus.plexus.components.secdispatcher.internal.cipher;
21+
22+
import javax.crypto.Cipher;
23+
import javax.crypto.SecretKey;
24+
import javax.crypto.SecretKeyFactory;
25+
import javax.crypto.spec.GCMParameterSpec;
26+
import javax.crypto.spec.PBEKeySpec;
27+
import javax.crypto.spec.SecretKeySpec;
28+
import javax.inject.Named;
29+
import javax.inject.Singleton;
30+
31+
import java.nio.ByteBuffer;
32+
import java.nio.charset.StandardCharsets;
33+
import java.security.NoSuchAlgorithmException;
34+
import java.security.SecureRandom;
35+
import java.security.spec.InvalidKeySpecException;
36+
import java.security.spec.KeySpec;
37+
import java.util.Base64;
38+
39+
import org.codehaus.plexus.components.secdispatcher.CipherException;
40+
41+
@Singleton
42+
@Named(AESGCMNoPadding.CIPHER_ALG)
43+
public class AESGCMNoPadding implements org.codehaus.plexus.components.secdispatcher.Cipher {
44+
public static final String CIPHER_ALG = "AES/GCM/NoPadding";
45+
46+
private static final int TAG_LENGTH_BIT = 128;
47+
private static final int IV_LENGTH_BYTE = 12;
48+
private static final int SALT_LENGTH_BYTE = 16;
49+
private static final int PBE_ITERATIONS = 310000;
50+
private static final int PBE_KEY_SIZE = SALT_LENGTH_BYTE * 16;
51+
private static final String KEY_FACTORY = "PBKDF2WithHmacSHA512";
52+
private static final String KEY_ALGORITHM = "AES";
53+
54+
@Override
55+
public String encrypt(String clearText, String password) throws CipherException {
56+
try {
57+
byte[] salt = getRandomNonce(SALT_LENGTH_BYTE);
58+
byte[] iv = getRandomNonce(IV_LENGTH_BYTE);
59+
SecretKey secretKey = getAESKeyFromPassword(password.toCharArray(), salt);
60+
Cipher cipher = Cipher.getInstance(CIPHER_ALG);
61+
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv));
62+
byte[] cipherText = cipher.doFinal(clearText.getBytes(StandardCharsets.UTF_8));
63+
byte[] cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length)
64+
.put(iv)
65+
.put(salt)
66+
.put(cipherText)
67+
.array();
68+
return Base64.getEncoder().encodeToString(cipherTextWithIvSalt);
69+
} catch (Exception e) {
70+
throw new CipherException("Failed encrypting", e);
71+
}
72+
}
73+
74+
@Override
75+
public String decrypt(String encryptedText, String password) throws CipherException {
76+
try {
77+
byte[] material = Base64.getDecoder().decode(encryptedText.getBytes(StandardCharsets.UTF_8));
78+
ByteBuffer buffer = ByteBuffer.wrap(material);
79+
byte[] iv = new byte[IV_LENGTH_BYTE];
80+
buffer.get(iv);
81+
byte[] salt = new byte[SALT_LENGTH_BYTE];
82+
buffer.get(salt);
83+
byte[] cipherText = new byte[buffer.remaining()];
84+
buffer.get(cipherText);
85+
SecretKey secretKey = getAESKeyFromPassword(password.toCharArray(), salt);
86+
Cipher cipher = Cipher.getInstance(CIPHER_ALG);
87+
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv));
88+
byte[] plainText = cipher.doFinal(cipherText);
89+
return new String(plainText, StandardCharsets.UTF_8);
90+
} catch (Exception e) {
91+
throw new CipherException("Failed decrypting", e);
92+
}
93+
}
94+
95+
private static byte[] getRandomNonce(int numBytes) throws NoSuchAlgorithmException {
96+
byte[] nonce = new byte[numBytes];
97+
SecureRandom.getInstanceStrong().nextBytes(nonce);
98+
return nonce;
99+
}
100+
101+
private static SecretKey getAESKeyFromPassword(char[] password, byte[] salt)
102+
throws NoSuchAlgorithmException, InvalidKeySpecException {
103+
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_FACTORY);
104+
KeySpec spec = new PBEKeySpec(password, salt, PBE_ITERATIONS, PBE_KEY_SIZE);
105+
return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), KEY_ALGORITHM);
106+
}
107+
}

0 commit comments

Comments
 (0)