1217 lines
61 KiB
Handlebars
1217 lines
61 KiB
Handlebars
<!DOCTYPE html> <html dir="ltr" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta content="text/html; charset=utf-8" http-equiv="Content-Type"> <meta name="viewport" content="user-scalable=1.0,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="format-detection" content="telephone=no"> <title>MeshCentral - Login</title> <style> a{color:#036;text-decoration:underline;}#footer a{color:#fff;text-decoration:underline;}#footer a:hover{color:#fff;text-decoration:none;}</style> </head> <body onload="if (typeof(startup) !== 'undefined') startup();" style="overflow-y:hidden;margin:0;padding:0;border:0;color:black;font-size:13px;font-family:\'Trebuchet MS\', Arial, Helvetica, sans-serif"> <div id="container"> <div id="mastheadx"></div> <div id="masthead" style="background:url(logo.png) 0px 0px;background-size:341px 50px;background-color:#036;background-repeat:no-repeat;height:50px;width:100%;overflow:hidden"> <div style="float:left;height:66px;color:#c8c8c8;padding-left:10px;padding-top:6px"> <strong><font style="font-size:36px;font-family:Arial,Helvetica,sans-serif">{{{title}}}</font></strong> </div> <div style="float:left;height:66px;color:#c8c8c8;padding-left:5px;padding-top:10px"> <strong><font style="font-size:12px;font-family:Arial,Helvetica,sans-serif">{{{title2}}}</font></strong> </div> </div> <div id="page_content" style="overflow-y:scroll;position:absolute;bottom:32px;top:50px;width:100%;display:flex;align-items:center"> <div id="column_l" style="padding:10px;width:100%"> <table style="width:100%"> <tr> <td align="center"> <div id="loginpanel" style="background-color:#979797;border-radius:16px;width:260px;padding:16px;text-align:center;clear:both;display:none"> <form action="login" method="post"> <div id="message1"> {{{message}}} </div> <div> <b>Log In</b> </div> <table> <tr> <td align="right" width="100">Username:</td> <td><input id="username" type="text" maxlength="64" name="username" onchange="validateLogin(1)" onkeyup="validateLogin(1,event)"></td> </tr> <tr> <td align="right">Password:</td> <td><input id="password" type="password" maxlength="256" name="password" autocomplete="off" onchange="validateLogin(2)" onkeyup="validateLogin(2,event)"></td> </tr> <tr> <td><div id="showPassHintLink" style="display:none"><a onclick="showPassHint()" style="cursor:pointer">Show Hint</a></div></td> <td align="right"><input id="loginButton" type="submit" value="Log In" disabled="disabled"></td> </tr> </table> <div id="hrAccountDiv" style="display:none"><hr></div> <div id="resetAccountDiv" style="display:none;padding:2px"> Forgot user/password? <a onclick="xgo(3)" style="cursor:pointer">Reset account</a>. </div> <div id="newAccountDiv" style="display:none;padding:2px"> Don't have an account? <a onclick="xgo(2)" style="cursor:pointer">Create one</a>. </div> </form> </div> <div id="createpanel" style="position:relative;display:none"> <div style="background-color:#979797;border-radius:16px;width:260px;padding:16px;text-align:center;clear:both"> <form action="createaccount" method="post"> <div id="message2"> {{{message}}} </div> <div> <b>Account Creation</b> </div> <div id="passwordPolicyCallout" style="left:-5px;top:10px;width:100px;position:absolute;background-color:#FFC;border-radius:5px;padding:5px;box-shadow:0px 0px 15px #666;font-size:10px"></div> <table> <tr> <td align="right" width="100">Username:</td> <td><input id="ausername" type="text" name="username" onchange="validateCreate(1)" maxlength="64" onkeydown="haltReturn(event)" onkeyup="validateCreate(1,event)"></td> </tr> <tr> <td align="right" width="100">Email:</td> <td><input id="aemail" type="text" name="email" onchange="validateCreate(2)" maxlength="256" onkeydown="haltReturn(event)" onkeyup="validateCreate(2,event)"></td> </tr> <tr> <td align="right">Password:</td> <td><input id="apassword1" type="password" name="password1" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validateCreate(3)" onkeyup="validateCreate(3,event)"></td> </tr> <tr> <td align="right">Password:</td> <td><input id="apassword2" type="password" name="password2" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validateCreate(4)" onkeyup="validateCreate(4,event)"></td> </tr> <tr id="createPanelHint" style="display:none"> <td align="right">Pass Hint:</td> <td><input id="apasswordhint" type="text" name="apasswordhint" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validateCreate(5)" onkeyup="validateCreate(5,event)"></td> </tr> <tr id="newAccountPass" title="Enter the account creation token"> <td align="right">Creation Token:</td> <td><input id="anewaccountpass" type="password" name="anewaccountpass" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validateCreate(6)" onkeyup="validateCreate(6,event)"></td> </tr> <tr> <td colspan="2"> <div style="float:right"><input id="createButton" type="submit" value="Create Account" disabled="disabled"></div> <div id="passWarning" style="padding-top:6px"></div> </td> </tr> </table> <hr><a onclick="xgo(1)" style="cursor:pointer">Back to login</a> </form> </div> </div> <div id="resetpanel" style="background-color:#979797;border-radius:16px;width:260px;padding:16px;text-align:center;display:none;clear:both"> <form action="resetaccount" method="post"> <div id="message3"> {{{message}}} </div> <div> <b>Account Reset</b> </div> <table> <tr> <td align="right" width="100">Email:</td> <td><input id="remail" type="text" name="email" maxlength="256" onchange="validateReset()" onkeyup="validateReset(event)"></td> </tr> <tr> <td colspan="2"> <div style="float:right"><input id="eresetButton" type="submit" value="Reset Account" disabled="disabled"></div> <div id="passWarning" style="padding-top:6px"></div> </td> </tr> </table> <hr><a onclick="xgo(1)" style="cursor:pointer">Back to login</a> </form> </div> <div id="tokenpanel" style="background-color:#979797;border-radius:16px;width:260px;padding:16px;text-align:center;display:none;clear:both"> <form action="tokenlogin" method="post" autocomplete="off"> <div id="message4"> {{{message}}} </div> <table> <tr> <td align="right" width="100">Login token:</td> <td> <input id="tokenInput" type="text" name="token" maxlength="50" onchange="checkToken(event)" onkeyup="checkToken(event)" onkeydown="checkToken(event)" onfocus="checkTokenTimer(1)" onblur="checkTokenTimer(0)"> <input id="hwtokenInput" type="text" name="hwtoken" style="display:none"> </td> </tr> <tr> <td colspan="2"> <div style="float:right"><input id="tokenOkButton" type="submit" value="Login" disabled="disabled"></div> </td> </tr> </table> <hr><a onclick="xgo(1)" style="cursor:pointer">Back to login</a> </form> </div> <div id="resettokenpanel" style="background-color:#979797;border-radius:16px;width:260px;padding:16px;text-align:center;display:none;clear:both"> <form action="resetaccount" method="post" autocomplete="off"> <div id="message5"> {{{message}}} </div> <table> <tr> <td align="right" width="100">Login token:</td> <td> <input id="resetTokenInput" type="text" name="token" maxlength="50" onchange="resetCheckToken(event)" onpaste="resetCheckToken(event)" onkeyup="resetCheckToken(event)" onkeydown="resetCheckToken(event)"> <input id="resetHwtokenInput" type="text" name="hwtoken" style="display:none"> </td> </tr> <tr> <td colspan="2"> <div style="float:right"><input id="resetTokenOkButton" type="submit" value="Login" disabled="disabled"></div> </td> </tr> </table> <hr><a onclick="xgo(1)" style="cursor:pointer">Back to login</a> </form> </div> <div id="resetpasswordpanel" style="position:relative;background-color:#979797;border-radius:16px;width:300px;padding:16px;text-align:center;display:none"> <form action="resetpassword" method="post"> <div id="message6"> {{{message}}} </div> <div id="rpasswordPolicyCallout" style="left:-10px;width:100px;display:none;position:absolute;background-color:#FFC;border-radius:5px;padding:5px;box-shadow:0px 0px 15px #666;font-size:10px"></div> <table> <tr> <td id="rnuPass1" width="100" align="right">Password:</td> <td><input id="rapassword1" type="password" name="rpassword1" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validatePassReset(3,event)" onkeyup="validatePassReset(3,event)"></td> </tr> <tr> <td id="rnuPass2" align="right">Password:</td> <td><input id="rapassword2" type="password" name="rpassword2" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validatePassReset(4,event)" onkeyup="validatePassReset(4,event)"></td> </tr> <tr id="resetpasswordpanelHint" style="display:none"> <td id="rnuHint" align="right">Password Hint:</td> <td><input id="rapasswordhint" type="text" name="rpasswordhint" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validatePassReset(5,event)" onkeyup="validatePassReset(5,event)"></td> </tr> <tr> <td colspan="2"> <div style="float:right"><input id="resetPassButton" type="submit" value="Reset Password" disabled="disabled"></div> <div id="rpassWarning" style="padding-top:6px"></div> </td> </tr> </table> <hr><a onclick="xgo(1)" style="cursor:pointer">Back to login</a> </form> </div> </td> </tr> </table> </div> </div> <div id="footer" style="height:32px;width:100%;text-align:center;background-color:#113962;position:absolute;bottom:0px"> <table cellpadding="0" cellspacing="6" style="width:100%"> <tr> <td style="text-align:left;color:white">{{{footer}}}</td> <td style="text-align:right">{{{rootCertLink}}} <a href="terms">Terms & Privacy</a></td> </tr> </table> </div> </div> <div id="dialog" style="z-index:1000;background-color:#EEE;box-shadow:0px 0px 15px #666;font-family:Arial,Helvetica,sans-serif;border-radius:5px;position:fixed;top:180px;width:400px;display:none"> <div style="width:100%;background-color:#003366;color:#FFF;border-radius:5px 5px 0 0"> <div id="id_dialogclose" style="float:right;padding:5px;cursor:pointer" onclick="setDialogMode()"><b>X</b></div> <div id="id_dialogtitle" style="padding:5px"></div> <div style="width:100%;margin:6px"></div> </div> <div style="margin-right:16px;margin-left:8px"> <div id="dialog1" style="margin:auto;text-align:center;margin:3px"> <div id="id_dialogMessage" style="padding:10px"></div> </div> <div id="dialog2" style="margin:auto;margin:3px"> <div id="id_dialogOptions"></div> </div> </div> <div id="idx_dlgButtonBar" style="padding:10px;margin-bottom:20px"> <input id="idx_dlgCancelButton" type="button" value="Cancel" style="float:right;width:80px;margin-left:5px" onclick="dialogclose(0)"> <input id="idx_dlgOkButton" type="button" value="OK" style="float:right;width:80px" onclick="dialogclose(1)"> </div> </div> <script>/**
|
|
* @description Set of short commonly used methods for handling HTML elements
|
|
* @author Ylian Saint-Hilaire
|
|
* @version v0.0.1b
|
|
*/
|
|
|
|
// Add startsWith for IE browser
|
|
if (!String.prototype.startsWith) { String.prototype.startsWith = function (str) { return this.lastIndexOf(str, 0) === 0; }; }
|
|
if (!String.prototype.endsWith) { String.prototype.endsWith = function (str) { return this.indexOf(str, this.length - str.length) !== -1; }; }
|
|
|
|
// Quick UI functions, a bit of a replacement for jQuery
|
|
//function Q(x) { if (document.getElementById(x) == null) { console.log('Invalid element: ' + x); } return document.getElementById(x); } // "Q"
|
|
function Q(x) { return document.getElementById(x); } // "Q"
|
|
function QS(x) { try { return Q(x).style; } catch (x) { } } // "Q" style
|
|
function QE(x, y) { try { Q(x).disabled = !y; } catch (x) { } } // "Q" enable
|
|
function QV(x, y) { try { QS(x).display = (y ? '' : 'none'); } catch (x) { } } // "Q" visible
|
|
function QA(x, y) { Q(x).innerHTML += y; } // "Q" append
|
|
function QH(x, y) { Q(x).innerHTML = y; } // "Q" html
|
|
|
|
// Move cursor to end of input box
|
|
function inputBoxFocus(x) { Q(x).focus(); var v = Q(x).value; Q(x).value = ''; Q(x).value = v; }
|
|
|
|
// Binary encoding and decoding functions
|
|
function ReadShort(v, p) { return (v.charCodeAt(p) << 8) + v.charCodeAt(p + 1); }
|
|
function ReadShortX(v, p) { return (v.charCodeAt(p + 1) << 8) + v.charCodeAt(p); }
|
|
function ReadInt(v, p) { return (v.charCodeAt(p) * 0x1000000) + (v.charCodeAt(p + 1) << 16) + (v.charCodeAt(p + 2) << 8) + v.charCodeAt(p + 3); } // We use "*0x1000000" instead of "<<24" because the shift converts the number to signed int32.
|
|
function ReadSInt(v, p) { return (v.charCodeAt(p) << 24) + (v.charCodeAt(p + 1) << 16) + (v.charCodeAt(p + 2) << 8) + v.charCodeAt(p + 3); }
|
|
function ReadIntX(v, p) { return (v.charCodeAt(p + 3) * 0x1000000) + (v.charCodeAt(p + 2) << 16) + (v.charCodeAt(p + 1) << 8) + v.charCodeAt(p); }
|
|
function ShortToStr(v) { return String.fromCharCode((v >> 8) & 0xFF, v & 0xFF); }
|
|
function ShortToStrX(v) { return String.fromCharCode(v & 0xFF, (v >> 8) & 0xFF); }
|
|
function IntToStr(v) { return String.fromCharCode((v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF); }
|
|
function IntToStrX(v) { return String.fromCharCode(v & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF, (v >> 24) & 0xFF); }
|
|
function MakeToArray(v) { if (!v || v == null || typeof v == 'object') return v; return [v]; }
|
|
function SplitArray(v) { return v.split(','); }
|
|
function Clone(v) { return JSON.parse(JSON.stringify(v)); }
|
|
function EscapeHtml(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, '''); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
|
|
function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, ''').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, ' '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
|
|
|
|
// Move an element from one position in an array to a new position
|
|
function ArrayElementMove(arr, from, to) { arr.splice(to, 0, arr.splice(from, 1)[0]); };
|
|
|
|
// Print object for HTML
|
|
function ObjectToStringEx(x, c) {
|
|
var r = "";
|
|
if (x != 0 && (!x || x == null)) return "(Null)";
|
|
if (x instanceof Array) { for (var i in x) { r += '<br />' + gap(c) + "Item #" + i + ": " + ObjectToStringEx(x[i], c + 1); } }
|
|
else if (x instanceof Object) { for (var i in x) { r += '<br />' + gap(c) + i + " = " + ObjectToStringEx(x[i], c + 1); } }
|
|
else { r += EscapeHtml(x); }
|
|
return r;
|
|
}
|
|
|
|
// Print object for console
|
|
function ObjectToStringEx2(x, c) {
|
|
var r = "";
|
|
if (x != 0 && (!x || x == null)) return "(Null)";
|
|
if (x instanceof Array) { for (var i in x) { r += '\r\n' + gap2(c) + "Item #" + i + ": " + ObjectToStringEx2(x[i], c + 1); } }
|
|
else if (x instanceof Object) { for (var i in x) { r += '\r\n' + gap2(c) + i + " = " + ObjectToStringEx2(x[i], c + 1); } }
|
|
else { r += EscapeHtml(x); }
|
|
return r;
|
|
}
|
|
|
|
// Create an ident gap
|
|
function gap(c) { var x = ''; for (var i = 0; i < (c * 4) ; i++) { x += ' '; } return x; }
|
|
function gap2(c) { var x = ''; for (var i = 0; i < (c * 4) ; i++) { x += ' '; } return x; }
|
|
|
|
// Print an object in html
|
|
function ObjectToString(x) { return ObjectToStringEx(x, 0); }
|
|
function ObjectToString2(x) { return ObjectToStringEx2(x, 0); }
|
|
|
|
// Convert a hex string to a raw string
|
|
function hex2rstr(d) {
|
|
if (typeof d != "string" || d.length == 0) return '';
|
|
var r = '', m = ('' + d).match(/../g), t;
|
|
while (t = m.shift()) r += String.fromCharCode('0x' + t);
|
|
return r
|
|
}
|
|
|
|
// Convert decimal to hex
|
|
function char2hex(i) { return (i + 0x100).toString(16).substr(-2).toUpperCase(); }
|
|
|
|
// Convert a raw string to a hex string
|
|
function rstr2hex(input) { var r = '', i; for (i = 0; i < input.length; i++) { r += char2hex(input.charCodeAt(i)); } return r; }
|
|
|
|
// UTF-8 encoding & decoding functions
|
|
function encode_utf8(s) { return unescape(encodeURIComponent(s)); }
|
|
function decode_utf8(s) { return decodeURIComponent(escape(s)); }
|
|
|
|
// Convert a string into a blob
|
|
function data2blob(data) {
|
|
var bytes = new Array(data.length);
|
|
for (var i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i);
|
|
var blob = new Blob([new Uint8Array(bytes)]);
|
|
return blob;
|
|
}
|
|
|
|
// Generate random numbers
|
|
function random(max) { return Math.floor(Math.random() * max); }
|
|
|
|
// Trademarks
|
|
function trademarks(x) { return x.replace(/\(R\)/g, '®').replace(/\(TM\)/g, '™'); }
|
|
//Copyright 2014-2015 Google Inc. All rights reserved.
|
|
|
|
//Use of this source code is governed by a BSD-style
|
|
//license that can be found in the LICENSE file or at
|
|
//https://developers.google.com/open-source/licenses/bsd
|
|
|
|
/**
|
|
* @fileoverview The U2F api.
|
|
*/
|
|
'use strict';
|
|
if (!window.u2f) {
|
|
|
|
/**
|
|
* Namespace for the U2F api.
|
|
* @type {Object}
|
|
*/
|
|
var u2f = u2f || {};
|
|
|
|
/**
|
|
* FIDO U2F Javascript API Version
|
|
* @number
|
|
*/
|
|
var js_api_version;
|
|
|
|
/**
|
|
* The U2F extension id
|
|
* @const {string}
|
|
*/
|
|
// The Chrome packaged app extension ID.
|
|
// Uncomment this if you want to deploy a server instance that uses
|
|
// the package Chrome app and does not require installing the U2F Chrome extension.
|
|
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
|
|
// The U2F Chrome extension ID.
|
|
// Uncomment this if you want to deploy a server instance that uses
|
|
// the U2F Chrome extension to authenticate.
|
|
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
|
|
|
|
|
|
/**
|
|
* Message types for messsages to/from the extension
|
|
* @const
|
|
* @enum {string}
|
|
*/
|
|
u2f.MessageTypes = {
|
|
'U2F_REGISTER_REQUEST': 'u2f_register_request',
|
|
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
|
|
'U2F_SIGN_REQUEST': 'u2f_sign_request',
|
|
'U2F_SIGN_RESPONSE': 'u2f_sign_response',
|
|
'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
|
|
'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
|
|
};
|
|
|
|
|
|
/**
|
|
* Response status codes
|
|
* @const
|
|
* @enum {number}
|
|
*/
|
|
u2f.ErrorCodes = {
|
|
'OK': 0,
|
|
'OTHER_ERROR': 1,
|
|
'BAD_REQUEST': 2,
|
|
'CONFIGURATION_UNSUPPORTED': 3,
|
|
'DEVICE_INELIGIBLE': 4,
|
|
'TIMEOUT': 5
|
|
};
|
|
|
|
|
|
/**
|
|
* A message for registration requests
|
|
* @typedef {{
|
|
* type: u2f.MessageTypes,
|
|
* appId: ?string,
|
|
* timeoutSeconds: ?number,
|
|
* requestId: ?number
|
|
* }}
|
|
*/
|
|
u2f.U2fRequest;
|
|
|
|
|
|
/**
|
|
* A message for registration responses
|
|
* @typedef {{
|
|
* type: u2f.MessageTypes,
|
|
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
|
|
* requestId: ?number
|
|
* }}
|
|
*/
|
|
u2f.U2fResponse;
|
|
|
|
|
|
/**
|
|
* An error object for responses
|
|
* @typedef {{
|
|
* errorCode: u2f.ErrorCodes,
|
|
* errorMessage: ?string
|
|
* }}
|
|
*/
|
|
u2f.Error;
|
|
|
|
/**
|
|
* Data object for a single sign request.
|
|
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
|
|
*/
|
|
u2f.Transport;
|
|
|
|
|
|
/**
|
|
* Data object for a single sign request.
|
|
* @typedef {Array<u2f.Transport>}
|
|
*/
|
|
u2f.Transports;
|
|
|
|
/**
|
|
* Data object for a single sign request.
|
|
* @typedef {{
|
|
* version: string,
|
|
* challenge: string,
|
|
* keyHandle: string,
|
|
* appId: string
|
|
* }}
|
|
*/
|
|
u2f.SignRequest;
|
|
|
|
|
|
/**
|
|
* Data object for a sign response.
|
|
* @typedef {{
|
|
* keyHandle: string,
|
|
* signatureData: string,
|
|
* clientData: string
|
|
* }}
|
|
*/
|
|
u2f.SignResponse;
|
|
|
|
|
|
/**
|
|
* Data object for a registration request.
|
|
* @typedef {{
|
|
* version: string,
|
|
* challenge: string
|
|
* }}
|
|
*/
|
|
u2f.RegisterRequest;
|
|
|
|
|
|
/**
|
|
* Data object for a registration response.
|
|
* @typedef {{
|
|
* version: string,
|
|
* keyHandle: string,
|
|
* transports: Transports,
|
|
* appId: string
|
|
* }}
|
|
*/
|
|
u2f.RegisterResponse;
|
|
|
|
|
|
/**
|
|
* Data object for a registered key.
|
|
* @typedef {{
|
|
* version: string,
|
|
* keyHandle: string,
|
|
* transports: ?Transports,
|
|
* appId: ?string
|
|
* }}
|
|
*/
|
|
u2f.RegisteredKey;
|
|
|
|
|
|
/**
|
|
* Data object for a get API register response.
|
|
* @typedef {{
|
|
* js_api_version: number
|
|
* }}
|
|
*/
|
|
u2f.GetJsApiVersionResponse;
|
|
|
|
|
|
//Low level MessagePort API support
|
|
|
|
/**
|
|
* Sets up a MessagePort to the U2F extension using the
|
|
* available mechanisms.
|
|
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
|
|
*/
|
|
u2f.getMessagePort = function (callback) {
|
|
if (typeof chrome != 'undefined' && chrome.runtime) {
|
|
// The actual message here does not matter, but we need to get a reply
|
|
// for the callback to run. Thus, send an empty signature request
|
|
// in order to get a failure response.
|
|
var msg = {
|
|
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
|
signRequests: []
|
|
};
|
|
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function () {
|
|
if (!chrome.runtime.lastError) {
|
|
// We are on a whitelisted origin and can talk directly
|
|
// with the extension.
|
|
u2f.getChromeRuntimePort_(callback);
|
|
} else {
|
|
// chrome.runtime was available, but we couldn't message
|
|
// the extension directly, use iframe
|
|
u2f.getIframePort_(callback);
|
|
}
|
|
});
|
|
} else if (u2f.isAndroidChrome_()) {
|
|
u2f.getAuthenticatorPort_(callback);
|
|
} else if (u2f.isIosChrome_()) {
|
|
u2f.getIosPort_(callback);
|
|
} else {
|
|
// chrome.runtime was not available at all, which is normal
|
|
// when this origin doesn't have access to any extensions.
|
|
u2f.getIframePort_(callback);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Detect chrome running on android based on the browser's useragent.
|
|
* @private
|
|
*/
|
|
u2f.isAndroidChrome_ = function () {
|
|
var userAgent = navigator.userAgent;
|
|
return userAgent.indexOf('Chrome') != -1 &&
|
|
userAgent.indexOf('Android') != -1;
|
|
};
|
|
|
|
/**
|
|
* Detect chrome running on iOS based on the browser's platform.
|
|
* @private
|
|
*/
|
|
u2f.isIosChrome_ = function () {
|
|
var r = ["iPhone", "iPad", "iPod"];
|
|
for (var i in r) { if (navigator.platform == r[i]) { return true; } }
|
|
return false;
|
|
//return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1;
|
|
};
|
|
|
|
/**
|
|
* Connects directly to the extension via chrome.runtime.connect.
|
|
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
|
|
* @private
|
|
*/
|
|
u2f.getChromeRuntimePort_ = function (callback) {
|
|
var port = chrome.runtime.connect(u2f.EXTENSION_ID,
|
|
{ 'includeTlsChannelId': true });
|
|
setTimeout(function () {
|
|
callback(new u2f.WrappedChromeRuntimePort_(port));
|
|
}, 0);
|
|
};
|
|
|
|
/**
|
|
* Return a 'port' abstraction to the Authenticator app.
|
|
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
|
|
* @private
|
|
*/
|
|
u2f.getAuthenticatorPort_ = function (callback) {
|
|
setTimeout(function () {
|
|
callback(new u2f.WrappedAuthenticatorPort_());
|
|
}, 0);
|
|
};
|
|
|
|
/**
|
|
* Return a 'port' abstraction to the iOS client app.
|
|
* @param {function(u2f.WrappedIosPort_)} callback
|
|
* @private
|
|
*/
|
|
u2f.getIosPort_ = function (callback) {
|
|
setTimeout(function () {
|
|
callback(new u2f.WrappedIosPort_());
|
|
}, 0);
|
|
};
|
|
|
|
/**
|
|
* A wrapper for chrome.runtime.Port that is compatible with MessagePort.
|
|
* @param {Port} port
|
|
* @constructor
|
|
* @private
|
|
*/
|
|
u2f.WrappedChromeRuntimePort_ = function (port) {
|
|
this.port_ = port;
|
|
};
|
|
|
|
/**
|
|
* Format and return a sign request compliant with the JS API version supported by the extension.
|
|
* @param {Array<u2f.SignRequest>} signRequests
|
|
* @param {number} timeoutSeconds
|
|
* @param {number} reqId
|
|
* @return {Object}
|
|
*/
|
|
u2f.formatSignRequest_ =
|
|
function (appId, challenge, registeredKeys, timeoutSeconds, reqId) {
|
|
if (js_api_version === undefined || js_api_version < 1.1) {
|
|
// Adapt request to the 1.0 JS API
|
|
var signRequests = [];
|
|
for (var i = 0; i < registeredKeys.length; i++) {
|
|
signRequests[i] = {
|
|
version: registeredKeys[i].version,
|
|
challenge: challenge,
|
|
keyHandle: registeredKeys[i].keyHandle,
|
|
appId: appId
|
|
};
|
|
}
|
|
return {
|
|
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
|
signRequests: signRequests,
|
|
timeoutSeconds: timeoutSeconds,
|
|
requestId: reqId
|
|
};
|
|
}
|
|
// JS 1.1 API
|
|
return {
|
|
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
|
appId: appId,
|
|
challenge: challenge,
|
|
registeredKeys: registeredKeys,
|
|
timeoutSeconds: timeoutSeconds,
|
|
requestId: reqId
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Format and return a register request compliant with the JS API version supported by the extension..
|
|
* @param {Array<u2f.SignRequest>} signRequests
|
|
* @param {Array<u2f.RegisterRequest>} signRequests
|
|
* @param {number} timeoutSeconds
|
|
* @param {number} reqId
|
|
* @return {Object}
|
|
*/
|
|
u2f.formatRegisterRequest_ =
|
|
function (appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
|
|
if (js_api_version === undefined || js_api_version < 1.1) {
|
|
// Adapt request to the 1.0 JS API
|
|
for (var i = 0; i < registerRequests.length; i++) {
|
|
registerRequests[i].appId = appId;
|
|
}
|
|
var signRequests = [];
|
|
for (var i = 0; i < registeredKeys.length; i++) {
|
|
signRequests[i] = {
|
|
version: registeredKeys[i].version,
|
|
challenge: registerRequests[0],
|
|
keyHandle: registeredKeys[i].keyHandle,
|
|
appId: appId
|
|
};
|
|
}
|
|
return {
|
|
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
|
|
signRequests: signRequests,
|
|
registerRequests: registerRequests,
|
|
timeoutSeconds: timeoutSeconds,
|
|
requestId: reqId
|
|
};
|
|
}
|
|
// JS 1.1 API
|
|
return {
|
|
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
|
|
appId: appId,
|
|
registerRequests: registerRequests,
|
|
registeredKeys: registeredKeys,
|
|
timeoutSeconds: timeoutSeconds,
|
|
requestId: reqId
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
* Posts a message on the underlying channel.
|
|
* @param {Object} message
|
|
*/
|
|
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function (message) {
|
|
this.port_.postMessage(message);
|
|
};
|
|
|
|
|
|
/**
|
|
* Emulates the HTML 5 addEventListener interface. Works only for the
|
|
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
|
|
* @param {string} eventName
|
|
* @param {function({data: Object})} handler
|
|
*/
|
|
u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
|
|
function (eventName, handler) {
|
|
var name = eventName.toLowerCase();
|
|
if (name == 'message' || name == 'onmessage') {
|
|
this.port_.onMessage.addListener(function (message) {
|
|
// Emulate a minimal MessageEvent object
|
|
handler({ 'data': message });
|
|
});
|
|
} else {
|
|
console.error('WrappedChromeRuntimePort only supports onMessage');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Wrap the Authenticator app with a MessagePort interface.
|
|
* @constructor
|
|
* @private
|
|
*/
|
|
u2f.WrappedAuthenticatorPort_ = function () {
|
|
this.requestId_ = -1;
|
|
this.requestObject_ = null;
|
|
}
|
|
|
|
/**
|
|
* Launch the Authenticator intent.
|
|
* @param {Object} message
|
|
*/
|
|
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) {
|
|
var intentUrl =
|
|
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
|
|
';S.request=' + encodeURIComponent(JSON.stringify(message)) +
|
|
';end';
|
|
document.location = intentUrl;
|
|
};
|
|
|
|
/**
|
|
* Tells what type of port this is.
|
|
* @return {String} port type
|
|
*/
|
|
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () {
|
|
return "WrappedAuthenticatorPort_";
|
|
};
|
|
|
|
|
|
/**
|
|
* Emulates the HTML 5 addEventListener interface.
|
|
* @param {string} eventName
|
|
* @param {function({data: Object})} handler
|
|
*/
|
|
u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function (eventName, handler) {
|
|
var name = eventName.toLowerCase();
|
|
if (name == 'message') {
|
|
var self = this;
|
|
/* Register a callback to that executes when
|
|
* chrome injects the response. */
|
|
window.addEventListener(
|
|
'message', self.onRequestUpdate_.bind(self, handler), false);
|
|
} else {
|
|
console.error('WrappedAuthenticatorPort only supports message');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Callback invoked when a response is received from the Authenticator.
|
|
* @param function({data: Object}) callback
|
|
* @param {Object} message message Object
|
|
*/
|
|
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
|
|
function (callback, message) {
|
|
var messageObject = JSON.parse(message.data);
|
|
var intentUrl = messageObject['intentURL'];
|
|
|
|
var errorCode = messageObject['errorCode'];
|
|
var responseObject = null;
|
|
if (messageObject.hasOwnProperty('data')) {
|
|
responseObject = /** @type {Object} */ (
|
|
JSON.parse(messageObject['data']));
|
|
}
|
|
|
|
callback({ 'data': responseObject });
|
|
};
|
|
|
|
/**
|
|
* Base URL for intents to Authenticator.
|
|
* @const
|
|
* @private
|
|
*/
|
|
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
|
|
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
|
|
|
|
/**
|
|
* Wrap the iOS client app with a MessagePort interface.
|
|
* @constructor
|
|
* @private
|
|
*/
|
|
u2f.WrappedIosPort_ = function () { };
|
|
|
|
/**
|
|
* Launch the iOS client app request
|
|
* @param {Object} message
|
|
*/
|
|
u2f.WrappedIosPort_.prototype.postMessage = function (message) {
|
|
var str = JSON.stringify(message);
|
|
var url = "u2f://auth?" + encodeURI(str);
|
|
location.replace(url);
|
|
};
|
|
|
|
/**
|
|
* Tells what type of port this is.
|
|
* @return {String} port type
|
|
*/
|
|
u2f.WrappedIosPort_.prototype.getPortType = function () {
|
|
return "WrappedIosPort_";
|
|
};
|
|
|
|
/**
|
|
* Emulates the HTML 5 addEventListener interface.
|
|
* @param {string} eventName
|
|
* @param {function({data: Object})} handler
|
|
*/
|
|
u2f.WrappedIosPort_.prototype.addEventListener = function (eventName, handler) {
|
|
var name = eventName.toLowerCase();
|
|
if (name !== 'message') {
|
|
console.error('WrappedIosPort only supports message');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sets up an embedded trampoline iframe, sourced from the extension.
|
|
* @param {function(MessagePort)} callback
|
|
* @private
|
|
*/
|
|
u2f.getIframePort_ = function (callback) {
|
|
// Create the iframe
|
|
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
|
|
var iframe = document.createElement('iframe');
|
|
iframe.src = iframeOrigin + '/u2f-comms.html';
|
|
iframe.setAttribute('style', 'display:none');
|
|
document.body.appendChild(iframe);
|
|
|
|
var channel = new MessageChannel();
|
|
var ready = function (message) {
|
|
if (message.data == 'ready') {
|
|
channel.port1.removeEventListener('message', ready);
|
|
callback(channel.port1);
|
|
} else {
|
|
console.error('First event on iframe port was not "ready"');
|
|
}
|
|
};
|
|
channel.port1.addEventListener('message', ready);
|
|
channel.port1.start();
|
|
|
|
iframe.addEventListener('load', function () {
|
|
// Deliver the port to the iframe and initialize
|
|
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
|
|
});
|
|
};
|
|
|
|
|
|
//High-level JS API
|
|
|
|
/**
|
|
* Default extension response timeout in seconds.
|
|
* @const
|
|
*/
|
|
u2f.EXTENSION_TIMEOUT_SEC = 30;
|
|
|
|
/**
|
|
* A singleton instance for a MessagePort to the extension.
|
|
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
|
|
* @private
|
|
*/
|
|
u2f.port_ = null;
|
|
|
|
/**
|
|
* Callbacks waiting for a port
|
|
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
|
|
* @private
|
|
*/
|
|
u2f.waitingForPort_ = [];
|
|
|
|
/**
|
|
* A counter for requestIds.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
u2f.reqCounter_ = 0;
|
|
|
|
/**
|
|
* A map from requestIds to client callbacks
|
|
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
|
|
* |function((u2f.Error|u2f.SignResponse)))>}
|
|
* @private
|
|
*/
|
|
u2f.callbackMap_ = {};
|
|
|
|
/**
|
|
* Creates or retrieves the MessagePort singleton to use.
|
|
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
|
|
* @private
|
|
*/
|
|
u2f.getPortSingleton_ = function (callback) {
|
|
if (u2f.port_) {
|
|
callback(u2f.port_);
|
|
} else {
|
|
if (u2f.waitingForPort_.length == 0) {
|
|
u2f.getMessagePort(function (port) {
|
|
u2f.port_ = port;
|
|
u2f.port_.addEventListener('message',
|
|
/** @type {function(Event)} */ (u2f.responseHandler_));
|
|
|
|
// Careful, here be async callbacks. Maybe.
|
|
while (u2f.waitingForPort_.length)
|
|
u2f.waitingForPort_.shift()(u2f.port_);
|
|
});
|
|
}
|
|
u2f.waitingForPort_.push(callback);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handles response messages from the extension.
|
|
* @param {MessageEvent.<u2f.Response>} message
|
|
* @private
|
|
*/
|
|
u2f.responseHandler_ = function (message) {
|
|
var response = message.data;
|
|
var reqId = response['requestId'];
|
|
if (!reqId || !u2f.callbackMap_[reqId]) {
|
|
console.error('Unknown or missing requestId in response.');
|
|
return;
|
|
}
|
|
var cb = u2f.callbackMap_[reqId];
|
|
delete u2f.callbackMap_[reqId];
|
|
cb(response['responseData']);
|
|
};
|
|
|
|
/**
|
|
* Dispatches an array of sign requests to available U2F tokens.
|
|
* If the JS API version supported by the extension is unknown, it first sends a
|
|
* message to the extension to find out the supported API version and then it sends
|
|
* the sign request.
|
|
* @param {string=} appId
|
|
* @param {string=} challenge
|
|
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
|
* @param {function((u2f.Error|u2f.SignResponse))} callback
|
|
* @param {number=} opt_timeoutSeconds
|
|
*/
|
|
u2f.sign = function (appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
|
|
if (js_api_version === undefined) {
|
|
// Send a message to get the extension to JS API version, then send the actual sign request.
|
|
u2f.getApiVersion(
|
|
function (response) {
|
|
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
|
|
//console.log("Extension JS API Version: ", js_api_version);
|
|
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
|
|
});
|
|
} else {
|
|
// We know the JS API version. Send the actual sign request in the supported API version.
|
|
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Dispatches an array of sign requests to available U2F tokens.
|
|
* @param {string=} appId
|
|
* @param {string=} challenge
|
|
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
|
* @param {function((u2f.Error|u2f.SignResponse))} callback
|
|
* @param {number=} opt_timeoutSeconds
|
|
*/
|
|
u2f.sendSignRequest = function (appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
|
|
u2f.getPortSingleton_(function (port) {
|
|
var reqId = ++u2f.reqCounter_;
|
|
u2f.callbackMap_[reqId] = callback;
|
|
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
|
|
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
|
|
var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
|
|
port.postMessage(req);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Dispatches register requests to available U2F tokens. An array of sign
|
|
* requests identifies already registered tokens.
|
|
* If the JS API version supported by the extension is unknown, it first sends a
|
|
* message to the extension to find out the supported API version and then it sends
|
|
* the register request.
|
|
* @param {string=} appId
|
|
* @param {Array<u2f.RegisterRequest>} registerRequests
|
|
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
|
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
|
|
* @param {number=} opt_timeoutSeconds
|
|
*/
|
|
u2f.register = function (appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
|
|
if (js_api_version === undefined) {
|
|
// Send a message to get the extension to JS API version, then send the actual register request.
|
|
u2f.getApiVersion(
|
|
function (response) {
|
|
js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
|
|
//console.log("Extension JS API Version: ", js_api_version);
|
|
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
|
|
callback, opt_timeoutSeconds);
|
|
});
|
|
} else {
|
|
// We know the JS API version. Send the actual register request in the supported API version.
|
|
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
|
|
callback, opt_timeoutSeconds);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Dispatches register requests to available U2F tokens. An array of sign
|
|
* requests identifies already registered tokens.
|
|
* @param {string=} appId
|
|
* @param {Array<u2f.RegisterRequest>} registerRequests
|
|
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
|
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
|
|
* @param {number=} opt_timeoutSeconds
|
|
*/
|
|
u2f.sendRegisterRequest = function (appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
|
|
u2f.getPortSingleton_(function (port) {
|
|
var reqId = ++u2f.reqCounter_;
|
|
u2f.callbackMap_[reqId] = callback;
|
|
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
|
|
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
|
|
var req = u2f.formatRegisterRequest_(
|
|
appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
|
|
port.postMessage(req);
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Dispatches a message to the extension to find out the supported
|
|
* JS API version.
|
|
* If the user is on a mobile phone and is thus using Google Authenticator instead
|
|
* of the Chrome extension, don't send the request and simply return 0.
|
|
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
|
|
* @param {number=} opt_timeoutSeconds
|
|
*/
|
|
u2f.getApiVersion = function (callback, opt_timeoutSeconds) {
|
|
u2f.getPortSingleton_(function (port) {
|
|
// If we are using Android Google Authenticator or iOS client app,
|
|
// do not fire an intent to ask which JS API version to use.
|
|
if (port.getPortType) {
|
|
var apiVersion;
|
|
switch (port.getPortType()) {
|
|
case 'WrappedIosPort_':
|
|
case 'WrappedAuthenticatorPort_':
|
|
apiVersion = 1.1;
|
|
break;
|
|
|
|
default:
|
|
apiVersion = 0;
|
|
break;
|
|
}
|
|
callback({ 'js_api_version': apiVersion });
|
|
return;
|
|
}
|
|
var reqId = ++u2f.reqCounter_;
|
|
u2f.callbackMap_[reqId] = callback;
|
|
var req = {
|
|
type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
|
|
timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
|
|
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
|
|
requestId: reqId
|
|
};
|
|
port.postMessage(req);
|
|
});
|
|
};
|
|
|
|
}
|
|
'use strict';
|
|
var passhint = "{{{passhint}}}";
|
|
var newAccountPass = parseInt('{{{newAccountPass}}}');
|
|
var emailCheck = ('{{{emailcheck}}}' == 'true');
|
|
var features = parseInt('{{{features}}}');
|
|
var passRequirements = "{{{passRequirements}}}";
|
|
if (passRequirements != "") { passRequirements = JSON.parse(decodeURIComponent(passRequirements)); } else { passRequirements = {}; }
|
|
var passRequirementsEx = ((passRequirements.min != null) || (passRequirements.max != null) || (passRequirements.upper != null) || (passRequirements.lower != null) || (passRequirements.numeric != null) || (passRequirements.nonalpha != null));
|
|
var hardwareKeyChallenge = '{{{hkey}}}';
|
|
var currentpanel = 0;
|
|
|
|
function startup() {
|
|
if ((features & 32) == 0) {
|
|
// Guard against other site's top frames (web bugs).
|
|
var loc = null;
|
|
try { loc = top.location.toString().toLowerCase(); } catch (e) { }
|
|
if (top != self && (loc == null || top.active == false)) { top.location = self.location; return; }
|
|
}
|
|
|
|
QV('createPanelHint', passRequirements.hint === true);
|
|
QV('resetpasswordpanelHint', passRequirements.hint === true);
|
|
|
|
window.onresize = center;
|
|
center();
|
|
validateLogin();
|
|
validateCreate();
|
|
if ('{{loginmode}}' != '') { go(parseInt('{{loginmode}}')); } else { go(1); }
|
|
QV('newAccountDiv', ('{{{newAccount}}}' != '0') && ('{{{newAccount}}}' != 'false')); // If new accounts are not allowed, don't display the new account link.
|
|
if ((passRequirements.hint === true) && (passhint != null) && (passhint.length > 0)) { QV("showPassHintLink", true); }
|
|
QV("newAccountPass", (newAccountPass == 1));
|
|
QV("resetAccountDiv", (emailCheck == true));
|
|
QV("hrAccountDiv", (emailCheck == true) || (newAccountPass == 1));
|
|
|
|
if ('{{loginmode}}' == '4') {
|
|
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
|
|
if ((hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')) {
|
|
hardwareKeyChallenge.challenge = Uint8Array.from(atob(hardwareKeyChallenge.challenge), c => c.charCodeAt(0)).buffer;
|
|
|
|
const publicKeyCredentialRequestOptions = { challenge: hardwareKeyChallenge.challenge, allowCredentials: [], timeout: hardwareKeyChallenge.timeout }
|
|
for (var i = 0; i < hardwareKeyChallenge.keyIds.length; i++) {
|
|
publicKeyCredentialRequestOptions.allowCredentials.push(
|
|
{ id: Uint8Array.from(atob(hardwareKeyChallenge.keyIds[i]), c => c.charCodeAt(0)), type: 'public-key', transports: ['usb', 'ble', 'nfc'], }
|
|
);
|
|
}
|
|
|
|
// New WebAuthn hardware keys
|
|
navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions }).then(
|
|
function (rawAssertion) {
|
|
var assertion = {
|
|
id: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.rawId))),
|
|
clientDataJSON: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.clientDataJSON))),
|
|
userHandle: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.userHandle))),
|
|
signature: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.signature))),
|
|
authenticatorData: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.authenticatorData))),
|
|
};
|
|
Q('hwtokenInput').value = JSON.stringify(assertion);
|
|
QE('tokenOkButton', true);
|
|
Q('tokenOkButton').click();
|
|
},
|
|
function (error) { console.log('credentials-get error', error); }
|
|
);
|
|
} else if ((hardwareKeyChallenge != null) && u2fSupported()) {
|
|
// Old U2F hardware keys
|
|
window.u2f.sign(hardwareKeyChallenge.appId, hardwareKeyChallenge.challenge, hardwareKeyChallenge.registeredKeys, function (authResponse) {
|
|
if ((currentpanel == 4) && authResponse.signatureData) {
|
|
Q('hwtokenInput').value = JSON.stringify(authResponse);
|
|
QE('tokenOkButton', true);
|
|
Q('tokenOkButton').click();
|
|
}
|
|
}, hardwareKeyChallenge.timeoutSeconds);
|
|
}
|
|
}
|
|
|
|
if ('{{loginmode}}' == '5') {
|
|
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
|
|
if ((hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')) {
|
|
hardwareKeyChallenge.challenge = Uint8Array.from(atob(hardwareKeyChallenge.challenge), c => c.charCodeAt(0)).buffer;
|
|
|
|
const publicKeyCredentialRequestOptions = { challenge: hardwareKeyChallenge.challenge, allowCredentials: [], timeout: hardwareKeyChallenge.timeout }
|
|
for (var i = 0; i < hardwareKeyChallenge.keyIds.length; i++) {
|
|
publicKeyCredentialRequestOptions.allowCredentials.push(
|
|
{ id: Uint8Array.from(atob(hardwareKeyChallenge.keyIds[i]), c => c.charCodeAt(0)), type: 'public-key', transports: ['usb', 'ble', 'nfc'], }
|
|
);
|
|
}
|
|
|
|
// New WebAuthn hardware keys
|
|
navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions }).then(
|
|
function (rawAssertion) {
|
|
var assertion = {
|
|
id: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.rawId))),
|
|
clientDataJSON: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.clientDataJSON))),
|
|
userHandle: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.userHandle))),
|
|
signature: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.signature))),
|
|
authenticatorData: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.authenticatorData))),
|
|
};
|
|
Q('resetHwtokenInput').value = JSON.stringify(assertion);
|
|
QE('resetTokenOkButton', true);
|
|
Q('resetTokenOkButton').click();
|
|
},
|
|
function (error) { console.log('credentials-get error', error); }
|
|
);
|
|
} else if ((hardwareKeyChallenge != null) && u2fSupported()) {
|
|
// Old U2F hardware keys
|
|
window.u2f.sign(hardwareKeyChallenge.appId, hardwareKeyChallenge.challenge, hardwareKeyChallenge.registeredKeys, function (authResponse) {
|
|
if ((currentpanel == 5) && authResponse.signatureData) {
|
|
Q('resetHwtokenInput').value = JSON.stringify(authResponse);
|
|
QE('resetTokenOkButton', true);
|
|
Q('resetTokenOkButton').click();
|
|
}
|
|
}, hardwareKeyChallenge.timeoutSeconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
function showPassHint() {
|
|
if (passRequirements.hint === true) { messagebox("Password Hint", passhint); }
|
|
}
|
|
|
|
function xgo(x) {
|
|
QV('message1', false);
|
|
QV('message2', false);
|
|
QV('message3', false);
|
|
QV('message4', false);
|
|
QV('message5', false);
|
|
QV('message6', false);
|
|
go(x);
|
|
}
|
|
|
|
function go(x) {
|
|
currentpanel = x;
|
|
setDialogMode(0);
|
|
QV("showPassHintLink", false);
|
|
QV('loginpanel', x == 1);
|
|
QV('createpanel', x == 2);
|
|
QV('resetpanel', x == 3);
|
|
QV('tokenpanel', x == 4);
|
|
QV('resettokenpanel', x == 5);
|
|
QV('resetpasswordpanel', x == 6);
|
|
if (x == 1) { Q('username').focus(); }
|
|
if (x == 2) { Q('ausername').focus(); }
|
|
if (x == 3) { Q('remail').focus(); }
|
|
if (x == 4) { Q('tokenInput').focus(); }
|
|
if (x == 5) { Q('resetTokenInput').focus(); }
|
|
if (x == 6) { Q('rapassword1').focus(); }
|
|
}
|
|
|
|
function validateLogin(box, e) {
|
|
var ok = ((Q('username').value.length > 0) && (Q('username').value.indexOf(' ') == -1) && (Q('password').value.length > 0));
|
|
QE('loginButton', ok);
|
|
setDialogMode(0);
|
|
if ((e != null) && (e.keyCode == 13)) { if (box == 1) { Q('password').focus(); } else if (box == 2) { Q('loginButton').click(); } }
|
|
if (e != null) { haltEvent(e); }
|
|
}
|
|
|
|
function validateCreate(box,e) {
|
|
setDialogMode(0);
|
|
var ok = ((Q('ausername').value.length > 0) && (Q('ausername').value.indexOf(' ') == -1) && (validateEmail(Q('aemail').value) == true) && (Q('apassword1').value.length > 0) && (Q('apassword2').value == Q('apassword1').value));
|
|
if ((newAccountPass == 1) && (Q('anewaccountpass').value.length == 0)) { ok = false; }
|
|
if (Q('apassword1').value == '') {
|
|
QH('passWarning', '');
|
|
QV('passwordPolicyCallout', false);
|
|
} else {
|
|
if (!passRequirementsEx) {
|
|
// No password requirements, display password strength
|
|
var passStrength = checkPasswordStrength(Q('apassword1').value);
|
|
if (passStrength >= 80) { QH('passWarning', '<span style=color:green><b>Strong Password</b><span>'); }
|
|
else if (passStrength >= 60) { QH('passWarning', '<span style=color:blue><b>Good Password</b><span>'); }
|
|
else { QH('passWarning', '<span style=color:red><b>Weak Password</b><span>'); }
|
|
} else {
|
|
// Password requirements provided, use that
|
|
var passReq = checkPasswordRequirements(Q('apassword1').value, passRequirements);
|
|
if (passReq == false) {
|
|
ok = false;
|
|
//QS('nuPass1').color = '#7b241c';
|
|
//QS('nuPass2').color = '#7b241c';
|
|
QH('passWarning', '<span style=color:red><b>Password Policy</b><span>'); // TODO: Display problem hint
|
|
QV('passwordPolicyCallout', true);
|
|
QH('passwordPolicyCallout', passwordPolicyText(Q('apassword1').value));
|
|
} else {
|
|
QH('passWarning', '');
|
|
QV('passwordPolicyCallout', false);
|
|
}
|
|
}
|
|
}
|
|
QE('createButton', ok);
|
|
if ((e != null) && (e.keyCode == 13)) {
|
|
if (box == 1) { Q('aemail').focus(); }
|
|
if (box == 2) { Q('apassword1').focus(); }
|
|
if (box == 3) { Q('apassword2').focus(); }
|
|
if (box == 4) { Q('apasswordhint').focus(); }
|
|
if (box == 5) { if (newAccountPass == 1) { Q('anewaccountpass').focus(); } else { Q('createButton').click(); } }
|
|
if (box == 6) { Q('createButton').click(); }
|
|
}
|
|
if (e != null) { haltEvent(e); }
|
|
}
|
|
|
|
function validatePassReset(box, e) {
|
|
setDialogMode(0);
|
|
var pass1ok = (Q('rapassword1').value.length > 0);
|
|
var pass2ok = (Q('rapassword2').value.length > 0) && (Q('rapassword2').value == Q('rapassword1').value);
|
|
var ok = (pass1ok && pass2ok);
|
|
|
|
// Color the fields
|
|
QS('rnuPass1').color = pass1ok ? 'black' : '#7b241c';
|
|
QS('rnuPass2').color = pass2ok ? 'black' : '#7b241c';
|
|
|
|
if (Q('rapassword1').value == '') {
|
|
QH('rpassWarning', '');
|
|
QV('rpasswordPolicyCallout', false);
|
|
} else {
|
|
if (!passRequirementsEx) {
|
|
// No password requirements, display password strength
|
|
var passStrength = checkPasswordStrength(Q('rapassword1').value);
|
|
if (passStrength >= 80) { QH('rpassWarning', '<span style=color:green><b>Strong Password</b><span>'); }
|
|
else if (passStrength >= 60) { QH('rpassWarning', '<span style=color:blue><b>Good Password</b><span>'); }
|
|
else { QH('rpassWarning', '<span style=color:red><b>Weak Password</b><span>'); }
|
|
} else {
|
|
// Password requirements provided, use that
|
|
var passReq = checkPasswordRequirements(Q('rapassword1').value, passRequirements);
|
|
if (passReq == false) {
|
|
ok = false;
|
|
QS('rnuPass1').color = '#7b241c';
|
|
QS('rnuPass2').color = '#7b241c';
|
|
QH('rpassWarning', '<div style=color:red;cursor:pointer onclick=showPasswordPolicy()><b>Password Policy</b><div>'); // This is also a link to the password policy
|
|
QV('rpasswordPolicyCallout', true);
|
|
QH('rpasswordPolicyCallout', passwordPolicyText(Q('rapassword1').value));
|
|
} else {
|
|
QH('rpassWarning', '');
|
|
QV('rpasswordPolicyCallout', false);
|
|
}
|
|
}
|
|
}
|
|
if ((e != null) && (e.keyCode == 13)) {
|
|
if (box == 2) { Q('rapassword1').focus(); }
|
|
if (box == 3) { Q('rapassword2').focus(); }
|
|
if (box == 4) { Q('rapasswordhint').focus(); }
|
|
if (box == 6) { Q('resetPassButton').click(); }
|
|
}
|
|
if (e != null) { haltEvent(e); }
|
|
QE('resetPassButton', ok);
|
|
}
|
|
|
|
function validateReset(e) {
|
|
setDialogMode(0);
|
|
var x = validateEmail(Q('remail').value);
|
|
QE('eresetButton', x);
|
|
if ((e != null) && (e.keyCode == 13) && (x == true)) {
|
|
Q('eresetButton').click();
|
|
}
|
|
if (e != null) { haltEvent(e); }
|
|
}
|
|
|
|
function passwordPolicyText(pass) {
|
|
var policy = '<div style=text-align:left>';
|
|
var counts = strCount(pass);
|
|
if (passRequirements.min && ((pass == null) || (pass.length < passRequirements.min))) { policy += 'Minimum length of ' + passRequirements.min + '<br />'; }
|
|
if (passRequirements.max && ((pass == null) || (pass.length > passRequirements.max))) { policy += 'Maximum length of ' + passRequirements.max + '<br />'; }
|
|
if (passRequirements.upper && ((pass == null) || (counts.upper < passRequirements.upper))) { policy += '' + passRequirements.upper + ' upper case<br />'; }
|
|
if (passRequirements.lower && ((pass == null) || (counts.lower < passRequirements.lower))) { policy += '' + passRequirements.lower + ' lower case<br />'; }
|
|
if (passRequirements.numeric && ((pass == null) || (counts.numeric < passRequirements.numeric))) { policy += '' + passRequirements.numeric + ' numeric<br />'; }
|
|
if (passRequirements.nonalpha && ((pass == null) || (counts.nonalpha < passRequirements.nonalpha))) { policy += passRequirements.nonalpha + ' non-alphanumeric<br />'; }
|
|
policy += '</div>';
|
|
return policy;
|
|
}
|
|
|
|
// Return a password strength score
|
|
function checkPasswordStrength(password) {
|
|
var r = 0, letters = {}, varCount = 0, variations = { digits: /\d/.test(password), lower: /[a-z]/.test(password), upper: /[A-Z]/.test(password), nonWords: /\W/.test(password) }
|
|
if (!password) return 0;
|
|
for (var i = 0; i< password.length; i++) { letters[password[i]] = (letters[password[i]] || 0) + 1; r += 5.0 / letters[password[i]]; }
|
|
for (var c in variations) { varCount += (variations[c] == true) ? 1 : 0; }
|
|
return parseInt(r + (varCount - 1) * 10);
|
|
}
|
|
|
|
// Check password requirements
|
|
function checkPasswordRequirements(password, requirements) {
|
|
if ((requirements == null) || (requirements == '') || (typeof requirements != 'object')) return true;
|
|
if (requirements.min) { if (password.length < requirements.min) return false; }
|
|
if (requirements.max) { if (password.length > requirements.max) return false; }
|
|
var counts = strCount(password);
|
|
if (requirements.numeric && (counts.numeric < requirements.numeric)) return false;
|
|
if (requirements.lower && (counts.lower < requirements.lower)) return false;
|
|
if (requirements.upper && (counts.upper < requirements.upper)) return false;
|
|
if (requirements.nonalpha && (counts.nonalpha < requirements.nonalpha)) return false;
|
|
return true;
|
|
}
|
|
|
|
function strCount(password) {
|
|
var counts = { numeric: 0, lower: 0, upper: 0, nonalpha: 0 };
|
|
if (typeof password != 'string') return counts;
|
|
for (var i = 0; i < password.length; i++) {
|
|
if (/\d/.test(password[i])) { counts.numeric++; }
|
|
if (/[a-z]/.test(password[i])) { counts.lower++; }
|
|
if (/[A-Z]/.test(password[i])) { counts.upper++; }
|
|
if (/\W/.test(password[i])) { counts.nonalpha++; }
|
|
}
|
|
return counts;
|
|
}
|
|
|
|
var xcheckTokenTimer = null;
|
|
function checkTokenTimer(enter) {
|
|
if ((enter == 0) && (xcheckTokenTimer != null)) { clearInterval(xcheckTokenTimer); xcheckTokenTimer = null; }
|
|
if ((enter == 1) && (xcheckTokenTimer == null)) { xcheckTokenTimer = setInterval(checkToken, 200); }
|
|
}
|
|
|
|
function checkToken() {
|
|
var t1 = Q('tokenInput').value, t2 = t1.split(' ').join('');
|
|
if (t1 != t2) { Q('tokenInput').value = t2; }
|
|
QE('tokenOkButton', (Q('tokenInput').value.length == 6) || (Q('tokenInput').value.length == 8) || (Q('tokenInput').value.length == 44));
|
|
}
|
|
|
|
function resetCheckToken() {
|
|
var t1 = Q('resetTokenInput').value, t2 = t1.split(' ').join('');
|
|
if (t1 != t2) { Q('resetTokenInput').value = t2; }
|
|
QE('resetTokenOkButton', (Q('resetTokenInput').value.length == 6) || (Q('resetTokenInput').value.length == 8) || (Q('resetTokenInput').value.length == 44));
|
|
}
|
|
|
|
//
|
|
// POPUP DIALOG
|
|
//
|
|
|
|
// undefined = Hidden, 1 = Generic Message
|
|
var xxdialogMode;
|
|
var xxdialogFunc;
|
|
var xxdialogButtons;
|
|
var xxdialogTag;
|
|
var xxcurrentView = 0;
|
|
|
|
// Display a dialog box
|
|
// Parameters: Dialog Mode (0 = none), Dialog Title, Buttons (1 = OK, 2 = Cancel, 3 = OK & Cancel), Call back function(0 = Cancel, 1 = OK), Dialog Content (Mode 2 only)
|
|
function setDialogMode(x, y, b, f, c, tag) {
|
|
xxdialogMode = x;
|
|
xxdialogFunc = f;
|
|
xxdialogButtons = b;
|
|
xxdialogTag = tag;
|
|
QE('idx_dlgOkButton', true);
|
|
QV('idx_dlgOkButton', b & 1);
|
|
QV('idx_dlgCancelButton', b & 2);
|
|
QV('id_dialogclose', (b & 2) || (b & 8));
|
|
QV('idx_dlgButtonBar', b & 7);
|
|
if (y) QH('id_dialogtitle', y);
|
|
for (var i = 1; i < 24; i++) { QV('dialog' + i, i == x); } // Edit this line when more dialogs are added
|
|
QV('dialog', x);
|
|
if (c) { if (x == 2) { QH('id_dialogOptions', c); } else { QH('id_dialogMessage', c); } }
|
|
}
|
|
|
|
function dialogclose(x) {
|
|
var f = xxdialogFunc;
|
|
var b = xxdialogButtons;
|
|
var t = xxdialogTag;
|
|
setDialogMode();
|
|
if (((b & 8) || x) && f) f(x, t);
|
|
}
|
|
|
|
function center() { QS('dialog').left = ((((getDocWidth() - 400) / 2)) + "px"); }
|
|
function messagebox(t, m) { QH('id_dialogMessage', m); setDialogMode(1, t, 1); }
|
|
function statusbox(t, m) { QH('id_dialogMessage', m); setDialogMode(1, t); }
|
|
function getDocWidth() { if (window.innerWidth) return window.innerWidth; if (document.documentElement && document.documentElement.clientWidth && document.documentElement.clientWidth != 0) return document.documentElement.clientWidth; return document.getElementsByTagName('body')[0].clientWidth; }
|
|
function haltEvent(e) { if (e.preventDefault) e.preventDefault(); if (e.stopPropagation) e.stopPropagation(); return false; }
|
|
function haltReturn(e) { if (e.keyCode == 13) { haltEvent(e); } }
|
|
function validateEmail(v) { var emailReg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return emailReg.test(v); } // New version
|
|
function u2fSupported() { return (window.u2f && ((navigator.userAgent.indexOf('Chrome/') > 0) || (navigator.userAgent.indexOf('Firefox/') > 0) || (navigator.userAgent.indexOf('Opera/') > 0) || (navigator.userAgent.indexOf('Safari/') > 0))); }
|
|
|
|
</script></body></html> |