keeweb/app/scripts/util/otp.js

156 lines
4.8 KiB
JavaScript
Raw Normal View History

2016-03-31 22:52:04 +02:00
'use strict';
2016-04-01 23:09:16 +02:00
var Logger = require('./logger');
2016-04-01 21:19:27 +02:00
var logger = new Logger('otp');
2016-03-31 22:52:04 +02:00
var Otp = function(url, params) {
if (['hotp', 'totp'].indexOf(params.type) < 0) {
throw 'Bad type: ' + params.type;
}
if (!params.secret) {
throw 'Empty secret';
}
if (params.algorithm && ['SHA1', 'SHA256', 'SHA512'].indexOf(params.algorithm) < 0) {
throw 'Bad algorithm: ' + params.algorithm;
}
if (params.digits && ['6', '8'].indexOf(params.digits) < 0) {
throw 'Bad digits: ' + params.digits;
}
if (params.type === 'hotp' && !params.counter) {
throw 'Bad counter: ' + params.counter;
}
if (params.period && isNaN(params.period) || params.period < 1) {
throw 'Bad period: ' + params.period;
}
2016-04-01 21:19:27 +02:00
2016-03-31 22:52:04 +02:00
this.url = url;
2016-04-01 21:19:27 +02:00
2016-03-31 22:52:04 +02:00
this.type = params.type;
this.issuer = params.issuer;
this.account = params.account;
this.secret = params.secret;
this.issuer = params.issuer;
this.algorithm = params.algorithm ? params.algorithm.toUpperCase() : 'SHA1';
this.digits = params.digits ? +params.digits : 6;
this.counter = params.counter;
this.period = params.period ? +params.period : 30;
2016-04-01 21:19:27 +02:00
this.key = Otp.fromBase32(this.secret);
2016-04-04 20:45:17 +02:00
if (!this.key) {
throw 'Bad key: ' + this.key;
}
2016-04-01 21:19:27 +02:00
};
Otp.prototype.next = function(callback) {
2016-04-01 23:16:23 +02:00
var valueForHashing;
var timeLeft;
if (this.type === 'totp') {
var now = Date.now();
var epoch = Math.round(now / 1000);
valueForHashing = Math.floor(epoch / this.period);
var msPeriod = this.period * 1000;
timeLeft = msPeriod - (now % msPeriod);
} else {
valueForHashing = this.counter;
}
2016-04-02 15:57:38 +02:00
var data = new Uint8Array(8).buffer;
new DataView(data).setUint32(4, valueForHashing);
2016-07-17 13:30:38 +02:00
this.hmac(data, (sig, err) => {
2016-04-01 21:19:27 +02:00
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();
2016-07-17 13:30:38 +02:00
pass = Otp.leftPad(pass.substr(pass.length - this.digits), this.digits);
2016-04-01 21:19:27 +02:00
callback(pass, timeLeft);
});
};
Otp.prototype.hmac = function(data, callback) {
2016-04-02 18:00:15 +02:00
if (!window.crypto && window.msCrypto) {
return this.hmacMsCrypto(data, callback);
}
2016-04-01 21:19:27 +02:00
var subtle = window.crypto.subtle || window.crypto.webkitSubtle;
2016-04-02 15:57:38 +02:00
var algo = { name: 'HMAC', hash: { name: this.algorithm.replace('SHA', 'SHA-') } };
subtle.importKey('raw', this.key, algo, false, ['sign'])
2016-07-17 13:30:38 +02:00
.then(key => {
2016-04-02 15:57:38 +02:00
subtle.sign(algo, key, data)
2016-07-17 13:30:38 +02:00
.then(sig => { callback(sig); })
.catch(err => { callback(null, err); });
2016-04-01 21:19:27 +02:00
})
2016-07-17 13:30:38 +02:00
.catch(err => { callback(null, err); });
2016-04-01 21:19:27 +02:00
};
2016-04-02 18:00:15 +02:00
Otp.prototype.hmacMsCrypto = function(data, callback) {
var subtle = window.msCrypto.subtle;
var algo = { name: 'HMAC', hash: { name: this.algorithm.replace('SHA', 'SHA-') } };
subtle.importKey('raw', this.key, algo, false, ['sign']).oncomplete = function(e) {
var key = e.target.result;
subtle.sign(algo, key, data).oncomplete = function(e) {
var sig = e.target.result;
callback(sig);
};
};
};
2016-04-01 21:19:27 +02:00
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) {
2016-04-04 20:45:17 +02:00
return null;
2016-04-01 21:19:27 +02:00
}
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;
2016-03-31 22:52:04 +02:00
};
Otp.parseUrl = function(url) {
var match = /^otpauth:\/\/(\w+)\/([^\?]+)\?(.*)/i.exec(url);
if (!match) {
throw 'Not OTP url';
}
var params = {};
var label = decodeURIComponent(match[2]);
if (label) {
var parts = label.split(':');
params.issuer = parts[0].trim();
if (parts.length > 1) {
params.account = parts[1].trim();
}
}
params.type = match[1].toLowerCase();
2016-07-17 13:30:38 +02:00
match[3].split('&').forEach(part => {
2016-03-31 22:52:04 +02:00
var parts = part.split('=', 2);
params[parts[0].toLowerCase()] = decodeURIComponent(parts[1]);
});
return new Otp(url, params);
};
2016-04-04 20:45:17 +02:00
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 : '');
};
2016-03-31 22:52:04 +02:00
module.exports = Otp;