Repository: keycloak/keycloak (Java)

Issue:
Client secret generation provides lower than expected entropy
### Before reporting an issue

- [x] I have read and understood the above terms for submitting issues, and I understand that my issue may be closed without action if I do not follow them.

### Area

authentication

### Describe the bug

The way how we generate client secrets in authentication flows such as "Client Id and Secret" and "Signed Jwt with Client Secret" uses a character set consisting of 62 alphanumeric characters (A-Z, a-z, 0-9). Although the secret lengths are defined as 256, 384, or 512 bits, this method produces secrets with much less actual entropy due to the constrained character set.

For example, a 256-bit secret generated using 32 characters from a 62-character set results in only ~192 bits of entropy, not the expected 256 bits. Similar entropy shortfalls exist for 384- and 512-bit secrets, capping their entropy at ~286 and ~381 bits respectively. This mismatch reduces the strength of MAC-based authentication methods, such as those using HS256, HS384, or HS512, thereby weakening message integrity and authentication.

### Version

26.1.4

### Regression

- [ ] The issue is a regression

### Expected behavior

Client secrets should meet the entropy level associated with their configured bit-length (e.g., 256 bits of entropy for a 256-bit secret).

### Actual behavior

Secrets use only 62 characters for generation, resulting in reduced entropy (e.g., 192 bits instead of 256 for 256-bit secrets). This reduces the strength of HMAC-based client authentication.

### How to Reproduce?

1. Go to the Keycloak admin console.
    
2. Create a new client or select an existing one.
    
3. Navigate to the "Credentials" tab.
    
4. Observe the generated client secret.
    
5. Note that the secret is composed of alphanumeric characters only.
    
6. Calculate the theoretical entropy using the formula: `entropy = length * log2(alphabet size)`. Or use the following tools https://www.omnicalculator.com/other/password-entropy

### Anything else?

**Suggested Fixes:**

- **Expand the character set:** Include special characters (e.g., `!@#$%^&*()_+=-`) in the character pool to increase the entropy of generated secrets.
    
- **Verify use of CSPRNG:** Ensure that the `SecretGenerator.getInstance().randomString` method utilizes a cryptographically secure pseudo-random number generator (CSPRNG).


Discussion: Changing to an enhancement and adding to sprint 67 for now, but we can postpone if lots of other things...

We already use `SecureRandom` for generating random strings, but we can likely increase the character set and/or increase the length of the secret. We need to make sure that increasing length works fine for the DB as I am not 100% sure if clientSecret field has some limit in the DB level (we should not likely touch this and update DB schema unless absolutely necessary as increasing DB schema might be a pain for migration).



Fix (patch):
diff --git a/common/src/main/java/org/keycloak/common/util/SecretGenerator.java b/common/src/main/java/org/keycloak/common/util/SecretGenerator.java
index ff73e855eeec..42eb86fb66d8 100644
--- a/common/src/main/java/org/keycloak/common/util/SecretGenerator.java
+++ b/common/src/main/java/org/keycloak/common/util/SecretGenerator.java
@@ -56,6 +56,7 @@ public String randomString(int length, char[] symbols) {
 
         return new String(buf);
     }
+
     public byte[] randomBytes() {
         return randomBytes(SECRET_LENGTH_256_BITS);
     }
@@ -70,4 +71,28 @@ public byte[] randomBytes(int length) {
         return buf;
     }
 
+    /**
+     * Returns the equivalent length for a destination alphabet to have the same
+     * entropy bits than a byte array random generated.
+     *
+     * @param byteLengthEntropy The desired entropy in bytes
+     * @param dstAlphabetLeng The length of the destination alphabet
+     * @return The equivalent length in destination alphabet to have the same entropy bits
+     */
+    public static int equivalentEntropySize(int byteLengthEntropy, int dstAlphabetLeng) {
+        return equivalentEntropySize(byteLengthEntropy, 256, dstAlphabetLeng);
+    }
+
+    /**
+     * Returns the equivalent length for a destination alphabet to have the same
+     * entropy bits than another source alphabet.
+     *
+     * @param length The length of the string encoded in source alphabet
+     * @param srcAlphabetLength The length of the source alphabet
+     * @param dstAlphabetLeng The length of the destination alphabet
+     * @return The equivalent length (same entropy) in destination alphabet for a string of length in source alphabet
+     */
+    public static int equivalentEntropySize(int length, int srcAlphabetLength, int dstAlphabetLeng) {
+        return (int) Math.ceil(length * ((Math.log(srcAlphabetLength)) / (Math.log(dstAlphabetLeng))));
+    }
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index d4a1c1dbd71b..f1294feb2792 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -1167,8 +1167,12 @@ public static int getSecretLengthByAuthenticationType(String clientAuthenticator
         if (clientAuthenticatorType != null)
             switch (clientAuthenticatorType) {
                 case AUTH_TYPE_CLIENT_SECRET_JWT: {
-                    if (Algorithm.HS384.equals(signingAlg)) return SecretGenerator.SECRET_LENGTH_384_BITS;
-                    if (Algorithm.HS512.equals(signingAlg)) return SecretGenerator.SECRET_LENGTH_512_BITS;
+                    if (Algorithm.HS384.equals(signingAlg))
+                        return SecretGenerator.equivalentEntropySize(SecretGenerator.SECRET_LENGTH_384_BITS, SecretGenerator.ALPHANUM.length);
+                    else if (Algorithm.HS512.equals(signingAlg))
+                        return SecretGenerator.equivalentEntropySize(SecretGenerator.SECRET_LENGTH_512_BITS, SecretGenerator.ALPHANUM.length);
+                    else
+                        return SecretGenerator.equivalentEntropySize(SecretGenerator.SECRET_LENGTH_256_BITS, SecretGenerator.ALPHANUM.length);
                 }
             }
         return SecretGenerator.SECRET_LENGTH_256_BITS;