mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-05-11 01:30:18 +03:00
Compare commits
2 Commits
master
...
pmaier/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c74d7fbd0d | ||
|
|
d7f4d20471 |
@@ -97,7 +97,7 @@ Please install the following dependencies:
|
|||||||
- pyscard
|
- pyscard
|
||||||
- pyserial
|
- pyserial
|
||||||
- pytlv
|
- pytlv
|
||||||
- pyyaml >= 5.4
|
- pyyaml >= 5.1
|
||||||
- smpp.pdu (from `github.com/hologram-io/smpp.pdu`)
|
- smpp.pdu (from `github.com/hologram-io/smpp.pdu`)
|
||||||
- termcolor
|
- termcolor
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,6 @@
|
|||||||
|
|
||||||
export PYTHONUNBUFFERED=1
|
export PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
setup_venv() {
|
|
||||||
virtualenv -p python3 venv --system-site-packages
|
|
||||||
. venv/bin/activate
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ ! -d "./tests/" ] ; then
|
if [ ! -d "./tests/" ] ; then
|
||||||
echo "###############################################"
|
echo "###############################################"
|
||||||
echo "Please call from pySim-prog top directory"
|
echo "Please call from pySim-prog top directory"
|
||||||
@@ -28,7 +23,8 @@ fi
|
|||||||
|
|
||||||
case "$JOB_TYPE" in
|
case "$JOB_TYPE" in
|
||||||
"test")
|
"test")
|
||||||
setup_venv
|
virtualenv -p python3 venv --system-site-packages
|
||||||
|
. venv/bin/activate
|
||||||
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install pyshark
|
pip install pyshark
|
||||||
@@ -36,27 +32,23 @@ case "$JOB_TYPE" in
|
|||||||
# Execute automatically discovered unit tests first
|
# Execute automatically discovered unit tests first
|
||||||
python -m unittest discover -v -s tests/unittests
|
python -m unittest discover -v -s tests/unittests
|
||||||
|
|
||||||
# Run pySim-trace test
|
# Run pySim-prog integration tests (requires physical cards)
|
||||||
tests/pySim-trace_test/pySim-trace_test.sh
|
|
||||||
;;
|
|
||||||
"card-test") # tests requiring physical cards
|
|
||||||
setup_venv
|
|
||||||
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Run pySim-prog integration tests
|
|
||||||
cd tests/pySim-prog_test/
|
cd tests/pySim-prog_test/
|
||||||
./pySim-prog_test.sh
|
./pySim-prog_test.sh
|
||||||
cd ../../
|
cd ../../
|
||||||
|
|
||||||
# Run pySim-shell integration tests
|
# Run pySim-trace test
|
||||||
|
tests/pySim-trace_test/pySim-trace_test.sh
|
||||||
|
|
||||||
|
# Run pySim-shell integration tests (requires physical cards)
|
||||||
python3 -m unittest discover -v -s ./tests/pySim-shell_test/
|
python3 -m unittest discover -v -s ./tests/pySim-shell_test/
|
||||||
|
|
||||||
# Run pySim-smpp2sim test
|
# Run pySim-smpp2sim test
|
||||||
tests/pySim-smpp2sim_test/pySim-smpp2sim_test.sh
|
tests/pySim-smpp2sim_test/pySim-smpp2sim_test.sh
|
||||||
;;
|
;;
|
||||||
"distcheck")
|
"distcheck")
|
||||||
setup_venv
|
virtualenv -p python3 venv --system-site-packages
|
||||||
|
. venv/bin/activate
|
||||||
|
|
||||||
pip install .
|
pip install .
|
||||||
pip install pyshark
|
pip install pyshark
|
||||||
@@ -69,7 +61,8 @@ case "$JOB_TYPE" in
|
|||||||
# Print pylint version
|
# Print pylint version
|
||||||
pip3 freeze | grep pylint
|
pip3 freeze | grep pylint
|
||||||
|
|
||||||
setup_venv
|
virtualenv -p python3 venv --system-site-packages
|
||||||
|
. venv/bin/activate
|
||||||
|
|
||||||
pip install .
|
pip install .
|
||||||
|
|
||||||
@@ -87,7 +80,8 @@ case "$JOB_TYPE" in
|
|||||||
contrib/*.py
|
contrib/*.py
|
||||||
;;
|
;;
|
||||||
"docs")
|
"docs")
|
||||||
setup_venv
|
virtualenv -p python3 venv --system-site-packages
|
||||||
|
. venv/bin/activate
|
||||||
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
@@ -435,7 +436,7 @@ def gen_parameters(opts):
|
|||||||
if not re.match('^[0-9a-fA-F]{32}$', ki):
|
if not re.match('^[0-9a-fA-F]{32}$', ki):
|
||||||
raise ValueError('Ki needs to be 128 bits, in hex format')
|
raise ValueError('Ki needs to be 128 bits, in hex format')
|
||||||
else:
|
else:
|
||||||
ki = os.urandom(16).hex()
|
ki = ''.join(['%02x' % random.randrange(0, 256) for i in range(16)])
|
||||||
|
|
||||||
# OPC (random)
|
# OPC (random)
|
||||||
if opts.opc is not None:
|
if opts.opc is not None:
|
||||||
@@ -446,7 +447,7 @@ def gen_parameters(opts):
|
|||||||
elif opts.op is not None:
|
elif opts.op is not None:
|
||||||
opc = derive_milenage_opc(ki, opts.op)
|
opc = derive_milenage_opc(ki, opts.op)
|
||||||
else:
|
else:
|
||||||
opc = os.urandom(16).hex()
|
opc = ''.join(['%02x' % random.randrange(0, 256) for i in range(16)])
|
||||||
|
|
||||||
pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
|
pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
|
||||||
|
|
||||||
|
|||||||
@@ -1079,13 +1079,6 @@ class SecurityDomainKey:
|
|||||||
'keyVersionNumber': bytes([self.key_version_number]),
|
'keyVersionNumber': bytes([self.key_version_number]),
|
||||||
'keyComponents': [k.to_saip_dict() for k in self.key_components]}
|
'keyComponents': [k.to_saip_dict() for k in self.key_components]}
|
||||||
|
|
||||||
def get_key_component(self, key_type):
|
|
||||||
for kc in self.key_components:
|
|
||||||
if kc.key_type == key_type:
|
|
||||||
return kc.key_data
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileElementSD(ProfileElement):
|
class ProfileElementSD(ProfileElement):
|
||||||
"""Class representing a securityDomain ProfileElement."""
|
"""Class representing a securityDomain ProfileElement."""
|
||||||
type = 'securityDomain'
|
type = 'securityDomain'
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
"""Implementation of Personalization of eSIM profiles in SimAlliance/TCA Interoperable Profile:
|
|
||||||
Run a batch of N personalizations"""
|
|
||||||
|
|
||||||
# (C) 2025-2026 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
|
|
||||||
#
|
|
||||||
# Author: nhofmeyr@sysmocom.de
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import copy
|
|
||||||
from typing import Generator
|
|
||||||
from pySim.esim.saip.personalization import ConfigurableParameter
|
|
||||||
from pySim.esim.saip import param_source
|
|
||||||
from pySim.esim.saip import ProfileElementSequence
|
|
||||||
|
|
||||||
class BatchPersonalization:
|
|
||||||
"""Produce a series of eSIM profiles from predefined parameters.
|
|
||||||
Personalization parameters are derived from pysim.esim.saip.param_source.ParamSource.
|
|
||||||
|
|
||||||
Usage example:
|
|
||||||
|
|
||||||
der_input = open('some_file', 'rb').read()
|
|
||||||
pes = ProfileElementSequence.from_der(der_input)
|
|
||||||
p = BatchPersonalization(
|
|
||||||
n=10,
|
|
||||||
src_pes=pes,
|
|
||||||
csv_rows=get_csv_reader())
|
|
||||||
|
|
||||||
p.add_param_and_src(
|
|
||||||
personalization.Iccid(),
|
|
||||||
param_source.IncDigitSource(
|
|
||||||
num_digits=18,
|
|
||||||
first_value=123456789012340001,
|
|
||||||
last_value=123456789012340010))
|
|
||||||
|
|
||||||
# add more parameters here, using ConfigurableParameter and ParamSource subclass instances to define the profile
|
|
||||||
# ...
|
|
||||||
|
|
||||||
# generate all 10 profiles (from n=10 above)
|
|
||||||
for result_pes in p.generate_profiles():
|
|
||||||
upp = result_pes.to_der()
|
|
||||||
store_upp(upp)
|
|
||||||
"""
|
|
||||||
|
|
||||||
class ParamAndSrc:
|
|
||||||
"""tie a ConfigurableParameter to a source of actual values"""
|
|
||||||
def __init__(self, param: ConfigurableParameter, src: param_source.ParamSource):
|
|
||||||
if isinstance(param, type):
|
|
||||||
self.param_cls = param
|
|
||||||
else:
|
|
||||||
self.param_cls = param.__class__
|
|
||||||
self.src = src
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
n: int,
|
|
||||||
src_pes: ProfileElementSequence,
|
|
||||||
params: list[ParamAndSrc]=None,
|
|
||||||
csv_rows: Generator=None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
n: number of eSIM profiles to generate.
|
|
||||||
src_pes: a decoded eSIM profile as ProfileElementSequence, to serve as template. This is not modified, only
|
|
||||||
copied.
|
|
||||||
params: list of ParamAndSrc instances, defining a ConfigurableParameter and corresponding ParamSource to fill in
|
|
||||||
profile values.
|
|
||||||
csv_rows: A generator (e.g. iter(list_of_rows)) producing all CSV rows one at a time, starting with a row
|
|
||||||
containing the column headers. This is compatible with the python csv.reader. Each row gets passed to
|
|
||||||
ParamSource.get_next(), such that ParamSource implementations can access the row items. See
|
|
||||||
param_source.CsvSource.
|
|
||||||
"""
|
|
||||||
self.n = n
|
|
||||||
self.params = params or []
|
|
||||||
self.src_pes = src_pes
|
|
||||||
self.csv_rows = csv_rows
|
|
||||||
|
|
||||||
def add_param_and_src(self, param:ConfigurableParameter, src:param_source.ParamSource):
|
|
||||||
self.params.append(BatchPersonalization.ParamAndSrc(param, src))
|
|
||||||
|
|
||||||
def generate_profiles(self):
|
|
||||||
# get first row of CSV: column names
|
|
||||||
csv_columns = None
|
|
||||||
if self.csv_rows:
|
|
||||||
try:
|
|
||||||
csv_columns = next(self.csv_rows)
|
|
||||||
except StopIteration as e:
|
|
||||||
raise ValueError('the input CSV file appears to be empty') from e
|
|
||||||
|
|
||||||
for i in range(self.n):
|
|
||||||
csv_row = None
|
|
||||||
if self.csv_rows and csv_columns:
|
|
||||||
try:
|
|
||||||
csv_row_list = next(self.csv_rows)
|
|
||||||
except StopIteration as e:
|
|
||||||
raise ValueError(f'not enough rows in the input CSV for eSIM nr {i+1} of {self.n}') from e
|
|
||||||
|
|
||||||
csv_row = dict(zip(csv_columns, csv_row_list))
|
|
||||||
|
|
||||||
pes = copy.deepcopy(self.src_pes)
|
|
||||||
|
|
||||||
for p in self.params:
|
|
||||||
try:
|
|
||||||
input_value = p.src.get_next(csv_row=csv_row)
|
|
||||||
assert input_value is not None
|
|
||||||
value = p.param_cls.validate_val(input_value)
|
|
||||||
p.param_cls.apply_val(pes, value)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(f'{p.param_cls.get_name()} fed by {p.src.name}: {e}') from e
|
|
||||||
|
|
||||||
yield pes
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
# Implementation of SimAlliance/TCA Interoperable Profile handling: parameter sources for batch personalization.
|
|
||||||
#
|
|
||||||
# (C) 2025 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
|
|
||||||
#
|
|
||||||
# Author: nhofmeyr@sysmocom.de
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
from osmocom.utils import b2h
|
|
||||||
|
|
||||||
class ParamSourceExn(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ParamSourceExhaustedExn(ParamSourceExn):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ParamSourceUndefinedExn(ParamSourceExn):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ParamSource:
|
|
||||||
"""abstract parameter source. For usage, see personalization.BatchPersonalization."""
|
|
||||||
|
|
||||||
# This name should be short but descriptive, useful for a user interface, like 'random decimal digits'.
|
|
||||||
name = "none"
|
|
||||||
numeric_base = None # or 10 or 16
|
|
||||||
|
|
||||||
def __init__(self, input_str:str):
|
|
||||||
"""Subclasses should call super().__init__(input_str) before evaluating self.input_str. Each subclass __init__()
|
|
||||||
may in turn manipulate self.input_str to apply expansions or decodings."""
|
|
||||||
self.input_str = input_str
|
|
||||||
|
|
||||||
def get_next(self, csv_row:dict=None):
|
|
||||||
"""Subclasses implement this: return the next value from the parameter source.
|
|
||||||
When there are no more values from the source, raise a ParamSourceExhaustedExn.
|
|
||||||
This default implementation is an empty source."""
|
|
||||||
raise ParamSourceExhaustedExn()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_str(cls, input_str:str):
|
|
||||||
"""compatibility with earlier version of ParamSource. Just use the constructor."""
|
|
||||||
return cls(input_str)
|
|
||||||
|
|
||||||
class ConstantSource(ParamSource):
|
|
||||||
"""one value for all"""
|
|
||||||
name = "constant"
|
|
||||||
|
|
||||||
def get_next(self, csv_row:dict=None):
|
|
||||||
return self.input_str
|
|
||||||
|
|
||||||
class InputExpandingParamSource(ParamSource):
|
|
||||||
|
|
||||||
def __init__(self, input_str:str):
|
|
||||||
super().__init__(input_str)
|
|
||||||
self.input_str = self.expand_input_str(self.input_str)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def expand_input_str(cls, input_str:str):
|
|
||||||
# user convenience syntax '0*32' becomes '00000000000000000000000000000000'
|
|
||||||
if "*" not in input_str:
|
|
||||||
return input_str
|
|
||||||
# re: "XX * 123" with optional spaces
|
|
||||||
tokens = re.split(r"([^ \t]+)[ \t]*\*[ \t]*([0-9]+)", input_str)
|
|
||||||
if len(tokens) < 3:
|
|
||||||
return input_str
|
|
||||||
parts = []
|
|
||||||
for unchanged, snippet, repeat_str in zip(tokens[0::3], tokens[1::3], tokens[2::3]):
|
|
||||||
parts.append(unchanged)
|
|
||||||
repeat = int(repeat_str)
|
|
||||||
parts.append(snippet * repeat)
|
|
||||||
|
|
||||||
return "".join(parts)
|
|
||||||
|
|
||||||
class DecimalRangeSource(InputExpandingParamSource):
|
|
||||||
"""abstract: decimal numbers with a value range"""
|
|
||||||
|
|
||||||
numeric_base = 10
|
|
||||||
|
|
||||||
def __init__(self, input_str:str=None, num_digits:int=None, first_value:int=None, last_value:int=None):
|
|
||||||
"""Constructor to set up values from a (user entered) string: DecimalRangeSource(input_str).
|
|
||||||
Constructor to set up values directly: DecimalRangeSource(num_digits=3, first_value=123, last_value=456)
|
|
||||||
|
|
||||||
num_digits produces leading zeros when first_value..last_value are shorter.
|
|
||||||
"""
|
|
||||||
assert ((input_str is not None and (num_digits, first_value, last_value) == (None, None, None))
|
|
||||||
or (input_str is None and None not in (num_digits, first_value, last_value)))
|
|
||||||
|
|
||||||
if input_str is not None:
|
|
||||||
super().__init__(input_str)
|
|
||||||
|
|
||||||
input_str = self.input_str
|
|
||||||
|
|
||||||
if ".." in input_str:
|
|
||||||
first_str, last_str = input_str.split('..')
|
|
||||||
first_str = first_str.strip()
|
|
||||||
last_str = last_str.strip()
|
|
||||||
else:
|
|
||||||
first_str = input_str.strip()
|
|
||||||
last_str = None
|
|
||||||
|
|
||||||
num_digits = len(first_str)
|
|
||||||
first_value = int(first_str)
|
|
||||||
last_value = int(last_str if last_str is not None else "9" * num_digits)
|
|
||||||
|
|
||||||
assert num_digits > 0
|
|
||||||
assert first_value <= last_value
|
|
||||||
self.num_digits = num_digits
|
|
||||||
self.first_value = first_value
|
|
||||||
self.last_value = last_value
|
|
||||||
|
|
||||||
def val_to_digit(self, val:int):
|
|
||||||
return "%0*d" % (self.num_digits, val) # pylint: disable=consider-using-f-string
|
|
||||||
|
|
||||||
class RandomDigitSource(DecimalRangeSource):
|
|
||||||
"""return a different sequence of random decimal digits each"""
|
|
||||||
name = "random decimal digits"
|
|
||||||
|
|
||||||
def get_next(self, csv_row:dict=None):
|
|
||||||
val = random.randint(self.first_value, self.last_value) # TODO secure random source?
|
|
||||||
return self.val_to_digit(val)
|
|
||||||
|
|
||||||
class RandomHexDigitSource(InputExpandingParamSource):
|
|
||||||
"""return a different sequence of random hexadecimal digits each"""
|
|
||||||
name = "random hexadecimal digits"
|
|
||||||
numeric_base = 16
|
|
||||||
|
|
||||||
def __init__(self, input_str:str):
|
|
||||||
super().__init__(input_str)
|
|
||||||
input_str = self.input_str
|
|
||||||
|
|
||||||
num_digits = len(input_str.strip())
|
|
||||||
if num_digits < 1:
|
|
||||||
raise ValueError("zero number of digits")
|
|
||||||
# hex digits always come in two
|
|
||||||
if (num_digits & 1) != 0:
|
|
||||||
raise ValueError(f"hexadecimal value should have even number of digits, not {num_digits}")
|
|
||||||
self.num_digits = num_digits
|
|
||||||
|
|
||||||
def get_next(self, csv_row:dict=None):
|
|
||||||
val = random.randbytes(self.num_digits // 2) # TODO secure random source?
|
|
||||||
return b2h(val)
|
|
||||||
|
|
||||||
class IncDigitSource(DecimalRangeSource):
|
|
||||||
"""incrementing sequence of digits"""
|
|
||||||
name = "incrementing decimal digits"
|
|
||||||
|
|
||||||
def __init__(self, input_str:str=None, num_digits:int=None, first_value:int=None, last_value:int=None):
|
|
||||||
"""input_str: the range of values to iterate. Format: 'FIRST..LAST' (e.g. '0001..9999') or
|
|
||||||
just 'FIRST' (iterates to the maximum value for the given digit width). Leading zeros in
|
|
||||||
FIRST determine the digit width and are preserved in returned values."""
|
|
||||||
super().__init__(input_str, num_digits, first_value, last_value)
|
|
||||||
self.next_val = None
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""Restart from the first value of the defined range passed to __init__()."""
|
|
||||||
self.next_val = self.first_value
|
|
||||||
|
|
||||||
def get_next(self, csv_row:dict=None):
|
|
||||||
val = self.next_val
|
|
||||||
if val is None:
|
|
||||||
raise ParamSourceExhaustedExn()
|
|
||||||
|
|
||||||
returnval = self.val_to_digit(val)
|
|
||||||
|
|
||||||
val += 1
|
|
||||||
if val > self.last_value:
|
|
||||||
self.next_val = None
|
|
||||||
else:
|
|
||||||
self.next_val = val
|
|
||||||
|
|
||||||
return returnval
|
|
||||||
|
|
||||||
class CsvSource(ParamSource):
|
|
||||||
"""apply a column from a CSV row, as passed in to ParamSource.get_next(csv_row)"""
|
|
||||||
name = "from CSV"
|
|
||||||
|
|
||||||
def __init__(self, input_str:str):
|
|
||||||
"""input_str: the CSV column name to read values from.
|
|
||||||
The caller passes the current CSV row to get_next(), from which CsvSource picks the column matching
|
|
||||||
this name."""
|
|
||||||
super().__init__(input_str)
|
|
||||||
self.csv_column = self.input_str
|
|
||||||
|
|
||||||
def get_next(self, csv_row:dict=None):
|
|
||||||
val = None
|
|
||||||
if csv_row:
|
|
||||||
val = csv_row.get(self.csv_column)
|
|
||||||
if val is None:
|
|
||||||
raise ParamSourceUndefinedExn(f"no value for CSV column {self.csv_column!r}")
|
|
||||||
return val
|
|
||||||
@@ -16,22 +16,13 @@
|
|||||||
# 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 enum
|
|
||||||
import io
|
import io
|
||||||
import re
|
from typing import List, Tuple
|
||||||
from typing import List, Tuple, Generator, Optional
|
|
||||||
|
|
||||||
from osmocom.tlv import camel_to_snake
|
from osmocom.tlv import camel_to_snake
|
||||||
from osmocom.utils import hexstr
|
from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid
|
||||||
from pySim.utils import enc_iccid, dec_iccid, enc_imsi, dec_imsi, h2b, b2h, rpad, sanitize_iccid
|
from pySim.esim.saip import ProfileElement, ProfileElementSequence
|
||||||
from pySim.ts_51_011 import EF_SMSP
|
from pySim.ts_51_011 import EF_SMSP
|
||||||
from pySim.esim.saip import param_source
|
|
||||||
from pySim.esim.saip import ProfileElement, ProfileElementSD, ProfileElementSequence
|
|
||||||
from pySim.esim.saip import SecurityDomainKey, SecurityDomainKeyComponent
|
|
||||||
from pySim.global_platform import KeyUsageQualifier, KeyType
|
|
||||||
|
|
||||||
def unrpad(s: hexstr, c='f') -> hexstr:
|
|
||||||
return hexstr(s.rstrip(c))
|
|
||||||
|
|
||||||
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]:
|
||||||
"""In a list of tuples, remove all tuples whose first part equals 'unwanted_key'."""
|
"""In a list of tuples, remove all tuples whose first part equals 'unwanted_key'."""
|
||||||
@@ -126,7 +117,6 @@ class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
|
|||||||
max_len = None
|
max_len = None
|
||||||
allow_len = None # a list of specific lengths
|
allow_len = None # a list of specific lengths
|
||||||
example_input = None
|
example_input = None
|
||||||
default_source = None # a param_source.ParamSource subclass
|
|
||||||
|
|
||||||
def __init__(self, input_value=None):
|
def __init__(self, input_value=None):
|
||||||
self.input_value = input_value # the raw input value as given by caller
|
self.input_value = input_value # the raw input value as given by caller
|
||||||
@@ -209,29 +199,6 @@ class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
|
|||||||
Write the given val in the right format in all the right places in pes."""
|
Write the given val in the right format in all the right places in pes."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence) -> Generator:
|
|
||||||
"""This is what subclasses implement: yield all values from a decoded profile package.
|
|
||||||
Find all values in the pes, and yield them decoded to a valid cls.input_value format.
|
|
||||||
Should be a generator function, i.e. use 'yield' instead of 'return'.
|
|
||||||
|
|
||||||
Yielded value must be a dict(). Usually, an implementation will return only one key, like
|
|
||||||
|
|
||||||
{ "ICCID": "1234567890123456789" }
|
|
||||||
|
|
||||||
Some implementations have more than one value to return, like
|
|
||||||
|
|
||||||
{ "IMSI": "00101012345678", "IMSI-ACC" : "5" }
|
|
||||||
|
|
||||||
Implementation example:
|
|
||||||
|
|
||||||
for pe in pes:
|
|
||||||
if my_condition(pe):
|
|
||||||
yield { cls.name: b2h(my_bin_value_from(pe)) }
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_len_range(cls):
|
def get_len_range(cls):
|
||||||
"""considering all of min_len, max_len and allow_len, get a tuple of the resulting (min, max) of permitted
|
"""considering all of min_len, max_len and allow_len, get a tuple of the resulting (min, max) of permitted
|
||||||
@@ -252,13 +219,6 @@ class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
|
|||||||
return (None, None)
|
return (None, None)
|
||||||
return (min(vals), max(vals))
|
return (min(vals), max(vals))
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_typical_input_len(cls):
|
|
||||||
'''return a good length to use as the visible width of a user interface input field.
|
|
||||||
May be overridden by subclasses.
|
|
||||||
This default implementation returns the maximum allowed value length -- a good fit for most subclasses.
|
|
||||||
'''
|
|
||||||
return cls.get_len_range()[1] or 16
|
|
||||||
|
|
||||||
class DecimalParam(ConfigurableParameter):
|
class DecimalParam(ConfigurableParameter):
|
||||||
"""Decimal digits. The input value may be a string of decimal digits like '012345', or an int. The output of
|
"""Decimal digits. The input value may be a string of decimal digits like '012345', or an int. The output of
|
||||||
@@ -289,7 +249,6 @@ class DecimalHexParam(DecimalParam):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def validate_val(cls, val):
|
def validate_val(cls, val):
|
||||||
val = super().validate_val(val)
|
val = super().validate_val(val)
|
||||||
assert isinstance(val, str)
|
|
||||||
val = ''.join('%02x' % ord(x) for x in val)
|
val = ''.join('%02x' % ord(x) for x in val)
|
||||||
if cls.rpad is not None:
|
if cls.rpad is not None:
|
||||||
c = cls.rpad_char
|
c = cls.rpad_char
|
||||||
@@ -297,17 +256,6 @@ class DecimalHexParam(DecimalParam):
|
|||||||
# a DecimalHexParam subclass expects the apply_val() input to be a bytes instance ready for the pes
|
# a DecimalHexParam subclass expects the apply_val() input to be a bytes instance ready for the pes
|
||||||
return h2b(val)
|
return h2b(val)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def decimal_hex_to_str(cls, val):
|
|
||||||
"""useful for get_values_from_pes() implementations of subclasses"""
|
|
||||||
if isinstance(val, bytes):
|
|
||||||
val = b2h(val)
|
|
||||||
assert isinstance(val, hexstr)
|
|
||||||
if cls.rpad is not None:
|
|
||||||
c = cls.rpad_char or 'f'
|
|
||||||
val = unrpad(val, c)
|
|
||||||
return val.to_bytes().decode('ascii')
|
|
||||||
|
|
||||||
class IntegerParam(ConfigurableParameter):
|
class IntegerParam(ConfigurableParameter):
|
||||||
allow_types = (str, int)
|
allow_types = (str, int)
|
||||||
allow_chars = '0123456789'
|
allow_chars = '0123456789'
|
||||||
@@ -331,19 +279,10 @@ class IntegerParam(ConfigurableParameter):
|
|||||||
raise ValueError(f'Value {val} is out of range, must be [{cls.min_val}..{cls.max_val}]')
|
raise ValueError(f'Value {val} is out of range, must be [{cls.min_val}..{cls.max_val}]')
|
||||||
return val
|
return val
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
|
||||||
for valdict in super().get_values_from_pes(pes):
|
|
||||||
for key, val in valdict.items():
|
|
||||||
if isinstance(val, int):
|
|
||||||
valdict[key] = str(val)
|
|
||||||
yield valdict
|
|
||||||
|
|
||||||
class BinaryParam(ConfigurableParameter):
|
class BinaryParam(ConfigurableParameter):
|
||||||
allow_types = (str, io.BytesIO, bytes, bytearray)
|
allow_types = (str, io.BytesIO, bytes, bytearray)
|
||||||
allow_chars = '0123456789abcdefABCDEF'
|
allow_chars = '0123456789abcdefABCDEF'
|
||||||
strip_chars = ' \t\r\n'
|
strip_chars = ' \t\r\n'
|
||||||
default_source = param_source.RandomHexDigitSource
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_val(cls, val):
|
def validate_val(cls, val):
|
||||||
@@ -362,82 +301,6 @@ class BinaryParam(ConfigurableParameter):
|
|||||||
val = super().validate_val(val)
|
val = super().validate_val(val)
|
||||||
return bytes(val)
|
return bytes(val)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_typical_input_len(cls):
|
|
||||||
# override to return twice the length, because of hex digits.
|
|
||||||
min_len, max_len = cls.get_len_range()
|
|
||||||
if max_len is None:
|
|
||||||
return None
|
|
||||||
# two hex characters per value octet.
|
|
||||||
# (maybe *3 to also allow for spaces?)
|
|
||||||
return max_len * 2
|
|
||||||
|
|
||||||
|
|
||||||
class EnumParam(ConfigurableParameter):
|
|
||||||
"""ConfigurableParameter for named integer enumeration values.
|
|
||||||
|
|
||||||
Subclasses must define a nested enum.IntEnum named 'Values' listing all valid names and their
|
|
||||||
integer codes. apply_val() and get_values_from_pes() are not implemented here and this must
|
|
||||||
be inherited from another mixin."""
|
|
||||||
|
|
||||||
class Values(enum.IntEnum):
|
|
||||||
pass # subclasses override this
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate_val(cls, val) -> int:
|
|
||||||
if isinstance(val, int):
|
|
||||||
try:
|
|
||||||
return int(cls.Values(val))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
elif isinstance(val, str):
|
|
||||||
member = cls.map_name_to_val(val, strict=False)
|
|
||||||
if member is not None:
|
|
||||||
return member
|
|
||||||
|
|
||||||
valid = ', '.join(m.name for m in cls.Values)
|
|
||||||
raise ValueError(f"{cls.get_name()}: invalid argument: {val!r}. Valid arguments are: {valid}")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def map_name_to_val(cls, name: str, strict=True) -> int:
|
|
||||||
"""Return the integer value for a given enum member name. Performs an exact match first,
|
|
||||||
then falls back to fuzzy matching (case-insensitive, punctuation-insensitive)."""
|
|
||||||
try:
|
|
||||||
return int(cls.Values[name])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
clean = cls.clean_name_str(name)
|
|
||||||
for member in cls.Values:
|
|
||||||
if cls.clean_name_str(member.name) == clean:
|
|
||||||
return int(member)
|
|
||||||
|
|
||||||
if strict:
|
|
||||||
valid = ', '.join(m.name for m in cls.Values)
|
|
||||||
raise ValueError(f"{cls.get_name()}: {name!r} is not a known value. Known values are: {valid}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def map_val_to_name(cls, val, strict=False) -> str:
|
|
||||||
"""Return the enum member name for a given integer value."""
|
|
||||||
try:
|
|
||||||
return cls.Values(val).name
|
|
||||||
except ValueError:
|
|
||||||
if strict:
|
|
||||||
raise ValueError(f"{cls.get_name()}: {val!r} ({type(val).__name__}) is not a known value.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def name_normalize(cls, name: str) -> str:
|
|
||||||
"""Map a (possibly fuzzy) name to its canonical enum member name."""
|
|
||||||
return cls.Values(cls.map_name_to_val(name)).name
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def clean_name_str(cls, val: str) -> str:
|
|
||||||
"""Strip punctuation and case for fuzzy name comparison.
|
|
||||||
Treats hyphens and underscores as equivalent (both removed)."""
|
|
||||||
return re.sub('[^0-9A-Za-z]', '', val).lower()
|
|
||||||
|
|
||||||
|
|
||||||
class Iccid(DecimalParam):
|
class Iccid(DecimalParam):
|
||||||
"""ICCID Parameter. Input: string of decimal digits.
|
"""ICCID Parameter. Input: string of decimal digits.
|
||||||
@@ -446,7 +309,6 @@ class Iccid(DecimalParam):
|
|||||||
min_len = 18
|
min_len = 18
|
||||||
max_len = 20
|
max_len = 20
|
||||||
example_input = '998877665544332211'
|
example_input = '998877665544332211'
|
||||||
default_source = param_source.IncDigitSource
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_val(cls, val):
|
def validate_val(cls, val):
|
||||||
@@ -460,17 +322,6 @@ class Iccid(DecimalParam):
|
|||||||
# patch MF/EF.ICCID
|
# patch MF/EF.ICCID
|
||||||
file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], h2b(enc_iccid(val)))
|
file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], h2b(enc_iccid(val)))
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
|
||||||
padded = b2h(pes.get_pe_for_type('header').decoded['iccid'])
|
|
||||||
iccid = unrpad(padded)
|
|
||||||
yield { cls.name: iccid }
|
|
||||||
|
|
||||||
for pe in pes.get_pes_for_type('mf'):
|
|
||||||
iccid_f = pe.files.get('ef-iccid', None)
|
|
||||||
if iccid_f is not None:
|
|
||||||
yield { cls.name: dec_iccid(b2h(iccid_f.body)) }
|
|
||||||
|
|
||||||
class Imsi(DecimalParam):
|
class Imsi(DecimalParam):
|
||||||
"""Configurable IMSI. Expects value to be a string of digits. Automatically sets the ACC to
|
"""Configurable IMSI. Expects value to be a string of digits. Automatically sets the ACC to
|
||||||
the last digit of the IMSI."""
|
the last digit of the IMSI."""
|
||||||
@@ -479,7 +330,6 @@ class Imsi(DecimalParam):
|
|||||||
min_len = 6
|
min_len = 6
|
||||||
max_len = 15
|
max_len = 15
|
||||||
example_input = '00101' + ('0' * 10)
|
example_input = '00101' + ('0' * 10)
|
||||||
default_source = param_source.IncDigitSource
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def apply_val(cls, pes: ProfileElementSequence, val):
|
def apply_val(cls, pes: ProfileElementSequence, val):
|
||||||
@@ -492,18 +342,6 @@ class Imsi(DecimalParam):
|
|||||||
file_replace_content(pe.decoded['ef-acc'], acc.to_bytes(2, 'big'))
|
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?
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
|
||||||
for pe in pes.get_pes_for_type('usim'):
|
|
||||||
imsi_f = pe.files.get('ef-imsi', None)
|
|
||||||
acc_f = pe.files.get('ef-acc', None)
|
|
||||||
y = {}
|
|
||||||
if imsi_f:
|
|
||||||
y[cls.name] = dec_imsi(b2h(imsi_f.body))
|
|
||||||
if acc_f:
|
|
||||||
y[cls.name + '-ACC'] = b2h(acc_f.body)
|
|
||||||
yield y
|
|
||||||
|
|
||||||
class SmspTpScAddr(ConfigurableParameter):
|
class SmspTpScAddr(ConfigurableParameter):
|
||||||
"""Configurable SMSC (SMS Service Centre) TP-SC-ADDR. Expects to be a phone number in national or
|
"""Configurable SMSC (SMS Service Centre) TP-SC-ADDR. Expects to be a phone number in national or
|
||||||
international format (designated by a leading +). Automatically sets the NPI to E.164 and the TON based on
|
international format (designated by a leading +). Automatically sets the NPI to E.164 and the TON based on
|
||||||
@@ -515,41 +353,22 @@ class SmspTpScAddr(ConfigurableParameter):
|
|||||||
max_len = 21 # '+' and 20 digits
|
max_len = 21 # '+' and 20 digits
|
||||||
min_len = 1
|
min_len = 1
|
||||||
example_input = '+49301234567'
|
example_input = '+49301234567'
|
||||||
default_source = param_source.ConstantSource
|
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def str_to_tuple(addr_str):
|
def validate_val(cls, val):
|
||||||
|
val = super().validate_val(val)
|
||||||
|
addr_str = str(val)
|
||||||
if addr_str[0] == '+':
|
if addr_str[0] == '+':
|
||||||
digits = addr_str[1:]
|
digits = addr_str[1:]
|
||||||
international = True
|
international = True
|
||||||
else:
|
else:
|
||||||
digits = addr_str
|
digits = addr_str
|
||||||
international = False
|
international = False
|
||||||
return (international, digits)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def tuple_to_str(addr_tuple):
|
|
||||||
international, digits = addr_tuple
|
|
||||||
if international:
|
|
||||||
ret = '+'
|
|
||||||
else:
|
|
||||||
ret = ''
|
|
||||||
ret += digits
|
|
||||||
return ret
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate_val(cls, val):
|
|
||||||
val = super().validate_val(val)
|
|
||||||
|
|
||||||
addr_tuple = cls.str_to_tuple(str(val))
|
|
||||||
|
|
||||||
international, digits = addr_tuple
|
|
||||||
if len(digits) > 20:
|
if len(digits) > 20:
|
||||||
raise ValueError(f'TP-SC-ADDR must not exceed 20 digits: {digits!r}')
|
raise ValueError(f'TP-SC-ADDR must not exceed 20 digits: {digits!r}')
|
||||||
if not digits.isdecimal():
|
if not digits.isdecimal():
|
||||||
raise ValueError(f'TP-SC-ADDR must only contain decimal digits: {digits!r}')
|
raise ValueError(f'TP-SC-ADDR must only contain decimal digits: {digits!r}')
|
||||||
|
return (international, digits)
|
||||||
return addr_tuple
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def apply_val(cls, pes: ProfileElementSequence, val):
|
def apply_val(cls, pes: ProfileElementSequence, val):
|
||||||
@@ -579,32 +398,6 @@ class SmspTpScAddr(ConfigurableParameter):
|
|||||||
# re-generate the pe.decoded member from the File instance
|
# re-generate the pe.decoded member from the File instance
|
||||||
pe.file2pe(f_smsp)
|
pe.file2pe(f_smsp)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
|
||||||
for pe in pes.get_pes_for_type('usim'):
|
|
||||||
f_smsp = pe.files['ef-smsp']
|
|
||||||
ef_smsp = EF_SMSP()
|
|
||||||
ef_smsp_dec = ef_smsp.decode_record_bin(f_smsp.body, 1)
|
|
||||||
|
|
||||||
tp_sc_addr = ef_smsp_dec.get('tp_sc_addr', None)
|
|
||||||
if not tp_sc_addr:
|
|
||||||
continue
|
|
||||||
|
|
||||||
digits = tp_sc_addr.get('call_number', None)
|
|
||||||
if not digits:
|
|
||||||
continue
|
|
||||||
|
|
||||||
ton_npi = tp_sc_addr.get('ton_npi', None)
|
|
||||||
if not ton_npi:
|
|
||||||
continue
|
|
||||||
international = ton_npi.get('type_of_number', None)
|
|
||||||
if international is None:
|
|
||||||
continue
|
|
||||||
international = (international == 'international')
|
|
||||||
|
|
||||||
yield { cls.name: cls.tuple_to_str((international, digits)) }
|
|
||||||
|
|
||||||
|
|
||||||
class SdKey(BinaryParam, metaclass=ClassVarMeta):
|
class SdKey(BinaryParam, metaclass=ClassVarMeta):
|
||||||
"""Configurable Security Domain (SD) Key. Value is presented as bytes."""
|
"""Configurable Security Domain (SD) Key. Value is presented as bytes."""
|
||||||
# these will be set by subclasses
|
# these will be set by subclasses
|
||||||
@@ -614,40 +407,28 @@ class SdKey(BinaryParam, metaclass=ClassVarMeta):
|
|||||||
key_usage_qual = None
|
key_usage_qual = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def apply_val(cls, pes: ProfileElementSequence, val):
|
def _apply_sd(cls, pe: ProfileElement, value):
|
||||||
set_components = [ SecurityDomainKeyComponent(cls.key_type, val) ]
|
assert pe.type == 'securityDomain'
|
||||||
|
for key in pe.decoded['keyList']:
|
||||||
for pe in pes.pe_list:
|
if key['keyIdentifier'][0] == cls.key_id and key['keyVersionNumber'][0] == cls.kvn:
|
||||||
if pe.type != 'securityDomain':
|
assert len(key['keyComponents']) == 1
|
||||||
continue
|
key['keyComponents'][0]['keyData'] = value
|
||||||
assert isinstance(pe, ProfileElementSD)
|
return
|
||||||
|
|
||||||
key = pe.find_key(key_version_number=cls.kvn, key_id=cls.key_id)
|
|
||||||
if not key:
|
|
||||||
# Could not find matching key to patch, create a new one
|
# Could not find matching key to patch, create a new one
|
||||||
key = SecurityDomainKey(
|
key = {
|
||||||
key_version_number=cls.kvn,
|
'keyUsageQualifier': bytes([cls.key_usage_qual]),
|
||||||
key_id=cls.key_id,
|
'keyIdentifier': bytes([cls.key_id]),
|
||||||
key_usage_qualifier=KeyUsageQualifier.build(cls.key_usage_qual),
|
'keyVersionNumber': bytes([cls.kvn]),
|
||||||
key_components=set_components,
|
'keyComponents': [
|
||||||
)
|
{ 'keyType': bytes([cls.key_type]), 'keyData': value },
|
||||||
pe.add_key(key)
|
]
|
||||||
else:
|
}
|
||||||
key.key_components = set_components
|
pe.decoded['keyList'].append(key)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
def apply_val(cls, pes: ProfileElementSequence, value):
|
||||||
for pe in pes.pe_list:
|
for pe in pes.get_pes_for_type('securityDomain'):
|
||||||
if pe.type != 'securityDomain':
|
cls._apply_sd(pe, value)
|
||||||
continue
|
|
||||||
assert isinstance(pe, ProfileElementSD)
|
|
||||||
|
|
||||||
key = pe.find_key(key_version_number=cls.kvn, key_id=cls.key_id)
|
|
||||||
if not key:
|
|
||||||
continue
|
|
||||||
kc = key.get_key_component(cls.key_type)
|
|
||||||
if kc:
|
|
||||||
yield { cls.name: b2h(kc) }
|
|
||||||
|
|
||||||
class SdKeyScp80_01(SdKey, kvn=0x01, key_type=0x88, permitted_len=[16,24,32]): # AES key type
|
class SdKeyScp80_01(SdKey, kvn=0x01, key_type=0x88, permitted_len=[16,24,32]): # AES key type
|
||||||
pass
|
pass
|
||||||
@@ -721,8 +502,7 @@ class Puk(DecimalHexParam):
|
|||||||
allow_len = 8
|
allow_len = 8
|
||||||
rpad = 16
|
rpad = 16
|
||||||
keyReference = None
|
keyReference = None
|
||||||
example_input = f'0*{allow_len}'
|
example_input = '0' * allow_len
|
||||||
default_source = param_source.RandomDigitSource
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def apply_val(cls, pes: ProfileElementSequence, val):
|
def apply_val(cls, pes: ProfileElementSequence, val):
|
||||||
@@ -736,14 +516,6 @@ class Puk(DecimalHexParam):
|
|||||||
raise ValueError("input template UPP has unexpected structure:"
|
raise ValueError("input template UPP has unexpected structure:"
|
||||||
f" cannot find pukCode with keyReference={cls.keyReference}")
|
f" cannot find pukCode with keyReference={cls.keyReference}")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
|
||||||
mf_pes = pes.pes_by_naa['mf'][0]
|
|
||||||
for pukCodes in obtain_all_pe_from_pelist(mf_pes, 'pukCodes'):
|
|
||||||
for pukCode in pukCodes.decoded['pukCodes']:
|
|
||||||
if pukCode['keyReference'] == cls.keyReference:
|
|
||||||
yield { cls.name: cls.decimal_hex_to_str(pukCode['pukValue']) }
|
|
||||||
|
|
||||||
class Puk1(Puk):
|
class Puk1(Puk):
|
||||||
name = 'PUK1'
|
name = 'PUK1'
|
||||||
keyReference = 0x01
|
keyReference = 0x01
|
||||||
@@ -757,8 +529,7 @@ class Pin(DecimalHexParam):
|
|||||||
rpad = 16
|
rpad = 16
|
||||||
min_len = 4
|
min_len = 4
|
||||||
max_len = 8
|
max_len = 8
|
||||||
example_input = f'0*{max_len}'
|
example_input = '0' * max_len
|
||||||
default_source = param_source.RandomDigitSource
|
|
||||||
keyReference = None
|
keyReference = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -780,24 +551,9 @@ class Pin(DecimalHexParam):
|
|||||||
raise ValueError('input template UPP has unexpected structure:'
|
raise ValueError('input template UPP has unexpected structure:'
|
||||||
+ f' {cls.get_name()} cannot find pinCode with keyReference={cls.keyReference}')
|
+ f' {cls.get_name()} cannot find pinCode with keyReference={cls.keyReference}')
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _read_all_pinvalues_from_pe(cls, pe: ProfileElement):
|
|
||||||
"This is a separate function because subclasses may feed different pe arguments."
|
|
||||||
for pinCodes in obtain_all_pe_from_pelist(pe, 'pinCodes'):
|
|
||||||
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
|
||||||
continue
|
|
||||||
|
|
||||||
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
|
||||||
if pinCode['keyReference'] == cls.keyReference:
|
|
||||||
yield { cls.name: cls.decimal_hex_to_str(pinCode['pinValue']) }
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
|
||||||
yield from cls._read_all_pinvalues_from_pe(pes.pes_by_naa['mf'][0])
|
|
||||||
|
|
||||||
class Pin1(Pin):
|
class Pin1(Pin):
|
||||||
name = 'PIN1'
|
name = 'PIN1'
|
||||||
example_input = '0*4' # PIN are usually 4 digits
|
example_input = '0' * 4 # PIN are usually 4 digits
|
||||||
keyReference = 0x01
|
keyReference = 0x01
|
||||||
|
|
||||||
class Pin2(Pin1):
|
class Pin2(Pin1):
|
||||||
@@ -816,14 +572,6 @@ class Pin2(Pin1):
|
|||||||
raise ValueError('input template UPP has unexpected structure:'
|
raise ValueError('input template UPP has unexpected structure:'
|
||||||
+ f' {cls.get_name()} cannot find pinCode with keyReference={cls.keyReference} in {naa=}')
|
+ f' {cls.get_name()} cannot find pinCode with keyReference={cls.keyReference} in {naa=}')
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
|
||||||
for naa in pes.pes_by_naa:
|
|
||||||
if naa not in ['usim','isim','csim','telecom']:
|
|
||||||
continue
|
|
||||||
for pe in pes.pes_by_naa[naa]:
|
|
||||||
yield from cls._read_all_pinvalues_from_pe(pe)
|
|
||||||
|
|
||||||
class Adm1(Pin):
|
class Adm1(Pin):
|
||||||
name = 'ADM1'
|
name = 'ADM1'
|
||||||
keyReference = 0x0A
|
keyReference = 0x0A
|
||||||
@@ -848,59 +596,26 @@ class AlgoConfig(ConfigurableParameter):
|
|||||||
raise ValueError('input template UPP has unexpected structure:'
|
raise ValueError('input template UPP has unexpected structure:'
|
||||||
f' {cls.__name__} cannot find algoParameter with key={cls.algo_config_key}')
|
f' {cls.__name__} cannot find algoParameter with key={cls.algo_config_key}')
|
||||||
|
|
||||||
@classmethod
|
class AlgorithmID(DecimalParam, AlgoConfig):
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
|
||||||
for pe in pes.get_pes_for_type('akaParameter'):
|
|
||||||
algoConfiguration = pe.decoded['algoConfiguration']
|
|
||||||
if len(algoConfiguration) < 2:
|
|
||||||
continue
|
|
||||||
if algoConfiguration[0] != 'algoParameter':
|
|
||||||
continue
|
|
||||||
if not algoConfiguration[1]:
|
|
||||||
continue
|
|
||||||
val = algoConfiguration[1].get(cls.algo_config_key, None)
|
|
||||||
if val is None:
|
|
||||||
continue
|
|
||||||
if isinstance(val, bytes):
|
|
||||||
val = b2h(val)
|
|
||||||
# if it is an int (algorithmID), just pass thru as int
|
|
||||||
yield { cls.name: val }
|
|
||||||
|
|
||||||
class AlgorithmID(EnumParam, AlgoConfig):
|
|
||||||
"""use validate_val() from EnumParam, and apply_val() from AlgoConfig.
|
|
||||||
In get_values_from_pes(), return enum value names, not raw values."""
|
|
||||||
name = "Algorithm"
|
|
||||||
algo_config_key = 'algorithmID'
|
algo_config_key = 'algorithmID'
|
||||||
example_input = "Milenage"
|
allow_len = 1
|
||||||
default_source = param_source.ConstantSource
|
example_input = 1 # Milenage
|
||||||
|
|
||||||
# as in pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
|
|
||||||
class Values(enum.IntEnum):
|
|
||||||
Milenage = 1
|
|
||||||
TUAK = 2
|
|
||||||
usim_test = 3 # input 'usim-test' also accepted via fuzzy matching
|
|
||||||
|
|
||||||
# EnumParam.validate_val() returns the int values from Values
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
def validate_val(cls, val):
|
||||||
# return enum names, not raw values.
|
val = super().validate_val(val)
|
||||||
# use of super(): this intends to call AlgoConfig.get_values_from_pes() so that the cls argument is this cls
|
val = int(val)
|
||||||
# here (AlgorithmID); i.e. AlgoConfig.get_values_from_pes(pes) doesn't work, because AlgoConfig needs to look up
|
valid = (1, 2, 3)
|
||||||
# cls.algo_config_key.
|
if val not in valid:
|
||||||
for d in super(cls, cls).get_values_from_pes(pes):
|
raise ValueError(f'Invalid algorithmID {val!r}, must be one of {valid}')
|
||||||
if cls.name in d:
|
return val
|
||||||
# convert int to value string
|
|
||||||
val = d[cls.name]
|
|
||||||
d[cls.name] = cls.map_val_to_name(val, strict=True)
|
|
||||||
yield d
|
|
||||||
|
|
||||||
class K(BinaryParam, AlgoConfig):
|
class K(BinaryParam, AlgoConfig):
|
||||||
"""use validate_val() from BinaryParam, and apply_val() from AlgoConfig"""
|
"""use validate_val() from BinaryParam, and apply_val() from AlgoConfig"""
|
||||||
name = 'K'
|
name = 'K'
|
||||||
algo_config_key = 'key'
|
algo_config_key = 'key'
|
||||||
allow_len = (128 // 8, 256 // 8) # length in bytes (from BinaryParam); TUAK also allows 256 bit
|
allow_len = (128 // 8, 256 // 8) # length in bytes (from BinaryParam); TUAK also allows 256 bit
|
||||||
example_input = f'00*{allow_len[0]}'
|
example_input = '00' * allow_len[0]
|
||||||
|
|
||||||
class Opc(K):
|
class Opc(K):
|
||||||
name = 'OPc'
|
name = 'OPc'
|
||||||
@@ -914,7 +629,6 @@ class MilenageRotationConstants(BinaryParam, AlgoConfig):
|
|||||||
algo_config_key = 'rotationConstants'
|
algo_config_key = 'rotationConstants'
|
||||||
allow_len = 5 # length in bytes (from BinaryParam)
|
allow_len = 5 # length in bytes (from BinaryParam)
|
||||||
example_input = '40 00 20 40 60'
|
example_input = '40 00 20 40 60'
|
||||||
default_source = param_source.ConstantSource
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_val(cls, val):
|
def validate_val(cls, val):
|
||||||
@@ -945,7 +659,6 @@ class MilenageXoringConstants(BinaryParam, AlgoConfig):
|
|||||||
' 00000000000000000000000000000002'
|
' 00000000000000000000000000000002'
|
||||||
' 00000000000000000000000000000004'
|
' 00000000000000000000000000000004'
|
||||||
' 00000000000000000000000000000008')
|
' 00000000000000000000000000000008')
|
||||||
default_source = param_source.ConstantSource
|
|
||||||
|
|
||||||
class TuakNumberOfKeccak(IntegerParam, AlgoConfig):
|
class TuakNumberOfKeccak(IntegerParam, AlgoConfig):
|
||||||
"""Number of iterations of Keccak-f[1600] permutation as recomended by Section 7.2 of 3GPP TS 35.231"""
|
"""Number of iterations of Keccak-f[1600] permutation as recomended by Section 7.2 of 3GPP TS 35.231"""
|
||||||
@@ -954,4 +667,3 @@ class TuakNumberOfKeccak(IntegerParam, AlgoConfig):
|
|||||||
min_val = 1
|
min_val = 1
|
||||||
max_val = 255
|
max_val = 255
|
||||||
example_input = '1'
|
example_input = '1'
|
||||||
default_source = param_source.ConstantSource
|
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ class UiccSdInstallParams(TLV_IE_Collection, nested=[UiccScp, AcceptExtradAppsAn
|
|||||||
|
|
||||||
# Key Usage:
|
# Key Usage:
|
||||||
# KVN 0x01 .. 0x0F reserved for SCP80
|
# KVN 0x01 .. 0x0F reserved for SCP80
|
||||||
# KVN 0x81 .. 0x8f reserved for SCP81
|
|
||||||
# KVN 0x11 reserved for DAP specified in ETSI TS 102 226
|
# KVN 0x11 reserved for DAP specified in ETSI TS 102 226
|
||||||
# KVN 0x20 .. 0x2F reserved for SCP02
|
# KVN 0x20 .. 0x2F reserved for SCP02
|
||||||
# KID 0x01 = ENC; 0x02 = MAC; 0x03 = DEK
|
# KID 0x01 = ENC; 0x02 = MAC; 0x03 = DEK
|
||||||
|
|||||||
@@ -152,8 +152,7 @@ class SimCard(SimCardBase):
|
|||||||
return sw
|
return sw
|
||||||
|
|
||||||
def update_smsp(self, smsp):
|
def update_smsp(self, smsp):
|
||||||
print("using update_smsp")
|
data, sw = self._scc.update_record(EF['SMSP'], 1, rpad(smsp, 84))
|
||||||
data, sw = self._scc.update_record(EF['SMSP'], 1, smsp, leftpad=True)
|
|
||||||
return sw
|
return sw
|
||||||
|
|
||||||
def update_ad(self, mnc=None, opmode=None, ofm=None, path=EF['AD']):
|
def update_ad(self, mnc=None, opmode=None, ofm=None, path=EF['AD']):
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ class LinkBaseTpdu(LinkBase):
|
|||||||
|
|
||||||
# After sending the APDU/TPDU the UICC/eUICC or SIM may response with a status word that indicates that further
|
# After sending the APDU/TPDU the UICC/eUICC or SIM may response with a status word that indicates that further
|
||||||
# TPDUs have to be sent in order to complete the task.
|
# TPDUs have to be sent in order to complete the task.
|
||||||
|
if sw is not None:
|
||||||
if case == 4 or self.apdu_strict == False:
|
if case == 4 or self.apdu_strict == False:
|
||||||
# In case the APDU is a case #4 APDU, the UICC/eUICC/SIM may indicate that there is response data
|
# In case the APDU is a case #4 APDU, the UICC/eUICC/SIM may indicate that there is response data
|
||||||
# available which has to be retrieved using a GET RESPONSE command TPDU.
|
# available which has to be retrieved using a GET RESPONSE command TPDU.
|
||||||
|
|||||||
@@ -251,16 +251,6 @@ class EF_SMSP(LinFixedEF):
|
|||||||
"numbering_plan_id": "isdn_e164" },
|
"numbering_plan_id": "isdn_e164" },
|
||||||
"call_number": "4915790109999" },
|
"call_number": "4915790109999" },
|
||||||
"tp_pid": b"\x00", "tp_dcs": b"\x00", "tp_vp_minutes": 4320 } ),
|
"tp_pid": b"\x00", "tp_dcs": b"\x00", "tp_vp_minutes": 4320 } ),
|
||||||
( 'e1ffffffffffffffffffffffff0891945197109099f9ffffff0000a9',
|
|
||||||
{ "alpha_id": "", "parameter_indicators": { "tp_dest_addr": False, "tp_sc_addr": True,
|
|
||||||
"tp_pid": True, "tp_dcs": True, "tp_vp": True },
|
|
||||||
"tp_dest_addr": { "length": 255, "ton_npi": { "ext": True, "type_of_number": "reserved_for_extension",
|
|
||||||
"numbering_plan_id": "reserved_for_extension" },
|
|
||||||
"call_number": "" },
|
|
||||||
"tp_sc_addr": { "length": 8, "ton_npi": { "ext": True, "type_of_number": "international",
|
|
||||||
"numbering_plan_id": "isdn_e164" },
|
|
||||||
"call_number": "4915790109999" },
|
|
||||||
"tp_pid": b"\x00", "tp_dcs": b"\x00", "tp_vp_minutes": 4320 } ),
|
|
||||||
( '454e6574776f726b73fffffffffffffff1ffffffffffffffffffffffffffffffffffffffffffffffff0000a7',
|
( '454e6574776f726b73fffffffffffffff1ffffffffffffffffffffffffffffffffffffffffffffffff0000a7',
|
||||||
{ "alpha_id": "ENetworks", "parameter_indicators": { "tp_dest_addr": False, "tp_sc_addr": True,
|
{ "alpha_id": "ENetworks", "parameter_indicators": { "tp_dest_addr": False, "tp_sc_addr": True,
|
||||||
"tp_pid": True, "tp_dcs": True, "tp_vp": False },
|
"tp_pid": True, "tp_dcs": True, "tp_vp": False },
|
||||||
@@ -341,8 +331,7 @@ class EF_SMSP(LinFixedEF):
|
|||||||
'ton_npi'/TonNpi, 'call_number'/PaddedBcdAdapter(Rpad(Bytes(10))))
|
'ton_npi'/TonNpi, 'call_number'/PaddedBcdAdapter(Rpad(Bytes(10))))
|
||||||
DestAddr = Struct('length'/Rebuild(Int8ub, lambda ctx: EF_SMSP.dest_addr_len(ctx)),
|
DestAddr = Struct('length'/Rebuild(Int8ub, lambda ctx: EF_SMSP.dest_addr_len(ctx)),
|
||||||
'ton_npi'/TonNpi, 'call_number'/PaddedBcdAdapter(Rpad(Bytes(10))))
|
'ton_npi'/TonNpi, 'call_number'/PaddedBcdAdapter(Rpad(Bytes(10))))
|
||||||
# (see comment below)
|
self._construct = Struct('alpha_id'/COptional(GsmOrUcs2Adapter(Rpad(Bytes(this._.total_len-28)))),
|
||||||
self._construct = Struct('alpha_id'/GsmOrUcs2Adapter(Rpad(Bytes(this._.total_len-28))),
|
|
||||||
'parameter_indicators'/InvertAdapter(BitStruct(
|
'parameter_indicators'/InvertAdapter(BitStruct(
|
||||||
Const(7, BitsInteger(3)),
|
Const(7, BitsInteger(3)),
|
||||||
'tp_vp'/Flag,
|
'tp_vp'/Flag,
|
||||||
@@ -356,25 +345,6 @@ class EF_SMSP(LinFixedEF):
|
|||||||
'tp_dcs'/Bytes(1),
|
'tp_dcs'/Bytes(1),
|
||||||
'tp_vp_minutes'/EF_SMSP.ValidityPeriodAdapter(Byte))
|
'tp_vp_minutes'/EF_SMSP.ValidityPeriodAdapter(Byte))
|
||||||
|
|
||||||
# Ensure 'alpha_id' is always present
|
|
||||||
def encode_record_hex(self, abstract_data: dict, record_nr: int, total_len: int = None) -> str:
|
|
||||||
# Problem: TS 51.011 Section 10.5.6 describes the 'alpha_id' field as optional. However, this is only true
|
|
||||||
# at the time when the record length of the file is set up in the file system. A card manufacturer may decide
|
|
||||||
# to remove the field by setting the record length to 28. Likewise, the card manaufacturer may also decide to
|
|
||||||
# set the field to a distinct length by setting the record length to a value greater than 28 (e.g. 14 bytes
|
|
||||||
# 'alpha_id' + 28 bytes). Due to the fixed nature of the record length, this eventually means that in practice
|
|
||||||
# 'alpha_id' is a mandatory field with a fixed length.
|
|
||||||
#
|
|
||||||
# Due to the problematic specification of 'alpha_id' as a pseudo-optional field at the beginning of a
|
|
||||||
# fixed-size memory, the construct definition in self._construct has been incorrectly implemented and the field
|
|
||||||
# has been marked as COptional. We may correct the problem by removing COptional. But to maintain compatibility,
|
|
||||||
# we then have to ensure that in case the field is not provided (None), it is set to an empty string ('').
|
|
||||||
#
|
|
||||||
# See also ts_31_102.py, class EF_OCI for a correct example.
|
|
||||||
if abstract_data['alpha_id'] is None:
|
|
||||||
abstract_data['alpha_id'] = ''
|
|
||||||
return super().encode_record_hex(abstract_data, record_nr, total_len)
|
|
||||||
|
|
||||||
# TS 51.011 Section 10.5.7
|
# TS 51.011 Section 10.5.7
|
||||||
class EF_SMSS(TransparentEF):
|
class EF_SMSS(TransparentEF):
|
||||||
class MemCapAdapter(Adapter):
|
class MemCapAdapter(Adapter):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ jsonpath-ng
|
|||||||
construct>=2.10.70
|
construct>=2.10.70
|
||||||
bidict
|
bidict
|
||||||
pyosmocom>=0.0.12
|
pyosmocom>=0.0.12
|
||||||
pyyaml>=5.4
|
pyyaml>=5.1
|
||||||
termcolor
|
termcolor
|
||||||
colorlog
|
colorlog
|
||||||
pycryptodomex
|
pycryptodomex
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -26,7 +26,7 @@ setup(
|
|||||||
"construct >= 2.10.70",
|
"construct >= 2.10.70",
|
||||||
"bidict",
|
"bidict",
|
||||||
"pyosmocom >= 0.0.12",
|
"pyosmocom >= 0.0.12",
|
||||||
"pyyaml >= 5.4",
|
"pyyaml >= 5.1",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"colorlog",
|
"colorlog",
|
||||||
"pycryptodomex",
|
"pycryptodomex",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ ICCID: 8988219000000117833
|
|||||||
IMSI: 001010000000111
|
IMSI: 001010000000111
|
||||||
GID1: ffffffffffffffff
|
GID1: ffffffffffffffff
|
||||||
GID2: ffffffffffffffff
|
GID2: ffffffffffffffff
|
||||||
SMSP: ffffffffffffffffffffffffffffe1ffffffffffffffffffffffff0581005155f5ffffffffffff000000
|
SMSP: e1ffffffffffffffffffffffff0581005155f5ffffffffffff000000ffffffffffffffffffffffffffff
|
||||||
SMSC: 0015555
|
SMSC: 0015555
|
||||||
SPN: Fairwaves
|
SPN: Fairwaves
|
||||||
Show in HPLMN: False
|
Show in HPLMN: False
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ ICCID: 89445310150011013678
|
|||||||
IMSI: 001010000000102
|
IMSI: 001010000000102
|
||||||
GID1: Can't read file -- SW match failed! Expected 9000 and got 6a82.
|
GID1: Can't read file -- SW match failed! Expected 9000 and got 6a82.
|
||||||
GID2: Can't read file -- SW match failed! Expected 9000 and got 6a82.
|
GID2: Can't read file -- SW match failed! Expected 9000 and got 6a82.
|
||||||
SMSP: ffffffffffffffffffffffffffffe1ffffffffffffffffffffffff0581005155f5ffffffffffff000000
|
SMSP: e1ffffffffffffffffffffffff0581005155f5ffffffffffff000000ffffffffffffffffffffffffffff
|
||||||
SMSC: 0015555
|
SMSC: 0015555
|
||||||
SPN: wavemobile
|
SPN: wavemobile
|
||||||
Show in HPLMN: False
|
Show in HPLMN: False
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Utility to verify the functionality of pySim-smpp2sim.py
|
# Utility to verify the functionality of pySim-trace.py
|
||||||
#
|
#
|
||||||
# (C) 2026 by sysmocom - s.f.m.c. GmbH
|
# (C) 2026 by sysmocom - s.f.m.c. GmbH
|
||||||
# All Rights Reserved
|
# All Rights Reserved
|
||||||
|
|||||||
Reference in New Issue
Block a user