import * as forge from 'node-forge'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { Utils } from '../misc/utils'; import { DecryptParameters } from '../models/domain/decryptParameters'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; export class WebCryptoFunctionService implements CryptoFunctionService { private crypto: Crypto; private subtle: SubtleCrypto; private isEdge: boolean; private isIE: boolean; private isOldSafari: boolean; constructor(private win: Window, private platformUtilsService: PlatformUtilsService) { this.crypto = typeof win.crypto !== 'undefined' ? win.crypto : null; this.subtle = (!!this.crypto && typeof win.crypto.subtle !== 'undefined') ? win.crypto.subtle : null; this.isEdge = platformUtilsService.isEdge(); this.isIE = platformUtilsService.isIE(); const ua = win.navigator.userAgent; this.isOldSafari = platformUtilsService.isSafari() && (ua.indexOf(' Version/10.') > -1 || ua.indexOf(' Version/9.') > -1); } async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number): Promise { if (this.isEdge || this.isIE || this.isOldSafari) { const forgeLen = algorithm === 'sha256' ? 32 : 64; const passwordBytes = this.toByteString(password); const saltBytes = this.toByteString(salt); const derivedKeyBytes = (forge as any).pbkdf2(passwordBytes, saltBytes, iterations, forgeLen, algorithm); return Utils.fromByteStringToArray(derivedKeyBytes).buffer; } const wcLen = algorithm === 'sha256' ? 256 : 512; const passwordBuf = this.toBuf(password); const saltBuf = this.toBuf(salt); const pbkdf2Params: Pbkdf2Params = { name: 'PBKDF2', salt: saltBuf, iterations: iterations, hash: { name: this.toWebCryptoAlgorithm(algorithm) }, }; const impKey = await this.subtle.importKey('raw', passwordBuf, { name: 'PBKDF2' } as any, false, ['deriveBits']); return await this.subtle.deriveBits(pbkdf2Params, impKey, wcLen); } async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5'): Promise { if (((this.isEdge || this.isIE) && algorithm === 'sha1') || algorithm === 'md5') { const md = algorithm === 'md5' ? forge.md.md5.create() : forge.md.sha1.create(); const valueBytes = this.toByteString(value); md.update(valueBytes, 'raw'); return Utils.fromByteStringToArray(md.digest().data).buffer; } const valueBuf = this.toBuf(value); return await this.subtle.digest({ name: this.toWebCryptoAlgorithm(algorithm) }, valueBuf); } async hmac(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { if (this.isIE && algorithm === 'sha512') { const hmac = (forge as any).hmac.create(); const keyBytes = this.toByteString(key); const valueBytes = this.toByteString(value); hmac.start(algorithm, keyBytes); hmac.update(valueBytes, 'raw'); return Utils.fromByteStringToArray(hmac.digest().data).buffer; } const signingAlgorithm = { name: 'HMAC', hash: { name: this.toWebCryptoAlgorithm(algorithm) }, }; const impKey = await this.subtle.importKey('raw', key, signingAlgorithm, false, ['sign']); return await this.subtle.sign(signingAlgorithm, impKey, value); } // Safely compare two values in a way that protects against timing attacks (Double HMAC Verification). // ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/ // ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy async compare(a: ArrayBuffer, b: ArrayBuffer): Promise { const macKey = await this.randomBytes(32); const signingAlgorithm = { name: 'HMAC', hash: { name: 'SHA-256' }, }; const impKey = await this.subtle.importKey('raw', macKey, signingAlgorithm, false, ['sign']); const mac1 = await this.subtle.sign(signingAlgorithm, impKey, a); const mac2 = await this.subtle.sign(signingAlgorithm, impKey, b); if (mac1.byteLength !== mac2.byteLength) { return false; } const arr1 = new Uint8Array(mac1); const arr2 = new Uint8Array(mac2); for (let i = 0; i < arr2.length; i++) { if (arr1[i] !== arr2[i]) { return false; } } return true; } hmacFast(value: string, key: string, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { const hmac = (forge as any).hmac.create(); hmac.start(algorithm, key); hmac.update(value); const bytes = hmac.digest().getBytes(); return Promise.resolve(bytes); } async compareFast(a: string, b: string): Promise { const rand = await this.randomBytes(32); const bytes = new Uint32Array(rand); const buffer = forge.util.createBuffer(); for (let i = 0; i < bytes.length; i++) { buffer.putInt32(bytes[i]); } const macKey = buffer.getBytes(); const hmac = (forge as any).hmac.create(); hmac.start('sha256', macKey); hmac.update(a); const mac1 = hmac.digest().getBytes(); hmac.start(null, null); hmac.update(b); const mac2 = hmac.digest().getBytes(); const equals = mac1 === mac2; return equals; } async aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' } as any, false, ['encrypt']); return await this.subtle.encrypt({ name: 'AES-CBC', iv: iv }, impKey, data); } aesDecryptFastParameters(data: string, iv: string, mac: string, key: SymmetricCryptoKey): DecryptParameters { const p = new DecryptParameters(); if (key.meta != null) { p.encKey = key.meta.encKeyByteString; p.macKey = key.meta.macKeyByteString; } if (p.encKey == null) { p.encKey = forge.util.decode64(key.encKeyB64); } p.data = forge.util.decode64(data); p.iv = forge.util.decode64(iv); p.macData = p.iv + p.data; if (p.macKey == null && key.macKeyB64 != null) { p.macKey = forge.util.decode64(key.macKeyB64); } if (mac != null) { p.mac = forge.util.decode64(mac); } // cache byte string keys for later if (key.meta == null) { key.meta = {}; } if (key.meta.encKeyByteString == null) { key.meta.encKeyByteString = p.encKey; } if (p.macKey != null && key.meta.macKeyByteString == null) { key.meta.macKeyByteString = p.macKey; } return p; } aesDecryptFast(parameters: DecryptParameters): Promise { const dataBuffer = (forge as any).util.createBuffer(parameters.data); const decipher = (forge as any).cipher.createDecipher('AES-CBC', parameters.encKey); decipher.start({ iv: parameters.iv }); decipher.update(dataBuffer); decipher.finish(); const val = decipher.output.toString('utf8'); return Promise.resolve(val); } async aesDecrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' } as any, false, ['decrypt']); return await this.subtle.decrypt({ name: 'AES-CBC', iv: iv }, impKey, data); } async rsaEncrypt(data: ArrayBuffer, publicKey: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { // Note: Edge browser requires that we specify name and hash for both key import and decrypt. // We cannot use the proper types here. const rsaParams = { name: 'RSA-OAEP', hash: { name: this.toWebCryptoAlgorithm(algorithm) }, }; const impKey = await this.subtle.importKey('spki', publicKey, rsaParams, false, ['encrypt']); return await this.subtle.encrypt(rsaParams, impKey, data); } async rsaDecrypt(data: ArrayBuffer, privateKey: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { // Note: Edge browser requires that we specify name and hash for both key import and decrypt. // We cannot use the proper types here. const rsaParams = { name: 'RSA-OAEP', hash: { name: this.toWebCryptoAlgorithm(algorithm) }, }; const impKey = await this.subtle.importKey('pkcs8', privateKey, rsaParams, false, ['decrypt']); return await this.subtle.decrypt(rsaParams, impKey, data); } async rsaExtractPublicKey(privateKey: ArrayBuffer): Promise { const rsaParams = { name: 'RSA-OAEP', // Have to specify some algorithm hash: { name: this.toWebCryptoAlgorithm('sha1') }, }; const impPrivateKey = await this.subtle.importKey('pkcs8', privateKey, rsaParams, true, ['decrypt']); const jwkPrivateKey = await this.subtle.exportKey('jwk', impPrivateKey); const jwkPublicKeyParams = { kty: 'RSA', e: jwkPrivateKey.e, n: jwkPrivateKey.n, alg: 'RSA-OAEP', ext: true, }; const impPublicKey = await this.subtle.importKey('jwk', jwkPublicKeyParams, rsaParams, true, ['encrypt']); return await this.subtle.exportKey('spki', impPublicKey); } async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> { const rsaParams = { name: 'RSA-OAEP', modulusLength: length, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537 // Have to specify some algorithm hash: { name: this.toWebCryptoAlgorithm('sha1') }, }; const keyPair = await this.subtle.generateKey(rsaParams, true, ['encrypt', 'decrypt']); const publicKey = await this.subtle.exportKey('spki', keyPair.publicKey); const privateKey = await this.subtle.exportKey('pkcs8', keyPair.privateKey); return [publicKey, privateKey]; } randomBytes(length: number): Promise { const arr = new Uint8Array(length); this.crypto.getRandomValues(arr); return Promise.resolve(arr.buffer); } private toBuf(value: string | ArrayBuffer): ArrayBuffer { let buf: ArrayBuffer; if (typeof (value) === 'string') { buf = Utils.fromUtf8ToArray(value).buffer; } else { buf = value; } return buf; } private toByteString(value: string | ArrayBuffer): string { let bytes: string; if (typeof (value) === 'string') { bytes = forge.util.encodeUtf8(value); } else { bytes = Utils.fromBufferToByteString(value); } return bytes; } private toWebCryptoAlgorithm(algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5'): string { if (algorithm === 'md5') { throw new Error('MD5 is not supported in WebCrypto.'); } return algorithm === 'sha1' ? 'SHA-1' : algorithm === 'sha256' ? 'SHA-256' : 'SHA-512'; } }