diff --git a/History.md b/History.md index 4de49f7b3d..d07087d3de 100644 --- a/History.md +++ b/History.md @@ -15,6 +15,9 @@ * The oplog observe driver handles errors communicating with Mongo better and knows to re-poll all queries during Mongo failovers. +* Add `Random.secret()` for generating security-critical secrets like + login tokens. + * Upgraded dependencies: - Node.js from 0.10.25 to 0.10.26. - MongoDB driver from 1.3.19 to 1.4.1 diff --git a/docs/client/packages/random.html b/docs/client/packages/random.html index a9684035f4..1b85ccd4bf 100644 --- a/docs/client/packages/random.html +++ b/docs/client/packages/random.html @@ -10,9 +10,18 @@ servers that don't have enough entropy to seed the cryptographically strong generator).
-{{#dtdd "Random.id()"}} -Returns a unique identifier, such as `"Jjwjg6gouWLXhMGKW"`, that is likely to -be unique in the whole world. +{{#dtdd "Random.id([n])"}} +Returns a unique identifier, such as `"Jjwjg6gouWLXhMGKW"`, that is +likely to be unique in the whole world. The optional argument `n` +specifies the length of the identifier in characters and defaults to 17. +{{/dtdd}} + +{{#dtdd "Random.secret([n])"}} +Returns a random string of printable characters with 6 bits of +entropy per character. The optional argument `n` specifies the length of +the secret string and defaults to 43 characters, or 256 bits of +entropy. Use `Random.secret` for security-critical secrets that are +intended for machine, rather than human, consumption. {{/dtdd}} {{#dtdd "Random.fraction()"}} diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 444f50db32..233f698a43 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -760,7 +760,7 @@ Accounts.registerLoginHandler("resume", function(options) { // (Also used by Meteor Accounts server and tests). // Accounts._generateStampedLoginToken = function () { - return {token: Random.id(), when: (new Date)}; + return {token: Random.secret(), when: (new Date)}; }; /// diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 8f4b8dd862..6a019f2d4d 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -263,7 +263,7 @@ Accounts.sendResetPasswordEmail = function (userId, email) { if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email)) throw new Error("No such email for user."); - var token = Random.id(); + var token = Random.secret(); var when = new Date(); Meteor.users.update(userId, {$set: { "services.password.reset": { @@ -312,7 +312,7 @@ Accounts.sendEnrollmentEmail = function (userId, email) { throw new Error("No such email for user."); - var token = Random.id(); + var token = Random.secret(); var when = new Date(); Meteor.users.update(userId, {$set: { "services.password.reset": { @@ -435,7 +435,7 @@ Accounts.sendVerificationEmail = function (userId, address) { var tokenRecord = { - token: Random.id(), + token: Random.secret(), address: address, when: new Date()}; Meteor.users.update( diff --git a/packages/facebook/facebook_client.js b/packages/facebook/facebook_client.js index d34d9b2cc3..ed86eed72d 100644 --- a/packages/facebook/facebook_client.js +++ b/packages/facebook/facebook_client.js @@ -19,7 +19,7 @@ Facebook.requestCredential = function (options, credentialRequestCompleteCallbac return; } - var credentialToken = Random.id(); + var credentialToken = Random.secret(); var mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone/i.test(navigator.userAgent); var display = mobile ? 'touch' : 'popup'; diff --git a/packages/github/github_client.js b/packages/github/github_client.js index d335702b6e..ccf06b2a5d 100644 --- a/packages/github/github_client.js +++ b/packages/github/github_client.js @@ -17,7 +17,7 @@ Github.requestCredential = function (options, credentialRequestCompleteCallback) credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError("Service not configured")); return; } - var credentialToken = Random.id(); + var credentialToken = Random.secret(); var scope = (options && options.requestPermissions) || []; var flatScope = _.map(scope, encodeURIComponent).join('+'); diff --git a/packages/google/google_client.js b/packages/google/google_client.js index a828571fd6..b080264669 100644 --- a/packages/google/google_client.js +++ b/packages/google/google_client.js @@ -20,7 +20,7 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback) return; } - var credentialToken = Random.id(); + var credentialToken = Random.secret(); // always need this to get user id from google. var requiredScope = ['profile']; diff --git a/packages/meetup/meetup_client.js b/packages/meetup/meetup_client.js index ee495a7598..277811d208 100644 --- a/packages/meetup/meetup_client.js +++ b/packages/meetup/meetup_client.js @@ -16,7 +16,7 @@ Meetup.requestCredential = function (options, credentialRequestCompleteCallback) credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError("Service not configured")); return; } - var credentialToken = Random.id(); + var credentialToken = Random.secret(); var scope = (options && options.requestPermissions) || []; var flatScope = _.map(scope, encodeURIComponent).join('+'); diff --git a/packages/meteor-developer/meteor_developer_client.js b/packages/meteor-developer/meteor_developer_client.js index 8f3ad1a9dd..81a57d58c8 100644 --- a/packages/meteor-developer/meteor_developer_client.js +++ b/packages/meteor-developer/meteor_developer_client.js @@ -16,7 +16,7 @@ var requestCredential = function (credentialRequestCompleteCallback) { return; } - var credentialToken = Random.id(); + var credentialToken = Random.secret(); var loginUrl = METEOR_DEVELOPER_URL + "/oauth2/authorize?" + diff --git a/packages/oauth1/oauth1_binding.js b/packages/oauth1/oauth1_binding.js index 43e63a111b..bcfafed683 100644 --- a/packages/oauth1/oauth1_binding.js +++ b/packages/oauth1/oauth1_binding.js @@ -85,7 +85,7 @@ OAuth1Binding.prototype._buildHeader = function(headers) { var self = this; return _.extend({ oauth_consumer_key: self._config.consumerKey, - oauth_nonce: Random.id().replace(/\W/g, ''), + oauth_nonce: Random.secret().replace(/\W/g, ''), oauth_signature_method: 'HMAC-SHA1', oauth_timestamp: (new Date().valueOf()/1000).toFixed().toString(), oauth_version: '1.0' diff --git a/packages/random/random.js b/packages/random/random.js index 0c1397df5b..f0d22f2eb5 100644 --- a/packages/random/random.js +++ b/packages/random/random.js @@ -86,6 +86,8 @@ var Alea = function () { }; var UNMISTAKABLE_CHARS = "23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz"; +var BASE64_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "0123456789-_"; // If seeds are provided, then the alea PRNG will be used, since cryptographic // PRNGs (Node crypto and window.crypto.getRandomValues) don't allow us to @@ -139,17 +141,36 @@ RandomGenerator.prototype.hexString = function (digits) { return hexDigits.join(''); } }; -RandomGenerator.prototype.id = function () { - var digits = []; + +RandomGenerator.prototype._randomString = function (charsCount, + alphabet) { var self = this; - // Length of 17 preserves around 96 bits of entropy, which is the - // amount of state in the Alea PRNG. - for (var i = 0; i < 17; i++) { - digits[i] = self.choice(UNMISTAKABLE_CHARS); + var digits = []; + for (var i = 0; i < charsCount; i++) { + digits[i] = self.choice(alphabet); } return digits.join(""); }; +RandomGenerator.prototype.id = function (charsCount) { + var self = this; + // 17 characters is around 96 bits of entropy, which is the amount of + // state in the Alea PRNG. + if (charsCount === undefined) + charsCount = 17; + + return self._randomString(charsCount, UNMISTAKABLE_CHARS); +}; + +RandomGenerator.prototype.secret = function (charsCount) { + var self = this; + // Default to 256 bits of entropy, or 43 characters at 6 bits per + // character. + if (charsCount === undefined) + charsCount = 43; + return self._randomString(charsCount, BASE64_CHARS); +}; + RandomGenerator.prototype.choice = function (arrayOrString) { var index = Math.floor(this.fraction() * arrayOrString.length); if (typeof arrayOrString === "string") diff --git a/packages/random/random_tests.js b/packages/random/random_tests.js index 436a87daef..c586e5ff8c 100644 --- a/packages/random/random_tests.js +++ b/packages/random/random_tests.js @@ -19,6 +19,7 @@ Tinytest.add('random', function (test) { Tinytest.add('random - format', function (test) { var idLen = 17; test.equal(Random.id().length, idLen); + test.equal(Random.id(29).length, 29); var numDigits = 9; var hexStr = Random.hexString(numDigits); test.equal(hexStr.length, numDigits); @@ -26,6 +27,9 @@ Tinytest.add('random - format', function (test) { var frac = Random.fraction(); test.isTrue(frac < 1.0); test.isTrue(frac >= 0.0); + + test.equal(Random.secret().length, 43); + test.equal(Random.secret(13).length, 13); }); Tinytest.add('random - Alea is last resort', function (test) { diff --git a/packages/srp/srp.js b/packages/srp/srp.js index e04860d577..231a1b59b7 100644 --- a/packages/srp/srp.js +++ b/packages/srp/srp.js @@ -15,8 +15,8 @@ SRP = {}; SRP.generateVerifier = function (password, options) { var params = paramsFromOptions(options); - var identity = (options && options.identity) || Random.id(); - var salt = (options && options.salt) || Random.id(); + var identity = (options && options.identity) || Random.secret(); + var salt = (options && options.salt) || Random.secret(); var x = params.hash(salt + params.hash(identity + ":" + password)); var xi = new BigInteger(x, 16); diff --git a/packages/twitter/twitter_client.js b/packages/twitter/twitter_client.js index c8ca6fd44e..f1553c9e96 100644 --- a/packages/twitter/twitter_client.js +++ b/packages/twitter/twitter_client.js @@ -18,7 +18,7 @@ Twitter.requestCredential = function (options, credentialRequestCompleteCallback return; } - var credentialToken = Random.id(); + var credentialToken = Random.secret(); // We need to keep credentialToken across the next two 'steps' so we're adding // a credentialToken parameter to the url and the callback url that we'll be returned // to by oauth provider diff --git a/packages/weibo/weibo_client.js b/packages/weibo/weibo_client.js index 32b95f292c..feff8f2920 100644 --- a/packages/weibo/weibo_client.js +++ b/packages/weibo/weibo_client.js @@ -18,7 +18,7 @@ Weibo.requestCredential = function (options, credentialRequestCompleteCallback) return; } - var credentialToken = Random.id(); + var credentialToken = Random.secret(); // XXX need to support configuring access_type and scope var loginUrl = 'https://api.weibo.com/oauth2/authorize' +