mirror of https://github.com/keeweb/keeweb.git
otp
This commit is contained in:
parent
2dbc350b53
commit
6217c97441
|
@ -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>'
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue