Push Tracker
ria-ran--sim-drop/server/profile_generator.py

149 lines
4.7 KiB
Python
Raw Normal View History

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 swap_nibbles(s: str) -> str:
"""Swap the nibbles of every byte in a hex string.
``"1234"`` -> ``"2143"``. Used for the BCD-with-swapped-nibbles layout
that 3GPP uses for ICCID and IMSI elementary files.
"""
if len(s) % 2:
raise ValueError(f"hex string must have even length: {s!r}")
return "".join(s[i + 1] + s[i] for i in range(0, len(s), 2))
def enc_iccid(iccid: str) -> bytes:
"""Encode an ICCID as EF_ICCID bytes (3GPP TS 31.102 §4.2.1).
Digits are BCD packed with swapped nibbles and right-padded to 10 bytes
(20 nibbles) with 0xF.
"""
if not iccid.isdigit():
raise ValueError(f"ICCID must be digits only: {iccid!r}")
digits = iccid.ljust(20, "f")
return bytes.fromhex(swap_nibbles(digits))
def enc_imsi(imsi: str) -> bytes:
"""Encode an IMSI as EF_IMSI bytes (3GPP TS 31.102 §4.2.2).
Layout: length octet, then a parity nibble ((odd<<3)|1) prepended to the
IMSI digits, the whole run nibble-swapped.
"""
if not imsi.isdigit():
raise ValueError(f"IMSI must be digits only: {imsi!r}")
odd = len(imsi) & 1
value = swap_nibbles(f"{(odd << 3) | 1:01x}{imsi}")
length = len(value) // 2
return bytes([length]) + bytes.fromhex(value)
def build_lpa_activation_code(iccid: str, smdp_address: str) -> str:
"""Build an SGP.22 LPA activation code string.
Format: ``LPA:1$<smdp-address>$<matching-id>``. The ICCID is used as the
matching id so the profile is self-referencing for this ad-hoc deployment.
"""
return f"LPA:1${smdp_address}${iccid}"
def create_esim_profile(
imsi: str,
ki: bytes,
opc: bytes,
iccid: str,
profile_name: str,
carrier_name: str,
smdp_address: str = "esim.local",
apn: str = "internet",
) -> bytes:
"""Create an eSIM profile package (``profile-package-v2``).
Emits a structured package embedding the raw credentials, the real 3GPP
EF_ICCID / EF_IMSI byte encodings, and an SGP.22 LPA activation code.
Full SGP.22 SIMalliance Interoperable Profile (ASN.1 UPP) generation via
osmocom pySim remains the follow-up step; see CHANGELOG.
"""
ki_hex = ki.hex() if isinstance(ki, bytes) else ki
opc_hex = opc.hex() if isinstance(opc, bytes) else opc
profile = {
"version": "2.0",
"type": "profile-package-v2",
"iccid": iccid,
"imsi": imsi,
"ki": ki_hex,
"opc": opc_hex,
"profile_name": profile_name,
"carrier_name": carrier_name,
"apn": apn,
"activation_code": build_lpa_activation_code(iccid, smdp_address),
"ef": {
"iccid": enc_iccid(iccid).hex(),
"imsi": enc_imsi(imsi).hex(),
},
}
logger.debug(f"Created profile-package-v2 for IMSI {imsi}")
return json.dumps(profile, indent=2).encode("utf-8")