enter otp manually

This commit is contained in:
antelle 2016-04-04 21:45:17 +03:00
parent 33f9df3fe3
commit 98ebff0534
7 changed files with 79 additions and 30 deletions

View File

@ -24,11 +24,12 @@ var OtpQrReader = {
Locale.detSetupOtpAlertBodyWith.replace('{}',
'<code>' + FeatureDetector.actionShortcutSymbol() + 'V</code>');
OtpQrReader.startListenClipoard();
var buttons = [Alerts.buttons.cancel];
var buttons = [{result: 'manually', title: Locale.detSetupOtpManualButton, silent: true},
Alerts.buttons.cancel];
if (FeatureDetector.isMobile()) {
buttons.unshift({result: 'select', title: Locale.detSetupOtoScanButton});
buttons.unshift({result: 'select', title: Locale.detSetupOtpScanButton});
}
var lastLine = FeatureDetector.isMobile() ? Locale.detSetupOtpAlertBody3Mobile :
var line3 = FeatureDetector.isMobile() ? Locale.detSetupOtpAlertBody3Mobile :
Locale.detSetupOtpAlertBody3.replace('{}', pasteKey || '');
OtpQrReader.alert = Alerts.alert({
icon: 'qrcode',
@ -36,7 +37,8 @@ var OtpQrReader = {
body: [Locale.detSetupOtpAlertBody,
Locale.detSetupOtpAlertBody1,
Locale.detSetupOtpAlertBody2.replace('{}', screenshotKey || ''),
lastLine
line3,
Locale.detSetupOtpAlertBody4
].join('<br/>'),
esc: '',
click: '',
@ -47,6 +49,8 @@ var OtpQrReader = {
OtpQrReader.stopListenClipboard();
if (res === 'select') {
OtpQrReader.selectFile();
} else if (res === 'manually') {
OtpQrReader.enterManually();
}
}
});
@ -150,6 +154,10 @@ var OtpQrReader = {
image.src = imageData;
},
enterManually: function() {
OtpQrReader.trigger('enter-manually');
},
removeAlert: function() {
if (OtpQrReader.alert) {
OtpQrReader.alert.closeImmediate();

View File

@ -377,7 +377,9 @@ var EntryModel = Backbone.Model.extend({
if (otpUrl.isProtected) {
otpUrl = otpUrl.getText();
}
if (otpUrl.toLowerCase().lastIndexOf('otpauth:', 0) !== 0) {
if (Otp.isSecret(otpUrl)) {
otpUrl = Otp.makeUrl(otpUrl);
} else if (otpUrl.toLowerCase().lastIndexOf('otpauth:', 0) !== 0) {
// KeeOTP plugin format
var args = {};
otpUrl.split('&').forEach(function(part) {
@ -385,34 +387,33 @@ var EntryModel = Backbone.Model.extend({
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 : '');
otpUrl = Otp.makeUrl(args.key, args.step, args.size);
}
}
} else if (this.entry.fields['TOTP Seed']) {
// TrayTOTP plugin format
var key = this.entry.fields['TOTP Seed'];
if (key.isProtected) {
key = key.getText();
var secret = this.entry.fields['TOTP Seed'];
if (secret.isProtected) {
secret = secret.getText();
}
if (key) {
otpUrl = 'otpauth://totp/null?secret=' + key;
}
var settings = this.entry.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 (secret) {
var settings = this.entry.fields['TOTP Settings'];
if (settings && settings.isProtected) {
settings = settings.getText();
}
if (settings.length > 1 && settings[1] > 0) {
otpUrl += '&digits=' + settings[1];
var period, digits;
if (settings) {
settings = settings.split(';');
if (settings.length > 0 && settings[0] > 0) {
period = settings[0];
}
if (settings.length > 1 && settings[1] > 0) {
digits = settings[1];
}
}
otpUrl = Otp.makeUrl(secret, period, digits);
this.fields.otp = kdbxweb.ProtectedValue.fromString(otpUrl);
}
this.fields.otp = kdbxweb.ProtectedValue.fromString(otpUrl);
}
if (otpUrl) {
if (this.otpGenerator && this.otpGenerator.url === otpUrl) {
@ -434,7 +435,7 @@ var EntryModel = Backbone.Model.extend({
},
setOtpUrl: function(url) {
this.setField('otp', kdbxweb.ProtectedValue.fromString(url));
this.setField('otp', url ? kdbxweb.ProtectedValue.fromString(url) : undefined);
delete this.entry.fields['TOTP Seed'];
delete this.entry.fields['TOTP Settings'];
}

View File

@ -207,7 +207,9 @@ var Locale = {
detSetupOtpAlertBody2: '2. make a screenshot of the QR code {}',
detSetupOtpAlertBody3: '3. paste it here {}',
detSetupOtpAlertBody3Mobile: '3. select it or scan with your camera using Select/Scan button below',
detSetupOtoScanButton: 'Select/Scan',
detSetupOtpAlertBody4: 'If you can\'t scan code, click Enter code manually',
detSetupOtpManualButton: 'Enter code manually',
detSetupOtpScanButton: 'Select/Scan',
detSetupOtpAlertBodyWith: 'with {}',
detOtpImageError: 'Error reading image',
detOtpImageErrorBody: 'Sorry, we could not read the image format, please contact the app authors with error details.',

View File

@ -37,6 +37,9 @@ var Otp = function(url, params) {
this.period = params.period ? +params.period : 30;
this.key = Otp.fromBase32(this.secret);
if (!this.key) {
throw 'Bad key: ' + this.key;
}
};
Otp.prototype.next = function(callback) {
@ -101,7 +104,7 @@ Otp.fromBase32 = function(str) {
for (i = 0; i < str.length; i++) {
var ix = alphabet.indexOf(str[i].toLowerCase());
if (ix < 0) {
throw 'Bad base32: ' + str;
return null;
}
bin += Otp.leftPad(ix.toString(2), 5);
}
@ -142,4 +145,12 @@ Otp.parseUrl = function(url) {
return new Otp(url, params);
};
Otp.isSecret = function(str) {
return !!Otp.fromBase32(str);
};
Otp.makeUrl = function(secret, period, digits) {
return 'otpauth://totp/default?secret=' + secret + (period ? '&period=' + period : '') + (digits ? '&digits=' + digits : '');
};
module.exports = Otp;

View File

@ -65,6 +65,7 @@ var DetailsView = Backbone.View.extend({
this.listenTo(Backbone, 'copy-user', this.copyUserName);
this.listenTo(Backbone, 'copy-url', this.copyUrl);
this.listenTo(OtpQrReqder, 'qr-read', this.otpCodeRead);
this.listenTo(OtpQrReqder, 'enter-manually', this.otpEnterManually);
KeyHandler.onKey(Keys.DOM_VK_C, this.copyPassword, this, KeyHandler.SHORTCUT_ACTION, false, true);
KeyHandler.onKey(Keys.DOM_VK_B, this.copyUserName, this, KeyHandler.SHORTCUT_ACTION, false, true);
KeyHandler.onKey(Keys.DOM_VK_U, this.copyUrl, this, KeyHandler.SHORTCUT_ACTION, false, true);
@ -488,7 +489,11 @@ var DetailsView = Backbone.View.extend({
if (oldValue && oldValue.isProtected) {
oldValue = oldValue.getText();
}
if (value && value.isProtected) {
value = value.getText();
}
if (oldValue === value) {
this.render();
return false;
}
this.model.setOtpUrl(value);
@ -712,6 +717,26 @@ var DetailsView = Backbone.View.extend({
otpCodeRead: function(otp) {
this.model.setOtp(otp);
this.entryUpdated();
},
otpEnterManually: function() {
if (this.model.fields.otp) {
var otpField = this.fieldViews.find(function(f) { return f.model.name === '$otp'; });
if (otpField) {
otpField.edit();
}
} else {
this.moreView.remove();
this.moreView = null;
var fieldView = new FieldViewCustom({ model: {
name: '$otp', title: 'otp', newField: 'otp',
value: kdbxweb.ProtectedValue.fromString('')
}});
fieldView.on('change', this.fieldChanged.bind(this));
fieldView.setElement(this.$el.find('.details__body-fields')).render();
fieldView.edit();
this.fieldViews.push(fieldView);
}
}
});

View File

@ -70,7 +70,9 @@ var FieldViewOtp = FieldViewText.extend({
this.otpValue = pass || '';
this.otpTimeLeft = timeLeft || 0;
this.otpValidUntil = Date.now() + timeLeft;
this.render();
if (!this.editing) {
this.render();
}
if (this.otpValue && timeLeft) {
this.otpTimeout = setTimeout(this.requestOtpUpdate.bind(this), timeLeft);
if (!this.otpTickInterval) {

View File

@ -10,7 +10,7 @@
</div>
<div class="modal__buttons">
{{#each buttons as |btn|}}
<button class="{{#unless btn.result}}btn-error{{/unless}} {{#if btn.error}}btn-error{{/if}}"
<button class="{{#unless btn.result}}btn-error{{/unless}} {{#if btn.error}}btn-error{{/if}} {{#if btn.silent}}btn-silent{{/if}}"
data-result="{{btn.result}}">{{btn.title}}</button>
{{/each}}
</div>