/** * JR Controller "Library". * * Conceptually this is the thread corresponding to the application controller. * The background script calls `b_main()` and does nothing else. The popup * script calls the various `p_...` functions, which black-box away the IPC * nonsense. Really, conceptually, the popup script is making a call to the * background script, and the code idiom merely reflects that conceptual * reality. * * In a perfect world, the content script would also act like this. However, * * 1. The content script is already a finished product. All it needs to do is * relay messages back and forth between this thread and the page script, * and it does that just fine. * 2. The content script isn't an ES6 module, and so "imports" are a giant pain * in the ass. So rewriting it to fit this "call the background script" * idiom is just not worth it. It already works. The meaningful IPC is * between the page script and a generic wallet, and that is already defined * in AWCP. * * # Naming/Notation Conventions * * ``` * - b_... -> background script API * - bi_... -> background internal * - bis_... -> background internal storage * - p_... -> popup script API * - !... -> bookmark for regex searches to quickly find a section * bang = bookmark * ``` * * * Re storage: the browser's storage layer for extensions basically stores * the data in JSON, so we need a separate type layer to make sure the * conversion between the program's internal state and what the browser * stores doesn't get fornicated up. Despite the fact that JSON is magical * fairy dust that seamlessly encodes/decodes losslessly to and from any type, * I still am much more comfortable writing it out myself. * * * FIXME: check bytes or sth for keypair storage to guard against json fornicateery * FIXME: versioned storage * FIXME: move some of the AWCP data making functions to the AWCP lib * FIXME: the address_subscribe data scraping nonsense needs to be factored out * FIXME: quiet mode (undetectable by default) * FIXME: ask user if he wants to connect/address/etc * FIXME: ask user if he wants to sign messages * * @module */ import * as awcp from './jex_include/local-awcp-0.2.3/dist/awcp.js'; import * as vdk_aecrypt from './jex_include/local-vdk_aecrypt-0.1.2/dist/vdk_aecrypt.js'; import * as vdk_aertext from './jex_include/local-vdk_aertext-0.1.1/dist/vdk_aertext.js'; import * as vdk_aeser from './jex_include/local-vdk_aeser-0.1.1/dist/vdk_aeser.js'; import * as vdk_binary from './jex_include/local-vdk_binary-0.1.0/dist/vdk_binary.js'; export type { // global types // popup script call/return data p_data, p_rich_pubkey }; export { // popup script api p_get_data, p_generate_keypair, p_get_unique_name, p_activate_key, p_delete_key, p_get_gphrase, p_gphrase_recover, // background script api b_main }; // INTERNAL CONSTANTS ALL IN ONE PLACE let GB_ITER_MS = 5; // [MS / ITER] 5 ms every 1 iteration let GB_ITERS = 1; let GB_SEC = 200*GB_ITERS; // [ITER]; 1iter / 5ms = 200iter / sec let GB_MIN = 60*GB_SEC; // [ITER] let MSG_SIGN_TIMEOUT = 30*GB_MIN; let TX_SIGN_TIMEOUT = 30*GB_MIN; //============================================================================= //============================================================================= // API: GLOBAL TYPES //============================================================================= //============================================================================= //============================================================================= //============================================================================= // API: POPUP TYPES //============================================================================= //============================================================================= /** * data that is displayed in the popup window */ type p_data = {rich_pubkeys: Array}; type p_rich_pubkey = {akstr : string, active : boolean, name : string}; //============================================================================= //============================================================================= // API: POPUP SCRIPT //============================================================================= //============================================================================= /** * Called by the popup script to get the data it's supposed to show */ async function p_get_data () : Promise { console.log('b_lib p_get_data'); return await browser.runtime.sendMessage({frum: 'popup', data: {method : 'p_get_data', args : {}}}) as p_data; } /** * Called when user clicks "generate keypair" button in popup window * * Destructively updates jr state * * TODO: send back error if name is not unique */ async function p_generate_keypair (name : string) : Promise { console.log('b_lib p_generate_keypair'); return await browser.runtime.sendMessage({frum: 'popup', data: {method : 'p_generate_keypair', args : {name: name}}}) as void; } /** * Get unique name for new keypair */ async function p_get_unique_name () : Promise { console.log('b_lib p_get_unique_name'); return await browser.runtime.sendMessage({frum: 'popup', data: {method : 'p_get_unique_name', args : {}}}) as string; } /** * Activate given key */ async function p_activate_key (name: string) : Promise { console.log('b_lib p_activate_key', {name: name}); return await browser.runtime.sendMessage({frum: 'popup', data: {method : 'p_activate_key', args : {name: name}}}) as void; } /** * Delete given key */ async function p_delete_key (name: string) : Promise { console.log('b_lib p_delete_key', {name: name}); return await browser.runtime.sendMessage({frum: 'popup', data: {method : 'p_delete_key', args : {name: name}}}) as void; } async function p_get_gphrase (name: string) : Promise { console.log('b_lib p_get_gphrase', {name: name}); return await browser.runtime.sendMessage({frum: 'popup', data: {method : 'p_get_gphrase', args : {name: name}}}) as string; } async function p_gphrase_recover (gphrase : string, name : string) : Promise { console.log('b_lib p_gphrase_recover', {gphrase: gphrase, name: name}); return await browser.runtime.sendMessage({frum: 'popup', data: {method : 'p_gphrase_recover', args : {gphrase : gphrase, name : name}}}) as void; } //============================================================================= //============================================================================= // API: BACKGROUND SCRIPT //============================================================================= //============================================================================= /** * main function for background thread */ async function b_main () : Promise { console.log('b_main'); // handle messages from content or popup scripts browser.runtime.onMessage.addListener(bi_runtime_msg_handler); } //============================================================================= //============================================================================= // INTERNALS: TYPES //============================================================================= //============================================================================= // !bi_types !bi_state !bi-types !bi-state /** * @example * ```ts * // EventData_A2w * {type : "to_waellet", * // RpcCall * data : {jsonrpc : "2.0", * id : "ske-connect-1", * method : "connection.open", * // Params_A2W_connection_open * params : {name : "sidekick examples", * version : 1}}} * ``` * * @internal */ type bi_a2w_calldata = awcp.EventData_A2W>; // method params /** * Return data for queries from a page script * * @internal */ type bi_w2a_return_data = awcp.EventData_W2A>; /** * Type copied from NaCL * * @internal */ type bi_nacl_keypair = {secretKey : Uint8Array, publicKey : Uint8Array}; type bi_rich_keypair = {nacl_keypair : bi_nacl_keypair, name : string, active : boolean}; /** * JR state used internally * * @internal */ type bi_state = {rich_keypairs: Array}; /** * JR storage state * * Storage is in JSON so we have to have a different type for storage and for * what's actually used during ordinary data processing. * * @internal */ type bis_state = {jr_state: {rich_keypairs: Array}}; type bis_rich_keypair = {nacl_keypair : bis_nacl_keypair, name : string, active : boolean}; /** * How keypairs are stored * * @internal */ type bis_nacl_keypair = {secretKey : Array, publicKey : Array}; //============================================================================= //============================================================================= // INTERNALS: FUNCTIONS //============================================================================= //============================================================================= /** * Handle messages from either content or popup script * * Note to self: `sendResponse` is fake. Doesn't work. Instead just return the * response * * @internal */ async function bi_runtime_msg_handler (msg : {frum: 'content' | 'popup', data: any}, sender : any, sendResponse : any) : Promise { //console.log('msg', msg); //console.log('sender', sender); //console.log('sendResponse', sendResponse); switch (msg.frum) { // messages from the content script (i.e. from page scripts) case 'content': return await bi_msg_handler_content(msg.data as bi_a2w_calldata, sender, sendResponse); // messages from the popup window case 'popup': return await bi_msg_handler_popup(msg.data as bi_popup_calldata, sender, sendResponse); } } //----------------------------------------------------------------------------- // INTERNALS: CONTENT SCRIPT MESSAGE HANDLING //----------------------------------------------------------------------------- /** * Handle messages from the content script * * Note to self: `sendResponse` is fake. Doesn't work. Instead just return the * response. * * Literally the way it works is that * * - IF this is a non-async function (i.e. does NOT return a `Promise`), THEN * `sendResponse` is fake and the runtime sends back the return value of the * function. This is our case. * - ELSE `sendResponse` works. Presumably this is for backward compabitility. * * @internal */ async function bi_msg_handler_content (msg : bi_a2w_calldata, sender : any, sendResponse : any) : Promise { console.log('bi_msg_handler_content', {msg: msg}); //console.log('msg', msg); //console.log('sender', sender); //console.log('sendResponse', sendResponse); let msg_method : string = msg.data.method; let msg_id : string | number = msg.data.id; // get the state let i_state : bi_state = await bi_get_state(); // function to encode the ok message // depends on parameters above so makes sense to have it be a lambda function w2a_ok(result_for_content_script: any) { return bi_mk_w2a_msg_ok(msg_method, msg_id, result_for_content_script); } // function to encode the error message // depends on parameters above so makes sense to have it be a lambda function w2a_err(code: number, message: string) { return bi_mk_w2a_msg_err(msg_method, msg_id, code, message); } console.log('jr bg content message handler method:', msg.data.method); // if no keypairs, throw error if (bi_no_keypairs(i_state)) return w2a_err(421, 'user has no keys'); // from now on can assume there is a keypair and it is active // @ts-ignore am assuming impossible things didn't happen let public_key : Uint8Array = bi_active_pubkey(i_state).result; // @ts-ignore am assuming impossible things didn't happen let secret_key : Uint8Array = bi_active_privkey(i_state).result; switch (msg.data.method) { // right now just give connect info back case "connection.open": return w2a_ok({id : "jr", name : "JR", networkId : "ae_uat", origin : browser.runtime.getURL('/'), type : "extension"}); // right now just give address back case "address.subscribe": console.log('jr bg content message handler address.subscribe'); // convert it to a string let address_ak_str : string = await vdk_aeser.pubkey2ak_str(public_key); console.log('public key:', address_ak_str); return w2a_ok(bi_address_reply(address_ak_str, [])); // right now just sign message case "message.sign": console.log('jr bg content message handler message sign'); let msg_str : string = msg.data.params.message; // TODONE: need to break here on whether the msg signature was good // (signed) or bad (not) let resultt = await msg_sign(msg_str, secret_key); if (resultt.result === 'good') { return w2a_ok({signature: resultt.signature}); } else { return w2a_err(awcp.ERROR_CODE_RpcRejectedByUserError, "User either rejected message signature request or did not confirm within 30 minutes of popup window shown."); } // right now just sign tx case "transaction.sign": console.log('jr bg content message handler transaction sign'); let tx_str : string = msg.data.params.tx; console.log('transaction: ', tx_str); let result = await tx_sign(tx_str, secret_key) if ('good' === result.result) { return w2a_ok({signedTransaction: result.signedTransaction}); } else { return w2a_err(awcp.ERROR_CODE_RpcRejectedByUserError, "your not pretty enough"); } //if (result.result === 'good') { // return w2a_ok({signedTransaction: result.signedTransaction}); //} //else { // return w2a_err(awcp.ERROR_CODE_RpcRejectedByUserError, "you will never be pretty enough"); //} // default is NYI default: return w2a_err(awcp.ERROR_CODE_RpcMethodNotFoundError, 'not yet implemented'); } } type Safe = Ok | Err; type Ok = {ok : true, result : t}; type Err = {ok : false, error : string}; function ok (x: t) : Ok { return {ok: true, result: x}; } function err (msg: string) : Err { return {ok: false, error: msg}; } function bi_active_pubkey (i_state : bi_state) : Safe { if (bi_no_keypairs(i_state)) return err('no keypairs'); else { // type bi_nacl_keypair // = {secretKey : Uint8Array, // publicKey : Uint8Array}; // type bi_rich_keypair // = {nacl_keypair : bi_nacl_keypair, // name : string, // active : boolean}; // type bi_state // = {keypairs: Array}; let rich_keypairs : Array = i_state.rich_keypairs; for (let rkp of rich_keypairs) { // if this keypair is active, return it if (rkp.active) return ok(rkp.nacl_keypair.publicKey); } // if no keypairs were active, error return err("impossible state: no active keypair"); } } function bi_active_privkey (i_state : bi_state) : Safe { if (bi_no_keypairs(i_state)) return err('no keypairs'); else { // type bi_nacl_keypair // = {secretKey : Uint8Array, // publicKey : Uint8Array}; // type bi_rich_keypair // = {nacl_keypair : bi_nacl_keypair, // name : string, // active : boolean}; // type bi_state // = {keypairs: Array}; let rich_keypairs : Array = i_state.rich_keypairs; for (let rkp of rich_keypairs) { // if this keypair is active, return it if (rkp.active) return ok(rkp.nacl_keypair.secretKey); } // if no keypairs were active, error return err("impossible state: no active keypair"); } } /** * Return true if there is no keypairs */ function bi_no_keypairs (i_state : bi_state) : boolean { return (0 === i_state.rich_keypairs.length); } type gb = 'good' | 'bad'; /** * Fornicating js block scope rules * * NOOOOOOOO YOU CAN'T DECLARE TWO DIFFERENT VARIABLES WITH THE SAME NAME IN * TWO DIFFERENT CASES EVEN THOUGH THEY'RE MUTUALLY EXCLUSIVE * * wojak.jpg * * Good result = user said yes * Bad result = user said no * * @internal */ async function msg_sign (msg_str : string, secret_key : Uint8Array) : Promise< {result : 'good', signature : string} | {result : 'bad'} > { let result : gb = await gb('Do you want to sign this message?', msg_str, MSG_SIGN_TIMEOUT); // if the user wants us to sign the transaction, return the signed transaction // confirm if (result === 'good') { // use nacl detached signatures // https://github.com/aeternity/aepp-sdk-js/blob/5df22dd297abebc0607710793a7234e6761570d4/src/utils/crypto.ts#L141-L143 // https://github.com/aeternity/aepp-sdk-js/blob/5df22dd297abebc0607710793a7234e6761570d4/src/utils/crypto.ts#L160-L167 //console.error('hi 1'); let hashed_salted_msg : Uint8Array = vdk_aecrypt.hash_and_salt_msg(msg_str); //console.error('hi 2'); // @ts-ignore yes nacl is stupid let signature : Uint8Array = nacl.sign.detached(hashed_salted_msg, secret_key); let signature_str : string = vdk_binary.bytes_to_hex_str(signature); return {result : 'good', signature : signature_str}; } // otherwise don't else { return {result : 'bad'}; } } /** * Fornicating js block scope rules * * NOOOOOOOO YOU CAN'T DECLARE TWO DIFFERENT VARIABLES WITH THE SAME NAME IN * TWO DIFFERENT CASES EVEN THOUGH THEY'RE MUTUALLY EXCLUSIVE * * wojak.jpg * * @internal */ async function tx_sign (tx_str : string, secret_key : Uint8Array) : Promise< {result : 'good', signedTransaction : string} | {result : 'bad'} > { let mansplained_tx : string = await vdk_aeser.mansplain_str(tx_str); let result : gb = await gb('Do you want to sign this transaction?', mansplained_tx, TX_SIGN_TIMEOUT); // if we're supposed to sign the tx, then sign it if ('good' === result) { let tx_bytes : Uint8Array = (await vdk_aeser.unbaseNcheck(tx_str)).bytes; // thank you ulf // https://github.com/aeternity/protocol/tree/fd179822fc70241e79cbef7636625cf344a08109/consensus#transaction-signature // we sign <> // SerializedObject can either be the object or the hash of the object // let's stick with hash for now // FIXME: get network id from application state let network_id : Uint8Array = vdk_binary.encode_utf8('ae_uat'); // let tx_hash_bytes : Uint8Array = hash(tx_bytes); let sign_data : Uint8Array = vdk_binary.bytes_concat(network_id, tx_bytes); // @ts-ignore yes nacl is stupid let signature : Uint8Array = nacl.sign.detached(sign_data, secret_key); let signed_tx_bytes : Uint8Array = vdk_aeser.signed_tx([signature], tx_bytes); let signed_tx_str : string = await vdk_aeser.baseNcheck('tx', signed_tx_bytes); // debugging let mansplained_stx : object = await vdk_aeser.mansplain(signed_tx_str); console.log('mansplained signed tx:', mansplained_stx); return {result : 'good', signedTransaction : signed_tx_str}; } // if user rejected the tx else { return {result : 'bad'}; } } /** * This pops up a window for the luser and asks if the thing is good or bad. * * Returns good or bad * * Timeout is in "iterations"; 1 iteration is 5 milliseconds. Use `GB_SEC` and * `GB_MIN` to get those units. * * # Why this is stupid * * Ok super important thing: there's a very stupid idiom in here for talking to * the good/bad popup window. This is not because I am stupid. This is because * web browsers are stupid. * * We cannot just spawn a window and then start talking to it. The problem is * that this isn't Erlang where the process has a mailbox and we can count on * the messages being delivered there and then being there when the process * initiates a `raseev`. * * Instead the good/bad popup-window process has to spawn *and then listen for * messages*, and if we send a message before it's listening, tough, it just * doesn't get there. * * First instinct was of course to just wait for however many milliseconds for * the popup window to spawn before sending it messages. But 200ms was not * enough time for this approach to work on my beast of a machine. Given that * our users are going to be running JR on normal machines, this approach is * simply infeasible. * * Instead after much denial and error, I found the approach where the good/bad page script * initiates a connection works consistently and is idiomatically sane, at * least in relative terms. In this case, we are using the Port abstraction * rather than the "one-off" connection thing that Mozilla says is low class. * It is better in this case so that we don't have to deal with process-level * global state in this function. * * If we wanted to interleave that nonsense into the global raseev loop at the * top of this file, we would have to basically have some registry of pages * we're trying to talk to, and then have some message queue that pairs each * message from a script with whomever is trying to listen to it. That's * doable but I would prefer not to create global state if it's unnecessary. * * @internal */ async function gb (title : string, miscinfo : string, timeout_iters : number) : Promise { // TODO: the thread model here is a bit dubious... it's not 100% clear that // we're always going to be talking to the correct window. Do some // experiments. // we're going to make an unresolved result first // write our code to handle messages from the popup window // which updates this variable // once this variable is updated with resolved: true // then the entire function returns let the_result : {resolved : false} | {resolved : true, result : gb} = {resolved : false}; // this is a closure that talks to the popup window from this context // it updates the_result when it gets something back from the user let port_talker_toer_lambda = function(port : browser.runtime.Port) { // @ts-ignore bad type info in the typedefs port.postMessage({title : title, miscinfo : miscinfo}); // oh my god // nested lambdas // i think we've reached peak js let result_handler = function(result_from_gb_popup_window : gb) { console.error('RESULT FROM POPUP WINDOW!!!!', result_from_gb_popup_window); // the message will either be good or bad the_result = {resolved : true, result : result_from_gb_popup_window}; }; // @ts-ignore types from github are wrong port.onMessage.addListener(result_handler); } browser.runtime.onConnect.addListener(port_talker_toer_lambda); // does the user want to sign the message let confirm_window = await browser.windows.create({url : '../pages/gb.html', type : 'popup'}); // @ts-ignore shut the fuck up let tabid : number = confirm_window.tabs[0].id; let this_iter_i1 : number = 1; let max_iters : number = timeout_iters; // poor man's for loop because I just don't care // sleep for 5 milliseconds until resolved // this loop is guaranteed to terminate // and at its termination, the_result will have resolved:true while (!(the_result.resolved)) { // if we've timed out, time out if (this_iter_i1 > max_iters) { // the result is resolved and it's bad the_result = {resolved: true, result: 'bad'}; } // in this case, we haven't timed out yet else { // so we simply increase the iteration this_iter_i1 = this_iter_i1 + 1; // sleep await sleep(GB_ITER_MS); } } // at this point, the_result is resolved:true // we know that the_result.result exists // typescript does not know that // close the tab browser.tabs.remove(tabid); // return the result // @ts-ignore i know this exists because I am jesus return the_result.result; } /** * Make the dumb address_subscribe thing * * @internal */ function bi_address_reply (current_pubkey_str : string, other_pubkey_strs : Array) : awcp.Result_W2A_address_subscribe { // From the AWCP docs: // // @example // This is if the user has many keypairs. The currently selected one is under // `current`. Craig, I agree this is stupid, but that's how it works. // // { // "subscription": [ // "connected" // ], // "address": { // "current": { // "ak_25C3xaAGQddyKAnaLLMjAhX24xMktH2NNZxY3fMaZQLMGED2Nf": {} // }, // "connected": { // "ak_BMtPGuqDhWLnMVL4t6VFfS32y2hd8TSYwiYa2Z3VdmGzgNtJP": {}, // "ak_25BqQuiVCasiqTkXHEffq7XCsuYEtgjNeZFeVFbuRtJkfC9NyX": {}, // "ak_4p6gGoCcwQzLXd88KhdjRWYgd4MfTsaCeD8f99pzZhJ6vzYYV": {} // } // } // } // so we make the "object" in the "current" field // this is so dumb but let current_obj = {}; // @ts-ignore shut up tsc i know this is stupid it's not my fault current_obj[current_pubkey_str] = {}; // make the object in the "connected" field let connected_obj = {}; for (let this_pubkey_str of other_pubkey_strs) { // @ts-ignore shut up tsc i know this is stupid it's not my fault connected_obj[this_pubkey_str] = {}; } return {subscription : ['connected'], address : {current : current_obj, connected : connected_obj}}; } /** * SUCCESS CASE: Wrap up bs for w2a message * * This really should go into the AWCP library but I'm lazy * * @internal */ function bi_mk_w2a_msg_ok (method : string, id : string | number, result : result_t) : awcp.EventData_W2A> { return {type : "to_aepp", data : {jsonrpc : "2.0", method : method, id : id, result : result}}; } /** * ERROR CASE: Wrap up bs for w2a message * * This really should go into the AWCP library but I'm lazy * * @internal */ function bi_mk_w2a_msg_err (method : string, id : string | number, code : number, message : string) : awcp.EventData_W2A> { return {type : "to_aepp", data : {jsonrpc : "2.0", method : method, id : id, error : {code : code, message : message}}}; } //---------------------------------------------------------------------------- // INTERNALS: POPUP WINDOW MESSAGE HANDLING //---------------------------------------------------------------------------- type bi_popup_calldata = bi_pc_p_get_data | bi_pc_p_generate_keypair | bi_pc_p_get_unique_name | bi_pc_p_activate_key | bi_pc_p_delete_key | bi_pc_p_get_gphrase | bi_pc_p_gphrase_recover ; type bi_pc_p_get_data = {method : 'p_get_data', args : {}}; type bi_pc_p_generate_keypair = {method : 'p_generate_keypair', args : {name: string}}; type bi_pc_p_get_unique_name = {method : 'p_get_unique_name', args : {}}; type bi_pc_p_activate_key = {method : 'p_activate_key', args : {name: string}}; type bi_pc_p_delete_key = {method : 'p_delete_key', args : {name: string}}; type bi_pc_p_get_gphrase = {method : 'p_get_gphrase', args : {name: string}}; type bi_pc_p_gphrase_recover = {method : 'p_gphrase_recover', args : {gphrase : string, name : string}}; type bi_popup_return_data = p_data | void | string ; /** * Handle messages from the popup * * @internal */ async function bi_msg_handler_popup (msg : bi_popup_calldata, sender : any, sendResponse : any) : Promise { console.log('jr bg_msg_handler_popup msg', msg); switch(msg.method) { case 'p_get_data': return await bi_p_get_data(msg.args); case 'p_generate_keypair': return await bi_p_generate_keypair(msg.args); case 'p_get_unique_name': return await bi_p_get_unique_name(msg.args); case 'p_activate_key': return await bi_p_activate_key(msg.args); case 'p_delete_key': return await bi_p_delete_key(msg.args); case 'p_get_gphrase': return await bi_p_get_gphrase(msg.args); case 'p_gphrase_recover': return await bi_p_gphrase_recover(msg.args); default: throw new Error('unsupported popup call method'); } } async function bi_p_get_data (args: {}) : Promise { console.log('b_lib bi_p_get_data'); // type p_data // = {rich_pubkeys: Array}; // type p_rich_pubkey // = {akstr : string, // active : boolean, // name : string}; // type bi_nacl_keypair // = {secretKey : Uint8Array, // publicKey : Uint8Array}; // type bi_rich_keypair // = {nacl_keypair : bi_nacl_keypair, // name : string, // active : boolean}; // type bi_state // = {rich_keypairs: Array}; let jr_state : bi_state = await bi_get_state(); // accumulator let p_rkps : Array = []; for (let i_rkp of jr_state.rich_keypairs) { let this_pubkey : Uint8Array = i_rkp.nacl_keypair.publicKey; let this_akstr : string = await vdk_aeser.pubkey2ak_str(this_pubkey); let this_active : boolean = i_rkp.active; let this_name : string = i_rkp.name; p_rkps.push({akstr : this_akstr, active : this_active, name : this_name}); } return {rich_pubkeys: p_rkps}; } async function bi_p_generate_keypair (args: {name: string}) : Promise { console.log('b_lib bi_p_generate_keypair'); // get state let state : bi_state = await bi_get_state(); // type bi_nacl_keypair // = {secretKey : Uint8Array, // publicKey : Uint8Array}; // type bi_rich_keypair // = {nacl_keypair : bi_nacl_keypair, // name : string, // active : boolean}; // type bi_state // = {rich_keypairs: Array}; // @ts-ignore nacl sucks i know let new_keypair : bi_nacl_keypair = nacl.sign.keyPair() as bi_nacl_keypair; // if there are no keypairs, the first one is active let active : boolean = bi_no_keypairs(state); // if name is not unique change it if (!bi_name_unique(state, args.name)) args.name = await bi_p_get_unique_name({}); // update state state.rich_keypairs.push({nacl_keypair : new_keypair, active : active, name : args.name}); // set new state await bi_set_state(state); } async function bi_p_get_unique_name (args: {}) : Promise { console.log('b_lib bi_p_get_unique_name'); // get state let istate : bi_state = await bi_get_state(); let default_name = 'default'; let i : number = 0; while (true) { if (bi_name_unique(istate, 'default' + i)) return 'default' + i; else i = i + 1; } } function bi_name_unique (istate : bi_state, name : string) : boolean { for (let this_irkp of istate.rich_keypairs) { if (name === this_irkp.name) return false; } return true; } async function bi_p_activate_key (args: {name: string}) : Promise { console.log('b_lib bi_p_activate_key ' + args.name); // get state let istate : bi_state = await bi_get_state(); // find key with name // type bi_rich_keypair // = {nacl_keypair : bi_nacl_keypair, // name : string, // active : boolean}; // type bi_state // = {rich_keypairs: Array}; let new_rich_keypairs : Array = []; for (let this_irkp of istate.rich_keypairs) { // if this is the name, set active = true if (args.name === this_irkp.name) this_irkp.active = true; else this_irkp.active = false; new_rich_keypairs.push(this_irkp); } let new_state : bi_state = {rich_keypairs: new_rich_keypairs}; await bi_set_state(new_state); } async function bi_p_delete_key (args: {name: string}) : Promise { console.log('b_lib bi_p_delete_key ' + args.name); // get state let istate : bi_state = await bi_get_state(); // find key with name // type bi_rich_keypair // = {nacl_keypair : bi_nacl_keypair, // name : string, // active : boolean}; // type bi_state // = {rich_keypairs: Array}; let new_rich_keypairs : Array = []; let deleting_active_key : boolean = false; for (let this_irkp of istate.rich_keypairs) { // if this is the name, don't push if (args.name === this_irkp.name) { deleting_active_key = this_irkp.active; continue; } else new_rich_keypairs.push(this_irkp); } // if the result is that no keys are active // two cases: // 1. no keys left, in which case do nothing // 2. at least one key left, in which case, make the first one active // cannot have a situation where there are keys and no key is active if (deleting_active_key && !(0 === new_rich_keypairs.length)) { // grab first keypair and set it to active new_rich_keypairs[0].active = true; } let new_state : bi_state = {rich_keypairs: new_rich_keypairs}; await bi_set_state(new_state); } async function bi_p_get_gphrase (args: {name: string}) : Promise { console.log('b_lib bi_p_get_gphrase', {name: name}); // get state let istate : bi_state = await bi_get_state(); // find key with name // type bi_rich_keypair // = {nacl_keypair : bi_nacl_keypair, // name : string, // active : boolean}; // type bi_state // = {rich_keypairs: Array}; for (let this_irkp of istate.rich_keypairs) { if (args.name === this_irkp.name) return await bi_privkey2gphrase(this_irkp.nacl_keypair.secretKey); } return "ERROR: impossible state (getting GajuPhrase of key that doesn't exist)"; } async function bi_privkey2gphrase (privkey: Uint8Array) : Promise { return await vdk_aertext.encode(privkey); } /** * If decoding throws an error we raise it here */ async function bi_gphrase2privkey_unsafe (gphrase: string) : Promise { let maybe_privkey = await vdk_aertext.decode(gphrase); if (maybe_privkey.ok) return maybe_privkey.result; else throw new Error(maybe_privkey.error); } async function bi_p_gphrase_recover (args: {gphrase : string, name : string}) : Promise { console.log('b_lib bi_p_gphrase_recover', {gphrase: args.gphrase, name: args.name}); // get state let istate : bi_state = await bi_get_state(); // find key with name // type bi_nacl_keypair // = {secretKey : Uint8Array, // publicKey : Uint8Array}; // type bi_rich_keypair // = {nacl_keypair : bi_nacl_keypair, // name : string, // active : boolean}; // type bi_state // = {rich_keypairs: Array}; // make secret key from this let privkey_bytes : Uint8Array = await bi_gphrase2privkey_unsafe(args.gphrase); // double check to make sure it's not already here for (let this_irkp of istate.rich_keypairs) { let this_privkey: Uint8Array = this_irkp.nacl_keypair.secretKey; if (vdk_binary.bytes_eq(this_privkey, privkey_bytes)) throw new Error('key already added!'); } // @ts-ignore fuck off typescript we all know nacl sucks let nacl_keypair : bi_nacl_keypair = nacl.sign.keyPair.fromSecretKey(privkey_bytes); // only active if it's the first keypair let active : boolean = bi_no_keypairs(istate); let new_rich_keypair : bi_rich_keypair = {nacl_keypair : nacl_keypair, name : args.name, active : active}; istate.rich_keypairs.push(new_rich_keypair); await bi_set_state(istate); } //============================================================================= //============================================================================= // INTERNALS: STORAGE API //============================================================================= //============================================================================= /** * Fetch the state from local storage * * If there is no state make an initial state and store it * * @internal */ async function bi_get_state () : Promise { console.log('jr bg bi_get_state'); // so // - if extension has NEVER committed state (i.e. this is the first // invocation), there will be NO key "jr_state" // - if we have committed state, that key will exist // // right now our game is branching on whether or not there is existing // state in the browser's storage // // - if there is state, we simply fetch it from storage, convert it to a // type suitable for use in code (e.g. keypairs are byte arrays rather // than dumb JSON number arrays) // - if there is no state, we create a default initial state, commit it, // and hand it back to the calling code // this may or may not have the keyword "jr_state" let gotten_state : ({} | bis_state) = await browser.storage.local.get(); console.log('jr bg gotten_state', gotten_state); // problem: browser storage is JSON basically // so we need to convert our state to and from json // if there is such a state, get it // @ts-ignore ts doesn't understand querying if a key exists if (!!(gotten_state.jr_state)) { console.log('jr state exists'); // TS doesn't know we've proven the key exists and so therefore this is // of type bis_state return bi_s2i(gotten_state as bis_state); } // otherwise return default state else { console.log('jr no state... generating'); // set the state let default_i_state : bi_state = bi_state_default(); await bi_set_state(default_i_state); // return it return default_i_state; } } /** * Set the state. Blocks until state is set, exception is thrown if there is an * exception. * * @internal */ async function bi_set_state (i_state : bi_state) : Promise { console.log('jr bg bi_set_state internal state:', i_state); // convert to storage state let s_state : bis_state = bi_i2s(i_state); console.log('jr bg bi_set_state storage state:', s_state); // store await browser.storage.local.set(s_state); } //----------------------------------------------------------------------------- // INTERNALS: INTERNAL->STORAGE TYPE COERCION //----------------------------------------------------------------------------- /** * Internal state to storage state converter * * @internal */ function bi_i2s (i_state : bi_state) : bis_state { // accumulator let srkps : Array = []; // convert internal rich keypairs to storage rich keypairs for (let this_irkp of i_state.rich_keypairs) { // nacl keypair let this_inkp : bi_nacl_keypair = this_irkp.nacl_keypair; let this_snkp : bis_nacl_keypair = bi_i2s_keypair(this_inkp); // this storage rich keypair let this_srkp : bis_rich_keypair = {nacl_keypair : this_snkp, active : this_irkp.active, name : this_irkp.name}; srkps.push(this_srkp); } return {jr_state : {rich_keypairs: srkps}}; } /** * JSONify a keypair * * basically turn each array into numbers * * @internal */ function bi_i2s_keypair (i_keypair : bi_nacl_keypair) : bis_nacl_keypair { let i_secretKey : Uint8Array = i_keypair.secretKey; let i_publicKey : Uint8Array = i_keypair.publicKey; let s_secretKey : Array = bi_i2s_bytes2nums(i_secretKey); let s_publicKey : Array = bi_i2s_bytes2nums(i_publicKey); return {secretKey : s_secretKey, publicKey : s_publicKey}; } /** * Convert a byte array to an array of numbers * * @internal */ function bi_i2s_bytes2nums (bytes: Uint8Array) : Array { let arr : Array = []; for (let this_byte of bytes) { arr.push(this_byte); } return arr; } /** * Default state for JR * * Note * 1. This returns the *internal* state that lives in RAM, **NOT** what is * stored in browser storage * 2. This function is **NOT** deterministic. It randomly generates a keypair. * * @internal */ function bi_state_default () : bi_state { console.log('jr_state_default'); // LEGACY: if no key, generate one // // @ts-ignore ts doesn't like that nacl is dumb. i don't like it either // let init_keypair : bi_nacl_keypair = nacl.sign.keyPair() as bi_nacl_keypair; // let default_state : bi_state = { keypairs: [init_keypair] }; let default_state : bi_state = { rich_keypairs: [] }; console.log('jr_state_default default_state', default_state); return default_state; } //----------------------------------------------------------------------------- // INTERNALS: STORAGE->INTERNAL TYPE COERCION //----------------------------------------------------------------------------- /** * Storage -> internal state converter * * @internal */ function bi_s2i (s_state : bis_state) : bi_state { // convert numbers to byte array function nums2bytes(nums: Array): Uint8Array { return new Uint8Array(nums); } // convert storage keypair to normal keypair function keypair_s2i(s_keypair: bis_nacl_keypair): bi_nacl_keypair { let s_secretKey : Array = s_keypair.secretKey; let s_publicKey : Array = s_keypair.publicKey; let i_secretKey : Uint8Array = nums2bytes(s_secretKey); let i_publicKey : Uint8Array = nums2bytes(s_publicKey); return {secretKey : i_secretKey, publicKey : i_publicKey}; } // convert each keypair // accumulator let irkps : Array = []; // for now bis_state is just keypairs // storage rich keypairs let srkps : Array = s_state.jr_state.rich_keypairs; for (let this_srkp of srkps) { let this_inkp : bi_nacl_keypair = keypair_s2i(this_srkp.nacl_keypair); irkps.push({nacl_keypair : this_inkp, active : this_srkp.active, name : this_srkp.name}); } return {rich_keypairs: irkps}; } /** * Hack from stack overflow somewhere to sleep for the given number of ms */ async function sleep (ms: number) : Promise { return new Promise(resolve => setTimeout(resolve, ms)); }