junkurihara/jscu

View on GitHub
packages/js-crypto-key-utils/src/key.ts

Summary

Maintainability
B
4 hrs
Test Coverage
/**
* key.js
*/
import {fromJwkTo, toJwkFrom} from './converter';
import {getJwkThumbprint} from './thumbprint';
import jseu from 'js-encoding-utils';
import {getJwkType, getSec1KeyType, isAsn1Encrypted, isAsn1Public} from './util';
import {
DER,
OctetEC,
PEM,
KeyFormat,
CurveTypes,
PublicOrPrivate,
KeyExportOptions,
JwkThumbprintFormat, HashTypes
} from './typedef';
 
const cloneDeep = require('lodash.clonedeep');
 
type CurrentKeyStatus = {jwk: boolean, der: boolean, oct: boolean};
/**
* Key class to abstract public and private key objects in string or binary.
* This class provides functions to interchangeably convert key formats,
* and key objects will be used for the root package, js-crypto-utils, as inputs to exposed APIs.
*/
export class Key {
private _type: PublicOrPrivate|null;
private _jwk: JsonWebKey|null;
private _der: Uint8Array|null;
private _oct: {namedCurve?: CurveTypes, key?: OctetEC};
private _isEncrypted: boolean;
private _current: CurrentKeyStatus;
/**
* @constructor
* @param {KeyFormat} format - Key format: 'jwk', 'der', 'pem' or 'oct' (only for ECC key).
* @param {JsonWebKey|PEM|DER|OctetEC} key - Key object in the specified format.
* @param {Object} [options={}] - Required if format='oct', and then it is {namedCurve: String}.
* @throws {Error} - Throws if the input format and key are incompatible to the constructor.
*/
Function `constructor` has a Cognitive Complexity of 13 (exceeds 5 allowed). Consider refactoring.
constructor(format: KeyFormat, key: JsonWebKey|PEM|DER|OctetEC, options: {namedCurve?: CurveTypes}={}){
const localKey = cloneDeep(key);
const localOpt = cloneDeep(options);
 
this._type = null;
this._jwk = null;
this._der = null;
this._oct = {}; // only for EC keys
this._isEncrypted = false;
this._current = {jwk: false, der: false, oct: false};
 
if(format === 'jwk'){
this._setJwk(<JsonWebKey>localKey);
}
else if (format === 'der' || format === 'pem'){
if(format === 'der' && !(localKey instanceof Uint8Array)) throw new Error('DerKeyMustBeUint8Array');
if(format === 'pem' && (typeof localKey !== 'string')) throw new Error('PemKeyMustBeString');
this._setAsn1(<any>localKey, format);
}
else if (format === 'oct'){
if(typeof localOpt.namedCurve !== 'string') throw new Error('namedCurveMustBeSpecified');
if(!(localKey instanceof Uint8Array)) throw new Error('OctetKeyMustBeUint8Array');
this._setSec1(<Uint8Array>localKey, localOpt.namedCurve);
}
else throw new Error('UnsupportedType');
}
 
///////////////////////////////////////////////////////////
// private method handling instance variables
// all instance variables must be set via these methods
/**
* Set a key in JWK to the Key object.
* @param {JsonWebKey} jwkey - The Json Web Key.
* @private
*/
private _setJwk(jwkey: JsonWebKey){
this._type = getJwkType(jwkey); // this also check key format
this._jwk = jwkey;
if(this._isEncrypted) this._der = null;
this._isEncrypted = false;
this._setCurrentStatus();
}
 
/**
* Set a key in DER or PEM to the Key object.
* @param {DER|PEM} asn1key - The DER key byte array or PEM key string.
* @param {String} format - 'der' or 'pem' specifying the format.
* @private
*/
private _setAsn1(asn1key: DER|PEM, format: 'der'|'pem'){
this._type = (isAsn1Public(asn1key, format)) ? 'public' : 'private'; // this also check key format
this._isEncrypted = isAsn1Encrypted(asn1key, format);
this._der = <Uint8Array>((format === 'pem') ? jseu.formatter.pemToBin(<PEM>asn1key): asn1key);
if(this._isEncrypted){
this._jwk = null;
this._oct = {};
}
this._setCurrentStatus();
}
 
/**
* Set a key in SEC1 = Octet format to the Key Object.
* @param {OctetEC} sec1key - The Octet SEC1 key byte array.
* @param {CurveTypes} namedCurve - Name of curve like 'P-256'.
* @private
*/
private _setSec1(sec1key: OctetEC, namedCurve: CurveTypes){
this._type = getSec1KeyType(sec1key, namedCurve); // this also check key format
this._oct = { namedCurve, key: sec1key };
if(this._isEncrypted) this._der = null;
this._isEncrypted = false;
this._setCurrentStatus();
}
 
/**
* Set the current internal status. In particular, manage what the object is based on.
* @private
*/
private _setCurrentStatus() {
this._current.jwk = (this._jwk !== null && (this._jwk.kty === 'RSA' || this._jwk.kty === 'EC') );
this._current.der = (this._der !== null && this._der.length > 0);
this._current.oct = (
typeof this._oct.key !== 'undefined'
&& typeof this._oct.namedCurve !== 'undefined'
&& this._oct.key.length > 0
);
}
///////////////////////////////////////////////////////////
// (pseudo) public methods allowed to be accessed from outside
/**
* Convert the stored key and export the key in desired format.
* Imported key must be basically decrypted except the case where the key is exported as-is.
* @param {String} format - Intended format of exported key. 'jwk', 'pem', 'der' or 'oct'
* @param {KeyExportOptions} [options={}] - Optional arguments.
* @return {Promise<JsonWebKey|PEM|DER|OctetEC>} - Exported key object.
*/
Function `export` has a Cognitive Complexity of 14 (exceeds 5 allowed). Consider refactoring.
Function `export` has 33 lines of code (exceeds 25 allowed). Consider refactoring.
async export(format: KeyFormat = 'jwk', options: KeyExportOptions = {}): Promise<JsonWebKey|PEM|DER|OctetEC>{
// return 'as is' without passphrase when nothing is given as 'options'
// only for the case to export der key from der key (considering encrypted key). expect to be called from getter
if(this._isEncrypted && this._type === 'private'){
if((format === 'der' || format === 'pem') && Object.keys(options).length === 0 && this._current.der) {
return (format === 'pem') ? jseu.formatter.binToPem(<DER>(this._der), 'encryptedPrivate') : <DER>this._der;
}
else throw new Error('DecryptionRequired');
}
 
// first converted to jwk
let jwkey: JsonWebKey;
if(this._current.jwk){
jwkey = <JsonWebKey>this._jwk;
}
else if(this._current.oct) { // options.type is not specified here to import jwk
jwkey = await toJwkFrom('oct', <OctetEC>this._oct.key, {namedCurve: this._oct.namedCurve});
}
else if(this._current.der) {
jwkey = await toJwkFrom('der', <DER>this._der);
}
else throw new Error('InvalidStatus');
 
this._setJwk(jwkey); // store jwk if the exiting private key is not encrypted
 
// then export as the key in intended format
if (format === 'der' || format === 'pem') {
return await fromJwkTo(format, jwkey, {
outputPublic: options.outputPublic,
compact: options.compact,
//passphrase: options.encryptParams.passphrase,
encryptParams: options.encryptParams
});
}
else if (format === 'oct') {
return await fromJwkTo(format, jwkey, {
outputPublic: options.outputPublic,
output: options.output,
compact: options.compact
});
}
else return jwkey;
}
 
/**
* Encrypt stored key and set the encrypted key to this instance.
* @param {String} passphrase - String passphrase.
* @return {Promise<boolean>} - Always true otherwise thrown.
* @throws {Error} - Throws if AlreadyEncrypted.
*/
async encrypt (passphrase: string): Promise<boolean>{
if(this._isEncrypted) throw new Error('AlreadyEncrypted');
const options = {encryptParams: {passphrase}};
this._setAsn1(<DER>(await this.export('der', options)), 'der');
 
return true;
}
 
/**
* Decrypted stored key and set the decrypted key in JWK to this instance.
* @param {String} passphrase - String passphrase.
* @return {Promise<boolean>} - Always true otherwise thrown.
* @throws {Error} - Throws if NotEncrypted or FailedToDecrypt.
*/
async decrypt (passphrase: string): Promise<boolean>{
if(!this._isEncrypted) throw new Error('NotEncrypted');
let jwkey;
if(this._current.der){
jwkey = await toJwkFrom('der', <DER>this._der, {passphrase}); // type is not specified here to import jwk
}
else throw new Error('FailedToDecrypt');
this._setJwk(jwkey);
 
return true;
}
 
/**
* Conpute JWK thumbprint specified in RFC7638 {@link https://tools.ietf.org/html/rfc7638}.
* @param {HashTypes} [alg='SHA-256'] - Name of hash algorithm for thumbprint computation like 'SHA-256'.
* @param {JwkThumbpirntFormat} [output='binary'] - Output format of JWK thumbprint. 'binary', 'hex' or 'base64'.
* @return {Promise<Uint8Array|String>} - Computed thumbprint.
* @throws {Error} - Throws if DecryptionRequired.
*/
async getJwkThumbprint(alg: HashTypes ='SHA-256', output: JwkThumbprintFormat='binary'){
if(this._isEncrypted) throw new Error('DecryptionRequired');
return await getJwkThumbprint(<JsonWebKey>(await this.export('jwk')), alg, output);
}
 
// getters
/**
* Get keyType in JWK format
* @return {Promise<String>} - 'RSA' or 'EC'
* @throws {Error} - Throws if DecryptionRequired.
*/
get keyType(): Promise<string> {
if(this._isEncrypted) throw new Error('DecryptionRequired');
return new Promise( (resolve, reject) => {
this.export('jwk')
.then( (r) => {
resolve( <string>((<JsonWebKey>r).kty));
})
.catch( (e) => {reject(e);});
});
}
 
/**
* Get jwkThumbprint of this key.
* @return {Promise<Uint8Array>} - Returns binary thumbprint.
*/
get jwkThumbprint(){
return this.getJwkThumbprint();
}
 
/**
* Check if this is encrypted.
* @return {boolean}
*/
get isEncrypted(){ return this._isEncrypted; }
 
/**
* Check if this is a private key.
* @return {boolean}
*/
get isPrivate(){ return this._type === 'private'; }
 
/**
* Returns the key in DER format.
* @return {Promise<DER>}
*/
get der(){ return this.export('der'); }
 
/**
* Returns the key in PEM format.
* @return {Promise<PEM>}
*/
get pem(){ return this.export('pem'); }
 
/**
* Returns the key in JWK format
* @return {Promise<JsonWebKey>}
*/
get jwk(){ return this.export('jwk'); }
 
/**
* Returns the 'EC' key in Octet SEC1 format.
* @return {Promise<OctetEC>}
*/
get oct(){ return this.export('oct', {output: 'string'}); }
}