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