mirror of https://github.com/keeweb/keeweb.git
ability to sync files with changed credentials
This commit is contained in:
parent
7e10b08c06
commit
7180f387d5
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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(); };
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 6em;
|
||||
font-size: $modal-icon-size;
|
||||
text-align: center;
|
||||
}
|
||||
&__header {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue