scp81: PSK TLS server and command scripting (phases B/C) + BIP fix (v2.1.5)
- scp81.py: PSK TLS listener (stdlib ssl PSK callbacks) speaking the GP HTTP administration dialog; configurable framing (chunked/Content-Length, TLS record split, Apache-style/compact headers, Connection header, keep-alive, Next-URI template with %d, TLS version/cipher, answer delay, keylog for capture decryption) - server.py: script responder + Response Scripting parsing (AF/AB, 80/23 TLVs), memory decoder, SCP81 start options, terminal-side timer management, background-mode BIP events, permissive OPEN CHANNEL - BIP fix: the RECEIVE DATA channel-data TLV length is BER long form (36 81 <len>) above 127 bytes; a raw length byte is mis-parsed on the card, so the large TLS records never reached its stack (a live card fetched the script response and silently never processed it - endless resume). The card now executes scripts and returns R-APDUs: memory (13 applets, 50646 B NV free, 2402 B volatile), ISD, stored HTTP OTA parameters, ELF and application registries - frontend: SCP81 tab (listener, script selection, HTTP OTA log), phone event forms, i18n; service worker v141 - docs: api.md, scp81-findings.md (attempt matrix + root cause analysis); tools/scp81_decrypt.py decrypts listener captures via the keylog - tests: 187 python + 337 frontend
This commit is contained in:
+618
-30
@@ -12,6 +12,7 @@ from io import StringIO
|
||||
from pySim.transport import ApduTracer, ProactiveHandler
|
||||
from pySim.cards import UiccCardBase
|
||||
from pysim_otaman_server import httpota
|
||||
from pysim_otaman_server import scp81
|
||||
from smartcard.CardMonitoring import CardMonitor, CardObserver
|
||||
|
||||
import gsm0338 # registers 'gsm03.38' codec
|
||||
@@ -20,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '2.1.2'
|
||||
VERSION = '2.1.5'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -709,14 +710,18 @@ class _DefaultProactiveHandler(ProactiveHandler):
|
||||
def receive_fetch_raw(self, pcmd, parsed):
|
||||
cmd_num, cmd_type, dev_src, dev_dst, cmd_qual = 1, 0, 0x83, 0x81, None
|
||||
entry = None
|
||||
# pySim parses the FETCH response into a command-specific object
|
||||
# ('parsed'); the 'pcmd' collection stays empty and is only useful as
|
||||
# a fallback. Use the parsed object for both the log and the response.
|
||||
cmd_obj = parsed if getattr(parsed, 'children', None) else pcmd
|
||||
try:
|
||||
raw = bytes.fromhex(parsed) if parsed else None
|
||||
raw = cmd_obj.to_tlv()
|
||||
if raw:
|
||||
cmd_num, cmd_type, dev_src, dev_dst, cmd_qual = _parse_proactive_header(raw)
|
||||
entry = _log_proactive(cmd_type, raw, cmd_qual, cmd_num)
|
||||
except Exception:
|
||||
pass
|
||||
ti_list = self.prepare_response(pcmd, 'performed_successfully')
|
||||
ti_list = self.prepare_response(cmd_obj, 'performed_successfully')
|
||||
if cmd_type == 0x26 and cmd_qual is not None:
|
||||
pli_hex = _PLI_DATA.get(cmd_qual, '')
|
||||
if pli_hex:
|
||||
@@ -740,6 +745,9 @@ class _RawBerTlv(BER_TLV_IE):
|
||||
def to_bytes(self, context={}):
|
||||
return self._raw
|
||||
|
||||
def to_tlv(self):
|
||||
return self._raw
|
||||
|
||||
|
||||
_STK_DECODE = GsmOrUcs2Adapter(GreedyBytes)
|
||||
|
||||
@@ -752,7 +760,7 @@ PROACTIVE_TYPE_NAMES = {
|
||||
0x13: 'SEND SHORT MESSAGE', 0x20: 'PLAY TONE',
|
||||
0x21: 'DISPLAY TEXT', 0x22: 'GET INKEY', 0x23: 'GET INPUT',
|
||||
0x24: 'SELECT ITEM', 0x25: 'SET UP MENU',
|
||||
0x26: 'PROVIDE LOCAL INFORMATION',
|
||||
0x26: 'PROVIDE LOCAL INFORMATION', 0x27: 'TIMER MANAGEMENT',
|
||||
0x15: 'LAUNCH BROWSER', 0x70: 'ACTIVATE',
|
||||
0x40: 'OPEN CHANNEL', 0x41: 'CLOSE CHANNEL',
|
||||
0x42: 'RECEIVE DATA', 0x43: 'SEND DATA', 0x44: 'GET CHANNEL STATUS',
|
||||
@@ -787,6 +795,7 @@ _PLI_DATA = {q: '' for q in PLI_QUALIFIER_NAMES}
|
||||
|
||||
_BIP = httpota.BipTerminal()
|
||||
_SCP81_LISTENER = None
|
||||
_SCP81_PSK = {}
|
||||
|
||||
_POLL_ENABLED = False
|
||||
_POLL_INTERVAL = 30
|
||||
@@ -959,6 +968,41 @@ def _dec_imei(hex8):
|
||||
return s[:15]
|
||||
|
||||
|
||||
def _cmd_tlv(tlvs, tag):
|
||||
"""Fetch a command TLV, tolerating both the plain tag and its
|
||||
comprehension-required variant (e.g. 0x24 and 0xA4, TS 101 220 7.1.1)."""
|
||||
return tlvs.get(tag) or tlvs.get(tag | 0x80) or b''
|
||||
|
||||
|
||||
def _tlv_map(data):
|
||||
"""Top-level COMPREHENSION-TLV map {tag: value} of a payload without a
|
||||
D0 wrapper (e.g. the command-specific TLVs of a TERMINAL RESPONSE)."""
|
||||
out = {}
|
||||
off = 0
|
||||
while off + 1 < len(data):
|
||||
tag, tlen = data[off], data[off + 1]
|
||||
out.setdefault(tag, data[off + 2: off + 2 + tlen])
|
||||
off += 2 + tlen
|
||||
return out
|
||||
|
||||
|
||||
def _bcd_swap(b):
|
||||
"""Semi-octet BCD digit pair (TS 123 040 TP-SCT): low nibble first."""
|
||||
return (b & 0x0F) * 10 + ((b >> 4) & 0x0F)
|
||||
|
||||
|
||||
def _hms_bcd(seconds):
|
||||
"""Encode seconds as hour/minute/second semi-octet BCD (TS 123 040)."""
|
||||
h, rem = divmod(int(seconds), 3600)
|
||||
m, s = divmod(rem, 60)
|
||||
return bytes([((h % 10) << 4) | (h // 10),
|
||||
((m % 10) << 4) | (m // 10),
|
||||
((s % 10) << 4) | (s // 10)])
|
||||
|
||||
|
||||
_TIMER_ACTIONS = {0x00: 'Start', 0x01: 'Deactivate', 0x02: 'Get current value'}
|
||||
|
||||
|
||||
def _decode_cmd(cmd_type, raw, qualifier):
|
||||
"""Decode a fetched proactive command into [{label, value}] pairs."""
|
||||
if not raw:
|
||||
@@ -1000,6 +1044,23 @@ def _decode_cmd(cmd_type, raw, qualifier):
|
||||
if cmd_type == 0x26 and qualifier is not None:
|
||||
name = PLI_QUALIFIER_NAMES.get(qualifier, 'Unknown')
|
||||
return [{'label': 'Qualifier', 'value': '%s (0x%02X)' % (name, qualifier)}]
|
||||
if cmd_type == 0x27:
|
||||
out = []
|
||||
if qualifier is not None:
|
||||
action = _TIMER_ACTIONS.get(qualifier & 0x03)
|
||||
out.append({'label': 'Action',
|
||||
'value': action or 'Reserved (0x%02X)' % qualifier})
|
||||
tlvs = httpota.proactive_tlvs(raw)
|
||||
timer = _cmd_tlv(tlvs, 0x24)
|
||||
if timer:
|
||||
tv = timer[0]
|
||||
out.append({'label': 'Timer',
|
||||
'value': str(tv) if 1 <= tv <= 8 else 'Invalid (0x%02X)' % tv})
|
||||
value = _cmd_tlv(tlvs, 0x25)
|
||||
if len(value) >= 3:
|
||||
out.append({'label': 'Value', 'value': '%02d:%02d:%02d' % (
|
||||
_bcd_swap(value[0]), _bcd_swap(value[1]), _bcd_swap(value[2]))})
|
||||
return out
|
||||
return [{'label': 'Data', 'value': raw.hex()}]
|
||||
|
||||
|
||||
@@ -1015,6 +1076,17 @@ def _decode_tr(type_hex, qual_hex, tr_hex):
|
||||
return [{'label': 'Data', 'value': h}]
|
||||
if cmd_type == 0x03 and len(h) >= 8 and h[0:2] == '84':
|
||||
return [{'label': 'Interval', 'value': '%d s' % int(h[6:8], 16)}]
|
||||
if cmd_type == 0x27:
|
||||
tlvs = _tlv_map(bytes.fromhex(h))
|
||||
out = []
|
||||
tid = _cmd_tlv(tlvs, 0x24)
|
||||
if tid:
|
||||
out.append({'label': 'Timer', 'value': str(tid[0])})
|
||||
val = _cmd_tlv(tlvs, 0x25)
|
||||
if len(val) >= 3:
|
||||
out.append({'label': 'Remaining', 'value': '%02d:%02d:%02d' % (
|
||||
_bcd_swap(val[0]), _bcd_swap(val[1]), _bcd_swap(val[2]))})
|
||||
return out or [{'label': 'Data', 'value': h}]
|
||||
if cmd_type == 0x26 and qual_hex:
|
||||
try:
|
||||
qual = int(qual_hex, 16)
|
||||
@@ -1088,28 +1160,31 @@ def _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, result=0x00, info=Non
|
||||
def _decode_bip_cmd(cmd_type, raw):
|
||||
tlvs = httpota.proactive_tlvs(raw)
|
||||
out = []
|
||||
dev = _cmd_tlv(tlvs, 0x02)
|
||||
if len(dev) >= 2 and 0x21 <= dev[1] <= 0x27:
|
||||
out.append({'label': 'Channel', 'value': str(dev[1] & 0x07)})
|
||||
if cmd_type == 0x40:
|
||||
bearer = tlvs.get(httpota.TAG_BEARER, b'')
|
||||
bearer = _cmd_tlv(tlvs, httpota.TAG_BEARER)
|
||||
if bearer:
|
||||
out.append({'label': 'Bearer', 'value': '0x%02X' % bearer[0]})
|
||||
bs = tlvs.get(httpota.TAG_BUFFER_SIZE, b'')
|
||||
bs = _cmd_tlv(tlvs, httpota.TAG_BUFFER_SIZE)
|
||||
if len(bs) >= 2:
|
||||
out.append({'label': 'Buffer size', 'value': str(int.from_bytes(bs[:2], 'big'))})
|
||||
naa = tlvs.get(httpota.TAG_NAA, b'')
|
||||
naa = _cmd_tlv(tlvs, httpota.TAG_NAA)
|
||||
if naa:
|
||||
out.append({'label': 'APN', 'value': naa[1:].decode('ascii', 'replace')})
|
||||
addr = httpota.parse_other_address(tlvs.get(httpota.TAG_OTHER_ADDRESS, b''))
|
||||
addr = httpota.parse_other_address(_cmd_tlv(tlvs, httpota.TAG_OTHER_ADDRESS))
|
||||
if addr:
|
||||
out.append({'label': 'Destination', 'value': addr})
|
||||
proto, port = httpota.parse_transport_level(tlvs.get(httpota.TAG_TRANSPORT_LEVEL, b''))
|
||||
proto, port = httpota.parse_transport_level(_cmd_tlv(tlvs, httpota.TAG_TRANSPORT_LEVEL))
|
||||
if port is not None:
|
||||
out.append({'label': 'Transport', 'value': '%s port %d' % ({0x02: 'TCP client'}.get(proto, 'proto 0x%02X' % (proto or 0)), port)})
|
||||
elif cmd_type == 0x42:
|
||||
req = tlvs.get(httpota.TAG_CHANNEL_DATA_LENGTH, b'')
|
||||
req = _cmd_tlv(tlvs, httpota.TAG_CHANNEL_DATA_LENGTH)
|
||||
if req:
|
||||
out.append({'label': 'Requested bytes', 'value': str(req[0])})
|
||||
elif cmd_type == 0x43:
|
||||
data = tlvs.get(httpota.TAG_CHANNEL_DATA, b'')
|
||||
data = _cmd_tlv(tlvs, httpota.TAG_CHANNEL_DATA)
|
||||
out.append({'label': 'Data bytes', 'value': str(len(data))})
|
||||
if data:
|
||||
out.append({'label': 'Data', 'value': data.hex()[:120]})
|
||||
@@ -1121,33 +1196,41 @@ def _handle_bip_command(scc, cmd_num, cmd_type, cmd_qual, raw, dev_src, dev_dst)
|
||||
tlvs = httpota.proactive_tlvs(raw)
|
||||
channel = _bip_channel_id(dev_dst)
|
||||
if cmd_type == 0x40:
|
||||
bs = tlvs.get(httpota.TAG_BUFFER_SIZE, b'\x02\x00')
|
||||
bs = _cmd_tlv(tlvs, httpota.TAG_BUFFER_SIZE) or b'\x02\x00'
|
||||
buffer_size = int.from_bytes(bs[:2], 'big') if len(bs) >= 2 else 0x0200
|
||||
bearer = tlvs.get(httpota.TAG_BEARER, b'\x03')
|
||||
bearer = _cmd_tlv(tlvs, httpota.TAG_BEARER) or b'\x03'
|
||||
extra = bytes([httpota.TAG_BEARER, len(bearer)]) + bearer
|
||||
extra += bytes([httpota.TAG_BUFFER_SIZE, 0x02]) + buffer_size.to_bytes(2, 'big')
|
||||
if not _BIP.enabled:
|
||||
_BIP.log('open-unavailable', reason='BIP not enabled')
|
||||
return _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, 0x3A, 0x00, extra)
|
||||
addr = httpota.parse_other_address(tlvs.get(httpota.TAG_OTHER_ADDRESS, b''))
|
||||
proto, port = httpota.parse_transport_level(tlvs.get(httpota.TAG_TRANSPORT_LEVEL, b''))
|
||||
addr = httpota.parse_other_address(_cmd_tlv(tlvs, httpota.TAG_OTHER_ADDRESS))
|
||||
proto, port = httpota.parse_transport_level(_cmd_tlv(tlvs, httpota.TAG_TRANSPORT_LEVEL))
|
||||
if not addr or port is None:
|
||||
_BIP.log('open-unavailable', reason='missing destination/transport', address=addr, port=port)
|
||||
return _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, 0x3A, 0x00, extra)
|
||||
cid, err = _BIP.open(addr, port, buffer_size)
|
||||
# Emulation is deliberately permissive: the APN, destination and
|
||||
# transport are informational, the channel always goes to the
|
||||
# configured local target (the live card emits truncated/empty
|
||||
# destination TLVs - see the AGENTS.md HTTP OTA notes).
|
||||
_BIP.log('open-relaxed', address=addr, port=port,
|
||||
note='destination/transport not fully specified')
|
||||
cid, err = _BIP.open(addr or '-', port or 0, buffer_size)
|
||||
if cid is None:
|
||||
return _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, 0x3A, 0x00, extra)
|
||||
if cmd_qual and (cmd_qual & 0x04):
|
||||
# Background mode: the terminal shall inform the UICC that the
|
||||
# link was established (TS 102 223 7.5.11).
|
||||
_BIP._queue_link_status(cid, status=0x80 | (cid & 0x07), info=0x00)
|
||||
status = bytes([httpota.TAG_CHANNEL_STATUS, 0x02, 0x80 | (cid & 0x07), 0x00])
|
||||
return _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, 0x00, None, status + extra)
|
||||
if cmd_type == 0x43:
|
||||
data = tlvs.get(httpota.TAG_CHANNEL_DATA, b'')
|
||||
data = _cmd_tlv(tlvs, httpota.TAG_CHANNEL_DATA)
|
||||
if channel is None or not _BIP.send(channel, data):
|
||||
return _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, 0x3A, 0x00)
|
||||
length = _BIP.send_capacity(channel)
|
||||
return _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, 0x00, None,
|
||||
bytes([httpota.TAG_CHANNEL_DATA_LENGTH, 0x01, length & 0xFF]))
|
||||
if cmd_type == 0x42:
|
||||
req = tlvs.get(httpota.TAG_CHANNEL_DATA_LENGTH, b'\x00')
|
||||
req = _cmd_tlv(tlvs, httpota.TAG_CHANNEL_DATA_LENGTH) or b'\x00'
|
||||
n = req[0] if req else 0
|
||||
if channel is None:
|
||||
return _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, 0x3A, 0x00)
|
||||
@@ -1155,7 +1238,17 @@ def _handle_bip_command(scc, cmd_num, cmd_type, cmd_qual, raw, dev_src, dev_dst)
|
||||
if data is None:
|
||||
return _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, 0x3A, 0x00)
|
||||
remaining = _BIP.available(channel)
|
||||
extra = bytes([httpota.TAG_CHANNEL_DATA, len(data)]) + data if data else b''
|
||||
# The channel data TLV length is BER-encoded: a single byte only up
|
||||
# to 127, then the 0x81 long form (the reference terminal traces use
|
||||
# `36 81 ed` for a 237-byte chunk). With a raw length byte >0x7F the
|
||||
# card reads a malformed TLV and the record bytes never reach its
|
||||
# TLS layer (it fetches, accepts, and never processes the response).
|
||||
extra = b''
|
||||
if data:
|
||||
if len(data) <= 0x7F:
|
||||
extra = bytes([httpota.TAG_CHANNEL_DATA, len(data)]) + data
|
||||
else:
|
||||
extra = bytes([httpota.TAG_CHANNEL_DATA, 0x81, len(data)]) + data
|
||||
extra += bytes([httpota.TAG_CHANNEL_DATA_LENGTH, 0x01, 0xFF if remaining > 0xFF else remaining])
|
||||
return _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, 0x00, None, extra)
|
||||
if cmd_type == 0x41:
|
||||
@@ -1173,11 +1266,258 @@ def _handle_bip_command(scc, cmd_num, cmd_type, cmd_qual, raw, dev_src, dev_dst)
|
||||
def _scp81_listener_status():
|
||||
if not _SCP81_LISTENER:
|
||||
return None
|
||||
if isinstance(_SCP81_LISTENER, scp81.PskTlsServer):
|
||||
return {'mode': 'tls', 'host': _SCP81_LISTENER.host, 'port': _SCP81_LISTENER.port,
|
||||
'psk_identity': _SCP81_LISTENER.identity,
|
||||
'identity_seen': _SCP81_LISTENER.identity_seen,
|
||||
'chunked': _SCP81_LISTENER.chunked,
|
||||
'chunk_size': _SCP81_LISTENER.chunk_size,
|
||||
'keep_alive': _SCP81_LISTENER.keep_alive,
|
||||
'compact_headers': _SCP81_LISTENER.compact_headers,
|
||||
'tls_version': _SCP81_LISTENER.tls_version,
|
||||
'cipher': _SCP81_LISTENER.cipher}
|
||||
return {'mode': 'dump', 'host': _SCP81_LISTENER.host, 'port': _SCP81_LISTENER.port}
|
||||
|
||||
|
||||
def _scp81_wait_drained(peer):
|
||||
"""Wait until the BIP channel for this TLS connection has delivered its
|
||||
buffered bytes to the card (matched by the terminal's ephemeral port), so
|
||||
a connection close does not truncate the response fetch."""
|
||||
if not peer or len(peer) < 2:
|
||||
return
|
||||
port = peer[1]
|
||||
deadline = time.time() + 5.0
|
||||
seen_data = False
|
||||
while time.time() < deadline:
|
||||
ch = None
|
||||
for c in list(_BIP.channels.values()):
|
||||
try:
|
||||
if c.sock.getsockname()[1] == port:
|
||||
ch = c
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
if ch is None:
|
||||
return
|
||||
if ch.rx:
|
||||
# Channel pump has picked up the response; wait for the card.
|
||||
seen_data = True
|
||||
elif seen_data:
|
||||
return
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def _bip_data_available(ch):
|
||||
"""Monitor-thread callback: tell the card there is server data to fetch.
|
||||
|
||||
TS 102 223 7.5.10 - ENVELOPE (Event Download - Data available) carries the
|
||||
Channel status and the number of bytes waiting; the card then issues
|
||||
RECEIVE DATA. Returns True when the event was sent (the caller then marks
|
||||
the bytes as notified)."""
|
||||
server = _server_ref
|
||||
if not server or not _CARD_CONNECTED or ch.peer_closed:
|
||||
return False
|
||||
ev_list = getattr(server, 'event_list', None) or []
|
||||
if 0x09 not in ev_list or not ch.rx:
|
||||
return False
|
||||
with _CARD_LOCK:
|
||||
server = _server_ref
|
||||
if not server or not _CARD_CONNECTED or not getattr(server, 'scc', None):
|
||||
return False
|
||||
if _PROACTIVE_BUSY or getattr(server, 'stk_pending', None):
|
||||
return False
|
||||
length = min(len(ch.rx), 0xFF)
|
||||
tlv = bytes([0xB8, 0x02, 0x80 | (ch.id & 0x07), 0x00,
|
||||
0xB7, 0x01, length])
|
||||
try:
|
||||
_send_event_download(server.scc, 0x09, tlv)
|
||||
except Exception as e:
|
||||
_BIP.log('data-available-skip', channel=ch.id, reason=str(e))
|
||||
return False
|
||||
_BIP.log('data-available', channel=ch.id, length=length)
|
||||
return True
|
||||
|
||||
|
||||
# ---- SCP81 command scripting (GP RAM over HTTP, TS 102 226 5.2) -----------
|
||||
# The administration server answers the card's POST with one C-APDU per
|
||||
# request (Command Scripting template 'AE 80 22 <len> <apdu> 00 00',
|
||||
# indefinite length as recommended for RAM over HTTPS) and reads the R-APDU
|
||||
# from the next POST's Response Scripting template ('AB'/'AF', with '80'
|
||||
# executed-count and '23' R-APDU TLVs whose last two bytes are SW1 SW2).
|
||||
|
||||
_SCP81_SCRIPTS = {
|
||||
# The command sequence of the reference administration server
|
||||
# (samples/HTTP_OTA/httpota_adminserver_php_v2, get_next_apdu), extended
|
||||
# with the registries: GET DATA FF21 (extended card resources / free
|
||||
# memory), GET STATUS P1=80 (Issuer Security Domain), GET DATA 0085,
|
||||
# GET STATUS P1=40 (executable load files / ELF), GET STATUS P1=10
|
||||
# (applications/applets); P2=02 with data '4F00' selects the TLV format,
|
||||
# Le=00 so no GET RESPONSE is needed.
|
||||
'explore': ['80CAFF2100', '80F28002024F0000', '80CA008500',
|
||||
'80F24002024F0000', '80F21002024F0000'],
|
||||
'none': [],
|
||||
}
|
||||
_SCP81_SCRIPT = list(_SCP81_SCRIPTS['explore'])
|
||||
_SCP81_SCRIPT_SENT = 0
|
||||
_SCP81_SCRIPT_RESULTS = []
|
||||
_SCP81_SCRIPT_TEMPLATE = 'indefinite'
|
||||
_SCP81_SCRIPT_CR_TAG = False
|
||||
# None = short per-command Next-URI ('/N'); '' = omit the header (spec: the
|
||||
# card executes the script, sends no response string and closes the session).
|
||||
_SCP81_NEXT_URI = None
|
||||
# Optional X-Admin-Targeted-Application header (spec syntax //aid/<RID>/<PIX>).
|
||||
# When it names an application that does not exist on the card, the SD answers
|
||||
# with X-Admin-Script-Status: unknown-application instead of executing.
|
||||
_SCP81_TARGETED_APP = None
|
||||
# Emit Apache-style responses (Date/Server/X-Powered-By, Content-Length before
|
||||
# Content-Type) exactly like the reference admin servers.
|
||||
_SCP81_APACHE_HEADERS = False
|
||||
# The listener's chunked flag (mirrored here for the response headers: a
|
||||
# chunked response must not carry Content-Length - invalid HTTP, and the
|
||||
# reference sends Transfer-Encoding before Content-Type).
|
||||
_SCP81_CHUNKED = False
|
||||
# Send automatic Channel status (link dropped) events to the card. Suppress
|
||||
# while testing flows where the terminal closes the connection on purpose:
|
||||
# the card must drain the buffered response and resume on a new connection.
|
||||
_SCP81_LINK_EVENTS = True
|
||||
|
||||
|
||||
def _scp81_command_body(apdu_hex, definite=False, cr_tag=False):
|
||||
"""Command Scripting template with one C-APDU TLV: the indefinite-length
|
||||
variant ('AE 80 22 <len> <apdu> 00 00', recommended for RAM over HTTPS) or
|
||||
the definite-length one ('AA <len> 22 <len> <apdu>'). The C-APDU TLV tag
|
||||
is '22' per TS 101 220 (CR flag 0); some cards expect the CR-set 'A2'
|
||||
instead, so it is configurable."""
|
||||
apdu = bytes.fromhex(re.sub(r'\s', '', apdu_hex))
|
||||
cmd_tlv = bytes([0xA2 if cr_tag else 0x22, len(apdu)]) + apdu
|
||||
if definite:
|
||||
return bytes([0xAA, len(cmd_tlv)]) + cmd_tlv
|
||||
return bytes([0xAE, 0x80]) + cmd_tlv + b'\x00\x00'
|
||||
|
||||
|
||||
def _scp81_parse_response(body):
|
||||
"""Parse a Response Scripting template (TS 102 226 5.2.2, definite 'AB'
|
||||
or indefinite 'AF'); returns (executed_count, [(rapdu, sw_hex), ...])."""
|
||||
if not body:
|
||||
return 0, []
|
||||
if body[0] == 0xAF and len(body) >= 2 and body[1] == 0x80:
|
||||
content = body[2:-2] if body.endswith(b'\x00\x00') else body[2:]
|
||||
elif body[0] == 0xAB:
|
||||
ln, off = httpota.ber_len_read(body, 1)
|
||||
content = body[off:off + ln]
|
||||
else:
|
||||
content = body
|
||||
count, out = 0, []
|
||||
off = 0
|
||||
while off + 1 < len(content):
|
||||
tag, tlen = content[off], content[off + 1]
|
||||
val = content[off + 2:off + 2 + tlen]
|
||||
off += 2 + tlen
|
||||
if tag == 0x80:
|
||||
count = int.from_bytes(val, 'big') if val else 0
|
||||
elif tag == 0x23 and len(val) >= 2:
|
||||
out.append((val[:-2], val[-2:].hex().upper()))
|
||||
return count, out
|
||||
|
||||
|
||||
def _scp81_decode_memory(rapdu):
|
||||
"""GET DATA FF21 value: 81 applet count, 82 free NV (3 B), 83 free volatile."""
|
||||
if len(rapdu) < 5 or rapdu[0] != 0xFF or rapdu[1] != 0x21:
|
||||
return None
|
||||
content = rapdu[3:3 + rapdu[2]]
|
||||
out = {}
|
||||
off = 0
|
||||
while off + 1 < len(content):
|
||||
tag, tlen = content[off], content[off + 1]
|
||||
val = content[off + 2:off + 2 + tlen]
|
||||
off += 2 + tlen
|
||||
if tag == 0x81:
|
||||
out['applets'] = int.from_bytes(val, 'big')
|
||||
elif tag == 0x82:
|
||||
out['free_nv'] = int.from_bytes(val, 'big')
|
||||
elif tag == 0x83:
|
||||
out['free_volatile'] = int.from_bytes(val, 'big')
|
||||
return out or None
|
||||
|
||||
|
||||
def _scp81_script_responder(method, target, headers, body):
|
||||
"""Remote Administration Server side of the administration session: send
|
||||
the next scripted C-APDU or close the session (TS 102 226 / GP 4.4.2)."""
|
||||
global _SCP81_SCRIPT_SENT, _SCP81_SCRIPT_RESULTS
|
||||
status = headers.get('x-admin-script-status')
|
||||
if status is not None:
|
||||
index = _SCP81_SCRIPT_SENT
|
||||
if status != 'ok':
|
||||
_BIP.log('script-status', index=index, status=status)
|
||||
else:
|
||||
count, rapdus = _scp81_parse_response(body)
|
||||
for rapdu, sw in rapdus:
|
||||
_BIP.log('script-rapdu', index=index, sw=sw, bytes=len(rapdu),
|
||||
hex=rapdu.hex().upper()[:2000])
|
||||
_SCP81_SCRIPT_RESULTS.append({'index': index, 'sw': sw,
|
||||
'rapdu': rapdu.hex().upper()})
|
||||
if rapdus and _SCP81_SCRIPT and index >= 1:
|
||||
apdu = _SCP81_SCRIPT[index - 1].upper()
|
||||
if apdu.startswith('80CAFF21'):
|
||||
decoded = _scp81_decode_memory(rapdus[-1][0])
|
||||
if decoded:
|
||||
_BIP.log('script-memory', **decoded)
|
||||
else:
|
||||
# First (or resumed) POST of a session: run the script from the start.
|
||||
_SCP81_SCRIPT_SENT = 0
|
||||
_SCP81_SCRIPT_RESULTS = []
|
||||
if _SCP81_SCRIPT_SENT < len(_SCP81_SCRIPT):
|
||||
apdu = _SCP81_SCRIPT[_SCP81_SCRIPT_SENT]
|
||||
_SCP81_SCRIPT_SENT += 1
|
||||
_BIP.log('script-send', index=_SCP81_SCRIPT_SENT, apdu=apdu)
|
||||
headers = _scp81_response_headers()
|
||||
if _SCP81_TARGETED_APP:
|
||||
headers['X-Admin-Targeted-Application'] = _SCP81_TARGETED_APP
|
||||
# The working reference session (samples/HTTPOTA_session_3311_success1)
|
||||
# answers with a relative URI plus a QUERY (/Download?req=N): a
|
||||
# query-less Next-URI makes the card abort the TLS session. A '%d'
|
||||
# in the configured/default URI is replaced with the command number.
|
||||
template = _SCP81_NEXT_URI if _SCP81_NEXT_URI is not None else '/api/scp81?req=%d'
|
||||
next_uri = template % _SCP81_SCRIPT_SENT if '%d' in template else template
|
||||
if next_uri:
|
||||
headers['X-Admin-Next-URI'] = next_uri
|
||||
body_out = _scp81_command_body(
|
||||
apdu, definite=(_SCP81_SCRIPT_TEMPLATE == 'definite'),
|
||||
cr_tag=_SCP81_SCRIPT_CR_TAG)
|
||||
if _SCP81_APACHE_HEADERS:
|
||||
if _SCP81_CHUNKED:
|
||||
headers['Transfer-Encoding'] = 'chunked'
|
||||
else:
|
||||
headers['Content-Length'] = str(len(body_out))
|
||||
headers['Content-Type'] = scp81.GP_CT_COMMAND
|
||||
return 200, headers, body_out
|
||||
_BIP.log('script-done', sent=_SCP81_SCRIPT_SENT,
|
||||
results=len(_SCP81_SCRIPT_RESULTS))
|
||||
headers = _scp81_response_headers()
|
||||
if _SCP81_APACHE_HEADERS:
|
||||
headers['Content-Type'] = 'text/html; charset=UTF-8'
|
||||
return 204, headers, b''
|
||||
|
||||
|
||||
def _scp81_response_headers():
|
||||
"""Base response headers, in the reference servers' order (Apache adds
|
||||
Date/Server/X-Powered-By before the admin headers)."""
|
||||
headers = {}
|
||||
if _SCP81_APACHE_HEADERS:
|
||||
import email.utils
|
||||
headers['Date'] = email.utils.formatdate(usegmt=True)
|
||||
headers['Server'] = 'Apache'
|
||||
headers['X-Powered-By'] = 'PHP/7.0.33'
|
||||
headers['X-Admin-Protocol'] = scp81.GP_PROTOCOL
|
||||
return headers
|
||||
|
||||
|
||||
def _scp81_bip_control(body):
|
||||
global _SCP81_LISTENER
|
||||
global _SCP81_LISTENER, _SCP81_PSK
|
||||
global _SCP81_SCRIPT, _SCP81_SCRIPT_SENT, _SCP81_SCRIPT_RESULTS
|
||||
global _SCP81_SCRIPT_TEMPLATE, _SCP81_SCRIPT_CR_TAG, _SCP81_NEXT_URI
|
||||
global _SCP81_LINK_EVENTS, _SCP81_TARGETED_APP, _SCP81_APACHE_HEADERS
|
||||
global _SCP81_CHUNKED
|
||||
body = body or {}
|
||||
action = body.get('action', 'start')
|
||||
if action == 'stop':
|
||||
@@ -1187,14 +1527,81 @@ def _scp81_bip_control(body):
|
||||
_BIP.disable()
|
||||
return {'ok': True, 'bip': _BIP.status(), 'listener': None}
|
||||
host = body.get('host') or '127.0.0.1'
|
||||
port = int(body.get('port') or 8443)
|
||||
port = body.get('port')
|
||||
port = int(port) if port not in (None, '') else 8443
|
||||
mode = body.get('mode', 'dump')
|
||||
if _SCP81_LISTENER:
|
||||
_SCP81_LISTENER.stop()
|
||||
_SCP81_LISTENER = None
|
||||
_BIP.disable()
|
||||
if mode == 'tls':
|
||||
psk_hex = body.get('psk_hex') or _SCP81_PSK.get('psk_hex')
|
||||
if not psk_hex:
|
||||
return {'ok': False, 'error': 'psk_hex is required for tls mode'}
|
||||
try:
|
||||
psk = bytes.fromhex(re.sub(r'\s', '', psk_hex))
|
||||
except ValueError:
|
||||
return {'ok': False, 'error': 'psk_hex is not valid hex'}
|
||||
if not psk:
|
||||
return {'ok': False, 'error': 'psk_hex is empty'}
|
||||
identity = body.get('psk_identity')
|
||||
if identity is not None:
|
||||
identity = identity.strip() or None # empty clears the pin
|
||||
else:
|
||||
identity = _SCP81_PSK.get('psk_identity')
|
||||
_SCP81_PSK = {'psk_hex': psk_hex, 'psk_identity': identity}
|
||||
script = body.get('script', 'explore')
|
||||
if isinstance(script, list):
|
||||
_SCP81_SCRIPT = [re.sub(r'\s', '', s) for s in script if s]
|
||||
elif script in _SCP81_SCRIPTS:
|
||||
_SCP81_SCRIPT = list(_SCP81_SCRIPTS[script])
|
||||
else:
|
||||
return {'ok': False, 'error': 'unknown script preset: %s' % script}
|
||||
_SCP81_SCRIPT_SENT = 0
|
||||
_SCP81_SCRIPT_RESULTS = []
|
||||
template = body.get('script_template', 'indefinite')
|
||||
if template not in ('indefinite', 'definite'):
|
||||
return {'ok': False, 'error': 'script_template must be indefinite or definite'}
|
||||
_SCP81_SCRIPT_TEMPLATE = template
|
||||
_SCP81_SCRIPT_CR_TAG = bool(body.get('cr_tag', False))
|
||||
if 'next_uri' in body:
|
||||
_SCP81_NEXT_URI = body.get('next_uri') or ''
|
||||
_SCP81_LINK_EVENTS = bool(body.get('link_events', True))
|
||||
_SCP81_TARGETED_APP = (body.get('targeted_app') or None)
|
||||
# Defaults reproduce the working reference session (decrypted from
|
||||
# samples/HTTP_OTA: RAM/HTTPOTA_test5.pcap): one keep-alive connection,
|
||||
# Apache-style response headers, a chunked body whose script sits in
|
||||
# one TLS record, no Connection header, and an X-Admin-Next-URI with a
|
||||
# query whose command id increments. Overrides remain available.
|
||||
_SCP81_APACHE_HEADERS = bool(body.get('apache_headers', True))
|
||||
_SCP81_CHUNKED = bool(body.get('chunked', True))
|
||||
cs = body.get('chunk_size')
|
||||
chunk_size = int(cs) if cs not in (None, '') else 0
|
||||
_SCP81_LISTENER = scp81.PskTlsServer(
|
||||
host, port, psk, identity=identity,
|
||||
responder=_scp81_script_responder,
|
||||
chunked=bool(body.get('chunked', True)),
|
||||
chunk_size=chunk_size,
|
||||
keep_alive=bool(body.get('keep_alive', True)),
|
||||
compact_headers=bool(body.get('compact_headers', False)),
|
||||
tls_version=str(body.get('tls_version') or '1.2'),
|
||||
cipher=(body.get('cipher') or None),
|
||||
on_before_close=_scp81_wait_drained,
|
||||
keylog=(body.get('keylog') or None),
|
||||
conn_header=(body.get('conn_header') or 'none'),
|
||||
answer_delay=(body.get('answer_delay') or 0),
|
||||
on_log=lambda kind, **fields: _BIP.log(kind, **fields))
|
||||
_BIP.on_data = _bip_data_available
|
||||
_BIP.enable(host, _SCP81_LISTENER.port)
|
||||
return {'ok': True, 'bip': _BIP.status(), 'listener': _scp81_listener_status(),
|
||||
'script': _SCP81_SCRIPT, 'script_template': _SCP81_SCRIPT_TEMPLATE,
|
||||
'cr_tag': _SCP81_SCRIPT_CR_TAG, 'link_events': _SCP81_LINK_EVENTS,
|
||||
'targeted_app': _SCP81_TARGETED_APP,
|
||||
'apache_headers': _SCP81_APACHE_HEADERS,
|
||||
'chunked': _SCP81_CHUNKED}
|
||||
if mode != 'dump':
|
||||
return {'ok': False, 'error': 'unsupported mode: %s' % mode}
|
||||
_BIP.on_data = _bip_data_available
|
||||
_SCP81_LISTENER = httpota.TcpDumpServer(
|
||||
host, port,
|
||||
on_rx=lambda peer, data: _BIP.log('dump-rx', peer=peer, bytes=len(data), hex=data.hex().upper()[:2000]),
|
||||
@@ -1232,9 +1639,9 @@ _RESULT_NAMES_BASIC = {
|
||||
0x21: 'Backward move in the proactive SIM session requested by the user',
|
||||
0x22: 'No response from user',
|
||||
0x23: 'Help information required by the user',
|
||||
0x24: 'USSD or SS transaction terminated by the user',
|
||||
0x25: 'Proactive SIM session terminated by the user',
|
||||
0x26: 'Backward move in the proactive SIM session requested by the user',
|
||||
0x24: 'Action in contradiction with the current timer state',
|
||||
0x25: 'Interaction with call control by NAA, temporary problem',
|
||||
0x26: 'Launch browser generic error',
|
||||
}
|
||||
|
||||
_RESULT_NAMES_GENERAL = {
|
||||
@@ -1309,6 +1716,7 @@ def _handle_card_disconnect():
|
||||
global _CARD_CONNECTED
|
||||
_poll_disable()
|
||||
_cancel_menu_timeout()
|
||||
_timer_cancel()
|
||||
_CARD_CONNECTED = False
|
||||
if _server_ref:
|
||||
_server_ref.card = None
|
||||
@@ -1329,6 +1737,7 @@ def _apply_equipped_card(server):
|
||||
server.stk_pending = None
|
||||
server.menu_active = False
|
||||
_cancel_menu_timeout()
|
||||
_timer_cancel()
|
||||
server.event_list = None
|
||||
_reset_proactive_log()
|
||||
server.card = server.app.card
|
||||
@@ -1502,6 +1911,39 @@ def _send_event_download(scc, event_type, event_data=None):
|
||||
return data, sw
|
||||
|
||||
|
||||
_FLUSHING_CHANNEL_EVENTS = False
|
||||
# True while a FETCH/TERMINAL RESPONSE chain is running: terminal-initiated
|
||||
# ENVELOPEs must never interleave with it.
|
||||
_PROACTIVE_BUSY = False
|
||||
|
||||
|
||||
def _bip_flush_channel_events(scc):
|
||||
"""Inform the UICC about BIP link changes detected outside its proactive
|
||||
commands (TS 102 223 7.5.11), if the card subscribed to Channel status.
|
||||
Called when the proactive session is idle, never between FETCH and TR."""
|
||||
global _FLUSHING_CHANNEL_EVENTS
|
||||
if _FLUSHING_CHANNEL_EVENTS:
|
||||
return
|
||||
if not _SCP81_LINK_EVENTS:
|
||||
_BIP.take_pending_events()
|
||||
return
|
||||
ev_list = getattr(_server_ref, 'event_list', None) or []
|
||||
if 0x0A not in ev_list:
|
||||
return
|
||||
events = _BIP.take_pending_events()
|
||||
if not events:
|
||||
return
|
||||
_FLUSHING_CHANNEL_EVENTS = True
|
||||
try:
|
||||
for ev in events:
|
||||
_send_event_download(scc, 0x0A, bytes([
|
||||
httpota.TAG_CHANNEL_STATUS | 0x80, 0x02, ev['status'], ev['info']]))
|
||||
except Exception as e:
|
||||
sys.stderr.write('Channel status event error: %s\n' % e)
|
||||
finally:
|
||||
_FLUSHING_CHANNEL_EVENTS = False
|
||||
|
||||
|
||||
def _skip_ber_len(raw, off):
|
||||
if off >= len(raw):
|
||||
return off
|
||||
@@ -1512,6 +1954,121 @@ def _skip_ber_len(raw, off):
|
||||
return off + 3
|
||||
|
||||
|
||||
# ---- TIMER MANAGEMENT (TS 102 223 6.6.21, 6.8.13/14, 7.4) -----------------
|
||||
# The terminal keeps up to 8 timers per card session. On expiry it must send
|
||||
# ENVELOPE (TIMER EXPIRATION, tag D7) so the card can act (a common OTA retry
|
||||
# mechanism); a reset or card removal deactivates all timers.
|
||||
|
||||
_TIMERS = {}
|
||||
_TIMER_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _timer_cancel(timer_id=None):
|
||||
"""Cancel one timer, or all of them (reset / card removal)."""
|
||||
with _TIMER_LOCK:
|
||||
ids = list(_TIMERS) if timer_id is None else [timer_id]
|
||||
for tid in ids:
|
||||
entry = _TIMERS.pop(tid, None)
|
||||
if entry:
|
||||
entry['timer'].cancel()
|
||||
|
||||
|
||||
def _timer_remaining(timer_id):
|
||||
with _TIMER_LOCK:
|
||||
entry = _TIMERS.get(timer_id)
|
||||
if not entry:
|
||||
return None
|
||||
return max(0, int(round(entry['deadline'] - time.time())))
|
||||
|
||||
|
||||
def _timer_start(timer_id, seconds):
|
||||
"""Start (or restart) a timer; returns False for an invalid identifier."""
|
||||
if not 1 <= timer_id <= 8:
|
||||
return False
|
||||
_timer_cancel(timer_id)
|
||||
timer = threading.Timer(seconds, _timer_fire, args=(timer_id, seconds))
|
||||
timer.daemon = True
|
||||
timer.start()
|
||||
with _TIMER_LOCK:
|
||||
_TIMERS[timer_id] = {'timer': timer, 'deadline': time.time() + seconds}
|
||||
return True
|
||||
|
||||
|
||||
def _timer_fire(timer_id, elapsed):
|
||||
"""Timer callback: the timer is consumed on expiry (7.4.1); a timer that
|
||||
was cancelled or restarted in the meantime must not report."""
|
||||
with _TIMER_LOCK:
|
||||
entry = _TIMERS.pop(timer_id, None)
|
||||
if entry is None:
|
||||
return
|
||||
_timer_expired(timer_id, elapsed)
|
||||
|
||||
|
||||
def _timer_expired(timer_id, elapsed):
|
||||
"""Pass an expired timer to the UICC with ENVELOPE (TIMER EXPIRATION)."""
|
||||
server = _server_ref
|
||||
if not _CARD_CONNECTED or not server:
|
||||
return
|
||||
# Never inject the ENVELOPE while a fetched command awaits its TERMINAL
|
||||
# RESPONSE (a paused STK menu); wait outside the card lock, then retry.
|
||||
for _ in range(10):
|
||||
if not getattr(server, 'stk_pending', None):
|
||||
break
|
||||
time.sleep(2)
|
||||
with _CARD_LOCK:
|
||||
if server is not _server_ref or not getattr(server, 'scc', None):
|
||||
return
|
||||
if getattr(server, 'stk_pending', None):
|
||||
_timer_start(timer_id, 5)
|
||||
return
|
||||
scc = server.scc
|
||||
inner = bytes([0x82, 0x02, 0x82, 0x81, 0xA4, 0x01, timer_id & 0xFF,
|
||||
0xA5, 0x03]) + _hms_bcd(elapsed)
|
||||
tlv = bytes([0xD7, len(inner)]) + inner
|
||||
apdu = '%sc20000%02x%s' % (scc.cat_cla, len(tlv), tlv.hex())
|
||||
for _ in range(3):
|
||||
try:
|
||||
data, sw = scc._tp.send_apdu(apdu)
|
||||
except Exception as e:
|
||||
sys.stderr.write('TIMER-EXPIRATION send error: %s\n' % e)
|
||||
_handle_card_disconnect()
|
||||
return
|
||||
sys.stderr.write('ENVELOPE(Timer Expiration): timer=%d elapsed=%ds -> %s\n'
|
||||
% (timer_id, elapsed, sw))
|
||||
if sw == '9300':
|
||||
# UICC busy: the terminal shall retry until accepted (7.4.1).
|
||||
time.sleep(1)
|
||||
continue
|
||||
if sw.startswith('91'):
|
||||
_handle_proactive_chain(scc, sw)
|
||||
break
|
||||
|
||||
|
||||
def _handle_timer_command(cmd_num, cmd_type, cmd_qual, raw, dev_src, dev_dst):
|
||||
"""Terminal side of TIMER MANAGEMENT. Returns the TERMINAL RESPONSE payload."""
|
||||
tlvs = httpota.proactive_tlvs(raw)
|
||||
tid = _cmd_tlv(tlvs, 0x24)
|
||||
timer_id = tid[0] if tid else 1
|
||||
action = (cmd_qual or 0) & 0x03
|
||||
base = bytes([0x81, 0x03, cmd_num, cmd_type, (cmd_qual or 0) & 0xFF,
|
||||
0x82, 0x02, dev_dst, dev_src])
|
||||
if action == 0x00:
|
||||
value = _cmd_tlv(tlvs, 0x25)
|
||||
if len(value) >= 3 and 1 <= timer_id <= 8:
|
||||
secs = (_bcd_swap(value[0]) * 3600 + _bcd_swap(value[1]) * 60
|
||||
+ _bcd_swap(value[2]))
|
||||
if secs > 0:
|
||||
_timer_start(timer_id, secs)
|
||||
return base + bytes([0x03, 0x01, 0x00])
|
||||
remaining = _timer_remaining(timer_id)
|
||||
if remaining is None:
|
||||
return base + bytes([0x03, 0x01, 0x24])
|
||||
if action == 0x01:
|
||||
_timer_cancel(timer_id)
|
||||
return (base + bytes([0xA4, 0x01, timer_id & 0xFF, 0xA5, 0x03])
|
||||
+ _hms_bcd(remaining) + bytes([0x03, 0x01, 0x00]))
|
||||
|
||||
|
||||
def _decode_stk_text(raw):
|
||||
try:
|
||||
return _STK_DECODE._decode(raw, {}, 'stk')
|
||||
@@ -1543,9 +2100,12 @@ def _parse_proactive_header(raw):
|
||||
while off < len(raw) - 1:
|
||||
tag, tlen = raw[off], raw[off + 1]
|
||||
val = raw[off + 2: off + 2 + tlen]; off += 2 + tlen
|
||||
if tag == 0x81 and tlen >= 3:
|
||||
# Cards use both the plain (01/02) and comprehension-required
|
||||
# (81/82) tag variants - TS 101 220 7.1.1 leaves the CR flag to
|
||||
# the application, and the reference cards switch between them.
|
||||
if tag in (0x01, 0x81) and tlen >= 3:
|
||||
cmd_num, cmd_type, cmd_qual = val[0], val[1], val[2]
|
||||
elif tag == 0x82 and tlen >= 2:
|
||||
elif tag in (0x02, 0x82) and tlen >= 2:
|
||||
dev_src, dev_dst = val[0], val[1]
|
||||
return cmd_num, cmd_type, dev_src, dev_dst, cmd_qual
|
||||
|
||||
@@ -1682,8 +2242,20 @@ def _parse_setup_menu_items(raw):
|
||||
|
||||
|
||||
def _handle_proactive_chain(scc, sw91, on_fetch=None):
|
||||
"""Run a FETCH/TERMINAL RESPONSE chain; marks the card as busy so that
|
||||
terminal-initiated ENVELOPEs (Data available, Channel status, timers) wait."""
|
||||
global _PROACTIVE_BUSY
|
||||
_PROACTIVE_BUSY = True
|
||||
try:
|
||||
return _run_proactive_chain(scc, sw91, on_fetch)
|
||||
finally:
|
||||
_PROACTIVE_BUSY = False
|
||||
|
||||
|
||||
def _run_proactive_chain(scc, sw91, on_fetch=None):
|
||||
sys.stderr.write('91XX chain: sw=%s\n' % sw91)
|
||||
sw = sw91
|
||||
paused = False
|
||||
while sw.startswith('91'):
|
||||
fetch_len = int(sw[2:], 16) if len(sw) == 4 else 0x100
|
||||
rv = scc._tp.send_apdu('%s120000%02x' % (scc.cat_cla, fetch_len))
|
||||
@@ -1699,10 +2271,13 @@ def _handle_proactive_chain(scc, sw91, on_fetch=None):
|
||||
entry = None
|
||||
if on_fetch:
|
||||
action = on_fetch(raw, cmd_num, cmd_type, dev_src, dev_dst)
|
||||
if action != 'pause':
|
||||
paused = action == 'pause'
|
||||
if not paused:
|
||||
tr_tlv = None
|
||||
if raw and cmd_type in (0x40, 0x41, 0x42, 0x43, 0x44):
|
||||
tr_tlv = _handle_bip_command(scc, cmd_num, cmd_type, cmd_qual, raw, dev_src, dev_dst)
|
||||
if cmd_type == 0x27:
|
||||
tr_tlv = _handle_timer_command(cmd_num, cmd_type, cmd_qual, raw, dev_src, dev_dst)
|
||||
if tr_tlv is None:
|
||||
tr_tlv = _build_tr(scc, cmd_num, cmd_type, dev_src, dev_dst, cmd_qual)
|
||||
tr_rv = scc._tp.send_apdu('%s140000%02x%s' % (scc.cat_cla, len(tr_tlv), tr_tlv.hex()))
|
||||
@@ -1716,7 +2291,12 @@ def _handle_proactive_chain(scc, sw91, on_fetch=None):
|
||||
if st_sw.startswith('91'):
|
||||
sw = st_sw
|
||||
if action == 'exit':
|
||||
_bip_flush_channel_events(scc)
|
||||
return sw
|
||||
if not paused:
|
||||
# Never inject an ENVELOPE while a fetched command awaits its
|
||||
# TERMINAL RESPONSE (the menu browser answers it later).
|
||||
_bip_flush_channel_events(scc)
|
||||
|
||||
|
||||
def _send_terminal_profile(scc, tp_hex):
|
||||
@@ -2080,6 +2660,13 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
resp = {'seq': _BIP.seq, 'entries': _BIP.entries_after(after)[-200:]}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/scp81/script':
|
||||
self._log_req()
|
||||
resp = {'script': _SCP81_SCRIPT, 'sent': _SCP81_SCRIPT_SENT,
|
||||
'template': _SCP81_SCRIPT_TEMPLATE, 'cr_tag': _SCP81_SCRIPT_CR_TAG,
|
||||
'results': _SCP81_SCRIPT_RESULTS}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path.startswith('/api/'):
|
||||
self._send_json({'error': _err('not_found', lang)}, 404)
|
||||
self._log_resp({'error': _err('not_found', lang)})
|
||||
@@ -2861,7 +3448,8 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
self._log_resp(err)
|
||||
elif self.path == '/api/scp81/bip':
|
||||
body = self._read_body()
|
||||
self._log_req(body)
|
||||
# Never log the pre-shared key.
|
||||
self._log_req(dict(body, psk_hex='<redacted>') if isinstance(body, dict) and body.get('psk_hex') else body)
|
||||
try:
|
||||
resp = _scp81_bip_control(body)
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user