diff --git a/src/app/services/apiService.js b/src/app/services/apiService.js index 23a19c4dd0..e2a5b9898d 100644 --- a/src/app/services/apiService.js +++ b/src/app/services/apiService.js @@ -40,7 +40,13 @@ del: { url: _apiUri + '/ciphers/:id/delete', method: 'POST', params: { id: '@id' } }, delAdmin: { url: _apiUri + '/ciphers/:id/delete-admin', method: 'POST', params: { id: '@id' } }, delMany: { url: _apiUri + '/ciphers/delete', method: 'POST' }, - moveMany: { url: _apiUri + '/ciphers/move', method: 'POST' } + moveMany: { url: _apiUri + '/ciphers/move', method: 'POST' }, + postAttachment: { + url: _apiUri + '/ciphers/:id/attachment', + method: 'POST', + headers: { 'Content-Type': undefined }, + params: { id: '@id' } + } }); _service.organizations = $resource(_apiUri + '/organizations/:id', {}, { diff --git a/src/app/services/cipherService.js b/src/app/services/cipherService.js index 6e9da09895..af1c3d2c23 100644 --- a/src/app/services/cipherService.js +++ b/src/app/services/cipherService.js @@ -35,9 +35,19 @@ angular uri: encryptedLogin.Uri && encryptedLogin.Uri !== '' ? cryptoService.decrypt(encryptedLogin.Uri, key) : null, username: encryptedLogin.Username && encryptedLogin.Username !== '' ? cryptoService.decrypt(encryptedLogin.Username, key) : null, password: encryptedLogin.Password && encryptedLogin.Password !== '' ? cryptoService.decrypt(encryptedLogin.Password, key) : null, - notes: encryptedLogin.Notes && encryptedLogin.Notes !== '' ? cryptoService.decrypt(encryptedLogin.Notes, key) : null + notes: encryptedLogin.Notes && encryptedLogin.Notes !== '' ? cryptoService.decrypt(encryptedLogin.Notes, key) : null, + attachments: null }; + if (!encryptedLogin.Attachments) { + return login; + } + + login.attachments = []; + for (var i = 0; i < encryptedLogin.Attachments.length; i++) { + login.attachments.push(_service.decryptAttachment(key, encryptedLogin.Attachments[i])); + } + return login; }; @@ -58,12 +68,23 @@ angular edit: encryptedCipher.Edit, name: _service.decryptProperty(encryptedCipher.Data.Name, key, false), username: _service.decryptProperty(encryptedCipher.Data.Username, key, true), - password: _service.decryptProperty(encryptedCipher.Data.Password, key, true) + password: _service.decryptProperty(encryptedCipher.Data.Password, key, true), + hasAttachments: !!encryptedCipher.Attachments }; return login; }; + _service.decryptAttachment = function (key, encryptedAttachment) { + if (!encryptedAttachment) throw "encryptedAttachment is undefined or null"; + + return { + id: encryptedAttachment.Id, + fileName: cryptoService.decrypt(encryptedAttachment.FileName, key), + size: encryptedAttachment.SizeName + }; + }; + _service.decryptFolders = function (encryptedFolders) { if (!encryptedFolders) throw "encryptedFolders is undefined or null"; diff --git a/src/app/services/cryptoService.js b/src/app/services/cryptoService.js index 1150b3f342..4f3017ec44 100644 --- a/src/app/services/cryptoService.js +++ b/src/app/services/cryptoService.js @@ -295,6 +295,41 @@ angular }; _service.encrypt = function (plainValue, key, plainValueEncoding) { + var encValue = aesEncrypt(plainValue, key, plainValueEncoding); + + var iv = forge.util.encode64(encValue.iv); + var ct = forge.util.encode64(encValue.ct); + var cipherString = iv + '|' + ct; + + if (encValue.mac) { + var mac = forge.util.encode64(encValue.mac) + cipherString = cipherString + '|' + mac; + } + + return encValue.key.encType + '.' + cipherString; + }; + + _service.encryptToBytes = function (plainValue, key) { + return aesEncryptWC(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(plainValue, key, plainValueEncoding) { key = key || _service.getEncKey() || _service.getKey(); if (!key) { @@ -309,18 +344,55 @@ angular cipher.update(buffer); cipher.finish(); - var iv = forge.util.encode64(ivBytes); var ctBytes = cipher.output.getBytes(); - var ct = forge.util.encode64(ctBytes); - var cipherString = iv + '|' + ct; + var macBytes = null; if (key.macKey) { - var mac = computeMac(ivBytes + ctBytes, key.macKey, true); - cipherString = cipherString + '|' + mac; + macBytes = computeMac(ivBytes + ctBytes, key.macKey, false); } - return key.encType + '.' + cipherString; - }; + return { + iv: ivBytes, + ct: ctBytes, + mac: macBytes, + key: key, + plainValueEncoding: plainValueEncoding + }; + } + + function aesEncryptWC(plainValue, key) { + key = key || _service.getEncKey() || _service.getKey(); + + if (!key) { + throw 'Encryption key unavailable.'; + } + + var obj = { + iv: new Uint8Array(16), + ct: null, + mac: null, + key: key + }; + + var keyBuf = key.getBuffers(); + window.crypto.getRandomValues(obj.iv); + + return window.crypto.subtle.importKey('raw', keyBuf.encKey, { name: 'AES-CBC' }, false, ['encrypt']) + .then(function (encKey) { + return window.crypto.subtle.encrypt({ name: 'AES-CBC', iv: obj.iv }, encKey, plainValue); + }).then(function (encValue) { + obj.ct = new Uint8Array(encValue); + if (!keyBuf.macKey) { + return null; + } + return computeMacWC(encValue, keyBuf.macKey); + }).then(function (mac) { + if (mac) { + obj.mac = new Uint8Array(mac); + } + return obj; + }); + } _service.rsaEncrypt = function (plainValue, publicKey, key) { publicKey = publicKey || _service.getPublicKey(); @@ -513,6 +585,13 @@ angular return b64Output ? forge.util.encode64(mac.getBytes()) : mac.getBytes(); } + function computeMacWC(data, macKey) { + return window.crypto.subtle.importKey('raw', macKey, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']) + .then(function (key) { + return window.crypto.subtle.sign({ name: 'HMAC' }, key, data); + }); + } + // 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) { @@ -577,5 +656,33 @@ angular } } + SymmetricCryptoKey.prototype.getBuffers = function () { + var key = b64ToArray(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; + } + + return keys; + }; + + function b64ToArray(b64Str) { + var binaryString = window.atob(b64Str); + var arr = new Uint8Array(binaryString.length); + for (var i = 0; i < binaryString.length; i++) { + arr[i] = binaryString.charCodeAt(i); + } + return arr; + } + return _service; }); \ No newline at end of file diff --git a/src/app/vault/vaultAttachmentsController.js b/src/app/vault/vaultAttachmentsController.js new file mode 100644 index 0000000000..53a184632f --- /dev/null +++ b/src/app/vault/vaultAttachmentsController.js @@ -0,0 +1,53 @@ +angular + .module('bit.vault') + + .controller('vaultAttachmentsController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, + loginId, $analytics, validationService) { + $analytics.eventTrack('vaultAttachmentsController', { category: 'Modal' }); + $scope.login = {}; + $scope.readOnly = false; + $scope.loading = true; + + apiService.logins.get({ id: loginId }, function (login) { + $scope.login = cipherService.decryptLogin(login); + $scope.readOnly = !login.Edit; + $scope.loading = false; + }, function () { + $scope.loading = false; + }); + + $scope.save = function (form) { + var files = document.getElementById('file').files; + if (!files || !files.length) { + validationService.addError(form, 'file', 'Select a file.', true); + return; + } + + var file = files[0]; + var reader = new FileReader(); + reader.readAsArrayBuffer(file); + reader.onload = function (evt) { + var key = null; + var encFilename = cryptoService.encrypt(file.name, key); + cryptoService.encryptToBytes(evt.target.result, key).then(function (encData) { + var fd = new FormData(); + var blob = new Blob([encData], { type: 'application/octet-stream' }); + fd.append('data', blob, encFilename); + return apiService.ciphers.postAttachment({ id: loginId }, fd).$promise; + }).then(function (response) { + $analytics.eventTrack('Added Attachment'); + $uibModalInstance.close({ + action: 'attach', + data: $scope.login + }); + }); + }; + reader.onerror = function (evt) { + validationService.addError(form, 'file', 'Error reading file.', true); + }; + } + + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + }); diff --git a/src/app/vault/vaultController.js b/src/app/vault/vaultController.js index b00fb84236..9944ca8004 100644 --- a/src/app/vault/vaultController.js +++ b/src/app/vault/vaultController.js @@ -199,6 +199,21 @@ }); }; + $scope.attachments = function (login) { + var addModel = $uibModal.open({ + animation: true, + templateUrl: 'app/vault/views/vaultAttachments.html', + controller: 'vaultAttachmentsController', + resolve: { + loginId: function () { return login.id; } + } + }); + + addModel.result.then(function (data) { + + }); + }; + $scope.editFolder = function (folder) { var editModel = $uibModal.open({ animation: true, diff --git a/src/app/vault/views/vault.html b/src/app/vault/views/vault.html index 8e05acb165..b11042a237 100644 --- a/src/app/vault/views/vault.html +++ b/src/app/vault/views/vault.html @@ -86,6 +86,11 @@ Edit +
  • + + Attachments + +
  • Share @@ -115,7 +120,9 @@ {{login.name}} - +
    {{login.username}} @@ -189,6 +196,11 @@ Edit
  • +
  • + + Attachments + +
  • Share @@ -219,7 +231,10 @@ {{login.name}} - + +
    {{login.username}} diff --git a/src/app/vault/views/vaultAttachments.html b/src/app/vault/views/vaultAttachments.html new file mode 100644 index 0000000000..7b0d8f700c --- /dev/null +++ b/src/app/vault/views/vaultAttachments.html @@ -0,0 +1,66 @@ + +
    + + +
    diff --git a/src/index.html b/src/index.html index 87be6f16a0..198315a420 100644 --- a/src/index.html +++ b/src/index.html @@ -163,6 +163,7 @@ +