From f1ce63812c8ea9df19d310ba469fea9bcc6cb849 Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Wed, 6 Aug 2025 19:06:00 +0200 Subject: [PATCH] restore backup via USB; new CLI commnad "restore"; add backup restore to "upload" cmd --- README.md | 33 +++++++++++++++++++++++++++++++++ ckcc/cli.py | 44 +++++++++++++++++++++++++++++++++++++++++--- ckcc/protocol.py | 20 ++++++++++++++++++++ 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b7aa5ea..89d36ed 100644 --- a/README.md +++ b/README.md @@ -159,3 +159,36 @@ Commands: get Get registered miniscript wallet by name. ls List registered miniscript wallet names. ``` + +## Backup/Restore + +``` +Usage: ckcc backup [OPTIONS] + + Creates 7z encrypted backup file after prompting user to remember a massive + passphrase. Downloads the AES-encrypted data backup and by default, saves + into current directory using a filename based on today's date. + +Options: + -d, --outdir DIRECTORY Save into indicated directory (auto filename) + -o, --outfile filename.7z Name for backup file + --help Show this message and exit. +``` + +``` +Usage: ckcc restore [OPTIONS] backup.7z + + Uploads 7z encrypted backup file & starts backup restore process. User needs + to specify what kind of backup is being uploaded. Default is 7z encrypted + file with word-based password. Use -p/--password flag if your backup has + custom not word-based password. User is prompted to enter backup password on + the device. + +Options: + -c, --plaintext Force plaintext restore. No need to use if file has proper + '.txt' suffix + -p, --password This backup has custom password. Not words. + -t, --tmp Force restoring backup as temporary seed. Only works for + seedless Coldcard. + --help Show this message and exit. +``` \ No newline at end of file diff --git a/ckcc/cli.py b/ckcc/cli.py index 808a9f7..40a554e 100755 --- a/ckcc/cli.py +++ b/ckcc/cli.py @@ -282,11 +282,14 @@ def real_file_upload(fd, dev, blksize=MAX_BLK_LEN, do_upgrade=False, do_reboot=T help='Attempt multisig enroll using file') @click.option('--miniscript', is_flag=True, help='Attempt miniscript enroll using file') -def file_upload(filename, blksize, multisig, miniscript): +@click.option('--backup', is_flag=True, + help='Upload encrypted backup') +def file_upload(filename, blksize, multisig, miniscript, backup): """Send file to Coldcard (PSBT transaction or firmware)""" - if multisig and miniscript: - click.echo("Failed: Only one can be specified from miniscript/multisig") + if sum([multisig, miniscript, backup]) > 1: + # only 1 or None can be True + click.echo("Failed: Only one can be specified from miniscript/multisig/backup") sys.exit(1) # NOTE: mostly for debug/dev usage. @@ -298,6 +301,9 @@ def file_upload(filename, blksize, multisig, miniscript): dev.send_recv(CCProtocolPacker.multisig_enroll(file_len, sha)) elif miniscript: dev.send_recv(CCProtocolPacker.miniscript_enroll(file_len, sha), timeout=None) + elif backup: + dev.send_recv(CCProtocolPacker.restore_backup(file_len, sha), timeout=None) + @main.command('upgrade') @@ -614,6 +620,38 @@ def start_backup(outdir, outfile): click.echo("Wrote %d bytes into: %s\nSHA256: %s" % (len(result), fn, str(b2a_hex(chk), 'ascii'))) +@main.command('restore') +@click.argument('filename', type=click.File('rb'), metavar="backup.7z") +@click.option('-c', '--plaintext', is_flag=True, + help="Force plaintext restore. No need to use if file has proper '.txt' suffix") +@click.option('-p', '--password', is_flag=True, + help="This backup has custom password. Not words.") +@click.option('-t', '--tmp', is_flag=True, + help="Force restoring backup as temporary seed. Only works for seedless Coldcard.") +@display_errors +def restore_backup(filename, plaintext, password, tmp): + """ + Uploads 7z encrypted backup file & starts backup restore process. User needs to specify + what kind of backup is being uploaded. Default is 7z encrypted file with word-based password. + Use -p/--password flag if your backup has custom not word-based password. + User is prompted to enter backup password on the device. + """ + + if not plaintext and filename.name.lower().endswith(".txt"): + plaintext = True + + if plaintext and password: + # only 1 or None can be True + click.echo("Failed: Plaintext backup cannot have custom password.") + sys.exit(1) + + with get_device() as dev: + file_len, sha = real_file_upload(filename, dev) + dev.send_recv( + CCProtocolPacker.restore_backup(file_len, sha, password, plaintext, tmp), + timeout=None) + + @main.command('addr') @click.argument('path', default=None, metavar='[m/1/2/3]', required=False) @click.option('--segwit', '-s', is_flag=True, help='Show in segwit native (p2wpkh, bech32)') diff --git a/ckcc/protocol.py b/ckcc/protocol.py index 868ac02..4799d9d 100644 --- a/ckcc/protocol.py +++ b/ckcc/protocol.py @@ -67,6 +67,26 @@ class CCProtocolPacker: # prompts user with password for encrypted backup return b'back' + @staticmethod + def restore_backup(length, file_sha, custom_pwd=False, plaintext=False, tmp=False): + # backup file has to be already uploaded + # custom_pwd: (bool) .7z encrypted with custom password + # plaintext: (bool) clear-text (dev) + # tmp (bool) force load as tmp, effective only on seed-less CC + assert len(file_sha) == 32 + assert not (custom_pwd and plaintext) + + bf = 0 + if custom_pwd: + bf |= 1 + if plaintext: + bf |= 2 + if tmp: + bf |= 4 + + return pack('<4sI32sB', b'rest', length, file_sha, bf) + + @staticmethod def encrypt_start(device_pubkey, version=USB_NCRY_V1): supported_versions = [USB_NCRY_V1, USB_NCRY_V2]