solo/web/js/wallet.js
2018-12-03 23:01:51 -05:00

1488 lines
42 KiB
JavaScript

/*
Copyright 2018 Conor Patrick
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
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] = data.length & 0xff;
var offset = 9;
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 = 0x8000;
var size = 186 * 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()