Merge pull request #372 from dchest/user-prng
Generate tokens with CSPRNG
This commit is contained in:
@@ -16,13 +16,11 @@
|
||||
"body-parser": "^1.14.2",
|
||||
"deepcopy": "^0.6.1",
|
||||
"express": "^4.13.4",
|
||||
"hat": "~0.0.3",
|
||||
"mime": "^1.3.4",
|
||||
"mongodb": "~2.1.0",
|
||||
"multer": "^1.1.0",
|
||||
"node-gcm": "^0.14.0",
|
||||
"parse": "^1.7.0",
|
||||
"randomstring": "^1.1.3",
|
||||
"request": "^2.65.0",
|
||||
"winston": "^2.1.1"
|
||||
},
|
||||
|
||||
83
spec/cryptoUtils.spec.js
Normal file
83
spec/cryptoUtils.spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -4,11 +4,9 @@ import express from 'express';
|
||||
import mime from 'mime';
|
||||
import { Parse } from 'parse/node';
|
||||
import BodyParser from 'body-parser';
|
||||
import hat from 'hat';
|
||||
import * as Middlewares from '../middlewares';
|
||||
import Config from '../Config';
|
||||
|
||||
const rack = hat.rack();
|
||||
import { randomHexString } from '../cryptoUtils';
|
||||
|
||||
export class FilesController {
|
||||
constructor(filesAdapter) {
|
||||
@@ -61,7 +59,7 @@ export class FilesController {
|
||||
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(() => {
|
||||
res.status(201);
|
||||
var location = this._filesAdapter.getFileLocation(req.config, filename);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
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 GCMRegistrationTokensMax = 1000;
|
||||
@@ -22,10 +22,7 @@ function GCM(args) {
|
||||
* @returns {Object} A promise which is resolved after we get results from gcm
|
||||
*/
|
||||
GCM.prototype.send = function(data, devices) {
|
||||
let pushId = randomstring.generate({
|
||||
length: 10,
|
||||
charset: 'alphanumeric'
|
||||
});
|
||||
let pushId = cryptoUtils.newObjectId();
|
||||
let timeStamp = Date.now();
|
||||
let expirationTime;
|
||||
// We handle the expiration_time convertion in push.js, so expiration_time is a valid date
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
// that writes to the database.
|
||||
// This could be either a "create" or an "update".
|
||||
|
||||
var crypto = require('crypto');
|
||||
var deepcopy = require('deepcopy');
|
||||
var rack = require('hat').rack();
|
||||
|
||||
var Auth = require('./Auth');
|
||||
var cache = require('./cache');
|
||||
var Config = require('./Config');
|
||||
var cryptoUtils = require('./cryptoUtils');
|
||||
var passwordCrypto = require('./password');
|
||||
var facebook = require('./facebook');
|
||||
var Parse = require('parse/node');
|
||||
@@ -56,7 +55,7 @@ function RestWrite(config, auth, className, query, data, originalData) {
|
||||
this.data.updatedAt = this.updatedAt;
|
||||
if (!this.query) {
|
||||
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,
|
||||
'this auth is already used');
|
||||
} else {
|
||||
this.data.username = rack();
|
||||
this.data.username = cryptoUtils.newToken();
|
||||
}
|
||||
|
||||
// This FB auth does not already exist, so transform it to a
|
||||
@@ -273,7 +272,7 @@ RestWrite.prototype.transformUser = function() {
|
||||
var promise = Promise.resolve();
|
||||
|
||||
if (!this.query) {
|
||||
var token = 'r:' + rack();
|
||||
var token = 'r:' + cryptoUtils.newToken();
|
||||
this.storage['token'] = token;
|
||||
promise = promise.then(() => {
|
||||
var expiresAt = new Date();
|
||||
@@ -319,7 +318,7 @@ RestWrite.prototype.transformUser = function() {
|
||||
// Check for username uniqueness
|
||||
if (!this.data.username) {
|
||||
if (!this.query) {
|
||||
this.data.username = newStringId(25);
|
||||
this.data.username = cryptoUtils.randomString(25);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -412,7 +411,7 @@ RestWrite.prototype.handleSession = function() {
|
||||
}
|
||||
|
||||
if (!this.query && !this.auth.isMaster) {
|
||||
var token = 'r:' + rack();
|
||||
var token = 'r:' + cryptoUtils.newToken();
|
||||
var expiresAt = new Date();
|
||||
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
|
||||
var sessionData = {
|
||||
@@ -713,20 +712,4 @@ RestWrite.prototype.objectId = function() {
|
||||
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;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// These methods handle the User-related routes.
|
||||
|
||||
import hat from 'hat';
|
||||
import deepcopy from 'deepcopy';
|
||||
|
||||
import ClassesRouter from './ClassesRouter';
|
||||
@@ -9,8 +8,7 @@ import rest from '../rest';
|
||||
import Auth from '../Auth';
|
||||
import passwordCrypto from '../password';
|
||||
import RestWrite from '../RestWrite';
|
||||
|
||||
const rack = hat.rack();
|
||||
import { newToken } from '../cryptoUtils';
|
||||
|
||||
export class UsersRouter extends ClassesRouter {
|
||||
handleFind(req) {
|
||||
@@ -89,7 +87,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
|
||||
}
|
||||
|
||||
let token = 'r:' + rack();
|
||||
let token = 'r:' + newToken();
|
||||
user.sessionToken = token;
|
||||
delete user.password;
|
||||
|
||||
|
||||
44
src/cryptoUtils.js
Normal file
44
src/cryptoUtils.js
Normal 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);
|
||||
}
|
||||
@@ -3,13 +3,13 @@
|
||||
var express = require('express'),
|
||||
cache = require('./cache'),
|
||||
middlewares = require('./middlewares'),
|
||||
rack = require('hat').rack();
|
||||
cryptoUtils = require('./cryptoUtils');
|
||||
|
||||
var router = express.Router();
|
||||
|
||||
// creates a unique app in the cache, with a collection prefix
|
||||
function createApp(req, res) {
|
||||
var appId = rack();
|
||||
var appId = cryptoUtils.randomHexString(32);
|
||||
cache.apps[appId] = {
|
||||
'collectionPrefix': appId + '_',
|
||||
'masterKey': 'master'
|
||||
@@ -70,4 +70,4 @@ router.post('/rest_configure_app',
|
||||
|
||||
module.exports = {
|
||||
router: router
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user