import json import secrets import logging from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend logger = logging.getLogger(__name__) def generate_imsi(mcc: str, mnc: str) -> str: """Generate a random 15-digit IMSI: MCC + MNC + MSIN. IMSI is always 15 digits. MCC is 3 digits; MNC is 2 or 3 digits, so the MSIN fills the remaining 9 or 10 digits. """ msin_len = 15 - len(mcc) - len(mnc) if msin_len <= 0: raise ValueError(f"MCC/MNC too long for a 15-digit IMSI: {mcc!r}/{mnc!r}") msin = str(secrets.randbelow(10**msin_len)).zfill(msin_len) imsi = mcc + mnc + msin assert len(imsi) == 15, f"IMSI length error: {imsi!r}" return imsi def generate_ki() -> bytes: """Generate a 16-byte cryptographically random authentication key.""" return secrets.token_bytes(16) def luhn_checksum(number: str) -> int: """Compute the Luhn check digit for a string of digits.""" digits = [int(d) for d in number] odd_digits = digits[-1::-2] even_digits = digits[-2::-2] total = sum(odd_digits) for d in even_digits: total += sum(divmod(d * 2, 10)) return (10 - (total % 10)) % 10 def generate_iccid() -> str: """Generate a 19-digit ICCID with a valid Luhn check digit. Format: 8910 (telecom prefix) + 10 random digits + 5 more + Luhn digit = 19 total. """ prefix = "8910" body = prefix + str(secrets.randbelow(10**14)).zfill(14) # 18 digits check = luhn_checksum(body) return body + str(check) def derive_opc(ki: bytes, op: bytes) -> bytes: """Derive OPc from Ki and OP using the Milenage algorithm. OPc = AES_Ki(OP) XOR OP """ cipher = Cipher(algorithms.AES(ki), modes.ECB(), backend=default_backend()) enc = cipher.encryptor() encrypted_op = enc.update(op) + enc.finalize() return bytes(a ^ b for a, b in zip(encrypted_op, op)) def create_esim_profile( imsi: str, ki: bytes, opc: bytes, iccid: str, profile_name: str, carrier_name: str, ) -> bytes: """Create an eSIM profile. Phase 1: returns a JSON-encoded mock profile suitable for end-to-end testing. Phase 2 (Unit 06 upgrade): replace with real pySim SGP.22 profile generation. """ ki_hex = ki.hex() if isinstance(ki, bytes) else ki opc_hex = opc.hex() if isinstance(opc, bytes) else opc profile = { "version": "1.0", "type": "mock", "iccid": iccid, "imsi": imsi, "ki": ki_hex, "opc": opc_hex, "profile_name": profile_name, "carrier_name": carrier_name, } logger.debug(f"Created mock eSIM profile for IMSI {imsi}") return json.dumps(profile, indent=2).encode("utf-8")