This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 4a9116dac07 Default Oauth2 encryption providers to use AES-GCM (#3408)
4a9116dac07 is described below
commit 4a9116dac070105e584251d994f232efdb8420e7
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Aug 28 16:01:23 2026 +0100
Default Oauth2 encryption providers to use AES-GCM (#3408)
* Default Oauth2 encryption providers to use AES-GCM
* Update
rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/utils/crypto/ModelEncryptionSupport.java
Co-authored-by: Andriy Redko <[email protected]>
* Removing SecureRandom
* Security fix
---------
Co-authored-by: Andriy Redko <[email protected]>
---
.../utils/crypto/ModelEncryptionSupport.java | 92 ++++++++++++++++++----
.../oauth2/utils/crypto/CryptoUtilsTest.java | 51 ++++++++++++
2 files changed, 127 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..8ae5403684f 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,6 +19,7 @@
package org.apache.cxf.rs.security.oauth2.utils.crypto;
+import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Arrays;
import java.util.Collections;
@@ -28,6 +29,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 +48,11 @@ 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 ModelEncryptionSupport() {
}
@@ -56,7 +63,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 +73,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 +84,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 +96,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 +126,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 +164,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 +202,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 +240,59 @@ 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 = CryptoUtils.generateSecureRandomBytes(DEFAULT_IV_SIZE);
+ byte[] cipherText =
CryptoUtils.encryptBytes(sequence.getBytes(StandardCharsets.UTF_8),
+ secretKey,
gcmProperties(iv));
+ try {
+ int outLength = Math.addExact(iv.length, cipherText.length);
+ byte[] out = new byte[outLength];
+ System.arraycopy(iv, 0, out, 0, iv.length);
+ System.arraycopy(cipherText, 0, out, iv.length, cipherText.length);
+ return CryptoUtils.encodeBytes(out);
+ } catch (ArithmeticException e) {
+ throw new SecurityException("Encrypted data size overflow", e);
+ }
+ }
+
+ 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();