Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
b81b6954ef
|
|||
|
61cce57401
|
|||
|
7f9df9db91
|
|||
|
0e8f1562c8
|
|||
|
a8b5bea8cb
|
|||
|
f77b96ebcc
|
|||
|
a4d047452a
|
|||
|
c30516c2bc
|
|||
|
8b4c3cdec9
|
|||
|
9409ebacf3
|
|||
|
831a653706
|
|||
|
c9f1b1833d
|
|||
|
a2d8ccfd7a
|
|||
|
357d0d0d65
|
|||
| fe91adad08 | |||
|
0d31b9c146
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
__pycache__
|
||||
gatekeeper.db
|
||||
.env
|
||||
.coverage
|
||||
|
||||
@@ -7,6 +7,8 @@ start dev server `uv run fastapi dev`
|
||||
Swagger UI @ http://127.0.0.1:8000/docs
|
||||
start prod server `uv run fastapi run`
|
||||
|
||||
You need to set services.pcscd.enable = true; for the smartcard reader to work
|
||||
|
||||
Issues:
|
||||
- `nix run` currently broken
|
||||
- no door state
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from typing import List
|
||||
|
||||
@@ -6,12 +6,16 @@ from ..model.models import Card
|
||||
from ..services.database import engine, get_session, add_and_refresh
|
||||
from ..services.auth import auth_is_admin
|
||||
import uuid as gen_uuid
|
||||
from app.services.scanner import WriteNewCard, DeleteCard
|
||||
|
||||
card_router = APIRouter(prefix="/cards", tags=["Card"])
|
||||
|
||||
def register_card(group_id: int):
|
||||
uuid = str(gen_uuid.uuid4()) #hier code für mifare registrierung
|
||||
card = Card(group_id=group_id, uuid=uuid)
|
||||
key = WriteNewCard()
|
||||
if key == None:
|
||||
print("No card registered. Check logs!")
|
||||
raise HTTPException(status.HTTP_417_EXPECTATION_FAILED, detail="No card registered. Check logs!")
|
||||
card = Card(group_id=group_id, uuid=key)
|
||||
return card
|
||||
|
||||
@card_router.post("/{group_id}", response_model=Card)
|
||||
@@ -19,14 +23,15 @@ def add_card(*, db: Session = Depends(get_session), group_id: int, admin: bool =
|
||||
card = register_card(group_id)
|
||||
return add_and_refresh(db, card)
|
||||
|
||||
@card_router.delete("/{card_id}")
|
||||
def del_card(*, db: Session = Depends(get_session), card_id: int, admin: bool = Depends(auth_is_admin)):
|
||||
card = db.get(Card, card_id)
|
||||
if card is None:
|
||||
raise HTTPException(status_code=404, detail="Card not found")
|
||||
db.delete(card)
|
||||
db.commit()
|
||||
return {"message": "Card deleted successfully"}
|
||||
@card_router.get("/delete")
|
||||
def del_card(*, db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||
key = DeleteCard()
|
||||
# card = db.get(Card, card_id)
|
||||
# if card is None:
|
||||
# raise HTTPException(status_code=404, detail="Card not found")
|
||||
# db.delete(card)
|
||||
# db.commit()
|
||||
# return {"message": "Card deleted successfully"}
|
||||
##TBH not a big fan of having creation using group_id but deletion using card_id
|
||||
@card_router.get("/{group_id}", response_model=List[Card])
|
||||
def get_cards(*, db: Session = Depends(get_session), group_id: int, admin: bool = Depends(auth_is_admin)):
|
||||
|
||||
20
app/controllers/doorManager.py
Normal file
20
app/controllers/doorManager.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.services.database import get_session
|
||||
from app.services.auth import auth_is_admin
|
||||
import app.services.door as doorService
|
||||
|
||||
door_router = APIRouter(prefix="/door",tags=["Door"])
|
||||
|
||||
@door_router.put("/open")
|
||||
def open_door(db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||
doorService.opendoor()
|
||||
|
||||
@door_router.put("/close")
|
||||
def open_door(db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||
doorService.closedoor()
|
||||
|
||||
@door_router.post("/test")
|
||||
def test_access(input: str, db: Session = Depends(get_session)):
|
||||
return doorService.checkAccess(input, db=db)
|
||||
12
app/main.py
12
app/main.py
@@ -1,18 +1,25 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from contextlib import asynccontextmanager
|
||||
from .controllers import userManager, cardManager, groupManager, aaManager
|
||||
from .services.database import create_db_and_tables
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from .controllers import userManager, cardManager, groupManager, aaManager, doorManager
|
||||
from .services.database import create_db_and_tables, get_session
|
||||
from .services.auth import token_router, create_first_user
|
||||
from app.services.scanner import BackgroundScanner
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
scanner = BackgroundScanner(db=get_session())
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
load_dotenv()
|
||||
create_db_and_tables()
|
||||
create_first_user()
|
||||
print("Database created and tables initialized.")
|
||||
scanner.start()
|
||||
yield
|
||||
#scanner.stop()
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
@@ -23,3 +30,4 @@ app.include_router(userManager.user_router)
|
||||
app.include_router(groupManager.group_router)
|
||||
app.include_router(cardManager.card_router)
|
||||
app.include_router(aaManager.aa_router)
|
||||
app.include_router(doorManager.door_router)
|
||||
@@ -1,5 +1,6 @@
|
||||
from sqlmodel import Field, Relationship, Session, SQLModel
|
||||
from typing import List
|
||||
from datetime import time
|
||||
|
||||
class Base(SQLModel):
|
||||
pass
|
||||
@@ -88,8 +89,8 @@ class Card(Base, table=True):
|
||||
group: GroupDB | None = Relationship(back_populates="cards")
|
||||
|
||||
class TimetableBase(Base):
|
||||
weekday: int = Field(le=7, ge=1)
|
||||
starttime: str
|
||||
weekday: int = Field(le=6, ge=0)
|
||||
starttime: time
|
||||
duration: int = Field(gt=0, lt=1440)
|
||||
|
||||
class Timetable(TimetableBase, table=True):
|
||||
|
||||
54
app/services/door.py
Normal file
54
app/services/door.py
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
import paho.mqtt.client as mqttClient
|
||||
import paho.mqtt.publish as publish
|
||||
|
||||
from sqlmodel import select
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from sqlalchemy.orm import selectinload
|
||||
import sqlalchemy.exc as exc
|
||||
import datetime
|
||||
|
||||
from app.services.database import Session, get_session
|
||||
from app.model.models import *
|
||||
|
||||
doorIsOpen = True
|
||||
client = mqttClient.Client(client_id="", userdata=None, protocol=mqttClient.MQTTv5)
|
||||
client.tls_set(tls_version=mqttClient.ssl.PROTOCOL_TLS)
|
||||
client.username_pw_set("username", "passwort")
|
||||
#client.connect("host", port=8883)
|
||||
# I think this could also be gpio controlled
|
||||
#See: https://github.com/technyon/nuki_hub#gpio-lock-control-optional
|
||||
|
||||
def openDoor():
|
||||
doorIsOpen = True
|
||||
publish.single(topic="/lock/action", payload="unlock")
|
||||
pass
|
||||
|
||||
def closeDoor():
|
||||
doorIsOpen = False
|
||||
publish.single(topic="/lock/action", payload="lock")
|
||||
pass
|
||||
|
||||
def isDoorOpen():
|
||||
return doorIsOpen
|
||||
|
||||
def checkAccess(uuid: str, db: Session = Depends(get_session)):
|
||||
try:
|
||||
current_weekday = datetime.datetime.weekday(datetime.date.today())
|
||||
current_time = datetime.datetime.now()
|
||||
card = db.exec(select(Card).where(Card.uuid == uuid)).one()
|
||||
for auth in card.group.accessauths:
|
||||
print(f"checking auth: {auth.name}")
|
||||
for timetable in auth.timetables:
|
||||
print(f" checking timetable {timetable.id}")
|
||||
print(f" comparing weekday: CUR:{current_weekday} TT:{timetable.weekday}")
|
||||
if current_weekday == timetable.weekday:
|
||||
starttime = datetime.datetime.combine(datetime.date.today(), timetable.starttime)
|
||||
endtime = starttime + datetime.timedelta(minutes=timetable.duration)
|
||||
print(f" comparing time: Start:{starttime} Current:{current_time} End:{endtime}")
|
||||
if starttime < current_time < endtime:
|
||||
return True
|
||||
return False
|
||||
except exc.NoResultFound:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
303
app/services/scanner.py
Normal file
303
app/services/scanner.py
Normal file
@@ -0,0 +1,303 @@
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
import secrets
|
||||
|
||||
from typing import Optional
|
||||
from sqlmodel import Session
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from smartcard.CardRequest import CardRequest
|
||||
from smartcard.CardType import AnyCardType
|
||||
from smartcard.Exceptions import CardRequestTimeoutException
|
||||
|
||||
from desfire import DESFire, DESFireKey, PCSCDevice, diversify_key, get_list, to_hex_string
|
||||
from desfire.enums import DESFireCommunicationMode, DESFireFileType, DESFireKeySettings, DESFireKeyType
|
||||
from desfire.schemas import FilePermissions, FileSettings, KeySettings
|
||||
import desfire.exceptions as desExceptions
|
||||
|
||||
|
||||
#ENV vars
|
||||
load_dotenv()
|
||||
MIFARE_APP_MASTER_KEY = os.getenv('MIFARE_APP_MASTER_KEY')
|
||||
MIFARE_ACL_READ_BASE_KEY = os.getenv('MIFARE_ACL_READ_BASE_KEY')
|
||||
MIFARE_ACL_WRITE_BASE_KEY = os.getenv('MIFARE_ACL_WRITE_BASE_KEY')
|
||||
|
||||
# Constants
|
||||
MIFARE_APP_ID = "DEAFFE" # 7 bytes
|
||||
MIFARE_ACL_READ_BASE_KEY_ID = 0x1
|
||||
MIFARE_ACL_WRITE_BASE_KEY_ID = 0x2
|
||||
MIFARE_SYS_ID = "FF0000" # 3 bytes, can essentially be anything
|
||||
MIFARE_ENCRYPTED_FILE_ID = 0x1
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def checkForKey():
|
||||
if MIFARE_APP_MASTER_KEY == None:
|
||||
logger.critical("NO MASTER KEY LOADED")
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="No key loaded! Check application.")
|
||||
|
||||
def getCardService(timeout: int = 10):
|
||||
cardtype = AnyCardType()
|
||||
cardrequest = CardRequest(timeout=timeout, cardType=cardtype)
|
||||
print("Please present DESfire tag...")
|
||||
try:
|
||||
cardservice = cardrequest.waitforcard()
|
||||
except CardRequestTimeoutException:
|
||||
logger.error("No tag detected within the timeout.")
|
||||
raise Exception
|
||||
return cardservice
|
||||
|
||||
def DeleteCard():
|
||||
try:
|
||||
checkForKey()
|
||||
from app.main import scanner as scannerThread
|
||||
scannerThread.stop()
|
||||
cardservice = getCardService(15)
|
||||
cardservice.connection.connect()
|
||||
|
||||
# Create Desfire object
|
||||
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
||||
|
||||
# Create Key objects
|
||||
AES_NULL_KEY_DATA = "00" * 8
|
||||
aes_keysettings = KeySettings(
|
||||
key_type=DESFireKeyType.DF_KEY_AES,
|
||||
)
|
||||
key_settings = desfire.get_key_setting()
|
||||
aes_null_key = DESFireKey(key_settings, AES_NULL_KEY_DATA)
|
||||
aes_master_key = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
||||
desfire.authenticate(0x0, aes_null_key)
|
||||
applications = desfire.get_application_ids()
|
||||
logger.debug(f"Applications: {applications}")
|
||||
if len(applications) == 0:
|
||||
raise HTTPException(status_code=status.HTTP_410_GONE, detail="No applications on card")
|
||||
|
||||
desfire.select_application(MIFARE_APP_ID)
|
||||
desfire.authenticate(0x0, aes_master_key)
|
||||
try:
|
||||
desfire.delete_application(MIFARE_APP_ID)
|
||||
except Exception:
|
||||
pass
|
||||
scannerThread.start()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in deletion function: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error: {e}")
|
||||
|
||||
def WriteNewCard():
|
||||
try:
|
||||
checkForKey()
|
||||
from app.main import scanner as scannerThread
|
||||
scannerThread.stop()
|
||||
|
||||
cardtype = AnyCardType()
|
||||
cardrequest = CardRequest(timeout=10, cardType=cardtype)
|
||||
print("Please present DESfire tag...")
|
||||
try:
|
||||
cardservice = cardrequest.waitforcard()
|
||||
except CardRequestTimeoutException:
|
||||
logger.error("No tag detected within the timeout.")
|
||||
return None
|
||||
|
||||
cardservice.connection.connect()
|
||||
|
||||
# Create Desfire object
|
||||
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
||||
|
||||
# Create Key objects
|
||||
AES_NULL_KEY_DATA = "00" * 16
|
||||
aes_keysettings = KeySettings(
|
||||
key_type=DESFireKeyType.DF_KEY_AES,
|
||||
)
|
||||
aes_null_key = DESFireKey(aes_keysettings, AES_NULL_KEY_DATA)
|
||||
aes_master_key = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
||||
keysetting = desfire.get_key_setting()
|
||||
desKey = DESFireKey(keysetting, "00" * 8)
|
||||
|
||||
# Authenticate with default DES key
|
||||
print("Authenticating with default DES key...")
|
||||
desfire.authenticate(0x0, desKey)
|
||||
|
||||
#get uid
|
||||
uid = desfire.get_real_uid()
|
||||
|
||||
# Set default key
|
||||
print("Setting default key...")
|
||||
desfire.change_default_key(aes_null_key, 0x0)
|
||||
|
||||
# Create application
|
||||
print("Creating application...")
|
||||
app_settings = KeySettings(
|
||||
settings=[
|
||||
DESFireKeySettings.KS_ALLOW_CHANGE_MK,
|
||||
DESFireKeySettings.KS_LISTING_WITHOUT_MK,
|
||||
DESFireKeySettings.KS_CREATE_DELETE_WITHOUT_MK,
|
||||
DESFireKeySettings.KS_CONFIGURATION_CHANGEABLE,
|
||||
],
|
||||
key_type=DESFireKeyType.DF_KEY_AES,
|
||||
)
|
||||
desfire.create_application(MIFARE_APP_ID, app_settings, 4)
|
||||
|
||||
# Verify application creation
|
||||
applications = desfire.get_application_ids()
|
||||
assert len(applications) == 1
|
||||
assert applications[0] == get_list(MIFARE_APP_ID)
|
||||
print(" - Application created successfully.")
|
||||
|
||||
# Select application
|
||||
desfire.select_application(MIFARE_APP_ID)
|
||||
#Auth again as 0key
|
||||
aes_null_auth_key = DESFireKey(aes_keysettings, AES_NULL_KEY_DATA)
|
||||
desfire.authenticate(0x0, aes_null_auth_key)
|
||||
# Authenticate with AES key, as this has been set as the default key
|
||||
aes_app_mk = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
||||
desfire.change_key(0x0, aes_null_key, aes_app_mk, 0x1)
|
||||
|
||||
print("new key auth")
|
||||
desfire.authenticate(0x0, aes_app_mk)
|
||||
|
||||
#generate div data
|
||||
diversification_data = [0x01] + uid + get_list(MIFARE_APP_ID) + get_list(MIFARE_SYS_ID)
|
||||
read_div_key_bytes = diversify_key(get_list(MIFARE_ACL_READ_BASE_KEY), diversification_data, pad_to_32=False)
|
||||
write_div_key_bytes = diversify_key(get_list(MIFARE_ACL_WRITE_BASE_KEY), diversification_data, pad_to_32=False)
|
||||
|
||||
print("Changing file read key...")
|
||||
aes_file_read_key = DESFireKey(aes_keysettings, read_div_key_bytes)
|
||||
desfire.change_key(MIFARE_ACL_READ_BASE_KEY_ID, aes_null_key, aes_file_read_key, 0x1)
|
||||
|
||||
print("Changing file write key...")
|
||||
aes_file_write_key = DESFireKey(aes_keysettings, write_div_key_bytes)
|
||||
desfire.change_key(MIFARE_ACL_WRITE_BASE_KEY_ID, aes_null_key, aes_file_write_key, 0x1)
|
||||
|
||||
print("Create encrypted file containing UUID...")
|
||||
file_settings = FileSettings(
|
||||
file_size=16,
|
||||
encryption=DESFireCommunicationMode.ENCRYPTED,
|
||||
permissions=FilePermissions(
|
||||
read_key=MIFARE_ACL_READ_BASE_KEY_ID,
|
||||
write_key=MIFARE_ACL_WRITE_BASE_KEY_ID,
|
||||
),
|
||||
file_type=DESFireFileType.MDFT_STANDARD_DATA_FILE,
|
||||
)
|
||||
desfire.create_standard_file(MIFARE_ENCRYPTED_FILE_ID, file_settings)
|
||||
|
||||
print("Read and verify file settings again...")
|
||||
file_data = desfire.get_file_settings(MIFARE_ENCRYPTED_FILE_ID)
|
||||
assert file_data.file_size == 16
|
||||
assert file_data.encryption == DESFireCommunicationMode.ENCRYPTED
|
||||
assert file_data.permissions is not None
|
||||
assert file_data.permissions.read_access == MIFARE_ACL_READ_BASE_KEY_ID
|
||||
assert file_data.permissions.write_access == MIFARE_ACL_WRITE_BASE_KEY_ID
|
||||
assert file_data.file_type == DESFireFileType.MDFT_STANDARD_DATA_FILE
|
||||
print(" - File created successfully.")
|
||||
|
||||
print("Writing UID to encrypted file...")
|
||||
key = secrets.token_hex(16)
|
||||
desfire.write_file_data(MIFARE_ENCRYPTED_FILE_ID, 0x0, file_data.encryption, get_list(key))
|
||||
|
||||
print("Reading from encrypted file...")
|
||||
rdata = desfire.read_file_data(MIFARE_ENCRYPTED_FILE_ID, file_data)
|
||||
assert rdata == get_list(key)
|
||||
print(" - Data written successfully.")
|
||||
scannerThread.start()
|
||||
return key
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in write function: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error: {e}")
|
||||
|
||||
|
||||
class BackgroundScanner:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.is_running = False
|
||||
self.thread: Optional[threading.Thread] = None
|
||||
|
||||
def start(self):
|
||||
if self.is_running:
|
||||
logger.info("Scanner already running")
|
||||
return
|
||||
self.is_running = True
|
||||
self.thread = threading.Thread(target=self._scan_loop, daemon=True)
|
||||
self.thread.start()
|
||||
logger.info("Scanner started")
|
||||
|
||||
def stop(self):
|
||||
self.is_running = False
|
||||
if self.thread:
|
||||
self.thread.join()
|
||||
logger.info("Scanner stopped")
|
||||
|
||||
def _scan_loop(self):
|
||||
while self.is_running:
|
||||
try:
|
||||
card_content = self._read_card()
|
||||
if card_content:
|
||||
logger.info(to_hex_string(card_content))
|
||||
time.sleep(5)
|
||||
logger.debug("READY after success")
|
||||
#self._check_db(card_content)
|
||||
else:
|
||||
time.sleep(0.5)
|
||||
logger.debug("READY after timout")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in scan function: {e}", exc_info=True)
|
||||
time.sleep(6)
|
||||
|
||||
def _read_card(self):
|
||||
cardtype = AnyCardType()
|
||||
cardrequest = CardRequest(timeout=5, cardType=cardtype)
|
||||
try:
|
||||
cardservice = cardrequest.waitforcard()
|
||||
except CardRequestTimeoutException:
|
||||
logger.debug("No tag detected within the timeout.")
|
||||
return
|
||||
|
||||
try:
|
||||
cardservice.connection.connect()
|
||||
|
||||
# Create Desfire object
|
||||
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
||||
aes_keysettings = KeySettings(
|
||||
key_type=DESFireKeyType.DF_KEY_AES,
|
||||
)
|
||||
|
||||
# Get real UID
|
||||
mk = DESFireKey(desfire.get_key_setting(), "00" * 8)
|
||||
desfire.authenticate(0x0, mk)
|
||||
#To get the uid you have to auth with an empty (default) key
|
||||
uid = desfire.get_real_uid()
|
||||
applications = desfire.get_application_ids()
|
||||
try:
|
||||
assert len(applications) == 1
|
||||
assert applications[0] == get_list(MIFARE_APP_ID)
|
||||
except AssertionError:
|
||||
logger.error("No application found!")
|
||||
time.sleep(4)
|
||||
return None
|
||||
#Then use the key derivation with that uid, the appid, the sysid
|
||||
diversification_data = [0x01] + uid + get_list(MIFARE_APP_ID) + get_list(MIFARE_SYS_ID)
|
||||
read_div_key_bytes = diversify_key(get_list(MIFARE_ACL_READ_BASE_KEY), diversification_data, pad_to_32=False)
|
||||
|
||||
#Log in with derived read key
|
||||
logger.info("Start auth")
|
||||
aes_app_read_key = DESFireKey(aes_keysettings, read_div_key_bytes)
|
||||
desfire.select_application(MIFARE_APP_ID)
|
||||
|
||||
desfire.authenticate(MIFARE_ACL_READ_BASE_KEY_ID, aes_app_read_key)
|
||||
|
||||
logger.info("Read data")
|
||||
file_data = desfire.get_file_settings(MIFARE_ENCRYPTED_FILE_ID)
|
||||
logger.info(f"File settings: {file_data}")
|
||||
rdata = desfire.read_file_data(MIFARE_ENCRYPTED_FILE_ID, file_data)
|
||||
return rdata
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"something went wrong: {e}")
|
||||
time.sleep(5)
|
||||
24
flake.lock
generated
24
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1775710090,
|
||||
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
|
||||
"lastModified": 1778869304,
|
||||
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
|
||||
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -29,11 +29,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773870109,
|
||||
"narHash": "sha256-ZoTdqZP03DcdoyxvpFHCAek4bkPUTUPUF3oCCgc3dP4=",
|
||||
"lastModified": 1776659114,
|
||||
"narHash": "sha256-qapCOQmR++yZSY43dzrp3wCrkOTLpod+ONtJWBk6iKU=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "build-system-pkgs",
|
||||
"rev": "b6e74f433b02fa4b8a7965ee24680f4867e2926f",
|
||||
"rev": "ffaa2161dd5d63e0e94591f86b54fc239660fb2e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -49,11 +49,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1776120154,
|
||||
"narHash": "sha256-mtIBTmVKzyoFYoAGdd8Cd7iswFna9YQVyjObZLXPO64=",
|
||||
"lastModified": 1778901413,
|
||||
"narHash": "sha256-GSKXTAnFqRAMlZkJrIPcQMYf+lpMr66K3i60mB9STvc=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "pyproject.nix",
|
||||
"rev": "29dc4e9960d2b7f122b52b155e0e8f87cd5c5c08",
|
||||
"rev": "a228447c3e179d477c1b6246ef3efa8cfe3c469a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -80,11 +80,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1776114780,
|
||||
"narHash": "sha256-aYUgp40qkY7oNSm+G9A/woNYP+eDeFM0bYckfmxEUiY=",
|
||||
"lastModified": 1779269674,
|
||||
"narHash": "sha256-P1LHCRdYpdtHAEzuEsNHrI6d9mVPl5a2fyFDZGHNVbI=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "uv2nix",
|
||||
"rev": "73ff87a3e489b07b9cf842f917963a9e40d49225",
|
||||
"rev": "69aec536f6d1acc415ed2e20299312802aba98c6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
12
flake.nix
12
flake.nix
@@ -58,6 +58,13 @@
|
||||
lib.composeManyExtensions [
|
||||
pyproject-build-systems.overlays.wheel
|
||||
overlay
|
||||
(final: prev: {
|
||||
pyscard = prev.pyscard.overrideAttrs (old: {
|
||||
nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ pkgs.swig pkgs.pkg-config];
|
||||
buildInputs = (old.buildInputs or []) ++ [ pkgs.pcsclite.dev ];
|
||||
NIX_CFLAGS_COMPILE = "-I${pkgs.pcsclite.dev}/include/PCSC";
|
||||
});
|
||||
})
|
||||
]
|
||||
)
|
||||
);
|
||||
@@ -76,11 +83,16 @@
|
||||
packages = [
|
||||
virtualenv
|
||||
pkgs.uv
|
||||
pkgs.pcsclite
|
||||
pkgs.pcsclite.dev
|
||||
pkgs.swig
|
||||
pkgs.pkg-config
|
||||
];
|
||||
env = {
|
||||
UV_NO_SYNC = "1";
|
||||
UV_PYTHON = pythonSet.python.interpreter;
|
||||
UV_PYTHON_DOWNLOADS = "never";
|
||||
LD_LIBRARY_PATH = "${lib.getLib pkgs.pcsclite}/lib";
|
||||
};
|
||||
shellHook = ''
|
||||
unset PYTHONPATH
|
||||
|
||||
@@ -15,6 +15,8 @@ dependencies = [
|
||||
"pytest>=9.0.3",
|
||||
"requests>=2.33.1",
|
||||
"pytest-cov>=7.1.0",
|
||||
"setuptools>=82.0.1",
|
||||
"pyscard>=2.3.1",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
@@ -22,3 +24,4 @@ python-desfire = { git = "https://github.com/waza-ari/python-desfire" }
|
||||
|
||||
[tool.uv.extra-build-dependencies]
|
||||
python-desfire = ["poetry"]
|
||||
"pyscard" = ["setuptools"]
|
||||
|
||||
164
test.py
Normal file
164
test.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
This is a more involved example that performs initial configuration (often called personalization) of a DESFire card.
|
||||
|
||||
It performs the following steps:
|
||||
1. Authenticate with the default DES key
|
||||
3. Change the default key
|
||||
2. Create an application
|
||||
4. Change the application master key
|
||||
6. Create a read and write key (diversified)
|
||||
7. Create an encrypted file
|
||||
8. Write the UID to the encrypted file
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from smartcard.CardRequest import CardRequest
|
||||
from smartcard.CardType import AnyCardType
|
||||
from smartcard.Exceptions import CardRequestTimeoutException
|
||||
|
||||
from desfire import DESFire, DESFireKey, PCSCDevice, diversify_key, get_list, to_hex_string
|
||||
from desfire.enums import DESFireCommunicationMode, DESFireFileType, DESFireKeySettings, DESFireKeyType
|
||||
from desfire.schemas import FilePermissions, FileSettings, KeySettings
|
||||
|
||||
from dotenv import load_dotenv
|
||||
# Please make sure to yet your own keys here before running this script
|
||||
load_dotenv()
|
||||
|
||||
MIFARE_APP_MASTER_KEY = os.getenv('MIFARE_APP_MASTER_KEY')
|
||||
MIFARE_ACL_READ_BASE_KEY = os.getenv('MIFARE_ACL_READ_BASE_KEY')
|
||||
MIFARE_ACL_WRITE_BASE_KEY = os.getenv('MIFARE_ACL_WRITE_BASE_KEY')
|
||||
|
||||
# Constants
|
||||
MIFARE_APP_ID = "DEAFFE" # 7 bytes
|
||||
MIFARE_ACL_READ_BASE_KEY_ID = 0x1
|
||||
MIFARE_ACL_WRITE_BASE_KEY_ID = 0x2
|
||||
MIFARE_SYS_ID = "FF0000" # 3 bytes, can essentially be anything
|
||||
MIFARE_ENCRYPTED_FILE_ID = 0x1
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
cardtype = AnyCardType()
|
||||
cardrequest = CardRequest(timeout=30, cardType=cardtype)
|
||||
print("Please present DESfire tag...")
|
||||
|
||||
try:
|
||||
cardservice = cardrequest.waitforcard()
|
||||
except CardRequestTimeoutException:
|
||||
print("No tag detected within the timeout.")
|
||||
raise
|
||||
|
||||
cardservice.connection.connect()
|
||||
|
||||
# Create Desfire object
|
||||
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
||||
|
||||
# Create Key objects
|
||||
AES_NULL_KEY_DATA = "00" * 16
|
||||
aes_keysettings = KeySettings(
|
||||
key_type=DESFireKeyType.DF_KEY_AES,
|
||||
)
|
||||
aes_null_key = DESFireKey(aes_keysettings, AES_NULL_KEY_DATA)
|
||||
|
||||
# Authenticate with default DES key
|
||||
print("Authenticating with default DES key...")
|
||||
key_settings = desfire.get_key_setting()
|
||||
mk = DESFireKey(key_settings, "00" * 8)
|
||||
desfire.authenticate(0x0, mk)
|
||||
|
||||
# Get real UID
|
||||
print("Getting real UID...")
|
||||
uid = desfire.get_real_uid()
|
||||
print(" - UID: ", to_hex_string(uid))
|
||||
|
||||
# Set default key
|
||||
print("Setting default key...")
|
||||
desfire.change_default_key(aes_null_key, 0x0)
|
||||
|
||||
# Create application
|
||||
print("Creating application...")
|
||||
app_settings = KeySettings(
|
||||
settings=[
|
||||
DESFireKeySettings.KS_ALLOW_CHANGE_MK,
|
||||
DESFireKeySettings.KS_LISTING_WITHOUT_MK,
|
||||
DESFireKeySettings.KS_CREATE_DELETE_WITHOUT_MK,
|
||||
DESFireKeySettings.KS_CONFIGURATION_CHANGEABLE,
|
||||
],
|
||||
key_type=DESFireKeyType.DF_KEY_AES,
|
||||
)
|
||||
desfire.create_application(MIFARE_APP_ID, app_settings, 4)
|
||||
|
||||
# Verify application creation
|
||||
applications = desfire.get_application_ids()
|
||||
assert len(applications) == 1
|
||||
assert applications[0] == get_list(MIFARE_APP_ID)
|
||||
print(" - Application created successfully.")
|
||||
|
||||
# Select application
|
||||
print("Selecting application...")
|
||||
desfire.select_application(MIFARE_APP_ID)
|
||||
|
||||
# Authenticate with AES key, as this has been set as the default key
|
||||
print("Authenticating with AES key...")
|
||||
# Create a new one as key data would be overriden by session data
|
||||
aes_null_auth_key = DESFireKey(aes_keysettings, AES_NULL_KEY_DATA)
|
||||
desfire.authenticate(0x0, aes_null_auth_key)
|
||||
|
||||
# Change Application master key
|
||||
print("Changing application master key (AMK)...")
|
||||
aes_app_mk = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
||||
desfire.change_key(0x0, aes_null_key, aes_app_mk, 0x1)
|
||||
|
||||
# Re-Authenticate with new AES key
|
||||
print("Re-authenticating with new AES key...")
|
||||
desfire.authenticate(0x0, aes_app_mk)
|
||||
|
||||
# Change file read and write keys (diversified)
|
||||
diversification_data = [0x01] + uid + get_list(MIFARE_APP_ID) + get_list(MIFARE_SYS_ID)
|
||||
read_div_key_bytes = diversify_key(get_list(MIFARE_ACL_READ_BASE_KEY), diversification_data, pad_to_32=False)
|
||||
write_div_key_bytes = diversify_key(get_list(MIFARE_ACL_WRITE_BASE_KEY), diversification_data, pad_to_32=False)
|
||||
|
||||
print("Changing file read key...")
|
||||
aes_file_read_key = DESFireKey(aes_keysettings, read_div_key_bytes)
|
||||
desfire.change_key(MIFARE_ACL_READ_BASE_KEY_ID, aes_null_key, aes_file_read_key, 0x1)
|
||||
|
||||
print("Changing file write key...")
|
||||
aes_file_write_key = DESFireKey(aes_keysettings, write_div_key_bytes)
|
||||
desfire.change_key(MIFARE_ACL_WRITE_BASE_KEY_ID, aes_null_key, aes_file_write_key, 0x1)
|
||||
|
||||
print("Create encrypted file containing UID...")
|
||||
file_settings = FileSettings(
|
||||
file_size=8,
|
||||
encryption=DESFireCommunicationMode.ENCRYPTED,
|
||||
permissions=FilePermissions(
|
||||
read_key=MIFARE_ACL_READ_BASE_KEY_ID,
|
||||
write_key=MIFARE_ACL_WRITE_BASE_KEY_ID,
|
||||
),
|
||||
file_type=DESFireFileType.MDFT_STANDARD_DATA_FILE,
|
||||
)
|
||||
desfire.create_standard_file(MIFARE_ENCRYPTED_FILE_ID, file_settings)
|
||||
|
||||
print("Read and verify file settings again...")
|
||||
file_data = desfire.get_file_settings(MIFARE_ENCRYPTED_FILE_ID)
|
||||
assert file_data.file_size == 8
|
||||
assert file_data.encryption == DESFireCommunicationMode.ENCRYPTED
|
||||
assert file_data.permissions is not None
|
||||
assert file_data.permissions.read_access == MIFARE_ACL_READ_BASE_KEY_ID
|
||||
assert file_data.permissions.write_access == MIFARE_ACL_WRITE_BASE_KEY_ID
|
||||
assert file_data.file_type == DESFireFileType.MDFT_STANDARD_DATA_FILE
|
||||
print(" - File created successfully.")
|
||||
|
||||
print("Writing UID to encrypted file...")
|
||||
data = [0x0] + uid
|
||||
assert len(data) == 8
|
||||
desfire.write_file_data(MIFARE_ENCRYPTED_FILE_ID, 0x0, file_data.encryption, get_list(data))
|
||||
|
||||
print("Reading from encrypted file...")
|
||||
rdata = desfire.read_file_data(MIFARE_ENCRYPTED_FILE_ID, file_data)
|
||||
assert rdata == data
|
||||
print(" - Data written successfully.")
|
||||
|
||||
print("Personalization finished.")
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
import datetime
|
||||
from app.model.models import (
|
||||
UserBase, UserResponse, UserCreate, UserDB, UserUpdate,
|
||||
GroupBase, GroupCreate, GroupDB, GroupResponse,
|
||||
@@ -68,13 +69,13 @@ def test_timetable_models():
|
||||
# Test TimetableBase with valid values
|
||||
timetable = TimetableCreate(weekday=1, starttime="09:00", duration=120)
|
||||
assert timetable.weekday == 1
|
||||
assert timetable.starttime == "09:00"
|
||||
assert timetable.starttime == datetime.time(9, 0)
|
||||
assert timetable.duration == 120
|
||||
|
||||
# Test boundary values
|
||||
max_duration = TimetableCreate(weekday=7, starttime="23:59", duration=1439)
|
||||
max_duration = TimetableCreate(weekday=6, starttime="23:59", duration=1439)
|
||||
assert max_duration.duration == 1439
|
||||
assert max_duration.weekday == 7
|
||||
assert max_duration.weekday == 6
|
||||
|
||||
|
||||
def test_token_models():
|
||||
|
||||
26
uv.lock
generated
26
uv.lock
generated
@@ -613,10 +613,12 @@ dependencies = [
|
||||
{ name = "poetry" },
|
||||
{ name = "pwdlib", extra = ["argon2"] },
|
||||
{ name = "pyjwt", extra = ["crypto"] },
|
||||
{ name = "pyscard" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "python-desfire" },
|
||||
{ name = "requests" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "sqlmodel" },
|
||||
]
|
||||
|
||||
@@ -627,10 +629,12 @@ requires-dist = [
|
||||
{ name = "poetry", specifier = ">=2.3.4" },
|
||||
{ name = "pwdlib", extras = ["argon2"], specifier = ">=0.3.0" },
|
||||
{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.12.1" },
|
||||
{ name = "pyscard", specifier = ">=2.3.1" },
|
||||
{ name = "pytest", specifier = ">=9.0.3" },
|
||||
{ name = "pytest-cov", specifier = ">=7.1.0" },
|
||||
{ name = "python-desfire", git = "https://github.com/waza-ari/python-desfire" },
|
||||
{ name = "requests", specifier = ">=2.33.1" },
|
||||
{ name = "setuptools", specifier = ">=82.0.1" },
|
||||
{ name = "sqlmodel", specifier = ">=0.0.38" },
|
||||
]
|
||||
|
||||
@@ -1230,6 +1234,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyscard"
|
||||
version = "2.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/c9/65c68738a94b44b67b3c5e68a815890bbd225f2ae11ef1ace9b61fa9d5f3/pyscard-2.3.1.tar.gz", hash = "sha256:a24356f57a0a950740b6e54f51f819edd5296ee8892a6625b0da04724e9e6c13", size = 160650, upload-time = "2025-10-29T15:49:08.353Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/e2/d8f967fe6fee82c02ee35d736e8707d65ed1e37fa7635f3bdca96bdcd2ff/pyscard-2.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c11a596407e18cdcf16a4ccd8cdeaa55846d4f7ec2eefc529483e201f6906658", size = 170956, upload-time = "2025-10-29T15:53:32.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/a1/508fc5733e2ce01a71c6abc94dae543311cf9d0962e309b0a78b4cb4031a/pyscard-2.3.1-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:72b1ab922fad5e050144ec72762e36741271b09d2389cda9b976b61ee0564e71", size = 138809, upload-time = "2025-10-29T15:49:06.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/a4/5481cd54f882ea61bfc334667f1e5126ac51124a464297dbe24592288b14/pyscard-2.3.1-cp313-cp313-win32.whl", hash = "sha256:a0b59d1961ff9fb15d980ad64edae13e4512b7e641ea8959e86133f34091aa5c", size = 139294, upload-time = "2025-10-29T15:53:35.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/2b/c98e7bcf45958905d1bfdab59fe5acd67fe9ff0ad78145c4783886c38c84/pyscard-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:df2b256bc719b701807114bdd179f7b303f309a954d5689188b088adfc33ead2", size = 145501, upload-time = "2025-10-29T15:53:33.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/a6/74cca0ec3c08f35c1a7468a8508d0e3989e63298d3a4b1f928542719aebc/pyscard-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:f58b46cd78455a29a0abceabff21b37da81a385a737b29e6dd5e25acb7e3f3da", size = 140758, upload-time = "2025-10-29T15:53:34.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.3"
|
||||
@@ -1529,6 +1546,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "82.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
|
||||
Reference in New Issue
Block a user