Skip to content

Commit 2510cab

Browse files
committed
Merge pull request #372 from dchest/user-prng
Generate tokens with CSPRNG
2 parents 0c75f60 + 62cbc45 commit 2510cab

File tree

8 files changed

+142
-41
lines changed

8 files changed

+142
-41
lines changed

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,11 @@
1616
"body-parser": "^1.14.2",
1717
"deepcopy": "^0.6.1",
1818
"express": "^4.13.4",
19-
"hat": "~0.0.3",
2019
"mime": "^1.3.4",
2120
"mongodb": "~2.1.0",
2221
"multer": "^1.1.0",
2322
"node-gcm": "^0.14.0",
2423
"parse": "^1.7.0",
25-
"randomstring": "^1.1.3",
2624
"request": "^2.65.0",
2725
"winston": "^2.1.1"
2826
},

spec/cryptoUtils.spec.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
var cryptoUtils = require('../src/cryptoUtils');
2+
3+
function givesUniqueResults(fn, iterations) {
4+
var results = {};
5+
for (var i = 0; i < iterations; i++) {
6+
var s = fn();
7+
if (results[s]) {
8+
return false;
9+
}
10+
results[s] = true;
11+
}
12+
return true;
13+
}
14+
15+
describe('randomString', () => {
16+
it('returns a string', () => {
17+
expect(typeof cryptoUtils.randomString(10)).toBe('string');
18+
});
19+
20+
it('returns result of the given length', () => {
21+
expect(cryptoUtils.randomString(11).length).toBe(11);
22+
expect(cryptoUtils.randomString(25).length).toBe(25);
23+
});
24+
25+
it('throws if requested length is zero', () => {
26+
expect(() => cryptoUtils.randomString(0)).toThrow();
27+
});
28+
29+
it('returns unique results', () => {
30+
expect(givesUniqueResults(() => cryptoUtils.randomString(10), 100)).toBe(true);
31+
});
32+
});
33+
34+
describe('randomHexString', () => {
35+
it('returns a string', () => {
36+
expect(typeof cryptoUtils.randomHexString(10)).toBe('string');
37+
});
38+
39+
it('returns result of the given length', () => {
40+
expect(cryptoUtils.randomHexString(10).length).toBe(10);
41+
expect(cryptoUtils.randomHexString(32).length).toBe(32);
42+
});
43+
44+
it('throws if requested length is zero', () => {
45+
expect(() => cryptoUtils.randomHexString(0)).toThrow();
46+
});
47+
48+
it('throws if requested length is not even', () => {
49+
expect(() => cryptoUtils.randomHexString(11)).toThrow();
50+
});
51+
52+
it('returns unique results', () => {
53+
expect(givesUniqueResults(() => cryptoUtils.randomHexString(20), 100)).toBe(true);
54+
});
55+
});
56+
57+
describe('newObjectId', () => {
58+
it('returns a string', () => {
59+
expect(typeof cryptoUtils.newObjectId()).toBe('string');
60+
});
61+
62+
it('returns result with at least 10 characters', () => {
63+
expect(cryptoUtils.newObjectId().length).toBeGreaterThan(9);
64+
});
65+
66+
it('returns unique results', () => {
67+
expect(givesUniqueResults(() => cryptoUtils.newObjectId(), 100)).toBe(true);
68+
});
69+
});
70+
71+
describe('newToken', () => {
72+
it('returns a string', () => {
73+
expect(typeof cryptoUtils.newToken()).toBe('string');
74+
});
75+
76+
it('returns result with at least 32 characters', () => {
77+
expect(cryptoUtils.newToken().length).toBeGreaterThan(31);
78+
});
79+
80+
it('returns unique results', () => {
81+
expect(givesUniqueResults(() => cryptoUtils.newToken(), 100)).toBe(true);
82+
});
83+
});

src/Controllers/FilesController.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ import express from 'express';
44
import mime from 'mime';
55
import { Parse } from 'parse/node';
66
import BodyParser from 'body-parser';
7-
import hat from 'hat';
87
import * as Middlewares from '../middlewares';
98
import Config from '../Config';
10-
11-
const rack = hat.rack();
9+
import { randomHexString } from '../cryptoUtils';
1210

1311
export class FilesController {
1412
constructor(filesAdapter) {
@@ -61,7 +59,7 @@ export class FilesController {
6159
extension = '.' + mime.extension(contentType);
6260
}
6361

64-
let filename = rack() + '_' + req.params.filename + extension;
62+
let filename = randomHexString(32) + '_' + req.params.filename + extension;
6563
this._filesAdapter.createFile(req.config, filename, req.body).then(() => {
6664
res.status(201);
6765
var location = this._filesAdapter.getFileLocation(req.config, filename);

src/GCM.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
const Parse = require('parse/node').Parse;
44
const gcm = require('node-gcm');
5-
const randomstring = require('randomstring');
5+
const cryptoUtils = require('./cryptoUtils');
66

77
const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
88
const GCMRegistrationTokensMax = 1000;
@@ -22,10 +22,7 @@ function GCM(args) {
2222
* @returns {Object} A promise which is resolved after we get results from gcm
2323
*/
2424
GCM.prototype.send = function(data, devices) {
25-
let pushId = randomstring.generate({
26-
length: 10,
27-
charset: 'alphanumeric'
28-
});
25+
let pushId = cryptoUtils.newObjectId();
2926
let timeStamp = Date.now();
3027
let expirationTime;
3128
// We handle the expiration_time convertion in push.js, so expiration_time is a valid date

src/RestWrite.js

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
// that writes to the database.
33
// This could be either a "create" or an "update".
44

5-
var crypto = require('crypto');
65
var deepcopy = require('deepcopy');
7-
var rack = require('hat').rack();
86

97
var Auth = require('./Auth');
108
var cache = require('./cache');
119
var Config = require('./Config');
10+
var cryptoUtils = require('./cryptoUtils');
1211
var passwordCrypto = require('./password');
1312
var facebook = require('./facebook');
1413
var Parse = require('parse/node');
@@ -56,7 +55,7 @@ function RestWrite(config, auth, className, query, data, originalData) {
5655
this.data.updatedAt = this.updatedAt;
5756
if (!this.query) {
5857
this.data.createdAt = this.updatedAt;
59-
this.data.objectId = newStringId(10);
58+
this.data.objectId = cryptoUtils.newObjectId();
6059
}
6160
}
6261
}
@@ -252,7 +251,7 @@ RestWrite.prototype.handleFacebookAuthData = function() {
252251
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
253252
'this auth is already used');
254253
} else {
255-
this.data.username = rack();
254+
this.data.username = cryptoUtils.newToken();
256255
}
257256

258257
// This FB auth does not already exist, so transform it to a
@@ -273,7 +272,7 @@ RestWrite.prototype.transformUser = function() {
273272
var promise = Promise.resolve();
274273

275274
if (!this.query) {
276-
var token = 'r:' + rack();
275+
var token = 'r:' + cryptoUtils.newToken();
277276
this.storage['token'] = token;
278277
promise = promise.then(() => {
279278
var expiresAt = new Date();
@@ -319,7 +318,7 @@ RestWrite.prototype.transformUser = function() {
319318
// Check for username uniqueness
320319
if (!this.data.username) {
321320
if (!this.query) {
322-
this.data.username = newStringId(25);
321+
this.data.username = cryptoUtils.randomString(25);
323322
}
324323
return;
325324
}
@@ -412,7 +411,7 @@ RestWrite.prototype.handleSession = function() {
412411
}
413412

414413
if (!this.query && !this.auth.isMaster) {
415-
var token = 'r:' + rack();
414+
var token = 'r:' + cryptoUtils.newToken();
416415
var expiresAt = new Date();
417416
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
418417
var sessionData = {
@@ -713,20 +712,4 @@ RestWrite.prototype.objectId = function() {
713712
return this.data.objectId || this.query.objectId;
714713
};
715714

716-
// Returns a unique string that's usable as an object or other id.
717-
function newStringId(size) {
718-
var chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
719-
'abcdefghijklmnopqrstuvwxyz' +
720-
'0123456789');
721-
var objectId = '';
722-
var bytes = crypto.randomBytes(size);
723-
for (var i = 0; i < bytes.length; ++i) {
724-
// Note: there is a slight modulo bias, because chars length
725-
// of 62 doesn't divide the number of all bytes (256) evenly.
726-
// It is acceptable for our purposes.
727-
objectId += chars[bytes.readUInt8(i) % chars.length];
728-
}
729-
return objectId;
730-
}
731-
732715
module.exports = RestWrite;

src/Routers/UsersRouter.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// These methods handle the User-related routes.
22

3-
import hat from 'hat';
43
import deepcopy from 'deepcopy';
54

65
import ClassesRouter from './ClassesRouter';
@@ -9,8 +8,7 @@ import rest from '../rest';
98
import Auth from '../Auth';
109
import passwordCrypto from '../password';
1110
import RestWrite from '../RestWrite';
12-
13-
const rack = hat.rack();
11+
import { newToken } from '../cryptoUtils';
1412

1513
export class UsersRouter extends ClassesRouter {
1614
handleFind(req) {
@@ -89,7 +87,7 @@ export class UsersRouter extends ClassesRouter {
8987
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
9088
}
9189

92-
let token = 'r:' + rack();
90+
let token = 'r:' + newToken();
9391
user.sessionToken = token;
9492
delete user.password;
9593

src/cryptoUtils.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { randomBytes } from 'crypto';
2+
3+
// Returns a new random hex string of the given even size.
4+
export function randomHexString(size) {
5+
if (size === 0) {
6+
throw new Error('Zero-length randomHexString is useless.');
7+
}
8+
if (size % 2 !== 0) {
9+
throw new Error('randomHexString size must be divisible by 2.')
10+
}
11+
return randomBytes(size/2).toString('hex');
12+
}
13+
14+
// Returns a new random alphanumeric string of the given size.
15+
//
16+
// Note: to simplify implementation, the result has slight modulo bias,
17+
// because chars length of 62 doesn't divide the number of all bytes
18+
// (256) evenly. Such bias is acceptable for most cases when the output
19+
// length is long enough and doesn't need to be uniform.
20+
export function randomString(size) {
21+
if (size === 0) {
22+
throw new Error('Zero-length randomString is useless.');
23+
}
24+
var chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
25+
'abcdefghijklmnopqrstuvwxyz' +
26+
'0123456789');
27+
var objectId = '';
28+
var bytes = randomBytes(size);
29+
for (var i = 0; i < bytes.length; ++i) {
30+
objectId += chars[bytes.readUInt8(i) % chars.length];
31+
}
32+
return objectId;
33+
}
34+
35+
// Returns a new random alphanumeric string suitable for object ID.
36+
export function newObjectId() {
37+
//TODO: increase length to better protect against collisions.
38+
return randomString(10);
39+
}
40+
41+
// Returns a new random hex string suitable for secure tokens.
42+
export function newToken() {
43+
return randomHexString(32);
44+
}

src/testing-routes.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
var express = require('express'),
44
cache = require('./cache'),
55
middlewares = require('./middlewares'),
6-
rack = require('hat').rack();
6+
cryptoUtils = require('./cryptoUtils');
77

88
var router = express.Router();
99

1010
// creates a unique app in the cache, with a collection prefix
1111
function createApp(req, res) {
12-
var appId = rack();
12+
var appId = cryptoUtils.randomHexString(32);
1313
cache.apps[appId] = {
1414
'collectionPrefix': appId + '_',
1515
'masterKey': 'master'
@@ -70,4 +70,4 @@ router.post('/rest_configure_app',
7070

7171
module.exports = {
7272
router: router
73-
};
73+
};

0 commit comments

Comments
 (0)