parent
f14a474178
commit
f979da095b
|
|
@ -7,7 +7,10 @@
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
body{font-family:sans-serif}
|
body{font-family:sans-serif}
|
||||||
body div{display:none}
|
body div{display:none}
|
||||||
body.select div#tokens,body.editing div#edit,body.scanning div#scan,body.showqr div#tokenqr{display:block}
|
body.select tr>:first-child,body.export tr>:nth-child(3),body.export tr>:nth-child(4){display:none}
|
||||||
|
body.select div.select,body.export div.export{display:block}
|
||||||
|
body.select div.export,body.export div.select{display:none}
|
||||||
|
body.select div#tokens,body.editing div#edit,body.scanning div#scan,body.showqr div#showqr,body.export div#tokens{display:block}
|
||||||
#tokens th,#tokens td{padding:5px}
|
#tokens th,#tokens td{padding:5px}
|
||||||
#tokens tr:nth-child(odd){background-color:#ccc}
|
#tokens tr:nth-child(odd){background-color:#ccc}
|
||||||
#tokens tr:nth-child(even){background-color:#eee}
|
#tokens tr:nth-child(even){background-color:#eee}
|
||||||
|
|
@ -33,6 +36,12 @@ form.totp tr.hotp,form.hotp tr.totp{display:none}
|
||||||
/* Start of all TOTP URLs */
|
/* Start of all TOTP URLs */
|
||||||
const otpAuthUrl = 'otpauth://';
|
const otpAuthUrl = 'otpauth://';
|
||||||
|
|
||||||
|
/* Start of all OTP migration URLs */
|
||||||
|
const otpMigrUrl = 'otpauth-migration://offline?data=';
|
||||||
|
|
||||||
|
/* Hash algorithms */
|
||||||
|
const otpAlgos = ['SHA1','SHA256','SHA512'];
|
||||||
|
|
||||||
const tokentypes = ['TOTP (Timed)', 'HOTP (Counter)'];
|
const tokentypes = ['TOTP (Timed)', 'HOTP (Counter)'];
|
||||||
|
|
||||||
/* Settings */
|
/* Settings */
|
||||||
|
|
@ -45,6 +54,8 @@ var tokens = settings.tokens;
|
||||||
*/
|
*/
|
||||||
function base32clean(val, nows) {
|
function base32clean(val, nows) {
|
||||||
var ret = val.replaceAll(/\s+/g, ' ');
|
var ret = val.replaceAll(/\s+/g, ' ');
|
||||||
|
ret = val.replaceAll(/0/g, 'O');
|
||||||
|
ret = val.replaceAll(/1/g, 'I');
|
||||||
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
|
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
|
||||||
if (nows) {
|
if (nows) {
|
||||||
ret = ret.replaceAll(/\s+/g, '');
|
ret = ret.replaceAll(/\s+/g, '');
|
||||||
|
|
@ -52,6 +63,48 @@ function base32clean(val, nows) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function b32encode(str) {
|
||||||
|
let buf = 0, bitcount = 0, ret = '';
|
||||||
|
while (str.length > 0) {
|
||||||
|
buf <<= 8;
|
||||||
|
buf |= str.charCodeAt(0);
|
||||||
|
bitcount += 8;
|
||||||
|
str = str.substr(1);
|
||||||
|
while (bitcount >= 5) {
|
||||||
|
ret += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'[(buf >> (bitcount - 5)) & 31];
|
||||||
|
bitcount -= 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function b32decode(seedstr) {
|
||||||
|
// RFC4648
|
||||||
|
var i, buf = 0, bitcount = 0, ret = '';
|
||||||
|
for (i in seedstr) {
|
||||||
|
var c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(seedstr.charAt(i).toUpperCase(), 0);
|
||||||
|
if (c != -1) {
|
||||||
|
buf <<= 5;
|
||||||
|
buf |= c;
|
||||||
|
bitcount += 5;
|
||||||
|
if (bitcount >= 8) {
|
||||||
|
ret += String.fromCharCode(buf >> (bitcount - 8));
|
||||||
|
buf &= (0xFF >> (16 - bitcount));
|
||||||
|
bitcount -= 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeLabel(token) {
|
||||||
|
let lbl = token['label'];
|
||||||
|
if (lbl == '') {
|
||||||
|
lbl = (token['issuer'] == '') ? token['account'] : token['issuer'] + ' (' + token['account'] + ')';
|
||||||
|
}
|
||||||
|
token['label'] = lbl.substr(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
/* Save changes to a token to the global tokens[] array.
|
/* Save changes to a token to the global tokens[] array.
|
||||||
* id is the index into the global tokens[].
|
* id is the index into the global tokens[].
|
||||||
* forget is a flag indicating if the token should be forgotten.
|
* forget is a flag indicating if the token should be forgotten.
|
||||||
|
|
@ -84,9 +137,16 @@ function saveEdit(id, forget) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showQr(url) {
|
||||||
|
tokenqr.clear();
|
||||||
|
tokenqr.makeCode(url);
|
||||||
|
qrPreviousClass = document.body.className;
|
||||||
|
document.body.className = 'showqr';
|
||||||
|
}
|
||||||
|
|
||||||
/* Generate and display a QR-code representing the current token.
|
/* Generate and display a QR-code representing the current token.
|
||||||
*/
|
*/
|
||||||
function showQrCode() {
|
function showTokenQr() {
|
||||||
var fe = document.forms['edittoken'].elements;
|
var fe = document.forms['edittoken'].elements;
|
||||||
var url = new String(otpAuthUrl);
|
var url = new String(otpAuthUrl);
|
||||||
switch (fe['type'].value) {
|
switch (fe['type'].value) {
|
||||||
|
|
@ -122,9 +182,7 @@ function showQrCode() {
|
||||||
if (fe['algorithm'].value != 'SHA1') {
|
if (fe['algorithm'].value != 'SHA1') {
|
||||||
url += '&algorithm=' + fe['algorithm'].value;
|
url += '&algorithm=' + fe['algorithm'].value;
|
||||||
}
|
}
|
||||||
tokenqr.clear();
|
showQr(url);
|
||||||
tokenqr.makeCode(url);
|
|
||||||
document.body.className = 'showqr';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTypeChanged() {
|
function onTypeChanged() {
|
||||||
|
|
@ -138,6 +196,7 @@ function onTypeChanged() {
|
||||||
* id is the index into the global tokens[].
|
* id is the index into the global tokens[].
|
||||||
*/
|
*/
|
||||||
function editToken(id) {
|
function editToken(id) {
|
||||||
|
if (document.body.className == 'export') return;
|
||||||
var p;
|
var p;
|
||||||
const selectMarkup = function(name, ary, cur, onchg) {
|
const selectMarkup = function(name, ary, cur, onchg) {
|
||||||
var ret = '<select name="' + name + '"' + ((typeof onchg == 'string') ? ' onchange="' + onchg + '"' : '') + '>';
|
var ret = '<select name="' + name + '"' + ((typeof onchg == 'string') ? ' onchange="' + onchg + '"' : '') + '>';
|
||||||
|
|
@ -163,7 +222,7 @@ function editToken(id) {
|
||||||
markup += selectMarkup('digits', ['6','7','8','9','10'], tokens[id].digits);
|
markup += selectMarkup('digits', ['6','7','8','9','10'], tokens[id].digits);
|
||||||
markup += '</td></tr>';
|
markup += '</td></tr>';
|
||||||
markup += '<tr><td>Hash:</td><td>';
|
markup += '<tr><td>Hash:</td><td>';
|
||||||
markup += selectMarkup('algorithm', ['SHA1','SHA256','SHA512'], tokens[id].algorithm);
|
markup += selectMarkup('algorithm', otpAlgos, tokens[id].algorithm);
|
||||||
markup += '</td></tr>';
|
markup += '</td></tr>';
|
||||||
markup += '</tbody><tr><td id="advbtn" colspan="2">';
|
markup += '</tbody><tr><td id="advbtn" colspan="2">';
|
||||||
markup += '<button type="button" onclick="document.getElementById(\'edittoken\').classList.toggle(\'showadv\')">Advanced</button>';
|
markup += '<button type="button" onclick="document.getElementById(\'edittoken\').classList.toggle(\'showadv\')">Advanced</button>';
|
||||||
|
|
@ -171,9 +230,9 @@ function editToken(id) {
|
||||||
markup += '<button type="button" onclick="updateTokens()">Cancel Edit</button>';
|
markup += '<button type="button" onclick="updateTokens()">Cancel Edit</button>';
|
||||||
markup += '<button type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>';
|
markup += '<button type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>';
|
||||||
if (tokens[id].isnew) {
|
if (tokens[id].isnew) {
|
||||||
markup += '<button type="button" onclick="startScan()">Scan QR Code</button>';
|
markup += '<button type="button" onclick="startScan(handleTokenQr,cancelTokenQr)">Scan QR</button>';
|
||||||
} else {
|
} else {
|
||||||
markup += '<button type="button" onclick="showQrCode()">Show QR Code</button>';
|
markup += '<button type="button" onclick="showTokenQr()">Show QR</button>';
|
||||||
markup += '<button type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>';
|
markup += '<button type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>';
|
||||||
}
|
}
|
||||||
document.getElementById('edit').innerHTML = markup;
|
document.getElementById('edit').innerHTML = markup;
|
||||||
|
|
@ -188,6 +247,46 @@ function addToken() {
|
||||||
editToken(tokens.length - 1);
|
editToken(tokens.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Convert a number to a proto3 varint.
|
||||||
|
*/
|
||||||
|
function int2proto3varint(val) {
|
||||||
|
var ret = '';
|
||||||
|
do {
|
||||||
|
let c = val & 0x7F;
|
||||||
|
val >>>= 7;
|
||||||
|
if (val > 0) {
|
||||||
|
c |= 0x80;
|
||||||
|
}
|
||||||
|
ret += String.fromCharCode(c);
|
||||||
|
} while (val > 0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Convert a string to a proto3 field.
|
||||||
|
*/
|
||||||
|
function str2proto3(field_number, str) {
|
||||||
|
return int2proto3varint((field_number << 3) + 2) + int2proto3varint(str.length) + str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Convert a number to a proto3 field.
|
||||||
|
*/
|
||||||
|
function int2proto3(field_number, val) {
|
||||||
|
return int2proto3varint(field_number << 3) + int2proto3varint(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Convert the specified token to its proto3 representation.
|
||||||
|
*/
|
||||||
|
function token2proto3(id) {
|
||||||
|
var secret = str2proto3(1, b32decode(tokens[id].secret));
|
||||||
|
var name = str2proto3(2, (tokens[id].account == '') ? tokens[id].label : tokens[id].account);
|
||||||
|
var issuer = (tokens[id].issuer == '') ? '' : str2proto3(3, tokens[id].issuer);
|
||||||
|
var algorithm = int2proto3(4, (tokens[id].algorithm == 'SHA512') ? 3 : ((tokens[id].algorithm == 'SHA256') ? 2 : 1));
|
||||||
|
var digits = int2proto3(5, (tokens[id].digits == 8) ? 2 : 1);
|
||||||
|
var type = int2proto3(6, (tokens[id].period <= 0) ? 1 : 2);
|
||||||
|
var counter = (tokens[id].period <= 0) ? int2proto3(7, -tokens[id].period) : '';
|
||||||
|
return str2proto3(1, secret + name + issuer + algorithm + digits + type + counter);
|
||||||
|
}
|
||||||
|
|
||||||
/* Move the specified token up or down in the global tokens[].
|
/* Move the specified token up or down in the global tokens[].
|
||||||
* id is the index in the global tokens[] of the token to move.
|
* id is the index in the global tokens[] of the token to move.
|
||||||
* dir is the direction to move: -1=up, 1=down.
|
* dir is the direction to move: -1=up, 1=down.
|
||||||
|
|
@ -200,10 +299,15 @@ function moveToken(id, dir) {
|
||||||
/* Update the display listing all the tokens.
|
/* Update the display listing all the tokens.
|
||||||
*/
|
*/
|
||||||
function updateTokens() {
|
function updateTokens() {
|
||||||
|
const tokenSelect = function(id) {
|
||||||
|
return '<input name="exp_' + id + '" type="checkbox" onclick="exportTokens(false, \'' + id + '\')">';
|
||||||
|
};
|
||||||
const tokenButton = function(fn, id, label, dir) {
|
const tokenButton = function(fn, id, label, dir) {
|
||||||
return '<button type="button" onclick="' + fn + '(' + id + (dir ? ',' + dir : '') + ')">' + label + '</button>';
|
return '<button type="button" onclick="' + fn + '(' + id + (dir ? ',' + dir : '') + ')">' + label + '</button>';
|
||||||
};
|
};
|
||||||
var markup = '<table><tr><th>Token</th><th colspan="2">Order</th></tr>';
|
var markup = '<table><tr><th>';
|
||||||
|
markup += tokenSelect('all');
|
||||||
|
markup += '</th><th>Token</th><th colspan="2">Order</th></tr>';
|
||||||
/* any tokens marked new are cancelled new additions and must be removed */
|
/* any tokens marked new are cancelled new additions and must be removed */
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
if (tokens[i].isnew) {
|
if (tokens[i].isnew) {
|
||||||
|
|
@ -212,6 +316,8 @@ function updateTokens() {
|
||||||
}
|
}
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
markup += '<tr><td>';
|
markup += '<tr><td>';
|
||||||
|
markup += tokenSelect(i);
|
||||||
|
markup += '</td><td>';
|
||||||
markup += tokenButton('editToken', i, tokens[i].label);
|
markup += tokenButton('editToken', i, tokens[i].label);
|
||||||
markup += '</td><td>';
|
markup += '</td><td>';
|
||||||
if (i < (tokens.length - 1)) {
|
if (i < (tokens.length - 1)) {
|
||||||
|
|
@ -224,14 +330,20 @@ function updateTokens() {
|
||||||
markup += '</td></tr>';
|
markup += '</td></tr>';
|
||||||
}
|
}
|
||||||
markup += '</table>';
|
markup += '</table>';
|
||||||
|
markup += '<div class="select">';
|
||||||
markup += '<button type="button" onclick="addToken()">Add Token</button>';
|
markup += '<button type="button" onclick="addToken()">Add Token</button>';
|
||||||
markup += '<button type="button" onclick="saveTokens()">Save to watch</button>';
|
markup += '<button type="button" onclick="saveTokens()">Save to watch</button>';
|
||||||
|
markup += '<button type="button" onclick="startScan(handleImportQr,cancelImportQr)">Import</button>';
|
||||||
|
markup += '<button type="button" onclick="document.body.className=\'export\'">Export</button>';
|
||||||
|
markup += '</div><div class="export">';
|
||||||
|
markup += '<button type="button" onclick="document.body.className=\'select\'">Cancel</button>';
|
||||||
|
markup += '<button type="button" onclick="exportTokens(true, null)">Show QR</button>';
|
||||||
|
markup += '</div>';
|
||||||
document.getElementById('tokens').innerHTML = markup;
|
document.getElementById('tokens').innerHTML = markup;
|
||||||
document.body.className = 'select';
|
document.body.className = 'select';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Original QR-code reader: https://www.sitepoint.com/create-qr-code-reader-mobile-website/ */
|
function handleTokenQr(res) {
|
||||||
qrcode.callback = res => {
|
|
||||||
if (res) {
|
if (res) {
|
||||||
if (res.startsWith(otpAuthUrl)) {
|
if (res.startsWith(otpAuthUrl)) {
|
||||||
res = decodeURIComponent(res);
|
res = decodeURIComponent(res);
|
||||||
|
|
@ -243,7 +355,8 @@ qrcode.callback = res => {
|
||||||
'counter':'0',
|
'counter':'0',
|
||||||
'period':'30',
|
'period':'30',
|
||||||
'secret':'',
|
'secret':'',
|
||||||
'issuer':''
|
'issuer':'',
|
||||||
|
'label':''
|
||||||
};
|
};
|
||||||
var otpok = true;
|
var otpok = true;
|
||||||
for (let pi in params) {
|
for (let pi in params) {
|
||||||
|
|
@ -261,8 +374,7 @@ qrcode.callback = res => {
|
||||||
if (otpok) {
|
if (otpok) {
|
||||||
scanning = false;
|
scanning = false;
|
||||||
editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value));
|
editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value));
|
||||||
t['label'] = (t['issuer'] == '') ? t['account'] : t['issuer'] + ' (' + t['account'] + ')';
|
makeLabel(t);
|
||||||
t['label'] = t['label'].substr(0, 10);
|
|
||||||
var fe = document.forms['edittoken'].elements;
|
var fe = document.forms['edittoken'].elements;
|
||||||
if (res.startsWith(otpAuthUrl + 'hotp/')) {
|
if (res.startsWith(otpAuthUrl + 'hotp/')) {
|
||||||
t['period'] = '30';
|
t['period'] = '30';
|
||||||
|
|
@ -283,8 +395,93 @@ qrcode.callback = res => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
function cancelTokenQr() {
|
||||||
|
editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value));
|
||||||
|
}
|
||||||
|
class proto3decoder {
|
||||||
|
constructor(str) {
|
||||||
|
this.buf = [];
|
||||||
|
for (let i in str) {
|
||||||
|
this.buf = this.buf.concat(str.charCodeAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getVarint() {
|
||||||
|
let c, ret = 0
|
||||||
|
do {
|
||||||
|
c = this.buf.shift();
|
||||||
|
ret = (ret << 7) | (c & 0x7F);
|
||||||
|
} while ((c & 0x80) != 0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
getString(length) {
|
||||||
|
let ret = '';
|
||||||
|
for (let i = 0; i < length; ++i) {
|
||||||
|
ret += String.fromCharCode(this.buf.shift());
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
parse() {
|
||||||
|
let ret = null;
|
||||||
|
if (this.buf.length > 0) {
|
||||||
|
let field_data = null;
|
||||||
|
let field_type = this.getVarint();
|
||||||
|
let field_number = field_type >>> 3;
|
||||||
|
let wire_type = field_type & 7;
|
||||||
|
switch (wire_type) {
|
||||||
|
case 0: field_data = this.getVarint(); break;
|
||||||
|
case 2: field_data = this.getString(this.getVarint()); break;
|
||||||
|
}
|
||||||
|
ret = {number:field_number,data:field_data};
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleImportQr(res) {
|
||||||
|
if (res) {
|
||||||
|
if (res.startsWith(otpMigrUrl)) {
|
||||||
|
scanning = false;
|
||||||
|
let data = new proto3decoder(atob(decodeURIComponent(res.substr(otpMigrUrl.length))));
|
||||||
|
while (data.buf.length > 0) {
|
||||||
|
let field = data.parse();
|
||||||
|
if (field?.number == 1) {
|
||||||
|
let newtoken = {'algorithm':'SHA1','digits':6,'period':30,'issuer':'','account':'','secret':'','label':''};
|
||||||
|
let p3token = new proto3decoder(field.data);
|
||||||
|
while (p3token.buf.length > 0) {
|
||||||
|
let buf = p3token.parse();
|
||||||
|
switch (buf?.number) {
|
||||||
|
case 1: newtoken.secret = b32encode(buf.data); break;
|
||||||
|
case 2: newtoken.account = buf.data; break;
|
||||||
|
case 3: newtoken.issuer = buf.data; break;
|
||||||
|
case 4: newtoken.algorithm = otpAlgos[buf.data - 1]; break;
|
||||||
|
case 5: newtoken.digits = (['6','8'])[buf.data - 1]; break;
|
||||||
|
case 7: newtoken.period = -buf.data; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
makeLabel(newtoken);
|
||||||
|
tokens[tokens.length] = newtoken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateTokens();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function cancelImportQr() {
|
||||||
|
document.body.className = 'select';
|
||||||
|
}
|
||||||
|
/* Original QR-code reader: https://www.sitepoint.com/create-qr-code-reader-mobile-website/ */
|
||||||
|
qrcode.callback = res => {
|
||||||
|
if (res) {
|
||||||
|
scanCallback(res);
|
||||||
|
if (scanning) {
|
||||||
|
scanning = false;
|
||||||
|
scanBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
function startScan() {
|
function startScan(handler,cancel) {
|
||||||
|
scanCallback = handler;
|
||||||
|
scanBack = cancel;
|
||||||
document.body.className = 'scanning';
|
document.body.className = 'scanning';
|
||||||
navigator.mediaDevices
|
navigator.mediaDevices
|
||||||
.getUserMedia({video:{facingMode:'environment'}})
|
.getUserMedia({video:{facingMode:'environment'}})
|
||||||
|
|
@ -339,30 +536,87 @@ function saveTokens() {
|
||||||
Util.hideModal();
|
Util.hideModal();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/* Handle token export.
|
||||||
|
* showqr is true if the QR code should be shown, if false the checkboxes need updating
|
||||||
|
* id is the name of the clicked checkbox, or null if the export button was pressed
|
||||||
|
*/
|
||||||
|
function exportTokens(showqr, id) {
|
||||||
|
let allchecked = true, allclear = true;
|
||||||
|
let cball;
|
||||||
|
let exp = '';
|
||||||
|
for (let cb of document.querySelectorAll('input[type=checkbox]')) {
|
||||||
|
let cbid = cb.name.substring(4);
|
||||||
|
if (cbid == 'all') {
|
||||||
|
cball = cb;
|
||||||
|
} else {
|
||||||
|
if (id == 'all') {
|
||||||
|
cb.checked = cball.checked;
|
||||||
|
} else {
|
||||||
|
if (cb.checked) {
|
||||||
|
if (showqr) {
|
||||||
|
exp += token2proto3(parseInt(cbid));
|
||||||
|
}
|
||||||
|
allclear = false;
|
||||||
|
} else {
|
||||||
|
allchecked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (id != 'all') {
|
||||||
|
if (allclear) {
|
||||||
|
cball.indeterminate = false;
|
||||||
|
cball.checked = false;
|
||||||
|
} else if (allchecked) {
|
||||||
|
cball.indeterminate = false;
|
||||||
|
cball.checked = true;
|
||||||
|
} else {
|
||||||
|
cball.indeterminate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showqr) {
|
||||||
|
if (exp != '') {
|
||||||
|
/* add version, batch_size, batch_index, but no batch_id */
|
||||||
|
exp += int2proto3(2, 1) + int2proto3(3, 1) + int2proto3(4, 0);
|
||||||
|
let url = otpMigrUrl + encodeURIComponent(btoa(exp));
|
||||||
|
showQr(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
function onInit() {
|
function onInit() {
|
||||||
loadTokens();
|
loadTokens();
|
||||||
updateTokens();
|
updateTokens();
|
||||||
}
|
}
|
||||||
|
function qrBack() {
|
||||||
|
document.body.className = qrPreviousClass;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="select">
|
<body class="select">
|
||||||
<h1>Authentiwatch</h1>
|
<h1>Authentiwatch</h1>
|
||||||
|
|
||||||
<div id="tokens">
|
<div id="tokens">
|
||||||
<p>No watch comms.</p>
|
<p>No watch comms.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="scan">
|
<div id="scan">
|
||||||
<table>
|
<table>
|
||||||
<tr><td><canvas id="qr-canvas"></canvas></td></tr>
|
<tr><td><canvas id="qr-canvas"></canvas></td></tr>
|
||||||
<tr><td><button type="button" onclick="editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value))">Cancel</button></td></tr>
|
<tr><td><button type="button" onclick="scanBack()">Cancel</button></td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="edit">
|
<div id="edit">
|
||||||
</div>
|
</div>
|
||||||
<div id="tokenqr">
|
|
||||||
|
<div id="showqr">
|
||||||
<table><tr><td id="qrcode"></td></tr><tr><td>
|
<table><tr><td id="qrcode"></td></tr><tr><td>
|
||||||
<button type="button" onclick="document.body.className='editing'">Back</button>
|
<button type="button" onclick="qrBack()">Back</button>
|
||||||
</td></tr></table>
|
</td></tr></table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const video=document.createElement('video');
|
const video=document.createElement('video');
|
||||||
const canvasElement=document.getElementById('qr-canvas');
|
const canvasElement=document.getElementById('qr-canvas');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue