import { sha1 } from "@noble/hashes/sha1"; import { sha256, sha384, sha512 } from "@noble/hashes/sha2"; import * as asn1js from "asn1js"; import * as pvutils from "pvutils"; import * as pvtsutils from "pvtsutils"; import * as common from "../common"; import { PublicKeyInfo } from "../PublicKeyInfo"; import { PrivateKeyInfo } from "../PrivateKeyInfo"; import { AlgorithmIdentifier } from "../AlgorithmIdentifier"; import { EncryptedContentInfo } from "../EncryptedContentInfo"; import { IRSASSAPSSParams, RSASSAPSSParams } from "../RSASSAPSSParams"; import { PBKDF2Params } from "../PBKDF2Params"; import { PBES2Params } from "../PBES2Params"; import { ArgumentError, AsnError, ParameterError } from "../errors"; import * as type from "./CryptoEngineInterface"; import { AbstractCryptoEngine } from "./AbstractCryptoEngine"; import { EMPTY_STRING } from "../constants"; import { ECNamedCurves } from "../ECNamedCurves";
/** *MakingMACkeyusingalgorithmdescribedinB.2ofPKCS#12standard.
*/
async function makePKCS12B2Key(hashAlgorithm: string, keyLength: number, password: ArrayBuffer, salt: ArrayBuffer, iterationCount: number) {
let u: number; // Output length of the hash function
let v: number; // Block size of the hash function
let md: (input: Uint8Array) => Uint8Array; // Hash function
// Determine the hash algorithm parameters switch (hashAlgorithm.toUpperCase()) { case"SHA-1":
u = 20; // 160 bits
v = 64; // 512 bits
md = sha1; break; case"SHA-256":
u = 32; // 256 bits
v = 64; // 512 bits
md = sha256; break; case"SHA-384":
u = 48; // 384 bits
v = 128; // 1024 bits
md = sha384; break; case"SHA-512":
u = 64; // 512 bits
v = 128; // 1024 bits
md = sha512; break; default: thrownew Error("Unsupported hashing algorithm");
}
const originalPassword = new Uint8Array(password);
let decodedPassword = new TextDecoder().decode(password); const encodedPassword = new TextEncoder().encode(decodedPassword); if (encodedPassword.some((byte, i) => byte !== originalPassword[i])) {
decodedPassword = String.fromCharCode(...originalPassword);
}
// Transform the password into a byte array const passwordTransformed = new Uint8Array(decodedPassword.length * 2 + 2); const passwordView = new DataView(passwordTransformed.buffer); for (let i = 0; i < decodedPassword.length; i++) {
passwordView.setUint16(i * 2, decodedPassword.charCodeAt(i), false);
} // Add null-terminator
passwordView.setUint16(decodedPassword.length * 2, 0, false);
// Create a filled array D with the value 3 (ID for MACing) const D = new Uint8Array(v).fill(3);
// Repeat the salt to fill the block size const saltView = new Uint8Array(salt); const S = new Uint8Array(v * Math.ceil(saltView.length / v)).map((_, i) => saltView[i % saltView.length]);
// Repeat the password to fill the block size const P = new Uint8Array(v * Math.ceil(passwordTransformed.length / v)).map((_, i) => passwordTransformed[i % passwordTransformed.length]);
// Concatenate S and P to form I
let I = new Uint8Array(S.length + P.length);
I.set(S);
I.set(P, S.length);
// Calculate the number of hash iterations needed const c = Math.ceil((keyLength >> 3) / u); const result: number[] = [];
// Main loop to generate the key material for (let i = 0; i < c; i++) { // Concatenate D and I
let A: Uint8Array = new Uint8Array(D.length + I.length);
A.set(D);
A.set(I, D.length);
// Perform hash iterations for (let j = 0; j < iterationCount; j++) {
A = md(A);
}
// Create a repeated block B from the hash output A const B = new Uint8Array(v).map((_, i) => A[i % A.length]);
// Determine the number of blocks const k = Math.ceil(saltView.length / v) + Math.ceil(passwordTransformed.length / v); const iRound: number[] = [];
// Adjust I based on B for (let j = 0; j < k; j++) { const chunk = Array.from(I.slice(j * v, (j + 1) * v));
let x = 0x1ff;
for (let l = B.length - 1; l >= 0; l--) {
x >>= 8;
x += B[l] + (chunk[l] || 0);
chunk[l] = x & 0xff;
}
iRound.push(...chunk);
}
// Update I for the next iteration
I = new Uint8Array(iRound);
// Collect the result
result.push(...A);
}
return new Uint8Array(result.slice(0, keyLength >> 3)).buffer;
}
function prepareAlgorithm(data: globalThis.AlgorithmIdentifier | EcdsaParams): Algorithm & { hash?: Algorithm; } { const res = typeof data === "string"
? { name: data }
: data;
// TODO fix type casting `as EcdsaParams` if ("hash" in (res as EcdsaParams)) {
return {
...res,
hash: prepareAlgorithm((res as EcdsaParams).hash)
};
}
return res;
}
/** *DefaultcryptographicengineforWebCryptographyAPI
*/
export class CryptoEngine extends AbstractCryptoEngine {
if (publicKeyInfo.algorithm.algorithmId !== "1.2.840.113549.1.1.1") thrownew Error(`Incorrect public key algorithm: ${publicKeyInfo.algorithm.algorithmId}`);
//#region Get information about used hash function if (!jwk.alg) { if (!alg.hash) { thrownew ParameterError("hash", "algorithm.hash", "Incorrect hash algorithm: Hash algorithm is missed");
} switch (alg.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: thrownew Error(`Incorrect hash algorithm: ${alg.hash.name.toUpperCase()}`);
}
} //#endregion
//#region Create RSA Public Key elements const publicKeyJSON = publicKeyInfo.toJSON();
Object.assign(jwk, publicKeyJSON); //#endregion
} break; case"ECDSA":
keyUsages = ["verify"]; // Override existing keyUsages value since the key is a public key // break omitted // eslint-disable-next-line no-fallthrough 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") { thrownew Error(`Incorrect public key algorithm: ${publicKeyInfo.algorithm.algorithmId}`);
} //#endregion
//#region Get information about used hash function if (privateKeyInfo.privateKeyAlgorithm.algorithmId !== "1.2.840.113549.1.1.1") thrownew Error(`Incorrect private key algorithm: ${privateKeyInfo.privateKeyAlgorithm.algorithmId}`); //#endregion
//#region Get information about used hash function if (("alg" in jwk) === false) { switch (alg.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: thrownew Error(`Incorrect hash algorithm: ${alg.hash?.name.toUpperCase()}`);
}
} //#endregion
//#region Create RSA Private Key elements const privateKeyJSON = privateKeyInfo.toJSON();
Object.assign(jwk, privateKeyJSON); //#endregion
} break; case"ECDSA":
keyUsages = ["sign"]; // Override existing keyUsages value since the key is a private key // break omitted // eslint-disable-next-line no-fallthrough 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") thrownew Error(`Incorrect algorithm: ${privateKeyInfo.privateKeyAlgorithm.algorithmId}`); //#endregion
//#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) try {
return this.subtle.importKey("jwk", pvutils.stringToArrayBuffer(JSON.stringify(jwk)) as any, algorithm, extractable, keyUsages);
} catch {
return this.subtle.importKey("jwk", jwk, algorithm, extractable, keyUsages);
}
} //#endregion
/** *ExportWebCryptokeystodifferentformats *@paramformat *@paramkey
*/ public override exportKey(format: "jwk", key: CryptoKey): Promise<JsonWebKey>; public override exportKey(format: Exclude<KeyFormat, "jwk">, key: CryptoKey): Promise<ArrayBuffer>; public override exportKey(format: string, key: CryptoKey): Promise<ArrayBuffer | JsonWebKey>; public override async exportKey(format: KeyFormat, key: CryptoKey): Promise<ArrayBuffer | JsonWebKey> {
let jwk = await this.subtle.exportKey("jwk", key);
//#region Currently Safari returns ArrayBuffer as JWK thus we need an additional transformation if (this.name.toLowerCase() === "safari") { // Some additional checks for Safari Technology Preview if (jwk instanceof ArrayBuffer) {
jwk = JSON.parse(pvutils.arrayBufferToString(jwk));
}
} //#endregion
/** *Gettinghashalgorithmbysignaturealgorithm *@paramsignatureAlgorithmSignaturealgorithm
*/ // TODO use safety
getHashAlgorithm(signatureAlgorithm: AlgorithmIdentifier): string {
let result = EMPTY_STRING;
switch (signatureAlgorithm.algorithmId) { case"1.2.840.10045.4.1": // ecdsa-with-SHA1 case"1.2.840.113549.1.1.5": // rsa-encryption-with-SHA1
result = "SHA-1"; break; case"1.2.840.10045.4.3.2": // ecdsa-with-SHA256 case"1.2.840.113549.1.1.11": // rsa-encryption-with-SHA256
result = "SHA-256"; break; case"1.2.840.10045.4.3.3": // ecdsa-with-SHA384 case"1.2.840.113549.1.1.12": // rsa-encryption-with-SHA384
result = "SHA-384"; break; case"1.2.840.10045.4.3.4": // ecdsa-with-SHA512 case"1.2.840.113549.1.1.13": // rsa-encryption-with-SHA512
result = "SHA-512"; break; case"1.2.840.113549.1.1.10": // RSA-PSS
{ try { const params = new RSASSAPSSParams({ schema: signatureAlgorithm.algorithmParams }); if (params.hashAlgorithm) { const algorithm = this.getAlgorithmByOID(params.hashAlgorithm.algorithmId); if ("name" in algorithm) {
result = algorithm.name;
} else {
return EMPTY_STRING;
}
} else
result = "SHA-1";
} catch { // nothing
}
} break; default:
}
return result;
}
public async encryptEncryptedContentInfo(parameters: type.CryptoEngineEncryptParams): Promise<EncryptedContentInfo> { //#region Check for input parameters
ParameterError.assert(parameters, "password", "contentEncryptionAlgorithm", "hmacHashAlgorithm", "iterationCount", "contentToEncrypt", "contentToEncrypt", "contentType");
// TODO Should we reuse iv from parameters.contentEncryptionAlgorithm or use it's length for ivBuffer? 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 const passwordView = new Uint8Array(parameters.password);
//#region Encrypt content // TODO encrypt doesn't use all parameters from parameters.contentEncryptionAlgorithm (eg additionalData and tagLength for AES-GCM) const encryptedData = await this.encrypt(
{
name: parameters.contentEncryptionAlgorithm.name,
iv: ivView
},
derivedKey,
contentView); //#endregion
//#region Store all parameters in EncryptedData object 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: encryptedData })
}); //#endregion
}
/** *Decryptdatastoredin"EncryptedContentInfo"objectusingparameters *@paramparameters
*/ public async decryptEncryptedContentInfo(parameters: type.CryptoEngineDecryptParams): Promise<ArrayBuffer> { //#region Check for input parameters
ParameterError.assert(parameters, "password", "encryptedContentInfo");
public async stampDataWithPassword(parameters: type.CryptoEngineStampDataWithPasswordParams): Promise<ArrayBuffer> { //#region Check for input parameters if ((parameters instanceof Object) === false) thrownew Error("Parameters must have type \"Object\"");
//#region Make signed HMAC value
return this.verify(hmacAlgorithm, hmacKey, new Uint8Array(parameters.signatureToVerify), new Uint8Array(parameters.contentToVerify)); //#endregion
}
// Initial variables const signatureAlgorithm = new AlgorithmIdentifier();
//#region Get "default parameters" for the current algorithm const parameters = this.getAlgorithmParameters(privateKey.algorithm.name, "sign"); if (!Object.keys(parameters.algorithm).length) { thrownew Error("Parameter 'algorithm' is empty");
} // Use the hash from the privateKey.algorithm.hash.name for keys with hash algorithms (like RSA) const algorithm = parameters.algorithm as any; // TODO remove `as any` if ("hash" in privateKey.algorithm && privateKey.algorithm.hash && (privateKey.algorithm.hash as Algorithm).name) {
algorithm.hash.name = (privateKey.algorithm.hash as Algorithm).name;
} else {
algorithm.hash.name = hashAlgorithm;
} //#endregion
//#region Fill internal structures based on "privateKey" and "hashAlgorithm" switch (privateKey.algorithm.name.toUpperCase()) { case"RSASSA-PKCS1-V1_5": case"ECDSA":
signatureAlgorithm.algorithmId = this.getOIDByAlgorithm(algorithm, true); break; case"RSA-PSS":
{ //#region Set "saltLength" as the length (in octets) of the hash function result switch (algorithm.hash.name.toUpperCase()) { case"SHA-256":
algorithm.saltLength = 32; break; case"SHA-384":
algorithm.saltLength = 48; break; case"SHA-512":
algorithm.saltLength = 64; break; default:
} //#endregion
//#region Fill "RSASSA_PSS_params" object const paramsObject: Partial<IRSASSAPSSParams> = {};
//#region Special case for ECDSA algorithm if (parameters.algorithm.name === "ECDSA") {
return common.createCMSECDSASignature(signature);
} //#endregion
return signature;
}
public fillPublicKeyParameters(publicKeyInfo: PublicKeyInfo, signatureAlgorithm: AlgorithmIdentifier): type.CryptoEnginePublicKeyParams { const parameters = {} as any;
//#region Get information about public key algorithm and default parameters for import
let algorithmId: string; if (signatureAlgorithm.algorithmId === "1.2.840.113549.1.1.10")
algorithmId = signatureAlgorithm.algorithmId; else
algorithmId = publicKeyInfo.algorithm.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 const publicKeyAlgorithm = publicKeyInfo.algorithm; if (!publicKeyAlgorithm.algorithmParams) { thrownew Error("Algorithm parameters for ECDSA public key are missed");
} const publicKeyAlgorithmParams = publicKeyAlgorithm.algorithmParams; if ("idBlock" in publicKeyAlgorithm.algorithmParams) { if (!((publicKeyAlgorithmParams.idBlock.tagClass === 1) && (publicKeyAlgorithmParams.idBlock.tagNumber === 6))) { thrownew Error("Incorrect type for ECDSA public key parameters");
}
}
return this.importKey("spki",
publicKeyInfoBuffer,
parameters.algorithm.algorithm as Algorithm, true,
parameters.algorithm.usages
);
}
public async verifyWithPublicKey(data: BufferSource, signature: asn1js.BitString | asn1js.OctetString, publicKeyInfo: PublicKeyInfo, signatureAlgorithm: AlgorithmIdentifier, shaAlgorithm?: string): Promise<boolean> { //#region Find signer's hashing algorithm
let publicKey: CryptoKey; if (!shaAlgorithm) {
shaAlgorithm = this.getHashAlgorithm(signatureAlgorithm); if (!shaAlgorithm) thrownew Error(`Unsupported signature algorithm: ${signatureAlgorithm.algorithmId}`);
//#region Import public key
publicKey = await this.getPublicKey(publicKeyInfo, signatureAlgorithm); //#endregion
} else { const parameters = {} as type.CryptoEnginePublicKeyParams;
//#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;
parameters.algorithm = this.getAlgorithmParameters(algorithmObject.name, "importKey"); if ("hash" in parameters.algorithm.algorithm)
(parameters.algorithm.algorithm as any).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) { thrownew Error("Incorrect type for ECDSA public key parameters");
}
(parameters.algorithm.algorithm as any).namedCurve = curveObject.name;
} //#endregion //#endregion
//#region Import public key
publicKey = await this.getPublicKey(publicKeyInfo, null as any, parameters); // TODO null!!! //#endregion
} //#endregion
//#region Verify signature //#region Get default algorithm parameters for verification const algorithm = this.getAlgorithmParameters(publicKey.algorithm.name, "verify"); if ("hash" in algorithm.algorithm)
(algorithm.algorithm as any).hash.name = shaAlgorithm; //#endregion
//#region Special case for ECDSA signatures
let signatureValue: Uint8Array | ArrayBuffer = signature.valueBlock.valueHexView;
if (publicKey.algorithm.name === "ECDSA") { const namedCurve = ECNamedCurves.find((publicKey.algorithm as EcKeyAlgorithm).namedCurve); if (!namedCurve) { thrownew Error("Unsupported named curve in use");
} const asn1 = asn1js.fromBER(signatureValue);
AsnError.assert(asn1, "Signature value");
signatureValue = common.createECDSASignatureFromCMS(asn1.result, namedCurve.size);
} //#endregion
//#region Special case for RSA-PSS if (publicKey.algorithm.name === "RSA-PSS") { const pssParameters = new RSASSAPSSParams({ schema: signatureAlgorithm.algorithmParams });
if ("saltLength" in pssParameters)
(algorithm.algorithm as any).saltLength = pssParameters.saltLength; else
(algorithm.algorithm as any).saltLength = 20;
let hashAlgo = "SHA-1";
if ("hashAlgorithm" in pssParameters) { const hashAlgorithm = this.getAlgorithmByOID(pssParameters.hashAlgorithm.algorithmId, true);
hashAlgo = hashAlgorithm.name;
}
(algorithm.algorithm as any).hash.name = hashAlgo;
} //#endregion
return this.verify((algorithm.algorithm as any),
publicKey,
signatureValue as BufferSource,
data,
); //#endregion
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.