src/common.js
import * as asn1js from "asn1js";
import { utilConcatBuf } from "pvutils";
import CryptoEngine from "./CryptoEngine.js";
//**************************************************************************************
//region Crypto engine related function
//**************************************************************************************
let engine = {
name: "none",
crypto: null,
subtle: null
};
//**************************************************************************************
export function setEngine(name, crypto, subtle)
{
//region We are in Node
// noinspection JSUnresolvedVariable
if((typeof process !== "undefined") && ("pid" in process) && (typeof global !== "undefined"))
{
// noinspection ES6ModulesDependencies, JSUnresolvedVariable
if(typeof global[process.pid] === "undefined")
{
// noinspection JSUnresolvedVariable
global[process.pid] = {};
}
else
{
// noinspection JSUnresolvedVariable
if(typeof global[process.pid] !== "object")
{
// noinspection JSUnresolvedVariable
throw new Error(`Name global.${process.pid} already exists and it is not an object`);
}
}
// noinspection JSUnresolvedVariable
if(typeof global[process.pid].pkijs === "undefined")
{
// noinspection JSUnresolvedVariable
global[process.pid].pkijs = {};
}
else
{
// noinspection JSUnresolvedVariable
if(typeof global[process.pid].pkijs !== "object")
{
// noinspection JSUnresolvedVariable
throw new Error(`Name global.${process.pid}.pkijs already exists and it is not an object`);
}
}
// noinspection JSUnresolvedVariable
global[process.pid].pkijs.engine = {
name: name,
crypto: crypto,
subtle: subtle
};
}
//endregion
//region We are in browser
else
{
engine = {
name: name,
crypto: crypto,
subtle: subtle
};
}
//endregion
}
//**************************************************************************************
export function getEngine()
{
//region We are in Node
// noinspection JSUnresolvedVariable
if((typeof process !== "undefined") && ("pid" in process) && (typeof global !== "undefined"))
{
let _engine;
try
{
// noinspection JSUnresolvedVariable
_engine = global[process.pid].pkijs.engine;
}
catch(ex)
{
throw new Error("Please call \"setEngine\" before call to \"getEngine\"");
}
return _engine;
}
//endregion
return engine;
}
//**************************************************************************************
(function initCryptoEngine()
{
if(typeof self !== "undefined")
{
if("crypto" in self)
{
let engineName = "webcrypto";
/**
* Standard crypto object
* @type {Object}
* @property {Object} [webkitSubtle] Subtle object from Apple
*/
const cryptoObject = self.crypto;
let subtleObject = null;
// Apple Safari support
if("webkitSubtle" in self.crypto)
{
try
{
subtleObject = self.crypto.webkitSubtle;
}
catch(ex)
{
subtleObject = self.crypto.subtle;
}
engineName = "safari";
}
if("subtle" in self.crypto)
subtleObject = self.crypto.subtle;
engine = {
name: engineName,
crypto: cryptoObject,
subtle: new CryptoEngine({ name: engineName, crypto: self.crypto, subtle: subtleObject })
};
}
}
setEngine(engine.name, engine.crypto, engine.subtle);
})();
//**************************************************************************************
//endregion
//**************************************************************************************
//region Declaration of common functions
//**************************************************************************************
/**
* Get crypto subtle from current "crypto engine" or "undefined"
* @returns {({decrypt, deriveKey, digest, encrypt, exportKey, generateKey, importKey, sign, unwrapKey, verify, wrapKey}|null)}
*/
export function getCrypto()
{
const _engine = getEngine();
if(_engine.subtle !== null)
return _engine.subtle;
return undefined;
}
//**************************************************************************************
/**
* Initialize input Uint8Array by random values (with help from current "crypto engine")
* @param {!Uint8Array} view
* @returns {*}
*/
export function getRandomValues(view)
{
return getEngine().subtle.getRandomValues(view);
}
//**************************************************************************************
/**
* Get OID for each specific algorithm
* @param {Object} algorithm
* @returns {string}
*/
export function getOIDByAlgorithm(algorithm)
{
return getEngine().subtle.getOIDByAlgorithm(algorithm);
}
//**************************************************************************************
/**
* Get default algorithm parameters for each kind of operation
* @param {string} algorithmName Algorithm name to get common parameters for
* @param {string} operation Kind of operation: "sign", "encrypt", "generatekey", "importkey", "exportkey", "verify"
* @returns {*}
*/
export function getAlgorithmParameters(algorithmName, operation)
{
return getEngine().subtle.getAlgorithmParameters(algorithmName, operation);
}
//**************************************************************************************
/**
* Create CMS ECDSA signature from WebCrypto ECDSA signature
* @param {ArrayBuffer} signatureBuffer WebCrypto result of "sign" function
* @returns {ArrayBuffer}
*/
export function createCMSECDSASignature(signatureBuffer)
{
//region Initial check for correct length
if((signatureBuffer.byteLength % 2) !== 0)
return new ArrayBuffer(0);
//endregion
//region Initial variables
const length = signatureBuffer.byteLength / 2; // There are two equal parts inside incoming ArrayBuffer
const rBuffer = new ArrayBuffer(length);
const rView = new Uint8Array(rBuffer);
rView.set(new Uint8Array(signatureBuffer, 0, length));
const rInteger = new asn1js.Integer({ valueHex: rBuffer });
const sBuffer = new ArrayBuffer(length);
const sView = new Uint8Array(sBuffer);
sView.set(new Uint8Array(signatureBuffer, length, length));
const sInteger = new asn1js.Integer({ valueHex: sBuffer });
//endregion
return (new asn1js.Sequence({
value: [
rInteger.convertToDER(),
sInteger.convertToDER()
]
})).toBER(false);
}
//**************************************************************************************
/**
* String preparation function. In a future here will be realization of algorithm from RFC4518
* @param {string} inputString JavaScript string. As soon as for each ASN.1 string type we have a specific transformation function here we will work with pure JavaScript string
* @returns {string} Formated string
*/
export function stringPrep(inputString)
{
//region Initial variables
let isSpace = false;
let cuttedResult = "";
//endregion
const result = inputString.trim(); // Trim input string
//region Change all sequence of SPACE down to SPACE char
for(let i = 0; i < result.length; i++)
{
if(result.charCodeAt(i) === 32)
{
if(isSpace === false)
isSpace = true;
}
else
{
if(isSpace)
{
cuttedResult += " ";
isSpace = false;
}
cuttedResult += result[i];
}
}
//endregion
return cuttedResult.toLowerCase();
}
//**************************************************************************************
/**
* Create a single ArrayBuffer from CMS ECDSA signature
* @param {Sequence} cmsSignature ASN.1 SEQUENCE contains CMS ECDSA signature
* @returns {ArrayBuffer}
*/
export function createECDSASignatureFromCMS(cmsSignature)
{
//region Check input variables
if((cmsSignature instanceof asn1js.Sequence) === false)
return new ArrayBuffer(0);
if(cmsSignature.valueBlock.value.length !== 2)
return new ArrayBuffer(0);
if((cmsSignature.valueBlock.value[0] instanceof asn1js.Integer) === false)
return new ArrayBuffer(0);
if((cmsSignature.valueBlock.value[1] instanceof asn1js.Integer) === false)
return new ArrayBuffer(0);
//endregion
const rValue = cmsSignature.valueBlock.value[0].convertFromDER();
const sValue = cmsSignature.valueBlock.value[1].convertFromDER();
//region Check the lengths of two parts are equal
switch(true)
{
case (rValue.valueBlock.valueHex.byteLength < sValue.valueBlock.valueHex.byteLength):
{
if((sValue.valueBlock.valueHex.byteLength - rValue.valueBlock.valueHex.byteLength) !== 1)
throw new Error("Incorrect DER integer decoding");
const correctedLength = sValue.valueBlock.valueHex.byteLength;
const rValueView = new Uint8Array(rValue.valueBlock.valueHex);
const rValueBufferCorrected = new ArrayBuffer(correctedLength);
const rValueViewCorrected = new Uint8Array(rValueBufferCorrected);
rValueViewCorrected.set(rValueView, 1);
rValueViewCorrected[0] = 0x00; // In order to be sure we do not have any garbage here
return utilConcatBuf(rValueBufferCorrected, sValue.valueBlock.valueHex);
}
case (rValue.valueBlock.valueHex.byteLength > sValue.valueBlock.valueHex.byteLength):
{
if((rValue.valueBlock.valueHex.byteLength - sValue.valueBlock.valueHex.byteLength) !== 1)
throw new Error("Incorrect DER integer decoding");
const correctedLength = rValue.valueBlock.valueHex.byteLength;
const sValueView = new Uint8Array(sValue.valueBlock.valueHex);
const sValueBufferCorrected = new ArrayBuffer(correctedLength);
const sValueViewCorrected = new Uint8Array(sValueBufferCorrected);
sValueViewCorrected.set(sValueView, 1);
sValueViewCorrected[0] = 0x00; // In order to be sure we do not have any garbage here
return utilConcatBuf(rValue.valueBlock.valueHex, sValueBufferCorrected);
}
default:
{
//region In case we have equal length and the length is not even with 2
if(rValue.valueBlock.valueHex.byteLength % 2)
{
const correctedLength = (rValue.valueBlock.valueHex.byteLength + 1);
const rValueView = new Uint8Array(rValue.valueBlock.valueHex);
const rValueBufferCorrected = new ArrayBuffer(correctedLength);
const rValueViewCorrected = new Uint8Array(rValueBufferCorrected);
rValueViewCorrected.set(rValueView, 1);
rValueViewCorrected[0] = 0x00; // In order to be sure we do not have any garbage here
const sValueView = new Uint8Array(sValue.valueBlock.valueHex);
const sValueBufferCorrected = new ArrayBuffer(correctedLength);
const sValueViewCorrected = new Uint8Array(sValueBufferCorrected);
sValueViewCorrected.set(sValueView, 1);
sValueViewCorrected[0] = 0x00; // In order to be sure we do not have any garbage here
return utilConcatBuf(rValueBufferCorrected, sValueBufferCorrected);
}
//endregion
}
}
//endregion
return utilConcatBuf(rValue.valueBlock.valueHex, sValue.valueBlock.valueHex);
}
//**************************************************************************************
/**
* Get WebCrypto algorithm by wel-known OID
* @param {string} oid well-known OID to search for
* @returns {Object}
*/
export function getAlgorithmByOID(oid)
{
return getEngine().subtle.getAlgorithmByOID(oid);
}
//**************************************************************************************
/**
* Getting hash algorithm by signature algorithm
* @param {AlgorithmIdentifier} signatureAlgorithm Signature algorithm
* @returns {string}
*/
export function getHashAlgorithm(signatureAlgorithm)
{
return getEngine().subtle.getHashAlgorithm(signatureAlgorithm);
}
//**************************************************************************************
/**
* ANS X9.63 Key Derivation Function having a "Counter" as a parameter
* @param {string} hashFunction Used hash function
* @param {ArrayBuffer} Zbuffer ArrayBuffer containing ECDH shared secret to derive from
* @param {number} Counter
* @param {ArrayBuffer} SharedInfo Usually DER encoded "ECC_CMS_SharedInfo" structure
*/
export function kdfWithCounter(hashFunction, Zbuffer, Counter, SharedInfo)
{
//region Check of input parameters
switch(hashFunction.toUpperCase())
{
case "SHA-1":
case "SHA-256":
case "SHA-384":
case "SHA-512":
break;
default:
return Promise.reject(`Unknown hash function: ${hashFunction}`);
}
if((Zbuffer instanceof ArrayBuffer) === false)
return Promise.reject("Please set \"Zbuffer\" as \"ArrayBuffer\"");
if(Zbuffer.byteLength === 0)
return Promise.reject("\"Zbuffer\" has zero length, error");
if((SharedInfo instanceof ArrayBuffer) === false)
return Promise.reject("Please set \"SharedInfo\" as \"ArrayBuffer\"");
if(Counter > 255)
return Promise.reject("Please set \"Counter\" variable to value less or equal to 255");
//endregion
//region Initial variables
const counterBuffer = new ArrayBuffer(4);
const counterView = new Uint8Array(counterBuffer);
counterView[0] = 0x00;
counterView[1] = 0x00;
counterView[2] = 0x00;
counterView[3] = Counter;
let combinedBuffer = new ArrayBuffer(0);
//endregion
//region Get a "crypto" extension
const crypto = getCrypto();
if(typeof crypto === "undefined")
return Promise.reject("Unable to create WebCrypto object");
//endregion
//region Create a combined ArrayBuffer for digesting
combinedBuffer = utilConcatBuf(combinedBuffer, Zbuffer);
combinedBuffer = utilConcatBuf(combinedBuffer, counterBuffer);
combinedBuffer = utilConcatBuf(combinedBuffer, SharedInfo);
//endregion
//region Return digest of combined ArrayBuffer and information about current counter
return crypto.digest({
name: hashFunction
},
combinedBuffer)
.then(result =>
({
counter: Counter,
result
}));
//endregion
}
//**************************************************************************************
/**
* ANS X9.63 Key Derivation Function
* @param {string} hashFunction Used hash function
* @param {ArrayBuffer} Zbuffer ArrayBuffer containing ECDH shared secret to derive from
* @param {number} keydatalen Length (!!! in BITS !!!) of used kew derivation function
* @param {ArrayBuffer} SharedInfo Usually DER encoded "ECC_CMS_SharedInfo" structure
*/
export function kdf(hashFunction, Zbuffer, keydatalen, SharedInfo)
{
//region Initial variables
let hashLength = 0;
let maxCounter = 1;
const kdfArray = [];
//endregion
//region Check of input parameters
switch(hashFunction.toUpperCase())
{
case "SHA-1":
hashLength = 160; // In bits
break;
case "SHA-256":
hashLength = 256; // In bits
break;
case "SHA-384":
hashLength = 384; // In bits
break;
case "SHA-512":
hashLength = 512; // In bits
break;
default:
return Promise.reject(`Unknown hash function: ${hashFunction}`);
}
if((Zbuffer instanceof ArrayBuffer) === false)
return Promise.reject("Please set \"Zbuffer\" as \"ArrayBuffer\"");
if(Zbuffer.byteLength === 0)
return Promise.reject("\"Zbuffer\" has zero length, error");
if((SharedInfo instanceof ArrayBuffer) === false)
return Promise.reject("Please set \"SharedInfo\" as \"ArrayBuffer\"");
//endregion
//region Calculated maximum value of "Counter" variable
const quotient = keydatalen / hashLength;
if(Math.floor(quotient) > 0)
{
maxCounter = Math.floor(quotient);
if((quotient - maxCounter) > 0)
maxCounter++;
}
//endregion
//region Create an array of "kdfWithCounter"
for(let i = 1; i <= maxCounter; i++)
kdfArray.push(kdfWithCounter(hashFunction, Zbuffer, i, SharedInfo));
//endregion
//region Return combined digest with specified length
return Promise.all(kdfArray).then(incomingResult =>
{
//region Initial variables
let combinedBuffer = new ArrayBuffer(0);
let currentCounter = 1;
let found = true;
//endregion
//region Combine all buffer together
while(found)
{
found = false;
for(const result of incomingResult)
{
if(result.counter === currentCounter)
{
combinedBuffer = utilConcatBuf(combinedBuffer, result.result);
found = true;
break;
}
}
currentCounter++;
}
//endregion
//region Create output buffer with specified length
keydatalen >>= 3; // Divide by 8 since "keydatalen" is in bits
if(combinedBuffer.byteLength > keydatalen)
{
const newBuffer = new ArrayBuffer(keydatalen);
const newView = new Uint8Array(newBuffer);
const combinedView = new Uint8Array(combinedBuffer);
for(let i = 0; i < keydatalen; i++)
newView[i] = combinedView[i];
return newBuffer;
}
return combinedBuffer; // Since the situation when "combinedBuffer.byteLength < keydatalen" here we have only "combinedBuffer.byteLength === keydatalen"
//endregion
});
//endregion
}
//**************************************************************************************
//endregion
//**************************************************************************************