From 22607a760ca87391b34ca388cea065d11e641c59 Mon Sep 17 00:00:00 2001 From: "Peter D. Gray" Date: Mon, 1 Mar 2021 09:36:26 -0500 Subject: [PATCH] Add clone-your-Coldcard feature --- LICENSE | 17 +---- releases/ChangeLog.md | 12 ++-- shared/backups.py | 150 +++++++++++++++++++++++++++++++++++++-- shared/flow.py | 5 ++ stm32/COLDCARD/c-modules | 1 + 5 files changed, 159 insertions(+), 26 deletions(-) mode change 100644 => 120000 LICENSE create mode 120000 stm32/COLDCARD/c-modules diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 97760481..00000000 --- a/LICENSE +++ /dev/null @@ -1,16 +0,0 @@ - - (c) Copyright 2017-2020 by Coinkite Inc. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -in the file COPYING. If not, see . - diff --git a/LICENSE b/LICENSE new file mode 120000 index 00000000..e5584ffb --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +COPYING-CC \ No newline at end of file diff --git a/releases/ChangeLog.md b/releases/ChangeLog.md index ab249a08..7f4e7e98 100644 --- a/releases/ChangeLog.md +++ b/releases/ChangeLog.md @@ -1,15 +1,19 @@ ## 4.0.0 - , 2021 -- Major internal changes! Minimal external change... +- Major internal changes! Minimal external changes... - now using Bitcoin Core's "libsecp256k1" for EC crypto operations - super fast pure-assembly AES256-CTR code makes USB communications faster - - new optimized SHA256 and SHA256(SHA256()) code in use + - newly optimized SHA256 and SHA256(SHA256()) code + - all BIP39 related code replaced - HSM/CKBunker mode: - users with passwords will have to be recreated as hash used has changed +- New feature: Secure Device Cloning. Using a MicroSD card, copy your Coldcard's secrets + and settings to a blank Coldcard. Very quick and easy, uses public key encryption + (Diffie-Hellman key exchange) and AES-256-CBC for the transfer. - Bugfix: CSV of addresses explorer export via Address Explorere, when account number was used, did not reflect the (non-zero) account number. -- Enhancement: Show a progress bar during slow parts of the login process. - Enhancement: Paper wallet features restored as they were previously. Same cautions apply. -- Last remaining GPL code removed, so licence is now MIT+CC on everything. +- Enhancement: Show a progress bar during slow parts of the login process. +- Remaining GPL code has been removed, so licence is now MIT+CC on everything. ## 3.2.2 - Jan 14, 2021 diff --git a/shared/backups.py b/shared/backups.py index f62cf738..eca60d90 100644 --- a/shared/backups.py +++ b/shared/backups.py @@ -2,11 +2,11 @@ # # backups.py - Save and restore backup data. # -import compat7z, stash, ckcc, chains, gc, sys, bip39 +import compat7z, stash, ckcc, chains, gc, sys, bip39, uos, ngu from ubinascii import hexlify as b2a_hex from ubinascii import unhexlify as a2b_hex from utils import imported, xfp2str -from ux import ux_show_story, ux_confirm +from ux import ux_show_story, ux_confirm, ux_dramatic_pause import version, ujson from uio import StringIO import seed @@ -175,7 +175,7 @@ async def restore_from_dict(vals): await ux_show_story('Everything has been successfully restored. ' 'We must now reboot to install the ' - 'updated settings and/or seed.', title='Success!') + 'updated settings and seed.', title='Success!') from machine import reset reset() @@ -214,9 +214,9 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False): ch = await seed.word_quiz(words, limited=(num_pw_words//3)) if ch == 'x': return - return await write_complete_backup(words, fname_pattern, write_sflash) + return await write_complete_backup(words, fname_pattern, write_sflash=write_sflash) -async def write_complete_backup(words, fname_pattern, write_sflash): +async def write_complete_backup(words, fname_pattern, write_sflash=False, allow_copies=True): # Just do the writing from glob import dis from files import CardSlot, CardMissingError @@ -289,6 +289,9 @@ async def write_complete_backup(words, fname_pattern, write_sflash): if ch == 'x': break continue + if not allow_copies: + return + if copy == 0: while 1: msg = '''Backup file written:\n\n%s\n\n\ @@ -362,7 +365,7 @@ async def restore_complete(fname_or_fd): the_ux.push(m) -async def restore_complete_doit(fname_or_fd, words): +async def restore_complete_doit(fname_or_fd, words, file_cleanup=None): # Open file, read it, maybe decrypt it; return string if any error # - some errors will be shown, None return in that case # - no return if successful (due to reboot) @@ -411,6 +414,10 @@ async def restore_complete_doit(fname_or_fd, words): '\n\nTried:\n\n' + password) finally: fd.close() + + if file_cleanup: + file_cleanup(fname_or_fd) + except CardMissingError: await needs_microsd() return @@ -432,4 +439,135 @@ async def restore_complete_doit(fname_or_fd, words): # this leads to reboot if it works, else errors shown, etc. return await restore_from_dict(vals) +async def clone_start(*a): + # Begins cloning process, on target device. + from files import CardSlot, CardMissingError + + ch = await ux_show_story('''Insert a MicroSD card and press OK to start. A small \ +file with an ephemeral public key will be written.''') + if ch != 'y': return + + # pick a random key pair, just for this cloning session + pair = ngu.secp256k1.keypair() + my_pubkey = pair.pubkey().to_bytes(False) + + # write to SD Card, fixed filename for ease of use + try: + with CardSlot() as card: + fname, nice = card.pick_filename('ccbk-start.json', overwrite=True) + + with open(fname, 'wb') as fd: + fd.write(ujson.dumps(dict(pubkey=b2a_hex(my_pubkey)))) + + except CardMissingError: + await needs_microsd() + return + except Exception as e: + await ux_show_story('Error: ' + str(e)) + return + + # Wait for incoming clone file, allow retries + ch = await ux_show_story('''Keep power on this Coldcard, and take MicroSD card \ +to source Coldcard. Select Advanced > MicroSD > Clone Coldcard to write to card. Bring that card \ +back and press OK to complete clone process.''') + + while 1: + if ch != 'y': + # try to clean up, but card probably not there? No errors. + try: + with CardSlot() as card: + uos.remove(fname) + except: + pass + + await ux_dramatic_pause('Aborted.', 2) + return + + # Hopefully we have a suitable 7z file now. Pubkey in the filename + incoming = None + try: + with CardSlot() as card: + for path in card.get_paths(): + for fn, ftype, *var in uos.ilistdir(path): + if fn.endswith('-ccbk.7z'): + incoming = path + '/' + fn + his_pubkey = a2b_hex(fn[0:66]) + + assert len(his_pubkey) == 33 + assert 2 <= his_pubkey[0] <= 3 + break + + except CardMissingError: + await needs_microsd() + continue + except Exception as e: + pass + + if incoming: + break + + ch = await ux_show_story("Clone file not found. OK to try again, X to stop.") + + # calculate point + session_key = pair.ecdh_multiply(his_pubkey) + + # "password" is that hex value + words = [b2a_hex(session_key).decode()] + + def delme(xfn): + # Callback to delete file after its read; could still fail but + # need to start over in that case anyway. + uos.remove(xfn) + uos.remove(fname) # ccbk-start.json + + # this will reset in successful case, no return (but delme is called) + prob = await restore_complete_doit(incoming, words, file_cleanup=delme) + + if prob: + await ux_show_story(prob, title='FAILED') + +async def clone_write_data(*a): + # Write encrypted backup file, for cloning purposes, based on a public key + # found on the SD Card. + # - input file must already exist on inserted card + from files import CardSlot, CardMissingError + + try: + with CardSlot() as card: + path = card.get_sd_root() + with open(path + '/ccbk-start.json', 'rb') as fd: + d = ujson.load(fd) + his_pubkey = a2b_hex(d.get('pubkey')) + # expect compress pubkey + assert len(his_pubkey) == 33 + assert 2 <= his_pubkey[0] <= 3 + + # remove any other clone-files on this card, so no confusion + # on receiving end; unlikely they can work anyway since new key each time + for path in card.get_paths(): + for fn, ftype, *var in uos.ilistdir(path): + if fn.endswith('-ccbk.7z'): + try: + uos.remove(path + '/' + fn) + except: + pass + + except (CardMissingError, OSError) as exc: + # Standard msg shown if no SD card detected when we need one. + await ux_show_story("Start this process on the other Coldcard, which will write a file onto MicroSD card as the first step.\n\nInsert that card and try again here.") + return + + # pick our own temp keys for this encryption + pair = ngu.secp256k1.keypair() + my_pubkey = pair.pubkey().to_bytes(False) + session_key = pair.ecdh_multiply(his_pubkey) + + words = [b2a_hex(session_key).decode()] + + fname = b2a_hex(my_pubkey).decode() + '-ccbk.7z' + + await write_complete_backup(words, fname, allow_copies=False) + + await ux_show_story("Done.\n\nTake this MicroSD card back to other Coldcard and continue from there.") + # EOF diff --git a/shared/flow.py b/shared/flow.py index c0fde706..3cbedb9e 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -12,6 +12,7 @@ from multisig import make_multisig_menu from address_explorer import address_explore from users import make_users_menu from drv_entro import drv_entro_start +from backups import clone_start, clone_write_data # Optional feature: HSM if version.has_fatram: @@ -86,6 +87,7 @@ SDCardMenu = [ MenuItem('Export Wallet', menu=WalletExportMenu), MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd), MenuItem('Upgrade From SD', f=microsd_upgrade), + MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data), MenuItem('List Files', f=list_files), MenuItem('Format Card', f=wipe_sd_card), ] @@ -156,6 +158,7 @@ BackupStuffMenu = [ MenuItem("Backup System", f=backup_everything), MenuItem("Verify Backup", f=verify_backup), MenuItem("Restore Backup", f=restore_everything), # just a redirect really + MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data), MenuItem("Dump Summary", f=dump_summary), ] @@ -181,10 +184,12 @@ VirginSystem = [ ] ImportWallet = [ + # xxxxxxxxxxxxxxxx MenuItem("24 Words", menu=start_seed_import, arg=24), MenuItem("18 Words", menu=start_seed_import, arg=18), MenuItem("12 Words", menu=start_seed_import, arg=12), MenuItem("Restore Backup", f=restore_everything), + MenuItem("Clone Coldcard", menu=clone_start), MenuItem("Import XPRV", f=import_xprv), MenuItem("Dice Rolls", f=import_from_dice), ] diff --git a/stm32/COLDCARD/c-modules b/stm32/COLDCARD/c-modules new file mode 120000 index 00000000..4a8546fc --- /dev/null +++ b/stm32/COLDCARD/c-modules @@ -0,0 +1 @@ +../../external/c-modules \ No newline at end of file