Airgapped hardware wallet based on STM32F469-Discovery board using SD card to pass transaction data

Dependencies:   mbed QSPI_DISCO_F469NI BSP_DISCO_F469NI

main.cpp

Committer:
stepansnigirev
Date:
2019-07-30
Revision:
4:73e20d662d73
Parent:
2:120e0ca2f3a7

File content as of revision 4:73e20d662d73:

#include "mbed.h"
#include "helpers.h"

// bitcoin lib
#include "Bitcoin.h" // Public, Private, HD keys, scripts, raw transactions and stuff
#include "PSBT.h"    // Partially Signed Bitcoin Transaction format

/***************** bitcoin stuff ***************/

string mnemonic;
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

/****************** GUI elements ****************/

// some global scope GUI objects we will be changing in callbacks
Label titleLbl;
Label dataLbl;
QR qr;

Button btn;
Button printBtn;
Label lbl;

/*********** forward declarations **************/

// 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);
// signs transaction after user confirmation
static lv_res_t signConfirmCallback(lv_obj_t * btn);
// derives keys from mnemonic and password
void initKeys(const string mnemonic, const string password = "");
// 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();
// some functions that handle button clicks
static lv_res_t toMenuCallback(lv_obj_t * btn);
static lv_res_t showAddressesCallback(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 enterMnemonicCallback(lv_obj_t * btn);
// shows address on the screen
void showAddress(unsigned int child_index, bool change);

/*********** functions to complete **************/

// generates a new mnemonic
string generateNewMnemonic(){
    // TODO: 
    // - generate random buffer (16 or 32 bytes) - getRandomBuffer(buf, size)
    // - create a new mnemonic from it - generateMnemonic(buf, size)
    // - save this mnemonic
    // - display it to the user

    uint8_t randomBuffer[16];
    getRandomBuffer(randomBuffer, sizeof(randomBuffer));
    string mnemonic = generateMnemonic(randomBuffer, sizeof(randomBuffer));
    return mnemonic;
}

// checks if entered mnemonic is valid
static lv_res_t checkMnemonicCallback(lv_obj_t * btn){
    // TODO:
    // - check mnemonic
    // - if ok, init keys and show success message
    // - otherwise erase the whole mnemonic

    if(checkMnemonic(mnemonic.c_str())){
        saveMnemonic(mnemonic);
        initKeys(mnemonic);
        showMessage("Mnemonic is ok", "Recovered sucessfully");
    }else{
        mnemonic = "";
        dataLbl.text(mnemonic);
    }
    return LV_RES_OK;
}

// generates hd keys from the mnemonic
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();
}

void showAddress(unsigned int child_index, bool change){
    // TODO:
    // - derive an address from xpub according to the derivation
    // - set dataLbl text to the address
    // - set qr text to "bitcoin:address"
    // OPTIONAL:
    // - display both bech32 and nested segwit addresses

    stringstream title;
    title << "Your ";
    if(change){
        title << "change ";
    }else{
        title << "receiving ";
    }
    title << "address #" << child_index << ":";
    titleLbl.text(title.str());
    // generate the address here
    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);
}

// this will be called when we press "save master key" button
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
    // OPTIONAL:
    // - use [fingerprint/derivation]xpub format
    // - create bitcoin core descriptor for bitcoin-cli importmulti

    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;
}

// displays confirmation screen for the transaction signing
void showSignRequest(){
    // TODO:
    // display information of the transaction:
    // - go through all outputs
    // - detect if the address is the change address
    // - if not, show information in the form "address: amount"
    // - if it is change, hide or mark as a change
    // - display the transaction fee
    // OPTIONAL:
    // - verify that pubkey is actually used in the script
    // - do the same for bip49 and check redeem script
    // - check if derivation path is not weird (indexes are reasonable)

    stringstream ss;
    ss << "Sending:\n\n";
    for(unsigned int i=0; i<psbt.tx.outputsNumber; i++){
        bool mine = psbt.isMine(i, account.xpub());
        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);
}

// reads unsigned transaction from SD card
static lv_res_t signPSBTCallback(lv_obj_t * btn){
    // TODO:
    // - check if SD card is there
    // - read data from "unsigned.psbt"
    // - convert it from base64 to raw bytes
    // - parse psbt transaction
    // - call showSignRequest()
    // OPTIONAL
    // - also show signed transaction as a QR code

    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 = new uint8_t[s.length()];
    unsigned int len = fromBase64(s.c_str(), s.length(), raw, s.length());
    if(len == 0){
        delete [] raw;
        showMessage("Fail", "Can't convert from base64");
        return LV_RES_OK;
    }
    psbt.reset();
    len = psbt.parse(raw, len);
    delete [] raw;
    if(len == 0){
        showMessage("Fail", "Can't parse PSBT");
        return LV_RES_OK;
    }
    showSignRequest();
    return LV_RES_OK;
}

// signs transaction
static lv_res_t signConfirmCallback(lv_obj_t * btn){
    // TODO:
    // - check if SD card is still there
    // - serialize the psbt to byte array
    // - convert to base64
    // - save to "signed.psbt"
    // - show success message

    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();
    string b64 = toBase64(raw, len);

    int res = SD.save("signed.psbt", b64.c_str());
    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;
}

/***************** GUI functions ***************/

static lv_res_t newMnemonicCallback(lv_obj_t * btn){
    string mnemonic = generateNewMnemonic();
    saveMnemonic(mnemonic);
    showMessage("Write down your recovery phrase:", mnemonic);
    initKeys(mnemonic);
    return LV_RES_OK;
}

// show next address
static lv_res_t nextCallback(lv_obj_t * btn){
    child_index++;
    showAddress(child_index, change);
    return LV_RES_OK;
}

// show previous address
static lv_res_t prevCallback(lv_obj_t * btn){
    if(child_index > 0){
        child_index--;
        showAddress(child_index, change);
    }
    return LV_RES_OK;
}

// switch to change addresses
static lv_res_t changeCallback(lv_obj_t * btn){
    change = !change;
    showAddress(child_index, change);
    return LV_RES_OK;
}

// show master public key
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;
}

/******************* 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);
}

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 wipeCallback(lv_obj_t * btn){
    wipe();
    return LV_RES_OK;
}

/*************** mnemonic stuff ***************/

static lv_res_t showMnemonicCallback(lv_obj_t * btn){
    string mnemonic = loadMnemonic();
    showMessage("Here is your recovery phrase:", mnemonic);
    return LV_RES_OK;
}

static const char * keys[] = {"q","w","e","r","t","y","u","i","o","p","\n",
                              "a","s","d","f","g","h","j","k","l","\n",
                              " ","z","x","c","v","b","n","m","<",""};

static lv_res_t typeCallback(lv_obj_t * btn, const char * key){
    if(key[0] == '<'){
        if(mnemonic.length() > 0){
            mnemonic = mnemonic.substr(0,mnemonic.length()-1);
        }
    }else{
        mnemonic += key;
    }
    dataLbl.text(mnemonic);
    return LV_RES_OK;
}

static lv_res_t enterMnemonicCallback(lv_obj_t * btn){
    gui.clear();
    mnemonic = "";
    titleLbl = Label("Enter your mnemonic");
    titleLbl.size(gui.width(), 20);
    titleLbl.position(0, 40);
    titleLbl.alignText(ALIGN_TEXT_CENTER);

    dataLbl = Label(mnemonic);
    dataLbl.size(gui.width()-100, 50);
    dataLbl.position(50, 200);
    dataLbl.alignText(ALIGN_TEXT_CENTER);

    Keyboard kb(typeCallback, keys);
    kb.size(gui.width(), gui.height()/3);
    kb.position(0, gui.height()*2/3-150);

    Button btnx(checkMnemonicCallback, "Continue");
    btnx.size(gui.width()-100, 80);
    btnx.position(50, gui.height()-100);
    return LV_RES_OK;
}