Merge pull request #372 from dchest/user-prng

Generate tokens with CSPRNG
This commit is contained in:
Drew
2016-02-12 12:25:55 -08:00
8 changed files with 142 additions and 41 deletions

View File

@@ -16,13 +16,11 @@
"body-parser": "^1.14.2", "body-parser": "^1.14.2",
"deepcopy": "^0.6.1", "deepcopy": "^0.6.1",
"express": "^4.13.4", "express": "^4.13.4",
"hat": "~0.0.3",
"mime": "^1.3.4", "mime": "^1.3.4",
"mongodb": "~2.1.0", "mongodb": "~2.1.0",
"multer": "^1.1.0", "multer": "^1.1.0",
"node-gcm": "^0.14.0", "node-gcm": "^0.14.0",
"parse": "^1.7.0", "parse": "^1.7.0",
"randomstring": "^1.1.3",
"request": "^2.65.0", "request": "^2.65.0",
"winston": "^2.1.1" "winston": "^2.1.1"
}, },

83
spec/cryptoUtils.spec.js Normal file
View File

@@ -0,0 +1,83 @@
var cryptoUtils = require('../src/cryptoUtils');
function givesUniqueResults(fn, iterations) {
var results = {};
for (var i = 0; i < iterations; i++) {
var s = fn();
if (results[s]) {
return false;
}
results[s] = true;
}
return true;
}
describe('randomString', () => {
it('returns a string', () => {
expect(typeof cryptoUtils.randomString(10)).toBe('string');
});
it('returns result of the given length', () => {
expect(cryptoUtils.randomString(11).length).toBe(11);
expect(cryptoUtils.randomString(25).length).toBe(25);
});
it('throws if requested length is zero', () => {
expect(() => cryptoUtils.randomString(0)).toThrow();
});
it('returns unique results', () => {
expect(givesUniqueResults(() => cryptoUtils.randomString(10), 100)).toBe(true);
});
});
describe('randomHexString', () => {
it('returns a string', () => {
expect(typeof cryptoUtils.randomHexString(10)).toBe('string');
});
it('returns result of the given length', () => {
expect(cryptoUtils.randomHexString(10).length).toBe(10);
expect(cryptoUtils.randomHexString(32).length).toBe(32);
});
it('throws if requested length is zero', () => {
expect(() => cryptoUtils.randomHexString(0)).toThrow();
});
it('throws if requested length is not even', () => {
expect(() => cryptoUtils.randomHexString(11)).toThrow();
});
it('returns unique results', () => {
expect(givesUniqueResults(() => cryptoUtils.randomHexString(20), 100)).toBe(true);
});
});
describe('newObjectId', () => {
it('returns a string', () => {
expect(typeof cryptoUtils.newObjectId()).toBe('string');
});
it('returns result with at least 10 characters', () => {
expect(cryptoUtils.newObjectId().length).toBeGreaterThan(9);
});
it('returns unique results', () => {
expect(givesUniqueResults(() => cryptoUtils.newObjectId(), 100)).toBe(true);
});
});
describe('newToken', () => {
it('returns a string', () => {
expect(typeof cryptoUtils.newToken()).toBe('string');
});
it('returns result with at least 32 characters', () => {
expect(cryptoUtils.newToken().length).toBeGreaterThan(31);
});
it('returns unique results', () => {
expect(givesUniqueResults(() => cryptoUtils.newToken(), 100)).toBe(true);
});
});

View File

@@ -4,11 +4,9 @@ import express from 'express';
import mime from 'mime'; import mime from 'mime';
import { Parse } from 'parse/node'; import { Parse } from 'parse/node';
import BodyParser from 'body-parser'; import BodyParser from 'body-parser';
import hat from 'hat';
import * as Middlewares from '../middlewares'; import * as Middlewares from '../middlewares';
import Config from '../Config'; import Config from '../Config';
import { randomHexString } from '../cryptoUtils';
const rack = hat.rack();
export class FilesController { export class FilesController {
constructor(filesAdapter) { constructor(filesAdapter) {
@@ -61,7 +59,7 @@ export class FilesController {
extension = '.' + mime.extension(contentType); extension = '.' + mime.extension(contentType);
} }
let filename = rack() + '_' + req.params.filename + extension; let filename = randomHexString(32) + '_' + req.params.filename + extension;
this._filesAdapter.createFile(req.config, filename, req.body).then(() => { this._filesAdapter.createFile(req.config, filename, req.body).then(() => {
res.status(201); res.status(201);
var location = this._filesAdapter.getFileLocation(req.config, filename); var location = this._filesAdapter.getFileLocation(req.config, filename);

View File

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

View File

@@ -2,13 +2,12 @@
// that writes to the database. // that writes to the database.
// This could be either a "create" or an "update". // This could be either a "create" or an "update".
var crypto = require('crypto');
var deepcopy = require('deepcopy'); var deepcopy = require('deepcopy');
var rack = require('hat').rack();
var Auth = require('./Auth'); var Auth = require('./Auth');
var cache = require('./cache'); var cache = require('./cache');
var Config = require('./Config'); var Config = require('./Config');
var cryptoUtils = require('./cryptoUtils');
var passwordCrypto = require('./password'); var passwordCrypto = require('./password');
var facebook = require('./facebook'); var facebook = require('./facebook');
var Parse = require('parse/node'); var Parse = require('parse/node');
@@ -56,7 +55,7 @@ function RestWrite(config, auth, className, query, data, originalData) {
this.data.updatedAt = this.updatedAt; this.data.updatedAt = this.updatedAt;
if (!this.query) { if (!this.query) {
this.data.createdAt = this.updatedAt; this.data.createdAt = this.updatedAt;
this.data.objectId = newStringId(10); this.data.objectId = cryptoUtils.newObjectId();
} }
} }
} }
@@ -252,7 +251,7 @@ RestWrite.prototype.handleFacebookAuthData = function() {
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
'this auth is already used'); 'this auth is already used');
} else { } else {
this.data.username = rack(); this.data.username = cryptoUtils.newToken();
} }
// This FB auth does not already exist, so transform it to a // This FB auth does not already exist, so transform it to a
@@ -273,7 +272,7 @@ RestWrite.prototype.transformUser = function() {
var promise = Promise.resolve(); var promise = Promise.resolve();
if (!this.query) { if (!this.query) {
var token = 'r:' + rack(); var token = 'r:' + cryptoUtils.newToken();
this.storage['token'] = token; this.storage['token'] = token;
promise = promise.then(() => { promise = promise.then(() => {
var expiresAt = new Date(); var expiresAt = new Date();
@@ -319,7 +318,7 @@ RestWrite.prototype.transformUser = function() {
// Check for username uniqueness // Check for username uniqueness
if (!this.data.username) { if (!this.data.username) {
if (!this.query) { if (!this.query) {
this.data.username = newStringId(25); this.data.username = cryptoUtils.randomString(25);
} }
return; return;
} }
@@ -412,7 +411,7 @@ RestWrite.prototype.handleSession = function() {
} }
if (!this.query && !this.auth.isMaster) { if (!this.query && !this.auth.isMaster) {
var token = 'r:' + rack(); var token = 'r:' + cryptoUtils.newToken();
var expiresAt = new Date(); var expiresAt = new Date();
expiresAt.setFullYear(expiresAt.getFullYear() + 1); expiresAt.setFullYear(expiresAt.getFullYear() + 1);
var sessionData = { var sessionData = {
@@ -713,20 +712,4 @@ RestWrite.prototype.objectId = function() {
return this.data.objectId || this.query.objectId; return this.data.objectId || this.query.objectId;
}; };
// Returns a unique string that's usable as an object or other id.
function newStringId(size) {
var chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'abcdefghijklmnopqrstuvwxyz' +
'0123456789');
var objectId = '';
var bytes = crypto.randomBytes(size);
for (var i = 0; i < bytes.length; ++i) {
// Note: there is a slight modulo bias, because chars length
// of 62 doesn't divide the number of all bytes (256) evenly.
// It is acceptable for our purposes.
objectId += chars[bytes.readUInt8(i) % chars.length];
}
return objectId;
}
module.exports = RestWrite; module.exports = RestWrite;

View File

@@ -1,6 +1,5 @@
// These methods handle the User-related routes. // These methods handle the User-related routes.
import hat from 'hat';
import deepcopy from 'deepcopy'; import deepcopy from 'deepcopy';
import ClassesRouter from './ClassesRouter'; import ClassesRouter from './ClassesRouter';
@@ -9,8 +8,7 @@ import rest from '../rest';
import Auth from '../Auth'; import Auth from '../Auth';
import passwordCrypto from '../password'; import passwordCrypto from '../password';
import RestWrite from '../RestWrite'; import RestWrite from '../RestWrite';
import { newToken } from '../cryptoUtils';
const rack = hat.rack();
export class UsersRouter extends ClassesRouter { export class UsersRouter extends ClassesRouter {
handleFind(req) { handleFind(req) {
@@ -89,7 +87,7 @@ export class UsersRouter extends ClassesRouter {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
} }
let token = 'r:' + rack(); let token = 'r:' + newToken();
user.sessionToken = token; user.sessionToken = token;
delete user.password; delete user.password;

44
src/cryptoUtils.js Normal file
View File

@@ -0,0 +1,44 @@
import { randomBytes } from 'crypto';
// Returns a new random hex string of the given even size.
export function randomHexString(size) {
if (size === 0) {
throw new Error('Zero-length randomHexString is useless.');
}
if (size % 2 !== 0) {
throw new Error('randomHexString size must be divisible by 2.')
}
return randomBytes(size/2).toString('hex');
}
// Returns a new random alphanumeric string of the given size.
//
// Note: to simplify implementation, the result has slight modulo bias,
// because chars length of 62 doesn't divide the number of all bytes
// (256) evenly. Such bias is acceptable for most cases when the output
// length is long enough and doesn't need to be uniform.
export function randomString(size) {
if (size === 0) {
throw new Error('Zero-length randomString is useless.');
}
var chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'abcdefghijklmnopqrstuvwxyz' +
'0123456789');
var objectId = '';
var bytes = randomBytes(size);
for (var i = 0; i < bytes.length; ++i) {
objectId += chars[bytes.readUInt8(i) % chars.length];
}
return objectId;
}
// Returns a new random alphanumeric string suitable for object ID.
export function newObjectId() {
//TODO: increase length to better protect against collisions.
return randomString(10);
}
// Returns a new random hex string suitable for secure tokens.
export function newToken() {
return randomHexString(32);
}

View File

@@ -3,13 +3,13 @@
var express = require('express'), var express = require('express'),
cache = require('./cache'), cache = require('./cache'),
middlewares = require('./middlewares'), middlewares = require('./middlewares'),
rack = require('hat').rack(); cryptoUtils = require('./cryptoUtils');
var router = express.Router(); var router = express.Router();
// creates a unique app in the cache, with a collection prefix // creates a unique app in the cache, with a collection prefix
function createApp(req, res) { function createApp(req, res) {
var appId = rack(); var appId = cryptoUtils.randomHexString(32);
cache.apps[appId] = { cache.apps[appId] = {
'collectionPrefix': appId + '_', 'collectionPrefix': appId + '_',
'masterKey': 'master' 'masterKey': 'master'
@@ -70,4 +70,4 @@ router.post('/rest_configure_app',
module.exports = { module.exports = {
router: router router: router
}; };