Airgapped hardware wallet based on STM32F469-Discovery board using SD card to pass transaction data
Dependencies: mbed QSPI_DISCO_F469NI BSP_DISCO_F469NI
Diff: main.cpp
- Revision:
- 0:f43431023689
- Child:
- 1:b88ef7630eb3
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main.cpp Sat Jul 20 15:16:38 2019 +0000 @@ -0,0 +1,384 @@ +#include "mbed.h" +#include "helpers.h" + +// bitcoin lib +#include "Bitcoin.h" +#include "PSBT.h" + +/*********** forward declarations **************/ + +// generates a new mnemonic +string generateNewMnemonic(); + +// generates hd keys from the mnemonic +void initKeys(const string mnemonic, const string password = ""); + +/****************** GUI classes ****************/ + +Label titleLbl; +Label dataLbl; +QR qr; + +Button btn; +Button printBtn; +Label lbl; + +/***************** GUI functions ***************/ + +// if mnemonic is not present we can generate or recover it +void showInitScreen(); +// if mnemonic is there we go directly to the main menu +void showMenu(); +// handy function to display information to the user +// with a title, a message and OK button that goes to the menu +void showMessage(const string title, const string message); +static lv_res_t toMenuCallback(lv_obj_t * btn); +static lv_res_t showAddressesCallback(lv_obj_t * btn); +static lv_res_t saveXpubCallback(lv_obj_t * btn); +static lv_res_t signPSBTCallback(lv_obj_t * btn); +static lv_res_t showMnemonicCallback(lv_obj_t * btn); +static lv_res_t wipeCallback(lv_obj_t * btn); +static lv_res_t newMnemonicCallback(lv_obj_t * btn); +static lv_res_t enterMnemonicCallback(lv_obj_t * btn); +void showAddress(unsigned int child_index, bool change); + +/***************** bitcoin keys ***************/ + +HDPrivateKey root; // root private key +HDPrivateKey account; // account master private key +HDPublicKey xpub; // account master public key +bool change = false; // internal or external address +unsigned int child_index = 0; // current child index + +PSBT psbt; // psbt transaction we will be signing + +/******************* Main part *****************/ + +int main(){ + init(); + + string mnemonic = loadMnemonic(); + if(mnemonic.length() == 0){ + showInitScreen(); + }else{ + initKeys(mnemonic); + showMenu(); + } + + while(1){ + gui.update(); + } +} + +/****************** GUI stuff *****************/ +void showInitScreen(){ + gui.clear(); + titleLbl = Label("Let's set it up!"); + titleLbl.size(gui.width(), 20); + titleLbl.position(0, 40); + titleLbl.alignText(ALIGN_TEXT_CENTER); + + Button btn(newMnemonicCallback, "Generate new mnemonic"); + btn.size(gui.width()-100, 80); + btn.position(0, 200); + btn.align(ALIGN_CENTER); + + Button btn2(enterMnemonicCallback, "Enter existing mnemonic"); + btn2.size(gui.width()-100, 80); + btn2.position(0, 300); + btn2.align(ALIGN_CENTER); +} + +void showMenu(){ + gui.clear(); + titleLbl = Label("What do you want to do?"); + titleLbl.size(gui.width(), 20); + titleLbl.position(0, 40); + titleLbl.alignText(ALIGN_TEXT_CENTER); + + Button btn(showAddressesCallback, "Show addresses"); + btn.size(gui.width()-100, 80); + btn.position(0, 100); + btn.align(ALIGN_CENTER); + + Button btn2(saveXpubCallback, "Export xpub"); + btn2.size(gui.width()-100, 80); + btn2.position(0, 300); + btn2.align(ALIGN_CENTER); + + Button btn3(signPSBTCallback, "Sign PSBT transaction"); + btn3.size(gui.width()-100, 80); + btn3.position(0, 400); + btn3.align(ALIGN_CENTER); + + Button btn4(wipeCallback, "Wipe device"); + btn4.size(gui.width()-100, 80); + btn4.position(0, 600); + btn4.align(ALIGN_CENTER); + + Button btn5(showMnemonicCallback, "Show mnemonic"); + btn5.size(gui.width()-100, 80); + btn5.position(0, 700); + btn5.align(ALIGN_CENTER); +} + +void showMessage(const string title, const string message){ + gui.clear(); + titleLbl = Label(title); + titleLbl.size(gui.width(), 20); + titleLbl.position(0, 40); + titleLbl.alignText(ALIGN_TEXT_CENTER); + + dataLbl = Label(message); + dataLbl.size(gui.width()-100, 100); + dataLbl.position(50, 300); + dataLbl.alignText(ALIGN_TEXT_CENTER); + + Button btn(toMenuCallback, "OK"); + btn.size(gui.width()-100, 80); + btn.position(0, gui.height()-100); + btn.align(ALIGN_CENTER); +} + +static lv_res_t nextCallback(lv_obj_t * btn){ + child_index++; + showAddress(child_index, change); + return LV_RES_OK; +} +static lv_res_t prevCallback(lv_obj_t * btn){ + if(child_index > 0){ + child_index--; + showAddress(child_index, change); + } + return LV_RES_OK; +} +static lv_res_t changeCallback(lv_obj_t * btn){ + change = !change; + showAddress(child_index, change); + return LV_RES_OK; +} +static lv_res_t xpubCallback(lv_obj_t * btn){ + titleLbl.text("Your master public key"); + qr.text(xpub.toString()); + dataLbl.text(xpub.toString()); + qr.align(ALIGN_CENTER); + dataLbl.align(ALIGN_CENTER); + return LV_RES_OK; +} + +void showAddress(unsigned int child_index, bool change){ + stringstream title; + title << "Your "; + if(change){ + title << "change "; + }else{ + title << "receiving "; + } + title << "address #" << child_index << ":"; + titleLbl.text(title.str()); + string address = xpub.child(change).child(child_index).address(); + dataLbl.text(address); + dataLbl.align(ALIGN_CENTER); + qr.text(string("bitcoin:") + address); + qr.align(ALIGN_CENTER); +} + +void showAddressScreen(){ + gui.clear(); + titleLbl = Label("Your address"); + titleLbl.size(gui.width(), 20); + titleLbl.position(0, 40); + titleLbl.alignText(ALIGN_TEXT_CENTER); + + dataLbl = Label(" "); + dataLbl.size(gui.width()-100, 100); // full width + dataLbl.position(50, gui.height()-300); + dataLbl.alignText(ALIGN_TEXT_CENTER); + + qr = QR(" "); + qr.size(gui.width()-100); + qr.position(0, 100); + qr.align(ALIGN_CENTER); + + Button btn(nextCallback, "Next address"); + btn.size(gui.width()/3-20, 80); + btn.position(gui.width()*2/3 + 10, gui.height()-100); + + Button btn2(prevCallback, "Previous address"); + btn2.size(gui.width()/3-20, 80); + btn2.position(10, gui.height()-100); + + Button btn3(changeCallback, "Toggle\nchange"); + btn3.size(gui.width()/3-20, 80); + btn3.position(gui.width()/3 + 10, gui.height()-100); + + Button btn4(xpubCallback, "Show xpub"); + btn4.size(gui.width()/2-20, 80); + btn4.position(gui.width()/2+10, gui.height()-200); + + Button btn5(toMenuCallback, "Menu"); + btn5.size(gui.width()/2-20, 80); + btn5.position(10, gui.height()-200); +} + +static lv_res_t toMenuCallback(lv_obj_t * btn){ + showMenu(); + return LV_RES_OK; +} + +static lv_res_t showAddressesCallback(lv_obj_t * btn){ + showAddressScreen(); + showAddress(child_index, change); + return LV_RES_OK; +} +static lv_res_t saveXpubCallback(lv_obj_t * btn){ + // TODO: + // - check if SD card present + // - save xpub to the "xpub.txt" + // - check if write was sucessful + // - show success / fail message + if(!SD.detected()){ + showMessage("Error", "SD card is not present"); + return LV_RES_OK; + } + int res = SD.save("xpub.txt", xpub.toString()); + if(res != SD_SUCCESS){ + showMessage("Error", "Something wrong with SD card"); + return LV_RES_OK; + } + showMessage("Success","Master public key is saved to the SD card"); + return LV_RES_OK; +} +static lv_res_t signConfirmCallback(lv_obj_t * btn){ + if(!SD.detected()){ + showMessage("Error", "SD card is not present"); + return LV_RES_OK; + } + psbt.sign(account); + + uint8_t * raw = (uint8_t *)calloc(psbt.length(), sizeof(uint8_t)); + size_t len = psbt.serialize(raw, psbt.length()); + psbt = PSBT(); + // cout << len << endl; + char * b64 = (char *)calloc(2*len+1, sizeof(char)); + len = toBase64(raw, len, b64, 2*len+1); + free(raw); + + int res = SD.save("signed.psbt", b64); + free(b64); + if(res != SD_SUCCESS){ + showMessage("Error", "Something wrong with SD card"); + return LV_RES_OK; + } + showMessage("Success", "Signed transaction saved to the SD card"); + return LV_RES_OK; +} +void showSignRequest(){ + stringstream ss; + ss << "Sending:\n\n"; + for(unsigned int i=0; i<psbt.tx.outputsNumber; i++){ + // TODO: better to put it into PSBT method like check_output or something + bool mine = false; + if(psbt.txOutsMeta[i].derivationsLen > 0){ + for(unsigned int j=0; j<psbt.txOutsMeta[i].derivationsLen; j++){ + HDPrivateKey prv = account.derive(psbt.txOutsMeta[i].derivations[j].derivation, psbt.txOutsMeta[i].derivations[j].derivationLen); + PublicKey pub = prv.publicKey(); + // TODO: fix public key comparison + if(pub.toString() == psbt.txOutsMeta[i].derivations[j].pubkey.toString()){ + mine = true; + } + } + } + ss << psbt.tx.txOuts[i].address(&Testnet); + if(mine){ + ss << " (change)"; + } + ss << ": " << (float(psbt.tx.txOuts[i].amount)/1e5) << " mBTC\n\n"; + } + ss << "Fee: " << (float(psbt.fee())/1e5) << " mBTC"; + + gui.clear(); + titleLbl = Label("Sign transaction?"); + titleLbl.size(gui.width(), 20); + titleLbl.position(0, 40); + titleLbl.alignText(ALIGN_TEXT_CENTER); + + dataLbl = Label(ss.str()); + dataLbl.size(gui.width()-100, 100); + dataLbl.position(50, 300); + dataLbl.alignText(ALIGN_TEXT_CENTER); + + Button btn(toMenuCallback, "Cancel"); + btn.size(gui.width()/2-45, 80); + btn.position(30, gui.height()-100); + + Button btn2(signConfirmCallback, "Confirm"); + btn2.size(gui.width()/2-45, 80); + btn2.position(gui.width()/2+30, gui.height()-100); +} +static lv_res_t signPSBTCallback(lv_obj_t * btn){ + if(!SD.detected()){ + showMessage("Error", "SD card is not present"); + return LV_RES_OK; + } + string s = SD.read("unsigned.psbt"); + if(s.length() == 0){ + showMessage("Fail", "Can't read unsigned.psbt file"); + return LV_RES_OK; + } + uint8_t raw[1000]; + unsigned int len = fromBase64(s.c_str(), s.length(), raw, sizeof(raw)); + if(len == 0){ + showMessage("Fail", "Can't convert from base64"); + return LV_RES_OK; + } + psbt.reset(); + len = psbt.parse(raw, len); + if(len == 0){ + showMessage("Fail", "Can't parse PSBT"); + return LV_RES_OK; + } + showSignRequest(); + return LV_RES_OK; +} +static lv_res_t wipeCallback(lv_obj_t * btn){ + wipe(); + return LV_RES_OK; +} + +/*************** mnemonic stuff ***************/ + +static lv_res_t newMnemonicCallback(lv_obj_t * btn){ + // TODO: + // - generate random buffer (16 or 32 bytes) + // - create a new mnemonic from it + // - save this mnemonic + // - display it to the user + uint8_t randomBuffer[16]; + getRandomBuffer(randomBuffer, sizeof(randomBuffer)); + string mnemonic = generateMnemonic(randomBuffer, sizeof(randomBuffer)); + saveMnemonic(mnemonic); + showMessage("Write down your recovery phrase:", mnemonic); + initKeys(mnemonic); + return LV_RES_OK; +} + +static lv_res_t showMnemonicCallback(lv_obj_t * btn){ + string mnemonic = loadMnemonic(); + showMessage("Here is your recovery phrase:", mnemonic); + return LV_RES_OK; +} + +static lv_res_t enterMnemonicCallback(lv_obj_t * btn){ + return LV_RES_OK; +} + +void initKeys(const string mnemonic, const string password){ + // TODO: + // - derive root key from the mnemonic and empty password + // - derive account key using m/84'/1'/0'/ derivation path + // - get account master public key + root.fromMnemonic(mnemonic, password); + account = root.derive("m/84'/1'/0'/"); + xpub = account.xpub(); +}