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();

Reply via email to