242 lines
7.5 KiB
Python
242 lines
7.5 KiB
Python
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
|
#
|
|
# ndef.py -- NDEF records: making them and parsing them.
|
|
#
|
|
# - see ../docs/nfc-coldcard.md for background.
|
|
# - cross platform file
|
|
#
|
|
from struct import pack, unpack
|
|
from binascii import hexlify as b2a_hex
|
|
|
|
# From ST AN4911 - Fixed CC file that uses E2 to indicate 2-byte lengths
|
|
# - allocates entire memory (64k) to tag usage, read only
|
|
# - followed the "NDEF File Control TLV" tag (0x03) but not the length
|
|
CC_FILE = bytes([0xE2, 0x43, 0x00, 0x01, 0x00, 0x00, 0x04, 0x00, 0x03])
|
|
|
|
# When we are writable, empty file is given
|
|
CC_WR_FILE = bytes([0xE2, 0x40, 0x00, 0x01, 0x00, 0x00, 0x04, 0x00,
|
|
0x03, 0x00, # empty
|
|
0xfe, # end marker
|
|
])
|
|
|
|
class ndefMaker:
|
|
'''
|
|
make a few different types of NDEF records, very limited and only
|
|
for our use-cases
|
|
'''
|
|
def __init__(self):
|
|
# ndef records: len, TNF value, type (byte string), payload (bytes)
|
|
self.lst = []
|
|
|
|
def add_text(self, msg):
|
|
# assume: english, utf-8
|
|
ln = len(msg) + 3
|
|
self.lst.append( (ln, 0x1, b'T', b'\x02en' + msg.encode()) )
|
|
|
|
def add_url(self, url, https=True):
|
|
# always https since we're in Bitcoin, or else full URL
|
|
# - ascii only
|
|
proto_code = b'\x04' if https else b'\x00'
|
|
self.lst.append( (len(url)+1, 0x1, b'U', proto_code + url.encode()) )
|
|
|
|
def add_large_object(self, ext_type, offset, obj_len):
|
|
# zero-copy a binary file from PSRAM into NFC flash
|
|
# - or accept bytes
|
|
if isinstance(offset, int):
|
|
from glob import PSRAM
|
|
self.lst.append( (obj_len, 0x4, ext_type.encode(),
|
|
PSRAM.read_at(offset, obj_len)) )
|
|
else:
|
|
self.add_custom(ext_type, offset)
|
|
|
|
def add_custom(self, ext_type, payload):
|
|
# "NFC Forum external Type" using bitcoin.org domain
|
|
self.lst.append( (len(payload), 0x4, ext_type.encode(), payload) )
|
|
|
|
def add_mime_data(self, mime_type, payload):
|
|
# "image/png" or other RFC mime types, including application/json
|
|
self.lst.append( (len(payload), 0x2, mime_type.encode(), payload) )
|
|
|
|
def bytes(self):
|
|
# Walk list of records, and set various framing bits to first bytes of each
|
|
# and concat.
|
|
# - resist urge to make this a generator, it's not worth it.
|
|
rv = bytearray(CC_FILE)
|
|
|
|
# calc total length of all records
|
|
ln = sum((3 if ln <= 255 else 6) + len(ntype) + len(rec)
|
|
for (ln, _, ntype, rec) in self.lst)
|
|
if ln <= 0xfe:
|
|
rv.append(ln)
|
|
else:
|
|
rv.append(0xff)
|
|
rv.extend(pack('>H', ln))
|
|
|
|
last = len(self.lst) - 1
|
|
for n, (ln, tnf, ntype, rec) in enumerate(self.lst):
|
|
# First byte of the NDEF record: it's a bitmask + TNF 3-bit value
|
|
|
|
# TNF=1 => well-known type
|
|
# TNF=2 => mime-type from RFC 2046
|
|
# TNF=4 => NFC Forum external type
|
|
assert 0 < tnf < 7
|
|
first = tnf
|
|
|
|
if ln <= 255:
|
|
first |= 0x10 # SR=1
|
|
|
|
if n == 0:
|
|
first |= 0x80 # = MB Message Begin
|
|
if n == last:
|
|
first |= 0x40 # = ME Message End
|
|
|
|
rv.append(first) # NDEF header byte
|
|
rv.append(len(ntype)) # type-length always one, if well-known
|
|
if ln <= 255:
|
|
rv.append(ln) # value-length
|
|
else:
|
|
rv.extend(pack('>I', ln))
|
|
rv.extend(ntype)
|
|
rv.extend(rec)
|
|
|
|
rv.append(0xfe) # Terminator TLV
|
|
|
|
return rv
|
|
|
|
def ccfile_decode(taste):
|
|
# Given first 16 bytes of tag's memory (user memory):
|
|
# - returns start and length of real Ndef records
|
|
# - and is_writable flag
|
|
# - and max size of tag memory capacity, in bytes (poorly spec'ed / compat issues)
|
|
ex, b1, b2 = taste[0:3]
|
|
assert b1 & 0xf0 == 0x40 # bad version.
|
|
if ex == 0xE1:
|
|
# "one byte addressing mode" -- max of 2040 bytes, 4-6 byte header
|
|
if b2 != 0x00:
|
|
st = 4
|
|
mlen = b2 # aka MLEN
|
|
else:
|
|
st = 6
|
|
mlen = unpack('>H', taste[4:4+2])[0]
|
|
elif ex == 0xE2:
|
|
# 8-byte CC Field, allows 2 byte address mode
|
|
st = 8
|
|
mlen = unpack('>H', taste[6:6+2])[0]
|
|
else:
|
|
raise ValueError("bad first byte") # not one of 2 magic values we support
|
|
|
|
assert taste[st] == 0x03 # special first TLV
|
|
st += 1
|
|
ll = taste[st:st+3]
|
|
if ll[0] == 0xff:
|
|
ll = unpack('>H', ll[1:])[0]
|
|
st += 3
|
|
else:
|
|
ll = ll[0]
|
|
st += 1
|
|
|
|
assert 0 <= ll < 8196 # 64kbit max part
|
|
|
|
return st, ll, ((b1 & 3) == 0), mlen*4
|
|
|
|
def record_parser(msg):
|
|
# Given body of ndef records, yield a tuple for each record:
|
|
# - type info, as urn string
|
|
# - bytes of body
|
|
# - dict of meta data, appropriate to type
|
|
# - we gag on chunks
|
|
pos = 0
|
|
while 1:
|
|
meta = {}
|
|
hdr = msg[pos]
|
|
|
|
MB = hdr & 0x80
|
|
ME = hdr & 0x40
|
|
CF = hdr & 0x20
|
|
SR = hdr & 0x10
|
|
IL = hdr & 0x08
|
|
TNF = hdr & 0x7
|
|
|
|
assert not CF, "no chunks please"
|
|
assert (pos == 0) == bool(MB), "first needs MB set"
|
|
|
|
ty_len = msg[pos+1]
|
|
pos += 2
|
|
|
|
if SR: # short record: one byte for payload length
|
|
pl_len = msg[pos]
|
|
pos += 1
|
|
else:
|
|
pl_len = unpack('>I', msg[pos:pos+4])[0]
|
|
pos += 4
|
|
|
|
id_len = 0
|
|
if IL:
|
|
id_len = msg[pos]
|
|
pos += 1
|
|
|
|
urn = None
|
|
|
|
# type is next
|
|
ty = msg[pos:pos+ty_len]
|
|
pos += ty_len
|
|
|
|
if TNF == 0x0: # empty
|
|
assert ty_len == pl_len == 0, "ty_len = pl_len = 0"
|
|
urn = None
|
|
elif TNF == 0x1: # WKT
|
|
urn = 'urn:nfc:wkt:'
|
|
urn += ty.decode()
|
|
|
|
if ty == b'T':
|
|
# unwrap Text
|
|
hdr2 = msg[pos]
|
|
assert hdr2 & 0xc0 == 0x00, "only UTF supported"
|
|
lang_len = hdr2 & 0x3f
|
|
|
|
meta['lang'] = msg[pos+1:pos+1 + lang_len].decode()
|
|
skip = 1 + lang_len
|
|
pl_len -= skip
|
|
pos += skip
|
|
|
|
if ty == b'U':
|
|
# limited URL support
|
|
meta['prefix'] = msg[pos]
|
|
pos += 1
|
|
pl_len -= 1
|
|
|
|
elif TNF == 0x2: # mime-type, like 'image/png'
|
|
urn = ty.decode()
|
|
elif TNF == 0x3: # absolute URI??
|
|
urn = 'uri'
|
|
elif TNF == 0x4: # NFC forum external type
|
|
urn = 'urn:nfc:ext:'
|
|
urn += ty.decode()
|
|
else:
|
|
raise ValueError("TNF") # unknown/reserved/not handled.
|
|
|
|
if IL:
|
|
meta['ident'] = bytes(msg[pos:pos+id_len])
|
|
pos += id_len
|
|
|
|
yield urn, memoryview(msg)[pos:pos+pl_len], meta
|
|
|
|
if ME: return
|
|
|
|
pos += pl_len
|
|
assert pos < len(msg), "missing ME/truncated"
|
|
|
|
|
|
# EOF
|
|
|
|
# from NXP:
|
|
# E1 40 80 09 03 10 D1 01 0C 55 01 6E 78 70 2E 63 6F 6D 2F 6E 66 63 FE 00
|
|
|
|
# ST AN5439 -- works
|
|
# 4-byte CCfile then "NDEF File Control TLV":
|
|
# E1 40 40 00 03 2A
|
|
# NDef records:
|
|
# D1012655016578616D706C652E636F6D2F74656D703D303030302F746170636F756E7465723D30303030FE000000
|
|
#
|
|
# m=b'\xe1@@\x00\x03*\xd1\x01&U\x01example.com/temp=0000/tapcounter=0000\xfe\x00\x00\x00'
|