"""Response unpackers for the Handshake Ledger app. The app returns COMPRESSED (33-byte) secp256k1 public keys, unlike the Ledger boilerplate, which returns the 65-byte uncompressed form. """ from struct import unpack from typing import Tuple def _pop_sized(buffer: bytes, size: int) -> Tuple[bytes, bytes]: return buffer[size:], buffer[0:size] def _pop_len_prefixed(buffer: bytes) -> Tuple[bytes, bytes]: n = buffer[0] return buffer[1 + n:], buffer[1:n + 1] def compress_pubkey(uncompressed: bytes) -> bytes: """65-byte `04 || X || Y` to 33-byte `02|03 || X`.""" assert len(uncompressed) == 65 and uncompressed[0] == 0x04 prefix = 0x02 if uncompressed[64] % 2 == 0 else 0x03 return bytes([prefix]) + uncompressed[1:33] def unpack_get_app_name_response(response: bytes) -> str: return response.decode("ascii") def unpack_get_version_response(response: bytes) -> Tuple[int, int, int]: assert len(response) == 3 major, minor, patch = unpack("BBB", response) return (major, minor, patch) def unpack_get_public_key_response(response: bytes) -> Tuple[bytes, bytes, str]: """`pk_len(1) || pk(33) || cc_len(1) || cc(32) || addr_len(1) || addr`.""" response, pubkey = _pop_len_prefixed(response) response, chaincode = _pop_len_prefixed(response) response, address = _pop_len_prefixed(response) assert len(pubkey) == 33, f"expected a compressed pubkey, got {len(pubkey)} bytes" assert len(chaincode) == 32 assert len(response) == 0 return pubkey, chaincode, address.decode("ascii") def unpack_sign_input_response(response: bytes) -> Tuple[bytes, int, bytes]: """`sig(64) || sighash_type(1) || pk_len(1) || pk(33)`.""" response, signature = _pop_sized(response, 64) response, sighash = _pop_sized(response, 1) response, pubkey = _pop_len_prefixed(response) assert len(pubkey) == 33 assert len(response) == 0 return signature, sighash[0], pubkey def unpack_sign_message_response(response: bytes) -> Tuple[int, bytes, bytes]: """`recovery_id(1) || sig(64) || pk(33)`, 98 bytes.""" assert len(response) == 98, f"expected 98 bytes, got {len(response)}" return response[0], response[1:65], response[65:]