Add NFC pushtx from any existing file

This commit is contained in:
Peter D. Gray 2024-06-12 14:55:01 -04:00
parent 85c1a8cc63
commit de41770853
No known key found for this signature in database
GPG Key ID: A2DCD558C2BE5D7C
5 changed files with 113 additions and 17 deletions

View File

@ -1412,13 +1412,22 @@ Erases and reformats MicroSD card. This is not a secure erase but more of a quic
async def nfc_share_file(*A):
# Mk4: Share txt, txn and PSBT files over NFC.
# Share txt, txn and PSBT files over NFC.
from glob import NFC
try:
await NFC.share_file()
except Exception as e:
await ux_show_story(title="ERROR", msg="Failed to share file. %s" % str(e))
async def nfc_pushtx_file(*A):
# Share a signed txn over NFC using PushTx technology
# - requires a signed txn, perhaps from another system on SD card
from glob import NFC
try:
await NFC.push_tx_from_file()
except Exception as e:
await ux_show_story(title="ERROR", msg="Failed to share file. %s" % str(e))
async def nfc_show_address(*A):
from glob import NFC

View File

@ -806,7 +806,7 @@ class ApproveTransaction(UserAuthorizedAction):
except BaseException as exc:
return await self.failure("PSBT output failed", exc)
from glob import NFC, settings
from glob import NFC, settings, PSRAM
if self.do_finalize and txid and not hsm_active:
@ -814,7 +814,8 @@ class ApproveTransaction(UserAuthorizedAction):
url = settings.get('ptxurl', False)
if NFC and url:
try:
await NFC.share_push_tx(url, txid, TXN_OUTPUT_OFFSET, self.result[0], self.result[1])
data = PSRAM.read_at(TXN_OUTPUT_OFFSET, self.result[0])
await NFC.share_push_tx(url, txid, data, self.result[1])
return
except:
# continue normally if it fails, perhaps too big?

View File

@ -329,6 +329,7 @@ NFCToolsMenu = [
MenuItem('Verify Address', f=nfc_address_verify),
MenuItem('File Share', f=nfc_share_file),
MenuItem('Import Multisig', f=import_multisig_nfc),
MenuItem('NFC Push Tx', f=nfc_pushtx_file, predicate=lambda: settings.get("ptxurl", False)),
]
AdvancedNormalMenu = [

View File

@ -239,16 +239,13 @@ class NFCHandler:
return await self.share_start(n)
async def share_push_tx(self, url, txid, file_offset, txn_len, txn_sha):
async def share_push_tx(self, url, txid, txn, txn_sha, line2=None):
# Given a signed TXN, we convert to URL which a web backend can broadcast directly
# - using base64url encoding
# - just appends to provided URL
# - keeps showing it until they press CANCEL
# - may fail late if txn is too big.. not clear what limit is
#
if (txn_len * 1.4) >= MAX_NFC_SIZE:
raise ValueError('too big')
from glob import PSRAM
from utils import b2a_base64url
from chains import current_chain
@ -256,8 +253,7 @@ class NFCHandler:
if is_https:
url = url[8:]
url += 't=' + b2a_base64url(PSRAM.read_at(file_offset, txn_len)) \
+ '&c=' + b2a_base64url(txn_sha[-8:])
url += 't=' + b2a_base64url(txn) + '&c=' + b2a_base64url(txn_sha[-8:])
ch = current_chain()
if ch.ctype != 'BTC':
@ -266,12 +262,69 @@ class NFCHandler:
n = ndef.ndefMaker()
n.add_url(url, https=is_https)
if line2 is None:
line2 = "Signed TXID: %s%s" % (txid[0:8], txid[-8:])
while 1:
done = await self.share_start(n, prompt="Tap to broadcast, CANCEL when done",
line2="Signed TXID: %s%s" % (txid[0:8], txid[-8:]))
line2=line2)
if done: break
async def push_tx_from_file(self):
# Pick (signed txn) file from SD card and broadcast via PushTx
# - assumes .txn extension (required)
# - hex encoding or binary
# - txid is filename, if 64 chars long; else shown on-screen
# - assumes txn on same chain as this CC is; ie. not testnet typically
from actions import file_picker
from files import CardSlot, CardMissingError, needs_microsd
from glob import settings
def is_suitable(fname):
return fname.lower().endswith('.txn')
url = settings.get('ptxurl', False)
assert url # or else not in menu, cant get here.
while 1:
fn = await file_picker(min_size=10, max_size=MAX_NFC_SIZE*2, taster=is_suitable)
if not fn: return
basename = fn.split('/')[-1]
try:
with CardSlot() as card:
with open(fn, 'rb') as fp:
data = fp.read(MAX_NFC_SIZE*2)
assert len(data) < MAX_NFC_SIZE*2, "bad read"
except CardMissingError:
await needs_microsd()
return
# maybe decode
if data[2:6] == b'000000':
# it's a txn, and we wrote as hex
data = a2b_hex(data)
elif data[1:4] == bytes(3):
# looks like binary
pass
else:
raise ValueError("Doesn't look like txn?")
sha = ngu.hash.sha256s(data)
txid = basename[0:64]
line2 = None
if len(txid) != 64:
# assume a r random filename, and not easy to recalc txid here
# so show filename instead
line2 = 'File: ' + basename
if len(line2) > 34: # CHARS_W
line2 = line2[:32]+'' # 34-2=32 => because double-width char
await self.share_push_tx(url, txid, data, sha, line2=line2)
async def share_psbt(self, file_offset, psbt_len, psbt_sha, label=None):
# we just signed something, share it over NFC
if psbt_len >= MAX_NFC_SIZE:
@ -562,7 +615,7 @@ class NFCHandler:
if not fn: return
basename = fn.split('/')[-1]
ctype = fn.split('.')[-1].lower()
ext = fn.split('.')[-1].lower()
try:
with CardSlot() as card:
@ -573,24 +626,24 @@ class NFCHandler:
await needs_microsd()
return
if data[2:6] == b'000000' and ctype == 'txn':
if data[2:6] == b'000000' and ext == 'txn':
# it's a txn, and we wrote as hex
data = a2b_hex(data)
if ctype == 'psbt':
if ext == 'psbt':
sha = ngu.hash.sha256s(data)
await self.share_psbt(data, len(data), sha, label="PSBT file: " + basename)
elif ctype == 'txn':
elif ext == 'txn':
sha = ngu.hash.sha256s(data)
txid = basename[0:64]
if len(txid) != 64:
# maybe some other txn file?
txid = None
await self.share_signed_txn(txid, data, len(data), sha)
elif ctype == 'txt':
elif ext == 'txt':
await self.share_text(data.decode())
else:
raise ValueError(ctype)
raise ValueError(ext)
async def import_multisig_nfc(self, *a):
# user is pushing a file downloaded from another CC over NFC

View File

@ -9,6 +9,36 @@ TAG_DATA = bytearray(8196)
# unix/work/nfc-dump.ndef
DATA_FILE = 'nfc-dump.ndef'
def debug_dump_nfc(data):
import ndef, sys
from utils import B2A
# Debug aid: pull out URL's and maybe other things
try:
st, ll, *_ = ndef.ccfile_decode(data)
data = data[st:st+ll]
if not data:
print("sim_nfc: CCFILE is empty (ie. blank tag, no NDEF records)")
else:
for urn, msg, meta in ndef.record_parser(data):
try:
msg = bytes(msg).decode() # from memory view
except UnicodeError:
msg = B2A(msg)
if urn == 'urn:nfc:wkt:U':
prefix = 'https://' if meta.get('prefix') == 4 else 'http://'
print("sim_nfc: NDEF URL record:\n\n%s%s\n" % (prefix, msg))
else:
print("sim_nfc: NDEF record: %s %s meta=%r" % (urn, msg, meta))
except Exception as exc:
print("sim_nfc: Failed to decode CCFILE/NDEFS contained?")
sys.print_exception(exc)
class SimulatedNFCHandler(NFCHandler):
def __init__(self):
self.rf_on = False
@ -34,6 +64,8 @@ class SimulatedNFCHandler(NFCHandler):
self._atime = atime
print("%d bytes of NDEF written to work/nfc-dump.ndef .. Ctrl-N or touch or read that file to simulate taps" % n)
debug_dump_nfc(data)
async def wipe(self, full_wipe):
print("NFC chip wiped (full=%d)" % int(full_wipe))