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' +