Skip to content

Commit

Permalink
Add clone-your-Coldcard feature
Browse files Browse the repository at this point in the history
  • Loading branch information
doc-hex committed Mar 1, 2021
1 parent b18723d commit 22607a7
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 26 deletions.
16 changes: 0 additions & 16 deletions LICENSE

This file was deleted.

1 change: 1 addition & 0 deletions LICENSE
12 changes: 8 additions & 4 deletions 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

Expand Down
150 changes: 144 additions & 6 deletions shared/backups.py
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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\
Expand Down Expand Up @@ -362,7 +365,7 @@ async def done(words):

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)
Expand Down Expand Up @@ -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
Expand All @@ -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
5 changes: 5 additions & 0 deletions shared/flow.py
Expand Up @@ -12,6 +12,7 @@
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:
Expand Down Expand Up @@ -86,6 +87,7 @@ def has_secrets():
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),
]
Expand Down Expand Up @@ -156,6 +158,7 @@ def has_secrets():
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),
]

Expand All @@ -181,10 +184,12 @@ def has_secrets():
]

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),
]
Expand Down
1 change: 1 addition & 0 deletions stm32/COLDCARD/c-modules

0 comments on commit 22607a7

Please sign in to comment.