Using cryptoAPI if available in generateRandomSessionId
authorNuria Ruiz <nuria@wikimedia.org>
Sat, 31 Jan 2015 03:50:09 +0000 (19:50 -0800)
committerNuria Ruiz <nuria@wikimedia.org>
Fri, 13 Feb 2015 22:51:34 +0000 (14:51 -0800)
BREAKING CHANGE:
The alphabet of the prior string returned was
A-Za-z0-9 and now it is 0-9A-F

Bug: T78449

Change-Id: I71b5ccc5887b842485971917809d1eb01bc56a90

RELEASE-NOTES-1.25
resources/src/mediawiki/mediawiki.user.js
tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js

index 3f24db0..070e60a 100644 (file)
@@ -344,6 +344,8 @@ changes to languages because of Bugzilla reports.
     Instead, do this:
       $form = HTMLForm::factory( 'vform', … );
 * Deprecated Revision methods getRawUser(), getRawUserText() and getRawComment().
+* BREAKING CHANGE: mediawiki.user.generateRandomSessionId:
+  The alphabet of the prior string returned was A-Za-z0-9 and now it is 0-9A-F
 
 == Compatibility ==
 
index 5e0cfcc..c3ec3f3 100644 (file)
@@ -3,13 +3,20 @@
  * @singleton
  */
 ( function ( mw, $ ) {
-       var user,
+       var user, i,
                deferreds = {},
+               byteToHex = [],
                // Extend the skeleton mw.user from mediawiki.js
                // This is kind of ugly but we're stuck with this for b/c reasons
                options = mw.user.options || new mw.Map(),
                tokens = mw.user.tokens || new mw.Map();
 
+       // Maps for number -> hex string conversion (with padding)
+       // idea from: https://github.com/broofa/node-uuid/blob/master/uuid.js
+       for ( i = 0; i < 256; i++ ) {
+               byteToHex[i] = (i + 0x100).toString(16).substr(1);
+       }
+
        /**
         * Get the current user's groups or rights
         *
                tokens: tokens,
 
                /**
-                * Generate a random user session ID (32 alpha-numeric characters)
-                *
+                * Generate a random user session ID
                 * This information would potentially be stored in a cookie to identify a user during a
-                * session or series of sessions. Its uniqueness should not be depended on.
+                * session or series of sessions. Its uniqueness should
+                * not be depended on unless the browser supports the crypto API.
+                *
+                * Known problems with Math.random():
+                * Using the Math.random function we have seen sets
+                * with 1% of non uniques among 200.000 values with Safari providing most of these.
+                * Given the prevalence of Safari in mobile the percentage of duplicates in
+                * mobile usages of this code is probably higher.
+                *
+                * Rationale:
+                * We need about 64 bits to make sure that probability of collision
+                * on 500 million (5*10^8) is <= 1%
+                * See: https://en.wikipedia.org/wiki/Birthday_problem#Probability_table
                 *
-                * @return {string} Random set of 32 alpha-numeric characters
+                * @return {string} 64 bit integer in hex format, padded
                 */
                generateRandomSessionId: function () {
-                       var i, r,
-                               id = '',
-                               seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
-                       for ( i = 0; i < 32; i++ ) {
-                               r = Math.floor( Math.random() * seed.length );
-                               id += seed.charAt( r );
+                       /*jshint bitwise:false */
+                       var rnds, i, r, cryptoObj, hexRnds = new Array( 8 );
+                       cryptoObj = window.crypto || window.msCrypto; // for IE 11
+
+                       if ( cryptoObj ) {
+                               // We fill an array with 8 random values, each of which is 8 bits.
+                               // note that rnds is an array-like object not a true array
+                               rnds = new Uint8Array( 8 );
+                               cryptoObj.getRandomValues( rnds );
+                       } else {
+                               rnds = new Array( 8 );
+                               // From: https://github.com/broofa/node-uuid/blob/master/uuid.js
+                               for ( i = 0, r; i < 8; i++ ) {
+                                       if ( ( i & 0x03 ) === 0 ) {
+                                               r = Math.random() * 0x100000000;
+                                       }
+                                       rnds[i] = r >>> ( ( i & 0x03 ) << 3 ) & 0xff;
+                               }
                        }
-                       return id;
+                       // convert to hex using byteToHex that already contains padding
+                       for ( i = 0; i < rnds.length; i++ ) {
+                               hexRnds[i] = byteToHex[rnds[i]];
+                       }
+
+                       // concatenation of two random integers with entrophy n and m
+                       // returns a string with entrophy n+m if those strings are independent
+                       return hexRnds.join( '' );
                },
 
                /**
index 91321a2..fe22b7a 100644 (file)
 
                this.server.respond();
        } );
+
+       QUnit.test( 'session numbers', 4, function ( assert ) {
+               /*global $:false */
+               var sessionId = mw.user.generateRandomSessionId(),
+               cryptoObj = window.crypto;
+
+               assert.equal( typeof sessionId, 'string', 'generateRandomSessionId should return a string' );
+               assert.equal( $.trim(sessionId), sessionId, 'generateRandomSessionId should not contain whitespace' );
+               // pretend crypto API is not there and do same test, make sure code runs
+               // through  Math.random loop
+               window.crypto = undefined;
+               sessionId =  mw.user.generateRandomSessionId();
+               assert.equal( typeof sessionId, 'string', 'generateRandomSessionId should return a string' );
+               assert.equal( sessionId.trim(), sessionId, 'generateRandomSessionId should not be empty' );
+               //restoring crypto object
+               window.crypto = cryptoObj;
+
+       } );
 }( mediaWiki ) );