From ddc60f80fb6fb2892bbcf69481f52ec7b2748e4c Mon Sep 17 00:00:00 2001 From: David Rodenkirchen Date: Thu, 26 Feb 2026 08:18:45 +0100 Subject: [PATCH] initial commit --- ClipboardReaderService.py | 70 ++++++++++++++++++++++++++ card_reader.py | 101 ++++++++++++++++++++++++++++++++++++++ card_writer.py | 86 ++++++++++++++++++++++++++++++++ hmac_key.example.txt | 1 + main.py | 58 ++++++++++++++++++++++ secret.py | 7 +++ 6 files changed, 323 insertions(+) create mode 100644 ClipboardReaderService.py create mode 100644 card_reader.py create mode 100644 card_writer.py create mode 100644 hmac_key.example.txt create mode 100644 main.py create mode 100644 secret.py diff --git a/ClipboardReaderService.py b/ClipboardReaderService.py new file mode 100644 index 0000000..4313bd5 --- /dev/null +++ b/ClipboardReaderService.py @@ -0,0 +1,70 @@ +import time +import pyperclip + +from smartcard.CardRequest import CardRequest +from smartcard.Exceptions import CardConnectionException +from smartcard.CardConnection import CardConnection +from smartcard.util import toHexString + +from card_reader import read_card + + +class ClipboardReaderService: + def __init__(self) -> None: + self._is_running = True + self._last_uid = None + + def run(self) -> None: + print("Reader ready.") + + while self._is_running: + try: + card_request = CardRequest(timeout=None) + card_service = card_request.waitforcard() + connection = card_service.connection + + try: + connection.connect(CardConnection.T1_protocol) + + GET_UID = [0xFF, 0xCA, 0x00, 0x00, 0x00] + uid, sw1, sw2 = connection.transmit(GET_UID) + + if sw1 != 0x90: + raise CardConnectionException("UID read failed") + + uid_hex = toHexString(uid) + + if uid_hex == self._last_uid: + connection.disconnect() + time.sleep(0.5) + continue + + self._last_uid = uid_hex + + user_id = read_card(connection) + + if user_id is not None: + pyperclip.copy(str(user_id)) + else: + pyperclip.copy("Fehler: Erneut scannen") + + except CardConnectionException: + pass + + finally: + try: + connection.disconnect() + except: + pass + + time.sleep(0.5) + + except (KeyboardInterrupt, SystemExit): + self.stop() + + def stop(self) -> None: + self._is_running = False + +if __name__ == "__main__": + service = ClipboardReaderService() + service.run() diff --git a/card_reader.py b/card_reader.py new file mode 100644 index 0000000..f7c65a7 --- /dev/null +++ b/card_reader.py @@ -0,0 +1,101 @@ +import hashlib +import hmac +import struct +from typing import Optional + +from smartcard.CardRequest import CardRequest +from smartcard.CardConnection import CardConnection + +from secret import SECRET + + +def read_page(connection, page): + apdu = [0xFF, 0xB0, 0x00, page, 0x04] + data, sw1, sw2 = connection.transmit(apdu) + if sw1 == 0x90 and sw2 == 0x00: + return data[:4] + return None + +def extract_user_id(raw: bytes) -> int | None: + try: + # Must be exactly 10 bytes (2 ID + 8 MAC) + if len(raw) != 10: + return None + + user_bytes = raw[:2] + stored_mac = raw[2:] + + expected_mac = hmac.new( + SECRET, + user_bytes, + hashlib.sha256 + ).digest()[:8] + + # Constant-time comparison + if not hmac.compare_digest(stored_mac, expected_mac): + return None + + # Decode big-endian unsigned short + user_id = struct.unpack(">H", user_bytes)[0] + + return user_id + + except Exception as e: + print("Error extracting user id:", e) + return None + +def read_card(connection) -> Optional[int]: + # Read pages 4–15 + try: + raw = [] + for page in range(4, 16): + data = read_page(connection, page) + if data: + raw.extend(data) + + # Parse NDEF + if raw[0] == 0x03: # NDEF TLV + length = raw[1] + ndef = raw[2:2 + length] + + if ndef[0] == 0xD1 and ndef[3] == 0x54: + lang_len = ndef[4] + payload_hex = bytes(ndef[5 + lang_len:]).decode("utf-8") + payload_bytes = bytes.fromhex(payload_hex) + + return extract_user_id(payload_bytes) + except Exception as e: + print("Error reading card:", e) + return None + return None + +def perform_single_console_read() -> None: + """ Prints the user ID of the card to console """ + print("Auf Ticket warten...") + card_request = CardRequest(timeout=None) + card_service = card_request.waitforcard() + connection = card_service.connection + connection.connect(CardConnection.T1_protocol) + + user_id = read_card(connection) + if user_id is not None: + print("User ID:", user_id) + else: + print("User ID nicht auf Ticket gefunden") + +def main(): + print("Auf Ticket warten...") + card_request = CardRequest(timeout=None) + card_service = card_request.waitforcard() + connection = card_service.connection + connection.connect(CardConnection.T1_protocol) + + user_id = read_card(connection) + if user_id is not None: + print("User ID:", user_id) + else: + print("User ID nicht auf Ticket gefunden") + + +if __name__ == "__main__": + perform_single_console_read() \ No newline at end of file diff --git a/card_writer.py b/card_writer.py new file mode 100644 index 0000000..6dfc164 --- /dev/null +++ b/card_writer.py @@ -0,0 +1,86 @@ +import hmac +import hashlib +import struct + +from smartcard.CardConnection import CardConnection +from smartcard.CardRequest import CardRequest + +from secret import SECRET + + +def generate_payload(user_id: int) -> bytes: + user_bytes = struct.pack(">H", user_id) + + mac = hmac.new( + SECRET, + user_bytes, + hashlib.sha256 + ).digest() + + mac_truncated = mac[:8] + + return user_bytes + mac_truncated + +def write_page(connection, page, data4): + apdu = [0xFF, 0xD6, 0x00, page, 0x04] + data4 + response, sw1, sw2 = connection.transmit(apdu) + return sw1 == 0x90 and sw2 == 0x00 + +def write_ndef_text(connection, text, start_page=4) -> bool: + text_bytes = text.encode("utf-8") + lang = b"en" + + ndef_record = bytearray() + + ndef_record.append(0xD1) + ndef_record.append(0x01) + ndef_record.append(len(text_bytes) + 1 + len(lang)) + ndef_record.append(0x54) + ndef_record.append(len(lang)) + ndef_record.extend(lang) + ndef_record.extend(text_bytes) + + tlv = bytearray() + tlv.append(0x03) + tlv.append(len(ndef_record)) + tlv.extend(ndef_record) + tlv.append(0xFE) + + # Pad to multiple of 4 bytes + while len(tlv) % 4 != 0: + tlv.append(0x00) + + page = start_page + for i in range(0, len(tlv), 4): + chunk = list(tlv[i:i + 4]) + if not write_page(connection, page, chunk): + print(f"Konnte Seite {page} nicht schreiben. Ticket kaputt.") + return False + page += 1 + + print("Erfolgreich geschrieben") + return True + +def write_secure_user_id(connection, user_id: int): + payload = generate_payload(user_id) + hex_string = payload.hex().upper() + write_ndef_text(connection, hex_string) + +def main(): + print("Auf Ticket warten...") + card_request = CardRequest(timeout=None) + card_service = card_request.waitforcard() + connection = card_service.connection + connection.connect(CardConnection.T1_protocol) + + try: + user_id = int(input("User ID eingeben: ")) + except ValueError: + print("User ID muss numerisch sein!") + exit(1) + + write_secure_user_id(connection, user_id) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/hmac_key.example.txt b/hmac_key.example.txt new file mode 100644 index 0000000..54ef4e4 --- /dev/null +++ b/hmac_key.example.txt @@ -0,0 +1 @@ +Alkohol1 \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..83f05cd --- /dev/null +++ b/main.py @@ -0,0 +1,58 @@ +from smartcard.CardRequest import CardRequest +from smartcard.util import toHexString +from smartcard.CardConnection import CardConnection + +def read_page(connection, page): + apdu = [0xFF, 0xB0, 0x00, page, 0x04] + data, sw1, sw2 = connection.transmit(apdu) + if sw1 == 0x90 and sw2 == 0x00: + return data[:4] + return None + + +def main(): + c = input("1 = read, 2 = write") + print("Waiting for NTAG...") + card_request = CardRequest(timeout=None) + card_service = card_request.waitforcard() + connection = card_service.connection + connection.connect(CardConnection.T1_protocol) + + if c == "1": + # UID + GET_UID = [0xFF, 0xCA, 0x00, 0x00, 0x00] + uid, _, _ = connection.transmit(GET_UID) + print("UID:", toHexString(uid)) + + # Read pages 4–15 + raw = [] + for page in range(4, 16): + data = read_page(connection, page) + if data: + raw.extend(data) + + print("\nRaw HEX:") + print(toHexString(raw)) + + # Parse NDEF + if raw[0] == 0x03: # NDEF TLV + length = raw[1] + ndef = raw[2:2+length] + + if ndef[0] == 0xD1 and ndef[3] == 0x54: # Text record + lang_len = ndef[4] + text = bytes(ndef[5+lang_len:]).decode("utf-8") + print("\nDecoded NDEF Text:") + print(text) + else: + print("\nUnsupported NDEF record type") + else: + print("\nNo NDEF message found") + + if c == "2": + + write_ndef_text(connection, "EZLAN00000123") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/secret.py b/secret.py new file mode 100644 index 0000000..e139022 --- /dev/null +++ b/secret.py @@ -0,0 +1,7 @@ +try: + f = open("hmac_key.txt", "r") +except FileNotFoundError: + print("Datei 'hmac_key.txt' fehlt.") + exit(1) +SECRET = f.readline().strip().encode("utf-8") +f.close()