This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/aes-gcm in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 041b395a5877c22895bb47c3ab95b6b43d86c01d Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Fri Aug 28 10:42:02 2026 +0100 Default Oauth2 encryption providers to use AES-GCM --- .../utils/crypto/ModelEncryptionSupport.java | 90 ++++++++++++++++++---- .../oauth2/utils/crypto/CryptoUtilsTest.java | 51 ++++++++++++ 2 files changed, 125 insertions(+), 16 deletions(-) diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/utils/crypto/ModelEncryptionSupport.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/utils/crypto/ModelEncryptionSupport.java index e5c7d3e13e3..dfe180d19c4 100644 --- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/utils/crypto/ModelEncryptionSupport.java +++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/utils/crypto/ModelEncryptionSupport.java @@ -19,7 +19,9 @@ package org.apache.cxf.rs.security.oauth2.utils.crypto; +import java.nio.charset.StandardCharsets; import java.security.Key; +import java.security.SecureRandom; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -28,6 +30,7 @@ import java.util.List; import java.util.Map; import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; import javax.security.auth.DestroyFailedException; import org.apache.cxf.rs.security.oauth2.common.Client; @@ -46,6 +49,12 @@ import org.apache.cxf.rt.security.crypto.KeyProperties; */ public final class ModelEncryptionSupport { public static final String SEP = "|"; + + private static final String DEFAULT_TRANSFORMATION = "AES/GCM/NoPadding"; + private static final int DEFAULT_IV_SIZE = 12; + private static final int DEFAULT_AUTH_TAG_LENGTH = 128; + private static final SecureRandom RANDOM = new SecureRandom(); + private ModelEncryptionSupport() { } @@ -56,7 +65,7 @@ public final class ModelEncryptionSupport { public static String encryptClient(Client client, Key secretKey, KeyProperties props) throws SecurityException { String tokenSequence = tokenizeClient(client); - return CryptoUtils.encryptSequence(tokenSequence, secretKey, props); + return encryptSequence(tokenSequence, secretKey, props); } public static String encryptAccessToken(ServerAccessToken token, Key secretKey) throws SecurityException { @@ -66,7 +75,7 @@ public final class ModelEncryptionSupport { public static String encryptAccessToken(ServerAccessToken token, Key secretKey, KeyProperties props) throws SecurityException { String tokenSequence = tokenizeServerToken(token); - return CryptoUtils.encryptSequence(tokenSequence, secretKey, props); + return encryptSequence(tokenSequence, secretKey, props); } public static String encryptRefreshToken(RefreshToken token, Key secretKey) throws SecurityException { @@ -77,7 +86,7 @@ public final class ModelEncryptionSupport { KeyProperties props) throws SecurityException { String tokenSequence = tokenizeRefreshToken(token); - return CryptoUtils.encryptSequence(tokenSequence, secretKey, props); + return encryptSequence(tokenSequence, secretKey, props); } public static String encryptCodeGrant(ServerAuthorizationCodeGrant grant, Key secretKey) @@ -89,17 +98,18 @@ public final class ModelEncryptionSupport { KeyProperties props) throws SecurityException { String tokenSequence = tokenizeCodeGrant(grant); - return CryptoUtils.encryptSequence(tokenSequence, secretKey, props); + return encryptSequence(tokenSequence, secretKey, props); } public static Client decryptClient(String encodedSequence, String encodedSecretKey) throws SecurityException { - return decryptClient(encodedSequence, encodedSecretKey, new KeyProperties("AES")); + return decryptClient(encodedSequence, encodedSecretKey, null); } public static Client decryptClient(String encodedSequence, String encodedSecretKey, KeyProperties props) throws SecurityException { - SecretKey key = CryptoUtils.decodeSecretKey(encodedSecretKey, props.getKeyAlgo()); + SecretKey key = CryptoUtils.decodeSecretKey(encodedSecretKey, + props == null ? "AES" : props.getKeyAlgo()); Client client = decryptClient(encodedSequence, key, props); // Clean the secret key from memory when we're done @@ -118,21 +128,22 @@ public final class ModelEncryptionSupport { public static Client decryptClient(String encodedData, Key secretKey, KeyProperties props) throws SecurityException { - String decryptedSequence = CryptoUtils.decryptSequence(encodedData, secretKey, props); + String decryptedSequence = decryptSequence(encodedData, secretKey, props); return recreateClient(decryptedSequence); } public static ServerAccessToken decryptAccessToken(OAuthDataProvider provider, String encodedToken, String encodedSecretKey) throws SecurityException { - return decryptAccessToken(provider, encodedToken, encodedSecretKey, new KeyProperties("AES")); + return decryptAccessToken(provider, encodedToken, encodedSecretKey, null); } public static ServerAccessToken decryptAccessToken(OAuthDataProvider provider, String encodedToken, String encodedSecretKey, KeyProperties props) throws SecurityException { - SecretKey key = CryptoUtils.decodeSecretKey(encodedSecretKey, props.getKeyAlgo()); + SecretKey key = CryptoUtils.decodeSecretKey(encodedSecretKey, + props == null ? "AES" : props.getKeyAlgo()); ServerAccessToken serverAccessToken = decryptAccessToken(provider, encodedToken, key, props); // Clean the secret key from memory when we're done @@ -155,21 +166,22 @@ public final class ModelEncryptionSupport { String encodedData, Key secretKey, KeyProperties props) throws SecurityException { - String decryptedSequence = CryptoUtils.decryptSequence(encodedData, secretKey, props); + String decryptedSequence = decryptSequence(encodedData, secretKey, props); return recreateAccessToken(provider, encodedData, decryptedSequence); } public static RefreshToken decryptRefreshToken(OAuthDataProvider provider, String encodedToken, String encodedSecretKey) throws SecurityException { - return decryptRefreshToken(provider, encodedToken, encodedSecretKey, new KeyProperties("AES")); + return decryptRefreshToken(provider, encodedToken, encodedSecretKey, null); } public static RefreshToken decryptRefreshToken(OAuthDataProvider provider, String encodedToken, String encodedSecretKey, KeyProperties props) throws SecurityException { - SecretKey key = CryptoUtils.decodeSecretKey(encodedSecretKey, props.getKeyAlgo()); + SecretKey key = CryptoUtils.decodeSecretKey(encodedSecretKey, + props == null ? "AES" : props.getKeyAlgo()); RefreshToken refreshToken = decryptRefreshToken(provider, encodedToken, key, props); // Clean the secret key from memory when we're done @@ -192,21 +204,22 @@ public final class ModelEncryptionSupport { String encodedData, Key key, KeyProperties props) throws SecurityException { - String decryptedSequence = CryptoUtils.decryptSequence(encodedData, key, props); + String decryptedSequence = decryptSequence(encodedData, key, props); return recreateRefreshToken(provider, encodedData, decryptedSequence); } public static ServerAuthorizationCodeGrant decryptCodeGrant(OAuthDataProvider provider, String encodedToken, String encodedSecretKey) throws SecurityException { - return decryptCodeGrant(provider, encodedToken, encodedSecretKey, new KeyProperties("AES")); + return decryptCodeGrant(provider, encodedToken, encodedSecretKey, null); } public static ServerAuthorizationCodeGrant decryptCodeGrant(OAuthDataProvider provider, String encodedToken, String encodedSecretKey, KeyProperties props) throws SecurityException { - SecretKey key = CryptoUtils.decodeSecretKey(encodedSecretKey, props.getKeyAlgo()); + SecretKey key = CryptoUtils.decodeSecretKey(encodedSecretKey, + props == null ? "AES" : props.getKeyAlgo()); ServerAuthorizationCodeGrant authzCodeGrant = decryptCodeGrant(provider, encodedToken, key, props); // Clean the secret key from memory when we're done @@ -229,10 +242,55 @@ public final class ModelEncryptionSupport { String encodedData, Key key, KeyProperties props) throws SecurityException { - String decryptedSequence = CryptoUtils.decryptSequence(encodedData, key, props); + String decryptedSequence = decryptSequence(encodedData, key, props); return recreateCodeGrant(provider, decryptedSequence); } + /** + * Encrypts the sequence. When no explicit KeyProperties are supplied the + * authenticated AES/GCM transformation is used, with a random IV prepended + * to the ciphertext. The previous default (the JCE "AES" shorthand, i.e. + * AES/ECB/PKCS5Padding) produced deterministic, malleable ciphertexts which + * a client holding several tokens could cut and splice block-by-block to + * forge token state; pass explicit KeyProperties only if a legacy + * transformation must be retained. + */ + private static String encryptSequence(String sequence, Key secretKey, + KeyProperties props) throws SecurityException { + if (props != null) { + return CryptoUtils.encryptSequence(sequence, secretKey, props); + } + byte[] iv = new byte[DEFAULT_IV_SIZE]; + RANDOM.nextBytes(iv); + byte[] cipherText = CryptoUtils.encryptBytes(sequence.getBytes(StandardCharsets.UTF_8), + secretKey, gcmProperties(iv)); + byte[] out = new byte[iv.length + cipherText.length]; + System.arraycopy(iv, 0, out, 0, iv.length); + System.arraycopy(cipherText, 0, out, iv.length, cipherText.length); + return CryptoUtils.encodeBytes(out); + } + + private static String decryptSequence(String encodedData, Key secretKey, + KeyProperties props) throws SecurityException { + if (props != null) { + return CryptoUtils.decryptSequence(encodedData, secretKey, props); + } + byte[] bytes = CryptoUtils.decodeSequence(encodedData); + if (bytes.length <= DEFAULT_IV_SIZE) { + throw new SecurityException(); + } + KeyProperties gcmProps = gcmProperties(Arrays.copyOf(bytes, DEFAULT_IV_SIZE)); + byte[] decrypted = CryptoUtils.decryptBytes( + Arrays.copyOfRange(bytes, DEFAULT_IV_SIZE, bytes.length), secretKey, gcmProps); + return new String(decrypted, StandardCharsets.UTF_8); + } + + private static KeyProperties gcmProperties(byte[] iv) { + KeyProperties props = new KeyProperties(DEFAULT_TRANSFORMATION); + props.setAlgoSpec(new GCMParameterSpec(DEFAULT_AUTH_TAG_LENGTH, iv)); + return props; + } + public static ServerAccessToken recreateAccessToken(OAuthDataProvider provider, String newTokenKey, String decryptedSequence) throws SecurityException { diff --git a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/utils/crypto/CryptoUtilsTest.java b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/utils/crypto/CryptoUtilsTest.java index dd529d45ad3..d457079b9d6 100644 --- a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/utils/crypto/CryptoUtilsTest.java +++ b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/utils/crypto/CryptoUtilsTest.java @@ -50,6 +50,7 @@ import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -111,6 +112,56 @@ public class CryptoUtilsTest { compareAccessTokens(token, token2); } + @Test + public void testModelEncryptionDefaultIsNonDeterministic() throws Exception { + AccessTokenRegistration atr = prepareTokenRegistration(); + BearerAccessToken token = p.createAccessTokenInternal(atr); + SecretKey secretKey = CryptoUtils.getSecretKey("AES"); + + String encrypted1 = ModelEncryptionSupport.encryptAccessToken(token, secretKey); + String encrypted2 = ModelEncryptionSupport.encryptAccessToken(token, secretKey); + // AES/GCM with a random IV: encrypting the same token twice must not + // produce the same ciphertext (the old AES/ECB default was deterministic) + assertNotEquals(encrypted1, encrypted2); + + token.setTokenKey(encrypted1); + ServerAccessToken token2 = ModelEncryptionSupport.decryptAccessToken(p, encrypted1, secretKey); + compareAccessTokens(token, token2); + } + + @Test(expected = SecurityException.class) + public void testModelEncryptionDefaultRejectsTamperedToken() throws Exception { + AccessTokenRegistration atr = prepareTokenRegistration(); + BearerAccessToken token = p.createAccessTokenInternal(atr); + SecretKey secretKey = CryptoUtils.getSecretKey("AES"); + + String encrypted = ModelEncryptionSupport.encryptAccessToken(token, secretKey); + // flip one character of the ciphertext: authenticated encryption must reject it + char[] chars = encrypted.toCharArray(); + int pos = chars.length - 2; + chars[pos] = chars[pos] == 'A' ? 'B' : 'A'; + ModelEncryptionSupport.decryptAccessToken(p, new String(chars), secretKey); + } + + @Test + public void testModelEncryptionExplicitLegacyPropertiesRoundTrip() throws Exception { + AccessTokenRegistration atr = prepareTokenRegistration(); + BearerAccessToken token = p.createAccessTokenInternal(atr); + SecretKey secretKey = CryptoUtils.getSecretKey("AES"); + KeyProperties legacyProps = new KeyProperties("AES"); + + String encrypted = ModelEncryptionSupport.encryptAccessToken(token, secretKey, legacyProps); + token.setTokenKey(encrypted); + ServerAccessToken decrypted = ModelEncryptionSupport.decryptAccessToken( + p, encrypted, secretKey, legacyProps); + compareAccessTokens(token, decrypted); + + String encodedSecretKey = CryptoUtils.encodeSecretKey(secretKey); + ServerAccessToken decryptedWithEncodedKey = ModelEncryptionSupport.decryptAccessToken( + p, encrypted, encodedSecretKey, legacyProps); + compareAccessTokens(token, decryptedWithEncodedKey); + } + @Test public void testBearerTokenJSON() throws Exception { AccessTokenRegistration atr = prepareTokenRegistration();
