This commit is contained in:
antelle 2016-04-01 22:19:27 +03:00
parent 2dbc350b53
commit 6217c97441
5 changed files with 194 additions and 6 deletions

View File

@ -85,13 +85,14 @@ var OtpQrReader = {
try {
var ts = logger.ts();
var url = new QrCode(image).decode();
logger.info('QR code read', logger.ts(ts), url);
logger.info('QR code read', logger.ts(ts));
OtpQrReader.alert.remove();
OtpQrReader.stopListenClipboard();
try {
var otp = Otp.parseUrl(url);
OtpQrReader.trigger('qr-read', otp);
} catch (err) {
logger.error('Error parsing QR code', err);
Alerts.error({
header: Locale.detOtpQrWrong,
body: Locale.detOtpQrWrongBody + '<pre class="modal__pre">' + _.escape(err.toString()) +'</pre>'

View File

@ -1,5 +1,9 @@
'use strict';
var Logger = require('../util/logger');
var logger = new Logger('otp');
var Otp = function(url, params) {
if (['hotp', 'totp'].indexOf(params.type) < 0) {
throw 'Bad type: ' + params.type;
@ -19,7 +23,9 @@ var Otp = function(url, params) {
if (params.period && isNaN(params.period) || params.period < 1) {
throw 'Bad period: ' + params.period;
}
this.url = url;
this.type = params.type;
this.issuer = params.issuer;
this.account = params.account;
@ -29,6 +35,71 @@ var Otp = function(url, params) {
this.digits = params.digits ? +params.digits : 6;
this.counter = params.counter;
this.period = params.period ? +params.period : 30;
this.key = Otp.fromBase32(this.secret);
};
Otp.prototype.next = function(callback) {
var now = Date.now();
var epoch = Math.round(now / 1000);
var time = Math.floor(epoch / this.period);
var msPeriod = this.period * 1000;
var timeLeft = msPeriod - (now % msPeriod);
var data = new Uint8Array(8);
new DataView(data.buffer).setUint32(4, time);
var that = this;
this.hmac(data, function(sig, err) {
if (!sig) {
logger.error('OTP calculation error', err);
return callback();
}
sig = new DataView(sig);
var offset = sig.getInt8(sig.byteLength - 1) & 0xf;
var pass = (sig.getUint32(offset) & 0x7fffffff).toString();
pass = Otp.leftPad(pass.substr(pass.length - that.digits), that.digits);
callback(pass, timeLeft);
});
};
Otp.prototype.hmac = function(data, callback) {
var subtle = window.crypto.subtle || window.crypto.webkitSubtle;
subtle.importKey('raw', this.key,
{ name: 'HMAC', hash: { name: this.algorithm.replace('SHA', 'SHA-') } },
false, ['sign'])
.then(function(key) {
subtle.sign({ name: 'HMAC' }, key, data)
.then(function(sig) {
callback(sig);
})
.catch(function(err) { callback(null, err); });
})
.catch(function(err) { callback(null, err); });
};
Otp.fromBase32 = function(str) {
var alphabet = 'abcdefghijklmnopqrstuvwxyz234567';
var bin = '';
var i;
for (i = 0; i < str.length; i++) {
var ix = alphabet.indexOf(str[i].toLowerCase());
if (ix < 0) {
throw 'Bad base32: ' + str;
}
bin += Otp.leftPad(ix.toString(2), 5);
}
var hex = new Uint8Array(Math.floor(bin.length / 8));
for (i = 0; i < hex.length; i++) {
var chunk = bin.substr(i * 8, 8);
hex[i] = parseInt(chunk, 2);
}
return hex.buffer;
};
Otp.leftPad = function(str, len) {
while (str.length < len) {
str = '0' + str;
}
return str;
};
Otp.parseUrl = function(url) {

View File

@ -13,6 +13,7 @@ var Backbone = require('backbone'),
FieldViewReadOnly = require('../fields/field-view-read-only'),
FieldViewHistory = require('../fields/field-view-history'),
FieldViewCustom = require('../fields/field-view-custom'),
FieldViewOtp = require('../fields/field-view-otp'),
IconSelectView = require('../icon-select-view'),
DetailsHistoryView = require('./details-history-view'),
DetailsAttachmentView = require('./details-attachment-view'),
@ -23,6 +24,7 @@ var Backbone = require('backbone'),
Alerts = require('../../comp/alerts'),
CopyPaste = require('../../comp/copy-paste'),
OtpQrReqder = require('../../comp/otp-qr-reader'),
Otp = require('../../comp/otp'),
Format = require('../../util/format'),
Locale = require('../../util/locale'),
Tip = require('../../util/tip'),
@ -40,6 +42,10 @@ var DetailsView = Backbone.View.extend({
userEditView: null,
urlEditView: null,
fieldCopyTip: null,
otpTimer: null,
otp: null,
otpValue: null,
otpView: null,
events: {
'click .details__colors-popup-item': 'selectColor',
@ -78,6 +84,7 @@ var DetailsView = Backbone.View.extend({
KeyHandler.offKey(Keys.DOM_VK_DELETE, this.deleteKeyPress, this, KeyHandler.SHORTCUT_ACTION);
KeyHandler.offKey(Keys.DOM_VK_BACK_SPACE, this.deleteKeyPress, this, KeyHandler.SHORTCUT_ACTION);
this.removeFieldViews();
this.stopOtpTimer();
Backbone.View.prototype.remove.call(this);
},
@ -90,9 +97,19 @@ var DetailsView = Backbone.View.extend({
}
},
stopOtpTimer: function() {
if (this.otpTimer) {
clearTimeout(this.otpTimer);
this.otpTimer = null;
}
this.otp = null;
this.otpValue = null;
},
render: function () {
this.removeScroll();
this.removeFieldViews();
this.stopOtpTimer();
if (this.views.sub) {
this.views.sub.remove();
delete this.views.sub;
@ -114,6 +131,7 @@ var DetailsView = Backbone.View.extend({
this.$el.html(this.template(model));
Tip.createTips(this.$el);
this.setSelectedColor(this.model.color);
this.setOtpByEntry();
this.addFieldViews();
this.createScroll({
root: this.$el.find('.details__body')[0],
@ -156,8 +174,15 @@ var DetailsView = Backbone.View.extend({
this.fieldViews.push(new FieldViewHistory({ model: { name: 'History', title: Locale.detHistory,
value: function() { return { length: model.historyLength, unsaved: model.unsaved }; } } }));
_.forEach(model.fields, function(value, field) {
this.fieldViews.push(new FieldViewCustom({ model: { name: '$' + field, title: field,
value: function() { return model.fields[field]; } } }));
if (field.toLowerCase() === 'otp') {
var that = this;
this.otpView = new FieldViewOtp({ model: { name: '$' + field, title: field,
value: function() { return that.otpValue; } } });
this.fieldViews.push(this.otpView);
} else {
this.fieldViews.push(new FieldViewCustom({ model: { name: '$' + field, title: field,
value: function() { return model.fields[field]; } } }));
}
}, this);
var hideEmptyFields = AppSettingsModel.instance.get('hideEmptyFields');
@ -681,12 +706,88 @@ var DetailsView = Backbone.View.extend({
Backbone.trigger('toggle-details', false);
},
setOtpByEntry: function() {
this.otp = null;
this.otpValue = null;
this.stopOtpTimer();
var otpUrl;
if (this.model.fields.otp) {
otpUrl = this.model.fields.otp;
if (otpUrl.isProtected) {
otpUrl = otpUrl.getText();
}
if (otpUrl.toLowerCase().lastIndexOf('otpauth:', 0) !== 0) {
// KeeOTP plugin format
var args = {};
otpUrl.split('&').forEach(function(part) {
var parts = part.split('=', 2);
args[parts[0]] = decodeURIComponent(parts[1]).replace(/=/g, '');
});
if (args.key) {
otpUrl = 'otpauth://totp/null?secret=' + args.key +
(args.step ? '&period=' + args.step : '') +
(args.size ? '&digits=' + args.size : '');
}
}
}
if (this.model.fields['TOTP Seed']) {
// TrayTOTP plugin format
var key = this.model.fields['TOTP Seed'];
if (key.isProtected) {
key = key.getText();
}
if (key) {
otpUrl = 'otpauth://totp/null?secret=' + key;
}
var settings = this.model.fields['TOTP Settings'];
if (settings && settings.isProtected) {
settings = settings.getText();
}
if (settings) {
settings = settings.split(';');
if (settings.length > 0 && settings[0] > 0) {
otpUrl += '&period=' + settings[0];
}
if (settings.length > 1 && settings[1] > 0) {
otpUrl += '&digits=' + settings[1];
}
}
this.model.fields.otp = kdbxweb.ProtectedValue.fromString(otpUrl);
}
if (otpUrl) {
try {
this.otp = Otp.parseUrl(otpUrl);
this.refreshOtp();
} catch (e) {
this.otp = null;
}
}
},
setupOtp: function() {
OtpQrReqder.read();
},
otpCodeRead: function(/*otpParams*/) {
Alerts.notImplemented();
otpCodeRead: function(otpParams) {
this.otp = otpParams;
this.refreshOtp();
},
refreshOtp: function() {
this.otpValue = null;
if (this.otp) {
var that = this;
this.otp.next(function (pass, timeLeft) {
if (!pass || !that.otp) {
return;
}
that.otpValue = { url: that.otp.url, pass: pass, time: timeLeft };
if (that.otpView) {
that.otpView.update();
}
setTimeout(that.refreshOtp.bind(that), timeLeft);
});
}
}
});

View File

@ -0,0 +1,15 @@
'use strict';
var FieldViewText = require('./field-view-text');
var FieldViewOtp = FieldViewText.extend({
renderValue: function(value) {
return value && value.pass || '';
},
getEditValue: function(value) {
return value ? value.url : '';
}
});
module.exports = FieldViewOtp;

View File

@ -34,6 +34,6 @@
"pikaday": "~1.3.3",
"FileSaver.js": "eligrey/FileSaver.js",
"jquery": "~2.2.0",
"jsqrcode": "antelle/jsqrcode#^0.1.0"
"jsqrcode": "antelle/jsqrcode#^0.1.1"
}
}