mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-24 14:28:32 +03:00
saip.personalization: include encode/decode of value; add validation method
Change-Id: Ia9fa39c25817448afb191061acd4be894300eeef
This commit is contained in:
@@ -16,8 +16,10 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
import io
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid
|
||||||
from pySim.esim.saip import ProfileElement, ProfileElementSequence
|
from pySim.esim.saip import ProfileElement, ProfileElementSequence
|
||||||
|
|
||||||
def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_keys: List[str]) -> List[Tuple]:
|
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):
|
def __init__(self, value):
|
||||||
self.value = 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
|
@abc.abstractmethod
|
||||||
def apply(self, pes: ProfileElementSequence):
|
def apply(self, pes: ProfileElementSequence):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class Iccid(ConfigurableParameter):
|
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'
|
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):
|
def apply(self, pes: ProfileElementSequence):
|
||||||
# patch the header; FIXME: swap nibbles!
|
iccid_str = sanitize_iccid(self.value)
|
||||||
pes.get_pe_for_type('header').decoded['iccid'] = self.value
|
# patch the header
|
||||||
|
pes.get_pe_for_type('header').decoded['iccid'] = iccid_str
|
||||||
# patch MF/EF.ICCID
|
# 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):
|
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'
|
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):
|
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
|
# patch ADF.USIM/EF.IMSI
|
||||||
for pe in pes.get_pes_by_type('usim'):
|
for pe in pes.get_pes_for_type('usim'):
|
||||||
file_replace_content(pe.decoded['ef-imsi'], self.value)
|
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?
|
# TODO: DF.GSM_ACCESS if not linked?
|
||||||
|
|
||||||
def obtain_singleton_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> ProfileElement:
|
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):
|
class Puk(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||||
"""Configurable PUK (Pin Unblock Code). String ASCII-encoded digits."""
|
"""Configurable PUK (Pin Unblock Code). String ASCII-encoded digits."""
|
||||||
keyReference = None
|
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):
|
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]
|
mf_pes = pes.pes_by_naa['mf'][0]
|
||||||
pukCodes = obtain_singleton_pe_from_pelist(mf_pes, 'pukCodes')
|
pukCodes = obtain_singleton_pe_from_pelist(mf_pes, 'pukCodes')
|
||||||
for pukCode in pukCodes.decoded['pukCodes']:
|
for pukCode in pukCodes.decoded['pukCodes']:
|
||||||
if pukCode['keyReference'] == self.keyReference:
|
if pukCode['keyReference'] == self.keyReference:
|
||||||
pukCode['pukValue'] = self.value
|
pukCode['pukValue'] = h2b(padded_puk)
|
||||||
return
|
return
|
||||||
raise ValueError('cannot find pukCode')
|
raise ValueError('cannot find pukCode')
|
||||||
class Puk1(Puk, keyReference=0x01):
|
class Puk1(Puk, keyReference=0x01):
|
||||||
@@ -97,29 +138,46 @@ class Puk2(Puk, keyReference=0x81):
|
|||||||
class Pin(ConfigurableParameter, metaclass=ClassVarMeta):
|
class Pin(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||||
"""Configurable PIN (Personal Identification Number). String of digits."""
|
"""Configurable PIN (Personal Identification Number). String of digits."""
|
||||||
keyReference = None
|
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):
|
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]
|
mf_pes = pes.pes_by_naa['mf'][0]
|
||||||
pinCodes = obtain_first_pe_from_pelist(mf_pes, 'pinCodes')
|
pinCodes = obtain_first_pe_from_pelist(mf_pes, 'pinCodes')
|
||||||
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
||||||
return
|
return
|
||||||
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
||||||
if pinCode['keyReference'] == self.keyReference:
|
if pinCode['keyReference'] == self.keyReference:
|
||||||
pinCode['pinValue'] = self.value
|
pinCode['pinValue'] = h2b(padded_pin)
|
||||||
return
|
return
|
||||||
raise ValueError('cannot find pinCode')
|
raise ValueError('cannot find pinCode')
|
||||||
class AppPin(ConfigurableParameter, metaclass=ClassVarMeta):
|
class AppPin(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||||
"""Configurable PIN (Personal Identification Number). String of digits."""
|
"""Configurable PIN (Personal Identification Number). String of digits."""
|
||||||
keyReference = None
|
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):
|
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')
|
pinCodes = obtain_first_pe_from_pelist(pe, 'pinCodes')
|
||||||
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
||||||
return
|
return
|
||||||
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
||||||
if pinCode['keyReference'] == self.keyReference:
|
if pinCode['keyReference'] == self.keyReference:
|
||||||
pinCode['pinValue'] = self.value
|
pinCode['pinValue'] = h2b(padded_pin)
|
||||||
return
|
return
|
||||||
raise ValueError('cannot find pinCode')
|
raise ValueError('cannot find pinCode')
|
||||||
|
|
||||||
def apply(self, pes: ProfileElementSequence):
|
def apply(self, pes: ProfileElementSequence):
|
||||||
for naa in pes.pes_by_naa:
|
for naa in pes.pes_by_naa:
|
||||||
if naa not in ['usim','isim','csim','telecom']:
|
if naa not in ['usim','isim','csim','telecom']:
|
||||||
@@ -140,6 +198,9 @@ class Adm2(Pin, keyReference=0x0B):
|
|||||||
class AlgoConfig(ConfigurableParameter, metaclass=ClassVarMeta):
|
class AlgoConfig(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||||
"""Configurable Algorithm parameter. bytes."""
|
"""Configurable Algorithm parameter. bytes."""
|
||||||
key = None
|
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):
|
def apply(self, pes: ProfileElementSequence):
|
||||||
for pe in pes.get_pes_for_type('akaParameter'):
|
for pe in pes.get_pes_for_type('akaParameter'):
|
||||||
algoConfiguration = pe.decoded['algoConfiguration']
|
algoConfiguration = pe.decoded['algoConfiguration']
|
||||||
@@ -152,5 +213,6 @@ class K(AlgoConfig, key='key'):
|
|||||||
class Opc(AlgoConfig, key='opc'):
|
class Opc(AlgoConfig, key='opc'):
|
||||||
pass
|
pass
|
||||||
class AlgorithmID(AlgoConfig, key='algorithmID'):
|
class AlgorithmID(AlgoConfig, key='algorithmID'):
|
||||||
pass
|
def validate(self):
|
||||||
|
if self.value not in [1, 2, 3]:
|
||||||
|
raise ValueError('Invalid algorithmID %s' % (self.value))
|
||||||
|
|||||||
@@ -55,9 +55,10 @@ class SaipTest(unittest.TestCase):
|
|||||||
def test_personalization(self):
|
def test_personalization(self):
|
||||||
"""Test some of the personalization operations."""
|
"""Test some of the personalization operations."""
|
||||||
pes = copy.deepcopy(self.pes)
|
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'))]
|
K(h2b('000102030405060708090a0b0c0d0e0f')), Opc(h2b('101112131415161718191a1b1c1d1e1f'))]
|
||||||
for p in params:
|
for p in params:
|
||||||
|
p.validate()
|
||||||
p.apply(pes)
|
p.apply(pes)
|
||||||
# TODO: we don't actually test the results here, but we just verify there is no exception
|
# TODO: we don't actually test the results here, but we just verify there is no exception
|
||||||
pes.to_der()
|
pes.to_der()
|
||||||
|
|||||||
Reference in New Issue
Block a user