BangleApps/apps/authentiwatch/app.js

364 lines
11 KiB
JavaScript
Raw Normal View History

2022-03-09 13:53:24 +00:00
const TOKEN_EXTRA_HEIGHT = 16;
var TOKEN_DIGITS_HEIGHT = 30;
var TOKEN_HEIGHT = TOKEN_DIGITS_HEIGHT + TOKEN_EXTRA_HEIGHT;
2021-10-29 13:37:38 +00:00
// Hash functions
const crypto = require("crypto");
2021-11-09 15:24:05 +00:00
const algos = {
"SHA512":{sha:crypto.SHA512,retsz:64,blksz:128},
"SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 },
"SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 },
};
2022-03-09 13:53:24 +00:00
const CALCULATING = /*LANG*/"Calculating";
const NO_TOKENS = /*LANG*/"No tokens";
const NOT_SUPPORTED = /*LANG*/"Not supported";
2021-10-29 13:37:38 +00:00
2021-12-03 04:24:46 +00:00
// sample settings:
// {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}}
2021-12-01 14:05:53 +00:00
var settings = require("Storage").readJSON("authentiwatch.json", true) || {tokens:[],misc:{}};
if (settings.data ) tokens = settings.data ; /* v0.02 settings */
if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */
2021-10-29 13:37:38 +00:00
function b32decode(seedstr) {
// RFC4648
var buf = 0, bitcount = 0, retstr = "";
for (var c of seedstr.toUpperCase()) {
if (c == '0') c = 'O';
if (c == '1') c = 'I';
if (c == '8') c = 'B';
c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(c);
2021-10-29 13:37:38 +00:00
if (c != -1) {
buf <<= 5;
buf |= c;
bitcount += 5;
if (bitcount >= 8) {
retstr += String.fromCharCode(buf >> (bitcount - 8));
buf &= (0xFF >> (16 - bitcount));
bitcount -= 8;
}
}
}
var retbuf = new Uint8Array(retstr.length);
for (var i in retstr) {
2021-10-29 13:37:38 +00:00
retbuf[i] = retstr.charCodeAt(i);
}
return retbuf;
}
2022-03-09 13:53:24 +00:00
function doHmac(key, message, algo) {
2021-11-09 15:24:05 +00:00
var a = algos[algo];
2021-10-29 13:37:38 +00:00
// RFC2104
2021-11-09 15:24:05 +00:00
if (key.length > a.blksz) {
key = a.sha(key);
2021-10-29 13:37:38 +00:00
}
2021-11-09 15:24:05 +00:00
var istr = new Uint8Array(a.blksz + message.length);
var ostr = new Uint8Array(a.blksz + a.retsz);
for (var i = 0; i < a.blksz; ++i) {
var c = (i < key.length) ? key[i] : 0;
2021-10-29 13:37:38 +00:00
istr[i] = c ^ 0x36;
ostr[i] = c ^ 0x5C;
}
2021-11-09 15:24:05 +00:00
istr.set(message, a.blksz);
ostr.set(a.sha(istr), a.blksz);
var ret = a.sha(ostr);
2021-10-29 13:37:38 +00:00
// RFC4226 dynamic truncation
var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
return v.getUint32(0) & 0x7FFFFFFF;
}
2022-03-09 13:53:24 +00:00
function formatOtp(otp, digits) {
var re = (digits % 3 == 0 || (digits % 3 >= digits % 4 && digits % 4 != 0)) ? "" : ".";
return otp.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
}
2022-03-09 13:53:24 +00:00
function hotp(d, token, calcHmac) {
2021-11-12 16:58:50 +00:00
var tick;
if (token.period > 0) {
// RFC6238 - timed
var seconds = Math.floor(d.getTime() / 1000);
tick = Math.floor(seconds / token.period);
} else {
// RFC4226 - counter
tick = -token.period;
}
2021-10-29 13:37:38 +00:00
var msg = new Uint8Array(8);
var v = new DataView(msg.buffer);
v.setUint32(0, tick >> 16 >> 16);
v.setUint32(4, tick & 0xFFFFFFFF);
2022-03-09 13:53:24 +00:00
var ret = CALCULATING;
if (calcHmac) {
try {
2022-03-09 13:53:24 +00:00
var hash = doHmac(b32decode(token.secret), msg, token.algorithm.toUpperCase());
ret = "" + hash % Math.pow(10, token.digits);
while (ret.length < token.digits) {
ret = "0" + ret;
}
// add a space after every 3rd or 4th digit
ret = formatOtp(ret, token.digits);
} catch(err) {
2022-03-09 13:53:24 +00:00
ret = NOT_SUPPORTED;
2021-11-13 09:44:32 +00:00
}
2021-10-29 13:37:38 +00:00
}
2021-11-12 16:58:50 +00:00
return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)};
2021-10-29 13:37:38 +00:00
}
2022-03-09 13:53:24 +00:00
var fontszCache = {};
2021-10-29 13:37:38 +00:00
var state = {
listy: 0,
2021-11-13 09:44:32 +00:00
prevcur:0,
2021-10-29 13:37:38 +00:00
curtoken:-1,
nextTime:0,
otp:"",
2021-11-12 16:58:50 +00:00
rem:0,
hide:0
2021-10-29 13:37:38 +00:00
};
2022-03-09 13:53:24 +00:00
function sizeFont(id, txt, w) {
var sz = fontszCache[id];
if (sz) {
g.setFont("Vector", sz);
} else {
2022-03-09 13:53:24 +00:00
sz = TOKEN_DIGITS_HEIGHT;
do {
g.setFont("Vector", sz--);
} while (g.stringWidth(txt) > w);
2022-03-09 13:53:24 +00:00
fontszCache[id] = sz + 1;
}
}
2021-10-29 13:37:38 +00:00
function drawToken(id, r) {
var x1 = r.x;
var y1 = r.y;
var x2 = r.x + r.w - 1;
var y2 = r.y + r.h - 1;
var adj, lbl;
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));
2021-12-05 11:42:44 +00:00
lbl = tokens[id].label.substr(0, 10);
2021-10-29 13:37:38 +00:00
if (id == state.curtoken) {
// current token
g.setColor(g.theme.fgH)
.setBgColor(g.theme.bgH)
2022-03-09 13:53:24 +00:00
.setFont("Vector", TOKEN_EXTRA_HEIGHT)
2021-10-29 13:37:38 +00:00
// center just below top line
.setFontAlign(0, -1, 0);
2021-11-13 04:04:30 +00:00
adj = y1;
2021-10-29 13:37:38 +00:00
} else {
g.setColor(g.theme.fg)
.setBgColor(g.theme.bg);
2022-03-09 13:53:24 +00:00
sizeFont("l" + id, lbl, r.w);
2021-10-29 13:37:38 +00:00
// center in box
g.setFontAlign(0, 0, 0);
2021-11-13 04:04:30 +00:00
adj = (y1 + y2) / 2;
2021-10-29 13:37:38 +00:00
}
g.clearRect(x1, y1, x2, y2)
.drawString(lbl, (x1 + x2) / 2, adj, false);
2021-10-29 13:37:38 +00:00
if (id == state.curtoken) {
2021-11-13 04:04:30 +00:00
if (tokens[id].period > 0) {
// timed - draw progress bar
let xr = Math.floor(Bangle.appRect.w * state.rem / tokens[id].period);
g.fillRect(x1, y2 - 4, xr, y2 - 1);
adj = 0;
} else {
// counter - draw triangle as swipe hint
let yc = (y1 + y2) / 2;
g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]);
2021-12-05 11:27:50 +00:00
adj = 12;
2021-11-13 04:04:30 +00:00
}
2021-10-29 13:37:38 +00:00
// digits just below label
2022-03-09 13:53:24 +00:00
sizeFont("d" + id, state.otp, r.w - adj);
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + TOKEN_EXTRA_HEIGHT, false);
2021-10-29 13:37:38 +00:00
}
g.setClipRect(0, 0, g.getWidth(), g.getHeight());
2021-10-29 13:37:38 +00:00
}
function draw() {
2021-12-03 14:39:39 +00:00
var timerfn = exitApp;
2021-12-03 04:02:31 +00:00
var timerdly = 10000;
2021-11-12 16:58:50 +00:00
var d = new Date();
2021-10-29 13:37:38 +00:00
if (state.curtoken != -1) {
var t = tokens[state.curtoken];
2022-03-09 13:53:24 +00:00
if (state.otp == CALCULATING) {
state.otp = hotp(d, t, true).hotp;
}
2021-10-29 13:37:38 +00:00
if (d.getTime() > state.nextTime) {
2021-11-12 16:58:50 +00:00
if (state.hide == 0) {
// auto-hide the current token
2021-11-13 09:44:32 +00:00
if (state.curtoken != -1) {
state.prevcur = state.curtoken;
state.curtoken = -1;
}
2021-10-29 13:37:38 +00:00
state.nextTime = 0;
2021-11-12 16:58:50 +00:00
} else {
// time to generate a new token
var r = hotp(d, t, state.otp != "");
2021-11-13 09:44:32 +00:00
state.nextTime = r.next;
state.otp = r.hotp;
if (t.period <= 0) {
state.hide = 1;
2021-11-12 16:58:50 +00:00
}
state.hide--;
2021-10-29 13:37:38 +00:00
}
}
state.rem = Math.max(0, Math.floor((state.nextTime - d.getTime()) / 1000));
}
if (tokens.length > 0) {
var drewcur = false;
2022-03-09 13:53:24 +00:00
var id = Math.floor(state.listy / TOKEN_HEIGHT);
var y = id * TOKEN_HEIGHT + Bangle.appRect.y - state.listy;
while (id < tokens.length && y < Bangle.appRect.y2) {
2022-03-09 13:53:24 +00:00
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:TOKEN_HEIGHT});
2021-11-12 16:58:50 +00:00
if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
2021-10-29 13:37:38 +00:00
drewcur = true;
}
id++;
2022-03-09 13:53:24 +00:00
y += TOKEN_HEIGHT;
2021-10-29 13:37:38 +00:00
}
if (drewcur) {
2021-12-02 15:55:46 +00:00
// the current token has been drawn - schedule a redraw
if (tokens[state.curtoken].period > 0) {
2022-03-09 13:53:24 +00:00
timerdly = (state.otp == CALCULATING) ? 1 : 1000; // timed
} else {
2021-12-03 04:02:31 +00:00
timerdly = state.nexttime - d.getTime(); // counter
}
2021-12-03 04:02:31 +00:00
timerfn = draw;
2021-11-12 16:58:50 +00:00
if (tokens[state.curtoken].period <= 0) {
state.hide = 0;
}
} else {
// de-select the current token if it is scrolled out of view
2021-11-13 09:44:32 +00:00
if (state.curtoken != -1) {
state.prevcur = state.curtoken;
state.curtoken = -1;
}
2021-11-12 16:58:50 +00:00
state.nexttime = 0;
2021-10-29 13:37:38 +00:00
}
} else {
2022-03-09 13:53:24 +00:00
g.setFont("Vector", TOKEN_DIGITS_HEIGHT)
.setFontAlign(0, 0, 0)
2022-03-09 13:53:24 +00:00
.drawString(NO_TOKENS, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
2021-10-29 13:37:38 +00:00
}
2021-12-03 04:02:31 +00:00
if (state.drawtimer) {
clearTimeout(state.drawtimer);
}
state.drawtimer = setTimeout(timerfn, timerdly);
2021-10-29 13:37:38 +00:00
}
function onTouch(zone, e) {
2021-11-13 09:44:32 +00:00
if (e) {
2022-03-09 13:53:24 +00:00
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / TOKEN_HEIGHT);
2021-12-02 13:54:53 +00:00
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
2021-11-13 09:44:32 +00:00
id = -1;
}
if (state.curtoken != id) {
if (id != -1) {
2022-03-09 13:53:24 +00:00
var y = id * TOKEN_HEIGHT - state.listy;
2021-11-13 09:44:32 +00:00
if (y < 0) {
state.listy += y;
y = 0;
}
2022-03-09 13:53:24 +00:00
y += TOKEN_HEIGHT;
2021-11-13 09:44:32 +00:00
if (y > Bangle.appRect.h) {
state.listy += (y - Bangle.appRect.h);
}
state.otp = "";
2021-10-29 13:37:38 +00:00
}
2021-11-13 09:44:32 +00:00
state.nextTime = 0;
state.curtoken = id;
state.hide = 2;
2021-10-29 13:37:38 +00:00
}
}
2021-12-03 04:02:31 +00:00
draw();
2021-10-29 13:37:38 +00:00
}
function onDrag(e) {
if (e.b != 0 && e.x < g.getWidth() && e.y < g.getHeight() && e.dy != 0) {
2022-03-09 13:53:24 +00:00
var y = Math.max(0, Math.min(state.listy - e.dy, tokens.length * TOKEN_HEIGHT - Bangle.appRect.h));
if (state.listy != y) {
var id, dy = state.listy - y;
state.listy = y;
g.setClipRect(Bangle.appRect.x,Bangle.appRect.y,Bangle.appRect.x2,Bangle.appRect.y2)
.scroll(0, dy);
if (dy > 0) {
2022-03-09 13:53:24 +00:00
id = Math.floor((state.listy + dy) / TOKEN_HEIGHT);
y = id * TOKEN_HEIGHT + Bangle.appRect.y - state.listy;
do {
2022-03-09 13:53:24 +00:00
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:TOKEN_HEIGHT});
id--;
2022-03-09 13:53:24 +00:00
y -= TOKEN_HEIGHT;
} while (y > 0);
}
if (dy < 0) {
2022-03-09 13:53:24 +00:00
id = Math.floor((state.listy + dy + Bangle.appRect.h) / TOKEN_HEIGHT);
y = id * TOKEN_HEIGHT + Bangle.appRect.y - state.listy;
while (y < Bangle.appRect.y2) {
2022-03-09 13:53:24 +00:00
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:TOKEN_HEIGHT});
id++;
2022-03-09 13:53:24 +00:00
y += TOKEN_HEIGHT;
}
}
}
}
2021-10-29 13:37:38 +00:00
}
function onSwipe(e) {
2021-12-31 03:39:22 +00:00
if (e == 1) {
2021-12-31 07:56:39 +00:00
exitApp();
2021-12-31 03:39:22 +00:00
}
2021-11-12 16:58:50 +00:00
if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) {
tokens[state.curtoken].period--;
2021-12-01 14:05:53 +00:00
let newsettings={tokens:tokens,misc:settings.misc};
require("Storage").writeJSON("authentiwatch.json", newsettings);
2021-11-12 16:58:50 +00:00
state.nextTime = 0;
state.otp = "";
2021-11-12 16:58:50 +00:00
state.hide = 2;
}
2021-12-03 04:02:31 +00:00
draw();
2021-10-29 13:37:38 +00:00
}
2021-11-13 09:44:32 +00:00
function bangle1Btn(e) {
if (tokens.length > 0) {
if (state.curtoken == -1) {
state.curtoken = state.prevcur;
} else {
switch (e) {
case -1: state.curtoken--; break;
case 1: state.curtoken++; break;
}
}
state.curtoken = Math.max(state.curtoken, 0);
state.curtoken = Math.min(state.curtoken, tokens.length - 1);
2022-03-09 13:53:24 +00:00
state.listy = state.curtoken * TOKEN_HEIGHT;
state.listy -= (Bangle.appRect.h - TOKEN_HEIGHT) / 2;
state.listy = Math.min(state.listy, tokens.length * TOKEN_HEIGHT - Bangle.appRect.h);
state.listy = Math.max(state.listy, 0);
2021-11-13 09:44:32 +00:00
var fakee = {};
2022-03-09 13:53:24 +00:00
fakee.y = state.curtoken * TOKEN_HEIGHT - state.listy + Bangle.appRect.y;
2021-11-13 09:44:32 +00:00
state.curtoken = -1;
2021-11-13 13:50:44 +00:00
state.nextTime = 0;
2021-11-13 09:44:32 +00:00
onTouch(0, fakee);
2021-12-03 04:02:31 +00:00
} else {
draw(); // resets idle timer
2021-11-13 09:44:32 +00:00
}
}
2021-12-03 14:39:39 +00:00
function exitApp() {
Bangle.showLauncher();
}
2021-10-29 13:37:38 +00:00
Bangle.on('touch', onTouch);
Bangle.on('drag' , onDrag );
Bangle.on('swipe', onSwipe);
2021-11-13 09:44:32 +00:00
if (typeof BTN2 == 'number') {
setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising" , debounce:50, repeat:true});
setWatch(function(){exitApp(); }, BTN2, {edge:"falling", debounce:50});
setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising" , debounce:50, repeat:true});
} else {
setWatch(function(){exitApp(); }, BTN1, {edge:"falling", debounce:50});
2021-11-13 09:44:32 +00:00
}
Bangle.loadWidgets();
2021-10-29 13:37:38 +00:00
// Clear the screen once, at startup
g.clear();
draw();
Bangle.drawWidgets();