155 lines
5.0 KiB
Plaintext
155 lines
5.0 KiB
Plaintext
component /*https://www.bennadel.com/blog/2976-trying-to-generate-cryptographically-strong-random-tokens-in-coldfusion.htm*/
|
|
output = false
|
|
hint = "I generate random tokens using Java's SecureRandom class."
|
|
{
|
|
|
|
/**
|
|
* I initialize the token generator.
|
|
*
|
|
* @output false
|
|
*/
|
|
public any function init() {
|
|
|
|
// I am the generator implementation used to generate the random token data.
|
|
// --
|
|
// NOTE: The SHA1PRNG is not the default in all implementations of the JVM. As
|
|
// such, we're defining the algorithm explicitly to keep this code consistent.
|
|
generator = createObject( "java", "java.security.SecureRandom" )
|
|
.getInstance(
|
|
javaCast( "string", "SHA1PRNG" ),
|
|
javaCast( "string", "SUN" )
|
|
)
|
|
;
|
|
|
|
// Now that we've initialized the random generator, we have to generate a random
|
|
// byte of data. This will ensure that the random generator is self-seeded using
|
|
// a shared seed generator.
|
|
// --
|
|
// CAUTION: Since the underlying seed generator reads from sources of entropy,
|
|
// this may hang until enough entropy has been collected. This is another good
|
|
// reason to do it during initialization time rather than at first-use time.
|
|
generator.nextBytes( charsetDecode( " ", "utf-8" ) );
|
|
|
|
// I hold the future date at which time the generator should be reseeded in order
|
|
// to keep it unpredictable.
|
|
// --
|
|
// NOTE: This doesn't affect the randomness of the values. But, the thinking
|
|
// is that the longer the generator is producing values using the same seed,
|
|
// the more likely an attacker is to be able to determine the original seed by
|
|
// passively observing generated values.
|
|
reseedAt = getNextReseedAt();
|
|
|
|
return( this );
|
|
|
|
}
|
|
|
|
|
|
// ---
|
|
// PUBLIC METHODS.
|
|
// ---
|
|
|
|
|
|
/**
|
|
* I generate "cryptographically strong" random token strings that are based on the
|
|
* given number of random bytes. The random bytes are subsequently encoded using a
|
|
* base64url character-set so that they are URL-safe and can be used in a variety of
|
|
* contexts. And, since base65url is a case-sensitive schema, the tokens will
|
|
* naturally be case-sensitive.
|
|
*
|
|
* @byteCount I am the number of random bytes used to generate the token.
|
|
* @output false
|
|
*/
|
|
public string function nextToken( numeric byteCount = 32 ) {
|
|
|
|
// Check to see if the generator needs to be reseeded (using a double-check
|
|
// locking approach to reduce the bottleneck).
|
|
if ( now() >= reseedAt ) {
|
|
|
|
// Synchronize the reseeding.
|
|
// --
|
|
// NOTE: From what I have read, I DON'T BELIEVE that .generateSeed() will
|
|
// ever hang with the SHA1PRNG algorithm. However, it is unclear to me. As
|
|
// such, I'm using [throwOnTimeout = false] so that parallel threads won't
|
|
// error if the lock cannot be obtained in a timely manner and will just
|
|
// fall through to using the generator with the pre-seeding state.
|
|
lock
|
|
name = "TokenGenerator.reseedCheck"
|
|
type = "exclusive"
|
|
timeout = 1
|
|
throwOnTimeout = false
|
|
{
|
|
|
|
// Perform double-check - generator may have already been reseeded by
|
|
// a parallel request.
|
|
if ( now() >= reseedAt ) {
|
|
|
|
reseedAt = getNextReseedAt();
|
|
|
|
// NOTE: Once the generator was seeded internally, this re-seeding
|
|
// will only ever "add to" the existing seed. As such, this is still
|
|
// building on top of the original randomness and calling this, on
|
|
// interval, this will never reduce randomness.
|
|
generator.setSeed( generator.generateSeed( javaCast( "int", 32 ) ) );
|
|
|
|
}
|
|
|
|
} // END: Lock.
|
|
|
|
}
|
|
|
|
// Create the byte buffer into which the random bytes will be written. Since
|
|
// there's no "correct" way to generate a byte array in ColdFusion, we can
|
|
// generate a string of the desired length and then decode it into bytes.
|
|
var byteBuffer = charsetDecode( repeatString( " ", byteCount ), "utf-8" );
|
|
|
|
generator.nextBytes( byteBuffer );
|
|
|
|
return( encodeBytes( byteBuffer ) );
|
|
|
|
}
|
|
|
|
|
|
// ---
|
|
// PRIVATE METHODS.
|
|
// ---
|
|
|
|
|
|
/**
|
|
* I encode the given byte array using the base64url character-set.
|
|
*
|
|
* @bytes I am the byte array being encoded.
|
|
* @output false
|
|
*/
|
|
private string function encodeBytes( required binary bytes ) {
|
|
|
|
var token = binaryEncode( bytes, "base64" ); // *** вот поэтому длина отличается от заявленной
|
|
|
|
// Replace the characters that are not allowed in the base64url format. The
|
|
// characters [+, /, =] are removed for URL-based base64 values because they
|
|
// have significant meaning in the context of URL paths and query-strings.
|
|
/*token = replace( token, "+", "-", "all" );
|
|
token = replace( token, "/", "_", "all" );
|
|
token = replace( token, "=", "", "all" ); */
|
|
|
|
// мы хотим убрать все спецсимволы, потому что мы используем токен в качестве псевдослучайного суффикса
|
|
token = replace( token, "+", "a", "all" );
|
|
token = replace( token, "/", "b", "all" );
|
|
token = replace( token, "=", "c", "all" );
|
|
|
|
return( token );
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* I calculate the next date of reseeding.
|
|
*
|
|
* @output false
|
|
*/
|
|
private date function getNextReseedAt() {
|
|
|
|
return( dateAdd( "h", 1, now() ) );
|
|
|
|
}
|
|
|
|
} |