initial commit

This commit is contained in:
David Rodenkirchen 2026-02-26 08:18:45 +01:00
parent 1b9a89ce73
commit ddc60f80fb
6 changed files with 323 additions and 0 deletions

70
ClipboardReaderService.py Normal file
View File

@ -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()

101
card_reader.py Normal file
View File

@ -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 415
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()

86
card_writer.py Normal file
View File

@ -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()

1
hmac_key.example.txt Normal file
View File

@ -0,0 +1 @@
Alkohol1

58
main.py Normal file
View File

@ -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 415
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()

7
secret.py Normal file
View File

@ -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()