cmsmanage/doorcontrol/hid/Credential.py

80 lines
2.2 KiB
Python

import dataclasses
from typing import Literal
import bitstring
# Reference for H10301 card format:
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf
class InvalidHexCode(Exception):
pass
class Not26Bit(InvalidHexCode):
def __init__(self, bits: bitstring.Bits) -> None:
super().__init__(f"Card number > 26 bits [{bits.hex}]")
class InvalidParity(InvalidHexCode):
def __init__(self, bits: bitstring.Bits, even_odd: Literal["even", "odd"]) -> None:
super().__init__(f"Bad {even_odd} parity [{bits.hex}]")
@dataclasses.dataclass(frozen=True)
class Credential:
bits: bitstring.Bits
is_26bit: bool = dataclasses.field(default=False, compare=False)
@staticmethod
def even_parity(bits: bitstring.Bits) -> bool:
return bool(bits[7:19].count(1) % 2)
@staticmethod
def odd_parity(bits: bitstring.Bits) -> bool:
return (bits[19:31].count(1) % 2) == 0
@classmethod
def from_code(cls, facility: int, card_number: int) -> "Credential":
bits = bitstring.pack(
"0b000000, 0b0, uint:8=facility, uint:16=card_number, 0b0",
facility=facility,
card_number=card_number,
)
bits[6] = cls.even_parity(bits)
bits[31] = cls.odd_parity(bits)
return cls(bitstring.Bits(bits), is_26bit=True)
@classmethod
def from_26bit_hex(cls, hex_code: str) -> "Credential":
bits = bitstring.Bits(hex=hex_code)
if bits[:6].any(1):
raise Not26Bit(bits)
if bits[6] != cls.even_parity(bits):
raise InvalidParity(bits, "even")
if bits[31] != (cls.odd_parity(bits)):
raise InvalidParity(bits, "odd")
return cls(bits, is_26bit=True)
@classmethod
def from_raw_hex(cls, hex_code: str) -> "Credential":
return cls(bitstring.Bits(hex=hex_code))
@property
def facility_code(self) -> int:
if not self.is_26bit:
raise Not26Bit(self.bits)
return self.bits[7:15].uint
@property
def card_number(self) -> int:
if not self.is_26bit:
raise Not26Bit(self.bits)
return self.bits[15:31].uint
@property
def hex(self) -> str:
return self.bits.hex.upper()