GUACAMOLE-839: Generate case-insensitive nonce values that can safely be used in domains.

This commit is contained in:
Michael Jumper
2023-01-26 10:24:08 -08:00
parent f2c7d746ea
commit 841190df5a
2 changed files with 47 additions and 9 deletions

View File

@@ -21,6 +21,7 @@ package org.apache.guacamole.auth.sso;
import com.google.common.io.BaseEncoding; import com.google.common.io.BaseEncoding;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.math.BigInteger;
import java.security.SecureRandom; import java.security.SecureRandom;
/** /**
@@ -40,7 +41,8 @@ public class IdentifierGenerator {
/** /**
* Generates a unique and unpredictable identifier. Each identifier is at * Generates a unique and unpredictable identifier. Each identifier is at
* least 256-bit and produced using a cryptographically-secure random * least 256-bit and produced using a cryptographically-secure random
* number generator. * number generator. The identifier may contain characters that differ only
* in case.
* *
* @return * @return
* A unique and unpredictable identifier with at least 256 bits of * A unique and unpredictable identifier with at least 256 bits of
@@ -53,7 +55,8 @@ public class IdentifierGenerator {
/** /**
* Generates a unique and unpredictable identifier having at least the * Generates a unique and unpredictable identifier having at least the
* given number of bits of entropy. The resulting identifier may have more * given number of bits of entropy. The resulting identifier may have more
* than the number of bits required. * than the number of bits required. The identifier may contain characters
* that differ only in case.
* *
* @param minBits * @param minBits
* The number of bits of entropy that the identifier should contain. * The number of bits of entropy that the identifier should contain.
@@ -63,9 +66,41 @@ public class IdentifierGenerator {
* of bits of entropy. * of bits of entropy.
*/ */
public String generateIdentifier(int minBits) { public String generateIdentifier(int minBits) {
byte[] bytes = new byte[(minBits + 23) / 24 * 3]; // Round up to nearest multiple of 3 bytes, as base64 encodes blocks of 3 bytes at a time return generateIdentifier(minBits, true);
}
/**
* Generates a unique and unpredictable identifier having at least the
* given number of bits of entropy. The resulting identifier may have more
* than the number of bits required. The identifier may contain characters
* that differ only in case.
*
* @param minBits
* The number of bits of entropy that the identifier should contain.
*
* @param caseSensitive
* Whether identifiers are permitted to contain characters that vary
* by case. If false, all characters that may vary by case will be
* lowercase, and the generated identifier will be longer.
*
* @return
* A unique and unpredictable identifier with at least the given number
* of bits of entropy.
*/
public String generateIdentifier(int minBits, boolean caseSensitive) {
// Generate a base64 identifier if we're allowed to vary by case
if (caseSensitive) {
int minBytes = (minBits + 23) / 24 * 3; // Round up to nearest multiple of 3 bytes, as base64 encodes blocks of 3 bytes at a time
byte[] bytes = new byte[minBytes];
secureRandom.nextBytes(bytes); secureRandom.nextBytes(bytes);
return BaseEncoding.base64().encode(bytes); return BaseEncoding.base64().encode(bytes);
} }
// Generate base32 identifiers if we cannot vary by case
minBits = (minBits + 4) / 5 * 5; // Round up to nearest multiple of 5 bits, as base32 encodes 5 bits at a time
return new BigInteger(minBits, secureRandom).toString(32);
}
} }

View File

@@ -22,11 +22,13 @@ package org.apache.guacamole.auth.sso;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.util.Iterator; import java.util.Iterator;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* Service for generating and validating single-use random tokens (nonces). * Service for generating and validating single-use random tokens (nonces).
* Each generated nonce is at least 128 bits and case-insensitive.
*/ */
@Singleton @Singleton
public class NonceService { public class NonceService {
@@ -98,7 +100,8 @@ public class NonceService {
* valid, in milliseconds. * valid, in milliseconds.
* *
* @return * @return
* A cryptographically-secure nonce value. * A cryptographically-secure nonce value. Generated nonces are at
* least 128-bit and are case-insensitive.
*/ */
public String generate(long maxAge) { public String generate(long maxAge) {
@@ -106,7 +109,7 @@ public class NonceService {
sweepExpiredNonces(); sweepExpiredNonces();
// Generate and store nonce, along with expiration timestamp // Generate and store nonce, along with expiration timestamp
String nonce = idGenerator.generateIdentifier(NONCE_BITS); String nonce = idGenerator.generateIdentifier(NONCE_BITS, false);
nonces.put(nonce, System.currentTimeMillis() + maxAge); nonces.put(nonce, System.currentTimeMillis() + maxAge);
return nonce; return nonce;
@@ -119,7 +122,7 @@ public class NonceService {
* invalidates that nonce. * invalidates that nonce.
* *
* @param nonce * @param nonce
* The nonce value to test. * The nonce value to test. Comparisons are case-insensitive.
* *
* @return * @return
* true if the provided nonce is valid, false otherwise. * true if the provided nonce is valid, false otherwise.
@@ -127,7 +130,7 @@ public class NonceService {
public boolean isValid(String nonce) { public boolean isValid(String nonce) {
// Remove nonce, verifying whether it was present at all // Remove nonce, verifying whether it was present at all
Long expires = nonces.remove(nonce); Long expires = nonces.remove(nonce.toLowerCase(Locale.US));
if (expires == null) if (expires == null)
return false; return false;