Add clone-your-Coldcard feature
This commit is contained in:
parent
b18723dddb
commit
22607a760c
16
LICENSE
16
LICENSE
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
@ -1,15 +1,19 @@
|
|||||||
## 4.0.0 - , 2021
|
## 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
|
- now using Bitcoin Core's "libsecp256k1" for EC crypto operations
|
||||||
- super fast pure-assembly AES256-CTR code makes USB communications faster
|
- 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:
|
- HSM/CKBunker mode:
|
||||||
- users with passwords will have to be recreated as hash used has changed
|
- 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
|
- Bugfix: CSV of addresses explorer export via Address Explorere, when account number
|
||||||
was used, did not reflect the (non-zero) 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.
|
- 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
|
## 3.2.2 - Jan 14, 2021
|
||||||
|
|
||||||
|
|||||||
@ -2,11 +2,11 @@
|
|||||||
#
|
#
|
||||||
# backups.py - Save and restore backup data.
|
# 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 hexlify as b2a_hex
|
||||||
from ubinascii import unhexlify as a2b_hex
|
from ubinascii import unhexlify as a2b_hex
|
||||||
from utils import imported, xfp2str
|
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
|
import version, ujson
|
||||||
from uio import StringIO
|
from uio import StringIO
|
||||||
import seed
|
import seed
|
||||||
@ -175,7 +175,7 @@ async def restore_from_dict(vals):
|
|||||||
|
|
||||||
await ux_show_story('Everything has been successfully restored. '
|
await ux_show_story('Everything has been successfully restored. '
|
||||||
'We must now reboot to install the '
|
'We must now reboot to install the '
|
||||||
'updated settings and/or seed.', title='Success!')
|
'updated settings and seed.', title='Success!')
|
||||||
|
|
||||||
from machine import reset
|
from machine import reset
|
||||||
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))
|
ch = await seed.word_quiz(words, limited=(num_pw_words//3))
|
||||||
if ch == 'x': return
|
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
|
# Just do the writing
|
||||||
from glob import dis
|
from glob import dis
|
||||||
from files import CardSlot, CardMissingError
|
from files import CardSlot, CardMissingError
|
||||||
@ -289,6 +289,9 @@ async def write_complete_backup(words, fname_pattern, write_sflash):
|
|||||||
if ch == 'x': break
|
if ch == 'x': break
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if not allow_copies:
|
||||||
|
return
|
||||||
|
|
||||||
if copy == 0:
|
if copy == 0:
|
||||||
while 1:
|
while 1:
|
||||||
msg = '''Backup file written:\n\n%s\n\n\
|
msg = '''Backup file written:\n\n%s\n\n\
|
||||||
@ -362,7 +365,7 @@ async def restore_complete(fname_or_fd):
|
|||||||
|
|
||||||
the_ux.push(m)
|
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
|
# Open file, read it, maybe decrypt it; return string if any error
|
||||||
# - some errors will be shown, None return in that case
|
# - some errors will be shown, None return in that case
|
||||||
# - no return if successful (due to reboot)
|
# - 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)
|
'\n\nTried:\n\n' + password)
|
||||||
finally:
|
finally:
|
||||||
fd.close()
|
fd.close()
|
||||||
|
|
||||||
|
if file_cleanup:
|
||||||
|
file_cleanup(fname_or_fd)
|
||||||
|
|
||||||
except CardMissingError:
|
except CardMissingError:
|
||||||
await needs_microsd()
|
await needs_microsd()
|
||||||
return
|
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.
|
# this leads to reboot if it works, else errors shown, etc.
|
||||||
return await restore_from_dict(vals)
|
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
|
# EOF
|
||||||
|
|||||||
@ -12,6 +12,7 @@ from multisig import make_multisig_menu
|
|||||||
from address_explorer import address_explore
|
from address_explorer import address_explore
|
||||||
from users import make_users_menu
|
from users import make_users_menu
|
||||||
from drv_entro import drv_entro_start
|
from drv_entro import drv_entro_start
|
||||||
|
from backups import clone_start, clone_write_data
|
||||||
|
|
||||||
# Optional feature: HSM
|
# Optional feature: HSM
|
||||||
if version.has_fatram:
|
if version.has_fatram:
|
||||||
@ -86,6 +87,7 @@ SDCardMenu = [
|
|||||||
MenuItem('Export Wallet', menu=WalletExportMenu),
|
MenuItem('Export Wallet', menu=WalletExportMenu),
|
||||||
MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd),
|
MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd),
|
||||||
MenuItem('Upgrade From SD', f=microsd_upgrade),
|
MenuItem('Upgrade From SD', f=microsd_upgrade),
|
||||||
|
MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data),
|
||||||
MenuItem('List Files', f=list_files),
|
MenuItem('List Files', f=list_files),
|
||||||
MenuItem('Format Card', f=wipe_sd_card),
|
MenuItem('Format Card', f=wipe_sd_card),
|
||||||
]
|
]
|
||||||
@ -156,6 +158,7 @@ BackupStuffMenu = [
|
|||||||
MenuItem("Backup System", f=backup_everything),
|
MenuItem("Backup System", f=backup_everything),
|
||||||
MenuItem("Verify Backup", f=verify_backup),
|
MenuItem("Verify Backup", f=verify_backup),
|
||||||
MenuItem("Restore Backup", f=restore_everything), # just a redirect really
|
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),
|
MenuItem("Dump Summary", f=dump_summary),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -181,10 +184,12 @@ VirginSystem = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
ImportWallet = [
|
ImportWallet = [
|
||||||
|
# xxxxxxxxxxxxxxxx
|
||||||
MenuItem("24 Words", menu=start_seed_import, arg=24),
|
MenuItem("24 Words", menu=start_seed_import, arg=24),
|
||||||
MenuItem("18 Words", menu=start_seed_import, arg=18),
|
MenuItem("18 Words", menu=start_seed_import, arg=18),
|
||||||
MenuItem("12 Words", menu=start_seed_import, arg=12),
|
MenuItem("12 Words", menu=start_seed_import, arg=12),
|
||||||
MenuItem("Restore Backup", f=restore_everything),
|
MenuItem("Restore Backup", f=restore_everything),
|
||||||
|
MenuItem("Clone Coldcard", menu=clone_start),
|
||||||
MenuItem("Import XPRV", f=import_xprv),
|
MenuItem("Import XPRV", f=import_xprv),
|
||||||
MenuItem("Dice Rolls", f=import_from_dice),
|
MenuItem("Dice Rolls", f=import_from_dice),
|
||||||
]
|
]
|
||||||
|
|||||||
1
stm32/COLDCARD/c-modules
Symbolic link
1
stm32/COLDCARD/c-modules
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../external/c-modules
|
||||||
Loading…
Reference in New Issue
Block a user