encrypt, upload, and view attachments
This commit is contained in:
parent
9c7b47c277
commit
7b4cf53ec4
|
@ -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', {}, {
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
|
@ -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');
|
||||
};
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue