blob: 2d3ad82f788abd8da797d0de03d35dacb476dcfe [file] [log] [blame]
Harald Welte21caf322022-07-16 14:06:46 +02001# coding=utf-8
2"""APDU definitions/decoders of ETSI TS 102 221, the core UICC spec.
3
4(C) 2022 by Harald Welte <laforge@osmocom.org>
5
6This program is free software: you can redistribute it and/or modify
7it under the terms of the GNU General Public License as published by
8the Free Software Foundation, either version 2 of the License, or
9(at your option) any later version.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program. If not, see <http://www.gnu.org/licenses/>.
18"""
19
20import logging
21from pySim.construct import *
22from pySim.filesystem import *
Harald Welte531894d2023-07-11 19:11:11 +020023from pySim.runtime import RuntimeLchan
Harald Welte21caf322022-07-16 14:06:46 +020024from pySim.apdu import ApduCommand, ApduCommandSet
25from typing import Optional, Dict, Tuple
26
27logger = logging.getLogger(__name__)
28
29# TS 102 221 Section 11.1.1
30class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
31 _apdu_case = 4
32 _construct_p1 = Enum(Byte, df_ef_or_mf_by_file_id=0, child_df_of_current_df=1, parent_df_of_current_df=3,
33 df_name=4, path_from_mf=8, path_from_current_df=9)
34 _construct_p2 = BitStruct(Flag,
35 'app_session_control'/Enum(BitsInteger(2), activation_reset=0, termination=2),
36 'return'/Enum(BitsInteger(3), fcp=1, no_data=3),
37 'aid_control'/Enum(BitsInteger(2), first_or_only=0, last=1, next=2, previous=3))
38
39 @staticmethod
40 def _find_aid_substr(selectables, aid) -> Optional[CardADF]:
41 # full-length match
42 if aid in selectables:
43 return selectables[aid]
44 # sub-string match
45 for s in selectables.keys():
46 if aid[:len(s)] == s:
47 return selectables[s]
48 return None
49
50 def process_on_lchan(self, lchan: RuntimeLchan):
51 mode = self.cmd_dict['p1']
52 if mode in ['path_from_mf', 'path_from_current_df']:
53 # rewind to MF, if needed
54 if mode == 'path_from_mf':
55 lchan.selected_file = lchan.rs.mf
56 path = [self.cmd_data[i:i+2] for i in range(0, len(self.cmd_data), 2)]
57 for file in path:
58 file_hex = b2h(file)
59 if file_hex == '7fff': # current application
60 if not lchan.selected_adf:
61 sels = lchan.rs.mf.get_app_selectables(['ANAMES'])
62 # HACK: Assume USIM
63 logger.warning('SELECT relative to current ADF, but no ADF selected. Assuming ADF.USIM')
64 lchan.selected_adf = sels['ADF.USIM']
65 lchan.selected_file = lchan.selected_adf
66 #print("\tSELECT CUR_ADF %s" % lchan.selected_file)
Harald Welte8e9c8442022-07-24 11:55:04 +020067 # iterate to next element in path
Harald Welte21caf322022-07-16 14:06:46 +020068 continue
69 else:
Harald Welte93c34aa2022-07-24 12:23:56 +020070 sels = lchan.selected_file.get_selectables(['FIDS','MF','PARENT','SELF'])
Harald Welte21caf322022-07-16 14:06:46 +020071 if file_hex in sels:
72 if self.successful:
73 #print("\tSELECT %s" % sels[file_hex])
74 lchan.selected_file = sels[file_hex]
75 else:
76 #print("\tSELECT %s FAILED" % sels[file_hex])
77 pass
Harald Welte8e9c8442022-07-24 11:55:04 +020078 # iterate to next element in path
Harald Welte21caf322022-07-16 14:06:46 +020079 continue
80 logger.warning('SELECT UNKNOWN FID %s (%s)' % (file_hex, '/'.join([b2h(x) for x in path])))
81 elif mode == 'df_ef_or_mf_by_file_id':
82 if len(self.cmd_data) != 2:
83 raise ValueError('Expecting a 2-byte FID')
Harald Welte93c34aa2022-07-24 12:23:56 +020084 sels = lchan.selected_file.get_selectables(['FIDS','MF','PARENT','SELF'])
Harald Welte498361f2022-07-24 11:39:07 +020085 file_hex = b2h(self.cmd_data)
86 if file_hex in sels:
87 if self.successful:
88 #print("\tSELECT %s" % sels[file_hex])
89 lchan.selected_file = sels[file_hex]
90 else:
91 #print("\tSELECT %s FAILED" % sels[file_hex])
92 pass
93 else:
94 logger.warning('SELECT UNKNOWN FID %s' % (file_hex))
Harald Welte21caf322022-07-16 14:06:46 +020095 elif mode == 'df_name':
96 # Select by AID (can be sub-string!)
97 aid = self.cmd_dict['body']
98 sels = lchan.rs.mf.get_app_selectables(['AIDS'])
99 adf = self._find_aid_substr(sels, aid)
100 if adf:
101 lchan.selected_adf = adf
102 lchan.selected_file = lchan.selected_adf
103 #print("\tSELECT AID %s" % adf)
104 else:
105 logger.warning('SELECT UNKNOWN AID %s' % aid)
106 pass
107 else:
108 raise ValueError('Select Mode %s not implemented' % mode)
109 # decode the SELECT response
110 if self.successful:
111 self.file = lchan.selected_file
Harald Weltec61fbf42022-07-24 09:44:31 +0200112 if 'body' in self.rsp_dict:
113 # not every SELECT is asking for the FCP in response...
114 return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
Harald Welte21caf322022-07-16 14:06:46 +0200115 return None
116
117
118
119# TS 102 221 Section 11.1.2
120class UiccStatus(ApduCommand, n='STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
121 _apdu_case = 2
122 _construct_p1 = Enum(Byte, no_indication=0, current_app_is_initialized=1, terminal_will_terminate_current_app=2)
123 _construct_p2 = Enum(Byte, response_like_select=0, response_df_name_tlv=1, response_no_data=0x0c)
124
125 def process_on_lchan(self, lchan):
126 if self.cmd_dict['p2'] == 'response_like_select':
127 return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
128
129def _decode_binary_p1p2(p1, p2) -> Dict:
130 ret = {}
131 if p1 & 0x80:
132 ret['file'] = 'sfi'
133 ret['sfi'] = p1 & 0x1f
134 ret['offset'] = p2
135 else:
136 ret['file'] = 'currently_selected_ef'
137 ret['offset'] = ((p1 & 0x7f) << 8) & p2
138 return ret
139
140# TS 102 221 Section 11.1.3
141class ReadBinary(ApduCommand, n='READ BINARY', ins=0xB0, cla=['0X', '4X', '6X']):
142 _apdu_case = 2
143 def _decode_p1p2(self):
144 return _decode_binary_p1p2(self.p1, self.p2)
145
146 def process_on_lchan(self, lchan):
147 self._determine_file(lchan)
148 if not isinstance(self.file, TransparentEF):
149 return b2h(self.rsp_data)
150 # our decoders don't work for non-zero offsets / short reads
151 if self.cmd_dict['offset'] != 0 or self.lr < self.file.size[0]:
152 return b2h(self.rsp_data)
153 method = getattr(self.file, 'decode_bin', None)
154 if self.successful and callable(method):
155 return method(self.rsp_data)
156
157# TS 102 221 Section 11.1.4
158class UpdateBinary(ApduCommand, n='UPDATE BINARY', ins=0xD6, cla=['0X', '4X', '6X']):
159 _apdu_case = 3
160 def _decode_p1p2(self):
161 return _decode_binary_p1p2(self.p1, self.p2)
162
163 def process_on_lchan(self, lchan):
164 self._determine_file(lchan)
165 if not isinstance(self.file, TransparentEF):
166 return b2h(self.rsp_data)
167 # our decoders don't work for non-zero offsets / short writes
168 if self.cmd_dict['offset'] != 0 or self.lc < self.file.size[0]:
169 return b2h(self.cmd_data)
170 method = getattr(self.file, 'decode_bin', None)
171 if self.successful and callable(method):
172 return method(self.cmd_data)
173
174def _decode_record_p1p2(p1, p2):
175 ret = {}
176 ret['record_number'] = p1
177 if p2 >> 3 == 0:
178 ret['file'] = 'currently_selected_ef'
179 else:
180 ret['file'] = 'sfi'
181 ret['sfi'] = p2 >> 3
182 mode = p2 & 0x7
183 if mode == 2:
184 ret['mode'] = 'next_record'
185 elif mode == 3:
186 ret['mode'] = 'previous_record'
187 elif mode == 8:
188 ret['mode'] = 'absolute_current'
189 return ret
190
191# TS 102 221 Section 11.1.5
192class ReadRecord(ApduCommand, n='READ RECORD', ins=0xB2, cla=['0X', '4X', '6X']):
193 _apdu_case = 2
194 def _decode_p1p2(self):
195 r = _decode_record_p1p2(self.p1, self.p2)
196 self.col_id = '%02u' % r['record_number']
197 return r
198
199 def process_on_lchan(self, lchan):
200 self._determine_file(lchan)
201 if not isinstance(self.file, LinFixedEF):
202 return b2h(self.rsp_data)
203 method = getattr(self.file, 'decode_record_bin', None)
204 if self.successful and callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100205 return method(self.rsp_data, self.cmd_dict['record_number'])
Harald Welte21caf322022-07-16 14:06:46 +0200206
207# TS 102 221 Section 11.1.6
208class UpdateRecord(ApduCommand, n='UPDATE RECORD', ins=0xDC, cla=['0X', '4X', '6X']):
209 _apdu_case = 3
210 def _decode_p1p2(self):
211 r = _decode_record_p1p2(self.p1, self.p2)
212 self.col_id = '%02u' % r['record_number']
213 return r
214
215 def process_on_lchan(self, lchan):
216 self._determine_file(lchan)
217 if not isinstance(self.file, LinFixedEF):
218 return b2h(self.cmd_data)
219 method = getattr(self.file, 'decode_record_bin', None)
220 if self.successful and callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100221 return method(self.cmd_data, self.cmd_dict['record_number'])
Harald Welte21caf322022-07-16 14:06:46 +0200222
223# TS 102 221 Section 11.1.7
224class SearchRecord(ApduCommand, n='SEARCH RECORD', ins=0xA2, cla=['0X', '4X', '6X']):
225 _apdu_case = 4
226 _construct_rsp = GreedyRange(Int8ub)
227
228 def _decode_p1p2(self):
229 ret = {}
230 sfi = self.p2 >> 3
231 if sfi == 0:
232 ret['file'] = 'currently_selected_ef'
233 else:
234 ret['file'] = 'sfi'
235 ret['sfi'] = sfi
236 mode = self.p2 & 0x7
237 if mode in [0x4, 0x5]:
238 if mode == 0x4:
239 ret['mode'] = 'forward_search'
240 else:
241 ret['mode'] = 'backward_search'
242 ret['record_number'] = self.p1
243 self.col_id = '%02u' % ret['record_number']
244 elif mode == 6:
245 ret['mode'] = 'enhanced_search'
246 # TODO: further decode
247 elif mode == 7:
248 ret['mode'] = 'proprietary_search'
249 return ret
250
251 def _decode_cmd(self):
252 ret = self._decode_p1p2()
253 if self.cmd_data:
254 if ret['mode'] == 'enhanced_search':
255 ret['search_indication'] = b2h(self.cmd_data[:2])
256 ret['search_string'] = b2h(self.cmd_data[2:])
257 else:
258 ret['search_string'] = b2h(self.cmd_data)
259 return ret
260
261 def process_on_lchan(self, lchan):
262 self._determine_file(lchan)
263 return self.to_dict()
264
265# TS 102 221 Section 11.1.8
266class Increase(ApduCommand, n='INCREASE', ins=0x32, cla=['8X', 'CX', 'EX']):
267 _apdu_case = 4
268
269PinConstructP2 = BitStruct('scope'/Enum(Flag, global_mf=0, specific_df_adf=1),
270 BitsInteger(2), 'reference_data_nr'/BitsInteger(5))
271# TS 102 221 Section 11.1.9
272class VerifyPin(ApduCommand, n='VERIFY PIN', ins=0x20, cla=['0X', '4X', '6X']):
273 _apdu_case = 3
274 _construct_p2 = PinConstructP2
275
276 @staticmethod
277 def _pin_process(apdu):
278 processed = {
279 'scope': apdu.cmd_dict['p2']['scope'],
280 'referenced_data_nr': apdu.cmd_dict['p2']['reference_data_nr'],
281 }
282 if apdu.lc == 0:
283 # this is just a question on the counters remaining
284 processed['mode'] = 'check_remaining_attempts'
285 else:
286 processed['pin'] = b2h(apdu.cmd_data)
287 if apdu.sw[0] == 0x63:
288 processed['remaining_attempts'] = apdu.sw[1] & 0xf
289 return processed
290
291 @staticmethod
292 def _pin_is_success(sw):
293 if sw[0] == 0x63:
294 return True
295 else:
296 return False
297
298 def process_on_lchan(self, lchan: RuntimeLchan):
299 return VerifyPin._pin_process(self)
300
301 def _is_success(self):
302 return VerifyPin._pin_is_success(self.sw)
303
304
305# TS 102 221 Section 11.1.10
306class ChangePin(ApduCommand, n='CHANGE PIN', ins=0x24, cla=['0X', '4X', '6X']):
307 _apdu_case = 3
308 _construct_p2 = PinConstructP2
309
310 def process_on_lchan(self, lchan: RuntimeLchan):
311 return VerifyPin._pin_process(self)
312
313 def _is_success(self):
314 return VerifyPin._pin_is_success(self.sw)
315
316
317# TS 102 221 Section 11.1.11
318class DisablePin(ApduCommand, n='DISABLE PIN', ins=0x26, cla=['0X', '4X', '6X']):
319 _apdu_case = 3
320 _construct_p2 = PinConstructP2
321
322 def process_on_lchan(self, lchan: RuntimeLchan):
323 return VerifyPin._pin_process(self)
324
325 def _is_success(self):
326 return VerifyPin._pin_is_success(self.sw)
327
328
329# TS 102 221 Section 11.1.12
330class EnablePin(ApduCommand, n='ENABLE PIN', ins=0x28, cla=['0X', '4X', '6X']):
331 _apdu_case = 3
332 _construct_p2 = PinConstructP2
333 def process_on_lchan(self, lchan: RuntimeLchan):
334 return VerifyPin._pin_process(self)
335
336 def _is_success(self):
337 return VerifyPin._pin_is_success(self.sw)
338
339
340# TS 102 221 Section 11.1.13
341class UnblockPin(ApduCommand, n='UNBLOCK PIN', ins=0x2C, cla=['0X', '4X', '6X']):
342 _apdu_case = 3
343 _construct_p2 = PinConstructP2
344
345 def process_on_lchan(self, lchan: RuntimeLchan):
346 return VerifyPin._pin_process(self)
347
348 def _is_success(self):
349 return VerifyPin._pin_is_success(self.sw)
350
351
352# TS 102 221 Section 11.1.14
353class DeactivateFile(ApduCommand, n='DEACTIVATE FILE', ins=0x04, cla=['0X', '4X', '6X']):
354 _apdu_case = 1
355 _construct_p1 = BitStruct(BitsInteger(4),
356 'select_mode'/Enum(BitsInteger(4), ef_by_file_id=0,
357 path_from_mf=8, path_from_current_df=9))
358
359# TS 102 221 Section 11.1.15
360class ActivateFile(ApduCommand, n='ACTIVATE FILE', ins=0x44, cla=['0X', '4X', '6X']):
361 _apdu_case = 1
362 _construct_p1 = DeactivateFile._construct_p1
363
364# TS 102 221 Section 11.1.16
365auth_p2_construct = BitStruct('scope'/Enum(Flag, mf=0, df_adf_specific=1),
366 BitsInteger(2),
367 'reference_data_nr'/BitsInteger(5))
368class Authenticate88(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '4X', '6X']):
369 _apdu_case = 4
370 _construct_p2 = auth_p2_construct
371
372# TS 102 221 Section 11.1.16
373class Authenticate89(ApduCommand, n='AUTHENTICATE', ins=0x89, cla=['0X', '4X', '6X']):
374 _apdu_case = 4
375 _construct_p2 = auth_p2_construct
376
377# TS 102 221 Section 11.1.17
378class ManageChannel(ApduCommand, n='MANAGE CHANNEL', ins=0x70, cla=['0X', '4X', '6X']):
379 _apdu_case = 2
380 _construct_p1 = Enum(Flag, open_channel=0, close_channel=1)
381 _construct_p2 = Struct('logical_channel_number'/Int8ub)
382 _construct_rsp = Struct('logical_channel_number'/Int8ub)
383
384 def process_global(self, rs):
385 if not self.successful:
386 return
387 mode = self.cmd_dict['p1']
388 if mode == 'open_channel':
389 created_channel_nr = self.cmd_dict['p2']['logical_channel_number']
390 if created_channel_nr == 0:
391 # auto-assignment by UICC
392 # pylint: disable=unsubscriptable-object
393 created_channel_nr = self.rsp_data[0]
394 manage_channel = rs.get_lchan_by_cla(self.cla)
395 manage_channel.add_lchan(created_channel_nr)
396 self.col_id = '%02u' % created_channel_nr
Harald Welte74899472022-12-02 23:16:12 +0100397 return {'mode': mode, 'created_channel': created_channel_nr }
Harald Welte21caf322022-07-16 14:06:46 +0200398 elif mode == 'close_channel':
Philipp Maier7d86fe12023-07-19 15:58:45 +0200399 closed_channel_nr = self.cmd_dict['p2']['logical_channel_number']
Harald Welte21caf322022-07-16 14:06:46 +0200400 rs.del_lchan(closed_channel_nr)
401 self.col_id = '%02u' % closed_channel_nr
Harald Welte74899472022-12-02 23:16:12 +0100402 return {'mode': mode, 'closed_channel': closed_channel_nr }
Harald Welte21caf322022-07-16 14:06:46 +0200403 else:
404 raise ValueError('Unsupported MANAGE CHANNEL P1=%02X' % self.p1)
405
406# TS 102 221 Section 11.1.18
407class GetChallenge(ApduCommand, n='GET CHALLENGE', ins=0x84, cla=['0X', '4X', '6X']):
408 _apdu_case = 2
409
410# TS 102 221 Section 11.1.19
411class TerminalCapability(ApduCommand, n='TERMINAL CAPABILITY', ins=0xAA, cla=['8X', 'CX', 'EX']):
412 _apdu_case = 3
413
414# TS 102 221 Section 11.1.20
415class ManageSecureChannel(ApduCommand, n='MANAGE SECURE CHANNEL', ins=0x73, cla=['0X', '4X', '6X']):
416 @classmethod
417 def _get_apdu_case(cls, hdr:bytes) -> int:
418 p1 = hdr[2]
419 p2 = hdr[3]
420 if p1 & 0x7 == 0: # retrieve UICC Endpoints
421 return 2
422 elif p1 & 0xf in [1,2,3]: # establish sa, start secure channel SA
423 p2_cmd = p2 >> 5
424 if p2_cmd in [0,2,4]: # command data
425 return 3
426 elif p2_cmd in [1,3,5]: # response data
427 return 2
428 elif p1 & 0xf == 4: # terminate secure channel SA
429 return 3
430 raise ValueError('%s: Unable to detect APDU case for %s' % (cls.__name__, b2h(hdr)))
431
432# TS 102 221 Section 11.1.21
433class TransactData(ApduCommand, n='TRANSACT DATA', ins=0x75, cla=['0X', '4X', '6X']):
434 @classmethod
435 def _get_apdu_case(cls, hdr:bytes) -> int:
436 p1 = hdr[2]
437 if p1 & 0x04:
438 return 3
439 else:
440 return 2
441
442# TS 102 221 Section 11.1.22
443class SuspendUicc(ApduCommand, n='SUSPEND UICC', ins=0x76, cla=['80']):
444 _apdu_case = 4
445 _construct_p1 = BitStruct('rfu'/BitsInteger(7), 'mode'/Enum(Flag, suspend=0, resume=1))
446
447# TS 102 221 Section 11.1.23
448class GetIdentity(ApduCommand, n='GET IDENTITY', ins=0x78, cla=['8X', 'CX', 'EX']):
449 _apdu_case = 4
450 _construct_p2 = BitStruct('scope'/Enum(Flag, mf=0, df_adf_specific=1), BitsInteger(7))
451
452# TS 102 221 Section 11.1.24
453class ExchangeCapabilities(ApduCommand, n='EXCHANGE CAPABILITIES', ins=0x7A, cla=['80']):
454 _apdu_case = 4
455
456# TS 102 221 Section 11.2.1
457class TerminalProfile(ApduCommand, n='TERMINAL PROFILE', ins=0x10, cla=['80']):
458 _apdu_case = 3
459
460# TS 102 221 Section 11.2.2 / TS 102 223
461class Envelope(ApduCommand, n='ENVELOPE', ins=0xC2, cla=['80']):
462 _apdu_case = 4
463
464# TS 102 221 Section 11.2.3 / TS 102 223
465class Fetch(ApduCommand, n='FETCH', ins=0x12, cla=['80']):
466 _apdu_case = 2
467
468# TS 102 221 Section 11.2.3 / TS 102 223
469class TerminalResponse(ApduCommand, n='TERMINAL RESPONSE', ins=0x14, cla=['80']):
470 _apdu_case = 3
471
472# TS 102 221 Section 11.3.1
473class RetrieveData(ApduCommand, n='RETRIEVE DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):
474 _apdu_case = 4
475
476 @staticmethod
477 def _tlv_decode_cmd(self : ApduCommand) -> Dict:
478 c = {}
479 if self.p2 & 0xc0 == 0x80:
480 c['mode'] = 'first_block'
481 sfi = self.p2 & 0x1f
482 if sfi == 0:
483 c['file'] = 'currently_selected_ef'
484 else:
485 c['file'] = 'sfi'
486 c['sfi'] = sfi
487 c['tag'] = i2h([self.cmd_data[0]])
488 elif self.p2 & 0xdf == 0x00:
489 c['mode'] = 'next_block'
490 elif self.p2 & 0xdf == 0x40:
491 c['mode'] = 'retransmit_previous_block'
492 else:
493 logger.warning('%s: invalid P2=%02x' % (self, self.p2))
494 return c
495
496 def _decode_cmd(self):
497 return RetrieveData._tlv_decode_cmd(self)
498
499 def _decode_rsp(self):
500 # TODO: parse tag/len/val?
501 return b2h(self.rsp_data)
502
503
504# TS 102 221 Section 11.3.2
505class SetData(ApduCommand, n='SET DATA', ins=0xDB, cla=['8X', 'CX', 'EX']):
506 _apdu_case = 3
507
508 def _decode_cmd(self):
509 c = RetrieveData._tlv_decode_cmd(self)
510 if c['mode'] == 'first_block':
511 if len(self.cmd_data) == 0:
512 c['delete'] = True
513 # TODO: parse tag/len/val?
514 c['data'] = b2h(self.cmd_data)
515 return c
516
517
518# TS 102 221 Section 12.1.1
519class GetResponse(ApduCommand, n='GET RESPONSE', ins=0xC0, cla=['0X', '4X', '6X']):
520 _apdu_case = 2
521
522ApduCommands = ApduCommandSet('TS 102 221', cmds=[UiccSelect, UiccStatus, ReadBinary, UpdateBinary, ReadRecord,
523 UpdateRecord, SearchRecord, Increase, VerifyPin, ChangePin, DisablePin,
524 EnablePin, UnblockPin, DeactivateFile, ActivateFile, Authenticate88,
525 Authenticate89, ManageChannel, GetChallenge, TerminalCapability,
526 ManageSecureChannel, TransactData, SuspendUicc, GetIdentity,
527 ExchangeCapabilities, TerminalProfile, Envelope, Fetch, TerminalResponse,
528 RetrieveData, SetData, GetResponse])