mirror of https://github.com/espruino/BangleApps
parent
2fc0a20311
commit
1523a15b8e
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue