UNPKG

cipherlib

Version:

AEAD Cryptographic Library

1,649 lines (1,171 loc) 46.4 kB
/* ============================================================================== The MIT License CipherLib Copyright © 2025 Bert Buffington 645576.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ================================================================================ */ require( 'use-strict' ); "use strict"; // modules external const crypto = require('node:crypto'); const os = require("node:os"); const fs = require("node:fs"); const fsp = require("node:fs/promises"); const util = require('util'); const debug = util.debuglog('cipherlib'); // CipherLib class CipherLib { constructor( constructor_options ) { this._systemDefaults = { keyfilepath : "~/.cipherlib/cipherlib.key", masterkey : null, // null indicates read from file nonce : "", algorithm : "id-aes128-GCM", encode : "utf8", decode : "", AADBuffer : "", authTagBuffer : "", decryptOutput : 0, byteorder : "big", // default byte order for AEAD payload byte lengths (big or little) KDF : "PBKDF2" // default key derivation function (scrypt or PBKDF2 }; this._options = { constants : { ENCODING_HEX : 0, ENCODING_UTF8 : 1, AEAD_Rev_1_Payload : 193, decryptOutputPayload : 0, decryptOutputAuthTag : 1, decryptOutputAAD : 2 }, defaults : { keyfilepath : "~/.cipherlib/cipherlib.key", masterkey : "", nonce : "", algorithm : "id-aes128-GCM", encode : "utf8", decode : "", AADBuffer : "", authTagBuffer : "", decryptOutput : 0, byteorder : "", KDF : "PBKDF2" }, max : { masterkeylength : 256 }, min : { masterkeylength : 8 }, active : { keyfilepath : "", masterkey : "", nonce : "", algorithm : "", encode : "", decode : "", AADBuffer : "", authTagBuffer : "", decryptOutput : 0, byteorder : "", KDF : "" }, payload : { bytes : { header : 1, // get doubled for hex format nonce : 16, aadlength : 16, tag : 16 }, encoding : "" } }; // end object this._options // preset defaults this._options.active.keyfilepath = this._options.defaults.keyfilepath; this._options.active.algorithm = this._options.defaults.algorithm; this._options.active.encode = this._options.defaults.encode; this._options.active.decode = this._options.defaults.decode; this._options.active.decryptOutput = this._options.defaults.decryptOutput; this._options.active.byteorder = this._options.defaults.byteorder; this._options.active.KDF = this._options.defaults.KDF; // check constructor options (overrides defaults) if (constructor_options) { // keyfilepath if ( (constructor_options.keyfilepath != undefined ) && (Object.prototype.toString.call(constructor_options.keyfilepath).slice(8,-1) == 'String') ) { this._options.defaults.keyfilepath = constructor_options.keyfilepath; this._options.active.keyfilepath = constructor_options.keyfilepath }; // algoritm if ( (constructor_options.algorithm != undefined ) && (Object.prototype.toString.call(constructor_options.algorithm).slice(8,-1) == 'String') ) { this._options.defaults.algorithm = constructor_options.algorithm; this._options.active.algorithm = constructor_options.algorithm }; // masterkey if (constructor_options.masterkey != undefined ) { if (constructor_options.masterkey.length > 0) { switch ( Object.prototype.toString.call(constructor_options.masterkey).slice(8,-1) ) { case 'String' : this._options.active.masterkey = Buffer.from( constructor_options.masterkey ); this._options.defaults.masterkey = this._options.active.masterkey; break; case 'Uint8Array' : this._options.active.masterkey = Buffer.from( constructor_options.masterkey ); this._options.defaults.masterkey = this._options.active.masterkey; break; }; // end switch }; }; // masterkey processing // encode if ( (constructor_options.encode != undefined ) && (Object.prototype.toString.call(constructor_options.encode).slice(8,-1) == 'String') ) { this._options.defaults.encode = constructor_options.encode; this._options.active.encode = constructor_options.encode; }; // decode if ( (constructor_options.decode != undefined ) && (Object.prototype.toString.call(constructor_options.decode).slice(8,-1) == 'String') ) { this._options.defaults.encode = constructor_options.encode; this._options.active.encode = constructor_options.encode; }; // KDF if ( (constructor_options.KDF != undefined ) && (Object.prototype.toString.call(constructor_options.KDF).slice(8,-1) == 'String') ) { this._options.defaults.KDF = constructor_options.KDF; this._options.active.KDF = constructor_options.KDF; }; // byteorder (for AEAD payload's length fields) if ( (constructor_options.byteorder != undefined ) && (Object.prototype.toString.call(constructor_options.byteorder).slice(8,-1) == 'String') ) { this._options.defaults.byteorder = constructor_options.byteorder; this._options.active.byteorder = constructor_options.byteorder; }; }; } // end constructor // ================================== // class methods // ================================== textEncrypt( plaintext, options ) { /* ======================================================================== * Returns: <Uint8Arry> | <hexstring> | null Payload output format * * plaintext : <Uint8Array> | <String> will become ciphertext * options : <Object) options (optional) * * the plaintext is encrypted and stored in payload's ciphertext field. * ======================================================================== */ return new Promise( (resolve, reject) => { var encrypt_options; if ((plaintext == undefined) || (plaintext == null) || (plaintext.length < 1) ) { reject(Error( 201000 )); // invalid or missing plaintext input return; }; if (options == undefined) { encrypt_options = null; } else { encrypt_options = options; }; text_encrypt( plaintext, this._options, encrypt_options, (err, encryptedtext) => { if (err) { reject(Error(err)); return; } else { resolve( encryptedtext ); return; }; }); //=> text_encrypt }); // Promise } // end textEncrypt payloadDecrypt( payload, options ) { /* ======================================================================== * Returns: <Uint8Arry> | null * * payload : <Uint8Array> | <hexstring> where ciphertext is stored * options : <Object) options (optional) * * plaintext will be decrypted from payload's ciphertext and verified. * ======================================================================== */ return new Promise( (resolve, reject) => { var decrypt_options; if ((payload == undefined) || (payload == null) || (payload == "") ) { reject(Error( 201100 )); // invalid or missing payload return; }; if (options == undefined) { decrypt_options = null; } else { decrypt_options = options; }; this._options.active.decryptOutput = this._options.constants.decryptOutputPayload; // returns plaintext from ciphertext payload_decrypt( payload, this._options, decrypt_options, (err, decryptedtext) => { if (err) { reject(Error(err)); return; } else { resolve( decryptedtext ); return; }; }); //=> payload_decrypt }); // Promise } // end payloadDecrypt getTag( payload, options ) { /* =================================================================== * Returns: <Uint8Arry> | <hexstring> | null * * payload : <Uint8Array> | <hexstring> where tag is stored * options : <Object) options (optional) * * the tag will be extracted from payload and verified. * ================================================================= */ return new Promise( (resolve, reject) => { var getTag_options; if ((payload == undefined) || (payload == null) || (payload == "") ) { reject(Error( 201130 )); // invalid or missing payload return; }; if (options == undefined) { getTag_options = null; } else { getTag_options = options; }; this._options.active.decryptOutput = this._options.constants.decryptOutputAuthTag; // return only the authTag payload_decrypt( payload, this._options, getTag_options, (err, authTag ) => { if (err) { reject(Error(err)); return; } else { resolve( authTag ); return; }; }); //=> payload_decrypt }); // Promise } // end getTag getAAD( payload, options ) { /* =================================================================== * Returns: <Uint8Arry> | <hexstring> | null * * payload : <Uint8Array> | <hexstring> where AAD is stored * options : <Object) options (optional) * * the tag will be extracted from payload and verified. * ================================================================= */ return new Promise( (resolve, reject) => { var getAAD_options; if ((payload == undefined) || (payload == null) || (payload == "") ) { reject(Error( 201140 )); // invalid or missing payload return; }; if (options == undefined) { getAAD_options = null; } else { getAAD_options = options; }; this._options.active.decryptOutput = this._options.constants.decryptOutputAAD; // return only the AAD payload_decrypt( payload, this._options, getAAD_options, (err, AAD ) => { if (err) { reject(Error(err)); return; } else { resolve( AAD ); return; }; }); //=> payload_decrypt }); // Promise } // end getAAD resetDefaults( options ) { /* =================================================================== * Returns: <boolean> * * options : <Object) options (optional) * * resets the default settings to the system defaults. Options that * are passed in override the system defaults much like constructor * options. * ================================================================= */ return new Promise( (resolve, reject) => { var reset_options; if (options == undefined) { reset_options = null; } else { reset_options = options; }; reset_defaults( this._systemDefaults, this._options, reset_options, (err) => { if (err) { reject(Error(err)); return; } else { resolve( true ); return; }; }); //=> reset_defaults }); // Promise } // end resetDefaults } // end class CipherLib function text_encrypt( text, options, method_options, cb ) { prepEncrypt( text, options, method_options, (err, encryptedtext) => { if (err) { cb( err ); return; } else { cb(0, encryptedtext); return; }; }); // prepEncrypt } // end text_encrypt async function prepEncrypt( plaintext, options, method_options, cb ) { // first reset defaults try { await setEncryptDefaults( options ); } catch(err){ cb( err ); return; }; try { await setEncryptOptions( options, method_options ); } catch(err) { cb( err ); return; }; if ((options.active.masterkey == null) || (options.active.masterkey == "")) { try { options.active.masterkey = await getMasterKeyFromFile( options ); } catch(err) { cb( err ); return; }; }; if ((options.active.masterkey.length < options.min.masterkeylength) || (options.active.masterkey.length > options.max.masterkeylength )) { debug('masterkey must be at least 8 and less than 256 bytes'); cb( 201001); // masterkey must be at least 8 and less than 256 bytes return; }; if (Object.prototype.toString.call(plaintext).slice(8,-1) == 'String') { var plaintextBuffer = Buffer.from( plaintext, 'utf8' ); } else { if (Object.prototype.toString.call(plaintext).slice(8,-1) == 'Uint8Array') { var plaintextBuffer = Buffer.from( plaintext ); } else { debug('plaintext for encryption must be string or Uint8aary'); cb( 201002 ); // plaintext for encryption must be string or Uint8aary return; }; }; processAEADEncrypt( plaintextBuffer, options, method_options, cb ); } // end prepEncrypt async function processAEADEncrypt( plaintextBuffer, options, method_options, cb ) { try { var cipherkeybytelength = await getAEADCipherkeyBytelength( options.active.algorithm ); } catch(err) { cb( err ); return; }; try { var noncelength = await getAEADNonceBytelength( options.active.algorithm ); } catch(err) { cb( err ); return; }; try { var nonceBuffer = await crypto.randomBytes( noncelength ); } catch(err) { cb( "201080"+":"+err ); // crypto.randomBytes error return; }; try { var saltBuffer = await getSaltFromNonce( nonceBuffer ); } catch(err) { cb( 201081 ); return; }; // masterkey hashed and salted try { var cipherkeyhash = await getCipherKeyHash( options, options.active.masterkey, saltBuffer, cipherkeybytelength); } catch(err) { cb( "201082"+":"+err ); // getCipherKeyHash return; }; try { var cipher = await crypto.createCipheriv( options.active.algorithm, cipherkeyhash, nonceBuffer, { authTagLength : 16 } ); } catch(err) { cb( "201083"+":"+err ); // crypto.createCipheriv error return; }; // set the Nonce + AAD as authenticated data try { await cipher.setAAD( Buffer.concat([nonceBuffer,options.active.AADBuffer]), { plaintextLength: plaintextBuffer.length } ); } catch(err) { cb( "201085"+":"+err ); // crypto.setAAD error return; }; AEADEncrypt( plaintextBuffer, options, method_options, cb, cipher, nonceBuffer ); } // end processAEADEncrypt async function AEADEncrypt( plaintextBuffer, options, method_options, cb, cipher, nonceBuffer ) { /* =========================================================================== * Creates the AEAD Payload * =========================================================================== */ try { var ciphertextBuffer = await cipher.update( plaintextBuffer ); } catch(err) { cb( "201090"+":"+err ); // crypto ciper update final error return; }; try { var ciphertextBufferfinal = await cipher.final( ); } catch(err) { cb( "201091"+":"+err ); // crypto ciper update final error return; }; try { var tagBuffer = await cipher.getAuthTag(); } catch(err) { cb( "201092"+":"+err ); // getAuthTag error return; }; // format payload's remaining components // header var header = "c1"; try { var headerBuffer = await Buffer.from( header, 'hex' ); } catch(err) { cb( "201093"+":"+err ); // Buffer.from utf8 payload header error return; }; // Nonce padded try { var paddedNonceBuffer = await getPaddedNonceBuffer( nonceBuffer ); } catch(err) { cb( "201094"+":"+err ); // getNonceUTF8Buffer error return; }; // AAD Length try { var aadLengthBuffer = await makeUint32BufferForByteLength( options.active.AADBuffer.length ); } catch(err) { cb( "201095"+":"+err ); // error makeUint32BufferForByteLength for AADBuffer return; }; // ciphertextLengthBuffer var ciphertextLength = (ciphertextBuffer.length + ciphertextBufferfinal.length); try { var ciphertextLengthBuffer = await makeUint32BufferForByteLength( ciphertextLength ); } catch(err) { cb( "201096"+":"+err ); // error makeUint32BufferForByteLength for AADBuffer return; }; // concat all the buffers to create payload if (ciphertextBufferfinal.length > 0) { try { var payload = await Buffer.concat([headerBuffer, paddedNonceBuffer, ciphertextLengthBuffer, ciphertextBuffer, ciphertextBufferfinal, aadLengthBuffer, options.active.AADBuffer, tagBuffer]); } catch(err) { cb( "201097"+":"+err ); // Buffer.concat utf8 payload for output error return; }; } else { try { var payload = await Buffer.concat([headerBuffer, paddedNonceBuffer, ciphertextLengthBuffer, ciphertextBuffer, aadLengthBuffer, options.active.AADBuffer, tagBuffer]); } catch(err) { cb( "201098"+":"+err ); // Buffer.concat utf8 payload for output error return; }; }; if (options.active.encode == 'hex') { debug('encoding payload into hex format'); options.payload.encoding = "hex"; cb( 0, payload.toString( 'hex' ) ); return; } else { debug('payload is in binary format'); options.payload.encoding = "binary"; cb( 0, payload ); return; }; } // end AEADEncrypt function setEncryptDefaults( options ) { return new Promise( (resolve, reject) => { options.active.algorithm = options.defaults.algorithm; options.active.decode = options.defaults.decode; options.active.encode = options.defaults.encode; options.payload.encoding = null; options.active.masterkey = options.defaults.masterkey; options.active.KDF = options.defaults.KDF; options.active.byteorder = options.defaults.byteorder; var dummy = ""; options.active.AADBuffer = Buffer.from( dummy ); resolve( true ); return; }); // Promise } // end setEncryptDefaults function setEncryptOptions( options, method_options ) { return new Promise( (resolve, reject) => { if ( method_options == null ) { resolve( true ); return; } else { // masterkey if (method_options.masterkey != undefined ) { if (method_options.masterkey == null ) { var dummy = ""; options.active.masterkey = Buffer.from( dummy ); } else { switch ( Object.prototype.toString.call(method_options.masterkey).slice(8,-1) ) { case 'String' : options.active.masterkey = Buffer.from( method_options.masterkey.normalize('NFC') ); break; case 'Uint8Array' : options.active.masterkey = Buffer.from( method_options.masterkey ); break; default : // invalid masterkey format reject( 201201 ); // masterkey must be string or Uint8Array return; break; }; // end switch }; }; // end process masterkey // algorithm if (method_options.algorithm != undefined ) { if ( (method_options.algorithm != null ) && (method_options.algorithm != "" ) && (Object.prototype.toString.call(method_options.algorithm).slice(8,-1) == 'String') ) { options.active.algorithm = method_options.algorithm; }; }; // end process algorithm // keyfilepath if (method_options.keyfilepath != undefined ) { if ( (method_options.keyfilepath != null ) && (method_options.keyfilepath != "" ) && (Object.prototype.toString.call(method_options.keyfilepath).slice(8,-1) == 'String') ) { options.active.keyfilepath = method_options.keyfilepath; options.active.masterkey = null; }; // force read of masterkey from new filepath }; // end process keyfilepath // encode if (method_options.encode != undefined ) { if ( (method_options.encode != null ) && (method_options.encode != "" ) && (Object.prototype.toString.call(method_options.encode).slice(8,-1) == 'String') ) { options.active.encode = method_options.encode; }; }; // end process encode // KDF if (method_options.KDF != undefined ) { if ( (method_options.KDF != null ) && (method_options.KDF != "" ) && (Object.prototype.toString.call(method_options.KDF).slice(8,-1) == 'String') ) { options.active.KDF = method_options.KDF; }; }; // end process KDF // AAD - AEAD's Additional Data var AAD_data = null; var hasAAD = false; if (method_options.aad != undefined) { AAD_data = method_options.aad; hasAAD = true; } else { if (method_options.AAD != undefined ) { AAD_data = method_options.AAD; hasAAD = true; }; }; if (hasAAD) { if (AAD_data == null ) { var dummy = ""; options.active.AADBuffer = Buffer.from( dummy ); } else { switch ( Object.prototype.toString.call(AAD_data).slice(8,-1) ) { case 'String' : options.active.AADBuffer = Buffer.from( AAD_data ); break; case 'Uint8Array' : options.active.AADBuffer = Buffer.from( AAD_data ); break; default : // invalid AAD format debug('AAD data must be string or Uint8Array'); reject( 201204 ); // AAD must be string or Uint8Array return; break; }; // end switch debug('AAD:', options.active.AADBuffer.toString() ); }; }; // end process AAD resolve( true ); return; }; // end processing method options }); // Promise } // end setEncryptOptions function getPaddedNonceBuffer( nonceBuffer ) { return new Promise( (resolve, reject) => { // check to see if we need to pad if (nonceBuffer.length < 16) { const filllen = 16-nonceBuffer.length; const padBuffer = Buffer.alloc( filllen, filllen, 'utf8' ); var paddedNonceBuffer = Buffer.concat( [nonceBuffer, padBuffer] ); resolve( paddedNonceBuffer ); return; } else { resolve( nonceBuffer ); return; }; }); //Promise } // end getPaddedNonceBuffer async function payload_decrypt( payload, options, method_options, cb ) { prepDecrypt( payload, options, method_options, (err, decryptedtext) => { if (err) { cb( err ); return; } else { cb(0, decryptedtext); return; }; }); // prepDecrypt } // end payload_decrypt async function prepDecrypt( payload, options, method_options, cb ) { try { await setDecryptDefaults( options ); } catch(err) { cb( err ); return; }; try { await setDecryptOptions( options, method_options ); } catch(err) { cb( err ); return; }; if ((options.active.masterkey == null) || (options.active.masterkey == "")) { try { options.active.masterkey = await getMasterKeyFromFile( options ); } catch(err) { cb( err ); return; }; }; if ((options.active.masterkey.length < options.min.masterkeylength) || (options.active.masterkey.length > options.max.masterkeylength )) { debug("masterkey must be at least 8 and less than 256 bytes"); cb( 201402); // masterkey must be at least 8 and less than 256 bytes return; }; // Create payloadBuffer var payloadBuffer; switch (Object.prototype.toString.call(payload).slice(8,-1) ) { case 'String' : // assume hex encoding for string type - same as option { decode : 'hex' } payloadBuffer = Buffer.from( payload, 'hex' ); options.payload.encoding = 'hex'; break; case 'Uint8Array' : payloadBuffer = Buffer.from( payload ); options.payload.encoding = 'utf8'; break; default : // invalid AAD format cb( 201405 ); // payload input was not hex string nor Uint8Array return; break; }; // end switch if (payloadBuffer.length == 0) { debug("payload buffer is length 0 - check payload input"); cb( 201406 ); return; }; // Header identifies payload's version for processing switch (payloadBuffer[0]) { case options.constants.AEAD_Rev_1_Payload : // c1 - version 1 AEADDecrypt( payloadBuffer, options, method_options, cb ); break; default : cb( 201406 ); // invalid payload format or non-supported version return; break; }; //end switch } // end prepDecrypt async function AEADDecrypt( payloadBuffer, options, method_options, cb ) { // check for valid algorithm and cipherkey's required length in bytes try { var cipherkeybytelength = await getAEADCipherkeyBytelength( options.active.algorithm ); } catch(err) { cb( err ); return; }; // get the algorithm's required noncelength in bytes try { var noncelength = await getAEADNonceBytelength( options.active.algorithm ); } catch(err) { cb( err ); return; }; // nonce is stored in payload but could be padded that we do not want if (noncelength > options.payload.bytes.nonce) { cb( 201410 ); // noncelength for algorithm is greater than nonce length in payload return; }; // payload extraction // nonce try { var nonceBuffer = await extractBufferBytes( payloadBuffer, 1, noncelength ); } catch( err ) { cb( "201411"+":"+err ); // missing nonce data return; } // salt try { var saltBuffer = await getSaltFromNonce( nonceBuffer ); } catch (err ) { cb( "201412"+":"+err ); return; }; // ciphertext-length try { var ciphertextLength = await getLengthFromUint32FieldBuffer( payloadBuffer, 17 ); } catch (err) { cb( "201413"+":"+err ); // error in reading ciphertext-length field in buffer return; }; // ciphertext try { var ciphertextBuffer = await extractBufferBytes( payloadBuffer, 21, ciphertextLength ); } catch( err ) { cb( "201414"+":"+err ); // Missing ciphertext data return; } // masterkey hashed and salted try { var cipherkeyhash = await getCipherKeyHash( options, options.active.masterkey, saltBuffer, cipherkeybytelength); } catch(err) { cb( "201415"+":"+err ); // getCipherKeyHash return; }; // AAD-length try { var aadLength = await getLengthFromUint32FieldBuffer( payloadBuffer, (21+ciphertextLength) ); } catch (err) { cb( "201416"+":"+err ); // error in reading aad-length field in buffer return; }; // AAD if (options.active.AADBuffer.length == 0) { // get AAD from payload if (aadLength > 0) { try { options.active.AADBuffer = await extractBufferBytes( payloadBuffer, 21+ciphertextLength+4, aadLength ); } catch( err ) { cb( "201417"+":"+err ); // aadlength is erroneous return; } }; }; // auth-tag if (options.active.authTagBuffer.length == 0) { // get auth-tag from payload when none was entered as method option var minPayloadLengthWithAuthTag = (21+ciphertextLength+4+aadLength+16); if (payloadBuffer.length < minPayloadLengthWithAuthTag) { cb( 201418 ); // no authTag - none in payload and none specified via option return; } else { try { options.active.authTagBuffer = await extractBufferBytes( payloadBuffer, (payloadBuffer.length-16), 16 ); } catch( err ) { cb( "201419"+":"+err ); //missing authtag data return; } }; }; // setup decipher try { var decipher = await crypto.createDecipheriv( options.active.algorithm, cipherkeyhash, nonceBuffer, { authTagLength : 16 } ); } catch(err) { cb( "201420"+":"+err ); // crypto.createDecipheriv error return; }; try { await decipher.setAuthTag( options.active.authTagBuffer); } catch( err ) { cb( "201421"+":"+err ); // crypto set auth-tag error return; }; try { await decipher.setAAD( Buffer.concat([nonceBuffer,options.active.AADBuffer]), { plaintextLength : ciphertextLength }); } catch( err ) { cb( "201422"+":"+err ); // crypto set AAD error return; }; var plaintext; try { plaintext = Buffer.concat( [decipher.update( ciphertextBuffer ), decipher.final()]); } catch( err ) { cb( "201423"+":"+err ); // crypto decipher update error return; }; var OK = false; switch (options.active.decryptOutput) { case 0 : // default output of plaintext from ciphertext cb(0, plaintext); break; return; case 1 : // authTag only if (options.active.encode != undefined) { if (options.active.encode == 'hex') { cb(0, options.active.authTagBuffer.toString('hex') ); break; return; } else { cb(0 , options.active.authTagBuffer ); break; return; } } else { cb(0 , options.active.authTagBuffer ); break; return; }; case 2 : // Verified AAD only if (options.active.encode != undefined) { if (options.active.encode == 'utf8') { cb(0, options.active.AADBuffer.toString('utf8') ); break; return; } else { cb(0 , options.active.AADBuffer ); break; return; } } else { cb(0 , options.active.AADBuffer ); break; return; }; default : // invalid formatting option reject( 201426 ); // invalid decrypt formatting option break; return; }; // switch } // end AEADDecrypt function setDecryptDefaults( options ) { return new Promise( (resolve, reject) => { options.active.algorithm = options.defaults.algorithm; options.active.decode = options.defaults.decode; options.active.encode = options.defaults.encode; options.payload.encoding = null; options.active.KDF = options.defaults.KDF; options.active.masterkey = options.defaults.masterkey; var dummy = ""; options.active.AADBuffer = Buffer.from( dummy ); options.active.authTagBuffer = Buffer.from( dummy ); resolve( true ); return; }); // Promise } // end setDecryptDefaults function setDecryptOptions( options, method_options ) { return new Promise( (resolve, reject) => { if ( method_options == null ) { resolve( true ); return; } else { // masterkey if (method_options.masterkey != undefined ) { if (method_options.masterkey == null ) { var dummy = ""; options.active.masterkey = Buffer.from( dummy ); } else { switch ( Object.prototype.toString.call(method_options.masterkey).slice(8,-1) ) { case 'String' : options.active.masterkey = Buffer.from( method_options.masterkey.normalize('NFC') ); break; case 'Uint8Array' : options.active.masterkey = Buffer.from( method_options.masterkey ); break; default : // invalid masterkey format reject( 201220 ); // masterkey must be string or Uint8Array return; break; }; // end switch }; }; // end process masterkey // algorithm if (method_options.algorithm != undefined ) { if ( (method_options.algorithm != null ) && (method_options.algorithm != "" ) && (Object.prototype.toString.call(method_options.algorithm).slice(8,-1) == 'String') ) { options.active.algorithm = method_options.algorithm; }; }; // algorithm // keyfilepath if (method_options.keyfilepath != undefined ) { if ( (method_options.keyfilepath != null ) && (method_options.keyfilepath != "" ) && (Object.prototype.toString.call(method_options.keyfilepath).slice(8,-1) == 'String') ) { options.active.keyfilepath = method_options.keyfilepath; options.active.masterkey = null; }; // force read of masterkey from new file }; // keyfilepath // decode if (method_options.decode != undefined ) { if ( (method_options.decode != null ) && (method_options.decode != "" ) && (Object.prototype.toString.call(method_options.decode).slice(8,-1) == 'String') ) { options.active.decode = method_options.decode; }; }; // decode // encode if (method_options.encode != undefined ) { if ( (method_options.encode != null ) && (method_options.encode != "" ) && (Object.prototype.toString.call(method_options.encode).slice(8,-1) == 'String') ) { // only valid for getTag and getAAD if (options.active.decryptOutput > 0) { options.active.encode = method_options.encode; }; }; }; // end process encode // KDF if (method_options.KDF != undefined ) { if ( (method_options.KDF != null ) && (method_options.KDF != "" ) && (Object.prototype.toString.call(method_options.KDF).slice(8,-1) == 'String') ) { options.active.KDF = method_options.KDF; }; }; // end process KDF // authTag if (method_options.authTag != undefined ) { // check for valid use of method based on output otherwise ignore if ((options.active.decryptOutput == options.constants.decryptOutputPayload) || (options.active.decryptOutput == options.constants.decryptOutputAAD) ) { if (method_options.authTag == null ) { reject( 201221 ); // invalid authTag must be 16 bytes not null return; } else { switch ( Object.prototype.toString.call(method_options.authTag).slice(8,-1) ) { case 'String' : if (method_options.authTag.length != 32) { reject ( 201221 ); // hex string authtag must be 32 nibbles return; } else { options.active.authTagBuffer = Buffer.from( method_options.authTag, 'hex' ); if (options.active.authTagBuffer.length != 16) { reject( 201222 ); // authTag as string must contain hex return; }; }; break; case 'Uint8Array' : if (method_options.authTag.length != 16) { reject ( 201223 ); // authtag must be 16 bytes return; } else { try { options.active.authTagBuffer = Buffer.from( method_options.authTag ); } catch(err) { reject( 201224 ); // error copying authtag to internal buffer return; }; }; break; default : // invalid authTag format reject( 201225 ); // authTag must be hex string or Uint8Array return; break; }; // end switch }; }; }; // authTag // AAD - AEAD's Additional Data var AAD_data = null; var hasAAD = false; if (method_options.aad != undefined) { AAD_data = method_options.aad; hasAAD = true; } else { if (method_options.AAD != undefined ) { AAD_data = method_options.AAD; hasAAD = true; }; }; if (hasAAD) { // check for valid use of method based on output otherwise ignore if ((options.active.decryptOutput == options.constants.decryptOutputPayload) || (options.active.decryptOutput == options.constants.decryptOutputAuthTag) ) { if (AAD_data == null ) { var dummy = ""; options.active.AADBuffer = Buffer.from( dummy ); } else { switch ( Object.prototype.toString.call(AAD_data).slice(8,-1) ) { case 'String' : options.active.AADBuffer = Buffer.from( AAD_data ); break; case 'Uint8Array' : options.active.AADBuffer = Buffer.from( AAD_data ); break; default : // invalid AAD format debug('AAD data must be string or Uint8Array'); reject( 201204 ); // AAD must be string or Uint8Array return; break; }; // end switch debug('AAD:', options.active.AADBuffer.toString() ); }; }; // check for valid use }; // end process AAD resolve( true ); return; }; }); // Promise } // end setDecryptOptions function reset_defaults( systemDefaults, options, reset_options, cb ) { /* keyfilepath : "~/.cipherlib/cipherlib.key", masterkey : "", nonce : "", algorithm : "id-aes128-GCM", encode : "utf8", decode : "", AADBuffer : "", authTagBuffer : "", decryptOutput : 0 */ options.defaults.keyfilepath = systemDefaults.keyfilepath; options.defaults.masterkey = systemDefaults.masterkey; options.defaults.nonce = systemDefaults.nonce; options.defaults.algorithm = systemDefaults.algorithm; options.defaults.encode = systemDefaults.encode; options.defaults.decode = systemDefaults.decode; options.defaults.AADBuffer = systemDefaults.AADBuffer; options.defaults.authTagBuffer = systemDefaults.authTagBuffer; options.defaults.decryptOutput = systemDefaults.decryptOutput; if (reset_options) { // keyfilepath if ( (reset_options.keyfilepath != undefined ) && (Object.prototype.toString.call(reset_options.keyfilepath).slice(8,-1) == 'String') ) { options.defaults.keyfilepath = reset_options.keyfilepath; options.active.keyfilepath = reset_options.keyfilepath }; // algoritm if ( (reset_options.algorithm != undefined ) && (Object.prototype.toString.call(reset_options.algorithm).slice(8,-1) == 'String') ) { options.defaults.algorithm = reset_options.algorithm; options.active.algorithm = reset_options.algorithm }; // masterkey if (reset_options.masterkey != undefined ) { if (reset_options.masterkey.length > 0) { switch ( Object.prototype.toString.call(reset_options.masterkey).slice(8,-1) ) { case 'String' : options.active.masterkey = Buffer.from( reset_options.masterkey ); options.defaults.masterkey = options.active.masterkey; break; case 'Uint8Array' : options.active.masterkey = Buffer.from( reset_options.masterkey ); options.defaults.masterkey = options.active.masterkey; break; }; // end switch }; }; // masterkey processing // encode if ( (reset_options.encode != undefined ) && (Object.prototype.toString.call(reset_options.encode).slice(8,-1) == 'String') ) { options.defaults.encode = reset_options.encode; options.active.encode = reset_options.encode; }; // decode if ( (reset_options.decode != undefined ) && (Object.prototype.toString.call(reset_options.decode).slice(8,-1) == 'String') ) { options.defaults.encode = reset_options.encode; options.active.encode = reset_options.encode; }; }; cb(0); return; } // end reset_defaults function getAEADCipherkeyBytelength( algorithm ) { return new Promise( (resolve, reject) => { var keylength; switch (algorithm) { case 'id-aes256-GCM' : keylength = 32; break; case 'id-aes128-GCM' : keylength = 16; break; case 'id-aes256-CCM' : keylength = 32; break; case 'id-aes128-CCM' : keylength = 16; break; default : // error reject( 201110 ); // unsupported or invalid algorithm for AEAD return; break; }; // end switch resolve( keylength ); return; }); // Promise } // end getCipherkeyBytelength function getAEADNonceBytelength( algorithm ) { return new Promise( (resolve, reject) => { var noncelength; switch (algorithm) { case 'id-aes256-CCM' : noncelength = 12; break; case 'id-aes128-CCM' : noncelength = 12; break; default : // 16 bytes is default noncelength = 12; break; }; // end switch resolve( noncelength ); return; }); // Promise } // end getAEADNonceByteLength function getSaltFromNonce( noncebuffer ) { return new Promise( (resolve, reject) => { var saltlength = 16; var saltarray = []; var i = noncebuffer.length - 1; var pos = 0; // reverse the bytes in nonce into salt while (i > -1) { saltarray[pos] = noncebuffer[i]; i = i - 1; pos = pos + 1; }; var salt = Buffer.from( saltarray ); // salt must always be 16 bytes otherwise prepend PKCS#7 style fill to make 16 bytes if (salt.length < saltlength) { const fillen = saltlength-salt.length; const saltpad = Buffer.alloc( fillen, fillen, 'utf8' ); const saltpadded = Buffer.concat([ saltpad, salt ] ); resolve (saltpadded ); return } else { resolve( salt ); return; }; }); // Promise } // end getSaltFromNonce function getMasterKeyFromFile( options ) { return new Promise( (resolve, reject) => { getMasterKeyFromFile_async( options.active.keyfilepath, options, (err, masterkey) => { if (err) { reject( err ); return; } else { options.defaults.masterkey = masterkey; resolve( masterkey ); return; } }); // getMasterKeyFromFile_async }); // Promise } // end getMasterKeyFromFile async function getMasterKeyFromFile_async( keyfilepath, options, cb ) { var keyfile; if ( (keyfilepath == undefined) || (keyfilepath == null) || (keyfilepath == "") ) { debug('getMasterKeyFromFile_async says keyfile path cannot be null'); cb( 201500, null ); // invalid key file - cannot be null path return; }; if (keyfilepath.substring(0,1) == "~") { // check the current users directory for file try { var userhomedir = await os.homedir(); } catch(err) { debug('getMasterKeyFromFile_async says os.homedir error=', err ); cb( 201501, null ); return; }; // error finding home directory for user for keyfilepath keyfile = userhomedir + keyfilepath.substring(1, keyfilepath.length); } else { keyfile = keyfilepath; }; // check that file has read access try { await fsp.access( keyfile, fs.R_OK ); } catch(err) { debug('getMasterKeyFromFile_async says fsp.access error=', err ); cb( 201502, null ); // key file does not have read access return; }; // check size - must be in range of keysize allowed try { var fhandle = await fsp.open( keyfile ); } catch(err) { debug("getMasterKeyFromFile_async says fsp.open error=", err ); cb( 201503, null ); // cannot open key file return; }; try { var stats = await fhandle.stat( ); } catch(err) { debug("getMasterKeyFromFile_async says fhandle.stat error=", err ); cb( 201504, null ); // unable to stat the key file return; }; if ( (stats.size > options.max.masterkeylength) || (stats.size < options.min.masterkeylength) ) { debug("getMasterKeyFromFile_async says masterkey file size is out of allowed range:", stats.size); cb( 201505, null ); // masterkey file size is out of allowed range return; }; try { await fhandle.close(); } catch( err ) { debug("getMasterKeyFromFile_async says fhandle.close error=", err ); cb( 201506, null ); // error trying to close key file return; }; debug('Using masterkey in file:', keyfile ); // read the file contents try { var masterkey = await fsp.readFile( keyfile ); } catch(err) { debug("getMasterKeyFromFile_async says fsp.readfile error=", err ); cb( 201509, null ); // error reading key file return; }; cb( 0, masterkey ); return; } // getMasterKeyFromFile_async function getLengthFromUint32FieldBuffer( payloadBuffer, pos ) { return new Promise( (resolve,reject) => { if ((pos+4) > payloadBuffer.length) { reject( Error( 'field requested is outside source array length' )); return; } else { var arrayBuffer = new ArrayBuffer( 4 ); var view = new DataView( arrayBuffer ); var intbuffer = Buffer.from( payloadBuffer.slice( pos, pos+4) ); for (var i =0; i < 4; ++i ) { view.setUint8( i, intbuffer[i]) }; var fieldlength = view.getUint32( 0 ); resolve( fieldlength ); return; }; }); // Promise } // end function getLengthFromUint32FieldBuffer function getCipherKeyHash( options, key, salt, cipherkeybytelength ) { return new Promise( (resolve, reject) => { if (options.active.KDF == 'scrypt') { // preferred but scrypt is not compatible between 32 bit and 64 bit machines crypto.scrypt( key, salt, cipherkeybytelength, (err, derivedkey) => { if (err) { reject( err ); return; } else { resolve( derivedkey ) return; } }); // crypto.scrypt } else { // pdkdf2 is default and generates same result for 32 and 64 bit machines crypto.pbkdf2( key, salt, 50000, cipherkeybytelength, 'sha512', (err, derivedkey) => { if (err) { reject( err ); return; } else { resolve( derivedkey ) return; } }); // crypto.pbkdf2 }; }); // Promise } // end getCipherKeyHash function makeUint32BufferForByteLength( byte_length) { /* ===================================================== * input: integer * * returns: Uint8Array(4) * * returns a 32 bit (4 byte Unit8array) representing * the byte_length * ===================================================== */ return new Promise( (resolve,reject) => { if (byte_length > 4294967295 ) { reject( Error('Requested byte length - must be < 4294967295 bytes' )); return; }; const intbuffer = new ArrayBuffer(4); const view = new DataView( intbuffer ); view.setUint32( 0, byte_length ); var lengthBuffer = Buffer.from( intbuffer.slice(0,4) ); resolve( lengthBuffer ); return }); //Promise } // end function makeUint32BufferForByteLength function extractBufferBytes( sourceBuffer, pos, bytes ) { return new Promise( (resolve,reject) => { if ( (pos + bytes) > (sourceBuffer.length + 1) ) { reject ( Error( 'field requested is outside source array bounds' ) ); return; } else { resolve( Buffer.from(sourceBuffer.slice( pos, pos+bytes )) ); return; }; }); //Promise } // end extractBufferBytes exports.CipherLib = CipherLib;