mirror of
https://github.com/Ylianst/MeshCommander
synced 2025-12-06 06:03:20 +00:00
573 lines
17 KiB
JavaScript
573 lines
17 KiB
JavaScript
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") && (typeof window === "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") && (typeof window === "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;
|
|
|
|
// 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;
|
|
|
|
|
|
if(typeof subtleObject === "undefined")
|
|
{
|
|
engine = {
|
|
name: engineName,
|
|
crypto: cryptoObject,
|
|
subtle: null
|
|
};
|
|
}
|
|
else
|
|
{
|
|
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
|
|
//**************************************************************************************
|