DEVELOPMENT = 0; var to_b58 = function(B){var A="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";var d=[],s="",i,j,c,n;for(i in B){j=0,c=B[i];s+=c||s.length^i?"":1;while(j in d||c){n=d[j];n=n?n*256+c:c;c=n/58|0;d[j]=n%58;j++}}while(j--)s+=A[d[j]];return s}; var from_b58 = function(S){var A="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";var d=[],b=[],i,j,c,n;for(i in S){j=0,c=A.indexOf(S[i]);if(c<0)throw new Error('Invald b58 character');c||b.length^i?i:b.push(0);while(j in d||c){n=d[j];n=n?n*58+c:c;c=n>>8;d[j]=n%256;j++}}while(j--)b.push(d[j]);return new Uint8Array(b)}; // Calculate the Shannon entropy of a string in bits per symbol. // https://gist.github.com/jabney/5018b4adc9b2bf488696 (function(shannon) { 'use strict'; // Create a dictionary of character frequencies and iterate over it. function process(s, evaluator) { var h = Object.create(null), k; s.split('').forEach(function(c) { h[c] && h[c]++ || (h[c] = 1); }); if (evaluator) for (k in h) evaluator(k, h[k]); return h; }; // Measure the entropy of a string in bits per symbol. shannon.entropy = function(s) { var sum = 0,len = s.length; process(s, function(k, f) { var p = f/len; sum -= p * Math.log(p) / Math.log(2); }); return sum; }; // Measure the entropy of a string in total bits. shannon.bits = function(s) { return shannon.entropy(s) * s.length; }; // Log the entropy of a string to the console. shannon.log = function(s) { console.log('Entropy of "' + s + '" in bits per symbol:', shannon.entropy(s)); }; })(window.shannon = window.shannon || Object.create(null)); function hex(byteArray, join) { if (join === undefined) join = ' '; return Array.from(byteArray, function(byte) { return ('0' + (byte & 0xFF).toString(16)).slice(-2); }).join(join) } // Convert from normal to web-safe, strip trailing "="s function webSafe64(base64) { return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } // Convert from web-safe to normal, add trailing "="s function normal64(base64) { return base64.replace(/\-/g, '+').replace(/_/g, '/') + '=='.substring(0, (3*base64.length)%4); } function websafe2array(base64) { var binary_string = window.atob(normal64(base64)); var len = binary_string.length; var bytes = new Uint8Array( len ); for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } return bytes; } function array2websafe(array) { var result = ""; for(var i = 0; i < array.length; ++i){ result+= (String.fromCharCode(array[i])); } return webSafe64(window.btoa(result)); } function string2websafe(string) { return webSafe64(window.btoa(string)); } function websafe2string(string) { return window.atob(normal64(string)); } function string2array(string) { var bytes = new Uint8Array( string.length ); for (var i = 0; i < string.len; i++) { bytes[i] = string.charCodeAt(i); } return bytes; } function hex2array(string) { if (string.slice(0,2) == '0x') { string = string.slice(2,string.length); } if (string.length & 1) { throw new Error('Odd length hex string'); } var arr = new Uint8Array(string.length/2); var i; for (i = 0; i < string.length; i+=2) { arr[i/2] = parseInt(string.slice(i,i+2),16); } return arr; } // @key input private key in hex string format function key2wif(key) { //2 key = '0x80' + key; bin = hex2array(key); //3 var hash = sha256.create(); hash.update(bin); bin = hash.array(); //4 hash = sha256.create(); hash.update(bin); bin = hash.array(); // 5 var chksum = bin.slice(0,4); // 6 key = key + array2hex(chksum); // 7 key = hex2array(key); key = to_b58(key); return key; } function array2hex(buffer) { return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(''); } // https://stackoverflow.com/questions/18729405/how-to-convert-utf8-string-to-byte-array function toUTF8Array(str) { var utf8 = []; for (var i=0; i < str.length; i++) { var charcode = str.charCodeAt(i); if (charcode < 0x80) utf8.push(charcode); else if (charcode < 0x800) { utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f)); } else if (charcode < 0xd800 || charcode >= 0xe000) { utf8.push(0xe0 | (charcode >> 12), 0x80 | ((charcode>>6) & 0x3f), 0x80 | (charcode & 0x3f)); } // surrogate pair else { i++; // UTF-16 encodes 0x10000-0x10FFFF by // subtracting 0x10000 and splitting the // 20 bits of 0x0-0xFFFFF into two halves charcode = 0x10000 + (((charcode & 0x3ff)<<10) | (str.charCodeAt(i) & 0x3ff)); utf8.push(0xf0 | (charcode >>18), 0x80 | ((charcode>>12) & 0x3f), 0x80 | ((charcode>>6) & 0x3f), 0x80 | (charcode & 0x3f)); } } return utf8; } function error2string(err) { var lut = { // Only some of these are used 0x00 : 'CTAP1_SUCCESS', 0x01 : 'CTAP1_ERR_INVALID_COMMAND', 0x02 : 'CTAP1_ERR_INVALID_PARAMETER', 0x03 : 'CTAP1_ERR_INVALID_LENGTH', 0x04 : 'CTAP1_ERR_INVALID_SEQ', 0x05 : 'CTAP1_ERR_TIMEOUT', 0x06 : 'CTAP1_ERR_CHANNEL_BUSY', 0x0A : 'CTAP1_ERR_LOCK_REQUIRED', 0x0B : 'CTAP1_ERR_INVALID_CHANNEL', 0x10 : 'CTAP2_ERR_CBOR_PARSING', 0x11 : 'CTAP2_ERR_CBOR_UNEXPECTED_TYPE', 0x12 : 'CTAP2_ERR_INVALID_CBOR', 0x13 : 'CTAP2_ERR_INVALID_CBOR_TYPE', 0x14 : 'CTAP2_ERR_MISSING_PARAMETER', 0x15 : 'CTAP2_ERR_LIMIT_EXCEEDED', 0x16 : 'CTAP2_ERR_UNSUPPORTED_EXTENSION', 0x17 : 'CTAP2_ERR_TOO_MANY_ELEMENTS', 0x18 : 'CTAP2_ERR_EXTENSION_NOT_SUPPORTED', 0x19 : 'CTAP2_ERR_CREDENTIAL_EXCLUDED', 0x20 : 'CTAP2_ERR_CREDENTIAL_NOT_VALID', 0x21 : 'CTAP2_ERR_PROCESSING', 0x22 : 'CTAP2_ERR_INVALID_CREDENTIAL', 0x23 : 'CTAP2_ERR_USER_ACTION_PENDING', 0x24 : 'CTAP2_ERR_OPERATION_PENDING', 0x25 : 'CTAP2_ERR_NO_OPERATIONS', 0x26 : 'CTAP2_ERR_UNSUPPORTED_ALGORITHM', 0x27 : 'CTAP2_ERR_OPERATION_DENIED', 0x28 : 'CTAP2_ERR_KEY_STORE_FULL', 0x29 : 'CTAP2_ERR_NOT_BUSY', 0x2A : 'CTAP2_ERR_NO_OPERATION_PENDING', 0x2B : 'CTAP2_ERR_UNSUPPORTED_OPTION', 0x2C : 'CTAP2_ERR_INVALID_OPTION', 0x2D : 'CTAP2_ERR_KEEPALIVE_CANCEL', 0x2E : 'CTAP2_ERR_NO_CREDENTIALS', 0x2F : 'CTAP2_ERR_USER_ACTION_TIMEOUT', 0x30 : 'CTAP2_ERR_NOT_ALLOWED', 0x31 : 'CTAP2_ERR_PIN_INVALID', 0x32 : 'CTAP2_ERR_PIN_BLOCKED', 0x33 : 'CTAP2_ERR_PIN_AUTH_INVALID', 0x34 : 'CTAP2_ERR_PIN_AUTH_BLOCKED', 0x35 : 'CTAP2_ERR_PIN_NOT_SET', 0x36 : 'CTAP2_ERR_PIN_REQUIRED', 0x37 : 'CTAP2_ERR_PIN_POLICY_VIOLATION', 0x38 : 'CTAP2_ERR_PIN_TOKEN_EXPIRED', 0x39 : 'CTAP2_ERR_REQUEST_TOO_LARGE', } return lut[err] } var CMD = { sign: 0x10, register: 0x11, pin: 0x12, reset: 0x13, version: 0x14, rng: 0x15, pubkey: 0x16, boot_write: 0x40, boot_done: 0x41, boot_check: 0x42, boot_erase: 0x43, boot_version: 0x44, }; var PIN = { getRetries: 0x01, getKeyAgreement: 0x02, setPin: 0x03, changePin: 0x04, getPinToken: 0x05, }; // Create XHR object. function createCORSRequest(method, url) { var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { // XHR for Chrome/Firefox/Opera/Safari. xhr.open(method, url, true); } else if (typeof XDomainRequest != "undefined") { // XDomainRequest for IE. xhr = new XDomainRequest(); xhr.open(method, url); } else { // CORS not supported. xhr = null; } return xhr; } function parse_device_response(arr) { var dataview = new DataView(arr.slice(1,5).buffer); count = dataview.getUint32(0,true); // get count as 32 bit LE integer data = null; if (arr[5] == 0) { data = arr.slice(6,arr.length); } return {count: count, status: error2string(arr[5]), data: data}; } // For development purposes function send_msg_http(data, func, timeout) { var url = 'https://localhost:8080'; var req = JSON.stringify({data: array2websafe(data)}); var xhr = createCORSRequest('POST', url); if (!xhr) { console.log('CORS not supported'); return; } // Response handlers. xhr.onload = function() { var text = xhr.responseText; var resp = JSON.parse(text); arr = websafe2array(resp.data); data = parse_device_response(arr); if (func) func(data); }; xhr.onerror = function() { console.log('Woops, there was an error making the request.'); }; xhr.send(req); } function get_firmware_http_(func) { var url = 'https://localhost:8080'; var xhr = createCORSRequest('GET', url); if (!xhr) { console.log('CORS not supported'); return; } // Response handlers. xhr.onload = function() { var text = xhr.responseText; var resp = JSON.parse(text); resp.firmware = websafe2string(resp.firmware); if (func) func(resp); }; xhr.onerror = function() { console.log('Woops, there was an error making the request.'); }; xhr.send(); } // For real function send_msg_u2f(data, func, timeout) { // Use key handle and signature response as comm channel var d = new Date(); var t1 = d.getTime(); timeout = timeout || 5; var appid = window.location.origin; //var chal = string2websafe('AABBCC'); var chal = array2websafe(hex2array('d1cd7357bcedc03fcec112fe5a7f3f890292ff6f758978928b736ce1e63479e5')); var args = { type: 'navigator.id.getAssertion', challenge: chal, origin: appid }; var keyHandle = array2websafe(data); var key = { version: 'U2F_V2', keyHandle: keyHandle, transports: [], appId: appid }; window.u2f.sign(appid,chal,[key], function(res){ var d2 = new Date(); t2 = d2.getTime(); if (!res.signatureData) func(res); sig = websafe2array(res.signatureData) data = parse_device_response(sig); func(data); },timeout); } var send_msg; if (DEVELOPMENT) { send_msg = send_msg_http; } else { send_msg = send_msg_u2f; } function formatBootRequest(cmd, addr, data) { var array = new Uint8Array(255); data = data || new Uint8Array(1); if (data.length > (255 - 9)) { throw new Error("Max size exceeded"); } array[0] = cmd & 0xff; array[1] = (addr >> 0) & 0xff; array[2] = (addr >> 8) & 0xff; array[3] = (addr >> 16) & 0xff; array[4] = 0x8C; // Wallet tag. To not interfere with U2F devices. array[5] = 0x27; array[6] = 0x90; array[7] = 0xf6; array[8] = 0; array[9] = data.length & 0xff; var offset = 10; var i; for (i = 0; i < data.length; i++){ array[offset + i] = data[i]; } return array; } // Format a request message // @cmd 0-255 value command // @p1,p2 optional "sub" commands/arguments, each 0-255 valued. // @pinAuth 16 byte Uint8Array needed for most commands to authorize command // @args array of Uint8Arrays, arguments for the command being run function formatRequest(cmd, p1, p2, pinAuth, args) { var argslen = 0; var i,j; args = args || []; for (i = 0; i < args.length; i+=1) { argslen += args[i].length + 1 } var len = 16 + 4 + 4 +argslen; if (len > 255) { throw new Error('Total length of request cannot exceed 255 bytes'); } var array = new Uint8Array(len); array[0] = cmd & 0xff; array[1] = p1 & 0xff; array[2] = p2 & 0xff; array[3] = (args.length) & 0xff; array[4] = 0x8C; // Wallet tag. To not interfere with U2F devices. array[5] = 0x27; array[6] = 0x90; array[7] = 0xf6; var offset = 8; if (pinAuth) { for (i = 0; i < 16; i += 1) { array[offset + i] = pinAuth[i]; } } offset = offset + i; for (i = 0; i < args.length; i += 1) { array[offset] = args[i].length; offset += 1 for (j = 0; j < args[i].length; j += 1) { array[offset] = args[i][j]; offset += 1 } } return array; } // Computes sha256 HMAC // @pinToken is key for HMAC // @cmd,p1,p2 each are bytes input to HMAC // @args array of Uint8Arrays input to HMAC // @return first 16 bytes of HMAC function computePinAuth(pinToken, cmd,p1,p2,args) { var hmac = sha256.hmac.create(pinToken); var i; hmac.update([cmd]); hmac.update([p1]); hmac.update([p2]); if (args && args.length) hmac.update([args.length]); else hmac.update([0]); hmac.update([0x8c,0x27,0x90,0xf6]); if (args) { for (i = 0; i < args.length; i++) { hmac.update([args[i].length]); hmac.update(args[i]); } } return hmac.array().slice(0,16) } function computePinAuthRaw(pinToken, data) { var hmac = sha256.hmac.create(pinToken); hmac.update(data); return hmac.array().slice(0,16) } // @sigAlg is a number 0-255 // @pinAuth token, see pinToken information. Uint8Array // @challenge is websafe base64 string. Data to be signed. // @keyid is optional, websafe base64 string function signRequestFormat(sigAlg,pinToken,challenge,keyid) { // Sign request // Field Value length // op: 0x10 1 // authType: sigAlg 1 // reserved: 0x00 1 // pinHashEnc: [dynamic] 16 // challenge-length: challenge.length 1 // challenge: challenge 1-234 // keyID-length: keyid.length 1 // keyID: keyid 0-233 // Note: total size must not exceed 255 bytes var cmd = CMD.sign; var p1 = sigAlg; var p2 = 0; if (typeof(challenge) == 'string') { challenge = websafe2array(challenge); } var args = [challenge]; if (keyid) args.push(keyid) var pinAuth; if (pinToken) pinAuth = computePinAuth(pinToken,cmd,p1,p2,args); else pinAuth = new Uint8Array(16); var req = formatRequest(cmd,p1,p2,pinAuth,args); return req; } // @wifkey is wif key in base58 format string function registerRequestFormat(wifkey, pinToken) { var cmd = CMD.register; var p1 = 0; var p2 = 0; var keyarr = from_b58(wifkey); var args = [keyarr]; var pinAuth; if (pinToken) pinAuth = computePinAuth(pinToken,cmd,p1,p2,args); else pinAuth = new Uint8Array(16); var req = formatRequest(cmd,p1,p2,pinAuth,args); return req; } // @subCmd is one of the following in PIN {} function pinRequestFormat(subcmd, pinAuth, pubkey, pinEnc, pinHashEnc) { var cmd = CMD.pin; var p1 = subcmd; var p2 = 0; //var args = [challenge]; //if (keyid) args.push(keyid) pinAuth = pinAuth || new Uint8Array(16); var args = []; if (pubkey) args.push(pubkey); if (pinEnc) args.push(pinEnc); if (pinHashEnc) args.push(pinHashEnc); //var pinAuth = computePinAuth(pinToken,cmd,p1,p2,args); //console.log(hex(pinAuth)); var req = formatRequest(cmd,p1,p2,pinAuth,args); return req; } var get_shared_secret_ = function(func) { // Get temporary pubkey from device to compute shared secret var req = pinRequestFormat(PIN.getKeyAgreement); var self = this; send_msg(req, function(resp){ if (resp.status == 'CTAP1_SUCCESS') { var i; var devicePubkeyHex = '04'+hex(resp.data,''); var devicePubkey = self.ecp256.keyFromPublic(devicePubkeyHex,'hex'); // Generate a new key pair for shared secret var platform_keypair = self.ecp256.genKeyPair(); self.platform_keypair = platform_keypair; // shared secret var shared = platform_keypair.derive(devicePubkey.getPublic()).toArray(); var hash = sha256.create(); hash.update(shared); shared = hash.array(); resp.data = shared; } if (func) func(resp); }); }; var authenticate_ = function(pin, func){ if (! this.shared_secret){ throw new Error('Device is not connected.'); } hash = sha256.create(); hash.update(toUTF8Array(pin)); pinHash = hash.array().slice(0,16); var iv = new Uint8Array(16); iv.fill(0); var aesCbc = new aesjs.ModeOfOperation.cbc(this.shared_secret, iv); pinHashEnc = aesCbc.encrypt(pinHash); var ourPubkey = this.platform_keypair.getPublic(undefined, 'hex'); var ourPubkeyBytes = hex2array(ourPubkey.slice(2,ourPubkey.length)); var req = pinRequestFormat(PIN.getPinToken, pinHashEnc, ourPubkeyBytes); var self = this; send_msg(req, function(resp){ var aesCbc = new aesjs.ModeOfOperation.cbc(self.shared_secret, iv); var pinTokenEnc = resp.data; if (resp.status == 'CTAP1_SUCCESS') { var pinToken = aesCbc.decrypt(pinTokenEnc); self.pinToken = pinToken; if (func) func({pinToken: pinToken, status: resp.status}); } else { self.init(function(){ if (func) func(resp); }); } }); }; function pin2bytes(pin){ var pinBytes = toUTF8Array(pin); var encLen = pinBytes.length + (16-(pinBytes.length % 16)); if (encLen < 64){ encLen = 64; } if (pin.length < 4){ throw Error('FIDO2 pin must be at least 4 unicode characters.'); } if (encLen > 255){ throw Error('FIDO2 pin may not exceed 255 bytes'); } if (encLen > 80){ throw Error('Recommended to not use pins longer than 80 bytes due to 255 byte max message size.'); } var pinBytesPadded = new Uint8Array(encLen); pinBytesPadded.fill(0); var i; for (i = 0; i < pinBytes.length; i++){ pinBytesPadded[i] = pinBytes[i]; } return pinBytesPadded; } var set_pin_ = function(pin, failAuth, func){ var subcmd = PIN.setPin; var pinBytesPadded = pin2bytes(pin); var encLen = pinBytesPadded.length; var iv = new Uint8Array(16); iv.fill(0); var aesCbc = new aesjs.ModeOfOperation.cbc(this.shared_secret, iv); pinEnc = aesCbc.encrypt(pinBytesPadded); var pinAuth = computePinAuthRaw(this.shared_secret, pinEnc); if (func == undefined && typeof failAuth == 'function'){ func = failAuth; failAuth = false; } if (failAuth){ pinAuth.fill(0xAA); pinEnc.fill(0xAA); } var ourPubkey = this.platform_keypair.getPublic(undefined, 'hex'); var ourPubkeyBytes = hex2array(ourPubkey.slice(2,ourPubkey.length)); var req = pinRequestFormat(subcmd, pinAuth, ourPubkeyBytes, pinEnc); send_msg(req, function(resp){ if (func) func(resp); }); } var is_pin_set_ = function(func) { this.set_pin('12345', true, function(resp){ if (resp.status == "CTAP2_ERR_NOT_ALLOWED") { func({data:true, status: 'CTAP1_SUCCESS'}); } else if (resp.status == "CTAP2_ERR_PIN_AUTH_INVALID"){ func({data:false, status: 'CTAP1_SUCCESS'}); } else { func({data: undefined, status: resp.status}); //throw new Error("Device returned expected status: " + stat); } }); } var change_pin_ = function(curpin, newpin, func, failAuth){ var subcmd = PIN.changePin; pin2bytes(curpin); // validation only var pinBytesPadded = pin2bytes(newpin); var encLen = pinBytesPadded.length; var iv = new Uint8Array(16); iv.fill(0); var aesCbc = new aesjs.ModeOfOperation.cbc(this.shared_secret, iv); newPinEnc = aesCbc.encrypt(pinBytesPadded); var hash = sha256.create(); hash.update(toUTF8Array(curpin)); curPinHash = hash.array().slice(0,16); aesCbc = new aesjs.ModeOfOperation.cbc(this.shared_secret, iv); curPinHashEnc = aesCbc.encrypt(curPinHash); var concat = new Uint8Array(newPinEnc.length + curPinHashEnc.length); concat.set(newPinEnc); concat.set(curPinHashEnc, newPinEnc.length); var pinAuth = computePinAuthRaw(this.shared_secret, concat); var ourPubkey = this.platform_keypair.getPublic(undefined, 'hex'); var ourPubkeyBytes = hex2array(ourPubkey.slice(2,ourPubkey.length)); var req = pinRequestFormat(subcmd, pinAuth, ourPubkeyBytes, newPinEnc, curPinHashEnc); send_msg(req, function(resp){ if (func) func(resp); }); } var get_retries_ = function(func){ var subcmd = PIN.getRetries; var req = pinRequestFormat(subcmd); send_msg(req, function(resp){ resp.data = resp.data[0]; if (func) func(resp); }); } var sign_ = function(obj, func){ if (!obj.challenge) throw new Error("Need something to sign"); var alg = obj.alg || 3; var pinToken = this.pinToken || undefined; var req = signRequestFormat(alg,pinToken,obj.challenge,obj.keyid); send_msg(req, function(resp){ if (resp.status == 'CTAP1_SUCCESS') { var r = resp.data.slice(0,32); var s = resp.data.slice(32,64); r = array2hex(r); s = array2hex(s); resp.sig = {}; resp.sig.r = r; resp.sig.s = s; } if (func) func(resp); }); }; var register_ = function(wifkey, func){ if (!wifkey) throw new Error("No key provided"); var req = registerRequestFormat(wifkey,this.pinToken); send_msg(req, function(resp){ if (func) func(resp); }); }; // @note authorization required beforehand if device is not already locked. var reset_ = function(func){ var pinAuth = undefined; if (this.pinToken) { pinAuth = computePinAuth(this.pinToken, CMD.reset, 0, 0); } var req = formatRequest(CMD.reset,0,0, pinAuth); var self = this; send_msg(req, function(resp){ if (resp.status == "CTAP1_SUCCESS") { self.init(function(resp){ if (func)func(resp); }); } else { if (func) func(resp); } }); }; // Read 72 random bytes from hardware RNG on device var get_rng_ = function(func){ var pinAuth = undefined; if (this.pinToken) { pinAuth = computePinAuth(this.pinToken, CMD.rng, 0, 0); } var req = formatRequest(CMD.rng,0,0, pinAuth); var self = this; send_msg(req, function(resp){ if (func)func(resp); }); }; // Derive public key from the private key stored on device. Returns X,Y point. 64 bytes. var get_pubkey_ = function(func){ var pinAuth = undefined; if (this.pinToken) { pinAuth = computePinAuth(this.pinToken, CMD.pubkey, 0, 0); } var req = formatRequest(CMD.pubkey,0,0, pinAuth); var self = this; send_msg(req, function(resp){ if (func)func(resp); }); }; var is_bootloader_ = function(func){ var req = formatBootRequest(CMD.boot_check); var self = this; send_msg(req, function(resp){ if (func)func(resp); }); }; var bootloader_finish_ = function(sig,func){ var req = formatBootRequest(CMD.boot_done, 0x8000, sig); send_msg(req, function(resp){ if (func)func(resp); }); }; var bootloader_write_ = function(addr,data,func){ var req = formatBootRequest(CMD.boot_write,addr,data); send_msg(req, function(resp){ if (func)func(resp); }); }; function wrap_promise(func) { var self = this; return function (){ var args = arguments; return new Promise(function(resolve,reject){ var i; var oldfunc = null; for (i = 0; i < args.length; i++) { if (typeof args[i] == 'function') { oldfunc = args[i]; args[i] = function(){ oldfunc.apply(self,arguments); resolve.apply(self,arguments); }; break; } } if (oldfunc === null) { args = Array.prototype.slice.call(args); args.push(function(){ resolve.apply(self,arguments); }); } func.apply(self,args); }); } } var get_firmware_http = wrap_promise(get_firmware_http_); function WalletDevice() { var self = this; this.shared_secret = null; this.ec256k1 = new EC('secp256k1'); this.ecp256 = new EC('p256'); this.init = function(func){ self.get_version(function(ver){ self.version = ver; self.get_shared_secret(function(resp){ if (resp.status == "CTAP1_SUCCESS") self.shared_secret = resp.data; else { } if (func) func(resp); }); }); }; this.get_version = function(func){ var req = formatRequest(CMD.version,0,0); send_msg(req, function(resp){ var ver = new TextDecoder("utf-8").decode(resp.data); if (func) func(ver); }); }; this.get_shared_secret = get_shared_secret_; // getPinToken using set pin this.authenticate = authenticate_; this.sign = sign_; this.set_pin = set_pin_; this.is_pin_set = is_pin_set_; this.change_pin = change_pin_; this.get_retries = get_retries_; this.register = register_; this.reset = reset_; this.get_rng = get_rng_; this.get_pubkey = get_pubkey_; this.is_bootloader = is_bootloader_; this.bootloader_write = bootloader_write_; this.bootloader_finish = bootloader_finish_; this.init = wrap_promise.call(this, this.init); this.get_version = wrap_promise.call(this, this.get_version); this.get_shared_secret = wrap_promise.call(this, this.get_shared_secret ); this.authenticate = wrap_promise.call(this,this.authenticate ); this.sign = wrap_promise.call(this, this.sign ); this.set_pin = wrap_promise.call(this,this.set_pin ); this.is_pin_set = wrap_promise.call(this, this.is_pin_set ); this.change_pin = wrap_promise.call(this, this.change_pin ); this.get_retries = wrap_promise.call(this, this.get_retries ); this.register = wrap_promise.call(this, this.register ); this.reset = wrap_promise.call(this,this.reset ); this.get_rng = wrap_promise.call(this,this.get_rng); this.get_pubkey = wrap_promise.call(this,this.get_pubkey); this.is_bootloader = wrap_promise.call(this,this.is_bootloader); this.bootloader_write = wrap_promise.call(this,this.bootloader_write); this.bootloader_finish = wrap_promise.call(this,this.bootloader_finish); } async function handleFirmware(files) { var dev = new WalletDevice(); var p = await dev.is_bootloader(); document.getElementById('errors').textContent = ''; if (p.status != 'CTAP1_SUCCESS') { document.getElementById('errors').textContent = 'Make sure device is in bootloader mode. Unplug, hold button, plug in, wait for flashing yellow light.'; return; } var reader = new FileReader(); reader.onload = async function(ev){ var resp = JSON.parse(ev.target.result); resp.firmware = websafe2string(resp.firmware); console.log(resp); var addr = 0x4000; var num_pages = 64; var sig = websafe2array(resp.signature); var badsig = websafe2array(resp.signature); badsig[40] = badsig[40] ^ 1; var blocks = MemoryMap.fromHex(resp.firmware); var addresses = blocks.keys(); console.log(blocks); console.log(addresses); var addr = addresses.next(); var chunk_size = 240; while(!addr.done) { var data = blocks.get(addr.value); var i; for (i = 0; i < data.length; i += chunk_size) { var chunk = data.slice(i,i+chunk_size); console.log('addr ',addr.value + i); p = await dev.bootloader_write(addr.value + i, chunk); TEST(p.status == 'CTAP1_SUCCESS', 'Device wrote data'); var progress = (((i/data.length) * 100 * 100) | 0)/100; document.getElementById('progress').textContent = ''+progress+' %'; } addr = addresses.next(); } p = await dev.bootloader_finish(sig); if(p.status != 'CTAP1_SUCCESS') { document.getElementById('errors').textContent = 'Firmware image signature denied'; } else { document.getElementById('errors').textContent = 'Update successful'; } }; reader.readAsText(files[0]); } function TEST(bool, test){ if (bool) { if (test ) console.log("PASS: " + test); } else { console.log("FAIL: " + test); throw new Error("FAIL: " + test); } } async function run_tests() { var dev = new WalletDevice(); var pin = "Conor's pin 👽 "; var pin2 = "sogyhdxoh3qwli😀"; function string2challenge(chal) { var hash = sha256.create(); hash.update(chal); chal = hash.array(); return chal; } async function device_start_over() { var p = await dev.init(); if (p.status == 'CTAP2_ERR_NOT_ALLOWED') { // its already locked p = await dev.reset(); TEST(p.status == "CTAP1_SUCCESS", 'Device reset'); p = await dev.init(); TEST(p.status == "CTAP1_SUCCESS", 'Device initialize'); } else { TEST(p.status == "CTAP1_SUCCESS", 'Device initialize'); //console.log(dev); TEST(dev.version == "WALLET_V1.0", 'Device reports right version'); p = await dev.is_pin_set(); TEST(p.status == "CTAP1_SUCCESS", 'Check if pin is set'); if (!p.data) { } else { p = await dev.authenticate(pin); if (p.status == "CTAP2_ERR_PIN_INVALID" ) { p = await dev.authenticate(pin2); // try second pin } else { } TEST(p.status == "CTAP1_SUCCESS", 'Authenticated'); } p = await dev.reset(); TEST(p.status == "CTAP1_SUCCESS", 'Device reset'); } } async function test_pin() { var p = await dev.is_pin_set(); TEST(p.status == "CTAP1_SUCCESS" && !p.data, 'Pin is not set'); p = await dev.set_pin(pin); TEST(p.status == "CTAP1_SUCCESS", 'A pin was set'); p = await dev.is_pin_set(); TEST(p.status == "CTAP1_SUCCESS" && p.data, 'Pin set is detected'); p = await dev.set_pin(pin); TEST(p.status == "CTAP2_ERR_NOT_ALLOWED", 'Trying to set a pin again will fail'); p = await dev.change_pin(pin, pin2); TEST(p.status == "CTAP1_SUCCESS", 'Going through change pin process is successful'); p = await dev.authenticate(pin); TEST(p.status == "CTAP2_ERR_PIN_INVALID", 'Authenticating to previous/wrong pin is denied'); p = await dev.get_retries(); TEST(p.status == "CTAP1_SUCCESS" && p.data > 2, 'Have at least 2 tries left ('+p.data+')'); var tries = p.data; p = await dev.authenticate(pin); TEST(p.status == "CTAP2_ERR_PIN_INVALID"); p = await dev.get_retries(); TEST(p.status == "CTAP1_SUCCESS" && (p.data > 1) && (p.data < tries), 'Have less attempts left after another failed attempt (' + p.data+')'); p = await dev.authenticate(pin2); TEST(p.status == "CTAP1_SUCCESS", 'Authenticating with correct pin success'); p = await dev.get_retries(); TEST(p.status == "CTAP1_SUCCESS" && p.data > tries, 'Retries reset ('+ p.data+')'); p = await dev.change_pin(pin2, pin); TEST(p.status == "CTAP1_SUCCESS", 'Change pin back'); // Reset device for next set of tests p = await dev.authenticate(pin); TEST(p.status == "CTAP1_SUCCESS"); p = await dev.reset(); TEST(p.status == "CTAP1_SUCCESS"); } async function test_crypto(leaveEarly, startLate,imkey){ var ec = new EC('secp256k1'); key = imkey || ec.genKeyPair(); var priv = key.getPrivate('hex'); var wif = key2wif(priv); // convert to wif // Corrupt 1 byte var b = (wif[32] == 'A') ? 'B' : 'A'; var badwif = wif.substring(0, 32) + b + wif.substring(32+1); var p; var chal = string2challenge('abc'); if (!startLate) { p = await dev.set_pin(pin); TEST(p.status == "CTAP1_SUCCESS"); p = await dev.sign({challenge: chal}); TEST(p.status == 'CTAP2_ERR_PIN_AUTH_INVALID', 'No signature without authenticating first'); p = await dev.register(wif); TEST(p.status == 'CTAP2_ERR_PIN_AUTH_INVALID', 'No key register without authenticating first'); p = await dev.get_rng(); TEST(p.status == "CTAP2_ERR_PIN_AUTH_INVALID", 'No rng without authenticating first'); p = await dev.authenticate(pin); TEST(p.status == "CTAP1_SUCCESS"); p = await dev.sign({challenge: chal}); TEST(p.status == 'CTAP2_ERR_NO_CREDENTIALS', 'No signature without key'); p = await dev.register(badwif); TEST(p.status == 'CTAP2_ERR_CREDENTIAL_NOT_VALID', 'Wallet does not accept corrupted key'); p = await dev.register(wif); TEST(p.status == 'CTAP1_SUCCESS', 'Wallet accepts good WIF key'); } else { p = await dev.authenticate(pin + 'A'); TEST(p.status == "CTAP2_ERR_PIN_INVALID", 'Wrong pin fails'); p = await dev.authenticate(pin); TEST(p.status == "CTAP1_SUCCESS", 'Right pin works'); } p = await dev.get_pubkey(); TEST(p.status == 'CTAP1_SUCCESS', '(1) Wallet derives public key from stored private key'); p = await dev.register(wif); TEST(p.status == 'CTAP2_ERR_KEY_STORE_FULL', 'Wallet does not accept another key'); p = await dev.sign({challenge: chal}); TEST(p.status == 'CTAP1_SUCCESS', 'Wallet returns signature'); var sig = p.sig; var ver = key.verify(chal, sig); TEST(ver, 'Signature is valid'); p = await dev.get_pubkey(); TEST(p.status == 'CTAP1_SUCCESS', '(2) Wallet derives public key from stored private key'); var key2 = ec.keyFromPublic('04'+array2hex(p.data), 'hex'); ver = key2.verify(chal, sig); TEST(ver, 'Signature verifies with the derived public key'); var count = p.count; p = await dev.sign({challenge: chal}); ver = key.verify(chal, p.sig); TEST(p.status == 'CTAP1_SUCCESS' && p.count > count && ver, 'Count increments for each signature ' + p.count); if (leaveEarly) return; // Test lockout console.log("Exceeding all pin attempts..."); p = await dev.get_retries(); TEST(p.status == "CTAP1_SUCCESS"); var tries = p.data; while (tries > 0) { p = await dev.authenticate('1234'); // wrong pin TEST(p.status == "CTAP2_ERR_PIN_INVALID"); p = await dev.get_retries(); TEST(p.status == "CTAP1_SUCCESS"); tries = p.data; } p = await dev.get_retries(); TEST(p.status == "CTAP1_SUCCESS"); tries = p.data; TEST(tries == 0, 'Device has 0 tries left (lockout)'); p = await dev.register(wif); TEST(p.status == 'CTAP2_ERR_PIN_AUTH_INVALID', 'Register is denied'); p = await dev.sign({challenge: chal}); TEST(p.status == 'CTAP2_ERR_PIN_AUTH_INVALID', 'Sign is denied'); p = await dev.set_pin(pin); TEST(p.status == "CTAP2_ERR_NOT_ALLOWED", 'set_pin is locked out'); p = await dev.change_pin(pin,pin2); TEST(p.status == "CTAP2_ERR_NOT_ALLOWED", 'change_pin is locked out'); p = await dev.get_rng(); TEST(p.status == "CTAP2_ERR_NOT_ALLOWED", 'get_rng is locked out'); p = await dev.init(); TEST(p.status == "CTAP2_ERR_NOT_ALLOWED", 'init (getKeyAgreement) is locked out'); p = await dev.reset(); TEST(p.status == "CTAP1_SUCCESS"); p = await dev.get_retries(); TEST(p.status == "CTAP1_SUCCESS"); tries = p.data; p = await dev.is_pin_set(); TEST(p.status == "CTAP1_SUCCESS"); var is_pin_set = p.data; p = await dev.sign({challenge: chal}); TEST(p.status == 'CTAP2_ERR_NO_CREDENTIALS'); TEST(tries > 2 && is_pin_set == false, 'Device is no longer locked after reset and pin and key are gone'); TEST(p.count >= count, 'Counter did not reset'); } async function test_rng(){ var pool = ''; var p = await dev.get_rng(); TEST(p.status == "CTAP1_SUCCESS", 'Rng responds'); pool += array2hex(p.data); console.log("Gathering many RNG bytes.."); while (pool.length < 1024 * 10) { var p = await dev.get_rng(); TEST(p.status == "CTAP1_SUCCESS"); pool += array2hex(p.data); } var entropy = shannon.entropy(pool) * 2; TEST(entropy > 7.99, 'Rng has good entropy: ' + entropy); } async function test_persistence() { var ec = new EC('secp256k1'); var key = ec.keyFromPrivate('693e3c441129af84ed10693e3c441129af84ed10693e3c441129af84ed10aabb'); var p = await dev.init(); p = await dev.is_pin_set(); TEST(p.status == "CTAP1_SUCCESS"); var is_pin_set = p.data; if (! is_pin_set) { console.log("Pin is not set, resetting and loading new pin and key."); await device_start_over(); await test_crypto(true,false,key); console.log("Now restart device and reload page."); } else { await test_crypto(true,true,key); } } async function benchmark() { var t1,t2,i; var ec = new EC('secp256k1'); var key = ec.genKeyPair(); var priv = key.getPrivate('hex'); var wif = key2wif(priv); // convert to wif var chal = string2challenge('abc'); var p = await dev.register(wif); TEST(p.status == 'CTAP1_SUCCESS', 'Wallet accepts good WIF key'); var count,lcount; lcount = -1; for (i = 0; i < 2048; i++) { t1 = performance.now(); p = await dev.sign({challenge: chal}); t2 = performance.now(); var ver = key.verify(chal, p.sig); count = p.count; TEST(ver && p.status == 'CTAP1_SUCCESS', 'Wallet returns signature ('+(t2-t1)+' ms)'); if (i != 0) TEST(count == (lcount+1), 'Count increased by 1 ('+count+')'); lcount = count; } } async function test_bootloader() { var start = 10 * 2048; var size = 198 * 1024 - 8; var num_pages = 64; var p = await dev.is_bootloader(); TEST(p.status == 'CTAP1_SUCCESS', 'Device is in bootloader mode'); var randdata = new Uint8Array(16); p = await dev.bootloader_write(0, randdata); TEST(p.status == 'CTAP2_ERR_NOT_ALLOWED', 'Denies accessing invalid address'); p = await dev.bootloader_write(start-4, randdata); TEST(p.status == 'CTAP2_ERR_NOT_ALLOWED', 'Denies accessing invalid address'); p = await dev.bootloader_write(start, randdata); TEST(p.status == 'CTAP1_SUCCESS', 'Allows write to beginning'); p = await dev.bootloader_write(start + size-16, randdata); TEST(p.status == 'CTAP1_SUCCESS', 'Allows write to end'); p = await dev.bootloader_write(start + size-8, randdata); TEST(p.status == 'CTAP2_ERR_NOT_ALLOWED', 'Denies overflow'); p = await dev.bootloader_write(start + size, randdata); TEST(p.status == 'CTAP2_ERR_NOT_ALLOWED', 'Denies accessing invalid address'); p = await dev.bootloader_write(start + size + 1024, randdata); TEST(p.status == 'CTAP2_ERR_NOT_ALLOWED', 'Denies accessing invalid address'); p = await dev.bootloader_write(start + size + 1024*10, randdata); TEST(p.status == 'CTAP2_ERR_NOT_ALLOWED', 'Denies accessing invalid address'); var badsig = new Uint8Array(64); badsig[40] = badsig[40] ^ 1; p = await dev.bootloader_finish(badsig); TEST(p.status == 'CTAP2_ERR_OPERATION_DENIED', 'Device rejected new image with bad signature'); } //while(1) { // await device_start_over(); //await test_pin(); // await test_crypto(); //await test_rng(); } //await benchmark(); //await test_persistence(); await test_bootloader(); } var test; EC = elliptic.ec //run_tests()