2024-08-26 23:23:53 -04:00
|
|
|
import dataclasses
|
|
|
|
from typing import Literal
|
|
|
|
|
2023-11-08 12:34:11 -05:00
|
|
|
import bitstring
|
|
|
|
|
|
|
|
# Reference for H10301 card format:
|
|
|
|
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf
|
|
|
|
|
|
|
|
|
2024-08-26 23:23:53 -04:00
|
|
|
class InvalidHexCode(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class Not26Bit(InvalidHexCode):
|
2024-09-01 09:47:50 -04:00
|
|
|
def __init__(self, bits: bitstring.Bits) -> None:
|
|
|
|
super().__init__(f"Card number > 26 bits [{bits.hex}]")
|
2024-08-26 23:23:53 -04:00
|
|
|
|
|
|
|
|
|
|
|
class InvalidParity(InvalidHexCode):
|
2024-09-01 09:47:50 -04:00
|
|
|
def __init__(self, bits: bitstring.Bits, even_odd: Literal["even", "odd"]) -> None:
|
|
|
|
super().__init__(f"Bad {even_odd} parity [{bits.hex}]")
|
2024-08-26 23:23:53 -04:00
|
|
|
|
|
|
|
|
2024-09-01 02:09:09 -04:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
2023-11-08 12:34:11 -05:00
|
|
|
class Credential:
|
2024-08-26 23:23:53 -04:00
|
|
|
bits: bitstring.Bits
|
|
|
|
|
2024-09-01 21:35:58 -04:00
|
|
|
@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
|
|
|
|
|
2024-08-26 23:23:53 -04:00
|
|
|
@classmethod
|
2024-09-01 21:35:58 -04:00
|
|
|
def from_code(cls, facility: int, card_number: int) -> "Credential":
|
2024-08-26 23:23:53 -04:00
|
|
|
bits = bitstring.pack(
|
|
|
|
"0b000000, 0b0, uint:8=facility, uint:16=card_number, 0b0",
|
|
|
|
facility=facility,
|
|
|
|
card_number=card_number,
|
|
|
|
)
|
2024-09-01 21:35:58 -04:00
|
|
|
bits[6] = cls.even_parity(bits)
|
|
|
|
bits[31] = cls.odd_parity(bits)
|
2024-09-01 02:21:34 -04:00
|
|
|
return cls(bitstring.Bits(bits))
|
2024-08-26 23:23:53 -04:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_hex(cls, hex_code: str) -> "Credential":
|
|
|
|
bits = bitstring.Bits(hex=hex_code)
|
|
|
|
|
|
|
|
if bits[:6].any(1):
|
2024-09-01 09:47:50 -04:00
|
|
|
raise Not26Bit(bits)
|
2024-09-01 21:35:58 -04:00
|
|
|
if bits[6] != cls.even_parity(bits):
|
2024-09-01 09:47:50 -04:00
|
|
|
raise InvalidParity(bits, "even")
|
2024-09-01 21:35:58 -04:00
|
|
|
if bits[31] != (cls.odd_parity(bits)):
|
2024-09-01 09:47:50 -04:00
|
|
|
raise InvalidParity(bits, "odd")
|
2024-08-26 23:23:53 -04:00
|
|
|
|
|
|
|
return cls(bits)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def facility_code(self) -> int:
|
|
|
|
return self.bits[7:15].uint
|
2023-11-08 12:34:11 -05:00
|
|
|
|
|
|
|
@property
|
2024-08-26 23:23:53 -04:00
|
|
|
def card_number(self) -> int:
|
|
|
|
return self.bits[15:31].uint
|
2023-11-08 12:34:11 -05:00
|
|
|
|
|
|
|
@property
|
2024-08-26 23:23:53 -04:00
|
|
|
def hex(self) -> str:
|
2023-11-08 12:34:11 -05:00
|
|
|
return self.bits.hex.upper()
|