encrypt, upload, and view attachments

This commit is contained in:
Kyle Spearrin 2017-06-30 16:22:39 -04:00
parent 9c7b47c277
commit 7b4cf53ec4
8 changed files with 296 additions and 12 deletions

View File

@ -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', {}, {

View File

@ -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";

View File

@ -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;
});

View File

@ -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');
};
});

View File

@ -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,

View File

@ -86,6 +86,11 @@
<i class="fa fa-fw fa-pencil"></i> Edit
</a>
</li>
<li ng-show="login.edit">
<a href="#" stop-click ng-click="attachments(login)">
<i class="fa fa-fw fa-paperclip"></i> Attachments
</a>
</li>
<li ng-show="!login.organizationId">
<a href="#" stop-click ng-click="share(login)">
<i class="fa fa-fw fa-share-alt"></i> Share
@ -115,7 +120,9 @@
</td>
<td ng-click="select($event)">
<a href="#" stop-click ng-click="editLogin(login)" stop-prop>{{login.name}}</a>
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="login.organizationId"
<i class="fa fa-share-alt text-muted" title="Shared" ng-if="login.organizationId"
stop-prop></i>
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="login.hasAttachments"
stop-prop></i><br />
<span class="text-sm text-muted" stop-prop>{{login.username}}</span>
</td>
@ -189,6 +196,11 @@
<i class="fa fa-fw fa-pencil"></i> Edit
</a>
</li>
<li ng-show="login.edit">
<a href="#" stop-click ng-click="attachments(login)">
<i class="fa fa-fw fa-paperclip"></i> Attachments
</a>
</li>
<li ng-show="!login.organizationId">
<a href="#" stop-click ng-click="share(login)">
<i class="fa fa-fw fa-share-alt"></i> Share
@ -219,7 +231,10 @@
<td ng-click="select($event)">
<a href="#" stop-click ng-click="editLogin(login)" stop-prop>{{login.name}}</a>
<i class="fa fa-star text-muted" title="Favorite" ng-show="login.favorite" stop-prop></i>
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="login.organizationId" stop-prop></i>
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="login.organizationId"
stop-prop></i>
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="login.hasAttachments"
stop-prop></i>
<br />
<span class="text-sm text-muted" stop-prop>{{login.username}}</span>
</td>

View File

@ -0,0 +1,66 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">
<i class="fa fa-paperclip"></i> Secure Attachments <small>{{login.name}}</small>
</h4>
</div>
<form name="form" ng-submit="form.$valid && save(form)" api-form="savePromise">
<div class="modal-body">
<div ng-if="loading">
Loading...
</div>
<div ng-if="!loading && !login.attachments.length">
There are no attachments for this login.
</div>
<div class="table-responsive" ng-if="login.attachments.length" style="margin: 0;">
<table class="table table-striped table-hover table-vmiddle" style="margin: 0;">
<tbody>
<tr ng-repeat="attachment in login.attachments | orderBy: ['fileName']">
<td style="width: 70px;">
<div class="btn-group" data-append-to=".modal">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-cog"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<a href="#" stop-click ng-click="download(attachment)">
<i class="fa fa-fw fa-download"></i> Download
</a>
<a href="#" stop-click ng-click="remove(attachment)" class="text-red"
ng-show="!readOnly">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
</ul>
</div>
</td>
<td>
<a href="#" stop-click ng-click="download(attachment)">{{attachment.fileName}}</a>
</td>
<td style="width: 80px; min-width: 80px;">
{{attachment.size}}
</td>
</tr>
</tbody>
</table>
</div>
<hr />
<h4>Add New Attachment</h4>
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-error>
<label for="file" class="sr-only">File</label>
<input type="file" id="file" name="file" />
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="form.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="form.$loading"></i>Save
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>
</form>

View File

@ -163,6 +163,7 @@
<script src="app/vault/vaultSharedController.js"></script>
<script src="app/vault/vaultLoginCollectionsController.js"></script>
<script src="app/vault/vaultMoveLoginsController.js"></script>
<script src="app/vault/vaultAttachmentsController.js"></script>
<script src="app/organization/organizationModule.js"></script>
<script src="app/organization/organizationDashboardController.js"></script>