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