diff --git a/www/icons/ic_add.svg b/www/icons/ic_add.svg new file mode 100644 index 0000000..012e184 --- /dev/null +++ b/www/icons/ic_add.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/www/icons/ic_vpn_key.svg b/www/icons/ic_vpn_key.svg new file mode 100644 index 0000000..2cbeda9 --- /dev/null +++ b/www/icons/ic_vpn_key.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/www/img/nokeys.svg b/www/img/nokeys.svg new file mode 100644 index 0000000..13d1379 --- /dev/null +++ b/www/img/nokeys.svg @@ -0,0 +1,70 @@ + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/www/js/app.js b/www/js/app.js index ae828db..50d3dfb 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -2,7 +2,7 @@ userinfo = null; document.addEventListener("deviceready", function () { if (cordova.platformId == 'android') { - StatusBar.backgroundColorByHexString("#1976d2"); + StatusBar.backgroundColorByHexString("#1976d2"); } // Enable/disable jQuery animations depending on user preference @@ -10,15 +10,15 @@ document.addEventListener("deviceready", function () { /* Fade out alerts */ $(".alert .close").click(function (e) { - $(this).parent().fadeOut("slow"); + $(this).parent().fadeOut("slow"); }); if (localStorage.getItem("setupcomplete")) { - getuserinfo(function () { - openscreen("home"); - }); + getuserinfo(function () { + openscreen("home"); + }); } else { - openscreen("setup1"); + openscreen("setup1"); } setTimeout(navigator.splashscreen.hide, 1000); }, false); @@ -32,24 +32,24 @@ document.addEventListener("deviceready", function () { function getuserinfo(callback) { $(".loading-text").text("Loading account..."); $.post(localStorage.getItem("syncurl"), { - username: localStorage.getItem("username"), - key: localStorage.getItem("key"), - password: localStorage.getItem("password"), - action: "user_info" + username: localStorage.getItem("username"), + key: localStorage.getItem("key"), + password: localStorage.getItem("password"), + action: "user_info" }, function (data) { - if (data.status === 'OK') { + if (data.status === 'OK') { $(".loading-text").text("Loading..."); - userinfo = data.info; - if (typeof callback == 'function') { - callback(); - } - } else { - navigator.notification.alert(data.msg, null, "Error", 'Dismiss'); - openscreen("homeloaderror"); - } + userinfo = data.info; + if (typeof callback == 'function') { + callback(); + } + } else { + navigator.notification.alert(data.msg, null, "Error", 'Dismiss'); + openscreen("homeloaderror"); + } }, "json").fail(function () { - navigator.notification.alert("Could not connect to the server. Try again later.", null, "Error", 'Dismiss'); - openscreen("homeloaderror"); + navigator.notification.alert("Could not connect to the server. Try again later.", null, "Error", 'Dismiss'); + openscreen("homeloaderror"); }); } @@ -61,38 +61,38 @@ function getuserinfo(callback) { */ function openscreen(screenname, effect) { if (effect === 'FADE') { - $('#content-zone').fadeOut(300, function () { - $('#content-zone').load("views/" + screenname + ".html", function () { - $('#content-zone').fadeIn(300); - }); - }); + $('#content-zone').fadeOut(300, function () { + $('#content-zone').load("views/" + screenname + ".html", function () { + $('#content-zone').fadeIn(300); + }); + }); } else if (effect === 'SLIDE') { - $('#content-zone').slideToggle('400', function () { - $('#content-zone').load("views/" + screenname + ".html", function () { - $('#content-zone').slideToggle('400'); - }); - }); + $('#content-zone').slideToggle('400', function () { + $('#content-zone').load("views/" + screenname + ".html", function () { + $('#content-zone').slideToggle('400'); + }); + }); } else { - $('#content-zone').load("views/" + screenname + ".html"); + $('#content-zone').load("views/" + screenname + ".html"); } currentscreen = screenname; } function openfragment(fragment, target, effect) { if (effect === 'FADE') { - $(target).fadeOut('slow', function () { - $(target).load("views/" + fragment + ".html", function () { - $(target).fadeIn('slow'); - }); - }); + $(target).fadeOut('slow', function () { + $(target).load("views/" + fragment + ".html", function () { + $(target).fadeIn('slow'); + }); + }); } else if (effect === 'SLIDE') { - $(target).slideToggle('400', function () { - $(target).load("views/" + fragment + ".html", function () { - $(target).slideToggle('400'); - }); - }); + $(target).slideToggle('400', function () { + $(target).load("views/" + fragment + ".html", function () { + $(target).slideToggle('400'); + }); + }); } else { - $(target).load("views/" + fragment + ".html"); + $(target).load("views/" + fragment + ".html"); } } @@ -108,39 +108,42 @@ function openfragment(fragment, target, effect) { function setnavbar(type, title, returnscreen) { var navbar = $('#navbar-header'); if (type == false) { - $('#navbar').css('display', 'none'); - $('#content-zone').css('margin-top', '0px'); + $('#navbar').css('display', 'none'); + $('#content-zone').css('margin-top', '0px'); } else { - if (cordova.platformId == 'android') { - StatusBar.backgroundColorByHexString("#1976d2"); - window.plugins.headerColor.tint("#2196f3"); - } else { - StatusBar.backgroundColorByHexString("#2196f3"); - } - $('#navbar').css('display', 'initial'); - $('#content-zone').css('margin-top', '75px'); - if (returnscreen === undefined) { - returnscreen = "home"; - _returnscreen = null; - } else { - _returnscreen = returnscreen; - } - navbar.fadeOut(150, function () { - switch (type) { - case "home": - navbar.html('Business'); - break; - case "settings": - navbar.html('Settings'); - break; - case "app": - navbar.html('' + title + ''); - break; - default: - navbar.html('Business'); - } - navbar.fadeIn(150); - }); + if (cordova.platformId == 'android') { + StatusBar.backgroundColorByHexString("#1976d2"); + window.plugins.headerColor.tint("#2196f3"); + } else { + StatusBar.backgroundColorByHexString("#2196f3"); + } + $('#navbar').css('display', 'initial'); + $('#content-zone').css('margin-top', '75px'); + if (returnscreen === undefined) { + returnscreen = "home"; + _returnscreen = null; + } else { + _returnscreen = returnscreen; + } + navbar.fadeOut(150, function () { + switch (type) { + case "home": + navbar.html('Business   '); + break; + case "settings": + navbar.html('Settings'); + break; + case "otp": + navbar.html('Auth Keys'); + break; + case "app": + navbar.html('' + title + ''); + break; + default: + navbar.html('Business'); + } + navbar.fadeIn(150); + }); } } @@ -157,11 +160,11 @@ function setnavbar(type, title, returnscreen) { */ function openapp(id, api, url, icon, title, injectcode, shownavbar) { $('#content-zone').fadeOut(300, function () { - $('#content-zone').load("views/app.html", function () { - $('#content-zone').fadeIn(300, function () { - launchapp(id, api, url, icon, title, injectcode, shownavbar); - }); - }); + $('#content-zone').load("views/app.html", function () { + $('#content-zone').fadeIn(300, function () { + launchapp(id, api, url, icon, title, injectcode, shownavbar); + }); + }); }); } @@ -173,8 +176,8 @@ function openapp(id, api, url, icon, title, injectcode, shownavbar) { */ function openmodal(filename, modalselector) { $('#modal-load-box').load("views/" + filename + ".html", null, function (x) { - $(modalselector).css('z-index', 9999999); - $(modalselector).modal('show'); + $(modalselector).css('z-index', 9999999); + $(modalselector).modal('show'); }); } @@ -191,7 +194,7 @@ function restartApplication() { navigator.splashscreen.show(); // We're doing the timeout so we don't run afoul of server-side rate limiting setTimeout(function () { - window.location = "index.html"; + window.location = "index.html"; }, 3000); } @@ -204,10 +207,15 @@ document.addEventListener("backbutton", function (event) { iframe.contentWindow.postMessage("goback", "*"); historyctr--; } else if (_returnscreen != null) { - openscreen(_returnscreen, "FADE"); - _returnscreen = null; - } else { - openscreen("home", "FADE"); - } + openscreen(_returnscreen, "FADE"); + _returnscreen = null; + } else { + openscreen("home", "FADE"); + } + } else { + if (_returnscreen != null) { + openscreen(_returnscreen, "FADE"); + _returnscreen = null; + } } }, false); \ No newline at end of file diff --git a/www/js/jsOTP.min.js b/www/js/jsOTP.min.js new file mode 100644 index 0000000..6f44102 --- /dev/null +++ b/www/js/jsOTP.min.js @@ -0,0 +1,6 @@ +/* + * File combining + * (1) sha.js by Brian Turek 2008-2013 under BSD license + * (2) and a modified js OTP implementation found on JSFiddle +*/ +(function(){var a,b;b=function(){function a(a,b){if(this.expiry=null!=a?a:30,this.length=null!=b?b:6,this.length>8||this.length<6)throw"Error: invalid code length"}return a.prototype.dec2hex=function(a){return(15.5>a?"0":"")+Math.round(a).toString(16)},a.prototype.hex2dec=function(a){return parseInt(a,16)},a.prototype.base32tohex=function(a){var b,c,d,e,f,g;for(b="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",c="",e="",f=0;f=a.length&&(a=Array(b+1-a.length).join(c)+a),a},a.prototype.getOtp=function(a,b){var c,d,e,f,g,h,i;if(null==b&&(b=(new Date).getTime()),e=this.base32tohex(a),c=Math.round(b/1e3),i=this.leftpad(this.dec2hex(Math.floor(c/this.expiry)),16,"0"),h=new jsSHA("SHA-1","HEX"),h.setHMACKey(e,"HEX"),h.update(i),d=h.getHMAC("HEX"),"KEY MUST BE IN BYTE INCREMENTS"===d)throw"Error: hex key must be in byte increments";return f=this.hex2dec(d.substring(d.length-1)),g=(this.hex2dec(d.substr(2*f,8))&this.hex2dec("7fffffff"))+"",g=g.substr(g.length-this.length,this.length)},a}(),a=function(){function a(a){if(this.length=null!=a?a:6,this.length>8||this.length<6)throw"Error: invalid code length"}return a.prototype.uintToString=function(a){var b,c;return c=String.fromCharCode.apply(null,a),b=decodeURIComponent(escape(c))},a.prototype.getOtp=function(a,b){var c,d,e,f,g;return f=new jsSHA("SHA-1","TEXT"),f.setHMACKey(a,"TEXT"),f.update(this.uintToString(new Uint8Array(this.intToBytes(b)))),c=f.getHMAC("HEX"),d=this.hexToBytes(c),e=15&d[19],g=(127&d[e])<<24|(255&d[e+1])<<16|(255&d[e+2])<<8|255&d[e+3],g+="",g.substr(g.length-this.length,this.length)},a.prototype.intToBytes=function(a){var b,c;for(b=[],c=7;c>=0;)b[c]=255&a,a>>=8,--c;return b},a.prototype.hexToBytes=function(a){var b,c,d;for(c=[],d=0,b=a.length;b>d;)c.push(parseInt(a.substr(d,2),16)),d+=2;return c},a}(),window.jsOTP={},jsOTP.totp=b,jsOTP.hotp=a}).call(this);var SUPPORTED_ALGS=7;!function(a){"use strict";function b(a,b){this.highOrder=a,this.lowOrder=b}function c(a,b,c,d){var e,f,g,h,i,j,k=[],l=[],m=0;if(k=c||[0],d=d||0,h=d>>>3,"UTF8"===b)for(f=0;fe?l.push(e):2048>e?(l.push(192|e>>>6),l.push(128|63&e)):55296>e||e>=57344?l.push(224|e>>>12,128|e>>>6&63,128|63&e):(f+=1,e=65536+((1023&e)<<10|1023&a.charCodeAt(f)),l.push(240|e>>>18,128|e>>>12&63,128|e>>>6&63,128|63&e)),g=0;g>>2;k.length<=i;)k.push(0);k[i]|=l[g]<<8*(3-j%4),m+=1}else if("UTF16BE"===b||"UTF16LE"===b)for(f=0;f>>8),j=m+h,i=j>>>2;k.length<=i;)k.push(0);k[i]|=e<<8*(2-j%4),m+=2}return{value:k,binLen:8*m+d}}function d(a,b,c){var d,e,f,g,h,i,j=a.length;if(d=b||[0],c=c||0,i=c>>>3,0!==j%2)throw new Error("String of HEX type must be in byte increments");for(e=0;j>e;e+=2){if(f=parseInt(a.substr(e,2),16),isNaN(f))throw new Error("String of HEX type contains invalid characters");for(h=(e>>>1)+i,g=h>>>2;d.length<=g;)d.push(0);d[g]|=f<<8*(3-h%4)}return{value:d,binLen:4*j+c}}function e(a,b,c){var d,e,f,g,h,i=[];for(i=b||[0],c=c||0,f=c>>>3,e=0;e>>2,i.length<=g&&i.push(0),i[g]|=d<<8*(3-h%4);return{value:i,binLen:8*a.length+c}}function f(a,b,c){var d,e,f,g,h,i,j,k,l,m=[],n=0,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";if(m=b||[0],c=c||0,j=c>>>3,-1===a.search(/^[a-zA-Z0-9=+\/]+$/))throw new Error("Invalid character in base-64 string");if(i=a.indexOf("="),a=a.replace(/\=/g,""),-1!==i&&i 1 && i < text.length) { + // add a space every 5 characters, + // unless it's the first character + // or the last character + formatted = formatted + " "; + } + } + $('#key').val(formatted.toUpperCase()); + }); + + function manualadd() { + var key = $('#key').val().replace(/\s+/g, ''); + var label = $('#label').val(); + var issuer = $('#issuer').val(); + addOTP(key, label, issuer); + } + + function manualshow() { + $('#manual_add').css('display', 'block'); + } + + function addOTP(key, label, issuer) { + if (key == "") { + navigator.notification.alert("Missing secret key.", null, "Error", 'Dismiss'); + return; + } + if (label == "") { + navigator.notification.alert("Missing label.", null, "Error", 'Dismiss'); + return; + } + var ls_text = localStorage.getItem("otp"); + var keys = []; + if (ls_text != null && ls_text != "") { + keys = JSON.parse(ls_text || "[]"); + } + + keys.push({"secret": key, "label": label, "issuer": issuer}); + localStorage.setItem("otp", JSON.stringify(keys)); + navigator.notification.alert("2-factor key saved.", null, "Key added", 'Dismiss'); + openscreen("otp"); + } + + function scanCode() { + try { + cordova.plugins.barcodeScanner.scan( + function (result) { + if (!result.cancelled) { + try { + var url = decodeURI(result.text); + } catch (e) { + navigator.notification.alert("Could not decode OTP URI.", null, "Error", 'Dismiss'); + return; + } + if (!url.startsWith("otpauth://")) { + navigator.notification.alert("Invalid OTP code. Try again.", null, "Error", 'Dismiss'); + return; + } + if (!url.startsWith("otpauth://totp/")) { + navigator.notification.alert("Unsupported key type.", null, "Error", 'Dismiss'); + return; + } + var stripped = url.replace("otpauth://totp/", ""); + var params = stripped.split("?")[1].split("&"); + var label = stripped.split("?")[0]; + var secret = ""; + var issuer = ""; + for (var i = 0; i < params.length; i++) { + var param = params[i].split("="); + if (param[0] == "secret") { + secret = param[1].toUpperCase(); + } else if (param[0] == "issuer") { + issuer = param[1]; + } else if (param[0] == "algorithm" && param[1].toLowerCase() != "sha1") { + navigator.notification.alert("Unsupported hash algorithm.", null, "Error", 'Dismiss'); + return; + } else if (param[0] == "digits" && param[1] != "6") { + navigator.notification.alert("Unsupported digit count.", null, "Error", 'Dismiss'); + return; + } else if (param[0] == "period" && param[1] != "30") { + navigator.notification.alert("Unsupported period.", null, "Error", 'Dismiss'); + return; + } + } + addOTP(secret, label, issuer); + } + }, + function (error) { + navigator.notification.alert("Scanning failed: " + error, null, "Error", 'Dismiss'); + }, + { + "showFlipCameraButton": false, + "prompt": "Scan OTP QR code." + } + ); + } catch (ex) { + navigator.notification.alert(ex.message, null, "Error", 'Dismiss'); + } + } + + setnavbar("app", "Add Auth Key", "otp"); + \ No newline at end of file diff --git a/www/views/otp.html b/www/views/otp.html new file mode 100644 index 0000000..7315e1f --- /dev/null +++ b/www/views/otp.html @@ -0,0 +1,73 @@ +
+
+
+
+
+
+
+ +

+ You haven't added any authentication keys yet. Press the icon to add one. +
+
+
+
+
+
+ + diff --git a/www/views/settings.html b/www/views/settings.html index e24eea0..8390b4d 100644 --- a/www/views/settings.html +++ b/www/views/settings.html @@ -47,16 +47,16 @@ setnavbar("settings"); function deleteall() { - navigator.notification.confirm("Really wipe user data? You will need to resync the app with AccountHub to use it again.", function (result) { + navigator.notification.confirm("Really wipe user data? You will need to resync the app with AccountHub to use it again. This will not delete 2-factor auth keys.", function (result) { if (result != 1) { return; } // Wipe localStorage localStorage.removeItem("setupcomplete"); localStorage.removeItem("username"); + localStorage.removeItem("password"); localStorage.removeItem("syncurl"); localStorage.removeItem("key"); - localStorage.clear(); // force-reload app navigator.notification.alert("Connection data and credentials erased.", function () { restartApplication();