Skip to content

Commit 62cbc45

Browse files
committed
Generate tokens and ids with cryptoUtils module.
Move object ID, token, and random string generation into their own module, cryptoUtils. Remove hat dependency, which was used to generate session and some other tokens, because it used non-cryptographic random number generator. Replace it with the cryptographically secure one. The result has the same format (32-character hex string, 128 bits of entropy). Remove randomstring dependency, as we already have this functionality. Add tests.
1 parent 0c75f60 commit 62cbc45

File tree

8 files changed

+142
-41
lines changed

8 files changed

+142
-41
lines changed

package.json

-2
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

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

+2-4
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

+2-5
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

+6-23
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

+2-4
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

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

+3-3
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)