'use strict'
/**
* Dependencies
* @ignore
*/
/**
* Module Dependencies
* @ignore
*/
const InvalidOperationError = require('./InvalidOperationError')
const OperationNotSupportedError = require('./OperationNotSupportedError')
const types = require('./types')
const asn = require('./asn1')
const EOL = RegExp('\r?\n', 'g');
/**
* JWK
*
* @typedef {Object} JWK
*
* @description
* A JWK Object
*/
/**
* SerializedFormat
*
* @typedef {String} SerializableFormat
*
* @description
* Available formats: 'jwk', 'pem', 'blk'.
*/
/**
* KeySelector
*
* @typedef {String} KeySelector
*
* @see SerializableFormat
* @see BufferFormat
*
* @description
* Available formats: 'public', 'private'.
*/
/**
* PEMKeySelector
*
* @typedef {String} PEMKeySelector
*
* @see SerializableFormat
* @see BufferFormat
*
* @description
* Available formats: 'public_pkcs1', 'public_pkcs8', 'private_pkcs1', 'private_pkcs8'.
*
* Note these refer specifically to different ASN encodings for PEM encoded keys
* and are not compatible with non-PEM output types.
*/
/**
* Key
* @ignore
*/
class Key {
/**
* constructor
*
* @class Key
*
* @internal For internal use only
*
* @description
* A high level class for accepting and processing keys
*
* @throws {InvalidOperationError} If key is omitted
*
* @throws {InvalidOperationError} If required options are omitted
*
* @param {Object} key
* @param {Object} options
* @param {SerializableFormat} options.format
* @param {String} options.kty - normalized key type name
* @param {String} [options.crv] - normalized curve name (EC & ED only)
* @param {String} [options.oid] - ASN oid algorithm descriptor
* @param {(KeySelector|PEMKeySelector)} options.selector
*/
constructor (key, options) {
if (!key) {
throw new InvalidOperationError('key is required')
}
if (!options) {
throw new InvalidOperationError('options are required')
}
let { kty, format, selector, crv, oid } = options
if (!options.kty) {
throw new InvalidOperationError('options.kty is required')
}
if (!options.format) {
throw new InvalidOperationError('options.format is required')
}
if (!options.selector) {
throw new InvalidOperationError('options.selector is required')
}
if (!options.crv && !options.oid) {
throw new InvalidOperationError('options.crv or options.oid is required')
}
this.kty = kty
this.format = format
this.selector = selector
this.crv = crv
this.oid = oid
this.key = this.parseKey(key)
}
/**
* from
*
* @description
* Import a key
*
* @example <caption>Decode PEM and convert to JWK</caption>
* const keyto = require('@trust/keyto')
*
* let pemPrivate = getPrivatePemStringSomehow()
* let jwk = getPublicJwkSomehow()
*
* // String data can either be passed in directly:
* let key = keyto.from(pemPrivate, 'pem').toJwk('public')
*
* // Or can be passed in as an object instead:
* let key = keyto.from({ key: pemPrivate }, 'pem').toJwk('public')
* assertEqual(jwk, key)
*
* @example <caption>Decode HEX (Blockchain) private key and convert to PEM PKCS8 public key</caption>
* const keyto = require('@trust/keyto')
*
* let blk = getPrivateBlockchainHexStringSomehow()
* let pemPublic = getPublicPemSomehow()
*
* let key = keyto.from(blk, 'blk').toString('pem', 'public_pkcs8')
* assertEqual(pemPublic, key)
*
* @throws {InvalidOperationError}
* If key is omitted.
*
* @throws {InvalidOperationError}
* If format is omitted.
*
* @param {(JWK|String)} key
* @param {SerializableFormat} format
* @return {Key}
*/
static from (key, format) {
// Sanity checking
if (!key) {
throw new InvalidOperationError('key is required')
}
if (!format) {
throw new InvalidOperationError('format is required')
}
// JWK
if (format === 'jwk') {
let jwk
// Parse JSON
if (typeof key === 'string') {
try {
jwk = JSON.parse(key)
} catch (error) {
throw new InvalidOperationError('key is not a valid JWK')
}
} else if (typeof key === 'object') {
jwk = key
}
let { kty, crv } = jwk
let oid
// Required properties
if (!kty) {
throw new InvalidOperationError('kty is required for JWK')
}
if (kty === 'EC' && !crv) {
throw new InvalidOperationError('crv is required for EC JWK')
}
if (kty === 'RSA') {
oid = types.find(param => param.kty === kty).oid
}
// Key type
let selector = jwk.d ? 'private' : 'public'
return new Key(jwk, { kty, crv, oid, format, selector })
}
// PEM
if (format === 'pem') {
if (typeof key !== 'string') {
throw new InvalidOperationError('key is not a valid PEM string')
}
// Extract Base64 String
let lines = key.split(EOL)
let header = lines.splice(0, 1)
lines.splice(lines.length - 1, 1)
let base64pem = lines.join('')
// Extract metadata from header
let match = /^-----BEGIN ((RSA|EC) )?(PUBLIC|PRIVATE) KEY-----$/.exec(header)
let oid, crv, kty = match ? match[2] : undefined
let selector = match ? match[3] : undefined
let pem = Buffer.from(base64pem, 'base64')
if (!selector) {
throw new InvalidOperationError('key is not a valid PEM string')
}
// PKCS8
if (!kty) {
let PrivateKeyInfo = asn.normalize('PrivateKeyInfo')
let PublicKeyInfo = asn.normalize('PublicKeyInfo')
let decoded
if (selector === 'PRIVATE') {
selector = 'private_pkcs8'
decoded = PrivateKeyInfo.decode(pem, 'der')
} else if (selector === 'PUBLIC') {
selector = 'public_pkcs8'
decoded = PublicKeyInfo.decode(pem, 'der')
}
let { algorithm: { algorithm, parameters } } = decoded
algorithm = algorithm.join('.')
// parameters are optional
parameters = parameters ? parameters.toString('hex') : undefined
// kty might not exist
const type = types.find(param => param.oid === algorithm)
kty = type ? type.kty : undefined
if (!kty) {
throw new OperationNotSupportedError(algorithm)
}
if (kty === 'RSA' || kty === 'OKP') {
oid = algorithm
} else if (kty === 'EC') {
crv = types.find(param => param.algParameters === parameters).crv
}
// PKCS1
} else {
if (kty === 'RSA') {
selector = selector === 'PRIVATE' ? 'private_pkcs1' : 'public_pkcs1'
oid = types.find(param => param.kty === kty).oid
} else if (kty === 'EC') {
let decoded
if (selector === 'PRIVATE') {
let KeyType = asn.normalize(`${kty}PrivateKey`)
selector = 'private_pkcs1'
decoded = KeyType.decode(pem, 'der')
} else if (selector === 'PUBLIC') {
let KeyType = asn.normalize(`${kty}PrivateKey`)
selector = 'public_pkcs1'
decoded = KeyType.decode(pem, 'der')
}
let { parameters: { value } } = decoded
crv = types.find(param => param.namedCurve === value.join('.')).crv
}
}
return new Key(pem, { kty, oid, crv, format, selector })
}
// BLK
if (format === 'blk') {
return new Key(key, { kty: 'EC', crv: 'K-256', format, selector: key.length > 64 ? 'public' : 'private' })
}
throw new InvalidOperationError(`Invalid format ${format}`)
}
/**
* alg
* @ignore
*
* @internal For internal use only
*/
get alg () {
let { kty, crv, oid } = this
if (!this.algorithm) {
if (crv) {
Object.defineProperty(this, 'algorithm', { value: types.normalize(kty, 'crv', crv) })
} else if (oid) {
Object.defineProperty(this, 'algorithm', { value: types.normalize(kty, 'oid', oid) })
} else {
throw new Error('Both crv and oid are undefined')
}
if (!this.algorithm) {
throw new Error(`${this.crv || this.oid} is not implemented yet`)
}
}
return this.algorithm
}
/**
* parseKey
* @ignore
*
* @internal For internal use only
*/
parseKey (key) {
let { alg, format, selector } = this
// PEM
if (format === 'pem') {
switch (selector) {
case 'public_pkcs1':
return alg.fromPublicPKCS1(key)
case 'public_pkcs8':
return alg.fromPublicPKCS8(key)
case 'private_pkcs1':
return alg.fromPrivatePKCS1(key)
case 'private_pkcs8':
return alg.fromPrivatePKCS8(key)
default:
throw new Error('Invalid selector value')
}
}
// JWK
if (format === 'jwk') {
return alg.fromJwk(key, selector)
}
// BLK
if (format === 'blk') {
switch (selector) {
case 'public':
return alg.fromPublicBlk(key)
case 'private':
return alg.fromPrivateBlk(key)
}
}
}
/**
* toJwk
*
* @description
* Convert a key to JWK.
*
* @param {KeySelector} selector
*
* @return {JWK}
*/
toJwk (selector) {
let { key, alg, selector: type } = this
switch (selector) {
case 'public':
return alg.toPublicJwk(key)
case 'private':
if (type.includes('public')) {
throw new InvalidOperationError('Cannot export a private key from a public key')
}
return alg.toPrivateJwk(key)
default:
throw new Error('Invalid key selector')
}
throw new OperationNotSupportedError()
}
/**
* toString
*
* @description
* Serialize a key to the specified format
*
* @param {SerializableFormat} [format=pem]
* @param {(KeySelector|PEMKeySelector)} [selector=public_pkcs8]
* @return {String}
*/
toString (format = 'pem', selector = 'public_pkcs8') {
let { key, alg, selector: type } = this
// PEM
if (format === 'pem') {
switch (selector) {
case 'public_pkcs1':
return alg.toPublicPKCS1(key)
case 'public_pkcs8':
return alg.toPublicPKCS8(key)
case 'private_pkcs1':
if (type.includes('public')) {
throw new InvalidOperationError('Cannot export a private key from a public key')
}
return alg.toPrivatePKCS1(key)
case 'private_pkcs8':
if (type.includes('public')) {
throw new InvalidOperationError('Cannot export a private key from a public key')
}
return alg.toPrivatePKCS8(key)
default:
throw new Error('Invalid key selector')
}
// JWK
} else if (format === 'jwk') {
switch (selector) {
case 'public':
return JSON.stringify(alg.toPublicJwk(key))
case 'private':
if (type.includes('public')) {
throw new InvalidOperationError('Cannot export a private key from a public key')
}
return JSON.stringify(alg.toPrivateJwk(key))
default:
throw new Error('Invalid key selector')
}
// BLK
} else if (format === 'blk') {
switch (selector) {
case 'private':
if (type.includes('public')) {
throw new InvalidOperationError('Cannot export a private key from a public key')
}
return alg.toPrivateBlk(key)
case 'public':
return alg.toPublicBlk(key)
default:
throw new Error('Invalid key selector')
}
}
throw new OperationNotSupportedError()
}
}
/**
* Export
* @ignore
*/
module.exports = Key