From 63be97d1b93d9597890b68c7bd4f67079def984a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 11 Jul 2017 23:04:53 -0400 Subject: [PATCH] attachment display and download. refactor to WC --- src/_locales/en/messages.json | 4 + src/models/domainModels.js | 42 ++- .../app/vault/vaultViewLoginController.js | 45 ++- src/popup/app/vault/views/vault.html | 1 + src/popup/app/vault/views/vaultViewLogin.html | 15 + src/popup/less/components.less | 6 +- src/services/cryptoService.js | 331 +++++++++++++----- 7 files changed, 348 insertions(+), 96 deletions(-) diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 7f8941852d9..eb7ef5166e6 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -714,5 +714,9 @@ "copyVerificationCode": { "message": "Copy Verification Code", "description": "Copy Verification Code" + }, + "attachments": { + "message": "Attachments", + "description": "Attachments" } } diff --git a/src/models/domainModels.js b/src/models/domainModels.js index fb08a9ac494..52dbb0b9ce2 100644 --- a/src/models/domainModels.js +++ b/src/models/domainModels.js @@ -113,10 +113,10 @@ var Login = function (obj, alreadyEncrypted) { this.totp = obj.totp ? new CipherString(obj.totp) : null; } - if (response.attachments) { + if (obj.attachments) { this.attachments = []; - for (var i = 0; i < response.attachments.length; i++) { - this.attachments.push(new Attachment(response.attachments[i], alreadyEncrypted)); + for (var i = 0; i < obj.attachments.length; i++) { + this.attachments.push(new Attachment(obj.attachments[i], alreadyEncrypted)); } } else { @@ -179,6 +179,7 @@ var Folder = function (obj, alreadyEncrypted) { favorite: self.favorite }; + var attachments = []; var deferred = Q.defer(); self.name.decrypt(self.organizationId).then(function (val) { @@ -217,6 +218,41 @@ var Folder = function (obj, alreadyEncrypted) { return null; }).then(function (val) { model.totp = val; + + if (self.attachments) { + var attachmentPromises = []; + for (var i = 0; i < self.attachments.length; i++) { + (function (attachment) { + var promise = attachment.decrypt(self.organizationId).then(function (decAttachment) { + attachments.push(decAttachment); + }); + attachmentPromises.push(promise); + })(self.attachments[i]); + } + return Q.all(attachmentPromises); + } + return; + }).then(function () { + model.attachments = attachments.length ? attachments : null; + deferred.resolve(model); + }); + + return deferred.promise; + }; + + Attachment.prototype.decrypt = function (orgId) { + var self = this; + var model = { + id: self.id, + size: self.size, + sizeName: self.sizeName, + url: self.url + }; + + var deferred = Q.defer(); + + self.fileName.decrypt(orgId).then(function (val) { + model.fileName = val; deferred.resolve(model); }); diff --git a/src/popup/app/vault/vaultViewLoginController.js b/src/popup/app/vault/vaultViewLoginController.js index a0e477263d8..6fa41792f88 100644 --- a/src/popup/app/vault/vaultViewLoginController.js +++ b/src/popup/app/vault/vaultViewLoginController.js @@ -2,7 +2,7 @@ angular .module('bit.vault') .controller('vaultViewLoginController', function ($scope, $state, $stateParams, loginService, toastr, $q, - $analytics, i18nService, utilsService, totpService, $timeout, tokenService) { + $analytics, i18nService, utilsService, totpService, $timeout, tokenService, $window, cryptoService) { $scope.i18n = i18nService; var from = $stateParams.from, totpInterval = null; @@ -104,6 +104,49 @@ angular $scope.showPassword = !$scope.showPassword; }; + $scope.download = function (attachment) { + if (attachment.downloading) { + return; + } + + attachment.downloading = true; + var req = new XMLHttpRequest(); + req.open('GET', attachment.url, true); + req.responseType = 'arraybuffer'; + req.onload = function (evt) { + if (!req.response) { + toastr.error(i18n.errorsOccurred); + $timeout(function () { + attachment.downloading = false; + }); + return; + } + + cryptoService.getOrgKey($scope.login.organizationId).then(function (key) { + return cryptoService.decryptFromBytes(req.response, key); + }).then(function (decBuf) { + var blob = new Blob([decBuf]); + + var a = $window.document.createElement('a'); + a.href = $window.URL.createObjectURL(blob); + a.download = attachment.fileName; + $window.document.body.appendChild(a); + a.click(); + $window.document.body.removeChild(a); + + $timeout(function () { + attachment.downloading = false; + }); + }, function () { + toastr.error(i18n.errorsOccurred); + $timeout(function () { + attachment.downloading = false; + }); + }); + }; + req.send(null); + }; + $scope.$on("$destroy", function () { if (totpInterval) { clearInterval(totpInterval); diff --git a/src/popup/app/vault/views/vault.html b/src/popup/app/vault/views/vault.html index 516f19e9ff4..403a963c828 100644 --- a/src/popup/app/vault/views/vault.html +++ b/src/popup/app/vault/views/vault.html @@ -52,6 +52,7 @@ {{login.name}} + {{login.username}} diff --git a/src/popup/app/vault/views/vaultViewLogin.html b/src/popup/app/vault/views/vaultViewLogin.html index c8bf6a87ca9..417c75819b7 100644 --- a/src/popup/app/vault/views/vaultViewLogin.html +++ b/src/popup/app/vault/views/vaultViewLogin.html @@ -76,5 +76,20 @@
{{login.notes}}
+
+
+ {{i18n.attachments}} +
+ +
diff --git a/src/popup/less/components.less b/src/popup/less/components.less index c6fcb335faa..cbb5a675593 100644 --- a/src/popup/less/components.less +++ b/src/popup/less/components.less @@ -354,6 +354,10 @@ color: @gray-light; } + small.item-sub-label { + margin-top: 2px; + } + &.condensed { padding: 3px 10px; @@ -449,7 +453,7 @@ } &.list-no-selection { - .list-grouped-item, .list-section-item { + .list-grouped-item:not(.list-allow-selection), .list-section-item:not(.list-allow-selection) { &:hover { background-color: white; } diff --git a/src/services/cryptoService.js b/src/services/cryptoService.js index 1feca3a3223..d9c293976ce 100644 --- a/src/services/cryptoService.js +++ b/src/services/cryptoService.js @@ -9,7 +9,9 @@ function initCryptoService(constantsService) { _legacyEtmKey, _keyHash, _privateKey, - _orgKeys; + _orgKeys, + _crypto = window.crypto, + _subtle = window.crypto.subtle; CryptoService.prototype.setKey = function (key, callback) { if (!callback || typeof callback !== 'function') { @@ -191,7 +193,7 @@ function initCryptoService(constantsService) { self.decrypt(new CipherString(obj.encPrivateKey), null, 'raw').then(function (privateKey) { var privateKeyB64 = forge.util.encode64(privateKey); - _privateKey = fromB64ToBuffer(privateKeyB64); + _privateKey = fromB64ToArray(privateKeyB64).buffer; deferred.resolve(_privateKey); }, function () { deferred.reject('Cannot get private key. Decryption failed.'); @@ -402,103 +404,175 @@ function initCryptoService(constantsService) { }; CryptoService.prototype.encrypt = function (plainValue, key, plainValueEncoding) { - var self = this; - var deferred = Q.defer(); - if (plainValue === null || plainValue === undefined) { - deferred.resolve(null); - } - else { - getKeyForEncryption(self, key).then(function (keyToUse) { - key = keyToUse; - if (!key) { - deferred.reject('Encryption key unavailable.'); - return; - } - - plainValueEncoding = plainValueEncoding || 'utf8'; - var buffer = forge.util.createBuffer(plainValue, plainValueEncoding); - var ivBytes = forge.random.getBytesSync(16); - var cipher = forge.cipher.createCipher('AES-CBC', key.encKey); - cipher.start({ iv: ivBytes }); - cipher.update(buffer); - cipher.finish(); - - var iv = forge.util.encode64(ivBytes); - var ctBytes = cipher.output.getBytes(); - var ct = forge.util.encode64(ctBytes); - var mac = !key.macKey ? null : computeMac(ivBytes + ctBytes, key.macKey, true); - - var cs = new CipherString(key.encType, iv, ct, mac); - deferred.resolve(cs); + return Q.fcall(function () { + return null; }); } - return deferred.promise; - }; - - CryptoService.prototype.decrypt = function (cipherString, key, outputEncoding) { - var deferred = Q.defer(); - var self = this; - - if (cipherString === null || cipherString === undefined || !cipherString.encryptedString) { - deferred.reject('cannot decrypt nothing'); - return; + plainValueEncoding = plainValueEncoding || 'utf8' + if (plainValueEncoding === 'utf8') { + plainValue = fromUtf8ToArray(plainValue); } - getKeyForEncryption(self, key).then(function (keyToUse) { - key = keyToUse; - if (!key) { - deferred.reject('Encryption key unavailable.'); - return; + 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 } - outputEncoding = outputEncoding || 'utf8'; + var encBytes = new Uint8Array(1 + encValue.iv.length + macLen + encValue.ct.length); - if (cipherString.encryptionType === constantsService.encType.AesCbc128_HmacSha256_B64 && - key.encType === constantsService.encType.AesCbc256_B64) { - // Old encrypt-then-mac scheme, swap out the key - _legacyEtmKey = _legacyEtmKey || - new SymmetricCryptoKey(key.key, false, constantsService.encType.AesCbc128_HmacSha256_B64); - key = _legacyEtmKey; + 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; } - if (cipherString.encryptionType !== key.encType) { - deferred.reject('encType unavailable.'); - return; + 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; + }); + } - var ivBytes = forge.util.decode64(cipherString.initializationVector); - var ctBytes = forge.util.decode64(cipherString.cipherText); + CryptoService.prototype.decrypt = function (cipherString, key, outputEncoding) { + outputEncoding = outputEncoding || 'utf8' - if (key.macKey && cipherString.mac) { - var macBytes = forge.util.decode64(cipherString.mac); - var computedMacBytes = computeMac(ivBytes + ctBytes, key.macKey, false); - if (!macsEqual(key.macKey, computedMacBytes, macBytes)) { - console.error('MAC failed.'); - deferred.reject('MAC failed.'); - } - } + var ivBuf = fromB64ToArray(cipherString.initializationVector).buffer; + var ctBuf = fromB64ToArray(cipherString.cipherText).buffer; + var macBuf = cipherString.mac ? fromB64ToArray(cipherString.mac).buffer : null; - var ctBuffer = forge.util.createBuffer(ctBytes); - var decipher = forge.cipher.createDecipher('AES-CBC', key.encKey); - decipher.start({ iv: ivBytes }); - decipher.update(ctBuffer); - decipher.finish(); - - var decValue; + return aesDecrypt(this, cipherString.encryptionType, ctBuf, ivBuf, macBuf, key).then(function (decValue) { if (outputEncoding === 'utf8') { - decValue = decipher.output.toString('utf8'); + return fromBufferToUtf8(decValue); } else { - decValue = decipher.output.getBytes(); + var b64 = fromBufferToB64(decValue); + return forge.util.decode64(b64); + } + }); + }; + + 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 aesDecrypt(this, encType, ctBytes.buffer, ivBytes.buffer, macBytes ? macBytes.buffer : null, key); + }; + + function aesDecrypt(self, encType, ctBuf, ivBuf, macBuf, key) { + var keyBuf, + encKey; + + return getKeyForEncryption(self, key).then(function (theKey) { + if (encType === constantsService.encType.AesCbc128_HmacSha256_B64 && + theKey.encType === constantsService.encType.AesCbc256_B64) { + // Old encrypt-then-mac scheme, swap out the key + _legacyEtmKey = _legacyEtmKey || + new SymmetricCryptoKey(theKey.key, false, constantsService.encType.AesCbc128_HmacSha256_B64); + theKey = _legacyEtmKey; } - deferred.resolve(decValue); - }); + keyBuf = theKey.getBuffers(); + return _subtle.importKey('raw', keyBuf.encKey, { name: 'AES-CBC' }, false, ['decrypt']); + }).then(function (theEncKey) { + encKey = theEncKey; - return deferred.promise; - }; + 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); + }); + } CryptoService.prototype.rsaDecrypt = function (encValue) { var headerPieces = encValue.split('.'), @@ -569,7 +643,7 @@ function initCryptoService(constantsService) { throw 'Cannot determine padding.'; } - return window.crypto.subtle.importKey('pkcs8', privateKeyBytes, padding, false, ['decrypt']); + return _subtle.importKey('pkcs8', privateKeyBytes, padding, false, ['decrypt']); }).then(function (privateKey) { if (!encPieces || !encPieces.length) { throw 'encPieces unavailable.'; @@ -584,12 +658,12 @@ function initCryptoService(constantsService) { } } - var ctBuff = fromB64ToBuffer(encPieces[0]); - return window.crypto.subtle.decrypt({ name: padding.name }, privateKey, ctBuff); + var ctArr = fromB64ToArray(encPieces[0]); + return _subtle.decrypt({ name: padding.name }, privateKey, ctArr.buffer); }, function () { throw 'Cannot import privateKey.'; }).then(function (decBytes) { - var b64DecValue = toB64FromBuffer(decBytes); + var b64DecValue = fromBufferToB64(decBytes); return b64DecValue; }, function () { throw 'Cannot rsa decrypt.'; @@ -604,6 +678,13 @@ function initCryptoService(constantsService) { 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(); @@ -637,6 +718,35 @@ function initCryptoService(constantsService) { 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); @@ -685,17 +795,31 @@ function initCryptoService(constantsService) { } } - function fromB64ToBuffer(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); + SymmetricCryptoKey.prototype.getBuffers = function () { + if (this.keyBuf) { + return this.keyBuf; } - return bytes.buffer; - } - function toB64FromBuffer(buffer) { + 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; @@ -704,4 +828,29 @@ function initCryptoService(constantsService) { } 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; + } };