diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index fb130969f..2734a064a 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -1,3 +1,4 @@ +const COUNTER_TRIANGLE_SIZE = 10; const TOKEN_EXTRA_HEIGHT = 16; var TOKEN_DIGITS_HEIGHT = 30; var TOKEN_HEIGHT = TOKEN_DIGITS_HEIGHT + TOKEN_EXTRA_HEIGHT; @@ -44,8 +45,8 @@ function b32decode(seedstr) { return retbuf; } -function doHmac(key, message, algo) { - var a = algos[algo]; +function hmac(key, message, algo) { + var a = algos[algo.toUpperCase()]; // RFC2104 if (key.length > a.blksz) { key = a.sha(key); @@ -66,50 +67,52 @@ function doHmac(key, message, algo) { } function formatOtp(otp, digits) { + // add 0 padding + var ret = "" + otp % Math.pow(10, digits); + while (ret.length < digits) { + ret = "0" + ret; + } + // add a space after every 3rd or 4th digit var re = (digits % 3 == 0 || (digits % 3 >= digits % 4 && digits % 4 != 0)) ? "" : "."; - return otp.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim(); + return ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim(); } -function hotp(d, token, calcHmac) { - var tick; +function hotp(token) { + var d = Date.now(); + var tick, next; if (token.period > 0) { // RFC6238 - timed - var seconds = Math.floor(d.getTime() / 1000); + var seconds = Math.floor(d / 1000); tick = Math.floor(seconds / token.period); + next = (tick + 1) * token.period * 1000; } else { // RFC4226 - counter tick = -token.period; + next = d + 30000; } var msg = new Uint8Array(8); var v = new DataView(msg.buffer); v.setUint32(0, tick >> 16 >> 16); v.setUint32(4, tick & 0xFFFFFFFF); - var ret = CALCULATING; - if (calcHmac) { - try { - 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) { - ret = NOT_SUPPORTED; - } + var ret; + try { + ret = hmac(b32decode(token.secret), msg, token.algorithm); + ret = formatOtp(ret, token.digits); + } catch(err) { + ret = NOT_SUPPORTED; } - return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)}; + return {hotp:ret, next:next}; } +// Tokens are displayed in three states: +// 1. Unselected (state.id==-1) +// 2. Selected, inactive (no code) (state.id!=-1,state.hotp.hotp=="") +// 3. Selected, active (code showing) (state.id!=-1,state.hotp.hotp!="") var fontszCache = {}; var state = { - listy: 0, - prevcur:0, - curtoken:-1, - nextTime:0, - otp:"", - rem:0, - hide:0 + listy:0, // list scroll position + id:-1, // current token ID + hotp:{hotp:"",next:0} }; function sizeFont(id, txt, w) { @@ -125,117 +128,42 @@ function sizeFont(id, txt, w) { } } -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)); - lbl = tokens[id].label.substr(0, 10); - if (id == state.curtoken) { - // current token - g.setColor(g.theme.fgH) - .setBgColor(g.theme.bgH) - .setFont("Vector", TOKEN_EXTRA_HEIGHT) - // center just below top line - .setFontAlign(0, -1, 0); - adj = y1; - } else { - g.setColor(g.theme.fg) - .setBgColor(g.theme.bg); - sizeFont("l" + id, lbl, r.w); - // center in box - g.setFontAlign(0, 0, 0); - adj = (y1 + y2) / 2; - } - g.clearRect(x1, y1, x2, y2) - .drawString(lbl, (x1 + x2) / 2, adj, false); - if (id == state.curtoken) { - 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]); - adj = 12; - } - // digits just below label - sizeFont("d" + id, state.otp, r.w - adj); - g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + TOKEN_EXTRA_HEIGHT, false); - } - g.setClipRect(0, 0, g.getWidth(), g.getHeight()); -} +tokenY = id => id * TOKEN_HEIGHT + AR.y - state.listy; +half = n => Math.floor(n / 2); -function draw() { - var timerfn = exitApp; - var timerdly = 10000; - var d = new Date(); - if (state.curtoken != -1) { - var t = tokens[state.curtoken]; - if (state.otp == CALCULATING) { - state.otp = hotp(d, t, true).hotp; - } - if (d.getTime() > state.nextTime) { - if (state.hide == 0) { - // auto-hide the current token - if (state.curtoken != -1) { - state.prevcur = state.curtoken; - state.curtoken = -1; +function timerCalc() { + let timerfn = exitApp; + let timerdly = 10000; + let id = state.id; + if (id != -1) { + if (state.hotp.hotp != "") { + if (tokens[id].period > 0) { + // timed HOTP + if (state.hotp.next < Date.now()) { + if (state.cnt > 0) { + --state.cnt; + state.hotp = hotp(tokens[id]); + } else { + state.hotp.hotp = ""; + } + timerdly = 1; + timerfn = updateCurrentToken; + } else { + timerdly = 1000; + timerfn = updateProgressBar; } - state.nextTime = 0; } else { - // time to generate a new token - var r = hotp(d, t, state.otp != ""); - state.nextTime = r.next; - state.otp = r.hotp; - if (t.period <= 0) { - state.hide = 1; + // counter HOTP + if (state.cnt > 0) { + --state.cnt; + timerdly = 30000; + } else { + state.hotp.hotp = ""; + timerdly = 1; } - state.hide--; + timerfn = updateCurrentToken; } } - state.rem = Math.max(0, Math.floor((state.nextTime - d.getTime()) / 1000)); - } - if (tokens.length > 0) { - var drewcur = false; - 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) { - drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:TOKEN_HEIGHT}); - if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) { - drewcur = true; - } - id++; - y += TOKEN_HEIGHT; - } - if (drewcur) { - // the current token has been drawn - schedule a redraw - if (tokens[state.curtoken].period > 0) { - timerdly = (state.otp == CALCULATING) ? 1 : 1000; // timed - } else { - timerdly = state.nexttime - d.getTime(); // counter - } - timerfn = draw; - if (tokens[state.curtoken].period <= 0) { - state.hide = 0; - } - } else { - // de-select the current token if it is scrolled out of view - if (state.curtoken != -1) { - state.prevcur = state.curtoken; - state.curtoken = -1; - } - state.nexttime = 0; - } - } else { - g.setFont("Vector", TOKEN_DIGITS_HEIGHT) - .setFontAlign(0, 0, 0) - .drawString(NO_TOKENS, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false); } if (state.drawtimer) { clearTimeout(state.drawtimer); @@ -243,101 +171,230 @@ function draw() { state.drawtimer = setTimeout(timerfn, timerdly); } -function onTouch(zone, e) { - if (e) { - var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / TOKEN_HEIGHT); - if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) { - id = -1; - } - if (state.curtoken != id) { - if (id != -1) { - var y = id * TOKEN_HEIGHT - state.listy; - if (y < 0) { - state.listy += y; - y = 0; +function updateCurrentToken() { + drawToken(state.id); + timerCalc(); +} + +function updateProgressBar() { + drawProgressBar(); + timerCalc(); +} + +function drawProgressBar() { + let id = state.id; + if (id != -1) { + if (tokens[id].period > 0) { + let rem = Math.floor((state.hotp.next - Date.now()) / 1000); + if (rem >= 0) { + let y1 = tokenY(id); + let y2 = y1 + TOKEN_HEIGHT - 1; + if (y2 >= AR.y && y1 <= AR.y2) { + // token visible + if ((y2 - 3) <= AR.y2) + { + // progress bar visible + y2 = Math.min(y2, AR.y2); + rem = Math.min(rem, tokens[id].period); + let xr = Math.floor(AR.w * rem / tokens[id].period) + AR.x; + g.setColor(g.theme.fgH) + .setBgColor(g.theme.bgH) + .fillRect(AR.x, y2 - 3, xr, y2) + .clearRect(xr + 1, y2 - 3, AR.x2, y2); + } + } else { + // token not visible + state.id = -1; } - y += TOKEN_HEIGHT; - if (y > Bangle.appRect.h) { - state.listy += (y - Bangle.appRect.h); - } - state.otp = ""; } - state.nextTime = 0; - state.curtoken = id; - state.hide = 2; } } - draw(); +} + +// id = token ID number (0...) +function drawToken(id) { + var x1 = AR.x; + var y1 = tokenY(id); + var x2 = AR.x2; + var y2 = y1 + TOKEN_HEIGHT - 1; + var adj, lbl; + g.setClipRect(x1, Math.max(y1, AR.y), x2, Math.min(y2, AR.y2)); + lbl = tokens[id].label.substr(0, 10); + if (id === state.id) { + g.setColor(g.theme.fgH) + .setBgColor(g.theme.bgH); + } else { + g.setColor(g.theme.fg) + .setBgColor(g.theme.bg); + } + if (id == state.id && state.hotp.hotp != "") { + // small label centered just below top line + g.setFont("Vector", TOKEN_EXTRA_HEIGHT) + .setFontAlign(0, -1, 0); + adj = y1; + } else { + // large label centered in box + sizeFont("l" + id, lbl, AR.w); + g.setFontAlign(0, 0, 0); + adj = half(y1 + y2); + } + g.clearRect(x1, y1, x2, y2) + .drawString(lbl, half(x1 + x2), adj, false); + if (id == state.id && state.hotp.hotp != "") { + adj = 0; + if (tokens[id].period <= 0) { + // counter - draw triangle as swipe hint + let yc = half(y1 + y2); + adj = COUNTER_TRIANGLE_SIZE; + g.fillPoly([AR.x, yc, AR.x + adj, yc - adj, AR.x + adj, yc + adj]); + adj += 2; + } + // digits just below label + x1 = half(x1 + adj + x2); + y1 += TOKEN_EXTRA_HEIGHT; + if (state.hotp.hotp == CALCULATING) { + sizeFont("c", CALCULATING, AR.w - adj); + g.drawString(CALCULATING, x1, y1, false) + .flip(); + state.hotp = hotp(tokens[id]); + g.clearRect(AR.x + adj, y1, AR.x2, y2); + } + sizeFont("d" + id, state.hotp.hotp, AR.w - adj); + g.drawString(state.hotp.hotp, x1, y1, false); + if (tokens[id].period > 0) { + drawProgressBar(); + } + } + g.setClipRect(0, 0, g.getWidth(), g.getHeight()); +} + +function startupDraw() { + if (tokens.length > 0) { + let id = 0; + let y = tokenY(id); + while (id < tokens.length && y < AR.y2) { + if ((y + TOKEN_HEIGHT) > AR.y) { + drawToken(id); + } + id++; + y += TOKEN_HEIGHT; + } + } else { + let x = AR.x + half(AR.w); + let y = AR.y + half(AR.h); + g.setFont("Vector", TOKEN_DIGITS_HEIGHT) + .setFontAlign(0, 0, 0) + .drawString(NO_TOKENS, x, y, false); + } + timerCalc(); } function onDrag(e) { - if (e.b != 0 && e.x < g.getWidth() && e.y < g.getHeight() && e.dy != 0) { - var y = Math.max(0, Math.min(state.listy - e.dy, tokens.length * TOKEN_HEIGHT - Bangle.appRect.h)); + state.cnt = 1; + if (e.b != 0 && e.dy != 0) { + var y = E.clip(state.listy - e.dy, 0, tokens.length * TOKEN_HEIGHT - AR.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) + g.setClipRect(AR.x, AR.y, AR.x2, AR.y2) .scroll(0, dy); if (dy > 0) { id = Math.floor((state.listy + dy) / TOKEN_HEIGHT); - y = id * TOKEN_HEIGHT + Bangle.appRect.y - state.listy; + y = tokenY(id + 1); do { - drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:TOKEN_HEIGHT}); + drawToken(id); id--; y -= TOKEN_HEIGHT; - } while (y > 0); + } while (y > AR.y); } if (dy < 0) { - 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) { - drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:TOKEN_HEIGHT}); + id = Math.floor((state.listy + dy + AR.h) / TOKEN_HEIGHT); + y = tokenY(id); + while (y < AR.y2) { + drawToken(id); id++; y += TOKEN_HEIGHT; } } } } + if (e.b == 0) { + timerCalc(); + } +} + +function changeId(id) { + if (id != state.id) { + state.hotp.hotp = CALCULATING; + let pid = state.id; + state.id = id; + if (pid != -1) { + drawToken(pid); + } + if (id != -1) { + drawToken( id); + } + timerCalc(); + } +} + +function onTouch(zone, e) { + state.cnt = 1; + if (e) { + var id = Math.floor((state.listy + e.y - AR.y) / TOKEN_HEIGHT); + if (id == state.id || tokens.length == 0 || id >= tokens.length) { + id = -1; + } + if (state.id != id) { + if (id != -1) { + // scroll token into view if necessary + var fakee = {b:1,x:0,y:0,dx:0,dy:0}; + var y = id * TOKEN_HEIGHT - state.listy; + if (y < 0) { + fakee.dy -= y; + y = 0; + } + y += TOKEN_HEIGHT; + if (y > AR.h) { + fakee.dy -= (y - AR.h); + } + onDrag(fakee); + } + changeId(id); + } + } } function onSwipe(e) { + state.cnt = 1; + let id = state.id; if (e == 1) { exitApp(); } - if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) { - tokens[state.curtoken].period--; + if (e == -1 && id != -1 && tokens[id].period <= 0) { + tokens[id].period--; let newsettings={tokens:tokens,misc:settings.misc}; require("Storage").writeJSON("authentiwatch.json", newsettings); - state.nextTime = 0; - state.otp = ""; - state.hide = 2; + state.hotp.hotp = CALCULATING; + drawToken(id); } - draw(); } function bangle1Btn(e) { + state.cnt = 1; 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; - } + var id = state.id; + switch (e) { + case -1: id--; break; + case 1: id++; break; } - state.curtoken = Math.max(state.curtoken, 0); - state.curtoken = Math.min(state.curtoken, tokens.length - 1); - 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); - var fakee = {}; - fakee.y = state.curtoken * TOKEN_HEIGHT - state.listy + Bangle.appRect.y; - state.curtoken = -1; - state.nextTime = 0; - onTouch(0, fakee); + id = E.clip(id, 0, tokens.length - 1); + var fakee = {b:1,x:0,y:0,dx:0}; + fakee.dy = state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, tokens.length * TOKEN_HEIGHT - AR.h); + onDrag(fakee); + changeId(id); } else { - draw(); // resets idle timer + timerCalc(); } } @@ -356,8 +413,7 @@ if (typeof BTN2 == 'number') { setWatch(function(){exitApp(); }, BTN1, {edge:"falling", debounce:50}); } Bangle.loadWidgets(); - -// Clear the screen once, at startup -g.clear(); -draw(); +const AR = Bangle.appRect; +g.clear(); // Clear the screen once, at startup +startupDraw(); Bangle.drawWidgets();