brightspace-auth-keys
Advanced tools
Comparing version 6.0.2 to 7.0.0
{ | ||
"name": "brightspace-auth-keys", | ||
"version": "6.0.2", | ||
"version": "7.0.0", | ||
"description": "Library for generating, storing, and retrieving keypairs for use in Brightspace's auth framework.", | ||
"files": [ | ||
"src", | ||
"errors", | ||
"README.md", | ||
"LICENSE" | ||
], | ||
"main": "src/index.js", | ||
"scripts": { | ||
"test": "nyc --all --produce-source-map --require source-map-support mocha --recursive ./test" | ||
"test": "nyc --all --produce-source-map --require source-map-support/register mocha --recursive ./test" | ||
}, | ||
@@ -17,9 +23,12 @@ "author": "D2L Corporation", | ||
"type": "git", | ||
"url": "git+ssh://git@github.com/Brightspace/node-auth.git" | ||
"url": "git+ssh://git@github.com/Brightspace/node-auth.git", | ||
"directory": "packages/node_modules/brightspace-auth-keys" | ||
}, | ||
"engines": { | ||
"node": ">=10.12.0" | ||
}, | ||
"dependencies": { | ||
"jwk-to-pem": "^2.0.0", | ||
"native-crypto": "^1.7.2", | ||
"@trust/keyto": "^0.3.5", | ||
"uuid": "^3.1.0" | ||
} | ||
} |
@@ -95,10 +95,6 @@ # brightspace-auth-keys | ||
signingKeyType: 'EC', // A type of signing keys to generate. 'RSA' or 'EC'. REQUIRED | ||
signingKeyAge: 3600, // Length of time, in seconds, for a private key to remain in use | ||
signingKeyOverlap: 300, // Length of time, in seconds, for a public key to remain valid | ||
// after its private key has been rotated out. This is effectively | ||
// the maximum lifetime of a signed token. | ||
// RSA-specific settings: | ||
rsa: { | ||
signingKeySize: 2048 // RSA key size, in bits | ||
lifetimes: { | ||
keyUse: 3600, // Length of time, in seconds, for a private key to remain in use | ||
token: 300 // Max length of time, in seconds, that a signed token will remain valid | ||
}, | ||
@@ -111,2 +107,7 @@ | ||
// RSA-specific settings: | ||
rsa: { | ||
signingKeySize: 2048 // RSA key size, in bits | ||
}, | ||
publicKeyStore: new RedisPublicKeyStore(...) // A backend for storing public keys. | ||
@@ -113,0 +114,0 @@ // Can be anything: Redis, MSSQL, PostgreSQL, etc. |
@@ -8,16 +8,2 @@ 'use strict'; | ||
function arrayOrEmpty(arr) { | ||
return Array.isArray(arr) | ||
? arr | ||
: []; | ||
} | ||
function parseKeys(keys) { | ||
return keys.map(JSON.parse); | ||
} | ||
function filterExpiredKeys(keys) { | ||
return keys.filter(isNotExpired); | ||
} | ||
function isNotExpired(key) { | ||
@@ -33,29 +19,26 @@ return key.exp >= clock(); | ||
lookupPublicKeys() { | ||
return Promise | ||
.resolve() | ||
.then(() => this._lookupPublicKeys()) | ||
.then(arrayOrEmpty) | ||
.then(parseKeys) | ||
.then(filterExpiredKeys); | ||
async lookupPublicKeys() { | ||
let keys = await this._lookupPublicKeys(); | ||
keys = Array.isArray(keys) ? keys : []; | ||
keys = keys.map(JSON.parse); | ||
keys = keys.filter(isNotExpired); | ||
return keys; | ||
} | ||
lookupPublicKey(kid) { | ||
return this | ||
.lookupPublicKeys() | ||
.then(keys => { | ||
for (const key of keys) { | ||
if (key.kid === kid) { | ||
return key; | ||
} | ||
} | ||
async lookupPublicKey(kid) { | ||
const keys = await this.lookupPublicKeys(); | ||
throw new PublicKeyNotFoundError(kid); | ||
}); | ||
for (const key of keys) { | ||
if (key.kid === kid) { | ||
return key; | ||
} | ||
} | ||
throw new PublicKeyNotFoundError(kid); | ||
} | ||
storePublicKey(key) { | ||
return Promise | ||
.resolve() | ||
.then(() => this._storePublicKey(JSON.stringify(key), key.exp)); | ||
async storePublicKey(key) { | ||
return this._storePublicKey(JSON.stringify(key), key.exp); | ||
} | ||
@@ -62,0 +45,0 @@ } |
@@ -14,86 +14,42 @@ 'use strict'; | ||
const DEFAULT_SIGNING_KEY_AGE = 60 * 60; | ||
const MINIMUM_SIGNING_KEY_AGE = 60 * 60; | ||
const MAXIMUM_SIGNING_KEY_AGE = 24 * 60 * 60; | ||
const DEFAULT_SIGNING_KEY_OVERLAP = 5 * 60; | ||
const MINIMUM_SIGNING_KEY_OVERLAP = 5 * 60; | ||
/* @this */ | ||
function parseOpts(opts) { | ||
if (typeof opts !== 'object') { | ||
throw new TypeError(`"opts" should be an Object. Got "${typeof opts}".`); | ||
} | ||
if (!(opts.publicKeyStore instanceof AbstractPublicKeyStore)) { | ||
throw new TypeError('"opts.publicKeyStore" should be an implementation of AbstractPublicKeyStore'); | ||
} | ||
this._publicKeyStore = opts.publicKeyStore; | ||
if (typeof opts.signingKeyAge !== 'undefined') { | ||
const age = opts.signingKeyAge; | ||
if (typeof age !== 'number' || age !== Math.round(age)) { | ||
throw new TypeError(`"opts.signingKeyAge" should be a integer. Got "${age}" (${typeof age}).`); | ||
class CoreKeyGenerator { | ||
constructor(opts) { | ||
if (typeof opts !== 'object') { | ||
throw new TypeError(`"opts" should be an Object. Got "${typeof opts}".`); | ||
} | ||
if (age < MINIMUM_SIGNING_KEY_AGE || MAXIMUM_SIGNING_KEY_AGE < age) { | ||
throw new Error(`"opts.signingKeyAge" must be between ${MINIMUM_SIGNING_KEY_AGE} and ${MAXIMUM_SIGNING_KEY_AGE}. Got "${age}".`); | ||
if (!(opts.publicKeyStore instanceof AbstractPublicKeyStore)) { | ||
throw new TypeError('"opts.publicKeyStore" should be an implementation of AbstractPublicKeyStore'); | ||
} | ||
this._signingKeyAge = age; | ||
} | ||
this._publicKeyStore = opts.publicKeyStore; | ||
if (typeof opts.signingKeyOverlap !== 'undefined') { | ||
const overlap = opts.signingKeyOverlap; | ||
if (typeof overlap !== 'number' || overlap !== Math.round(overlap)) { | ||
throw new TypeError(`"opts.signingKeyOverlap" should be a integer. Got "${overlap}" (${typeof overlap}).`); | ||
switch (opts.signingKeyType) { | ||
case SIGNING_KEY_TYPE_RSA: { | ||
this.keygen = rsaKeygen.bind(null, rsaKeygen.normalize(opts.rsa)); | ||
break; | ||
} | ||
case SIGNING_KEY_TYPE_EC: { | ||
this.keygen = ecKeygen.bind(null, ecKeygen.normalize(opts.ec)); | ||
break; | ||
} | ||
default: { | ||
throw new Error(`signingKeyType must be one of: "${SIGNING_KEY_TYPE_RSA}", "${SIGNING_KEY_TYPE_EC}"`); | ||
} | ||
} | ||
} | ||
if (overlap < MINIMUM_SIGNING_KEY_OVERLAP) { | ||
throw new Error(`"opts.signingKeyOverlap" must be at least ${MINIMUM_SIGNING_KEY_OVERLAP}. Got "${overlap}".`); | ||
} | ||
async generateNewKey(exp = clock() + DEFAULT_SIGNING_KEY_AGE) { | ||
const key = await this.keygen(uuid()); | ||
if (this._signingKeyAge < overlap) { | ||
throw new Error(`"opts.signingKeyOverlap" must be less than "opts.signingKeyAge" (${this._signingKeyAge}). Got "${overlap}".`); | ||
} | ||
key.jwk.exp = exp + EXPIRY_CLOCK_SKEW; | ||
this._signingKeyOverlap = overlap; | ||
} | ||
await this._publicKeyStore.storePublicKey(key.jwk); | ||
switch (opts.signingKeyType) { | ||
case SIGNING_KEY_TYPE_RSA: { | ||
this.keygen = rsaKeygen.bind(null, rsaKeygen.normalize(opts.rsa)); | ||
break; | ||
} | ||
case SIGNING_KEY_TYPE_EC: { | ||
this.keygen = ecKeygen.bind(null, ecKeygen.normalize(opts.ec)); | ||
break; | ||
} | ||
default: { | ||
throw new Error(`signingKeyType must be one of: "${SIGNING_KEY_TYPE_RSA}", "${SIGNING_KEY_TYPE_EC}"`); | ||
} | ||
return key.signingKey; | ||
} | ||
} | ||
function CoreKeyGenerator(opts) { | ||
this._signingKeyAge = DEFAULT_SIGNING_KEY_AGE; | ||
this._signingKeyOverlap = DEFAULT_SIGNING_KEY_OVERLAP; | ||
this._publicKeyStore = null; | ||
parseOpts.call(this, opts); | ||
} | ||
CoreKeyGenerator.prototype.generateNewKeys = function generateNewKeys() { | ||
return this | ||
.keygen(uuid()) | ||
.then(key => { | ||
key.jwk.exp = clock() + this._signingKeyAge + this._signingKeyOverlap + EXPIRY_CLOCK_SKEW; | ||
return this | ||
._publicKeyStore | ||
.storePublicKey(key.jwk) | ||
.then(() => key.signingKey); | ||
}); | ||
}; | ||
module.exports = CoreKeyGenerator; |
'use strict'; | ||
const jwkToPem = require('jwk-to-pem'); | ||
const generate = require('native-crypto/generate'); | ||
const { generateKeyPair, createPublicKey } = require('crypto'); | ||
const keyto = require('@trust/keyto'); | ||
const DEFAULT_CRV = 'P-256'; | ||
@@ -14,25 +15,41 @@ | ||
const SUPPORTS_KEY_OBJECTS = typeof createPublicKey === 'function'; | ||
const PUBLIC_EXPORT_OPTIONS = { | ||
type: 'spki', | ||
format: 'pem' | ||
}; | ||
const PRIVATE_EXPORT_OPTIONS = SUPPORTS_KEY_OBJECTS | ||
? null | ||
: { | ||
type: 'pkcs8', | ||
format: 'pem' | ||
}; | ||
function keygen(opts, kid) { | ||
return generate(opts.crv) | ||
.then(keypair => { | ||
return Promise | ||
.all([ | ||
keypair.publicKey, | ||
jwkToPem(keypair.privateKey, { private: true }) | ||
]); | ||
}) | ||
return new Promise((resolve, reject) => { | ||
generateKeyPair('ec', { | ||
namedCurve: opts.crv, | ||
publicKeyEncoding: PUBLIC_EXPORT_OPTIONS, | ||
privateKeyEncoding: PRIVATE_EXPORT_OPTIONS | ||
}, (err, publicKey, privateKey) => { | ||
if (err) { | ||
return reject(err); | ||
} | ||
resolve([ | ||
publicKey, | ||
privateKey | ||
]); | ||
}); | ||
}) | ||
.then(res => { | ||
return { | ||
jwk: { | ||
jwk: Object.assign(keyto.from(res[0], 'pem').toJwk('public'), { | ||
kid, | ||
kty: res[0].kty, | ||
crv: res[0].crv, | ||
x: res[0].x, | ||
y: res[0].y, | ||
alg: CRV_TO_ALG[opts.crv], | ||
use: 'sig' | ||
}, | ||
}), | ||
signingKey: { | ||
kid, | ||
pem: res[1], | ||
key: res[1], | ||
alg: CRV_TO_ALG[opts.crv] | ||
@@ -39,0 +56,0 @@ } |
'use strict'; | ||
const EventEmitter = require('events'); | ||
const CoreKeyGenerator = require('./core-key-generator'); | ||
const clock = require('./clock'); | ||
function KeyGenerator(opts) { | ||
if (typeof opts !== 'object') { | ||
throw new TypeError(`"opts" should be an Object. Got "${typeof opts}".`); | ||
} | ||
this._coreKeyGenerator = new CoreKeyGenerator(opts); | ||
this._keyGenerationTask = null; | ||
this._currentPrivateKey = null; | ||
this._generateNewKeys = this._generateNewKeys.bind(this); | ||
this._generateNewKeys(); | ||
setInterval(this._generateNewKeys, this._coreKeyGenerator._signingKeyAge * 1000); | ||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms).unref()); | ||
function createBackoff() { | ||
let current = 100; | ||
const max = current * (2 ** 8); | ||
return () => { | ||
const tmp = current; | ||
if (current < max) { | ||
current *= 2; | ||
} | ||
return delay(tmp); | ||
}; | ||
} | ||
KeyGenerator.prototype._generateNewKeys = function _generateNewKeys() { | ||
this._keyGenerationTask = this | ||
._coreKeyGenerator.generateNewKeys() | ||
.then(privateKey => { | ||
this._currentPrivateKey = privateKey; | ||
this._keyGenerationTask = undefined; | ||
return privateKey; | ||
}) | ||
.catch(() => { | ||
return new Promise(resolve => setTimeout(resolve, 100).unref()) | ||
.then(this._generateNewKeys); | ||
}); | ||
const DEFAULT_SIGNING_KEY_USE = 60 * 60; | ||
const MINIMUM_SIGNING_KEY_USE = 60 * 60; | ||
const MAXIMUM_SIGNING_KEY_USE = 23 * 60 * 60; | ||
return this._keyGenerationTask; | ||
}; | ||
const DEFAULT_MAX_TOKEN_LIFETIME = 5 * 60; | ||
const MINIMUM_MAX_TOKEN_LIFETIME = 5 * 60; | ||
KeyGenerator.prototype.getCurrentPrivateKey = function getCurrentPrivateKey() { | ||
return this._keyGenerationTask | ||
? this._keyGenerationTask | ||
: Promise.resolve(this._currentPrivateKey); | ||
}; | ||
class KeyGenerator extends EventEmitter { | ||
constructor(opts) { | ||
if (typeof opts !== 'object') { | ||
throw new TypeError(`"opts" should be an Object. Got "${typeof opts}".`); | ||
} | ||
super(); | ||
this._core = new CoreKeyGenerator(opts); | ||
this._keyUseLifetime = DEFAULT_SIGNING_KEY_USE; | ||
this._tokenLifetime = DEFAULT_MAX_TOKEN_LIFETIME; | ||
if (typeof opts.lifetimes !== 'undefined') { | ||
const lifetimes = opts.lifetimes; | ||
if (typeof lifetimes !== 'object') { | ||
throw new TypeError(`"opts.lifetimes" should be an object. Got "${lifetimes}" (${typeof lifetimes}).`); | ||
} | ||
if (typeof lifetimes.keyUse !== 'undefined') { | ||
const keyUse = opts.keyUse; | ||
if (typeof keyUse !== 'number' || keyUse !== Math.round(keyUse)) { | ||
throw new TypeError(`"opts.lifetimes.keyUse" should be an integer. Got "${keyUse}" (${typeof keyUse}).`); | ||
} | ||
if (keyUse < MINIMUM_SIGNING_KEY_USE || MAXIMUM_SIGNING_KEY_USE < keyUse) { | ||
throw new Error(`"opts.lifetimes.keyUse" must be between ${MINIMUM_SIGNING_KEY_USE} and ${MAXIMUM_SIGNING_KEY_USE}. Got "${keyUse}".`); | ||
} | ||
this._keyUseLifetime = keyUse; | ||
} | ||
if (typeof lifetimes.token !== 'undefined') { | ||
const token = lifetimes.token; | ||
if (typeof token !== 'number' || token !== Math.round(token)) { | ||
throw new TypeError(`"opts.lifetimes.token" should be an integer. Got "${token}" (${typeof token}).`); | ||
} | ||
if (token < MINIMUM_MAX_TOKEN_LIFETIME) { | ||
throw new Error(`"opts.lifetimes.token" must be at least ${MINIMUM_MAX_TOKEN_LIFETIME}. Got "${token}".`); | ||
} | ||
this._tokenLifetime = token; | ||
} | ||
} | ||
this._current = null; | ||
this._next = this._ensureGenerated(this._keyUseLifetime); | ||
this._rotate(); | ||
} | ||
async _rotate() { | ||
this._current = this._next; | ||
const { keyUseExpiry } = await this._current; | ||
this._next = this._ensureGenerated(this._keyUseLifetime * 2); | ||
await this._next; | ||
setTimeout(() => this._rotate(), (keyUseExpiry - clock()) * 1000).unref(); | ||
} | ||
async _ensureGenerated(keyUseLifetime) { | ||
const backoff = createBackoff(); | ||
for (;;) { | ||
try { | ||
const exp = clock() + keyUseLifetime + this._tokenLifetime; | ||
const keyUseExpiry = exp - this._tokenLifetime; | ||
const key = await this._core.generateNewKey(exp); | ||
return { key, keyUseExpiry }; | ||
} catch (err) { | ||
this.emit('error', err); | ||
await backoff(); | ||
} | ||
} | ||
} | ||
async getCurrentPrivateKey() { | ||
const { key, keyUseExpiry } = await this._current; | ||
if (clock() > keyUseExpiry) { | ||
throw new Error('Failed to generate a key before it expired'); | ||
} | ||
return key; | ||
} | ||
} | ||
module.exports = KeyGenerator; |
'use strict'; | ||
const jwkToPem = require('jwk-to-pem'); | ||
const generate = require('native-crypto/generate'); | ||
const { generateKeyPair, createPublicKey } = require('crypto'); | ||
const keyto = require('@trust/keyto'); | ||
const DEFAULT_SIZE = 2048; | ||
const MINIMUM_SIZE = 2048; | ||
const SUPPORTS_KEY_OBJECTS = typeof createPublicKey === 'function'; | ||
const PUBLIC_EXPORT_OPTIONS = { | ||
type: 'spki', | ||
format: 'pem' | ||
}; | ||
const PRIVATE_EXPORT_OPTIONS = SUPPORTS_KEY_OBJECTS | ||
? null | ||
: { | ||
type: 'pkcs8', | ||
format: 'pem' | ||
}; | ||
function keygen(opts, kid) { | ||
return generate('RS256', opts.size) | ||
.then(keypair => { | ||
return Promise | ||
.all([ | ||
keypair.publicKey, | ||
jwkToPem(keypair.privateKey, { private: true }) | ||
]); | ||
}) | ||
return new Promise((resolve, reject) => { | ||
generateKeyPair('rsa', { | ||
modulusLength: opts.size, | ||
publicKeyEncoding: PUBLIC_EXPORT_OPTIONS, | ||
privateKeyEncoding: PRIVATE_EXPORT_OPTIONS | ||
}, (err, publicKey, privateKey) => { | ||
if (err) { | ||
return reject(err); | ||
} | ||
resolve([ | ||
publicKey, | ||
privateKey | ||
]); | ||
}); | ||
}) | ||
.then(res => { | ||
return { | ||
jwk: { | ||
jwk: Object.assign(keyto.from(res[0], 'pem').toJwk('public'), { | ||
kid, | ||
kty: res[0].kty, | ||
n: res[0].n, | ||
e: res[0].e, | ||
alg: 'RS256', | ||
use: 'sig' | ||
}, | ||
}), | ||
signingKey: { | ||
kid, | ||
pem: res[1], | ||
key: res[1], | ||
alg: 'RS256' | ||
@@ -32,0 +50,0 @@ } |
24938
2
322
118
+ Added@trust/keyto@^0.3.5
+ Added@trust/keyto@0.3.7(transitive)
+ Addedbase64url@3.0.1(transitive)
- Removedjwk-to-pem@^2.0.0
- Removednative-crypto@^1.7.2
- Removedasn1.js@4.10.1(transitive)
- Removedbindings@1.5.0(transitive)
- Removedbn.js@5.2.1(transitive)
- Removedbrowserify-aes@1.2.0(transitive)
- Removedbrowserify-rsa@4.1.1(transitive)
- Removedbrowserify-sign@4.2.3(transitive)
- Removedbuffer-equal-constant-time@1.0.1(transitive)
- Removedbuffer-xor@1.0.3(transitive)
- Removedcipher-base@1.0.6(transitive)
- Removedcore-util-is@1.0.3(transitive)
- Removedcreate-ecdh@4.0.4(transitive)
- Removedcreate-hash@1.2.0(transitive)
- Removedcreate-hmac@1.1.7(transitive)
- Removeddebug@2.6.9(transitive)
- Removedecdsa-sig-formatter@1.0.11(transitive)
- Removedevp_bytestokey@1.0.3(transitive)
- Removedfile-uri-to-path@1.0.0(transitive)
- Removedhash-base@3.0.5(transitive)
- Removedisarray@1.0.0(transitive)
- Removedjwk-to-pem@1.2.62.0.7(transitive)
- Removedmd5.js@1.3.5(transitive)
- Removedmiller-rabin@4.0.1(transitive)
- Removedms@2.0.0(transitive)
- Removednan@2.22.02.3.5(transitive)
- Removednative-crypto@1.8.1(transitive)
- Removedparse-asn1@5.1.7(transitive)
- Removedpbkdf2@3.1.2(transitive)
- Removedpemstrip@0.0.1(transitive)
- Removedprocess-nextick-args@2.0.1(transitive)
- Removedpublic-encrypt@4.0.3(transitive)
- Removedrandombytes@2.1.0(transitive)
- Removedraw-ecdsa@1.1.1(transitive)
- Removedreadable-stream@2.3.8(transitive)
- Removedripemd160@2.0.2(transitive)
- Removedrsa-keygen@1.0.6(transitive)
- Removedsafe-buffer@5.1.25.2.1(transitive)
- Removedsha.js@2.4.11(transitive)
- Removedstring_decoder@1.1.1(transitive)
- Removedutil-deprecate@1.0.2(transitive)