Initial commit

This commit is contained in:
Ed 2023-07-11 22:16:30 +02:00
commit ec1d89298f
8 changed files with 661 additions and 0 deletions

130
badge.py Normal file
View File

@ -0,0 +1,130 @@
from time import time
from base64 import b16encode
from hashlib import sha3_512
import os.path
from security_const import *
import logging
import binascii
import ndef
from io import BytesIO
from nfc.tag.tt2 import Type2TagCommandError
logging.basicConfig(level='INFO')
log = logging.getLogger('badge')
START_PAGE = {
"NXP NTAG213": 0x11,
"NXP NTAG215": 0x6B,
"NXP NTAG216": 0xCB
}
USER_PAGE_BOUNDS = {
"NXP NTAG213": (0x04, 0x27),
"NXP NTAG215": (0x04, 0x81),
"NXP NTAG216": (0x04, 0xE1),
}
CONFIG_PAGES = {
"NXP NTAG213": (0x29, 0x2A, 0x2B, 0x2C),
"NXP NTAG215": (0x83, 0x84, 0x85, 0x86),
"NXP NTAG216": (0xE3, 0xE4, 0xE5, 0xE6),
}
class Badge:
def __init__(self, tag, uuid=None):
self.tag = tag
self.uuid = uuid
def read_transaction(self):
bounds = USER_PAGE_BOUNDS[self.tag.product]
start_page = START_PAGE[self.tag.product] # We assume transactions are 22 pages wide
data = b""
offset = 0
while 1:
print("Reading", hex(start_page+offset), end=' ')
chunk = self.tag.read(start_page+offset)
print(b16encode(chunk))
if len(data) == 0:
if bytes(chunk[:4]) != HEADER:
print('header is', HEADER)
print('chunk 4', bytes(chunk[:4]))
print('Does not start with header :(')
data += chunk
offset += 4
if FOOTER in chunk:
data = data[:data.find(FOOTER)+4]
break
return data
def write_transaction(self, tx, verify=True):
bounds = USER_PAGE_BOUNDS[self.tag.product]
start = START_PAGE[self.tag.product]
with BytesIO(tx.get_bytes()) as data:
offset = 0
# Write the transaction
while 1:
chunk = data.read(4)
if not chunk: break
print("Writing", hex(start+offset), b16encode(chunk))
assert bounds[0] <= start+offset <= bounds[1]
self.tag.write(start+offset, chunk)
offset += 1
# Read the transaction back
if verify:
print('expected:::', tx.get_bytes())
print('read:::::', self.read_transaction())
assert tx.get_bytes() == self.read_transaction()
def enable_count(self):
page_addr = CONFIG_PAGES[self.tag.product][1]
page = self.tag.read(page_addr)[:4]
if not page[0] & (1<<4):
log.info("Enabling tag counter")
page[0] |= 1 << 4 # Set NFC_CNT_EN = 1
self.tag.write(page_addr, page)
else:
log.info("Tag counter is already enabled!")
try:
count = self.tag.transceive(binascii.unhexlify('3902'), timeout=0.05) # READ_CNT command
except Type2TagCommandError:
log.error("Tag count is enabled, but didn't get any response anyway.")
else:
log.info(f"Tag count is {int.from_bytes(count, byteorder='little')}")
def protect(self):
self.tag.protect(self.password())
def password(self, with_uuid=True):
x = self.tag.identifier
for i in range(ITERATIONS):
x = sha3_512(x + (self.uuid if with_uuid and self.uuid else bytes(0)) + SALT).digest()
return x[:6]
if __name__ == '__main__':
from types import SimpleNamespace
tag = SimpleNamespace()
tag.identifier = bytes(8)
pt = Protect(tag, uuid=b"bye")
start = time()
print('with uuid', b16encode(pt.password()))
print('without uuid', b16encode(pt.password(with_uuid=False)))
print("took", (time()-start)*1000, 'ms')

147
enroll.py Normal file
View File

@ -0,0 +1,147 @@
import nfc
from nfc.clf import RemoteTarget
import logging
from time import time
import os.path
import coloredlogs
coloredlogs.install()
from base64 import b16encode, b16decode
import sys
import ndef
import binascii
from badge import Badge
from os import system
CONFIG_PAGES = {
"NXP NTAG213": (0x29, 0x2A, 0x2B, 0x2C),
"NXP NTAG215": (0x83, 0x84, 0x85, 0x86),
"NXP NTAG216": (0xE3, 0xE4, 0xE5, 0xE6),
}
session_start = time()
if __name__ == '__main__':
log = logging.getLogger('nfc')
nfc_type = sys.argv[1]
balance = 0
if len(sys.argv) > 2:
balance = sys.argv[2]
elif nfc_type == 'coin':
log.error("You cannot create empty coins.")
exit()
clf = nfc.ContactlessFrontend()
log.info(f"Enrolling mode: {nfc_type}")
log.info(f"Initial balance: {balance}")
try:
assert clf.open('tty:USB0:pn532')
except TimeoutError:
log.error('Got TimeoutError on reader connection attempt.')
except AssertionError:
log.error('There is no reader connected.')
exit()
# Set baud rate to 115200 to prevent reader from dying
clf.device.chipset.set_serial_baudrate(115200)
clf.device.chipset.transport.baudrate = 115200
try:
while 1:
target = clf.sense(RemoteTarget('106A'), iterations=10)
if not target: continue
start = time()
tag = nfc.tag.activate(clf, target)
if not tag:
print("Tag was gone while writing.")
break
if tag.product not in ["NXP NTAG215"]:
raise Exception(f"This tag ({tag.product}) is not compatible with the barcard system.")
#print("\n".join(tag.dump()))
#break
badge = Badge(tag)
password = badge.password()
try:
has_password = tag.read(CONFIG_PAGES[tag.product][0])[3] < 0xFF
except:
break
if has_password:
log.warning("TAG has a password already! Trying to re-enroll anyway.")
tag.authenticate(password)
#tx = Transaction(tag_id=tag.identifier)
try:
# Disable the mirror, for now
page_addr = CONFIG_PAGES[tag.product][0]
page = tag.read(page_addr)[:4]
page[0] &= 0b111111 # Disable UID + COUNT mirror
tag.write(page_addr, page)
except:
break
# Activate it again
tag = nfc.tag.activate(clf, target)
if has_password:
tag.authenticate(password)
# Write the correct url depending on the type of wanted badge
if nfc_type == 'badge':
url = "https://go.foxo.me/fz23/bxxxxxxxxxxxxxxxxxxxxx"
else:
url = f"https://go.foxo.me/fz23/cxxxxxxxxxxxxxxxxxxxxx"
tag.format()
tag.ndef.records = [ndef.uri.UriRecord(url), ]
if tag.ndef.records[0].uri != url:
raise Exception("Written uri is different than expected.")
boundary = tag.read(0x08)
if not boundary.startswith(b"e/fz23/"):
raise Exception(f"This tag does not seem to correctly store ndef data. Got wrong {boundary}")
try:
page_addr = CONFIG_PAGES[tag.product][0]
page = tag.read(page_addr)[:4]
page[0] |= 0b11 << 6 # Enable UID + COUNT mirror
if tag.product == 'NXP NTAG215':
page[2] = 0x0A # Location of UID + COUNT
elif tag.product == 'NXP NTAG213':
page[2] = 0x0B
tag.write(page_addr, page)
log.info("UID + COUNT mirror enabled.")
except:
raise
break
print('EC')
badge.enable_count()
tag = nfc.tag.activate(clf, target)
if not has_password:
print('PRT')
badge.protect()
with open(f'log_{int(session_start)}.txt', 'a') as f:
f.write(f"{b16encode(tag.identifier).decode()},{nfc_type},{balance}\n")
system('mpv success.wav')
log.info(f"{time()-start:.2f} tag write done.")
while clf.sense(RemoteTarget('106A')): pass
system
except KeyboardInterrupt:
pass
clf.close()

58
readme.md Normal file
View File

@ -0,0 +1,58 @@
# ⚠️ DO NOT USE THIS! READ BELOW! ⚠️
Due to a flaw of nfcpy (or maybe not?) tags enrolled by this script will have the capability bit set to 0x31. **This for some reason makes the NDEF payload only compatible with Android phones**!
This script can be used to enroll your NFC tags in the badge system. Effectively, it will:
1. Enable the READ_CNT flag, to enable the read counter
2. Add a NDEF Uri so that the badge can be scanned by attendants
3. Protect the badge with a password, generated uniquely for every tag
4. Log the UID and badge type, so you can import it in your backend
## Requirements
1. Check the requirements.txt file for python lib requirements
2. Install mpv to play a sound at the end of every succesfull write, or don't (and remove the row)
3. Change the SALT to a very secure password and adjust the iteration count to a sensible number for your computer
4. Connect a PN532 scanner via USB
5. Enjoy!
## Run the script
```python3 enroll.py [badge/coin] [initial balance]```
The badge/coin changes the url, while initial balance changes the column on the data export (the log_*.txt files)
## The ASCII mirror!
You will notice that every badge has a generic "https://go.foxo.me/fz23/bxxxxxxxxxxxxxxxxxxxxx" url written to it.
This is on purpose since after this write the UID + COUNT mirror is enabled.
This is a function of the NDEF tags that lets you "mirror" various registers in ascii format to other arbitrary addresses.
The code following the ndef part will effectively make so that when you read the link will actually become `"/fz23/b[TAG IDENTIFIER]x[READ COUNT]"` (e.g. `"/fz23/b048E77929A7181x0000EB"`) without any manual intervention and in a completely tear-free way.
## The "toys" folder
Inside of the toys folder there are two files which were a proof of concept for the storage of monetary transactions inside of NFC tags, for use to buy drinks and similar. Eventually, the system was moved completely server-side (due to slowness in writing the cards and risk of tearing), and the entire system scrapped.
The system consisted in ed25519-based transaction signatures, where offline clients (aka the PoS) would be able to verify and write transactions. The cards would actually store two transactions and then change a pointer atomically to prevent half-written transaction from being considered.
The transaction format is as follows:
```
[ CA$H ]
[signature ] * 16
[load][used]
[keyi][txid]
[timestamp ]
[ uuid ]
[ F0X0 ]
```
- CA$H and F0X0 are header and footer of the transaction (4 bytes each)
- signature = ed25519 signature of the transaction (4 * 16 = 64 bytes)
- load (2 bytes) + used (2 bytes) = the amount of loaded and used money. These two values can only increase and the balance is calculated from subtracting them so that "replay" attacks are harder
- keyi = id of the key used to sign the transaction (2 bytes)
- txid = increasing id of the transaction (2 bytes)
- timestamp = unix ts of the transaction (4 bytes)
- uuid = a custom uuid to identify the card (this was added because at one point we accidentally bought counterfeit cards with duplicated NFC identifiers)

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
coloredlogs
nfcpy

View File

@ -0,0 +1,2 @@
SALT = b"This is the salt to generate the badge password :)"
ITERATIONS = 10000

BIN
success.wav Normal file

Binary file not shown.

176
toys/enroll_card.py Normal file
View File

@ -0,0 +1,176 @@
import nfc
import sys
from nfc.clf import RemoteTarget
from nfc.tag.tt2 import Type2TagCommandError
import logging
from base64 import b64encode, b16encode
from os.path import join
from time import time
from hashlib import sha3_256
import ndef
import coloredlogs
import binascii
import os
import json
from colorama import init, Fore, Back, Style
coloredlogs.install(fmt='%(levelname)s %(message)s')
init()
uids = json.load(open('uids.json'))
def get_hash(payload, count=10000, salt=b"x!"):
data = sha3_256(payload + (salt or bytes(0)))
for _ in range(count):
data = sha3_256(data.digest() + (salt or bytes(0)))
return data.digest()
def activate_and_unlock(*args, **kwargs):
tag = nfc.tag.activate(*args, **kwargs)
tag.has_password = False
assert tag.signature
if not tag.ndef.is_writeable:
tag.has_password = True
unlock_result = tag.authenticate(password=get_hash(tag.identifier+tag.signature))
if not unlock_result:
log.error("This tag is locked and password is incorrect!")
return
return tag
if __name__ == '__main__':
log = logging.getLogger('nfc')
clf = nfc.ContactlessFrontend()
try:
assert clf.open('tty:USB0:pn532')
except TimeoutError:
log.error('Got TimeoutError on reader connection attempt.')
except AssertionError:
log.error('There is no reader connected.')
exit()
clf.device.chipset.set_serial_baudrate(115200)
clf.device.chipset.transport.baudrate = 115200
log.info(f"Setting device baud rate to {clf.device.chipset.transport.baudrate}.")
print(Style.BRIGHT + "What mode would you like to work in?\ns = single enroll\nm = mass enroll\n>>> ", end='')
mode = input()[0]
if mode == 'm':
print(Style.BRIGHT + "Choose the number from where to begin enrolling. To stop enrolling, just use CTRL+C to exit the program.\n>>> ", end='')
try:
uid = int(input())
except KeyboardInterrupt: exit()
except:
log.error("The supplied number is not valid.")
exit()
print(Fore.GREEN + "✔ Mass enroll mode" + Style.RESET_ALL)
else:
print(Fore.GREEN + "✔ Single enroll mode" + Style.RESET_ALL)
try:
while 1:
# Ask for a number if in single enroll mode.
if mode == 's':
while 1:
print(Style.BRIGHT + "Input the attendee number\n>>> ", end='')
try:
uid = int(input())
except KeyboardInterrupt: exit()
except:
log.error("The supplied number is not valid.")
continue
else:
break
user_id = uids[str(uid)][0]
url = "http://go.foxo.me/" + user_id
print(Fore.GREEN + "Attendee info:")
print(f"NAME: {uids[str(uid)][1]}")
print(f" INT: {uid}")
print(f"CODE: {user_id}\n")
print(Fore.YELLOW + "⌛ Waiting for nfc tag", end='')
# Wait for a successful reading
while 1:
target = clf.sense(RemoteTarget('106A'), iterations=10)
if not target:
print('.', end='')
sys.stdout.flush()
continue
break
print(Style.RESET_ALL)
# Tag was found, write the details
start = time()
tag = activate_and_unlock(clf, target)
if not tag:
log.error("Tag was gone mid-write.")
continue
if tag.product != 'NXP NTAG215':
log.error("Incorrect tag type detected! Tag must be a NXP 215!")
while clf.sense(RemoteTarget('106A')): pass
continue
if not tag.signature:
log.error("Tag signature was not read correctly. Try again.")
while clf.sense(RemoteTarget('106A')): pass
continue
password = get_hash(tag.identifier+tag.signature, 10000)[:6]
# If tag count is disabled, enable it
try:
count = tag.transceive(binascii.unhexlify('3902'), timeout=0.05) # READ_CNT command
except Type2TagCommandError:
page = tag.read(0x84)[:4]
if not page[0] & (1<<4):
log.info("Enabling tag counter")
page[0] |= 1 << 4 # Set NFC_CNT_EN = 1
self.tag.write(0x84, page)
tag = activate_and_unlock(clf, target)
count = tag.transceive(binascii.unhexlify('3902'), timeout=0.05) # READ_CNT command
print(Fore.CYAN+Style.BRIGHT)
print(f"UUID: {b16encode(tag.identifier).decode()}")
print(f"SIGN: {b16encode(tag.signature).decode()}")
print(f"PASS: {b16encode(password[:4]).decode()}")
print(f"PWCK: {b16encode(password[4:]).decode()}")
print(" CNT:", int.from_bytes(count, byteorder='little'))
print(Style.RESET_ALL)
tag.ndef.records = [ndef.UriRecord(url), ]
try:
assert tag.ndef.has_changed is False
except AssertionError:
log.error("NDEF data was written incorrectly!")
while clf.sense(RemoteTarget('106A')): pass
continue
else:
log.info("Writing URL")
if not tag.has_password:
log.info("Setting password")
tag.protect(password=password, read_protect=False, protect_from=0)
print(Back.GREEN + Fore.BLACK + f"\n✔ Badge enrolled with success in {time()-start:.2f}s!" + Style.RESET_ALL)
uid += 1
while clf.sense(RemoteTarget('106A')): pass
os.system('clear')
except KeyboardInterrupt:
pass
clf.close()

146
toys/transaction.py Normal file
View File

@ -0,0 +1,146 @@
from time import time
from base64 import b16encode
import os.path
import libnacl.utils, libnacl.sign
from transaction_const import *
from io import BytesIO
from hashlib import sha3_256
import logging
from random import randint
logging.basicConfig(level='DEBUG')
log = logging.getLogger('tx')
'''
[ CA$H ]
[signature ] * 16
[load][used]
[keyi][txid]
[timestamp ]
[ uuid ]
[ F0X0 ]
21*4 = 84bytes
76 bytes
'''
FIELDS = {
'signature': (0, 63),
'enc_key': 64,
'key_id': 65,
'load_amt': (66, 67),
'used_amt': (68, 69),
'tx_id': (70, 71),
'ts': (72, 75),
'uuid': (76, 79)
}
class Transaction:
def __init__(self, tag_id, nfc_bytes=None):
# These are needed for proper randomness
self.tag_id = tag_id
self.data = bytearray(max([(x[1] if isinstance(x, tuple) else x) for x in FIELDS.values()])+1)
self.set_field('key_id', KEY_ID)
self.set_field('ts', time())
self.set_field('uuid', libnacl.randombytes(4))
if nfc_bytes:
self.load(nfc_bytes)
def set_field(self, fid, val):
if isinstance(FIELDS[fid], int):
flen = 1
sbyte = FIELDS[fid]
else:
flen = 1 + FIELDS[fid][1] - FIELDS[fid][0]
sbyte = FIELDS[fid][0]
if isinstance(val, float):
val = int(val)
if isinstance(val, int):
val = val.to_bytes(flen)
assert len(val) <= flen
if flen > 1:
log.info(f"Writing {fid} at bytes {sbyte}:{sbyte+flen-1}: 0x{b16encode(val).decode()}")
self.data[sbyte:sbyte+flen] = val
else:
log.info(f"Writing {fid} at byte {sbyte}: 0x{b16encode(val).decode()}")
self.data[sbyte] = val[0]
def get_field(self, fid):
if isinstance(FIELDS[fid], int):
flen = 1
sbyte = FIELDS[fid]
else:
flen = 1 + FIELDS[fid][1] - FIELDS[fid][0]
sbyte = FIELDS[fid][0]
if flen > 1:
val = bytes(self.data[sbyte:sbyte+flen])
else:
val = self.data[sbyte]
return val
def load(self, data):
if data.startswith(HEADER):
data = data[len(HEADER):]
if data.find(FOOTER):
data = data[:-data.find(FOOTER)]
self.data = data
def sign(self):
self.set_field('key_id', KEY_ID)
signer = libnacl.sign.Signer(KEY_SEED)
signature = signer.signature(bytes(self.data[FIELDS['signature'][1]+1:]+self.tag_id))
self.set_field('signature', signature)
log.info("Signed message with success!")
self.verify()
def get_bytes(self):
return HEADER + self.data + FOOTER
# TODO: implement these
def topup(self, amt):
self.load_amount += amt
self.tx_bytes
def spend(self, amt):
self.used_amount += amt
def verify(self):
verifier = libnacl.sign.Verifier(KEYS[self.get_field('key_id')])
verifier.verify(bytes(self.data + self.tag_id))
log.info("Signature has passed!")
if __name__ == '__main__':
print("Testing transaction.")
tx = Transaction(tag_id=bytes(8))
tx.sign()
#for fname in FIELDS:
# print(' ', fname, tx.get_field(fname), 'len')
#res = tx.get_bytes()
#tx.verify()
#tx = Transaction(tag_id=bytes(8), nfc_bytes=res)
#res2 = tx.get_bytes()