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