From f0183e17be86ebe095374b105312a4ca704d93a5 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 14 Oct 2024 19:34:06 +0200 Subject: [PATCH] Ingest plexus-cipher (#78) Ingest plexus-cipher, add improvements to "detection" of encrypted strings. --- pom.xml | 5 - .../components/secdispatcher/Cipher.java | 44 ++++++ .../secdispatcher/CipherException.java | 28 ++++ .../secdispatcher/SecDispatcher.java | 18 ++- .../internal/DefaultSecDispatcher.java | 132 ++++++++++-------- .../internal/cipher/AESGCMNoPadding.java | 107 ++++++++++++++ .../dispatchers/LegacyDispatcher.java | 36 +++-- .../dispatchers/MasterDispatcher.java | 47 +++---- .../internal/DefaultSecDispatcherTest.java | 52 +++++-- .../internal/cipher/AESGCMNoPaddingTest.java | 10 ++ .../internal/cipher/CipherTestSupport.java | 66 +++++++++ .../dispatchers/LegacyDispatcherTest.java | 3 +- 12 files changed, 429 insertions(+), 119 deletions(-) create mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/Cipher.java create mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/CipherException.java create mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPadding.java create mode 100644 src/test/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPaddingTest.java create mode 100644 src/test/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/CipherTestSupport.java diff --git a/pom.xml b/pom.xml index 319719f..19f158d 100644 --- a/pom.xml +++ b/pom.xml @@ -45,11 +45,6 @@ slf4j-api ${version.slf4j} - - org.codehaus.plexus - plexus-cipher - 3.0.0 - javax.inject diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/Cipher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/Cipher.java new file mode 100644 index 0000000..4f2bb47 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/Cipher.java @@ -0,0 +1,44 @@ +/* +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; + +/** + * Cipher interface. + * + * @since 4.0.1 + */ +public interface Cipher { + /** + * Encrypts the clear text data with password and returns result. No argument is allowed to be {@code null}. + * + * @throws CipherException if encryption failed (is unexpected to happen, as it would mean that Java Runtime + * lacks some Crypto elements). + */ + String encrypt(final String clearText, final String password) throws CipherException; + + /** + * Decrypts the encrypted text with password and returns clear text result. No argument is allowed to be {@code null}. + * + * @throws CipherException if decryption failed. It may happen as with {@link #encrypt(String, String)} due Java + * Runtime lacking some Crypto elements (less likely). Most likely decrypt will fail due wrong provided password + * or maybe corrupted encrypted text. + */ + String decrypt(final String encryptedText, final String password) throws CipherException; +} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/CipherException.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/CipherException.java new file mode 100644 index 0000000..781ba37 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/CipherException.java @@ -0,0 +1,28 @@ +/* + * 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; + +/** + * Exception thrown by {@link Cipher}. + * + * @since 4.0.1 + */ +public class CipherException extends SecDispatcherException { + public CipherException(String message) { + super(message); + } + + public CipherException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java index fde9fa9..3361e96 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/SecDispatcher.java @@ -64,9 +64,23 @@ public interface SecDispatcher { String decrypt(String str) throws SecDispatcherException, IOException; /** - * Returns {@code true} if passed in string contains "legacy" password (Maven3 kind). + * Returns {@code true} if passed in string adheres to "encrypted string" format (current or legacy). + * + * @since 4.0.1 + */ + default boolean isAnyEncryptedString(String str) { + return isEncryptedString(str) || isLegacyEncryptedString(str); + } + + /** + * Returns {@code true} if passed in string adheres "encrypted string" format. + */ + boolean isEncryptedString(String str); + + /** + * Returns {@code true} if passed in string adheres to "legacy encrypted string" format. */ - boolean isLegacyPassword(String str); + boolean isLegacyEncryptedString(String str); /** * Reads the effective configuration, eventually creating new instance if not present. diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java index 37f4dbd..8cf5668 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcher.java @@ -25,8 +25,6 @@ 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.Dispatcher; import org.codehaus.plexus.components.secdispatcher.DispatcherMeta; import org.codehaus.plexus.components.secdispatcher.SecDispatcher; @@ -48,15 +46,15 @@ * @author Oleg Gusakov */ public class DefaultSecDispatcher implements SecDispatcher { + public static final String SHIELD_BEGIN = "{"; + public static final String SHIELD_END = "}"; public static final String ATTR_START = "["; public static final String ATTR_STOP = "]"; - protected final PlexusCipher cipher; protected final Map dispatchers; protected final Path configurationFile; - public DefaultSecDispatcher(PlexusCipher cipher, Map dispatchers, Path configurationFile) { - this.cipher = requireNonNull(cipher); + public DefaultSecDispatcher(Map dispatchers, Path configurationFile) { this.dispatchers = requireNonNull(dispatchers); this.configurationFile = requireNonNull(configurationFile); @@ -100,66 +98,90 @@ public Collection fields() { @Override public String encrypt(String str, Map attr) throws SecDispatcherException, IOException { if (isEncryptedString(str)) return str; - - try { - if (attr == null) { - attr = new HashMap<>(); - } else { - attr = new HashMap<>(attr); + if (attr == null) { + attr = new HashMap<>(); + } else { + attr = new HashMap<>(attr); + } + if (attr.get(DISPATCHER_NAME_ATTR) == null) { + SettingsSecurity conf = readConfiguration(false); + if (conf == null) { + throw new SecDispatcherException("No configuration found"); } - if (attr.get(DISPATCHER_NAME_ATTR) == null) { - SettingsSecurity conf = readConfiguration(false); - if (conf == null) { - throw new SecDispatcherException("No configuration found"); - } - String defaultDispatcher = conf.getDefaultDispatcher(); - if (defaultDispatcher == null) { - throw new SecDispatcherException("No defaultDispatcher set in configuration"); - } - attr.put(DISPATCHER_NAME_ATTR, defaultDispatcher); + String defaultDispatcher = conf.getDefaultDispatcher(); + if (defaultDispatcher == null) { + throw new SecDispatcherException("No defaultDispatcher set in configuration"); } - String name = attr.get(DISPATCHER_NAME_ATTR); - Dispatcher dispatcher = dispatchers.get(name); - if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name); - Dispatcher.EncryptPayload payload = dispatcher.encrypt(str, attr, prepareDispatcherConfig(name)); - HashMap resultAttributes = new HashMap<>(payload.getAttributes()); - resultAttributes.put(SecDispatcher.DISPATCHER_NAME_ATTR, name); - resultAttributes.put(SecDispatcher.DISPATCHER_VERSION_ATTR, SecUtil.specVersion()); - String res = ATTR_START - + resultAttributes.entrySet().stream() - .map(e -> e.getKey() + "=" + e.getValue()) - .collect(Collectors.joining(",")) - + ATTR_STOP; - res += payload.getEncrypted(); - return cipher.decorate(res); - } catch (PlexusCipherException e) { - throw new SecDispatcherException(e.getMessage(), e); + attr.put(DISPATCHER_NAME_ATTR, defaultDispatcher); } + String name = attr.get(DISPATCHER_NAME_ATTR); + Dispatcher dispatcher = dispatchers.get(name); + if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name); + Dispatcher.EncryptPayload payload = dispatcher.encrypt(str, attr, prepareDispatcherConfig(name)); + HashMap resultAttributes = new HashMap<>(payload.getAttributes()); + resultAttributes.put(SecDispatcher.DISPATCHER_NAME_ATTR, name); + resultAttributes.put(SecDispatcher.DISPATCHER_VERSION_ATTR, SecUtil.specVersion()); + return SHIELD_BEGIN + + ATTR_START + + resultAttributes.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(",")) + + ATTR_STOP + + payload.getEncrypted() + + SHIELD_END; } @Override public String decrypt(String str) throws SecDispatcherException, IOException { if (!isEncryptedString(str)) return str; - try { - String bare = cipher.unDecorate(str); - Map attr = requireNonNull(stripAttributes(bare)); - if (isLegacyPassword(str)) { - attr.put(DISPATCHER_NAME_ATTR, LegacyDispatcher.NAME); - } - String name = attr.get(DISPATCHER_NAME_ATTR); - Dispatcher dispatcher = dispatchers.get(name); - if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name); - return dispatcher.decrypt(strip(bare), attr, prepareDispatcherConfig(name)); - } catch (PlexusCipherException e) { - throw new SecDispatcherException(e.getMessage(), e); + String bare = unDecorate(str); + Map attr = requireNonNull(stripAttributes(bare)); + if (isLegacyEncryptedString(str)) { + attr.put(DISPATCHER_NAME_ATTR, LegacyDispatcher.NAME); } + String name = attr.get(DISPATCHER_NAME_ATTR); + Dispatcher dispatcher = dispatchers.get(name); + if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name); + return dispatcher.decrypt(strip(bare), attr, prepareDispatcherConfig(name)); } + /** + *
    + *
  • Current: {[name=master,cipher=AES/GCM/NoPadding,version=4.0]vvq66pZ7rkvzSPStGTI9q4QDnsmuDwo+LtjraRel2b0XpcGJFdXcYAHAS75HUA6GLpcVtEkmyQ==}
  • + *
+ */ @Override - public boolean isLegacyPassword(String str) { - if (!isEncryptedString(str)) return false; - Map attr = requireNonNull(stripAttributes(cipher.unDecorate(str))); - return !attr.containsKey(DISPATCHER_NAME_ATTR); + public boolean isEncryptedString(String str) { + boolean looksLike = str != null + && !str.isBlank() + && str.startsWith(SHIELD_BEGIN) + && str.endsWith(SHIELD_END) + && !unDecorate(str).contains(SHIELD_BEGIN) + && !unDecorate(str).contains(SHIELD_END); + if (looksLike) { + Map attributes = stripAttributes(unDecorate(str)); + return attributes.containsKey(DISPATCHER_NAME_ATTR) && attributes.containsKey(DISPATCHER_VERSION_ATTR); + } + return false; + } + + /** + *
    + *
  • Legacy: {jSMOWnoPFgsHVpMvz5VrIt5kRbzGpI8u+9EF1iFQyJQ=}
  • + *
+ */ + @Override + public boolean isLegacyEncryptedString(String str) { + boolean looksLike = str != null + && !str.isBlank() + && str.startsWith(SHIELD_BEGIN) + && str.endsWith(SHIELD_END) + && !unDecorate(str).contains(SHIELD_BEGIN) + && !unDecorate(str).contains(SHIELD_END); + if (looksLike) { + return stripAttributes(unDecorate(str)).isEmpty(); + } + return false; } @Override @@ -284,7 +306,7 @@ protected Map stripAttributes(String str) { return result; } - protected boolean isEncryptedString(String str) { - return cipher.isEncryptedString(str); + protected String unDecorate(String str) { + return str.substring(SHIELD_BEGIN.length(), str.length() - SHIELD_END.length()); } } diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPadding.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPadding.java new file mode 100644 index 0000000..5acf1f6 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPadding.java @@ -0,0 +1,107 @@ +/* +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.cipher; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import javax.inject.Named; +import javax.inject.Singleton; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Base64; + +import org.codehaus.plexus.components.secdispatcher.CipherException; + +@Singleton +@Named(AESGCMNoPadding.CIPHER_ALG) +public class AESGCMNoPadding implements org.codehaus.plexus.components.secdispatcher.Cipher { + public static final String CIPHER_ALG = "AES/GCM/NoPadding"; + + private static final int TAG_LENGTH_BIT = 128; + private static final int IV_LENGTH_BYTE = 12; + private static final int SALT_LENGTH_BYTE = 16; + private static final int PBE_ITERATIONS = 310000; + private static final int PBE_KEY_SIZE = SALT_LENGTH_BYTE * 16; + private static final String KEY_FACTORY = "PBKDF2WithHmacSHA512"; + private static final String KEY_ALGORITHM = "AES"; + + @Override + public String encrypt(String clearText, String password) throws CipherException { + try { + byte[] salt = getRandomNonce(SALT_LENGTH_BYTE); + byte[] iv = getRandomNonce(IV_LENGTH_BYTE); + SecretKey secretKey = getAESKeyFromPassword(password.toCharArray(), salt); + Cipher cipher = Cipher.getInstance(CIPHER_ALG); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); + byte[] cipherText = cipher.doFinal(clearText.getBytes(StandardCharsets.UTF_8)); + byte[] cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length) + .put(iv) + .put(salt) + .put(cipherText) + .array(); + return Base64.getEncoder().encodeToString(cipherTextWithIvSalt); + } catch (Exception e) { + throw new CipherException("Failed encrypting", e); + } + } + + @Override + public String decrypt(String encryptedText, String password) throws CipherException { + try { + byte[] material = Base64.getDecoder().decode(encryptedText.getBytes(StandardCharsets.UTF_8)); + ByteBuffer buffer = ByteBuffer.wrap(material); + byte[] iv = new byte[IV_LENGTH_BYTE]; + buffer.get(iv); + byte[] salt = new byte[SALT_LENGTH_BYTE]; + buffer.get(salt); + byte[] cipherText = new byte[buffer.remaining()]; + buffer.get(cipherText); + SecretKey secretKey = getAESKeyFromPassword(password.toCharArray(), salt); + Cipher cipher = Cipher.getInstance(CIPHER_ALG); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); + byte[] plainText = cipher.doFinal(cipherText); + return new String(plainText, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new CipherException("Failed decrypting", e); + } + } + + private static byte[] getRandomNonce(int numBytes) throws NoSuchAlgorithmException { + byte[] nonce = new byte[numBytes]; + SecureRandom.getInstanceStrong().nextBytes(nonce); + return nonce; + } + + private static SecretKey getAESKeyFromPassword(char[] password, byte[] salt) + throws NoSuchAlgorithmException, InvalidKeySpecException { + SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_FACTORY); + KeySpec spec = new PBEKeySpec(password, salt, PBE_ITERATIONS, PBE_KEY_SIZE); + return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), KEY_ALGORITHM); + } +} diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcher.java index 74daabe..942ff22 100644 --- a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcher.java +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcher.java @@ -38,8 +38,7 @@ import java.util.List; import java.util.Map; -import org.codehaus.plexus.components.cipher.PlexusCipher; -import org.codehaus.plexus.components.cipher.PlexusCipherException; +import org.codehaus.plexus.components.secdispatcher.CipherException; import org.codehaus.plexus.components.secdispatcher.Dispatcher; import org.codehaus.plexus.components.secdispatcher.DispatcherMeta; import org.codehaus.plexus.components.secdispatcher.SecDispatcher; @@ -56,12 +55,10 @@ public class LegacyDispatcher implements Dispatcher, DispatcherMeta { private static final String MASTER_MASTER_PASSWORD = "settings.security"; - private final PlexusCipher plexusCipher; private final LegacyCipher legacyCipher; @Inject - public LegacyDispatcher(PlexusCipher plexusCipher) { - this.plexusCipher = plexusCipher; + public LegacyDispatcher() { this.legacyCipher = new LegacyCipher(); } @@ -95,15 +92,11 @@ public EncryptPayload encrypt(String str, Map attributes, Map attributes, Map config) throws SecDispatcherException { - try { - String masterPassword = getMasterPassword(); - if (masterPassword == null) { - throw new SecDispatcherException("Master password could not be obtained"); - } - return legacyCipher.decrypt64(str, masterPassword); - } catch (PlexusCipherException e) { - throw new SecDispatcherException("Decrypt failed", e); + String masterPassword = getMasterPassword(); + if (masterPassword == null) { + throw new SecDispatcherException("Master password could not be obtained"); } + return legacyCipher.decrypt64(str, masterPassword); } @Override @@ -129,7 +122,7 @@ public SecDispatcher.ValidationResponse validateConfiguration(Map new ArrayList<>()) .add("Legacy master password decryption failed"); } @@ -137,11 +130,16 @@ public SecDispatcher.ValidationResponse validateConfiguration(Map masterCiphers; protected final Map masterSources; @Inject - public MasterDispatcher(PlexusCipher cipher, Map masterSources) { - this.cipher = cipher; + public MasterDispatcher(Map masterCiphers, Map masterSources) { + this.masterCiphers = masterCiphers; this.masterSources = masterSources; } @@ -94,7 +93,7 @@ public Collection fields() { Field.builder(CONF_MASTER_CIPHER) .optional(false) .description("Cipher to use with master password") - .options(cipher.availableCiphers().stream() + .options(masterCiphers.keySet().stream() .map(c -> Field.builder(c).description(c).build()) .toList()) .build()); @@ -103,26 +102,18 @@ public Collection fields() { @Override public EncryptPayload encrypt(String str, Map attributes, Map config) throws SecDispatcherException { - try { - String masterCipher = getMasterCipher(config, true); - String encrypted = cipher.encrypt(masterCipher, str, getMasterPassword(config)); - HashMap attr = new HashMap<>(attributes); - attr.put(MASTER_CIPHER_ATTR, masterCipher); - return new EncryptPayload(attr, encrypted); - } catch (PlexusCipherException e) { - throw new SecDispatcherException("Encrypt failed", e); - } + String masterCipher = getMasterCipher(config, true); + String encrypted = requireCipher(masterCipher).encrypt(str, getMasterPassword(config)); + HashMap attr = new HashMap<>(attributes); + attr.put(MASTER_CIPHER_ATTR, masterCipher); + return new EncryptPayload(attr, encrypted); } @Override public String decrypt(String str, Map attributes, Map config) throws SecDispatcherException { - try { - String masterCipher = getMasterCipher(attributes, false); - return cipher.decrypt(masterCipher, str, getMasterPassword(config)); - } catch (PlexusCipherException e) { - throw new SecDispatcherException("Decrypt failed", e); - } + String masterCipher = getMasterCipher(attributes, false); + return requireCipher(masterCipher).decrypt(str, getMasterPassword(config)); } @Override @@ -135,7 +126,7 @@ public SecDispatcher.ValidationResponse validateConfiguration(Map new ArrayList<>()) .add("Cipher configuration missing"); } else { - if (!cipher.availableCiphers().contains(masterCipher)) { + if (!masterCiphers.containsKey(masterCipher)) { report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>()) .add("Configured Cipher not supported"); } else { @@ -173,7 +164,7 @@ public SecDispatcher.ValidationResponse validateConfiguration(Map config) throws SecDispatcherException { + protected String getMasterPassword(Map config) throws SecDispatcherException { String masterSource = config.get(CONF_MASTER_SOURCE); if (masterSource == null) { throw new SecDispatcherException("Invalid configuration: Missing configuration " + CONF_MASTER_SOURCE); @@ -185,7 +176,7 @@ private String getMasterPassword(Map config) throws SecDispatche throw new SecDispatcherException("No source handled the given masterSource: " + masterSource); } - private String getMasterCipher(Map source, boolean config) throws SecDispatcherException { + protected String getMasterCipher(Map source, boolean config) throws SecDispatcherException { if (config) { String masterCipher = source.get(CONF_MASTER_CIPHER); if (masterCipher == null) { @@ -200,4 +191,12 @@ private String getMasterCipher(Map source, boolean config) throw return masterCipher; } } + + protected Cipher requireCipher(String name) { + Cipher masterCipher = masterCiphers.get(name); + if (masterCipher == null) { + throw new SecDispatcherException("No cipher exist with name " + name); + } + return masterCipher; + } } diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java index d1e947b..c520b4e 100644 --- a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java @@ -20,9 +20,8 @@ import java.nio.file.Paths; import java.util.Map; -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.internal.cipher.AESGCMNoPadding; import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher; import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.MasterDispatcher; import org.codehaus.plexus.components.secdispatcher.internal.sources.EnvMasterSource; @@ -35,6 +34,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class DefaultSecDispatcherTest { @@ -93,20 +93,50 @@ void validate() throws Exception { assertEquals(1, response.getSubsystems().get(0).getReport().size()); assertEquals(1, response.getSubsystems().get(0).getSubsystems().size()); // master source - assertTrue(response.getSubsystems() + assertEquals( + 1, + response.getSubsystems() .get(0) .getSubsystems() .get(0) .getReport() - .size() - == 1); - assertTrue(response.getSubsystems() + .size()); + assertEquals( + 0, + response.getSubsystems() .get(0) .getSubsystems() .get(0) .getSubsystems() - .size() - == 0); + .size()); + } + + @Test + void detection() { + SecDispatcher secDispatcher = construct(); + assertFalse(secDispatcher.isAnyEncryptedString(null)); + assertFalse(secDispatcher.isAnyEncryptedString("")); + assertFalse(secDispatcher.isAnyEncryptedString("foo")); + + assertFalse(secDispatcher.isEncryptedString("{foo}")); + assertTrue(secDispatcher.isLegacyEncryptedString("{foo}")); + + assertFalse(secDispatcher.isEncryptedString("{12345678901234567890123456789012345678901234567890}")); + assertTrue(secDispatcher.isLegacyEncryptedString("{12345678901234567890123456789012345678901234567890}")); + + // contains {} in the middle + assertFalse(secDispatcher.isEncryptedString("{KDvsYOFLlX{}gH4LU8tvpzAGg5otiosZXvfdQq0yO86LU=}")); + assertFalse(secDispatcher.isLegacyEncryptedString("{KDvsYOFLlX{}gH4LU8tvpzAGg5otiosZXvfdQq0yO86LU=}")); + + assertFalse(secDispatcher.isEncryptedString("{KDvsYOFLlXgH4LU8tvpzAGg5otiosZXvfdQq0yO86LU=}")); + assertTrue(secDispatcher.isLegacyEncryptedString("{KDvsYOFLlXgH4LU8tvpzAGg5otiosZXvfdQq0yO86LU=}")); + + assertTrue( + secDispatcher.isEncryptedString( + "{[name=master,cipher=AES/GCM/NoPadding,version=4.0,a=b]vvq66pZ7rkvzSPStGTI9q4QDnsmuDwo+LtjraRel2b0XpcGJFdXcYAHAS75HUA6GLpcVtEkmyQ==}")); + assertFalse( + secDispatcher.isLegacyEncryptedString( + "{[name=master,cipher=AES/GCM/NoPadding,version=4.0,a=b]vvq66pZ7rkvzSPStGTI9q4QDnsmuDwo+LtjraRel2b0XpcGJFdXcYAHAS75HUA6GLpcVtEkmyQ==}")); } protected void roundtrip() throws Exception { @@ -126,13 +156,11 @@ protected void roundtrip() throws Exception { } protected DefaultSecDispatcher construct() { - DefaultPlexusCipher dpc = new DefaultPlexusCipher(Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding())); return new DefaultSecDispatcher( - dpc, Map.of( "master", new MasterDispatcher( - dpc, + Map.of(AESGCMNoPadding.CIPHER_ALG, new AESGCMNoPadding()), Map.of( EnvMasterSource.NAME, new EnvMasterSource(), @@ -141,7 +169,7 @@ protected DefaultSecDispatcher construct() { GpgAgentMasterSource.NAME, new GpgAgentMasterSource())), "legacy", - new LegacyDispatcher(dpc)), + new LegacyDispatcher()), CONFIG_PATH); } } diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPaddingTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPaddingTest.java new file mode 100644 index 0000000..e711b87 --- /dev/null +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPaddingTest.java @@ -0,0 +1,10 @@ +package org.codehaus.plexus.components.secdispatcher.internal.cipher; + +import org.codehaus.plexus.components.secdispatcher.Cipher; + +public class AESGCMNoPaddingTest extends CipherTestSupport { + @Override + Cipher getCipher() { + return new AESGCMNoPadding(); + } +} diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/CipherTestSupport.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/CipherTestSupport.java new file mode 100644 index 0000000..9e8693f --- /dev/null +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/CipherTestSupport.java @@ -0,0 +1,66 @@ +/* +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.cipher; + +import org.codehaus.plexus.components.secdispatcher.Cipher; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public abstract class CipherTestSupport { + final String clearText = "veryOpenText"; + final String password = "testtest"; + + Cipher pbeCipher; + + @BeforeEach + void prepare() { + pbeCipher = getCipher(); + } + + abstract Cipher getCipher(); + + @Test + void testEncrypt() throws Exception { + String enc = pbeCipher.encrypt(clearText, password); + assertNotNull(enc); + String enc2 = pbeCipher.encrypt(clearText, password); + assertNotNull(enc2); + assertNotEquals(enc, enc2); + } + + @Test + void testDecrypt() throws Exception { + String enc = pbeCipher.encrypt(clearText, password); + String clear = pbeCipher.decrypt(enc, password); + assertEquals(clearText, clear); + } + + @Test + void testEncoding() throws Exception { + String pwd = "äüöÜÖÄæøåčćžšđß\"§$%&/()=?é"; + String encPwd = pbeCipher.encrypt(pwd, pwd); + String decPwd = pbeCipher.decrypt(encPwd, pwd); + assertEquals(pwd, decPwd); + } +} diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcherTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcherTest.java index f02e758..62336ea 100644 --- a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcherTest.java +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/LegacyDispatcherTest.java @@ -15,7 +15,6 @@ import java.util.Map; -import org.codehaus.plexus.components.cipher.internal.DefaultPlexusCipher; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -36,7 +35,7 @@ public class LegacyDispatcherTest { }) void smoke(String xml) { System.setProperty("settings.security", xml); - LegacyDispatcher legacyDispatcher = new LegacyDispatcher(new DefaultPlexusCipher(Map.of())); + LegacyDispatcher legacyDispatcher = new LegacyDispatcher(); // SecDispatcher "un decorates" the PW String cleartext = legacyDispatcher.decrypt("L6L/HbmrY+cH+sNkphnq3fguYepTpM04WlIXb8nB1pk=", Map.of(), Map.of()); assertEquals("password", cleartext);