BangleApps/apps/authentiwatch/interface.html

313 lines
10 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
2021-10-29 14:36:30 +00:00
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0"/>
2021-11-09 15:24:47 +00:00
<link rel="stylesheet" href="../../css/spectre.min.css">
<style type="text/css">
body{font-family:sans-serif}
body div{display:none}
body.select div#tokens,body.editing div#edit,body.scanning div#scan,body.showqr div#tokenqr{display:block}
#tokens th,#tokens td{padding:5px}
#tokens tr:nth-child(odd){background-color:#ccc}
#tokens tr:nth-child(even){background-color:#eee}
#qr-canvas{margin:auto;width:calc(100%-20px);max-width:400px}
#advbtn,#scan,#tokenqr table{text-align:center}
#edittoken tbody#adv{display:none}
#edittoken.showadv tbody#adv{display:table-row-group}
#advbtn button:before,#advbtn button:after{content:"\25bc"}
#edittoken.showadv #advbtn button:before,#edittoken.showadv #advbtn button:after{content:"\25b2"}
button{height:3em}
2021-11-09 15:24:47 +00:00
table button{width:100%}
</style>
2021-11-09 15:24:47 +00:00
<!-- https://rawgit.com/sitepoint-editors/jsqrcode/master/src/qr_packed.js -->
<script src="qr_packed.js"></script>
2021-11-09 15:24:47 +00:00
<!-- https://davidshimjs.github.io/qrcodejs/ -->
<script src="../../core/lib/qrcode.min.js"></script>
2021-11-09 15:24:47 +00:00
<script type="text/javascript">
2021-11-09 15:24:47 +00:00
/* Start of all TOTP URLs */
const otpAuthUrl = "otpauth://totp/";
/* Array of TOTP tokens */
var tokens=[];
2021-11-09 15:24:47 +00:00
/* Remove any non-base-32 characters from the given string and collapses
* whitespace to a single space. Optionally removes all whitespace from
* the string.
*/
function base32clean(val, nows) {
2021-11-02 05:05:20 +00:00
var ret = val.replaceAll(/\s+/g, " ");
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, "");
if (nows) {
2021-11-02 05:05:20 +00:00
ret = ret.replaceAll(/\s+/g, "");
}
return ret;
}
2021-11-09 15:24:47 +00:00
/* Save changes to a token to the global tokens[] array.
* id is the index into the global tokens[].
* forget is a flag indicating if the token should be forgotten.
*/
function saveEdit(id, forget) {
const parseIntValue = function(v, def) {
let ret = parseInt(v);
return (ret > 0) ? ret : def;
};
if (forget) {
2021-11-09 15:24:47 +00:00
if (confirm("Forget token?")) {
tokens.splice(id, 1);
updateTokens();
}
} else {
let fe = document.forms["edittoken"].elements;
2021-11-09 15:24:47 +00:00
tokens[id] = {
"algorithm":fe["algorithm"].value,
"digits":parseIntValue(fe["digits"].value,6),
"period":parseIntValue(fe["period"].value,30),
"secret":base32clean(fe["secret"].value, false),
"label":fe["label"].value
};
2021-11-09 15:24:47 +00:00
updateTokens();
}
}
2021-11-09 15:24:47 +00:00
/* Generate and display a QR-code representing the current token.
*/
function showQrCode() {
var fe = document.forms["edittoken"].elements;
2021-11-09 15:24:47 +00:00
var url = new String(otpAuthUrl);
url += encodeURIComponent(fe["label"].value);
url += "?secret=" + base32clean(fe["secret"].value, true);
if (parseInt(fe["period"].value) != 30) {
url += "&period=" + fe["period"].value;
}
if (parseInt(fe["digits"].value) != 6) {
url += "&digits=" + fe["digits"].value;
}
if (fe["algorithm"].value != "SHA1") {
url += "&algorithm=" + fe["algorithm"].value;
}
tokenqr.clear();
tokenqr.makeCode(url);
document.body.className = "showqr";
}
2021-11-09 15:24:47 +00:00
/* Generate a form for editing the specified token.
* id is the index into the global tokens[].
*/
function editToken(id) {
const selectMarkup = function(name, ary, cur) {
var ret = "<select name=\"" + name + "\">";
for (let i = 0; i < ary.length; i++) {
ret += "<option" + ((ary[i] == cur) ? " selected=selected" : "") + ">" + ary[i] + "</option>";
}
return ret + "</select>";
};
scanning=false;
var markup = "<form id=\"edittoken\"><input type=\"hidden\" name=\"tokenid\" value=\"" + id + "\"><table>";
markup += "<tr><td>Name:</td><td><input name=\"label\" type=\"text\" value=\"" + tokens[id].label + "\"></td></tr>";
markup += "<tr><td>Secret:</td><td><input name=\"secret\" type=\"text\" value=\"" + tokens[id].secret + "\"></td></tr>";
markup += "<tbody id=\"adv\"><tr><td>Period:</td><td><input name=\"period\" type=\"text\" value=\"" + tokens[id].period + "\"></td></tr>";
markup += "<tr><td>Digits:</td><td>";
2021-11-09 15:24:47 +00:00
markup += selectMarkup("digits", ["6","8"], tokens[id].digits);
markup += "</td></tr><tr><td>Hash:</td><td>";
markup += selectMarkup("algorithm", ["SHA1","SHA256","SHA512"], tokens[id].algorithm);
markup += "</td></tr></tbody><tr><td id=\"advbtn\" colspan=\"2\">";
markup += "<button type=\"button\" onclick=\"document.getElementById('edittoken').classList.toggle('showadv')\">Advanced</button>";
markup += "</td></tr></table></form>";
markup += "<button type=\"button\" onclick=\"updateTokens()\">Cancel Edit</button>";
markup += "<button type=\"button\" onclick=\"saveEdit(" + id + ", false)\">Save Changes</button>";
if (tokens[id].isnew) {
markup += "<button type=\"button\" onclick=\"startScan()\">Scan QR Code</button>";
} else {
markup += "<button type=\"button\" onclick=\"showQrCode()\">Show QR Code</button>";
2021-11-09 15:24:47 +00:00
markup += "<button type=\"button\" onclick=\"saveEdit(" + id + ", true)\">Forget Token</button>";
}
document.getElementById("edit").innerHTML = markup;
document.body.className = "editing";
}
2021-11-09 15:24:47 +00:00
/* Create a new blank token and open the editor for it.
*/
function addToken() {
tokens[tokens.length] = {"algorithm":"SHA1","digits":6,"period":30,"secret":"","label":"","isnew":true};
2021-11-09 15:24:47 +00:00
editToken(tokens.length - 1);
}
2021-11-09 15:24:47 +00:00
/* Move the specified token up or down in the global tokens[].
* id is the index in the global tokens[] of the token to move.
* dir is the direction to move: -1=up, 1=down.
*/
function moveToken(id, dir) {
tokens.splice(id + dir, 0, tokens.splice(id, 1)[0]);
updateTokens();
}
2021-11-09 15:24:47 +00:00
/* Update the display listing all the tokens.
*/
function updateTokens() {
2021-11-09 15:24:47 +00:00
const tokenButton = function(fn, id, label, dir) {
return "<button type=\"button\" onclick=\"" + fn + "(" + id + (dir ? "," + dir : "") + ")\">" + label + "</button>";
};
2021-11-09 15:24:47 +00:00
var markup = "<table><tr><th>Token</th><th colspan=\"2\">Order</th></tr>";
/* any tokens marked new are cancelled new additions and must be removed */
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].isnew) {
2021-11-09 15:24:47 +00:00
tokens.splice(i--, 1);
}
}
for (let i = 0; i < tokens.length; i++) {
markup += "<tr><td>";
2021-11-09 15:24:47 +00:00
markup += tokenButton("editToken", i, tokens[i].label);
markup += "</td><td>";
if (i < (tokens.length - 1)) {
2021-11-09 15:24:47 +00:00
markup += tokenButton("moveToken", i, "&#x25bc;", 1);
}
markup += "</td><td>";
if (i > 0) {
2021-11-09 15:24:47 +00:00
markup += tokenButton("moveToken", i, "&#x25b2;", -1);
}
markup += "</td></tr>";
}
markup += "</table>";
markup += "<button type=\"button\" onclick=\"addToken()\">Add Token</button>";
markup += "<button type=\"button\" onclick=\"saveTokens()\">Save to watch</button>";
document.getElementById("tokens").innerHTML = markup;
document.body.className = "select";
}
2021-11-09 15:24:47 +00:00
/* Original QR-code reader: https://www.sitepoint.com/create-qr-code-reader-mobile-website/ */
qrcode.callback = res => {
if (res) {
2021-11-09 15:24:47 +00:00
if (res.startsWith(otpAuthUrl)) {
res = decodeURIComponent(res);
var paramsidx = res.indexOf('?');
var otplabel = res.substring(res.lastIndexOf('/', paramsidx)+1, paramsidx);
var params = res.substr(paramsidx+1).split('&');
var t = {
"algorithm":"SHA1",
"digits":"6",
"period":"30",
"secret":"",
"issuer":""
};
var otpok = true;
for (let pi in params) {
var param = params[pi].split('=');
if (param[0] in t) {
t[param[0]] = param[1];
} else {
otpok = false;
}
}
if (t["secret"] == "") {
otpok = false;
}
2021-11-09 15:24:47 +00:00
if (otpok) {
scanning = false;
editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value));
t["issuer"] = (t["issuer"] == "") ? otplabel : t["issuer"] + " (" + otplabel + ")";
var fe = document.forms["edittoken"].elements;
fe["algorithm"].value = t["algorithm"];
fe["digits"].value = t["digits"];
fe["period"].value = t["period"];
fe["secret"].value = t["secret"];
fe["label"].value = t["issuer"];
}
}
}
};
function startScan() {
document.body.className = "scanning";
navigator.mediaDevices
.getUserMedia({video:{facingMode:"environment"}})
.then(function(stream){
scanning=true;
video.setAttribute("playsinline",true);
video.srcObject = stream;
video.play();
scanTick();
doScan();
});
}
function scanTick() {
canvasElement.height = video.videoHeight;
canvasElement.width = video.videoWidth;
canvas.drawImage(video,0,0,canvasElement.width,canvasElement.height);
if (scanning) {
requestAnimationFrame(scanTick);
} else {
video.srcObject.getTracks().forEach(track => {
track.stop();
});
}
}
function doScan() {
try {
qrcode.decode();
} catch (e) {
setTimeout(doScan,300);
}
}
2021-11-09 15:24:47 +00:00
/* Load settings JSON file from the watch.
*/
function loadTokens() {
2021-10-29 14:36:30 +00:00
Util.showModal("Loading...");
Puck.eval(`require("Storage").read(${JSON.stringify("authentiwatch.json")})`,data=>{
2021-10-29 14:36:30 +00:00
Util.hideModal();
try {
tokens = JSON.parse(data);
updateTokens();
} catch {
tokens = [];
2021-10-29 14:36:30 +00:00
}
});
}
2021-11-09 15:24:47 +00:00
/* Save settings as a JSON file on the watch.
*/
function saveTokens() {
Util.showModal("Saving...");
Puck.write(`\x10require("Storage").write(${JSON.stringify("authentiwatch.json")},${JSON.stringify(tokens)})\n`,()=>{
Util.hideModal();
});
}
2021-10-29 14:36:30 +00:00
function onInit() {
loadTokens();
updateTokens();
2021-10-29 14:36:30 +00:00
}
</script>
</head>
<body class="select">
<h1>Authentiwatch</h1>
<div id="tokens">
<p>No watch comms.</p>
</div>
<div id="scan">
<table>
<tr><td><canvas id="qr-canvas"></canvas></td></tr>
2021-11-09 15:24:47 +00:00
<tr><td><button type=\"button\" onclick="editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value))">Cancel</button></td></tr>
</table>
</div>
<div id="edit">
</div>
<div id="tokenqr">
<table><tr><td id="qrcode"></td></tr><tr><td>
<button type=\"button\" onclick="document.body.className='editing'">Back</button>
</td></tr></table>
</div>
<script type="text/javascript">
const video=document.createElement("video");
const canvasElement=document.getElementById("qr-canvas");
const canvas=canvasElement.getContext("2d");
let scanning=false;
const tokenqr=new QRCode(document.getElementById("qrcode"), "");
</script>
<script src="../../core/lib/interface.js"></script>
</body>
2021-10-29 14:36:30 +00:00
</html>