ckcc-protocol/ckcc/protocol.py
2018-07-13 11:35:15 -04:00

203 lines
5.9 KiB
Python

#
# Details of our USB level protocol. Shared file between desktop and embedded.
#
# - first 4 bytes of all messages is the command code or response code
# - use <I and <H, never >H
#
from struct import pack, unpack_from
from .constants import *
class CCProtoError(RuntimeError):
def __str__(self):
return self.args[0]
class CCUserRefused(RuntimeError):
def __str__(self):
return 'You refused permission to do the operation'
class CCBusyError(RuntimeError):
def __str__(self):
return 'Coldcard is handling another request right now'
class CCProtocolPacker:
# returns a lamba that will take correct args
# and then give you a binary string to encode the
# request
@staticmethod
def logout():
return pack('4s', b'logo')
@staticmethod
def reboot():
return pack('4s', b'rebo')
@staticmethod
def version():
# returns a string, with newline separators
return pack('4s', b'vers')
@staticmethod
def ping(msg):
# returns whatever binary you give it
return b'ping' + bytes(msg)
@staticmethod
def check_mitm():
return b'mitm'
@staticmethod
def start_backup():
# prompts user with password for encrytped backup
return b'back'
@staticmethod
def encrypt_start(device_pubkey, version=0x1):
assert len(device_pubkey) == 64, "want uncompressed 64-byte pubkey, no prefix byte"
return pack('<4sI64s', b'ncry', version, device_pubkey)
@staticmethod
def upload(offset, total_size, data):
# note: see MAX_MSG_LEN above
assert len(data) <= MAX_MSG_LEN, 'badlen'
return pack('<4sII', b'upld', offset, total_size) + data
@staticmethod
def download(offset, length, file_number=0):
assert 0 <= file_number < 2
return pack('<4sIII', b'dwld', offset, length, file_number)
@staticmethod
def sha256():
return b'sha2'
@staticmethod
def sign_transaction(length, file_sha, finalize=False):
# must have already uploaded binary, and give expected sha256
assert len(file_sha) == 32
return pack('<4sII32s', b'stxn', length, int(finalize), file_sha)
@staticmethod
def sign_message(raw_msg, subpath='m', addr_fmt=AF_CLASSIC):
# only begins user interaction
return pack('<4sIII', b'smsg', addr_fmt, len(subpath), len(raw_msg)) \
+ subpath.encode('ascii') + raw_msg
@staticmethod
def get_signed_msg():
# poll completion/results of message signing
return b'smok'
@staticmethod
def get_backup_file():
# poll completion/results of backup
return b'bkok'
@staticmethod
def get_signed_txn():
# poll completion/results of transaction signing
return b'stok'
@staticmethod
def get_xpub(subpath='m'):
# takes a string, like: m/44'/0'/23/23
return b'xpub' + subpath.encode('ascii')
@staticmethod
def show_address(subpath, addr_fmt=AF_CLASSIC):
# takes a string, like: m/44'/0'/23/23
# shows on screen, no feedback from user expected
return pack('<4sI', b'show', addr_fmt) + subpath.encode('ascii')
@staticmethod
def sim_keypress(key):
# Simulator ONLY: pretend a key is pressed
return b'XKEY' + key
@staticmethod
def bag_number(new_number=b''):
# one time only: put into bag, or readback bag
return b'bagi' + bytes(new_number)
class CCProtocolUnpacker:
# Take a binary response, and turn it into a python object
# - we support a number of signatures, and expand as needed
# - some will be general-purpose, but others can be very specific to one command
# - given full rx message to work from
# - this is done after un-framing
@classmethod
def decode(cls, msg):
assert len(msg) >= 4
sign = str(msg[0:4], 'utf8', 'ignore')
d = getattr(cls, sign, cls)
if d is cls:
raise CCProtoError('Unknown response signature: ' + repr(sign))
return d(msg)
# struct info for each response
def okay(msg):
# trivial response, w/ no content
assert len(msg) == 4
return None
# low-level errors
def fram(msg):
raise CCProtoError("Framing Error", str(msg[4:], 'utf8'))
def err_(msg):
raise CCProtoError("Remote Error: " + str(msg[4:], 'utf8', 'ignore'), msg[4:])
def refu(msg):
# user didn't want to approve something
raise CCUserRefused()
def busy(msg):
# user didn't want to approve something
raise CCBusyError()
def biny(msg):
# binary string: length implied by msg framing
return msg[4:]
def int1(msg):
return unpack_from('<I', msg, 4)[0]
def int2(msg):
return unpack_from('<2I', msg, 4)
def int3(msg):
return unpack_from('<3I', msg, 4)
def mypb(msg):
# response to "ncry" command:
# - the (uncompressed) pubkey of the Coldcard
# - info about master key: xpub, fingerprint of that
# - anti-MitM: remote xpub
# session key is SHA256(point on sec256pk1 in binary) via D-H
dev_pubkey, fingerprint, xpub_len = unpack_from('64sII', msg, 4)
xpub = msg[-xpub_len:] if xpub_len else b''
return dev_pubkey, fingerprint, xpub
def asci(msg):
# hex/base58 string or other for-computers string, which isn't international
return msg[4:].decode('ascii')
def smrx(msg):
# message signing result. application specific!
# returns actual address used (text), and raw binary signature (65 bytes)
aln = unpack_from('<I', msg, 4)[0]
return msg[8:aln+8].decode('ascii'), msg[8+aln:]
def strx(msg):
# txn signing result, or other file operation. application specific!
# returns length of resulting PSBT and it's sha256
ln, sha = unpack_from('<I32s', msg, 4)
return ln, sha
# EOF