diff --git a/dev/com.ibm.ws.crypto.passwordutil/src/com/ibm/websphere/crypto/PasswordUtil.java b/dev/com.ibm.ws.crypto.passwordutil/src/com/ibm/websphere/crypto/PasswordUtil.java index 32e1adebe3a..afa037c1351 100644 --- a/dev/com.ibm.ws.crypto.passwordutil/src/com/ibm/websphere/crypto/PasswordUtil.java +++ b/dev/com.ibm.ws.crypto.passwordutil/src/com/ibm/websphere/crypto/PasswordUtil.java @@ -264,12 +264,17 @@ public static String encode(String decoded_string, String crypto_algorithm, String current_crypto_algorithm = getCryptoAlgorithm(decoded_string); - if ((current_crypto_algorithm != null && current_crypto_algorithm.startsWith(crypto_algorithm)) || isHashed(decoded_string)) { - // don't accept encoded password + if ((current_crypto_algorithm != null && isValidCryptoAlgorithm(current_crypto_algorithm) + && current_crypto_algorithm.startsWith(crypto_algorithm)) || isHashed(decoded_string)) { + // don't accept already-encoded password throw new InvalidPasswordEncodingException(); - } else if (current_crypto_algorithm != null) { + } else if (current_crypto_algorithm != null && isValidCryptoAlgorithm(current_crypto_algorithm)) { + // password is encoded with a different valid algorithm — decode it first, then re-encode decoded_string = passwordDecode(decoded_string); } + // If current_crypto_algorithm is non-null but NOT a valid crypto algorithm, the input string + // merely starts with a {something} pattern (e.g. a vault-generated password like "{abc}def"). + // In that case we treat the entire string as literal plaintext and fall through to encode it. if (properties == null || !properties.containsKey(PROPERTY_NO_TRIM) || !"true".equalsIgnoreCase(properties.get(PROPERTY_NO_TRIM))) { decoded_string = decoded_string.trim(); } diff --git a/dev/com.ibm.ws.crypto.passwordutil/test/com/ibm/websphere/crypto/PasswordUtilTest.java b/dev/com.ibm.ws.crypto.passwordutil/test/com/ibm/websphere/crypto/PasswordUtilTest.java index 7799e29eb75..8128d785bf1 100644 --- a/dev/com.ibm.ws.crypto.passwordutil/test/com/ibm/websphere/crypto/PasswordUtilTest.java +++ b/dev/com.ibm.ws.crypto.passwordutil/test/com/ibm/websphere/crypto/PasswordUtilTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2009, 2025 IBM Corporation and others. + * Copyright (c) 2009, 2026 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -147,6 +147,59 @@ public void testUtil() { } } + /** + * TS022437211 - Passwords that begin with a {something} pattern (e.g. vault/PIM-generated + * passphrases like "{abc}def" or "{redacted}***{/redacted}") must be treated as literal + * plaintext and encoded successfully rather than misinterpreted as pre-encoded ciphertext. + * + * Before the fix, encode() would either: + * (a) silently return a truncated/empty result (XOR case observed by Keith), or + * (b) throw NullPointerException from decoded_string.trim() after passwordDecode() returned null. + */ + @Test + public void testEncodePasswordStartingWithCurlyBracePattern() throws Exception { + // Simple {something}value pattern — must encode to a valid {xor}... string and round-trip correctly + String plain1 = "{abc}def"; + String encoded1 = PasswordUtil.encode(plain1, "xor"); + assertTrue("Encoded password should start with {xor}: " + encoded1, encoded1.startsWith("{xor}")); + assertEquals("Round-trip decode must return the original plaintext", plain1, PasswordUtil.decode(encoded1)); + + // Pattern that triggered NPE in the customer's environment: {redacted}...{/redacted} + String plain2 = "{redacted}************{/redacted}"; + String encoded2 = PasswordUtil.encode(plain2, "xor"); + assertTrue("Encoded password should start with {xor}: " + encoded2, encoded2.startsWith("{xor}")); + assertEquals("Round-trip decode must return the original plaintext", plain2, PasswordUtil.decode(encoded2)); + + // Same patterns encoded with AES + String encoded3 = PasswordUtil.encode(plain1, "aes"); + assertTrue("Encoded password should start with {aes}: " + encoded3, encoded3.startsWith("{aes}")); + assertEquals("Round-trip AES decode must return the original plaintext", plain1, PasswordUtil.decode(encoded3)); + + // Verify that a genuinely already-encoded password still correctly throws InvalidPasswordEncodingException + try { + PasswordUtil.encode("{xor}CDo9Hgw=", "xor"); + fail("Should have thrown InvalidPasswordEncodingException for an already-xor-encoded password"); + } catch (InvalidPasswordEncodingException e) { + // expected + } + } + + /** + * TS022437211 - Verify that encode() with an explicit crypto key also handles passwords + * starting with {something} correctly (three-argument overload used by the customer). + */ + @Test + public void testEncodeWithKeyPasswordStartingWithCurlyBracePattern() throws Exception { + String cryptoKey = "MyCustomEncryptionKey123!"; + + // Reproduces the exact customer scenario from the support case + String plain = "{mySecretPassword123}"; + String encoded = PasswordUtil.encode(plain, "xor", cryptoKey); + assertFalse("Encoded result must not be just {xor} with empty payload: " + encoded, "{xor}".equals(encoded)); + assertTrue("Encoded password should start with {xor}: " + encoded, encoded.startsWith("{xor}")); + assertEquals("Round-trip decode must return the original plaintext", plain, PasswordUtil.decode(encoded)); + } + @Test public void testAESEncoding() throws Exception {