From ec1d89298f98ebf5e96d3976580830edc9bfc2d5 Mon Sep 17 00:00:00 2001 From: Ed Date: Tue, 11 Jul 2023 22:16:30 +0200 Subject: [PATCH] Initial commit --- badge.py | 130 ++++++++++++++++++++++++++++ enroll.py | 147 +++++++++++++++++++++++++++++++ readme.md | 58 +++++++++++++ requirements.txt | 2 + security_const.example.py | 2 + success.wav | Bin 0 -> 52516 bytes toys/enroll_card.py | 176 ++++++++++++++++++++++++++++++++++++++ toys/transaction.py | 146 +++++++++++++++++++++++++++++++ 8 files changed, 661 insertions(+) create mode 100644 badge.py create mode 100644 enroll.py create mode 100644 readme.md create mode 100644 requirements.txt create mode 100644 security_const.example.py create mode 100644 success.wav create mode 100644 toys/enroll_card.py create mode 100644 toys/transaction.py diff --git a/badge.py b/badge.py new file mode 100644 index 0000000..cfc7f78 --- /dev/null +++ b/badge.py @@ -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') diff --git a/enroll.py b/enroll.py new file mode 100644 index 0000000..6b4d177 --- /dev/null +++ b/enroll.py @@ -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() + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..44ce8f6 --- /dev/null +++ b/readme.md @@ -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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7b705c0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +coloredlogs +nfcpy diff --git a/security_const.example.py b/security_const.example.py new file mode 100644 index 0000000..44249f0 --- /dev/null +++ b/security_const.example.py @@ -0,0 +1,2 @@ +SALT = b"This is the salt to generate the badge password :)" +ITERATIONS = 10000 diff --git a/success.wav b/success.wav new file mode 100644 index 0000000000000000000000000000000000000000..aae49a4f928243553d5e66073deefdbff36b84b3 GIT binary patch literal 52516 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 ", 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() + diff --git a/toys/transaction.py b/toys/transaction.py new file mode 100644 index 0000000..ad9254f --- /dev/null +++ b/toys/transaction.py @@ -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() + + + +