kopia lustrzana https://github.com/solokeys/solo1
1468 wiersze
41 KiB
JavaScript
1468 wiersze
41 KiB
JavaScript
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()
|