diff options
author | Harald Welte <laforge@osmocom.org> | 2024-02-18 10:15:33 +0100 |
---|---|---|
committer | Harald Welte <laforge@osmocom.org> | 2024-02-21 09:22:40 +0100 |
commit | 318faef583c8afa9e7e1eb2d3b71337fa69f52b2 (patch) | |
tree | 9a1e63c6b967c78af6bb0cc7984c7ceb965e228b | |
parent | aa76546d165b54bd1113d6e38e722543be1069b4 (diff) |
saip.personalization: include encode/decode of value; add validation method
Change-Id: Ia9fa39c25817448afb191061acd4be894300eeef
-rw-r--r-- | pySim/esim/saip/personalization.py | 88 | ||||
-rwxr-xr-x | tests/test_esim_saip.py | 3 |
2 files changed, 77 insertions, 14 deletions
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py index 5adaf2e..ac9089c 100644 --- a/pySim/esim/saip/personalization.py +++ b/pySim/esim/saip/personalization.py @@ -16,8 +16,10 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import abc +import io from typing import List, Tuple +from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid from pySim.esim.saip import ProfileElement, ProfileElementSequence def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_keys: List[str]) -> List[Tuple]: @@ -47,26 +49,56 @@ class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta): def __init__(self, value): self.value = value + def validate(self): + """Optional validation method. Can be used by derived classes to perform validation + of the input value (self.value). Will raise an exception if validation fails.""" + pass + @abc.abstractmethod def apply(self, pes: ProfileElementSequence): pass class Iccid(ConfigurableParameter): - """Configurable ICCID. Expects the value to be in EF.ICCID format.""" + """Configurable ICCID. Expects the value to be a string of decimal digits. + If the string of digits is only 18 digits long, a Luhn check digit will be added.""" name = 'iccid' + + def validate(self): + # convert to string as it migt be an integer + iccid_str = str(self.value) + if len(iccid_str) < 18 or len(iccid_str) > 20: + raise ValueError('ICCID must be 18, 19 or 20 digits long') + if not iccid_str.isdecimal(): + raise ValueError('ICCID must only contain decimal digits') + def apply(self, pes: ProfileElementSequence): - # patch the header; FIXME: swap nibbles! - pes.get_pe_for_type('header').decoded['iccid'] = self.value + iccid_str = sanitize_iccid(self.value) + # patch the header + pes.get_pe_for_type('header').decoded['iccid'] = iccid_str # patch MF/EF.ICCID - file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], bytes(self.value)) + file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], h2b(enc_iccid(iccid_str))) class Imsi(ConfigurableParameter): - """Configurable IMSI. Expects value to be n EF.IMSI format.""" + """Configurable IMSI. Expects value to be a string of digits. Automatically sets the ACC to + the last digit of the IMSI.""" name = 'imsi' + + def validate(self): + # convert to string as it migt be an integer + imsi_str = str(self.value) + if len(imsi_str) < 6 or len(imsi_str) > 15: + raise ValueError('IMSI must be 6..15 digits long') + if not imsi_str.isdecimal(): + raise ValueError('IMSI must only contain decimal digits') + def apply(self, pes: ProfileElementSequence): + imsi_str = str(self.value) + # we always use the least significant byte of the IMSI as ACC + acc = (1 << int(imsi_str[-1])) # patch ADF.USIM/EF.IMSI - for pe in pes.get_pes_by_type('usim'): - file_replace_content(pe.decoded['ef-imsi'], self.value) + for pe in pes.get_pes_for_type('usim'): + file_replace_content(pe.decoded['ef-imsi'], h2b(enc_imsi(imsi_str))) + file_replace_content(pe.decoded['ef-acc'], acc.to_bytes(2, 'big')) # TODO: DF.GSM_ACCESS if not linked? def obtain_singleton_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> ProfileElement: @@ -81,12 +113,21 @@ def obtain_first_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> Pr class Puk(ConfigurableParameter, metaclass=ClassVarMeta): """Configurable PUK (Pin Unblock Code). String ASCII-encoded digits.""" keyReference = None + def validate(self): + if isinstance(self.value, int): + self.value = '%08d' % self.value + # FIXME: valid length? + if not self.value.isdecimal(): + raise ValueError('PUK must only contain decimal digits') + def apply(self, pes: ProfileElementSequence): + puk = ''.join(['%02x' % (ord(x)) for x in self.value]) + padded_puk = rpad(puk, 16) mf_pes = pes.pes_by_naa['mf'][0] pukCodes = obtain_singleton_pe_from_pelist(mf_pes, 'pukCodes') for pukCode in pukCodes.decoded['pukCodes']: if pukCode['keyReference'] == self.keyReference: - pukCode['pukValue'] = self.value + pukCode['pukValue'] = h2b(padded_puk) return raise ValueError('cannot find pukCode') class Puk1(Puk, keyReference=0x01): @@ -97,29 +138,46 @@ class Puk2(Puk, keyReference=0x81): class Pin(ConfigurableParameter, metaclass=ClassVarMeta): """Configurable PIN (Personal Identification Number). String of digits.""" keyReference = None + def validate(self): + if isinstance(self.value, int): + self.value = '%04d' % self.value + if len(self.value) < 4 or len(self.value) > 8: + raise ValueError('PIN mus be 4..8 digits long') + if not self.value.isdecimal(): + raise ValueError('PIN must only contain decimal digits') def apply(self, pes: ProfileElementSequence): + pin = ''.join(['%02x' % (ord(x)) for x in self.value]) + padded_pin = rpad(pin, 16) mf_pes = pes.pes_by_naa['mf'][0] pinCodes = obtain_first_pe_from_pelist(mf_pes, 'pinCodes') if pinCodes.decoded['pinCodes'][0] != 'pinconfig': return for pinCode in pinCodes.decoded['pinCodes'][1]: if pinCode['keyReference'] == self.keyReference: - pinCode['pinValue'] = self.value + pinCode['pinValue'] = h2b(padded_pin) return raise ValueError('cannot find pinCode') class AppPin(ConfigurableParameter, metaclass=ClassVarMeta): """Configurable PIN (Personal Identification Number). String of digits.""" keyReference = None + def validate(self): + if isinstance(self.value, int): + self.value = '%04d' % self.value + if len(self.value) < 4 or len(self.value) > 8: + raise ValueError('PIN mus be 4..8 digits long') + if not self.value.isdecimal(): + raise ValueError('PIN must only contain decimal digits') def _apply_one(self, pe: ProfileElement): + pin = ''.join(['%02x' % (ord(x)) for x in self.value]) + padded_pin = rpad(pin, 16) pinCodes = obtain_first_pe_from_pelist(pe, 'pinCodes') if pinCodes.decoded['pinCodes'][0] != 'pinconfig': return for pinCode in pinCodes.decoded['pinCodes'][1]: if pinCode['keyReference'] == self.keyReference: - pinCode['pinValue'] = self.value + pinCode['pinValue'] = h2b(padded_pin) return raise ValueError('cannot find pinCode') - def apply(self, pes: ProfileElementSequence): for naa in pes.pes_by_naa: if naa not in ['usim','isim','csim','telecom']: @@ -140,6 +198,9 @@ class Adm2(Pin, keyReference=0x0B): class AlgoConfig(ConfigurableParameter, metaclass=ClassVarMeta): """Configurable Algorithm parameter. bytes.""" key = None + def validate(self): + if not isinstance(self.value, (io.BytesIO, bytes, bytearray)): + raise ValueError('Value must be of bytes-like type') def apply(self, pes: ProfileElementSequence): for pe in pes.get_pes_for_type('akaParameter'): algoConfiguration = pe.decoded['algoConfiguration'] @@ -152,5 +213,6 @@ class K(AlgoConfig, key='key'): class Opc(AlgoConfig, key='opc'): pass class AlgorithmID(AlgoConfig, key='algorithmID'): - pass - + def validate(self): + if self.value not in [1, 2, 3]: + raise ValueError('Invalid algorithmID %s' % (self.value)) diff --git a/tests/test_esim_saip.py b/tests/test_esim_saip.py index 9e7afb2..a70c149 100755 --- a/tests/test_esim_saip.py +++ b/tests/test_esim_saip.py @@ -55,9 +55,10 @@ class SaipTest(unittest.TestCase): def test_personalization(self): """Test some of the personalization operations.""" pes = copy.deepcopy(self.pes) - params = [Puk1(value=b'01234567'), Puk2(value=b'98765432'), Pin1(b'1111'), Pin2(b'2222'), Adm1(b'11111111'), + params = [Puk1('01234567'), Puk2(98765432), Pin1('1111'), Pin2(2222), Adm1('11111111'), K(h2b('000102030405060708090a0b0c0d0e0f')), Opc(h2b('101112131415161718191a1b1c1d1e1f'))] for p in params: + p.validate() p.apply(pes) # TODO: we don't actually test the results here, but we just verify there is no exception pes.to_der() |