diff --git a/ghost/security/index.js b/ghost/security/index.js index f28289d4a3..b4ef42a7f3 100644 --- a/ghost/security/index.js +++ b/ghost/security/index.js @@ -17,5 +17,9 @@ module.exports = { get password() { return require('./lib/password'); + }, + + get secret() { + return require('./lib/secret'); } }; diff --git a/ghost/security/lib/identifier.js b/ghost/security/lib/identifier.js index 31f0c7a558..f8dcc9f004 100644 --- a/ghost/security/lib/identifier.js +++ b/ghost/security/lib/identifier.js @@ -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 = []; diff --git a/ghost/security/lib/secret.js b/ghost/security/lib/secret.js new file mode 100644 index 0000000000..205e03b93c --- /dev/null +++ b/ghost/security/lib/secret.js @@ -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); +}; diff --git a/ghost/security/test/secret.test.js b/ghost/security/test/secret.test.js new file mode 100644 index 0000000000..a7aa588e85 --- /dev/null +++ b/ghost/security/test/secret.test.js @@ -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]+/); + }); +});