Merge pull request #1396 from andrewgoz/master

Authentiwatch version 0.05
pull/1052/head^2
Gordon Williams 2022-02-07 10:20:51 +00:00 committed by GitHub
commit 413142bc36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 315 additions and 43 deletions

View File

@ -2,3 +2,4 @@
0.02: Fix JSON save format 0.02: Fix JSON save format
0.03: Add "Calculating" placeholder, update JSON save format 0.03: Add "Calculating" placeholder, update JSON save format
0.04: Fix tapping at very bottom of list, exit on inactivity 0.04: Fix tapping at very bottom of list, exit on inactivity
0.05: Add support for bulk importing and exporting tokens

View File

@ -3,6 +3,15 @@
* GitHub: https://github.com/andrewgoz/Authentiwatch <-- Report bugs here * GitHub: https://github.com/andrewgoz/Authentiwatch <-- Report bugs here
* Bleeding edge AppLoader: https://andrewgoz.github.io/Authentiwatch/ * Bleeding edge AppLoader: https://andrewgoz.github.io/Authentiwatch/
## Important!
Tokens are stored *ONLY* on the watch. Make sure you do one or more of the following:
* Make a backup copy of the "authentiwatch.json" file.
* Export all your tokens to another device or print the QR code.
Keep those copies safe and secure.
## Supports ## Supports
* Google Authenticator compatible 2-factor authentication * Google Authenticator compatible 2-factor authentication
@ -14,8 +23,8 @@
* Between 6 and 10 digits * Between 6 and 10 digits
* Phone/PC configuration web page: * Phone/PC configuration web page:
* Add/edit/delete/arrange tokens * Add/edit/delete/arrange tokens
* Scan QR codes * Scan token and migration(import) QR codes
* Produce scannable QR codes * Produce scannable token and migration(export) QR codes
## Usage ## Usage
@ -24,6 +33,8 @@
* Swipe right to exit to the app launcher. * Swipe right to exit to the app launcher.
* Swipe left on selected counter token to advance the counter to the next value. * Swipe left on selected counter token to advance the counter to the next value.
![Screenshot](screenshot.png)
## Creator ## Creator
Andrew Gregory (andrew.gregory at gmail) Andrew Gregory (andrew.gregory at gmail)

View File

@ -1,4 +1,5 @@
const tokenentryheight = 46; const tokenextraheight = 16;
var tokendigitsheight = 30;
// Hash functions // Hash functions
const crypto = require("crypto"); const crypto = require("crypto");
const algos = { const algos = {
@ -44,9 +45,6 @@ function b32decode(seedstr) {
} }
} }
} }
if (bitcount > 0) {
retstr += String.fromCharCode(buf << (8 - bitcount));
}
var retbuf = new Uint8Array(retstr.length); var retbuf = new Uint8Array(retstr.length);
for (i in retstr) { for (i in retstr) {
retbuf[i] = retstr.charCodeAt(i); retbuf[i] = retstr.charCodeAt(i);
@ -117,27 +115,31 @@ function drawToken(id, r) {
var y1 = r.y; var y1 = r.y;
var x2 = r.x + r.w - 1; var x2 = r.x + r.w - 1;
var y2 = r.y + r.h - 1; var y2 = r.y + r.h - 1;
var adj, sz; var adj, lbl, sz;
g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ), g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ),
Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2)); Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2));
lbl = tokens[id].label.substr(0, 10);
if (id == state.curtoken) { if (id == state.curtoken) {
// current token // current token
g.setColor(g.theme.fgH); g.setColor(g.theme.fgH);
g.setBgColor(g.theme.bgH); g.setBgColor(g.theme.bgH);
g.setFont("Vector", 16); g.setFont("Vector", tokenextraheight);
// center just below top line // center just below top line
g.setFontAlign(0, -1, 0); g.setFontAlign(0, -1, 0);
adj = y1; adj = y1;
} else { } else {
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
g.setBgColor(g.theme.bg); g.setBgColor(g.theme.bg);
g.setFont("Vector", 30); sz = tokendigitsheight;
do {
g.setFont("Vector", sz--);
} while (g.stringWidth(lbl) > r.w);
// center in box // center in box
g.setFontAlign(0, 0, 0); g.setFontAlign(0, 0, 0);
adj = (y1 + y2) / 2; adj = (y1 + y2) / 2;
} }
g.clearRect(x1, y1, x2, y2); g.clearRect(x1, y1, x2, y2);
g.drawString(tokens[id].label.substr(0, 10), (x1 + x2) / 2, adj, false); g.drawString(lbl, (x1 + x2) / 2, adj, false);
if (id == state.curtoken) { if (id == state.curtoken) {
if (tokens[id].period > 0) { if (tokens[id].period > 0) {
// timed - draw progress bar // timed - draw progress bar
@ -148,14 +150,14 @@ function drawToken(id, r) {
// counter - draw triangle as swipe hint // counter - draw triangle as swipe hint
let yc = (y1 + y2) / 2; let yc = (y1 + y2) / 2;
g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]); g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]);
adj = 10; adj = 12;
} }
// digits just below label // digits just below label
sz = 30; sz = tokendigitsheight;
do { do {
g.setFont("Vector", sz--); g.setFont("Vector", sz--);
} while (g.stringWidth(state.otp) > (r.w - adj)); } while (g.stringWidth(state.otp) > (r.w - adj));
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + 16, false); g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false);
} }
// shaded lines top and bottom // shaded lines top and bottom
g.setColor(0.5, 0.5, 0.5); g.setColor(0.5, 0.5, 0.5);
@ -196,15 +198,15 @@ function draw() {
} }
if (tokens.length > 0) { if (tokens.length > 0) {
var drewcur = false; var drewcur = false;
var id = Math.floor(state.listy / tokenentryheight); var id = Math.floor(state.listy / (tokendigitsheight + tokenextraheight));
var y = id * tokenentryheight + Bangle.appRect.y - state.listy; var y = id * (tokendigitsheight + tokenextraheight) + Bangle.appRect.y - state.listy;
while (id < tokens.length && y < Bangle.appRect.y2) { while (id < tokens.length && y < Bangle.appRect.y2) {
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenentryheight}); drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:(tokendigitsheight + tokenextraheight)});
if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) { if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
drewcur = true; drewcur = true;
} }
id += 1; id += 1;
y += tokenentryheight; y += (tokendigitsheight + tokenextraheight);
} }
if (drewcur) { if (drewcur) {
// the current token has been drawn - schedule a redraw // the current token has been drawn - schedule a redraw
@ -226,7 +228,7 @@ function draw() {
state.nexttime = 0; state.nexttime = 0;
} }
} else { } else {
g.setFont("Vector", 30); g.setFont("Vector", tokendigitsheight);
g.setFontAlign(0, 0, 0); g.setFontAlign(0, 0, 0);
g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false); g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
} }
@ -238,18 +240,18 @@ function draw() {
function onTouch(zone, e) { function onTouch(zone, e) {
if (e) { if (e) {
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenentryheight); var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / (tokendigitsheight + tokenextraheight));
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) { if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
id = -1; id = -1;
} }
if (state.curtoken != id) { if (state.curtoken != id) {
if (id != -1) { if (id != -1) {
var y = id * tokenentryheight - state.listy; var y = id * (tokendigitsheight + tokenextraheight) - state.listy;
if (y < 0) { if (y < 0) {
state.listy += y; state.listy += y;
y = 0; y = 0;
} }
y += tokenentryheight; y += (tokendigitsheight + tokenextraheight);
if (y > Bangle.appRect.h) { if (y > Bangle.appRect.h) {
state.listy += (y - Bangle.appRect.h); state.listy += (y - Bangle.appRect.h);
} }
@ -266,12 +268,15 @@ function onTouch(zone, e) {
function onDrag(e) { function onDrag(e) {
if (e.x > g.getWidth() || e.y > g.getHeight()) return; if (e.x > g.getWidth() || e.y > g.getHeight()) return;
if (e.dx == 0 && e.dy == 0) return; if (e.dx == 0 && e.dy == 0) return;
var newy = Math.min(state.listy - e.dy, tokens.length * tokenentryheight - Bangle.appRect.h); var newy = Math.min(state.listy - e.dy, tokens.length * (tokendigitsheight + tokenextraheight) - Bangle.appRect.h);
state.listy = Math.max(0, newy); state.listy = Math.max(0, newy);
draw(); draw();
} }
function onSwipe(e) { function onSwipe(e) {
if (e == 1) {
exitApp();
}
if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) { if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) {
tokens[state.curtoken].period--; tokens[state.curtoken].period--;
let newsettings={tokens:tokens,misc:settings.misc}; let newsettings={tokens:tokens,misc:settings.misc};
@ -296,7 +301,7 @@ function bangle1Btn(e) {
state.curtoken = Math.max(state.curtoken, 0); state.curtoken = Math.max(state.curtoken, 0);
state.curtoken = Math.min(state.curtoken, tokens.length - 1); state.curtoken = Math.min(state.curtoken, tokens.length - 1);
var fakee = {}; var fakee = {};
fakee.y = state.curtoken * tokenentryheight - state.listy + Bangle.appRect.y; fakee.y = state.curtoken * (tokendigitsheight + tokenextraheight) - state.listy + Bangle.appRect.y;
state.curtoken = -1; state.curtoken = -1;
state.nextTime = 0; state.nextTime = 0;
onTouch(0, fakee); onTouch(0, fakee);

View File

@ -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 = ret.replaceAll(/0/g, 'O');
ret = ret.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,94 @@ qrcode.callback = res => {
} }
} }
} }
}
function cancelTokenQr() {
scanning = false;
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() {
scanning = false;
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) {
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,36 +537,93 @@ 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');
const canvas=canvasElement.getContext('2d'); const canvas=canvasElement.getContext('2d');
let scanning=false; let scanning=false;
const tokenqr=new QRCode(document.getElementById('qrcode'), ''); const tokenqr=new QRCode(document.getElementById('qrcode'), {width:354,height:354});
</script> </script>
<script src="../../core/lib/interface.js"></script> <script src="../../core/lib/interface.js"></script>
</body> </body>

View File

@ -4,7 +4,7 @@
"shortName": "AuthWatch", "shortName": "AuthWatch",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot.png"}], "screenshots": [{"url":"screenshot.png"}],
"version": "0.04", "version": "0.05",
"description": "Google Authenticator compatible tool.", "description": "Google Authenticator compatible tool.",
"tags": "tool", "tags": "tool",
"interface": "interface.html", "interface": "interface.html",