diff --git a/src/background.d.ts b/src/background.d.ts new file mode 100644 index 00000000..fbf7b011 --- /dev/null +++ b/src/background.d.ts @@ -0,0 +1,3 @@ +declare function escape(s: string): string; +declare function unescape(s: string): string; +declare var forge: any; diff --git a/src/background.js b/src/background.js index 3b65a823..9e33c3b3 100644 --- a/src/background.js +++ b/src/background.js @@ -3,6 +3,7 @@ import ConstantsService from './services/constants.service'; import i18nService from './services/i18nService.js'; import LockService from './services/lockService.js'; import UtilsService from './services/utils.service'; +import CryptoService from './services/crypto.service'; // Model imports import { AttachmentData } from './models/data/attachmentData'; @@ -71,7 +72,7 @@ var bg_isBackground = true, window.bg_utilsService = bg_utilsService = new UtilsService(); window.bg_i18nService = bg_i18nService = new i18nService(bg_utilsService); window.bg_constantsService = bg_constantsService = new ConstantsService(bg_i18nService); - window.bg_cryptoService = bg_cryptoService = new CryptoService(bg_constantsService, bg_utilsService); + window.bg_cryptoService = bg_cryptoService = new CryptoService(bg_utilsService); window.bg_tokenService = bg_tokenService = new TokenService(bg_utilsService); window.bg_appIdService = bg_appIdService = new AppIdService(bg_utilsService); window.bg_apiService = bg_apiService = new ApiService(bg_tokenService, bg_appIdService, bg_utilsService, bg_constantsService, logout); diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index a9fede1d..69b458e7 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -1,41 +1,42 @@ +import { EncryptionType } from '../../enums/encryptionType.enum'; +import CryptoService from '../../services/crypto.service'; + class CipherString { encryptedString?: string; - encryptionType?: number; // TODO: enum + encryptionType?: EncryptionType; decryptedValue?: string; cipherText?: string; initializationVector?: string; mac?: string; - cryptoService: any; // TODO: type + cryptoService: CryptoService; - constructor() { - this.cryptoService = chrome.extension.getBackgroundPage().bg_cryptoService; - const constants = chrome.extension.getBackgroundPage().bg_constantsService; + constructor(encryptedStringOrType: string | EncryptionType, ct?: string, iv?: string, mac?: string) { + this.cryptoService = chrome.extension.getBackgroundPage().bg_cryptoService as CryptoService; - if (arguments.length >= 2) { + if (ct != null) { // ct and header - this.encryptedString = arguments[0] + '.' + arguments[1]; + const encType = encryptedStringOrType as EncryptionType; + this.encryptedString = encType + '.' + ct; // iv - if (arguments.length > 2 && arguments[2]) { - this.encryptedString += ('|' + arguments[2]); + if (iv != null) { + this.encryptedString += ('|' + iv); } // mac - if (arguments.length > 3 && arguments[3]) { - this.encryptedString += ('|' + arguments[3]); + if (mac != null) { + this.encryptedString += ('|' + mac); } - this.encryptionType = arguments[0]; - this.cipherText = arguments[1]; - this.initializationVector = arguments[2] || null; - this.mac = arguments[3] || null; + this.encryptionType = encType; + this.cipherText = ct; + this.initializationVector = iv; + this.mac = mac; - return; - } else if (arguments.length !== 1) { return; } - this.encryptedString = arguments[0]; + this.encryptedString = encryptedStringOrType as string; if (!this.encryptedString) { return; } @@ -52,13 +53,13 @@ class CipherString { } } else { encPieces = this.encryptedString.split('|'); - this.encryptionType = encPieces.length === 3 ? constants.encType.AesCbc128_HmacSha256_B64 : - constants.encType.AesCbc256_B64; + this.encryptionType = encPieces.length === 3 ? EncryptionType.AesCbc128_HmacSha256_B64 : + EncryptionType.AesCbc256_B64; } switch (this.encryptionType) { - case constants.encType.AesCbc128_HmacSha256_B64: - case constants.encType.AesCbc256_HmacSha256_B64: + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: if (encPieces.length !== 3) { return; } @@ -67,7 +68,7 @@ class CipherString { this.cipherText = encPieces[1]; this.mac = encPieces[2]; break; - case constants.encType.AesCbc256_B64: + case EncryptionType.AesCbc256_B64: if (encPieces.length !== 2) { return; } @@ -75,8 +76,8 @@ class CipherString { this.initializationVector = encPieces[0]; this.cipherText = encPieces[1]; break; - case constants.encType.Rsa2048_OaepSha256_B64: - case constants.encType.Rsa2048_OaepSha1_B64: + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_B64: if (encPieces.length !== 1) { return; } diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts new file mode 100644 index 00000000..fc4c20a0 --- /dev/null +++ b/src/services/crypto.service.ts @@ -0,0 +1,647 @@ +import { EncryptionType } from '../enums/encryptionType.enum'; + +import { CipherString } from '../models/domain/cipherString'; +import SymmetricCryptoKey from '../models/domain/symmetricCryptoKey'; + +import ConstantsService from './constants.service'; +import UtilsService from './utils.service'; + +const Keys = { + key: 'key', + encOrgKeys: 'encOrgKeys', + encPrivateKey: 'encPrivateKey', + encKey: 'encKey', + keyHash: 'keyHash', +}; + +const Crypto = window.crypto; +const Subtle = Crypto.subtle; + +class EncryptedObject { + iv: Uint8Array; + ct: Uint8Array; + mac: Uint8Array; + key: SymmetricCryptoKey; +} + +export default class CryptoService { + private key: SymmetricCryptoKey; + private encKey: SymmetricCryptoKey; + private legacyEtmKey: SymmetricCryptoKey; + private keyHash: string; + private privateKey: ArrayBuffer; + private orgKeys: Map; + + constructor(private utilsService: UtilsService) { + } + + async setKey(key: SymmetricCryptoKey) { + const self = this; + this.key = key; + + const option = await this.utilsService.getObjFromStorage(ConstantsService.lockOptionKey); + if (option != null) { + // if we have a lock option set, we do not store the key + return; + } + + return self.utilsService.saveObjToStorage(Keys.key, key.keyB64); + } + + // TODO: convert uses to promises + setKeyHash(keyHash: string) { + this.keyHash = keyHash; + return this.utilsService.saveObjToStorage(Keys.keyHash, keyHash); + } + + async setEncKey(encKey: string) { + if (encKey == null) { + return; + } + await this.utilsService.saveObjToStorage(Keys.encKey, encKey); + this.encKey = null; + } + + async setEncPrivateKey(encPrivateKey: string) { + if (encPrivateKey == null) { + return; + } + + await this.utilsService.saveObjToStorage(Keys.encPrivateKey, encPrivateKey); + this.privateKey = null; + } + + // TODO: proper response model type for orgs + setOrgKeys(orgs: any) { + let orgKeys: any = {}; + for (let org of orgs) { + orgKeys[org.id] = org.key; + } + + return this.utilsService.saveObjToStorage(Keys.encOrgKeys, orgKeys); + } + + async getKey(): Promise { + if (this.key != null) { + return; + } + + const option = await this.utilsService.getObjFromStorage(ConstantsService.lockOptionKey); + if (option != null) { + return null; + } + + const key = await this.utilsService.getObjFromStorage(Keys.key); + if (key) { + this.key = new SymmetricCryptoKey(key, true); + } + + return key == null ? null : this.key; + } + + // TODO: convert uses to promises + getKeyHash(): Promise { + if (this.keyHash != null) { + return Promise.resolve(this.keyHash); + } + + return this.utilsService.getObjFromStorage(Keys.keyHash); + } + + getEncKey(): Promise { + if (this.encKey != null) { + return Promise.resolve(this.encKey); + } + + const self = this; + let encKey: string = null; + return this.utilsService.getObjFromStorage(Keys.encKey).then((theEncKey?: string) => { + if (theEncKey == null) { + return null; + } + + encKey = theEncKey; + return self.getKey(); + }).then((key: SymmetricCryptoKey) => { + if (key == null) { + return null; + } + + // TODO: decrypt with local func + // return self.decrypt(new CipherString(encKey), key, 'raw'); + }).then((decEncKey: string) => { + if (decEncKey == null) { + return null; + } + + self.encKey = new SymmetricCryptoKey(decEncKey); + return self.encKey; + }, () => { + throw new Error('Cannot get enc key. Decryption failed.'); + }); + } + + getPrivateKey(): Promise { + if (this.privateKey != null) { + return Promise.resolve(this.privateKey); + } + + var self = this; + return this.utilsService.getObjFromStorage(Keys.encPrivateKey).then((encPrivateKey: string) => { + if (encPrivateKey == null) { + return null; + } + // TODO: decrypt with local func + //return self.decrypt(new CipherString(encPrivateKey), null, 'raw'); + }).then((privateKey: string) => { + if (privateKey != null) { + let privateKeyB64 = forge.util.encode64(privateKey); + this.privateKey = UtilsService.fromB64ToArray(privateKeyB64).buffer; + return this.privateKey; + } + + return null; + }, () => { + throw new Error('Cannot get private key. Decryption failed.'); + }); + } + + async getOrgKeys(): Promise> { + if (this.orgKeys != null && this.orgKeys.size > 0) { + return this.orgKeys; + } + + const self = this; + let encOrgKeys = await this.utilsService.getObjFromStorage(Keys.encOrgKeys); + if (!encOrgKeys) { + return null; + } + + let decPromises: Promise[] = []; + let orgKeys: Map = new Map(); + let setKey = false; + + for (var orgId in encOrgKeys) { + if (encOrgKeys.hasOwnProperty(orgId)) { + /* jshint ignore:start */ + (function (orgIdInstance) { + const p = self.rsaDecrypt(encOrgKeys[orgIdInstance]).then((decValueB64: string) => { + orgKeys.set(orgIdInstance, new SymmetricCryptoKey(decValueB64, true)); + setKey = true; + }, (e: any) => { + console.log('getOrgKeys error: ' + e); + }); + decPromises.push(p); + })(orgId); + /* jshint ignore:end */ + } + } + + await Promise.all(decPromises); + if (setKey) { + this.orgKeys = orgKeys; + } + + return this.orgKeys; + } + + async getOrgKey(orgId: string): Promise { + if (orgId == null) { + return null; + } + + var orgKeys = await this.getOrgKeys(); + if (orgKeys == null || !orgKeys.has(orgId)) { + return null; + } + + return orgKeys.get(orgId); + } + + clearKey(): Promise { + this.key = this.legacyEtmKey = null; + return this.utilsService.removeFromStorage(Keys.key); + } + + clearKeyHash(): Promise { + this.keyHash = null; + return this.utilsService.removeFromStorage(Keys.keyHash); + } + + clearEncKey(memoryOnly?: boolean): Promise { + this.encKey = null; + if (memoryOnly) { + return Promise.resolve(); + } + return this.utilsService.removeFromStorage(Keys.encKey); + } + + clearPrivateKey(memoryOnly?: boolean): Promise { + this.privateKey = null; + if (memoryOnly) { + return Promise.resolve(); + } + return this.utilsService.removeFromStorage(Keys.encPrivateKey); + } + + clearOrgKeys(memoryOnly?: boolean): Promise { + this.orgKeys = null; + if (memoryOnly) { + return Promise.resolve(); + } + return this.utilsService.removeFromStorage(Keys.encOrgKeys); + } + + clearKeys(): Promise { + var self = this; + return Promise.all([ + self.clearKey(), + self.clearKeyHash(), + self.clearOrgKeys(), + self.clearEncKey(), + self.clearPrivateKey() + ]); + } + + async toggleKey(): Promise { + const key = await this.getKey(); + const option = await this.utilsService.getObjFromStorage(ConstantsService.lockOptionKey); + if (option != null || option === 0) { + // if we have a lock option set, clear the key + await this.clearKey(); + this.key = key; + return; + } + + await this.setKey(key); + } + + makeKey(password: string, salt: string) { + const keyBytes: string = forge.pbkdf2(forge.util.encodeUtf8(password), forge.util.encodeUtf8(salt), + 5000, 256 / 8, 'sha256'); + return new SymmetricCryptoKey(keyBytes); + } + + // TODO: convert uses to promises + async hashPassword(password: string, key: SymmetricCryptoKey): Promise { + const storedKey = await this.getKey(); + key = key || storedKey + if (!password || !key) { + throw new Error('Invalid parameters.'); + } + + let hashBits = forge.pbkdf2(key.key, forge.util.encodeUtf8(password), 1, 256 / 8, 'sha256'); + return forge.util.encode64(hashBits); + } + + makeEncKey(key: SymmetricCryptoKey): Promise { + let bytes = new Uint8Array(512 / 8); + Crypto.getRandomValues(bytes); + return this.encrypt(bytes, key, 'raw'); + } + + async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey, + plainValueEncoding: string = 'utf8'): Promise { + if (!plainValue) { + return Promise.resolve(null); + } + + let plainValueArr: Uint8Array; + if (plainValueEncoding == 'utf8') { + plainValueArr = this.fromUtf8ToArray(plainValue as string); + } else { + plainValueArr = plainValue as Uint8Array; + } + + const encValue = await this.aesEncrypt(plainValueArr.buffer, key); + const iv = this.fromBufferToB64(encValue.iv.buffer); + const ct = this.fromBufferToB64(encValue.ct.buffer); + const mac = encValue.mac ? this.fromBufferToB64(encValue.mac.buffer) : null; + return new CipherString(encValue.key.encType, iv, ct, mac); + } + + async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise { + const encValue = await this.aesEncrypt(plainValue, key); + let macLen = 0; + if (encValue.mac) { + macLen = encValue.mac.length; + } + + const encBytes = new Uint8Array(1 + encValue.iv.length + macLen + encValue.ct.length); + encBytes.set([encValue.key.encType]); + encBytes.set(encValue.iv, 1); + if (encValue.mac) { + encBytes.set(encValue.mac, 1 + encValue.iv.length); + } + encBytes.set(encValue.ct, 1 + encValue.iv.length + macLen); + + return encBytes.buffer; + }; + + async decrypt(cipherString: CipherString, key: SymmetricCryptoKey, outputEncoding: string = 'utf8'): Promise { + const ivBytes: string = forge.util.decode64(cipherString.initializationVector); + const ctBytes: string = forge.util.decode64(cipherString.cipherText); + const macBytes: string = cipherString.mac ? forge.util.decode64(cipherString.mac) : null; + + const decipher = await this.aesDecrypt(cipherString.encryptionType, ctBytes, ivBytes, macBytes, key); + if (!decipher) { + return null; + } + + if (outputEncoding == 'utf8') { + return decipher.output.toString('utf8'); + } else { + return decipher.output.getBytes(); + } + } + + async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise { + if (!encBuf) { + throw new Error('no encBuf.'); + } + + const encBytes = new Uint8Array(encBuf); + const encType = encBytes[0]; + let ctBytes: Uint8Array = null; + let ivBytes: Uint8Array = null; + let macBytes: Uint8Array = null; + + switch (encType) { + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: + if (encBytes.length <= 49) { // 1 + 16 + 32 + ctLength + return null; + } + + ivBytes = encBytes.slice(1, 17); + macBytes = encBytes.slice(17, 49); + ctBytes = encBytes.slice(49); + break; + case EncryptionType.AesCbc256_B64: + if (encBytes.length <= 17) { // 1 + 16 + ctLength + return null; + } + + ivBytes = encBytes.slice(1, 17); + ctBytes = encBytes.slice(17); + break; + default: + return null; + } + + return await this.aesDecryptWC(encType, ctBytes.buffer, ivBytes.buffer, macBytes ? macBytes.buffer : null, key); + } + + async rsaDecrypt(encValue: string) { + const headerPieces = encValue.split('.'); + let encType: EncryptionType = null; + let encPieces: string[]; + + if (headerPieces.length == 1) { + encType = EncryptionType.Rsa2048_OaepSha256_B64; + encPieces = [headerPieces[0]]; + } else if (headerPieces.length == 2) { + try { + encType = parseInt(headerPieces[0]); + encPieces = headerPieces[1].split('|'); + } catch (e) { } + } + + switch (encType) { + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_B64: + if (encPieces.length !== 1) { + throw new Error('Invalid cipher format.'); + } + break; + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + if (encPieces.length !== 2) { + throw new Error('Invalid cipher format.'); + } + break; + default: + throw new Error('encType unavailable.'); + } + + if (encPieces == null || encPieces.length <= 0) { + throw new Error('encPieces unavailable.'); + } + + const key = await this.getEncKey(); + if (key != null && key.macKey != null && encPieces.length > 1) { + const ctBytes: string = forge.util.decode64(encPieces[0]); + const macBytes: string = forge.util.decode64(encPieces[1]); + const computedMacBytes = await this.computeMac(ctBytes, key.macKey, false); + const macsEqual = await this.macsEqual(key.macKey, macBytes, computedMacBytes); + if (!macsEqual) { + throw new Error('MAC failed.'); + } + } + + const privateKeyBytes = await this.getPrivateKey(); + if (!privateKeyBytes) { + throw new Error('No private key.'); + } + + let padding: any = null; + switch (encType) { + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + padding = { + name: 'RSA-OAEP', + hash: { name: 'SHA-256' } + }; + break; + case EncryptionType.Rsa2048_OaepSha1_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + padding = { + name: 'RSA-OAEP', + hash: { name: 'SHA-1' } + }; + break; + default: + throw new Error('encType unavailable.'); + } + + const privateKey = await Subtle.importKey('pkcs8', privateKeyBytes, padding, false, ['decrypt']); + + var ctArr = UtilsService.fromB64ToArray(encPieces[0]); + const decBytes = await Subtle.decrypt(padding, privateKey, ctArr.buffer); + + const b64DecValue = this.fromBufferToB64(decBytes); + return b64DecValue; + } + + // Helpers + + private async aesEncrypt(plainValue: ArrayBuffer, key: SymmetricCryptoKey): Promise { + const obj = new EncryptedObject(); + obj.key = await this.getKeyForEncryption(key); + const keyBuf = obj.key.getBuffers(); + + obj.iv = new Uint8Array(16); + Crypto.getRandomValues(obj.iv); + + const encKey = await Subtle.importKey('raw', keyBuf.encKey, { name: 'AES-CBC' }, false, ['encrypt']); + const encValue = await Subtle.encrypt({ name: 'AES-CBC', iv: obj.iv }, encKey, plainValue); + obj.ct = new Uint8Array(encValue); + + if (keyBuf.macKey) { + let data = new Uint8Array(obj.iv.length + obj.ct.length); + data.set(obj.iv, 0); + data.set(obj.ct, obj.iv.length); + const mac = await this.computeMacWC(data.buffer, keyBuf.macKey); + obj.mac = new Uint8Array(mac); + } + + return obj; + } + + private async aesDecrypt(encType: EncryptionType, ctBytes: string, ivBytes: string, macBytes: string, + key: SymmetricCryptoKey): Promise { + const keyForEnc = await this.getKeyForEncryption(key); + const theKey = this.resolveLegacyKey(encType, keyForEnc); + + if (encType != theKey.encType) { + console.error('encType unavailable.'); + return null; + } + + if (theKey.macKey != null && macBytes != null) { + var computedMacBytes = this.computeMac(ivBytes + ctBytes, theKey.macKey, false); + if (!this.macsEqual(theKey.macKey, computedMacBytes, macBytes)) { + console.error('MAC failed.'); + return null; + } + } + + var ctBuffer = forge.util.createBuffer(ctBytes); + var decipher = forge.cipher.createDecipher('AES-CBC', theKey.encKey); + decipher.start({ iv: ivBytes }); + decipher.update(ctBuffer); + decipher.finish(); + + return decipher; + } + + private async aesDecryptWC(encType: EncryptionType, ctBuf: ArrayBuffer, ivBuf: ArrayBuffer, + macBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise { + const theKey = await this.getKeyForEncryption(key); + const keyBuf = theKey.getBuffers(); + const encKey = await Subtle.importKey('raw', keyBuf.encKey, { name: 'AES-CBC' }, false, ['decrypt']); + if (!keyBuf.macKey || !macBuf) { + return null; + } + + const data = new Uint8Array(ivBuf.byteLength + ctBuf.byteLength); + data.set(new Uint8Array(ivBuf), 0); + data.set(new Uint8Array(ctBuf), ivBuf.byteLength); + const computedMacBuf = await this.computeMacWC(data.buffer, keyBuf.macKey); + if (computedMacBuf === null) { + return null; + } + + const macsMatch = await this.macsEqualWC(keyBuf.macKey, macBuf, computedMacBuf); + if (macsMatch === false) { + console.error('MAC failed.'); + return null; + } + + return await Subtle.decrypt({ name: 'AES-CBC', iv: ivBuf }, encKey, ctBuf); + } + + private computeMac(dataBytes: string, macKey: string, b64Output: boolean): string { + const hmac = forge.hmac.create(); + hmac.start('sha256', macKey); + hmac.update(dataBytes); + const mac = hmac.digest(); + return b64Output ? forge.util.encode64(mac.getBytes()) : mac.getBytes(); + } + + private async computeMacWC(dataBuf: ArrayBuffer, macKeyBuf: ArrayBuffer): Promise { + const key = await Subtle.importKey('raw', macKeyBuf, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']); + return await Subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, key, dataBuf); + } + + // Safely compare two MACs 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/ + private macsEqual(macKey: string, mac1: string, mac2: string): boolean { + const hmac = forge.hmac.create(); + + hmac.start('sha256', macKey); + hmac.update(mac1); + let mac1Bytes = hmac.digest().getBytes(); + + hmac.start(null, null); + hmac.update(mac2); + let mac2Bytes = hmac.digest().getBytes(); + + return mac1Bytes == mac2Bytes; + } + + private async macsEqualWC(macKeyBuf: ArrayBuffer, mac1Buf: ArrayBuffer, mac2Buf: ArrayBuffer): Promise { + const macKey = await Subtle.importKey('raw', macKeyBuf, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']); + const mac1 = await Subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, macKey, mac1Buf); + const mac2 = await Subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, macKey, mac2Buf); + + 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; + } + + private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise { + if (key) { + return key; + } + + const encKey = await this.getEncKey(); + return encKey || (await this.getKey()); + } + + private resolveLegacyKey(encType: EncryptionType, key: SymmetricCryptoKey): SymmetricCryptoKey { + if (encType == EncryptionType.AesCbc128_HmacSha256_B64 && key.encType == EncryptionType.AesCbc256_B64) { + // Old encrypt-then-mac scheme, make a new key + this.legacyEtmKey = this.legacyEtmKey || + new SymmetricCryptoKey(key.key, false, EncryptionType.AesCbc128_HmacSha256_B64); + return this.legacyEtmKey; + } + + return key; + } + + private fromBufferToB64(buffer: ArrayBuffer): string { + let binary = ''; + const bytes = new Uint8Array(buffer); + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + } + + private fromBufferToUtf8(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + const encodedString = String.fromCharCode.apply(null, bytes); + return decodeURIComponent(escape(encodedString)); + } + + private fromUtf8ToArray(str: string): Uint8Array { + const strUtf8 = unescape(encodeURIComponent(str)); + let arr = new Uint8Array(strUtf8.length); + for (let i = 0; i < strUtf8.length; i++) { + arr[i] = strUtf8.charCodeAt(i); + } + return arr; + } +} diff --git a/src/services/cryptoService.js b/src/services/cryptoService.js deleted file mode 100644 index e5dd74bf..00000000 --- a/src/services/cryptoService.js +++ /dev/null @@ -1,810 +0,0 @@ -function CryptoService(constantsService, utilsService) { - this.constantsService = constantsService; - this.utilsService = utilsService; - - initCryptoService(constantsService); -} - -function initCryptoService(constantsService) { - var _key, - _encKey, - _legacyEtmKey, - _keyHash, - _privateKey, - _orgKeys, - _crypto = window.crypto, - _subtle = window.crypto.subtle, - keyKey = 'key', - encOrgKeysKey = 'encOrgKeys', - encPrivateKeyKey = 'encPrivateKey', - encKeyKey = 'encKey', - keyHashKey = 'keyHash'; - - CryptoService.prototype.setKey = function (key) { - var self = this; - _key = key; - - return self.utilsService.getObjFromStorage(self.constantsService.lockOptionKey).then(function (option) { - if (option || option === 0) { - // if we have a lock option set, we do not store the key - return; - } - - return self.utilsService.saveObjToStorage(keyKey, key.keyB64); - }); - }; - - CryptoService.prototype.setKeyHash = function (keyHash, callback) { - if (!callback || typeof callback !== 'function') { - throw 'callback function required'; - } - - _keyHash = keyHash; - - chrome.storage.local.set({ - 'keyHash': _keyHash - }, function () { - callback(); - }); - }; - - CryptoService.prototype.setEncKey = function (encKey) { - if (encKey === undefined) { - return Q(); - } - - return this.utilsService.saveObjToStorage(encKeyKey, encKey).then(function () { - _encKey = null; - }); - }; - - CryptoService.prototype.setEncPrivateKey = function (encPrivateKey) { - if (encPrivateKey === undefined) { - return Q(); - } - - return this.utilsService.saveObjToStorage(encPrivateKeyKey, encPrivateKey).then(function () { - _privateKey = null; - }); - }; - - CryptoService.prototype.setOrgKeys = function (orgs) { - var orgKeys = {}; - for (var i = 0; i < orgs.length; i++) { - orgKeys[orgs[i].id] = orgs[i].key; - } - - return this.utilsService.saveObjToStorage(encOrgKeysKey, orgKeys); - }; - - CryptoService.prototype.getKey = function () { - if (_key) { - return Q(_key); - } - - var self = this; - return self.utilsService.getObjFromStorage(self.constantsService.lockOptionKey).then(function (option) { - if (option || option === 0) { - return false; - } - - return self.utilsService.getObjFromStorage(keyKey); - }).then(function (key) { - if (key) { - _key = new SymmetricCryptoKey(key, true); - } - return key === false ? null : _key; - }); - }; - - CryptoService.prototype.getKeyHash = function (callback) { - if (!callback || typeof callback !== 'function') { - throw 'callback function required'; - } - - if (_keyHash) { - callback(_keyHash); - return; - } - - chrome.storage.local.get(keyHashKey, function (obj) { - if (obj && obj.keyHash) { - _keyHash = obj.keyHash; - } - - callback(_keyHash); - }); - }; - - CryptoService.prototype.getEncKey = function () { - if (_encKey) { - return Q(_encKey); - } - - var self = this, - encKey = null; - return self.utilsService.getObjFromStorage(encKeyKey).then(function (theEncKey) { - if (!theEncKey) { - return null; - } - - encKey = theEncKey; - return self.getKey(); - }).then(function (key) { - if (!key) { - return null; - } - - return self.decrypt(new CipherString(encKey), key, 'raw'); - }).then(function (decEncKey) { - if (decEncKey) { - _encKey = new SymmetricCryptoKey(decEncKey); - return _encKey; - } - - return null; - }, function () { - throw 'Cannot get enc key. Decryption failed.'; - }); - }; - - CryptoService.prototype.getPrivateKey = function () { - if (_privateKey) { - return Q(_privateKey); - } - - var self = this; - return self.utilsService.getObjFromStorage(encPrivateKeyKey).then(function (encPrivateKey) { - if (!encPrivateKey) { - return null; - } - return self.decrypt(new CipherString(encPrivateKey), null, 'raw'); - }).then(function (privateKey) { - if (privateKey) { - var privateKeyB64 = forge.util.encode64(privateKey); - _privateKey = fromB64ToArray(privateKeyB64).buffer; - return _privateKey; - } - - return null; - }, function () { - throw 'Cannot get private key. Decryption failed.'; - }); - }; - - CryptoService.prototype.getOrgKeys = function () { - if (_orgKeys && _orgKeys.length) { - return Q(_orgKeys); - } - - var self = this, - deferred = Q.defer(); - - chrome.storage.local.get(encOrgKeysKey, function (obj) { - if (obj && obj.encOrgKeys) { - var orgKeys = {}, - setKey = false; - - var decPromises = []; - for (var orgId in obj.encOrgKeys) { - if (obj.encOrgKeys.hasOwnProperty(orgId)) { - /* jshint ignore:start */ - (function (orgIdInstance) { - var promise = self.rsaDecrypt(obj.encOrgKeys[orgIdInstance]).then(function (decValueB64) { - orgKeys[orgIdInstance] = new SymmetricCryptoKey(decValueB64, true); - setKey = true; - }, function (err) { - console.log('getOrgKeys error: ' + err); - }); - decPromises.push(promise); - })(orgId); - /* jshint ignore:end */ - } - } - - Q.all(decPromises).then(function () { - if (setKey) { - _orgKeys = orgKeys; - } - - deferred.resolve(_orgKeys); - }); - } - else { - deferred.resolve(null); - } - }); - - return deferred.promise; - }; - - CryptoService.prototype.getOrgKey = function (orgId) { - if (!orgId) { - return Q(null); - } - - return this.getOrgKeys().then(function (orgKeys) { - if (!orgKeys || !(orgId in orgKeys)) { - return null; - } - - return orgKeys[orgId]; - }); - }; - - CryptoService.prototype.clearKey = function (callback) { - _key = _legacyEtmKey = null; - return this.utilsService.removeFromStorage(keyKey); - }; - - CryptoService.prototype.clearKeyHash = function (callback) { - _keyHash = null; - return this.utilsService.removeFromStorage(keyHashKey); - }; - - CryptoService.prototype.clearEncKey = function (memoryOnly) { - _encKey = null; - if (memoryOnly) { - return Q(); - } - return this.utilsService.removeFromStorage(encKeyKey); - }; - - CryptoService.prototype.clearPrivateKey = function (memoryOnly) { - _privateKey = null; - if (memoryOnly) { - return Q(); - } - return this.utilsService.removeFromStorage(encPrivateKeyKey); - }; - - CryptoService.prototype.clearOrgKeys = function (memoryOnly) { - _orgKeys = null; - if (memoryOnly) { - return Q(); - } - return this.utilsService.removeFromStorage(encOrgKeysKey); - }; - - CryptoService.prototype.clearKeys = function () { - var self = this; - return Q.all([ - self.clearKey(), - self.clearKeyHash(), - self.clearOrgKeys(), - self.clearEncKey(), - self.clearPrivateKey() - ]); - }; - - CryptoService.prototype.toggleKey = function () { - var self = this, - key = null; - - return self.getKey().then(function (theKey) { - key = theKey; - return self.utilsService.getObjFromStorage(self.constantsService.lockOptionKey); - }).then(function (option) { - if (option || option === 0) { - // if we have a lock option set, clear the key - return self.clearKey().then(function () { - _key = key; - }); - } - else { - // no lock option, so store the current key - return self.setKey(key); - } - }); - }; - - CryptoService.prototype.makeKey = function (password, salt) { - var keyBytes = forge.pbkdf2(forge.util.encodeUtf8(password), forge.util.encodeUtf8(salt), - 5000, 256 / 8, 'sha256'); - - return new SymmetricCryptoKey(keyBytes); - }; - - CryptoService.prototype.hashPassword = function (password, key, callback) { - this.getKey().then(function (storedKey) { - key = key || storedKey; - - if (!password || !key) { - throw 'Invalid parameters.'; - } - - var hashBits = forge.pbkdf2(key.key, forge.util.encodeUtf8(password), 1, 256 / 8, 'sha256'); - callback(forge.util.encode64(hashBits)); - }); - }; - - CryptoService.prototype.makeEncKey = function (key) { - var bytes = new Uint8Array(512 / 8); - _crypto.getRandomValues(bytes); - return this.encrypt(bytes, key, 'raw'); - }; - - CryptoService.prototype.encrypt = function (plainValue, key, plainValueEncoding) { - if (plainValue === null || plainValue === undefined) { - return Q(null); - } - - plainValueEncoding = plainValueEncoding || 'utf8'; - if (plainValueEncoding === 'utf8') { - plainValue = fromUtf8ToArray(plainValue); - } - - return aesEncrypt(this, plainValue.buffer, key).then(function (encValue) { - var encType = encValue.key.encType; - var iv = fromBufferToB64(encValue.iv); - var ct = fromBufferToB64(encValue.ct); - var mac = encValue.mac ? fromBufferToB64(encValue.mac) : null; - return new CipherString(encType, iv, ct, mac); - }); - }; - - CryptoService.prototype.encryptToBytes = function (plainValue, key) { - return aesEncrypt(this, plainValue, key).then(function (encValue) { - var macLen = 0; - if (encValue.mac) { - macLen = encValue.mac.length; - } - - var encBytes = new Uint8Array(1 + encValue.iv.length + macLen + encValue.ct.length); - - encBytes.set([encValue.key.encType]); - encBytes.set(encValue.iv, 1); - if (encValue.mac) { - encBytes.set(encValue.mac, 1 + encValue.iv.length); - } - encBytes.set(encValue.ct, 1 + encValue.iv.length + macLen); - - return encBytes.buffer; - }); - }; - - function aesEncrypt(self, plainValue, key) { - var obj = { - iv: new Uint8Array(16), - ct: null, - mac: null, - key: null - }; - - _crypto.getRandomValues(obj.iv); - var keyBuf; - - return getKeyForEncryption(self, key).then(function (keyToUse) { - obj.key = keyToUse; - keyBuf = keyToUse.getBuffers(); - return _subtle.importKey('raw', keyBuf.encKey, { name: 'AES-CBC' }, false, ['encrypt']); - }).then(function (encKey) { - return _subtle.encrypt({ name: 'AES-CBC', iv: obj.iv }, encKey, plainValue); - }).then(function (encValue) { - obj.ct = new Uint8Array(encValue); - if (!keyBuf.macKey) { - return null; - } - - var data = new Uint8Array(obj.iv.length + obj.ct.length); - data.set(obj.iv, 0); - data.set(obj.ct, obj.iv.length); - return computeMacWC(data.buffer, keyBuf.macKey); - }).then(function (mac) { - if (mac) { - obj.mac = new Uint8Array(mac); - } - return obj; - }); - } - - CryptoService.prototype.decrypt = function (cipherString, key, outputEncoding) { - outputEncoding = outputEncoding || 'utf8'; - - var ivBytes = forge.util.decode64(cipherString.initializationVector); - var ctBytes = forge.util.decode64(cipherString.cipherText); - var macBytes = cipherString.mac ? forge.util.decode64(cipherString.mac) : null; - - return aesDecrypt(this, cipherString.encryptionType, ctBytes, ivBytes, macBytes, key).then(function (decipher) { - if (!decipher) { - return null; - } - - if (outputEncoding === 'utf8') { - return decipher.output.toString('utf8'); - } - else { - return decipher.output.getBytes(); - } - }); - }; - - CryptoService.prototype.decryptFromBytes = function (encBuf, key) { - if (!encBuf) { - throw 'no encBuf.'; - } - - var encBytes = new Uint8Array(encBuf), - encType = encBytes[0], - ctBytes = null, - ivBytes = null, - macBytes = null; - - switch (encType) { - case constantsService.encType.AesCbc128_HmacSha256_B64: - case constantsService.encType.AesCbc256_HmacSha256_B64: - if (encBytes.length <= 49) { // 1 + 16 + 32 + ctLength - return null; - } - - ivBytes = encBytes.slice(1, 17); - macBytes = encBytes.slice(17, 49); - ctBytes = encBytes.slice(49); - break; - case constantsService.encType.AesCbc256_B64: - if (encBytes.length <= 17) { // 1 + 16 + ctLength - return null; - } - - ivBytes = encBytes.slice(1, 17); - ctBytes = encBytes.slice(17); - break; - default: - return null; - } - - return aesDecryptWC(this, encType, ctBytes.buffer, ivBytes.buffer, macBytes ? macBytes.buffer : null, key); - }; - - function aesDecrypt(self, encType, ctBytes, ivBytes, macBytes, key) { - return getKeyForEncryption(self, key).then(function (theKey) { - theKey = resolveLegacyKey(encType, theKey); - - if (encType !== theKey.encType) { - console.error('encType unavailable.'); - return null; - } - - if (theKey.macKey && macBytes) { - var computedMacBytes = computeMac(ivBytes + ctBytes, theKey.macKey, false); - if (!macsEqual(theKey.macKey, computedMacBytes, macBytes)) { - console.error('MAC failed.'); - return null; - } - } - - var ctBuffer = forge.util.createBuffer(ctBytes); - var decipher = forge.cipher.createDecipher('AES-CBC', theKey.encKey); - decipher.start({ iv: ivBytes }); - decipher.update(ctBuffer); - decipher.finish(); - - return decipher; - }); - } - - function aesDecryptWC(self, encType, ctBuf, ivBuf, macBuf, key) { - var keyBuf, - encKey; - - return getKeyForEncryption(self, key).then(function (theKey) { - theKey = resolveLegacyKey(encType, theKey); - keyBuf = theKey.getBuffers(); - return _subtle.importKey('raw', keyBuf.encKey, { name: 'AES-CBC' }, false, ['decrypt']); - }).then(function (theEncKey) { - encKey = theEncKey; - - if (!keyBuf.macKey || !macBuf) { - return null; - } - - var data = new Uint8Array(ivBuf.byteLength + ctBuf.byteLength); - data.set(new Uint8Array(ivBuf), 0); - data.set(new Uint8Array(ctBuf), ivBuf.byteLength); - return computeMacWC(data.buffer, keyBuf.macKey); - }).then(function (computedMacBuf) { - if (computedMacBuf === null) { - return null; - } - return macsEqualWC(keyBuf.macKey, macBuf, computedMacBuf); - }).then(function (macsMatch) { - if (macsMatch === false) { - console.error('MAC failed.'); - return null; - } - return _subtle.decrypt({ name: 'AES-CBC', iv: ivBuf }, encKey, ctBuf); - }); - } - - function resolveLegacyKey(encType, key) { - if (encType === constantsService.encType.AesCbc128_HmacSha256_B64 && - key.encType === constantsService.encType.AesCbc256_B64) { - // Old encrypt-then-mac scheme, make a new key - _legacyEtmKey = _legacyEtmKey || - new SymmetricCryptoKey(key.key, false, constantsService.encType.AesCbc128_HmacSha256_B64); - return _legacyEtmKey; - } - - return key; - } - - CryptoService.prototype.rsaDecrypt = function (encValue) { - var headerPieces = encValue.split('.'), - encType, - encPieces; - - if (headerPieces.length === 1) { - encType = constantsService.encType.Rsa2048_OaepSha256_B64; - encPieces = [headerPieces[0]]; - } - else if (headerPieces.length === 2) { - try { - encType = parseInt(headerPieces[0]); - encPieces = headerPieces[1].split('|'); - } - catch (e) { } - } - - switch (encType) { - case constantsService.encType.Rsa2048_OaepSha256_B64: - case constantsService.encType.Rsa2048_OaepSha1_B64: - if (encPieces.length !== 1) { - throw 'Invalid cipher format.'; - } - break; - case constantsService.encType.Rsa2048_OaepSha256_HmacSha256_B64: - case constantsService.encType.Rsa2048_OaepSha1_HmacSha256_B64: - if (encPieces.length !== 2) { - throw 'Invalid cipher format.'; - } - break; - default: - throw 'encType unavailable.'; - } - - var padding = null; - switch (encType) { - case constantsService.encType.Rsa2048_OaepSha256_B64: - case constantsService.encType.Rsa2048_OaepSha256_HmacSha256_B64: - padding = { - name: 'RSA-OAEP', - hash: { name: 'SHA-256' } - }; - break; - case constantsService.encType.Rsa2048_OaepSha1_B64: - case constantsService.encType.Rsa2048_OaepSha1_HmacSha256_B64: - padding = { - name: 'RSA-OAEP', - hash: { name: 'SHA-1' } - }; - break; - default: - throw 'encType unavailable.'; - } - - var key = null, - self = this; - - return self.getEncKey().then(function (encKey) { - key = encKey; - return self.getPrivateKey(); - }).then(function (privateKeyBytes) { - if (!privateKeyBytes) { - throw 'No private key.'; - } - - if (!padding) { - throw 'Cannot determine padding.'; - } - - return _subtle.importKey('pkcs8', privateKeyBytes, padding, false, ['decrypt']); - }).then(function (privateKey) { - if (!encPieces || !encPieces.length) { - throw 'encPieces unavailable.'; - } - - if (key && key.macKey && encPieces.length > 1) { - var ctBytes = forge.util.decode64(encPieces[0]); - var macBytes = forge.util.decode64(encPieces[1]); - var computedMacBytes = computeMac(ctBytes, key.macKey, false); - if (!macsEqual(key.macKey, macBytes, computedMacBytes)) { - throw 'MAC failed.'; - } - } - - var ctArr = fromB64ToArray(encPieces[0]); - return _subtle.decrypt(padding, privateKey, ctArr.buffer); - }, function () { - throw 'Cannot import privateKey.'; - }).then(function (decBytes) { - var b64DecValue = fromBufferToB64(decBytes); - return b64DecValue; - }, function () { - throw 'Cannot rsa decrypt.'; - }); - }; - - function computeMac(dataBytes, macKey, b64Output) { - var hmac = forge.hmac.create(); - hmac.start('sha256', macKey); - hmac.update(dataBytes); - var mac = hmac.digest(); - return b64Output ? forge.util.encode64(mac.getBytes()) : mac.getBytes(); - } - - function computeMacWC(dataBuf, macKeyBuf) { - return _subtle.importKey('raw', macKeyBuf, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']) - .then(function (key) { - return _subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, key, dataBuf); - }); - } - - function getKeyForEncryption(self, key) { - var deferred = Q.defer(); - - if (key) { - deferred.resolve(key); - } - else { - self.getEncKey().then(function (encKey) { - return encKey || self.getKey(); - }).then(function (keyToUse) { - deferred.resolve(keyToUse); - }); - } - - return deferred.promise; - } - - // Safely compare two MACs 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/ - function macsEqual(macKey, mac1, mac2) { - var hmac = forge.hmac.create(); - - hmac.start('sha256', macKey); - hmac.update(mac1); - mac1 = hmac.digest().getBytes(); - - hmac.start(null, null); - hmac.update(mac2); - mac2 = hmac.digest().getBytes(); - - return mac1 === mac2; - } - - function macsEqualWC(macKeyBuf, mac1Buf, mac2Buf) { - var mac1, - macKey; - - return window.crypto.subtle.importKey('raw', macKeyBuf, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']) - .then(function (key) { - macKey = key; - return window.crypto.subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, macKey, mac1Buf); - }).then(function (mac) { - mac1 = mac; - return window.crypto.subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, macKey, mac2Buf); - }).then(function (mac2) { - if (mac1.byteLength !== mac2.byteLength) { - return false; - } - - var arr1 = new Uint8Array(mac1); - var arr2 = new Uint8Array(mac2); - - for (var i = 0; i < arr2.length; i++) { - if (arr1[i] !== arr2[i]) { - return false; - } - } - - return true; - }); - } - - function SymmetricCryptoKey(keyBytes, b64KeyBytes, encType) { - if (b64KeyBytes) { - keyBytes = forge.util.decode64(keyBytes); - } - - if (!keyBytes) { - throw 'Must provide keyBytes'; - } - - var buffer = forge.util.createBuffer(keyBytes); - if (!buffer || buffer.length() === 0) { - throw 'Couldn\'t make buffer'; - } - var bufferLength = buffer.length(); - - if (encType === null || encType === undefined) { - if (bufferLength === 32) { - encType = constantsService.encType.AesCbc256_B64; - } - else if (bufferLength === 64) { - encType = constantsService.encType.AesCbc256_HmacSha256_B64; - } - else { - throw 'Unable to determine encType.'; - } - } - - this.key = keyBytes; - this.keyB64 = forge.util.encode64(keyBytes); - this.encType = encType; - - if (encType === constantsService.encType.AesCbc256_B64 && bufferLength === 32) { - this.encKey = keyBytes; - this.macKey = null; - } - else if (encType === constantsService.encType.AesCbc128_HmacSha256_B64 && bufferLength === 32) { - this.encKey = buffer.getBytes(16); // first half - this.macKey = buffer.getBytes(16); // second half - } - else if (encType === constantsService.encType.AesCbc256_HmacSha256_B64 && bufferLength === 64) { - this.encKey = buffer.getBytes(32); // first half - this.macKey = buffer.getBytes(32); // second half - } - else { - throw 'Unsupported encType/key length.'; - } - } - - SymmetricCryptoKey.prototype.getBuffers = function () { - if (this.keyBuf) { - return this.keyBuf; - } - - var key = fromB64ToArray(this.keyB64); - - var keys = { - key: key.buffer - }; - - if (this.macKey) { - keys.encKey = key.slice(0, key.length / 2).buffer; - keys.macKey = key.slice(key.length / 2).buffer; - } - else { - keys.encKey = key.buffer; - keys.macKey = null; - } - - this.keyBuf = keys; - return this.keyBuf; - }; - - function fromBufferToB64(buffer) { - var binary = ''; - var bytes = new Uint8Array(buffer); - var len = bytes.byteLength; - for (var i = 0; i < len; i++) { - binary += String.fromCharCode(bytes[i]); - } - return window.btoa(binary); - } - - function fromBufferToUtf8(buffer) { - var bytes = new Uint8Array(buffer); - var encodedString = String.fromCharCode.apply(null, bytes); - return decodeURIComponent(escape(encodedString)); - } - - function fromB64ToArray(str) { - var binary_string = window.atob(str); - var len = binary_string.length; - var bytes = new Uint8Array(len); - for (var i = 0; i < len; i++) { - bytes[i] = binary_string.charCodeAt(i); - } - return bytes; - } - - function fromUtf8ToArray(str) { - var strUtf8 = unescape(encodeURIComponent(str)); - var arr = new Uint8Array(strUtf8.length); - for (var i = 0; i < strUtf8.length; i++) { - arr[i] = strUtf8.charCodeAt(i); - } - return arr; - } -}