ability to sync files with changed credentials

This commit is contained in:
Antelle 2016-01-16 23:32:50 +03:00
parent 7e10b08c06
commit 7180f387d5
12 changed files with 286 additions and 30 deletions

View File

@ -13,6 +13,7 @@ var Backbone = require('backbone'),
Storage = require('../storage'),
IdGenerator = require('../util/id-generator'),
Logger = require('../util/logger');
require('../mixins/protected-value-ex');
var AppModel = Backbone.Model.extend({
@ -465,10 +466,16 @@ var AppModel = Backbone.Model.extend({
Storage[storage].load(path, function(err, data, stat) {
logger.info('Load from storage', stat, err);
if (err) { return complete(err); }
file.mergeOrUpdate(data, function(err) {
file.mergeOrUpdate(data, options.remoteKey, function(err) {
logger.info('Merge complete', err);
that.refresh();
if (err) { return complete(err); }
if (err) {
if (err.code === 'InvalidKey') {
logger.info('Remote key changed, request to enter new key');
Backbone.trigger('remote-key-changed', { file: file });
}
return complete(err);
}
if (stat && stat.rev) {
logger.info('Update rev in file info');
fileInfo.set('rev', stat.rev);

View File

@ -44,6 +44,33 @@ var FileModel = Backbone.Model.extend({
},
open: function(password, fileData, keyFileData, callback) {
try {
password = this.convertPassword(password);
var credentials = new kdbxweb.Credentials(password, keyFileData);
var ts = logger.ts();
kdbxweb.Kdbx.load(fileData, credentials, (function(db, err) {
if (err) {
logger.error('Error opening file', err.code, err.message, err);
callback(err);
} else {
this.db = db;
this.readModel();
this.setOpenFile({ passwordLength: password.textLength });
if (keyFileData) {
kdbxweb.ByteUtils.zeroBuffer(keyFileData);
}
logger.info('Opened file ' + this.get('name') + ': ' + logger.ts(ts) + ', ' +
db.header.keyEncryptionRounds + ' rounds, ' + Math.round(fileData.byteLength / 1024) + ' kB');
callback();
}
}).bind(this));
} catch (e) {
logger.error('Error opening file', e, e.code, e.message, e);
callback(e);
}
},
convertPassword: function(password) {
var len = password.value.length,
byteLength = 0,
value = new Uint8Array(len * 4),
@ -57,30 +84,7 @@ var FileModel = Backbone.Model.extend({
byteLength++;
}
}
password = new kdbxweb.ProtectedValue(value.buffer.slice(0, byteLength), salt.buffer.slice(0, byteLength));
try {
var credentials = new kdbxweb.Credentials(password, keyFileData);
var ts = logger.ts();
kdbxweb.Kdbx.load(fileData, credentials, (function(db, err) {
if (err) {
logger.error('Error opening file', err.code, err.message, err);
callback(err);
} else {
this.db = db;
this.readModel();
this.setOpenFile({ passwordLength: len });
if (keyFileData) {
kdbxweb.ByteUtils.zeroBuffer(keyFileData);
}
logger.info('Opened file ' + this.get('name') + ': ' + logger.ts(ts) + ', ' +
db.header.keyEncryptionRounds + ' rounds, ' + Math.round(fileData.byteLength / 1024) + ' kB');
callback();
}
}).bind(this));
} catch (e) {
logger.error('Error opening file', e, e.code, e.message, e);
callback(e);
}
return new kdbxweb.ProtectedValue(value.buffer.slice(0, byteLength), salt.buffer.slice(0, byteLength));
},
create: function(name) {
@ -162,13 +166,39 @@ var FileModel = Backbone.Model.extend({
this.trigger('reload', this);
},
mergeOrUpdate: function(fileData, callback) {
kdbxweb.Kdbx.load(fileData, this.db.credentials, (function(remoteDb, err) {
mergeOrUpdate: function(fileData, remoteKey, callback) {
var credentials;
if (remoteKey) {
credentials = new kdbxweb.Credentials(kdbxweb.ProtectedValue.fromString(''));
if (remoteKey.password) {
remoteKey.password = this.convertPassword(remoteKey.password);
credentials.setPassword(remoteKey.password);
} else {
credentials.passwordHash = this.db.credentials.passwordHash;
}
if (remoteKey.keyFileName) {
if (remoteKey.keyFileData) {
credentials.setKeyFile(remoteKey.keyFileData);
} else {
credentials.keyFileHash = this.db.credentials.keyFileHash;
}
}
} else {
credentials = this.db.credentials;
}
kdbxweb.Kdbx.load(fileData, credentials, (function(remoteDb, err) {
if (err) {
logger.error('Error opening file to merge', err.code, err.message, err);
} else {
if (this.get('modified')) {
try {
if (remoteKey && remoteDb.meta.keyChanged > this.db.meta.keyChanged) {
this.db.credentials = remoteDb.credentials;
this.set('keyFileName', remoteKey.keyFileName || '');
if (remoteKey.password) {
this.set('passwordLength', remoteKey.password.textLength);
}
}
this.db.merge(remoteDb);
} catch (e) {
logger.error('File merge error', e);

View File

@ -42,11 +42,15 @@ var Locale = {
alertClose: 'Close',
footerOpen: 'Open / New',
footerSyncError: 'Sync error',
genLen: 'Length',
grpTitle: 'Group',
grpSearch: 'Enable searching entries in this group',
keyChangeTitle: 'Master Key Changed',
keyChangeMessage: 'Master key was changed for this database. Please enter a new key',
iconFavTitle: 'Download and use website favicon',
iconSelCustom: 'Select custom icon',

View File

@ -10,6 +10,7 @@ var Backbone = require('backbone'),
GrpView = require('../views/grp-view'),
OpenView = require('../views/open-view'),
SettingsView = require('../views/settings/settings-view'),
KeyChangeView = require('../views/key-change-view'),
Alerts = require('../comp/alerts'),
Keys = require('../const/keys'),
Timeouts = require('../const/timeouts'),
@ -60,6 +61,7 @@ var AppView = Backbone.View.extend({
this.listenTo(Backbone, 'show-file', this.showFileSettings);
this.listenTo(Backbone, 'open-file', this.toggleOpenFile);
this.listenTo(Backbone, 'save-all', this.saveAll);
this.listenTo(Backbone, 'remote-key-changed', this.remoteKeyChanged);
this.listenTo(Backbone, 'toggle-settings', this.toggleSettings);
this.listenTo(Backbone, 'toggle-menu', this.toggleMenu);
this.listenTo(Backbone, 'toggle-details', this.toggleDetails);
@ -105,6 +107,7 @@ var AppView = Backbone.View.extend({
this.views.footer.toggle(this.model.files.hasOpenFiles());
this.hideSettings();
this.hideOpenFile();
this.hideKeyChange();
this.views.open = new OpenView({ model: this.model });
this.views.open.setElement(this.$el.find('.app__body')).render();
this.views.open.on('close', this.showEntries, this);
@ -143,6 +146,7 @@ var AppView = Backbone.View.extend({
this.views.footer.show();
this.hideOpenFile();
this.hideSettings();
this.hideKeyChange();
},
hideOpenFile: function() {
@ -160,6 +164,13 @@ var AppView = Backbone.View.extend({
}
},
hideKeyChange: function() {
if (this.views.keyChange) {
this.views.keyChange.hide();
this.views.keyChange = null;
}
},
showSettings: function(selectedMenuItem) {
this.model.menu.setMenu('settings');
this.views.menu.show();
@ -170,6 +181,7 @@ var AppView = Backbone.View.extend({
this.views.details.hide();
this.views.grp.hide();
this.hideOpenFile();
this.hideKeyChange();
this.views.settings = new SettingsView({ model: this.model });
this.views.settings.setElement(this.$el.find('.app__body')).render();
if (!selectedMenuItem) {
@ -187,6 +199,22 @@ var AppView = Backbone.View.extend({
this.views.grp.show();
},
showKeyChange: function(file) {
if (this.views.keyChange || Alerts.alertDisplayed) {
return;
}
this.views.menu.hide();
this.views.listWrap.hide();
this.views.list.hide();
this.views.listDrag.hide();
this.views.details.hide();
this.views.grp.hide();
this.views.keyChange = new KeyChangeView({ model: file });
this.views.keyChange.setElement(this.$el.find('.app__body')).render();
this.views.keyChange.on('accept', this.keyChangeAccept.bind(this));
this.views.keyChange.on('cancel', this.showEntries.bind(this));
},
fileListUpdated: function() {
if (this.model.files.hasOpenFiles()) {
this.showEntries();
@ -402,6 +430,21 @@ var AppView = Backbone.View.extend({
}
},
remoteKeyChanged: function(e) {
this.showKeyChange(e.file);
},
keyChangeAccept: function(e) {
this.showEntries();
this.model.syncFile(e.file, {
remoteKey: {
password: e.password,
keyFileName: e.keyFileName,
keyFileData: e.keyFileData
}
});
},
toggleSettings: function(page) {
var menuItem = page ? this.model.menu[page + 'Section'] : null;
if (menuItem) {

View File

@ -0,0 +1,96 @@
'use strict';
var Backbone = require('backbone'),
SecureInput = require('../comp/secure-input'),
Alerts = require('../comp/alerts'),
Locale = require('../util/locale'),
Keys = require('../const/keys');
var KeyChangeView = Backbone.View.extend({
template: require('templates/key-change.hbs'),
events: {
'keydown .key-change__pass': 'inputKeydown',
'click .key-change__keyfile': 'keyFileClicked',
'change .key-change__file': 'keyFileSelected',
'click .key-change__btn-ok': 'accept',
'click .key-change__btn-cancel': 'cancel'
},
passwordInput: null,
inputEl: null,
initialize: function() {
this.passwordInput = new SecureInput();
},
render: function() {
this.keyFileName = this.model.get('keyFileName') || null;
this.keyFileData = null;
this.renderTemplate({
fileName: this.model.get('name'),
keyFileName: this.model.get('keyFileName')
});
this.$el.find('.key-change__keyfile-name').text(this.keyFileName ? ': ' + this.keyFileName : '');
this.inputEl = this.$el.find('.key-change__pass');
this.passwordInput.reset();
this.passwordInput.setElement(this.inputEl);
},
remove: function() {
Backbone.View.prototype.remove.apply(this, arguments);
},
inputKeydown: function(e) {
var code = e.keyCode || e.which;
if (code === Keys.DOM_VK_RETURN) {
this.accept();
} else if (code === Keys.DOM_VK_A) {
e.stopImmediatePropagation();
}
},
keyFileClicked: function() {
if (this.keyFileName) {
this.keyFileName = null;
this.keyFile = null;
this.$el.find('.key-change__keyfile-name').html('');
}
this.$el.find('.key-change__file').val(null).click();
this.inputEl.focus();
},
keyFileSelected: function(e) {
var file = e.target.files[0];
if (file) {
var reader = new FileReader();
reader.onload = (function(e) {
this.keyFileName = file.name;
this.keyFileData = e.target.result;
this.$el.find('.key-change__keyfile-name').text(': ' + this.keyFileName);
}).bind(this);
reader.onerror = function() {
Alerts.error({ header: Locale.openFailedRead });
};
reader.readAsArrayBuffer(file);
} else {
this.$el.find('.key-change__keyfile-name').html('');
}
this.inputEl.focus();
},
accept: function() {
this.trigger('accept', {
file: this.model,
password: this.passwordInput.value,
keyFileName: this.keyFileName,
keyFileData: this.keyFileData
});
},
cancel: function() {
this.trigger('cancel');
}
});
module.exports = KeyChangeView;

View File

@ -0,0 +1,55 @@
.key-change {
@include flex(1);
@include display(flex);
@include align-items(stretch);
@include flex-direction(column);
@include justify-content(center);
overflow: hidden;
padding: $base-spacing;
position: relative;
@include mobile {
padding: $base-padding;
}
&__icon {
font-size: $modal-icon-size;
text-align: center;
}
&__header {
font-size: $small-header-font-size;
text-align: center;
}
&__body {
@include flex(0);
@include display(flex);
@include align-items(flex-start);
@include flex-direction(column);
margin: $base-spacing 0;
}
&__input {
@include align-self(center);
}
input[type=password].key-change__pass {
font-size: $large-pass-font-size;
margin: $small-spacing 0 0;
}
&__keyfile {
@include th { color: muted-color(); }
&:hover { @include th { color: medium-color(); } }
margin-top: $base-padding-v;
cursor: pointer;
}
&__buttons {
text-align: right;
button ~ button {
margin-left: $small-spacing;
}
>button {
margin-bottom: $small-spacing;
}
}
&__body, &__buttons {
@include align-self(center);
width: 40%;
}
}

View File

@ -22,6 +22,7 @@ $base-padding-h: .8em;
$base-padding: $base-padding-v $base-padding-h;
$medium-padding: .8em 1em;
$base-padding-px: 4px 8px;
$modal-icon-size: 6em;
// Borders
@function base-border() { @return 1px solid base-border-color(); };

View File

@ -24,7 +24,7 @@
}
&__icon {
font-size: 6em;
font-size: $modal-icon-size;
text-align: center;
}
&__header {

View File

@ -25,6 +25,7 @@
@import "areas/footer";
@import "areas/grp";
@import "areas/generator";
@import "areas/key-change";
@import "areas/list";
@import "areas/menu";
@import "areas/open";

View File

@ -6,7 +6,7 @@
<i class="fa fa-refresh fa-spin footer__db-sign"></i>
{{~else if file.attributes.syncError~}}
<i class="fa {{#if file.attributes.modified}}fa-circle{{else}}fa-circle-thin{{/if}} footer__db-sign footer__db-sign--error"
title="Sync error: {{file.attributes.syncError}}"></i>
title="{{res 'footerSyncError'}}: {{file.attributes.syncError}}"></i>
{{~else if file.attributes.modified~}}
<i class="fa fa-circle footer__db-sign"></i>
{{~/if}}

View File

@ -0,0 +1,18 @@
<div class="key-change">
<i class="key-change__icon fa fa-lock"></i>
<div class="key-change__header">{{fileName}}: {{res 'keyChangeTitle'}}</div>
<div class="key-change__body">
<div class="key-change__message">{{res 'keyChangeMessage'}}:</div>
<div class="key-change__input">
<input class="key-change__file hide-by-pos" type="file" />
<input class="key-change__pass" type="password" size="30" autocomplete="off" maxlength="128" autofocus />
<div class="key-change__keyfile">
<i class="fa fa-key"></i> {{res 'openKeyFile'}}<span class="key-change__keyfile-name"></span>
</div>
</div>
</div>
<div class="key-change__buttons">
<button class="key-change__btn-ok" data-result="ok">{{res 'alertOk'}}</button>
<button class="btn-error key-change__btn-cancel" data-result="">{{res 'alertCancel'}}</button>
</div>
</div>

View File

@ -3,6 +3,7 @@ Release notes
##### v0.6.0 (not released yet)
Improvements
`+` advanced search
`+` ability to sync files with changed credentials
`+` save at exit for desktop app
`+` more reliable binaries management
`+` string resources globalization