Added secret.create util to security package

- this utility existed twice in the ghost codebase:
   - f6fb823ce9/core/server/models/api-key.js (L24)
   - f6fb823ce9/core/server/data/migrations/versions/4.0/22-solve-orphaned-webhooks.js (L7)
- We also potentially need it for a second migration use case
- so moved it here, made it slightly more generic and also deprecated identifier.uid in favour of using this method as they do the same thing, but secret.create uses crypto properly
This commit is contained in:
Hannah Wolfe 2022-05-06 15:04:23 +01:00
parent 877fdc7bfe
commit 3a7613a46e
4 changed files with 79 additions and 2 deletions

View File

@ -17,5 +17,9 @@ module.exports = {
get password() {
return require('./lib/password');
},
get secret() {
return require('./lib/secret');
}
};

View File

@ -1,6 +1,5 @@
let _private = {};
// @TODO: replace with crypto.randomBytes
_private.getRandomInt = function (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
@ -8,9 +7,9 @@ _private.getRandomInt = function (min, max) {
/**
* Return a unique identifier with the given `len`.
*
* @deprecated use secret.create() instead
* @param {Number} maxLength
* @return {String}
* @api private
*/
module.exports.uid = function uid(maxLength) {
const buf = [];

View File

@ -0,0 +1,40 @@
const crypto = require('crypto');
/*
* Uses birthday problem estimation to calculate chance of collision
* d = 16^26 // 26 char hex string
* n = 10,000,000 // 10 million
*
* (-n x (n-1)) / 2d
* 1 - e^
*
*
* 17
* ~= 4 x 10^
*
* ref: https://medium.freecodecamp.org/how-long-should-i-make-my-api-key-833ebf2dc26f
* ref: https://en.wikipedia.org/wiki/Birthday_problem#Approximations
*
* 26 char hex string = 13 bytes (content api)
* 64 char hex string JWT secret = 32 bytes (admin api / default)
*
* @param {String|Number} [typeOrLength=64]
* @returns
*/
module.exports.create = (typeOrLength) => {
let bytes;
let length;
if (Number.isInteger(typeOrLength)) {
bytes = Math.ceil(typeOrLength / 2);
length = typeOrLength;
} else if (typeOrLength === 'content') {
bytes = 13;
length = 26;
} else {
bytes = 32;
length = 64;
}
return crypto.randomBytes(bytes).toString('hex').slice(0, length);
};

View File

@ -0,0 +1,34 @@
require('./utils');
const security = require('../');
describe('Lib: Security - Secret', function () {
it('generates a 13 byte secret if asked for a content secret', function () {
let secret = security.secret.create('content');
secret.should.be.a.String().with.lengthOf(13 * 2);
secret.should.match(/[0-9][a-z]+/);
});
it('generates a specific length secret if given a length', function () {
let secret = security.secret.create(10);
secret.should.be.a.String().with.lengthOf(10);
secret.should.match(/[0-9][a-z]+/);
});
it('generates a specific length secret if given a length even when odd', function () {
let secret = security.secret.create(15);
secret.should.be.a.String().with.lengthOf(15);
secret.should.match(/[0-9][a-z]+/);
});
it('generates a 32 byte secret if asked for an admin secret', function () {
let secret = security.secret.create('admin');
secret.should.be.a.String().with.lengthOf(32 * 2);
secret.should.match(/[0-9][a-z]+/);
});
it('generates a 32 byte secret by default', function () {
let secret = security.secret.create();
secret.should.be.a.String().with.lengthOf(32 * 2);
secret.should.match(/[0-9][a-z]+/);
});
});