src/CryptoEngine.js
import * as asn1js from "asn1js";
import { getParametersValue, stringToArrayBuffer, arrayBufferToString, utilConcatBuf } from "pvutils";
import { createCMSECDSASignature, createECDSASignatureFromCMS } from "./common.js";
import PublicKeyInfo from "./PublicKeyInfo.js";
import PrivateKeyInfo from "./PrivateKeyInfo.js";
import AlgorithmIdentifier from "./AlgorithmIdentifier.js";
import EncryptedContentInfo from "./EncryptedContentInfo.js";
import RSASSAPSSParams from "./RSASSAPSSParams.js";
import PBKDF2Params from "./PBKDF2Params.js";
import PBES2Params from "./PBES2Params.js";
//**************************************************************************************
/**
* Making MAC key using algorithm described in B.2 of PKCS#12 standard.
*/
function makePKCS12B2Key(cryptoEngine, hashAlgorithm, keyLength, password, salt, iterationCount)
{
//region Initial variables
let u;
let v;
const result = [];
//endregion
//region Get "u" and "v" values
switch(hashAlgorithm.toUpperCase())
{
case "SHA-1":
u = 20; // 160
v = 64; // 512
break;
case "SHA-256":
u = 32; // 256
v = 64; // 512
break;
case "SHA-384":
u = 48; // 384
v = 128; // 1024
break;
case "SHA-512":
u = 64; // 512
v = 128; // 1024
break;
default:
throw new Error("Unsupported hashing algorithm");
}
//endregion
//region Main algorithm making key
//region Transform password to UTF-8 like string
const passwordViewInitial = new Uint8Array(password);
const passwordTransformed = new ArrayBuffer((password.byteLength * 2) + 2);
const passwordTransformedView = new Uint8Array(passwordTransformed);
for(let i = 0; i < passwordViewInitial.length; i++)
{
passwordTransformedView[i * 2] = 0x00;
passwordTransformedView[i * 2 + 1] = passwordViewInitial[i];
}
passwordTransformedView[passwordTransformedView.length - 2] = 0x00;
passwordTransformedView[passwordTransformedView.length - 1] = 0x00;
password = passwordTransformed.slice(0);
//endregion
//region Construct a string D (the "diversifier") by concatenating v/8 copies of ID
const D = new ArrayBuffer(v);
const dView = new Uint8Array(D);
for(let i = 0; i < D.byteLength; i++)
dView[i] = 3; // The ID value equal to "3" for MACing (see B.3 of standard)
//endregion
//region Concatenate copies of the salt together to create a string S of length v * ceil(s / v) bytes (the final copy of the salt may be trunacted to create S)
const saltLength = salt.byteLength;
const sLen = v * Math.ceil(saltLength / v);
const S = new ArrayBuffer(sLen);
const sView = new Uint8Array(S);
const saltView = new Uint8Array(salt);
for(let i = 0; i < sLen; i++)
sView[i] = saltView[i % saltLength];
//endregion
//region Concatenate copies of the password together to create a string P of length v * ceil(p / v) bytes (the final copy of the password may be truncated to create P)
const passwordLength = password.byteLength;
const pLen = v * Math.ceil(passwordLength / v);
const P = new ArrayBuffer(pLen);
const pView = new Uint8Array(P);
const passwordView = new Uint8Array(password);
for(let i = 0; i < pLen; i++)
pView[i] = passwordView[i % passwordLength];
//endregion
//region Set I=S||P to be the concatenation of S and P
const sPlusPLength = S.byteLength + P.byteLength;
let I = new ArrayBuffer(sPlusPLength);
let iView = new Uint8Array(I);
iView.set(sView);
iView.set(pView, sView.length);
//endregion
//region Set c=ceil(n / u)
const c = Math.ceil((keyLength >> 3) / u);
//endregion
//region Initial variables
let internalSequence = Promise.resolve(I);
//endregion
//region For i=1, 2, ..., c, do the following:
for(let i = 0; i <= c; i++)
{
internalSequence = internalSequence.then(_I =>
{
//region Create contecanetion of D and I
const dAndI = new ArrayBuffer(D.byteLength + _I.byteLength);
const dAndIView = new Uint8Array(dAndI);
dAndIView.set(dView);
dAndIView.set(iView, dView.length);
//endregion
return dAndI;
});
//region Make "iterationCount" rounds of hashing
for(let j = 0; j < iterationCount; j++)
internalSequence = internalSequence.then(roundBuffer => cryptoEngine.digest({ name: hashAlgorithm }, new Uint8Array(roundBuffer)));
//endregion
internalSequence = internalSequence.then(roundBuffer =>
{
//region Concatenate copies of Ai to create a string B of length v bits (the final copy of Ai may be truncated to create B)
const B = new ArrayBuffer(v);
const bView = new Uint8Array(B);
for(let j = 0; j < B.byteLength; j++)
bView[j] = roundBuffer[j % roundBuffer.length];
//endregion
//region Make new I value
const k = Math.ceil(saltLength / v) + Math.ceil(passwordLength / v);
const iRound = [];
let sliceStart = 0;
let sliceLength = v;
for(let j = 0; j < k; j++)
{
const chunk = Array.from(new Uint8Array(I.slice(sliceStart, sliceStart + sliceLength)));
sliceStart += v;
if((sliceStart + v) > I.byteLength)
sliceLength = I.byteLength - sliceStart;
let x = 0x1ff;
for(let l = (B.byteLength - 1); l >= 0; l--)
{
x >>= 8;
x += bView[l] + chunk[l];
chunk[l] = (x & 0xff);
}
iRound.push(...chunk);
}
I = new ArrayBuffer(iRound.length);
iView = new Uint8Array(I);
iView.set(iRound);
//endregion
result.push(...(new Uint8Array(roundBuffer)));
return I;
});
}
//endregion
//region Initialize final key
internalSequence = internalSequence.then(() =>
{
const resultBuffer = new ArrayBuffer(keyLength >> 3);
const resultView = new Uint8Array(resultBuffer);
resultView.set((new Uint8Array(result)).slice(0, keyLength >> 3));
return resultBuffer;
});
//endregion
//endregion
return internalSequence;
}
//**************************************************************************************
/**
* Default cryptographic engine for Web Cryptography API
*/
export default class CryptoEngine
{
//**********************************************************************************
/**
* Constructor for CryptoEngine class
* @param {Object} [parameters={}]
* @property {Object} [schema] asn1js parsed value
*/
constructor(parameters = {})
{
//region Internal properties of the object
/**
* @type {Object}
* @description Usually here we are expecting "window.crypto" or an equivalent from custom "crypto engine"
*/
this.crypto = getParametersValue(parameters, "crypto", {});
/**
* @type {Object}
* @description Usually here we are expecting "window.crypto.subtle" or an equivalent from custom "crypto engine"
*/
this.subtle = getParametersValue(parameters, "subtle", {});
/**
* @type {string}
* @description Name of the "crypto engine"
*/
this.name = getParametersValue(parameters, "name", "");
//endregion
}
//**********************************************************************************
/**
* Import WebCrypto keys from different formats
* @param {string} format
* @param {ArrayBuffer|Uint8Array} keyData
* @param {Object} algorithm
* @param {boolean} extractable
* @param {Array} keyUsages
* @returns {Promise}
*/
importKey(format, keyData, algorithm, extractable, keyUsages)
{
//region Initial variables
let jwk = {};
//endregion
//region Change "keyData" type if needed
if(keyData instanceof Uint8Array)
keyData = keyData.buffer;
//endregion
switch(format.toLowerCase())
{
case "raw":
return this.subtle.importKey("raw", keyData, algorithm, extractable, keyUsages);
case "spki":
{
const asn1 = asn1js.fromBER(keyData);
if(asn1.offset === (-1))
return Promise.reject("Incorrect keyData");
const publicKeyInfo = new PublicKeyInfo();
try
{
publicKeyInfo.fromSchema(asn1.result);
}
catch(ex)
{
return Promise.reject("Incorrect keyData");
}
// noinspection FallThroughInSwitchStatementJS
switch(algorithm.name.toUpperCase())
{
case "RSA-PSS":
{
//region Get information about used hash function
switch(algorithm.hash.name.toUpperCase())
{
case "SHA-1":
jwk.alg = "PS1";
break;
case "SHA-256":
jwk.alg = "PS256";
break;
case "SHA-384":
jwk.alg = "PS384";
break;
case "SHA-512":
jwk.alg = "PS512";
break;
default:
return Promise.reject(`Incorrect hash algorithm: ${algorithm.hash.name.toUpperCase()}`);
}
//endregion
}
// break omitted
case "RSASSA-PKCS1-V1_5":
{
keyUsages = ["verify"]; // Override existing keyUsages value since the key is a public key
jwk.kty = "RSA";
jwk.ext = extractable;
jwk.key_ops = keyUsages;
if(publicKeyInfo.algorithm.algorithmId !== "1.2.840.113549.1.1.1")
return Promise.reject(`Incorrect public key algorithm: ${publicKeyInfo.algorithm.algorithmId}`);
//region Get information about used hash function
if(("alg" in jwk) === false)
{
switch(algorithm.hash.name.toUpperCase())
{
case "SHA-1":
jwk.alg = "RS1";
break;
case "SHA-256":
jwk.alg = "RS256";
break;
case "SHA-384":
jwk.alg = "RS384";
break;
case "SHA-512":
jwk.alg = "RS512";
break;
default:
return Promise.reject(`Incorrect public key algorithm: ${publicKeyInfo.algorithm.algorithmId}`);
}
}
//endregion
//region Create RSA Public Key elements
const publicKeyJSON = publicKeyInfo.toJSON();
for(const key of Object.keys(publicKeyJSON))
jwk[key] = publicKeyJSON[key];
//endregion
}
break;
case "ECDSA":
keyUsages = ["verify"]; // Override existing keyUsages value since the key is a public key
// break omitted
case "ECDH":
{
//region Initial variables
jwk = {
kty: "EC",
ext: extractable,
key_ops: keyUsages
};
//endregion
//region Get information about algorithm
if(publicKeyInfo.algorithm.algorithmId !== "1.2.840.10045.2.1")
return Promise.reject(`Incorrect public key algorithm: ${publicKeyInfo.algorithm.algorithmId}`);
//endregion
//region Create ECDSA Public Key elements
const publicKeyJSON = publicKeyInfo.toJSON();
for(const key of Object.keys(publicKeyJSON))
jwk[key] = publicKeyJSON[key];
//endregion
}
break;
case "RSA-OAEP":
{
jwk.kty = "RSA";
jwk.ext = extractable;
jwk.key_ops = keyUsages;
if(this.name.toLowerCase() === "safari")
jwk.alg = "RSA-OAEP";
else
{
switch(algorithm.hash.name.toUpperCase())
{
case "SHA-1":
jwk.alg = "RSA-OAEP";
break;
case "SHA-256":
jwk.alg = "RSA-OAEP-256";
break;
case "SHA-384":
jwk.alg = "RSA-OAEP-384";
break;
case "SHA-512":
jwk.alg = "RSA-OAEP-512";
break;
default:
return Promise.reject(`Incorrect public key algorithm: ${publicKeyInfo.algorithm.algorithmId}`);
}
}
//region Create ECDSA Public Key elements
const publicKeyJSON = publicKeyInfo.toJSON();
for(const key of Object.keys(publicKeyJSON))
jwk[key] = publicKeyJSON[key];
//endregion
}
break;
default:
return Promise.reject(`Incorrect algorithm name: ${algorithm.name.toUpperCase()}`);
}
}
break;
case "pkcs8":
{
const privateKeyInfo = new PrivateKeyInfo();
//region Parse "PrivateKeyInfo" object
const asn1 = asn1js.fromBER(keyData);
if(asn1.offset === (-1))
return Promise.reject("Incorrect keyData");
try
{
privateKeyInfo.fromSchema(asn1.result);
}
catch(ex)
{
return Promise.reject("Incorrect keyData");
}
if(("parsedKey" in privateKeyInfo) === false)
return Promise.reject("Incorrect keyData");
//endregion
// noinspection FallThroughInSwitchStatementJS
// noinspection FallThroughInSwitchStatementJS
switch(algorithm.name.toUpperCase())
{
case "RSA-PSS":
{
//region Get information about used hash function
switch(algorithm.hash.name.toUpperCase())
{
case "SHA-1":
jwk.alg = "PS1";
break;
case "SHA-256":
jwk.alg = "PS256";
break;
case "SHA-384":
jwk.alg = "PS384";
break;
case "SHA-512":
jwk.alg = "PS512";
break;
default:
return Promise.reject(`Incorrect hash algorithm: ${algorithm.hash.name.toUpperCase()}`);
}
//endregion
}
// break omitted
case "RSASSA-PKCS1-V1_5":
{
keyUsages = ["sign"]; // Override existing keyUsages value since the key is a private key
jwk.kty = "RSA";
jwk.ext = extractable;
jwk.key_ops = keyUsages;
//region Get information about used hash function
if(privateKeyInfo.privateKeyAlgorithm.algorithmId !== "1.2.840.113549.1.1.1")
return Promise.reject(`Incorrect private key algorithm: ${privateKeyInfo.privateKeyAlgorithm.algorithmId}`);
//endregion
//region Get information about used hash function
if(("alg" in jwk) === false)
{
switch(algorithm.hash.name.toUpperCase())
{
case "SHA-1":
jwk.alg = "RS1";
break;
case "SHA-256":
jwk.alg = "RS256";
break;
case "SHA-384":
jwk.alg = "RS384";
break;
case "SHA-512":
jwk.alg = "RS512";
break;
default:
return Promise.reject(`Incorrect hash algorithm: ${algorithm.hash.name.toUpperCase()}`);
}
}
//endregion
//region Create RSA Private Key elements
const privateKeyJSON = privateKeyInfo.toJSON();
for(const key of Object.keys(privateKeyJSON))
jwk[key] = privateKeyJSON[key];
//endregion
}
break;
case "ECDSA":
keyUsages = ["sign"]; // Override existing keyUsages value since the key is a private key
// break omitted
case "ECDH":
{
//region Initial variables
jwk = {
kty: "EC",
ext: extractable,
key_ops: keyUsages
};
//endregion
//region Get information about used hash function
if(privateKeyInfo.privateKeyAlgorithm.algorithmId !== "1.2.840.10045.2.1")
return Promise.reject(`Incorrect algorithm: ${privateKeyInfo.privateKeyAlgorithm.algorithmId}`);
//endregion
//region Create ECDSA Private Key elements
const privateKeyJSON = privateKeyInfo.toJSON();
for(const key of Object.keys(privateKeyJSON))
jwk[key] = privateKeyJSON[key];
//endregion
}
break;
case "RSA-OAEP":
{
jwk.kty = "RSA";
jwk.ext = extractable;
jwk.key_ops = keyUsages;
//region Get information about used hash function
if(this.name.toLowerCase() === "safari")
jwk.alg = "RSA-OAEP";
else
{
switch(algorithm.hash.name.toUpperCase())
{
case "SHA-1":
jwk.alg = "RSA-OAEP";
break;
case "SHA-256":
jwk.alg = "RSA-OAEP-256";
break;
case "SHA-384":
jwk.alg = "RSA-OAEP-384";
break;
case "SHA-512":
jwk.alg = "RSA-OAEP-512";
break;
default:
return Promise.reject(`Incorrect hash algorithm: ${algorithm.hash.name.toUpperCase()}`);
}
}
//endregion
//region Create RSA Private Key elements
const privateKeyJSON = privateKeyInfo.toJSON();
for(const key of Object.keys(privateKeyJSON))
jwk[key] = privateKeyJSON[key];
//endregion
}
break;
default:
return Promise.reject(`Incorrect algorithm name: ${algorithm.name.toUpperCase()}`);
}
}
break;
case "jwk":
jwk = keyData;
break;
default:
return Promise.reject(`Incorrect format: ${format}`);
}
//region Special case for Safari browser (since its acting not as WebCrypto standard describes)
if(this.name.toLowerCase() === "safari")
{
// Try to use both ways - import using ArrayBuffer and pure JWK (for Safari Technology Preview)
return Promise.resolve().then(() => this.subtle.importKey("jwk", stringToArrayBuffer(JSON.stringify(jwk)), algorithm, extractable, keyUsages))
.then(result => result, () => this.subtle.importKey("jwk", jwk, algorithm, extractable, keyUsages));
}
//endregion
return this.subtle.importKey("jwk", jwk, algorithm, extractable, keyUsages);
}
//**********************************************************************************
/**
* Export WebCrypto keys to different formats
* @param {string} format
* @param {Object} key
* @returns {Promise}
*/
exportKey(format, key)
{
let sequence = this.subtle.exportKey("jwk", key);
//region Currently Safari returns ArrayBuffer as JWK thus we need an additional transformation
if(this.name.toLowerCase() === "safari")
{
sequence = sequence.then(result =>
{
// Some additional checks for Safari Technology Preview
if(result instanceof ArrayBuffer)
return JSON.parse(arrayBufferToString(result));
return result;
});
}
//endregion
switch(format.toLowerCase())
{
case "raw":
return this.subtle.exportKey("raw", key);
case "spki":
sequence = sequence.then(result =>
{
const publicKeyInfo = new PublicKeyInfo();
try
{
publicKeyInfo.fromJSON(result);
}
catch(ex)
{
return Promise.reject("Incorrect key data");
}
return publicKeyInfo.toSchema().toBER(false);
});
break;
case "pkcs8":
sequence = sequence.then(result =>
{
const privateKeyInfo = new PrivateKeyInfo();
try
{
privateKeyInfo.fromJSON(result);
}
catch(ex)
{
return Promise.reject("Incorrect key data");
}
return privateKeyInfo.toSchema().toBER(false);
});
break;
case "jwk":
break;
default:
return Promise.reject(`Incorrect format: ${format}`);
}
return sequence;
}
//**********************************************************************************
/**
* Convert WebCrypto keys between different export formats
* @param {string} inputFormat
* @param {string} outputFormat
* @param {ArrayBuffer|Object} keyData
* @param {Object} algorithm
* @param {boolean} extractable
* @param {Array} keyUsages
* @returns {Promise}
*/
convert(inputFormat, outputFormat, keyData, algorithm, extractable, keyUsages)
{
switch(inputFormat.toLowerCase())
{
case "raw":
switch(outputFormat.toLowerCase())
{
case "raw":
return Promise.resolve(keyData);
case "spki":
return Promise.resolve()
.then(() => this.importKey("raw", keyData, algorithm, extractable, keyUsages))
.then(result => this.exportKey("spki", result));
case "pkcs8":
return Promise.resolve()
.then(() => this.importKey("raw", keyData, algorithm, extractable, keyUsages))
.then(result => this.exportKey("pkcs8", result));
case "jwk":
return Promise.resolve()
.then(() => this.importKey("raw", keyData, algorithm, extractable, keyUsages))
.then(result => this.exportKey("jwk", result));
default:
return Promise.reject(`Incorrect outputFormat: ${outputFormat}`);
}
case "spki":
switch(outputFormat.toLowerCase())
{
case "raw":
return Promise.resolve()
.then(() => this.importKey("spki", keyData, algorithm, extractable, keyUsages))
.then(result => this.exportKey("raw", result));
case "spki":
return Promise.resolve(keyData);
case "pkcs8":
return Promise.reject("Impossible to convert between SPKI/PKCS8");
case "jwk":
return Promise.resolve()
.then(() => this.importKey("spki", keyData, algorithm, extractable, keyUsages))
.then(result => this.exportKey("jwk", result));
default:
return Promise.reject(`Incorrect outputFormat: ${outputFormat}`);
}
case "pkcs8":
switch(outputFormat.toLowerCase())
{
case "raw":
return Promise.resolve()
.then(() => this.importKey("pkcs8", keyData, algorithm, extractable, keyUsages))
.then(result => this.exportKey("raw", result));
case "spki":
return Promise.reject("Impossible to convert between SPKI/PKCS8");
case "pkcs8":
return Promise.resolve(keyData);
case "jwk":
return Promise.resolve()
.then(() => this.importKey("pkcs8", keyData, algorithm, extractable, keyUsages))
.then(result => this.exportKey("jwk", result));
default:
return Promise.reject(`Incorrect outputFormat: ${outputFormat}`);
}
case "jwk":
switch(outputFormat.toLowerCase())
{
case "raw":
return Promise.resolve()
.then(() => this.importKey("jwk", keyData, algorithm, extractable, keyUsages))
.then(result => this.exportKey("raw", result));
case "spki":
return Promise.resolve()
.then(() => this.importKey("jwk", keyData, algorithm, extractable, keyUsages))
.then(result => this.exportKey("spki", result));
case "pkcs8":
return Promise.resolve()
.then(() => this.importKey("jwk", keyData, algorithm, extractable, keyUsages))
.then(result => this.exportKey("pkcs8", result));
case "jwk":
return Promise.resolve(keyData);
default:
return Promise.reject(`Incorrect outputFormat: ${outputFormat}`);
}
default:
return Promise.reject(`Incorrect inputFormat: ${inputFormat}`);
}
}
//**********************************************************************************
/**
* Wrapper for standard function "encrypt"
* @param args
* @returns {Promise}
*/
encrypt(...args)
{
return this.subtle.encrypt(...args);
}
//**********************************************************************************
/**
* Wrapper for standard function "decrypt"
* @param args
* @returns {Promise}
*/
decrypt(...args)
{
return this.subtle.decrypt(...args);
}
//**********************************************************************************
/**
* Wrapper for standard function "sign"
* @param args
* @returns {Promise}
*/
sign(...args)
{
return this.subtle.sign(...args);
}
//**********************************************************************************
/**
* Wrapper for standard function "verify"
* @param args
* @returns {Promise}
*/
verify(...args)
{
return this.subtle.verify(...args);
}
//**********************************************************************************
/**
* Wrapper for standard function "digest"
* @param args
* @returns {Promise}
*/
digest(...args)
{
return this.subtle.digest(...args);
}
//**********************************************************************************
/**
* Wrapper for standard function "generateKey"
* @param args
* @returns {Promise}
*/
generateKey(...args)
{
return this.subtle.generateKey(...args);
}
//**********************************************************************************
/**
* Wrapper for standard function "deriveKey"
* @param args
* @returns {Promise}
*/
deriveKey(...args)
{
return this.subtle.deriveKey(...args);
}
//**********************************************************************************
/**
* Wrapper for standard function "deriveBits"
* @param args
* @returns {Promise}
*/
deriveBits(...args)
{
return this.subtle.deriveBits(...args);
}
//**********************************************************************************
/**
* Wrapper for standard function "wrapKey"
* @param args
* @returns {Promise}
*/
wrapKey(...args)
{
return this.subtle.wrapKey(...args);
}
//**********************************************************************************
/**
* Wrapper for standard function "unwrapKey"
* @param args
* @returns {Promise}
*/
unwrapKey(...args)
{
return this.subtle.unwrapKey(...args);
}
//**********************************************************************************
/**
* Initialize input Uint8Array by random values (with help from current "crypto engine")
* @param {!Uint8Array} view
* @returns {*}
*/
getRandomValues(view)
{
if(("getRandomValues" in this.crypto) === false)
throw new Error("No support for getRandomValues");
return this.crypto.getRandomValues(view);
}
//**********************************************************************************
/**
* Get WebCrypto algorithm by wel-known OID
* @param {string} oid well-known OID to search for
* @returns {Object}
*/
getAlgorithmByOID(oid)
{
switch(oid)
{
case "1.2.840.113549.1.1.1":
case "1.2.840.113549.1.1.5":
return {
name: "RSASSA-PKCS1-v1_5",
hash: {
name: "SHA-1"
}
};
case "1.2.840.113549.1.1.11":
return {
name: "RSASSA-PKCS1-v1_5",
hash: {
name: "SHA-256"
}
};
case "1.2.840.113549.1.1.12":
return {
name: "RSASSA-PKCS1-v1_5",
hash: {
name: "SHA-384"
}
};
case "1.2.840.113549.1.1.13":
return {
name: "RSASSA-PKCS1-v1_5",
hash: {
name: "SHA-512"
}
};
case "1.2.840.113549.1.1.10":
return {
name: "RSA-PSS"
};
case "1.2.840.113549.1.1.7":
return {
name: "RSA-OAEP"
};
case "1.2.840.10045.2.1":
case "1.2.840.10045.4.1":
return {
name: "ECDSA",
hash: {
name: "SHA-1"
}
};
case "1.2.840.10045.4.3.2":
return {
name: "ECDSA",
hash: {
name: "SHA-256"
}
};
case "1.2.840.10045.4.3.3":
return {
name: "ECDSA",
hash: {
name: "SHA-384"
}
};
case "1.2.840.10045.4.3.4":
return {
name: "ECDSA",
hash: {
name: "SHA-512"
}
};
case "1.3.133.16.840.63.0.2":
return {
name: "ECDH",
kdf: "SHA-1"
};
case "1.3.132.1.11.1":
return {
name: "ECDH",
kdf: "SHA-256"
};
case "1.3.132.1.11.2":
return {
name: "ECDH",
kdf: "SHA-384"
};
case "1.3.132.1.11.3":
return {
name: "ECDH",
kdf: "SHA-512"
};
case "2.16.840.1.101.3.4.1.2":
return {
name: "AES-CBC",
length: 128
};
case "2.16.840.1.101.3.4.1.22":
return {
name: "AES-CBC",
length: 192
};
case "2.16.840.1.101.3.4.1.42":
return {
name: "AES-CBC",
length: 256
};
case "2.16.840.1.101.3.4.1.6":
return {
name: "AES-GCM",
length: 128
};
case "2.16.840.1.101.3.4.1.26":
return {
name: "AES-GCM",
length: 192
};
case "2.16.840.1.101.3.4.1.46":
return {
name: "AES-GCM",
length: 256
};
case "2.16.840.1.101.3.4.1.4":
return {
name: "AES-CFB",
length: 128
};
case "2.16.840.1.101.3.4.1.24":
return {
name: "AES-CFB",
length: 192
};
case "2.16.840.1.101.3.4.1.44":
return {
name: "AES-CFB",
length: 256
};
case "2.16.840.1.101.3.4.1.5":
return {
name: "AES-KW",
length: 128
};
case "2.16.840.1.101.3.4.1.25":
return {
name: "AES-KW",
length: 192
};
case "2.16.840.1.101.3.4.1.45":
return {
name: "AES-KW",
length: 256
};
case "1.2.840.113549.2.7":
return {
name: "HMAC",
hash: {
name: "SHA-1"
}
};
case "1.2.840.113549.2.9":
return {
name: "HMAC",
hash: {
name: "SHA-256"
}
};
case "1.2.840.113549.2.10":
return {
name: "HMAC",
hash: {
name: "SHA-384"
}
};
case "1.2.840.113549.2.11":
return {
name: "HMAC",
hash: {
name: "SHA-512"
}
};
case "1.2.840.113549.1.9.16.3.5":
return {
name: "DH"
};
case "1.3.14.3.2.26":
return {
name: "SHA-1"
};
case "2.16.840.1.101.3.4.2.1":
return {
name: "SHA-256"
};
case "2.16.840.1.101.3.4.2.2":
return {
name: "SHA-384"
};
case "2.16.840.1.101.3.4.2.3":
return {
name: "SHA-512"
};
case "1.2.840.113549.1.5.12":
return {
name: "PBKDF2"
};
//region Special case - OIDs for ECC curves
case "1.2.840.10045.3.1.7":
return {
name: "P-256"
};
case "1.3.132.0.34":
return {
name: "P-384"
};
case "1.3.132.0.35":
return {
name: "P-521"
};
//endregion
default:
}
return {};
}
//**********************************************************************************
/**
* Get OID for each specific algorithm
* @param {Object} algorithm
* @returns {string}
*/
getOIDByAlgorithm(algorithm)
{
let result = "";
switch(algorithm.name.toUpperCase())
{
case "RSASSA-PKCS1-V1_5":
switch(algorithm.hash.name.toUpperCase())
{
case "SHA-1":
result = "1.2.840.113549.1.1.5";
break;
case "SHA-256":
result = "1.2.840.113549.1.1.11";
break;
case "SHA-384":
result = "1.2.840.113549.1.1.12";
break;
case "SHA-512":
result = "1.2.840.113549.1.1.13";
break;
default:
}
break;
case "RSA-PSS":
result = "1.2.840.113549.1.1.10";
break;
case "RSA-OAEP":
result = "1.2.840.113549.1.1.7";
break;
case "ECDSA":
switch(algorithm.hash.name.toUpperCase())
{
case "SHA-1":
result = "1.2.840.10045.4.1";
break;
case "SHA-256":
result = "1.2.840.10045.4.3.2";
break;
case "SHA-384":
result = "1.2.840.10045.4.3.3";
break;
case "SHA-512":
result = "1.2.840.10045.4.3.4";
break;
default:
}
break;
case "ECDH":
switch(algorithm.kdf.toUpperCase()) // Non-standard addition - hash algorithm of KDF function
{
case "SHA-1":
result = "1.3.133.16.840.63.0.2"; // dhSinglePass-stdDH-sha1kdf-scheme
break;
case "SHA-256":
result = "1.3.132.1.11.1"; // dhSinglePass-stdDH-sha256kdf-scheme
break;
case "SHA-384":
result = "1.3.132.1.11.2"; // dhSinglePass-stdDH-sha384kdf-scheme
break;
case "SHA-512":
result = "1.3.132.1.11.3"; // dhSinglePass-stdDH-sha512kdf-scheme
break;
default:
}
break;
case "AES-CTR":
break;
case "AES-CBC":
switch(algorithm.length)
{
case 128:
result = "2.16.840.1.101.3.4.1.2";
break;
case 192:
result = "2.16.840.1.101.3.4.1.22";
break;
case 256:
result = "2.16.840.1.101.3.4.1.42";
break;
default:
}
break;
case "AES-CMAC":
break;
case "AES-GCM":
switch(algorithm.length)
{
case 128:
result = "2.16.840.1.101.3.4.1.6";
break;
case 192:
result = "2.16.840.1.101.3.4.1.26";
break;
case 256:
result = "2.16.840.1.101.3.4.1.46";
break;
default:
}
break;
case "AES-CFB":
switch(algorithm.length)
{
case 128:
result = "2.16.840.1.101.3.4.1.4";
break;
case 192:
result = "2.16.840.1.101.3.4.1.24";
break;
case 256:
result = "2.16.840.1.101.3.4.1.44";
break;
default:
}
break;
case "AES-KW":
switch(algorithm.length)
{
case 128:
result = "2.16.840.1.101.3.4.1.5";
break;
case 192:
result = "2.16.840.1.101.3.4.1.25";
break;
case 256:
result = "2.16.840.1.101.3.4.1.45";
break;
default:
}
break;
case "HMAC":
switch(algorithm.hash.name.toUpperCase())
{
case "SHA-1":
result = "1.2.840.113549.2.7";
break;
case "SHA-256":
result = "1.2.840.113549.2.9";
break;
case "SHA-384":
result = "1.2.840.113549.2.10";
break;
case "SHA-512":
result = "1.2.840.113549.2.11";
break;
default:
}
break;
case "DH":
result = "1.2.840.113549.1.9.16.3.5";
break;
case "SHA-1":
result = "1.3.14.3.2.26";
break;
case "SHA-256":
result = "2.16.840.1.101.3.4.2.1";
break;
case "SHA-384":
result = "2.16.840.1.101.3.4.2.2";
break;
case "SHA-512":
result = "2.16.840.1.101.3.4.2.3";
break;
case "CONCAT":
break;
case "HKDF":
break;
case "PBKDF2":
result = "1.2.840.113549.1.5.12";
break;
//region Special case - OIDs for ECC curves
case "P-256":
result = "1.2.840.10045.3.1.7";
break;
case "P-384":
result = "1.3.132.0.34";
break;
case "P-521":
result = "1.3.132.0.35";
break;
//endregion
default:
}
return result;
}
//**********************************************************************************
/**
* 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 {*}
*/
getAlgorithmParameters(algorithmName, operation)
{
let result = {
algorithm: {},
usages: []
};
switch(algorithmName.toUpperCase())
{
case "RSASSA-PKCS1-V1_5":
switch(operation.toLowerCase())
{
case "generatekey":
result = {
algorithm: {
name: "RSASSA-PKCS1-v1_5",
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {
name: "SHA-256"
}
},
usages: ["sign", "verify"]
};
break;
case "verify":
case "sign":
case "importkey":
result = {
algorithm: {
name: "RSASSA-PKCS1-v1_5",
hash: {
name: "SHA-256"
}
},
usages: ["verify"] // For importKey("pkcs8") usage must be "sign" only
};
break;
case "exportkey":
default:
return {
algorithm: {
name: "RSASSA-PKCS1-v1_5"
},
usages: []
};
}
break;
case "RSA-PSS":
switch(operation.toLowerCase())
{
case "sign":
case "verify":
result = {
algorithm: {
name: "RSA-PSS",
hash: {
name: "SHA-1"
},
saltLength: 20
},
usages: ["sign", "verify"]
};
break;
case "generatekey":
result = {
algorithm: {
name: "RSA-PSS",
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {
name: "SHA-1"
}
},
usages: ["sign", "verify"]
};
break;
case "importkey":
result = {
algorithm: {
name: "RSA-PSS",
hash: {
name: "SHA-1"
}
},
usages: ["verify"] // For importKey("pkcs8") usage must be "sign" only
};
break;
case "exportkey":
default:
return {
algorithm: {
name: "RSA-PSS"
},
usages: []
};
}
break;
case "RSA-OAEP":
switch(operation.toLowerCase())
{
case "encrypt":
case "decrypt":
result = {
algorithm: {
name: "RSA-OAEP"
},
usages: ["encrypt", "decrypt"]
};
break;
case "generatekey":
result = {
algorithm: {
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {
name: "SHA-256"
}
},
usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
};
break;
case "importkey":
result = {
algorithm: {
name: "RSA-OAEP",
hash: {
name: "SHA-256"
}
},
usages: ["encrypt"] // encrypt for "spki" and decrypt for "pkcs8"
};
break;
case "exportkey":
default:
return {
algorithm: {
name: "RSA-OAEP"
},
usages: []
};
}
break;
case "ECDSA":
switch(operation.toLowerCase())
{
case "generatekey":
result = {
algorithm: {
name: "ECDSA",
namedCurve: "P-256"
},
usages: ["sign", "verify"]
};
break;
case "importkey":
result = {
algorithm: {
name: "ECDSA",
namedCurve: "P-256"
},
usages: ["verify"] // "sign" for "pkcs8"
};
break;
case "verify":
case "sign":
result = {
algorithm: {
name: "ECDSA",
hash: {
name: "SHA-256"
}
},
usages: ["sign"]
};
break;
default:
return {
algorithm: {
name: "ECDSA"
},
usages: []
};
}
break;
case "ECDH":
switch(operation.toLowerCase())
{
case "exportkey":
case "importkey":
case "generatekey":
result = {
algorithm: {
name: "ECDH",
namedCurve: "P-256"
},
usages: ["deriveKey", "deriveBits"]
};
break;
case "derivekey":
case "derivebits":
result = {
algorithm: {
name: "ECDH",
namedCurve: "P-256",
public: [] // Must be a "publicKey"
},
usages: ["encrypt", "decrypt"]
};
break;
default:
return {
algorithm: {
name: "ECDH"
},
usages: []
};
}
break;
case "AES-CTR":
switch(operation.toLowerCase())
{
case "importkey":
case "exportkey":
case "generatekey":
result = {
algorithm: {
name: "AES-CTR",
length: 256
},
usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
};
break;
case "decrypt":
case "encrypt":
result = {
algorithm: {
name: "AES-CTR",
counter: new Uint8Array(16),
length: 10
},
usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
};
break;
default:
return {
algorithm: {
name: "AES-CTR"
},
usages: []
};
}
break;
case "AES-CBC":
switch(operation.toLowerCase())
{
case "importkey":
case "exportkey":
case "generatekey":
result = {
algorithm: {
name: "AES-CBC",
length: 256
},
usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
};
break;
case "decrypt":
case "encrypt":
result = {
algorithm: {
name: "AES-CBC",
iv: this.getRandomValues(new Uint8Array(16)) // For "decrypt" the value should be replaced with value got on "encrypt" step
},
usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
};
break;
default:
return {
algorithm: {
name: "AES-CBC"
},
usages: []
};
}
break;
case "AES-GCM":
switch(operation.toLowerCase())
{
case "importkey":
case "exportkey":
case "generatekey":
result = {
algorithm: {
name: "AES-GCM",
length: 256
},
usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
};
break;
case "decrypt":
case "encrypt":
result = {
algorithm: {
name: "AES-GCM",
iv: this.getRandomValues(new Uint8Array(16)) // For "decrypt" the value should be replaced with value got on "encrypt" step
},
usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
};
break;
default:
return {
algorithm: {
name: "AES-GCM"
},
usages: []
};
}
break;
case "AES-KW":
switch(operation.toLowerCase())
{
case "importkey":
case "exportkey":
case "generatekey":
case "wrapkey":
case "unwrapkey":
result = {
algorithm: {
name: "AES-KW",
length: 256
},
usages: ["wrapKey", "unwrapKey"]
};
break;
default:
return {
algorithm: {
name: "AES-KW"
},
usages: []
};
}
break;
case "HMAC":
switch(operation.toLowerCase())
{
case "sign":
case "verify":
result = {
algorithm: {
name: "HMAC"
},
usages: ["sign", "verify"]
};
break;
case "importkey":
case "exportkey":
case "generatekey":
result = {
algorithm: {
name: "HMAC",
length: 32,
hash: {
name: "SHA-256"
}
},
usages: ["sign", "verify"]
};
break;
default:
return {
algorithm: {
name: "HMAC"
},
usages: []
};
}
break;
case "HKDF":
switch(operation.toLowerCase())
{
case "derivekey":
result = {
algorithm: {
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array([]),
info: new Uint8Array([])
},
usages: ["encrypt", "decrypt"]
};
break;
default:
return {
algorithm: {
name: "HKDF"
},
usages: []
};
}
break;
case "PBKDF2":
switch(operation.toLowerCase())
{
case "derivekey":
result = {
algorithm: {
name: "PBKDF2",
hash: { name: "SHA-256" },
salt: new Uint8Array([]),
iterations: 10000
},
usages: ["encrypt", "decrypt"]
};
break;
default:
return {
algorithm: {
name: "PBKDF2"
},
usages: []
};
}
break;
default:
}
return result;
}
//**********************************************************************************
/**
* Getting hash algorithm by signature algorithm
* @param {AlgorithmIdentifier} signatureAlgorithm Signature algorithm
* @returns {string}
*/
getHashAlgorithm(signatureAlgorithm)
{
let result = "";
switch(signatureAlgorithm.algorithmId)
{
case "1.2.840.10045.4.1": // ecdsa-with-SHA1
case "1.2.840.113549.1.1.5":
result = "SHA-1";
break;
case "1.2.840.10045.4.3.2": // ecdsa-with-SHA256
case "1.2.840.113549.1.1.11":
result = "SHA-256";
break;
case "1.2.840.10045.4.3.3": // ecdsa-with-SHA384
case "1.2.840.113549.1.1.12":
result = "SHA-384";
break;
case "1.2.840.10045.4.3.4": // ecdsa-with-SHA512
case "1.2.840.113549.1.1.13":
result = "SHA-512";
break;
case "1.2.840.113549.1.1.10": // RSA-PSS
{
try
{
const params = new RSASSAPSSParams({ schema: signatureAlgorithm.algorithmParams });
if("hashAlgorithm" in params)
{
const algorithm = this.getAlgorithmByOID(params.hashAlgorithm.algorithmId);
if(("name" in algorithm) === false)
return "";
result = algorithm.name;
}
else
result = "SHA-1";
}
catch(ex)
{
}
}
break;
default:
}
return result;
}
//**********************************************************************************
/**
* Specialized function encrypting "EncryptedContentInfo" object using parameters
* @param {Object} parameters
* @returns {Promise}
*/
encryptEncryptedContentInfo(parameters)
{
//region Check for input parameters
if((parameters instanceof Object) === false)
return Promise.reject("Parameters must have type \"Object\"");
if(("password" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"password\"");
if(("contentEncryptionAlgorithm" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"contentEncryptionAlgorithm\"");
if(("hmacHashAlgorithm" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"hmacHashAlgorithm\"");
if(("iterationCount" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"iterationCount\"");
if(("contentToEncrypt" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"contentToEncrypt\"");
if(("contentType" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"contentType\"");
const contentEncryptionOID = this.getOIDByAlgorithm(parameters.contentEncryptionAlgorithm);
if(contentEncryptionOID === "")
return Promise.reject("Wrong \"contentEncryptionAlgorithm\" value");
const pbkdf2OID = this.getOIDByAlgorithm({
name: "PBKDF2"
});
if(pbkdf2OID === "")
return Promise.reject("Can not find OID for PBKDF2");
const hmacOID = this.getOIDByAlgorithm({
name: "HMAC",
hash: {
name: parameters.hmacHashAlgorithm
}
});
if(hmacOID === "")
return Promise.reject(`Incorrect value for "hmacHashAlgorithm": ${parameters.hmacHashAlgorithm}`);
//endregion
//region Initial variables
let sequence = Promise.resolve();
const ivBuffer = new ArrayBuffer(16); // For AES we need IV 16 bytes long
const ivView = new Uint8Array(ivBuffer);
this.getRandomValues(ivView);
const saltBuffer = new ArrayBuffer(64);
const saltView = new Uint8Array(saltBuffer);
this.getRandomValues(saltView);
const contentView = new Uint8Array(parameters.contentToEncrypt);
const pbkdf2Params = new PBKDF2Params({
salt: new asn1js.OctetString({ valueHex: saltBuffer }),
iterationCount: parameters.iterationCount,
prf: new AlgorithmIdentifier({
algorithmId: hmacOID,
algorithmParams: new asn1js.Null()
})
});
//endregion
//region Derive PBKDF2 key from "password" buffer
sequence = sequence.then(() =>
{
const passwordView = new Uint8Array(parameters.password);
return this.importKey("raw",
passwordView,
"PBKDF2",
false,
["deriveKey"]);
}, error =>
Promise.reject(error)
);
//endregion
//region Derive key for "contentEncryptionAlgorithm"
sequence = sequence.then(result =>
this.deriveKey({
name: "PBKDF2",
hash: {
name: parameters.hmacHashAlgorithm
},
salt: saltView,
iterations: parameters.iterationCount
},
result,
parameters.contentEncryptionAlgorithm,
false,
["encrypt"]),
error =>
Promise.reject(error)
);
//endregion
//region Encrypt content
sequence = sequence.then(result =>
this.encrypt({
name: parameters.contentEncryptionAlgorithm.name,
iv: ivView
},
result,
contentView),
error =>
Promise.reject(error)
);
//endregion
//region Store all parameters in EncryptedData object
sequence = sequence.then(result =>
{
const pbes2Parameters = new PBES2Params({
keyDerivationFunc: new AlgorithmIdentifier({
algorithmId: pbkdf2OID,
algorithmParams: pbkdf2Params.toSchema()
}),
encryptionScheme: new AlgorithmIdentifier({
algorithmId: contentEncryptionOID,
algorithmParams: new asn1js.OctetString({ valueHex: ivBuffer })
})
});
return new EncryptedContentInfo({
contentType: parameters.contentType,
contentEncryptionAlgorithm: new AlgorithmIdentifier({
algorithmId: "1.2.840.113549.1.5.13", // pkcs5PBES2
algorithmParams: pbes2Parameters.toSchema()
}),
encryptedContent: new asn1js.OctetString({ valueHex: result })
});
}, error =>
Promise.reject(error)
);
//endregion
return sequence;
}
//**********************************************************************************
/**
* Decrypt data stored in "EncryptedContentInfo" object using parameters
* @param parameters
* @return {Promise}
*/
decryptEncryptedContentInfo(parameters)
{
//region Check for input parameters
if((parameters instanceof Object) === false)
return Promise.reject("Parameters must have type \"Object\"");
if(("password" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"password\"");
if(("encryptedContentInfo" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"encryptedContentInfo\"");
if(parameters.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId !== "1.2.840.113549.1.5.13") // pkcs5PBES2
return Promise.reject(`Unknown "contentEncryptionAlgorithm": ${parameters.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId}`);
//endregion
//region Initial variables
let sequence = Promise.resolve();
let pbes2Parameters;
try
{
pbes2Parameters = new PBES2Params({ schema: parameters.encryptedContentInfo.contentEncryptionAlgorithm.algorithmParams });
}
catch(ex)
{
return Promise.reject("Incorrectly encoded \"pbes2Parameters\"");
}
let pbkdf2Params;
try
{
pbkdf2Params = new PBKDF2Params({ schema: pbes2Parameters.keyDerivationFunc.algorithmParams });
}
catch(ex)
{
return Promise.reject("Incorrectly encoded \"pbkdf2Params\"");
}
const contentEncryptionAlgorithm = this.getAlgorithmByOID(pbes2Parameters.encryptionScheme.algorithmId);
if(("name" in contentEncryptionAlgorithm) === false)
return Promise.reject(`Incorrect OID for "contentEncryptionAlgorithm": ${pbes2Parameters.encryptionScheme.algorithmId}`);
const ivBuffer = pbes2Parameters.encryptionScheme.algorithmParams.valueBlock.valueHex;
const ivView = new Uint8Array(ivBuffer);
const saltBuffer = pbkdf2Params.salt.valueBlock.valueHex;
const saltView = new Uint8Array(saltBuffer);
const iterationCount = pbkdf2Params.iterationCount;
let hmacHashAlgorithm = "SHA-1";
if("prf" in pbkdf2Params)
{
const algorithm = this.getAlgorithmByOID(pbkdf2Params.prf.algorithmId);
if(("name" in algorithm) === false)
return Promise.reject("Incorrect OID for HMAC hash algorithm");
hmacHashAlgorithm = algorithm.hash.name;
}
//endregion
//region Derive PBKDF2 key from "password" buffer
sequence = sequence.then(() =>
this.importKey("raw",
parameters.password,
"PBKDF2",
false,
["deriveKey"]),
error =>
Promise.reject(error)
);
//endregion
//region Derive key for "contentEncryptionAlgorithm"
sequence = sequence.then(result =>
this.deriveKey({
name: "PBKDF2",
hash: {
name: hmacHashAlgorithm
},
salt: saltView,
iterations: iterationCount
},
result,
contentEncryptionAlgorithm,
false,
["decrypt"]),
error =>
Promise.reject(error)
);
//endregion
//region Decrypt internal content using derived key
sequence = sequence.then(result =>
{
//region Create correct data block for decryption
let dataBuffer = new ArrayBuffer(0);
if(parameters.encryptedContentInfo.encryptedContent.idBlock.isConstructed === false)
dataBuffer = parameters.encryptedContentInfo.encryptedContent.valueBlock.valueHex;
else
{
for(const content of parameters.encryptedContentInfo.encryptedContent.valueBlock.value)
dataBuffer = utilConcatBuf(dataBuffer, content.valueBlock.valueHex);
}
//endregion
return this.decrypt({
name: contentEncryptionAlgorithm.name,
iv: ivView
},
result,
dataBuffer);
}, error =>
Promise.reject(error)
);
//endregion
return sequence;
}
//**********************************************************************************
/**
* Stamping (signing) data using algorithm simular to HMAC
* @param {Object} parameters
* @return {Promise.<T>|Promise}
*/
stampDataWithPassword(parameters)
{
//region Check for input parameters
if((parameters instanceof Object) === false)
return Promise.reject("Parameters must have type \"Object\"");
if(("password" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"password\"");
if(("hashAlgorithm" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"hashAlgorithm\"");
if(("salt" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"iterationCount\"");
if(("iterationCount" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"salt\"");
if(("contentToStamp" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"contentToStamp\"");
//endregion
//region Choose correct length for HMAC key
let length;
switch(parameters.hashAlgorithm.toLowerCase())
{
case "sha-1":
length = 160;
break;
case "sha-256":
length = 256;
break;
case "sha-384":
length = 384;
break;
case "sha-512":
length = 512;
break;
default:
return Promise.reject(`Incorrect "parameters.hashAlgorithm" parameter: ${parameters.hashAlgorithm}`);
}
//endregion
//region Initial variables
let sequence = Promise.resolve();
const hmacAlgorithm = {
name: "HMAC",
length,
hash: {
name: parameters.hashAlgorithm
}
};
//endregion
//region Create PKCS#12 key for integrity checking
sequence = sequence.then(() => makePKCS12B2Key(this, parameters.hashAlgorithm, length, parameters.password, parameters.salt, parameters.iterationCount));
//endregion
//region Import HMAC key
// noinspection JSCheckFunctionSignatures
sequence = sequence.then(
result =>
this.importKey("raw",
new Uint8Array(result),
hmacAlgorithm,
false,
["sign"])
);
//endregion
//region Make signed HMAC value
sequence = sequence.then(
result =>
this.sign(hmacAlgorithm, result, new Uint8Array(parameters.contentToStamp)),
error => Promise.reject(error)
);
//endregion
return sequence;
}
//**********************************************************************************
verifyDataStampedWithPassword(parameters)
{
//region Check for input parameters
if((parameters instanceof Object) === false)
return Promise.reject("Parameters must have type \"Object\"");
if(("password" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"password\"");
if(("hashAlgorithm" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"hashAlgorithm\"");
if(("salt" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"iterationCount\"");
if(("iterationCount" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"salt\"");
if(("contentToVerify" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"contentToVerify\"");
if(("signatureToVerify" in parameters) === false)
return Promise.reject("Absent mandatory parameter \"signatureToVerify\"");
//endregion
//region Choose correct length for HMAC key
let length;
switch(parameters.hashAlgorithm.toLowerCase())
{
case "sha-1":
length = 160;
break;
case "sha-256":
length = 256;
break;
case "sha-384":
length = 384;
break;
case "sha-512":
length = 512;
break;
default:
return Promise.reject(`Incorrect "parameters.hashAlgorithm" parameter: ${parameters.hashAlgorithm}`);
}
//endregion
//region Initial variables
let sequence = Promise.resolve();
const hmacAlgorithm = {
name: "HMAC",
length,
hash: {
name: parameters.hashAlgorithm
}
};
//endregion
//region Create PKCS#12 key for integrity checking
sequence = sequence.then(() => makePKCS12B2Key(this, parameters.hashAlgorithm, length, parameters.password, parameters.salt, parameters.iterationCount));
//endregion
//region Import HMAC key
// noinspection JSCheckFunctionSignatures
sequence = sequence.then(result =>
this.importKey("raw",
new Uint8Array(result),
hmacAlgorithm,
false,
["verify"])
);
//endregion
//region Make signed HMAC value
sequence = sequence.then(
result =>
this.verify(hmacAlgorithm, result, new Uint8Array(parameters.signatureToVerify), new Uint8Array(parameters.contentToVerify)),
error => Promise.reject(error)
);
//endregion
return sequence;
}
//**********************************************************************************
/**
* Get signature parameters by analyzing private key algorithm
* @param {Object} privateKey The private key user would like to use
* @param {string} [hashAlgorithm="SHA-1"] Hash algorithm user would like to use
* @return {Promise.<T>|Promise}
*/
getSignatureParameters(privateKey, hashAlgorithm = "SHA-1")
{
//region Check hashing algorithm
const oid = this.getOIDByAlgorithm({ name: hashAlgorithm });
if(oid === "")
return Promise.reject(`Unsupported hash algorithm: ${hashAlgorithm}`);
//endregion
//region Initial variables
const signatureAlgorithm = new AlgorithmIdentifier();
//endregion
//region Get a "default parameters" for current algorithm
const parameters = this.getAlgorithmParameters(privateKey.algorithm.name, "sign");
parameters.algorithm.hash.name = hashAlgorithm;
//endregion
//region Fill internal structures base on "privateKey" and "hashAlgorithm"
switch(privateKey.algorithm.name.toUpperCase())
{
case "RSASSA-PKCS1-V1_5":
case "ECDSA":
signatureAlgorithm.algorithmId = this.getOIDByAlgorithm(parameters.algorithm);
break;
case "RSA-PSS":
{
//region Set "saltLength" as a length (in octets) of hash function result
switch(hashAlgorithm.toUpperCase())
{
case "SHA-256":
parameters.algorithm.saltLength = 32;
break;
case "SHA-384":
parameters.algorithm.saltLength = 48;
break;
case "SHA-512":
parameters.algorithm.saltLength = 64;
break;
default:
}
//endregion
//region Fill "RSASSA_PSS_params" object
const paramsObject = {};
if(hashAlgorithm.toUpperCase() !== "SHA-1")
{
const hashAlgorithmOID = this.getOIDByAlgorithm({ name: hashAlgorithm });
if(hashAlgorithmOID === "")
return Promise.reject(`Unsupported hash algorithm: ${hashAlgorithm}`);
paramsObject.hashAlgorithm = new AlgorithmIdentifier({
algorithmId: hashAlgorithmOID,
algorithmParams: new asn1js.Null()
});
paramsObject.maskGenAlgorithm = new AlgorithmIdentifier({
algorithmId: "1.2.840.113549.1.1.8", // MGF1
algorithmParams: paramsObject.hashAlgorithm.toSchema()
});
}
if(parameters.algorithm.saltLength !== 20)
paramsObject.saltLength = parameters.algorithm.saltLength;
const pssParameters = new RSASSAPSSParams(paramsObject);
//endregion
//region Automatically set signature algorithm
signatureAlgorithm.algorithmId = "1.2.840.113549.1.1.10";
signatureAlgorithm.algorithmParams = pssParameters.toSchema();
//endregion
}
break;
default:
return Promise.reject(`Unsupported signature algorithm: ${privateKey.algorithm.name}`);
}
//endregion
return Promise.resolve().then(() => ({
signatureAlgorithm,
parameters
}));
}
//**********************************************************************************
/**
* Sign data with pre-defined private key
* @param {ArrayBuffer} data Data to be signed
* @param {Object} privateKey Private key to use
* @param {Object} parameters Parameters for used algorithm
* @return {Promise.<T>|Promise}
*/
signWithPrivateKey(data, privateKey, parameters)
{
return this.sign(parameters.algorithm,
privateKey,
new Uint8Array(data))
.then(result =>
{
//region Special case for ECDSA algorithm
if(parameters.algorithm.name === "ECDSA")
result = createCMSECDSASignature(result);
//endregion
return result;
}, error =>
Promise.reject(`Signing error: ${error}`)
);
}
//**********************************************************************************
fillPublicKeyParameters(publicKeyInfo, signatureAlgorithm)
{
const parameters = {};
//region Find signer's hashing algorithm
const shaAlgorithm = this.getHashAlgorithm(signatureAlgorithm);
if(shaAlgorithm === "")
return Promise.reject(`Unsupported signature algorithm: ${signatureAlgorithm.algorithmId}`);
//endregion
//region Get information about public key algorithm and default parameters for import
let algorithmId;
if(signatureAlgorithm.algorithmId === "1.2.840.113549.1.1.10")
algorithmId = signatureAlgorithm.algorithmId;
else
algorithmId = publicKeyInfo.algorithm.algorithmId;
const algorithmObject = this.getAlgorithmByOID(algorithmId);
if(("name" in algorithmObject) === "")
return Promise.reject(`Unsupported public key algorithm: ${signatureAlgorithm.algorithmId}`);
parameters.algorithm = this.getAlgorithmParameters(algorithmObject.name, "importkey");
if("hash" in parameters.algorithm.algorithm)
parameters.algorithm.algorithm.hash.name = shaAlgorithm;
//region Special case for ECDSA
if(algorithmObject.name === "ECDSA")
{
//region Get information about named curve
let algorithmParamsChecked = false;
if(("algorithmParams" in publicKeyInfo.algorithm) === true)
{
if("idBlock" in publicKeyInfo.algorithm.algorithmParams)
{
if((publicKeyInfo.algorithm.algorithmParams.idBlock.tagClass === 1) && (publicKeyInfo.algorithm.algorithmParams.idBlock.tagNumber === 6))
algorithmParamsChecked = true;
}
}
if(algorithmParamsChecked === false)
return Promise.reject("Incorrect type for ECDSA public key parameters");
const curveObject = this.getAlgorithmByOID(publicKeyInfo.algorithm.algorithmParams.valueBlock.toString());
if(("name" in curveObject) === false)
return Promise.reject(`Unsupported named curve algorithm: ${publicKeyInfo.algorithm.algorithmParams.valueBlock.toString()}`);
//endregion
parameters.algorithm.algorithm.namedCurve = curveObject.name;
}
//endregion
//endregion
return parameters;
}
//**********************************************************************************
getPublicKey(publicKeyInfo, signatureAlgorithm, parameters = null)
{
if(parameters === null)
parameters = this.fillPublicKeyParameters(publicKeyInfo, signatureAlgorithm);
const publicKeyInfoSchema = publicKeyInfo.toSchema();
const publicKeyInfoBuffer = publicKeyInfoSchema.toBER(false);
const publicKeyInfoView = new Uint8Array(publicKeyInfoBuffer);
return this.importKey("spki",
publicKeyInfoView,
parameters.algorithm.algorithm,
true,
parameters.algorithm.usages
);
}
//**********************************************************************************
verifyWithPublicKey(data, signature, publicKeyInfo, signatureAlgorithm, shaAlgorithm = null)
{
//region Initial variables
let sequence = Promise.resolve();
//endregion
//region Find signer's hashing algorithm
if(shaAlgorithm === null)
{
shaAlgorithm = this.getHashAlgorithm(signatureAlgorithm);
if(shaAlgorithm === "")
return Promise.reject(`Unsupported signature algorithm: ${signatureAlgorithm.algorithmId}`);
//region Import public key
sequence = sequence.then(() =>
this.getPublicKey(publicKeyInfo, signatureAlgorithm));
//endregion
}
else
{
const parameters = {};
//region Get information about public key algorithm and default parameters for import
let algorithmId;
if(signatureAlgorithm.algorithmId === "1.2.840.113549.1.1.10")
algorithmId = signatureAlgorithm.algorithmId;
else
algorithmId = publicKeyInfo.algorithm.algorithmId;
const algorithmObject = this.getAlgorithmByOID(algorithmId);
if(("name" in algorithmObject) === "")
return Promise.reject(`Unsupported public key algorithm: ${signatureAlgorithm.algorithmId}`);
parameters.algorithm = this.getAlgorithmParameters(algorithmObject.name, "importkey");
if("hash" in parameters.algorithm.algorithm)
parameters.algorithm.algorithm.hash.name = shaAlgorithm;
//region Special case for ECDSA
if(algorithmObject.name === "ECDSA")
{
//region Get information about named curve
let algorithmParamsChecked = false;
if(("algorithmParams" in publicKeyInfo.algorithm) === true)
{
if("idBlock" in publicKeyInfo.algorithm.algorithmParams)
{
if((publicKeyInfo.algorithm.algorithmParams.idBlock.tagClass === 1) && (publicKeyInfo.algorithm.algorithmParams.idBlock.tagNumber === 6))
algorithmParamsChecked = true;
}
}
if(algorithmParamsChecked === false)
return Promise.reject("Incorrect type for ECDSA public key parameters");
const curveObject = this.getAlgorithmByOID(publicKeyInfo.algorithm.algorithmParams.valueBlock.toString());
if(("name" in curveObject) === false)
return Promise.reject(`Unsupported named curve algorithm: ${publicKeyInfo.algorithm.algorithmParams.valueBlock.toString()}`);
//endregion
parameters.algorithm.algorithm.namedCurve = curveObject.name;
}
//endregion
//endregion
//region Import public key
sequence = sequence.then(() =>
this.getPublicKey(publicKeyInfo, null, parameters));
//endregion
}
//endregion
//region Verify signature
sequence = sequence.then(publicKey =>
{
//region Get default algorithm parameters for verification
const algorithm = this.getAlgorithmParameters(publicKey.algorithm.name, "verify");
if("hash" in algorithm.algorithm)
algorithm.algorithm.hash.name = shaAlgorithm;
//endregion
//region Special case for ECDSA signatures
let signatureValue = signature.valueBlock.valueHex;
if(publicKey.algorithm.name === "ECDSA")
{
const asn1 = asn1js.fromBER(signatureValue);
// noinspection JSCheckFunctionSignatures
signatureValue = createECDSASignatureFromCMS(asn1.result);
}
//endregion
//region Special case for RSA-PSS
if(publicKey.algorithm.name === "RSA-PSS")
{
let pssParameters;
try
{
pssParameters = new RSASSAPSSParams({ schema: signatureAlgorithm.algorithmParams });
}
catch(ex)
{
return Promise.reject(ex);
}
if("saltLength" in pssParameters)
algorithm.algorithm.saltLength = pssParameters.saltLength;
else
algorithm.algorithm.saltLength = 20;
let hashAlgo = "SHA-1";
if("hashAlgorithm" in pssParameters)
{
const hashAlgorithm = this.getAlgorithmByOID(pssParameters.hashAlgorithm.algorithmId);
if(("name" in hashAlgorithm) === false)
return Promise.reject(`Unrecognized hash algorithm: ${pssParameters.hashAlgorithm.algorithmId}`);
hashAlgo = hashAlgorithm.name;
}
algorithm.algorithm.hash.name = hashAlgo;
}
//endregion
return this.verify(algorithm.algorithm,
publicKey,
new Uint8Array(signatureValue),
new Uint8Array(data)
);
});
//endregion
return sequence;
}
//**********************************************************************************
}
//**************************************************************************************