Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solid/keychain",
"version": "0.3.5",
"version": "0.4.0",
"description": "KeyChain for use with Web Cryptography API in Node.js",
"main": "src/index.js",
"directories": {
Expand Down Expand Up @@ -38,7 +38,7 @@
},
"homepage": "https://github.com/solid/keychain#README",
"dependencies": {
"@solid/jose": "^0.6.9",
"@solid/jose": "^0.7.0",
"base64url": "^3.0.1"
},
"devDependencies": {
Expand Down
60 changes: 47 additions & 13 deletions src/KeyChain.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Dependencies
*/
const supportedAlgorithms = require('./algorithms')
const InvalidDescriptorError = require('./errors/InvalidDescriptorError')

/**
* KeyChain
Expand All @@ -10,14 +11,21 @@ class KeyChain {

/**
* constructor
*
* @param data {Object} Keychain data
* @param options {Object} Optional configuration
* @param options.crypto {Object} Optional crypto instance for cross-package compatibility
*/
constructor (data) {
constructor (data, options = {}) {
// use data as the descriptor if descriptor property is missing
if (!data.descriptor) {
data = { descriptor: data }
}

Object.assign(this, data)

// Store crypto instance if provided
this._crypto = options.crypto
}

/**
Expand All @@ -34,14 +42,14 @@ class KeyChain {
* @param {Object} params
* @return {Promise}
*/
static generateKey (params) {
static generateKey (params, crypto) {
let normalizedAlgorithm = supportedAlgorithms.normalize('generateKey', params.alg)

if (normalizedAlgorithm instanceof Error) {
return Promise.reject(normalizedAlgorithm)
}

let algorithm = new normalizedAlgorithm(params)
let algorithm = new normalizedAlgorithm({...params, crypto})

return algorithm.generateKey()
}
Expand All @@ -53,24 +61,28 @@ class KeyChain {
* @param {Object} jwk
* @return {Promise}
*/
static importKey (jwk) {
static importKey (jwk, crypto) {
let {alg} = jwk
let normalizedAlgorithm = supportedAlgorithms.normalize('importKey', alg)

if (normalizedAlgorithm instanceof Error) {
return Promise.reject(normalizedAlgorithm)
}

let algorithm = new normalizedAlgorithm({alg})
let algorithm = new normalizedAlgorithm({alg, crypto})

return algorithm.importKey(jwk)
}

/**
* restore
*
* @param data {Object} Keychain data
* @param options {Object} Optional configuration
* @param options.crypto {Object} Optional crypto instance for cross-package compatibility
*/
static restore (data) {
let keys = new KeyChain(data)
static restore (data, options) {
let keys = new KeyChain(data, options)
return keys.importKeys().then(() => keys)
}

Expand All @@ -84,22 +96,44 @@ class KeyChain {

// import key
if (props.includes('alg')) {
return KeyChain.importKey(object).then(cryptoKey => {
if (cryptoKey.type === 'private') {
return KeyChain.importKey(object, this._crypto).then(cryptoKey => {
if (cryptoKey.type === 'private' && !container.privateKey) {
Object.defineProperty(container, 'privateKey', {
enumerable: false,
value: cryptoKey
})
}

if (cryptoKey.type === 'public') {
if (cryptoKey.type === 'public' && !container.publicKey) {
Object.defineProperty(container, 'publicKey', {
enumerable: false,
value: cryptoKey
})
}
})

// import key pair structure (has privateJwk and publicJwk)
} else if (props.includes('privateJwk') && props.includes('publicJwk')) {
// Import both private and public keys
return Promise.all([
KeyChain.importKey(object.privateJwk, this._crypto),
KeyChain.importKey(object.publicJwk, this._crypto)
]).then(([privateKey, publicKey]) => {
if (!object.privateKey) {
Object.defineProperty(object, 'privateKey', {
enumerable: false,
value: privateKey
})
}

if (!object.publicKey) {
Object.defineProperty(object, 'publicKey', {
enumerable: false,
value: publicKey
})
}
})

// recurse
} else {
return Promise.all(
Expand All @@ -109,7 +143,7 @@ class KeyChain {
let subProps = Object.keys(subObject)
//console.log('RECURSE WITH', name, subDescriptor, subObject, subProps)

this.importKeys({
return this.importKeys({
descriptor: subDescriptor,
object: subObject,
container: object,
Expand Down Expand Up @@ -141,7 +175,7 @@ class KeyChain {
// generate key(pair), assign resulting object to keychain,
// and add JWK for public key to JWK Set
if (params.alg) {
return KeyChain.generateKey(params).then(result => {
return KeyChain.generateKey(params, this._crypto).then(result => {
container[key] = result

if (result.publicJwk) {
Expand All @@ -164,7 +198,7 @@ class KeyChain {

// invalid descriptor
} else {
throw new InvalidDescriptorError(key, value)
throw new InvalidDescriptorError(key, params)
}
})
).then(() => {
Expand Down
131 changes: 131 additions & 0 deletions src/algorithms/EcKeyPair.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Dependencies
*/
const { crypto } = require('@solid/jose')
const base64url = require('base64url')

/**
* EcKeyPair
*/
class EcKeyPair {

/**
* constructor
*
* @param params {Object} Options hashmap
* @param params.alg {string} For example, 'ES256', 'ES384', 'ES512'
* @param params.namedCurve {string} For example, 'P-256', 'P-384', 'P-521'
* @param params.usages {Array<string>}
* @param params.crypto {Object} Optional crypto instance to use (for cross-package compatibility)
*/
constructor (params) {
let name = 'ECDSA'
let {alg, namedCurve, usages} = params

// Allow overriding crypto instance for cross-package compatibility
this.crypto = params.crypto || crypto

// Map algorithm to curve and hash
let algorithmMap = {
'ES256': { curve: 'P-256', hash: 'SHA-256' },
'ES384': { curve: 'P-384', hash: 'SHA-384' },
'ES512': { curve: 'P-521', hash: 'SHA-512' } // Note: P-521, not P-512
}

let algConfig = algorithmMap[alg]

if (!algConfig) {
throw new Error(`Unsupported EC algorithm: ${alg}`)
}

// Use provided namedCurve or default from algorithm
if (!namedCurve) {
namedCurve = algConfig.curve
}

let hash = { name: algConfig.hash }

if (!usages) {
usages = ['sign', 'verify']
}

this.alg = alg
this.algorithm = {name, namedCurve, hash}
this.extractable = true
this.usages = usages
}

/**
* generateKey
*/
generateKey () {
let {algorithm, extractable, usages} = this

return this.crypto.subtle
.generateKey(algorithm, extractable, usages)
.then(this.setCryptoKeyPair)
.then(result => this.setJwkKeyPair(result))
}

/**
* importKey
*/
importKey (jwk) {
let {name, namedCurve, hash} = this.algorithm
let algorithm = {name, namedCurve, hash}
let extractable = true
let usages = jwk.key_ops

return this.crypto.subtle
.importKey('jwk', jwk, algorithm, extractable, usages)
}

/**
* setCryptoKeyPair
*/
setCryptoKeyPair (cryptoKeyPair) {
let result = {}

Object.defineProperty(result, 'privateKey', {
enumerable: false,
value: cryptoKeyPair.privateKey
})

Object.defineProperty(result, 'publicKey', {
enumerable: false,
value: cryptoKeyPair.publicKey
})

return result
}

/**
* setJwkKeyPair
*/
setJwkKeyPair (result) {
return Promise.all([
this.crypto.subtle.exportKey('jwk', result.privateKey),
this.crypto.subtle.exportKey('jwk', result.publicKey)
])
.then(jwks => {
let [privateJwk, publicJwk] = jwks

result.privateJwk = Object.assign({
kid: base64url(Buffer.from(this.crypto.getRandomValues(new Uint8Array(8)))),
alg: this.alg
}, privateJwk)

result.publicJwk = Object.assign({
kid: base64url(Buffer.from(this.crypto.getRandomValues(new Uint8Array(8)))),
alg: this.alg
}, publicJwk)

return result
})
}
}

/**
* Export
*/
module.exports = EcKeyPair
18 changes: 11 additions & 7 deletions src/algorithms/RsaKeyPair.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ class RsaKeyPair {
* @param params.modulusLength {number}
* @param params.publicExponent {BufferSource} For example, a Uint8Array
* @param params.usages {Array<string>}
* @param params.crypto {Object} Optional crypto instance to use (for cross-package compatibility)
*/
constructor (params) {
let name = 'RSASSA-PKCS1-v1_5'
let {alg, modulusLength, publicExponent, usages} = params
let hashLengthValid = alg.match(/(256|384|512)$/)
let hashLength = hashLengthValid && hashLengthValid.shift()
let hash = { name: `SHA-${hashLength}` }

// Allow overriding crypto instance for cross-package compatibility
this.crypto = params.crypto || crypto

if (!hashLength) {
throw new Error('Invalid hash length')
Expand Down Expand Up @@ -52,10 +56,10 @@ class RsaKeyPair {
generateKey () {
let {algorithm, extractable, usages} = this

return crypto.subtle
return this.crypto.subtle
.generateKey(algorithm, extractable, usages)
.then(this.setCryptoKeyPair)
.then(this.setJwkKeyPair)
.then((result) => this.setJwkKeyPair(result))
}

/**
Expand All @@ -67,7 +71,7 @@ class RsaKeyPair {
let extractable = true
let usages = jwk.key_ops

return crypto.subtle
return this.crypto.subtle
.importKey('jwk', jwk, algorithm, extractable, usages)
}

Expand Down Expand Up @@ -95,18 +99,18 @@ class RsaKeyPair {
*/
setJwkKeyPair (result) {
return Promise.all([
crypto.subtle.exportKey('jwk', result.privateKey),
crypto.subtle.exportKey('jwk', result.publicKey)
this.crypto.subtle.exportKey('jwk', result.privateKey),
this.crypto.subtle.exportKey('jwk', result.publicKey)
])
.then(jwks => {
let [privateJwk, publicJwk] = jwks

result.privateJwk = Object.assign({
kid: base64url(Buffer.from(crypto.getRandomValues(new Uint8Array(8))))
kid: base64url(Buffer.from(this.crypto.getRandomValues(new Uint8Array(8))))
}, privateJwk)

result.publicJwk = Object.assign({
kid: base64url(Buffer.from(crypto.getRandomValues(new Uint8Array(8))))
kid: base64url(Buffer.from(this.crypto.getRandomValues(new Uint8Array(8))))
}, publicJwk)

return result
Expand Down
Loading