scp81: PSK by identity, scripts page, exact snapshots, SCP80 LOAD fit (v2.2.0)
Cards / SCP81:
- Cards is a top-level tab; presets gain PSK identity + key, HTTP-OTA
column, Edit/Update and a live PSK-map push into a running listener.
- SCP81 has Listener/Scripts pills; scripts are named local APDU lists
(Empty / Explore / Install from .cap / Delete templates), sent to the
server explicitly at start. The listener takes mode/host/port/script
only; PSK inputs and the .cap row are gone.
- Multi-PSK TLS listener: identity -> key lookup from the card presets
(POST /api/scp81/psk-map updates a running listener), unknown
identities log tls-psk-unknown and fail; handshake logs carry psk_match.
- Script engine: execution tracking (next/done/pending/results), a
resumed dialog sends only the unexecuted tail (unreported APDU is
resent), a fresh dialog restarts, listing continuation pages are
tracked separately (pending.pages/complete). Restart script button.
- POST /api/scp81/gen-install replaces the SCP81 ram-install queueing
(generation only; the .cap is never stored).
Profiler / snapshots:
- Snapshot comparison is always exact (mask checkboxes removed; the
first-4-bytes mask remains a profile-creation option).
- "matching records" line shows count + #record numbers.
- New Clone action: copy named "Copy of <profile>", opened in the editor.
- Matched-record count/numbers fix ("1 из 8 (#8)").
SCP80:
- Configurable / auto-fitted LOAD block size: each LOAD APDU encodes into
one SMS (pySim rejects secured packets above 140 octets, so a 240-byte
block could never be sent). Response reports the effective size and
clamps; encode failures are reported per step with the pySim message.
SW cache otaman-v161; docs/api.md, scp81-findings and help EN/RU updated.
Tests: 226 Python + 356 frontend.
This commit is contained in:
@@ -51,6 +51,16 @@ TLS_VERSIONS = {
|
||||
}
|
||||
|
||||
|
||||
def _norm_identity(identity):
|
||||
"""Normalize a PSK identity to the str OpenSSL reports (CPython hands it
|
||||
to the PSK callback as a str; bytes are decoded byte-exact)."""
|
||||
if identity is None:
|
||||
return None
|
||||
if isinstance(identity, (bytes, bytearray)):
|
||||
return bytes(identity).decode('latin-1')
|
||||
return str(identity)
|
||||
|
||||
|
||||
def parse_http_request(data):
|
||||
"""Parse an HTTP/1.1 request head (bytes up to CRLFCRLF) into
|
||||
(method, target, headers dict with lower-case names)."""
|
||||
@@ -120,13 +130,29 @@ def build_http_response(status, reason, headers, body=b'', chunked=False,
|
||||
class PskTlsServer:
|
||||
"""PSK TLS listener speaking the GP remote administration HTTP dialog."""
|
||||
|
||||
def __init__(self, host, port, psk, identity=None, on_log=None,
|
||||
def __init__(self, host, port, psk=None, identity=None, on_log=None,
|
||||
responder=None, timeout=10.0, chunked=False, chunk_size=0,
|
||||
keep_alive=False, compact_headers=False, tls_version='1.2',
|
||||
cipher=None, on_before_close=None, keylog=None,
|
||||
conn_header=None, half_close=False, answer_delay=0.0):
|
||||
conn_header=None, half_close=False, answer_delay=0.0,
|
||||
psk_map=None):
|
||||
# PSK lookup table: identity -> key. With an explicit psk_map a
|
||||
# handshake is accepted only for a listed identity; the legacy
|
||||
# single-key form (psk + optional identity pin, pin None = accept any
|
||||
# identity) remains for scripts and tests.
|
||||
self.wildcard_psk = None
|
||||
self.psk_map = {}
|
||||
if psk_map is not None:
|
||||
self.psk_map = {_norm_identity(k): bytes(v)
|
||||
for k, v in dict(psk_map).items() if v}
|
||||
elif psk is not None:
|
||||
pin = _norm_identity(identity)
|
||||
if pin is None:
|
||||
self.wildcard_psk = psk
|
||||
else:
|
||||
self.psk_map = {pin: bytes(psk)}
|
||||
self.psk = psk
|
||||
self.identity = identity
|
||||
self.identity = _norm_identity(identity)
|
||||
self.on_log = on_log
|
||||
self.responder = responder or self._default_responder
|
||||
self.timeout = timeout
|
||||
@@ -165,6 +191,7 @@ class PskTlsServer:
|
||||
# SEND-DATA conversation to settle before it accepts the response).
|
||||
self.answer_delay = float(answer_delay or 0)
|
||||
self.identity_seen = None
|
||||
self.identity_matched = None
|
||||
self.stopped = False
|
||||
self.conns = []
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
@@ -223,14 +250,34 @@ class PskTlsServer:
|
||||
return ctx
|
||||
|
||||
def _psk_cb(self, identity):
|
||||
"""OpenSSL asks for the key of the identity the client sent."""
|
||||
self.identity_seen = identity
|
||||
if self.identity is not None and identity != self.identity:
|
||||
self.log('tls-psk-unknown', identity=identity)
|
||||
# A dummy key keeps the callback type-safe; the handshake then
|
||||
# fails on the Finished MAC check.
|
||||
"""OpenSSL asks for the key of the identity the client sent.
|
||||
|
||||
The identity is looked up in the configured table (identity -> key);
|
||||
without a match the handshake fails on the Finished MAC check with a
|
||||
dummy key, and the attempt is logged as 'tls-psk-unknown'."""
|
||||
ident = _norm_identity(identity)
|
||||
self.identity_seen = ident
|
||||
key = self.psk_map.get(ident) if ident is not None else None
|
||||
if key is None:
|
||||
# Legacy single-key mode: no identity pin accepts any identity.
|
||||
key = self.wildcard_psk
|
||||
self.identity_matched = key is not None
|
||||
if key is None:
|
||||
self.log('tls-psk-unknown', identity=ident)
|
||||
return b'\x00' * 16
|
||||
return self.psk
|
||||
return key
|
||||
|
||||
@property
|
||||
def psk_identities(self):
|
||||
"""Identities the listener looks up (keys are never exposed)."""
|
||||
return sorted(self.psk_map)
|
||||
|
||||
def set_psk_map(self, psk_map):
|
||||
"""Replace the identity -> key table of a running listener."""
|
||||
self.psk_map = {_norm_identity(k): bytes(v)
|
||||
for k, v in dict(psk_map).items() if v}
|
||||
self.wildcard_psk = None
|
||||
return self.psk_identities
|
||||
|
||||
@staticmethod
|
||||
def _default_responder(method, target, headers, body):
|
||||
@@ -285,7 +332,8 @@ class PskTlsServer:
|
||||
try:
|
||||
tls = self.ctx.wrap_socket(conn, server_side=True)
|
||||
self.log('tls-handshake', peer=peer, cipher=tls.cipher()[0],
|
||||
version=tls.version(), identity=self.identity_seen)
|
||||
version=tls.version(), identity=self.identity_seen,
|
||||
psk_match=self.identity_matched)
|
||||
while not self.stopped:
|
||||
req = self._read_request(tls)
|
||||
if req is None:
|
||||
@@ -304,15 +352,15 @@ class PskTlsServer:
|
||||
status, resp_headers, resp_body = self.responder(
|
||||
method, target, headers, body)
|
||||
reason = {200: 'OK', 204: 'No Content'}.get(status, 'Status')
|
||||
conn = self.conn_header
|
||||
if conn == 'none':
|
||||
conn = None
|
||||
elif conn is None:
|
||||
conn = 'keep-alive' if self.keep_alive else 'close'
|
||||
conn_hdr = self.conn_header
|
||||
if conn_hdr == 'none':
|
||||
conn_hdr = None
|
||||
elif conn_hdr is None:
|
||||
conn_hdr = 'keep-alive' if self.keep_alive else 'close'
|
||||
response = build_http_response(
|
||||
status, reason, resp_headers, resp_body,
|
||||
chunked=self.chunked, compact=self.compact_headers,
|
||||
connection=conn)
|
||||
connection=conn_hdr)
|
||||
# The card's HTTP client reads its response record-by-record:
|
||||
# the whole response must arrive in ONE TLS record (chunk_size
|
||||
# 0), otherwise a split head stalls it and a head-only record
|
||||
|
||||
+415
-149
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '2.1.18'
|
||||
VERSION = '2.2.0'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -625,6 +625,28 @@ def _ota_reference(spi1, spi2, kic, kid, tar_hex, cntr_hex, apdu_hex, kic_key_he
|
||||
return b2h(out), spi
|
||||
|
||||
|
||||
def _max_load_block_size(spi1, spi2, kic, kid, tar_hex, cntr_hex,
|
||||
kic_key_hex, kid_key_hex, requested=240):
|
||||
"""Largest LOAD block payload that still fits one SMS (TS 31.115).
|
||||
|
||||
pySim's SMS dialect refuses to encode a secured packet above 140 octets,
|
||||
so a LOAD APDU of the default 240-byte block cannot be sent over SCP80.
|
||||
Trial-encode a synthetic LOAD APDU for decreasing payload sizes (the
|
||||
cipher padding makes a closed-form bound unreliable) and return the
|
||||
largest one that encodes; 0 = not even a 1-byte block fits."""
|
||||
cap = max(1, min(int(requested or 240), 240))
|
||||
for n in range(cap, 0, -1):
|
||||
apdu = '80E80000%02X%s00' % (n, '00' * n)
|
||||
try:
|
||||
out_hex, _ = _ota_reference(spi1, spi2, kic, kid, tar_hex, cntr_hex,
|
||||
apdu, kic_key_hex, kid_key_hex)
|
||||
except ValueError:
|
||||
continue
|
||||
if len(out_hex) // 2 <= 140:
|
||||
return n
|
||||
return 0
|
||||
|
||||
|
||||
def _decode_por(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex, response_hex):
|
||||
from pySim.ota import OtaDialectSms, CompactRemoteResp
|
||||
from osmocom.utils import h2b, b2h
|
||||
@@ -826,7 +848,12 @@ _PLI_DATA = {q: '' for q in PLI_QUALIFIER_NAMES}
|
||||
|
||||
_BIP = httpota.BipTerminal()
|
||||
_SCP81_LISTENER = None
|
||||
_SCP81_PSK = {}
|
||||
# PSK table of the TLS listener: identity -> key (memory only, never logged or
|
||||
# persisted; the PWA sends it from the card presets at listener start).
|
||||
# _SCP81_PSK_LEGACY keeps a single-key start (psk_hex [+ psk_identity]) so an
|
||||
# API restart without psk_map/psk_hex can reuse it.
|
||||
_SCP81_PSKS = {}
|
||||
_SCP81_PSK_LEGACY = None
|
||||
|
||||
_POLL_ENABLED = False
|
||||
_POLL_INTERVAL = 30
|
||||
@@ -1299,8 +1326,10 @@ def _scp81_listener_status():
|
||||
return None
|
||||
if isinstance(_SCP81_LISTENER, scp81.PskTlsServer):
|
||||
return {'mode': 'tls', 'host': _SCP81_LISTENER.host, 'port': _SCP81_LISTENER.port,
|
||||
'psk_identity': _SCP81_LISTENER.identity,
|
||||
'psk_identities': _SCP81_LISTENER.psk_identities,
|
||||
'psk_wildcard': _SCP81_LISTENER.wildcard_psk is not None,
|
||||
'identity_seen': _SCP81_LISTENER.identity_seen,
|
||||
'identity_matched': _SCP81_LISTENER.identity_matched,
|
||||
'chunked': _SCP81_LISTENER.chunked,
|
||||
'chunk_size': _SCP81_LISTENER.chunk_size,
|
||||
'keep_alive': _SCP81_LISTENER.keep_alive,
|
||||
@@ -1376,49 +1405,73 @@ def _bip_data_available(ch):
|
||||
# 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 DATA 0085, then GET STATUS with P2=02 (TLV structure,
|
||||
# 'first or all') and data '4F00' (match all): P1=80 (Issuer Security
|
||||
# Domain), P1=40 (applications and supplementary security domains),
|
||||
# P1=20 (executable load files), P1=10 (ELF and their modules);
|
||||
# Le=00 so no GET RESPONSE is needed. Long listings answer SW CAFE and
|
||||
# are auto-continued with the same command carrying P2.b1=1 ('next').
|
||||
'explore': ['80CAFF2100', '80F28002024F0000', '80CA008500',
|
||||
'80F24002024F0000', '80F22002024F0000', '80F21002024F0000'],
|
||||
'none': [],
|
||||
}
|
||||
_SCP81_SCRIPT = list(_SCP81_SCRIPTS['explore'])
|
||||
_SCP81_SCRIPT_SENT = 0
|
||||
# The command script served to the card, set by the PWA at listener start (an
|
||||
# explicit APDU list) or via /api/scp81/queue. The server is agnostic to what
|
||||
# the APDUs do: it serves them one per administration POST and tracks
|
||||
# execution so a resumed session sends only the leftover APDUs.
|
||||
# _SCP81_SCRIPT_BASE the configured APDU list (never mutated)
|
||||
# _SCP81_SCRIPT_NEXT index in BASE of the next APDU to send
|
||||
# _SCP81_SCRIPT_DONE BASE indices the card reported (executed, any result)
|
||||
# _SCP81_SCRIPT_PENDING the APDU sent in the previous POST, waiting for the
|
||||
# card's X-Admin-Script-Status report (None = none); a
|
||||
# session that dies before the report resends it
|
||||
# _SCP81_SCRIPT_RESULTS one entry per reported APDU
|
||||
# {index, pos, page, apdu, rapdu, sw}
|
||||
# _SCP81_SCRIPT_PAGE_QUEUE continuation pages auto-inserted for truncated
|
||||
# GET STATUS listings (sent before BASE[NEXT])
|
||||
_SCP81_SCRIPT_BASE = []
|
||||
_SCP81_SCRIPT_NEXT = 0
|
||||
_SCP81_SCRIPT_DONE = set()
|
||||
_SCP81_SCRIPT_PENDING = None
|
||||
_SCP81_SCRIPT_RESULTS = []
|
||||
# Continuation pages: long GET STATUS listings answer SW CAFE with 127-byte
|
||||
# pages; the responder auto-inserts a GET STATUS (P2=02, last AID as search
|
||||
# criterion) after each page until the listing ends.
|
||||
_SCP81_SCRIPT_INSERTED = []
|
||||
_SCP81_SCRIPT_PAGE_QUEUE = []
|
||||
_SCP81_SCRIPT_SENT_NO = 0
|
||||
_SCP81_PAGES = 0
|
||||
SCP81_MAX_PAGES = 24
|
||||
# What the queued script is ('explore', 'none', 'custom' or 'ram-install').
|
||||
_SCP81_SCRIPT_KIND = 'explore'
|
||||
# What the queued script is ('none', 'custom' or the name the PWA sent).
|
||||
_SCP81_SCRIPT_KIND = 'none'
|
||||
|
||||
|
||||
def _scp81_restart_run():
|
||||
"""Start a new run over the configured script (fresh dialog): clear the
|
||||
execution marks, the continuation pages and the results."""
|
||||
global _SCP81_SCRIPT_NEXT, _SCP81_SCRIPT_DONE, _SCP81_SCRIPT_PENDING
|
||||
global _SCP81_SCRIPT_RESULTS, _SCP81_SCRIPT_PAGE_QUEUE, _SCP81_PAGES
|
||||
global _SCP81_SCRIPT_SENT_NO
|
||||
_SCP81_SCRIPT_NEXT = 0
|
||||
_SCP81_SCRIPT_DONE = set()
|
||||
_SCP81_SCRIPT_PENDING = None
|
||||
_SCP81_SCRIPT_RESULTS = []
|
||||
_SCP81_SCRIPT_PAGE_QUEUE = []
|
||||
_SCP81_SCRIPT_SENT_NO = 0
|
||||
_SCP81_PAGES = 0
|
||||
|
||||
|
||||
def _scp81_script_mid_run():
|
||||
"""True while a run has started and not finished (queue replaces it only
|
||||
with force)."""
|
||||
return (0 < _SCP81_SCRIPT_NEXT < len(_SCP81_SCRIPT_BASE)
|
||||
or _SCP81_SCRIPT_PENDING is not None
|
||||
or bool(_SCP81_SCRIPT_PAGE_QUEUE))
|
||||
|
||||
|
||||
def _scp81_reset_script(script, kind):
|
||||
"""Install a new APDU script and clear all execution progress."""
|
||||
global _SCP81_SCRIPT_BASE, _SCP81_SCRIPT_KIND
|
||||
_SCP81_SCRIPT_BASE = [re.sub(r'\s', '', a).upper() for a in script if a]
|
||||
_SCP81_SCRIPT_KIND = kind
|
||||
_scp81_restart_run()
|
||||
_BIP.log('script-queued', script_kind=kind, apdus=len(_SCP81_SCRIPT_BASE))
|
||||
|
||||
|
||||
def _scp81_queue_script(apdus, kind='custom', force=False):
|
||||
"""Replace the SCP81 command script with a new APDU list. Refuses while
|
||||
a script is mid-run unless forced; the list runs on the card's next POST."""
|
||||
global _SCP81_SCRIPT, _SCP81_SCRIPT_SENT, _SCP81_SCRIPT_RESULTS
|
||||
global _SCP81_SCRIPT_INSERTED, _SCP81_PAGES, _SCP81_SCRIPT_KIND
|
||||
if not force and 0 < _SCP81_SCRIPT_SENT < len(_SCP81_SCRIPT):
|
||||
if not force and _scp81_script_mid_run():
|
||||
return {'queued': False, 'reason': 'script in progress',
|
||||
'sent': _SCP81_SCRIPT_SENT, 'of': len(_SCP81_SCRIPT)}
|
||||
_SCP81_SCRIPT = [a.upper().replace(' ', '') for a in apdus]
|
||||
_SCP81_SCRIPT_KIND = kind
|
||||
_SCP81_SCRIPT_SENT = 0
|
||||
_SCP81_SCRIPT_RESULTS = []
|
||||
_SCP81_SCRIPT_INSERTED = []
|
||||
_SCP81_PAGES = 0
|
||||
_BIP.log('script-queued', script_kind=kind, apdus=len(_SCP81_SCRIPT))
|
||||
return {'queued': True, 'apdus': len(_SCP81_SCRIPT)}
|
||||
'next': _SCP81_SCRIPT_NEXT, 'of': len(_SCP81_SCRIPT_BASE)}
|
||||
_scp81_reset_script(apdus, kind)
|
||||
return {'queued': True, 'apdus': len(_SCP81_SCRIPT_BASE)}
|
||||
_SCP81_SCRIPT_TEMPLATE = 'indefinite'
|
||||
_SCP81_SCRIPT_CR_TAG = False
|
||||
# None = short per-command Next-URI ('/N'); '' = omit the header (spec: the
|
||||
@@ -1531,54 +1584,125 @@ def _scp81_continuation(apdu):
|
||||
return '%s%02X%s' % (u[:6], p2, u[8:])
|
||||
|
||||
|
||||
def _scp81_script_state():
|
||||
"""State of the configured command script for `GET /api/scp81/script`.
|
||||
|
||||
Progress counts only the configured APDUs (`total`/`done`); the C-APDU
|
||||
awaiting the card's report is reported as `pending` ({index, pos, page,
|
||||
apdu} or null) and the auto-inserted listing continuation pages as
|
||||
`pages`/`pages_queued`, so a run whose script APDUs are all executed is
|
||||
not mistaken for 'still pending' while a page is in flight."""
|
||||
pending = None
|
||||
if _SCP81_SCRIPT_PENDING is not None:
|
||||
p = _SCP81_SCRIPT_PENDING
|
||||
pending = {'index': p['index'], 'pos': p.get('pos'),
|
||||
'page': bool(p.get('page')), 'apdu': p['apdu']}
|
||||
return {'script': list(_SCP81_SCRIPT_BASE),
|
||||
'next': _SCP81_SCRIPT_NEXT,
|
||||
'total': len(_SCP81_SCRIPT_BASE),
|
||||
'done': sorted(_SCP81_SCRIPT_DONE),
|
||||
'pending': pending,
|
||||
'pages': _SCP81_PAGES,
|
||||
'pages_queued': len(_SCP81_SCRIPT_PAGE_QUEUE),
|
||||
'complete': (len(_SCP81_SCRIPT_DONE) >= len(_SCP81_SCRIPT_BASE)
|
||||
and _SCP81_SCRIPT_PENDING is None
|
||||
and not _SCP81_SCRIPT_PAGE_QUEUE),
|
||||
'kind': _SCP81_SCRIPT_KIND,
|
||||
'template': _SCP81_SCRIPT_TEMPLATE,
|
||||
'cr_tag': _SCP81_SCRIPT_CR_TAG,
|
||||
'results': _SCP81_SCRIPT_RESULTS}
|
||||
|
||||
|
||||
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, _SCP81_PAGES
|
||||
global _SCP81_SCRIPT_INSERTED
|
||||
the next scripted C-APDU or close the session (TS 102 226 / GP 4.4.2).
|
||||
|
||||
Execution tracking: an APDU counts as executed only when the card reports
|
||||
it in the next POST (X-Admin-Script-Status, with the Response Scripting
|
||||
template on success). A session that dies before the report leaves the
|
||||
APDU pending: a resumed dialog (X-Admin-Resume) resends it, while a POST
|
||||
without that header is a fresh dialog where the script runs from the
|
||||
start (so a completed script runs again on a new trigger)."""
|
||||
global _SCP81_SCRIPT_NEXT, _SCP81_SCRIPT_DONE, _SCP81_SCRIPT_PENDING
|
||||
global _SCP81_SCRIPT_RESULTS, _SCP81_SCRIPT_PAGE_QUEUE, _SCP81_PAGES
|
||||
global _SCP81_SCRIPT_SENT_NO
|
||||
status = headers.get('x-admin-script-status')
|
||||
if status is not None:
|
||||
index = _SCP81_SCRIPT_SENT
|
||||
resume = headers.get('x-admin-resume')
|
||||
pending = _SCP81_SCRIPT_PENDING
|
||||
if pending is not None:
|
||||
_SCP81_SCRIPT_PENDING = None
|
||||
index = pending['index']
|
||||
if status is not None:
|
||||
# The card reports the outcome of the pending C-APDU.
|
||||
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, 'pos': pending.get('pos'),
|
||||
'page': bool(pending.get('page')), 'sw': sw,
|
||||
'apdu': pending['apdu'],
|
||||
'rapdu': rapdu.hex().upper()})
|
||||
if rapdus:
|
||||
if pending['apdu'].startswith('80CAFF21'):
|
||||
decoded = _scp81_decode_memory(rapdus[-1][0])
|
||||
if decoded:
|
||||
_BIP.log('script-memory', **decoded)
|
||||
# '63 10' = "more data available" (GP Table 11-38); the
|
||||
# live card uses a proprietary 'CA FE' for the same case.
|
||||
if (rapdus[-1][1].upper() in ('CAFE', '6310')
|
||||
and _SCP81_PAGES < SCP81_MAX_PAGES):
|
||||
cont = _scp81_continuation(pending['apdu'])
|
||||
if cont:
|
||||
_SCP81_PAGES += 1
|
||||
_SCP81_SCRIPT_PAGE_QUEUE.append(cont)
|
||||
_BIP.log('script-page', index=index,
|
||||
page=_SCP81_PAGES, apdu=cont)
|
||||
if pending.get('pos') is not None:
|
||||
_SCP81_SCRIPT_DONE.add(pending['pos'])
|
||||
elif resume:
|
||||
# Resumed dialog: the pending APDU was never reported, so it is
|
||||
# still unexecuted - put it back in line and resend it.
|
||||
if pending.get('page'):
|
||||
_SCP81_SCRIPT_PAGE_QUEUE.insert(0, pending['apdu'])
|
||||
else:
|
||||
_SCP81_SCRIPT_NEXT = pending['pos']
|
||||
_SCP81_SCRIPT_DONE.discard(pending['pos'])
|
||||
_BIP.log('script-resend', index=index, apdu=pending['apdu'])
|
||||
else:
|
||||
# Fresh dialog (new trigger): the script runs from the start.
|
||||
_scp81_restart_run()
|
||||
elif status is not None:
|
||||
# A report without a pending APDU (the server may have restarted
|
||||
# mid-session): log the R-APDUs but do not advance anything.
|
||||
if status != 'ok':
|
||||
_BIP.log('script-status', index=index, status=status)
|
||||
_BIP.log('script-status', index=None, status=status)
|
||||
else:
|
||||
count, rapdus = _scp81_parse_response(body)
|
||||
apdu = _SCP81_SCRIPT[index - 1].upper() if (_SCP81_SCRIPT and index >= 1) else ''
|
||||
for rapdu, sw in rapdus:
|
||||
_BIP.log('script-rapdu', index=index, sw=sw, bytes=len(rapdu),
|
||||
_BIP.log('script-rapdu', index=None, sw=sw, bytes=len(rapdu),
|
||||
hex=rapdu.hex().upper()[:2000])
|
||||
_SCP81_SCRIPT_RESULTS.append({'index': index, 'sw': sw,
|
||||
'apdu': apdu,
|
||||
'rapdu': rapdu.hex().upper()})
|
||||
if rapdus and _SCP81_SCRIPT and index >= 1:
|
||||
if apdu.startswith('80CAFF21'):
|
||||
decoded = _scp81_decode_memory(rapdus[-1][0])
|
||||
if decoded:
|
||||
_BIP.log('script-memory', **decoded)
|
||||
# '63 10' = "more data available" (GP Table 11-38); the live
|
||||
# card uses a proprietary 'CA FE' for the same condition.
|
||||
if rapdus[-1][1].upper() in ('CAFE', '6310') and _SCP81_PAGES < SCP81_MAX_PAGES:
|
||||
cont = _scp81_continuation(apdu)
|
||||
if cont:
|
||||
_SCP81_PAGES += 1
|
||||
_SCP81_SCRIPT.insert(_SCP81_SCRIPT_SENT, cont)
|
||||
_SCP81_SCRIPT_INSERTED.append(cont)
|
||||
_BIP.log('script-page', index=index, page=_SCP81_PAGES,
|
||||
apdu=cont)
|
||||
else:
|
||||
# First (or resumed) POST of a session: run the script from the start.
|
||||
# Drop continuation pages inserted by a previous session.
|
||||
if _SCP81_SCRIPT_INSERTED:
|
||||
_SCP81_SCRIPT[:] = [a for a in _SCP81_SCRIPT
|
||||
if a not in _SCP81_SCRIPT_INSERTED]
|
||||
_SCP81_SCRIPT_INSERTED = []
|
||||
_SCP81_SCRIPT_SENT = 0
|
||||
_SCP81_SCRIPT_RESULTS = []
|
||||
_SCP81_PAGES = 0
|
||||
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)
|
||||
elif not resume:
|
||||
# First POST of a fresh dialog: reset a previous run.
|
||||
_scp81_restart_run()
|
||||
apdu = None
|
||||
if _SCP81_SCRIPT_PAGE_QUEUE:
|
||||
apdu = _SCP81_SCRIPT_PAGE_QUEUE.pop(0)
|
||||
_SCP81_SCRIPT_PENDING = {'index': _SCP81_SCRIPT_SENT_NO + 1,
|
||||
'pos': None, 'page': True, 'apdu': apdu}
|
||||
elif _SCP81_SCRIPT_NEXT < len(_SCP81_SCRIPT_BASE):
|
||||
pos = _SCP81_SCRIPT_NEXT
|
||||
apdu = _SCP81_SCRIPT_BASE[pos]
|
||||
_SCP81_SCRIPT_NEXT = pos + 1
|
||||
_SCP81_SCRIPT_PENDING = {'index': _SCP81_SCRIPT_SENT_NO + 1,
|
||||
'pos': pos, 'page': False, 'apdu': apdu}
|
||||
if apdu is not None:
|
||||
index = _SCP81_SCRIPT_PENDING['index']
|
||||
_SCP81_SCRIPT_SENT_NO = index
|
||||
_BIP.log('script-send', index=index, apdu=apdu)
|
||||
headers = _scp81_response_headers()
|
||||
if _SCP81_TARGETED_APP:
|
||||
headers['X-Admin-Targeted-Application'] = _SCP81_TARGETED_APP
|
||||
@@ -1587,7 +1711,7 @@ def _scp81_script_responder(method, target, headers, body):
|
||||
# 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
|
||||
next_uri = template % index if '%d' in template else template
|
||||
if next_uri:
|
||||
headers['X-Admin-Next-URI'] = next_uri
|
||||
u = apdu.upper()
|
||||
@@ -1606,7 +1730,7 @@ def _scp81_script_responder(method, target, headers, body):
|
||||
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,
|
||||
_BIP.log('script-done', sent=_SCP81_SCRIPT_SENT_NO,
|
||||
results=len(_SCP81_SCRIPT_RESULTS))
|
||||
headers = _scp81_response_headers()
|
||||
if _SCP81_APACHE_HEADERS:
|
||||
@@ -1627,10 +1751,101 @@ def _scp81_response_headers():
|
||||
return headers
|
||||
|
||||
|
||||
def _parse_psk_map(raw):
|
||||
"""Parse a PSK lookup table from an API request.
|
||||
|
||||
Accepts a list of {identity, psk_hex} objects (the PWA form) or a
|
||||
{identity: psk_hex} map; entries without a usable identity or key are
|
||||
skipped. Returns (table, error)."""
|
||||
if raw is None:
|
||||
return None, None
|
||||
if isinstance(raw, dict):
|
||||
raw = [{'identity': k, 'psk_hex': v} for k, v in raw.items()]
|
||||
if not isinstance(raw, list):
|
||||
return None, 'psk_map must be a list of {identity, psk_hex}'
|
||||
table = {}
|
||||
for item in raw:
|
||||
if not isinstance(item, dict):
|
||||
return None, 'psk_map entries must be {identity, psk_hex} objects'
|
||||
ident = str(item.get('identity') or '').strip()
|
||||
key_hex = re.sub(r'\s', '', str(item.get('psk_hex') or ''))
|
||||
if not key_hex or not ident:
|
||||
continue
|
||||
try:
|
||||
key = bytes.fromhex(key_hex)
|
||||
except ValueError:
|
||||
return None, 'psk_map[%s]: psk_hex is not valid hex' % ident
|
||||
if key:
|
||||
table[ident] = key
|
||||
return table, None
|
||||
|
||||
|
||||
def _redact_psk_fields(body):
|
||||
"""Copy of a request body with PSK key material masked (keys must never
|
||||
reach the logs; identities stay visible for diagnostics)."""
|
||||
if not isinstance(body, dict):
|
||||
return body
|
||||
out = dict(body)
|
||||
if out.get('psk_hex'):
|
||||
out['psk_hex'] = '<redacted>'
|
||||
psk_map = out.get('psk_map')
|
||||
if isinstance(psk_map, dict):
|
||||
out['psk_map'] = {k: '<redacted>' for k in psk_map}
|
||||
elif isinstance(psk_map, list):
|
||||
out['psk_map'] = [
|
||||
dict(e, psk_hex='<redacted>')
|
||||
if isinstance(e, dict) and e.get('psk_hex') else e
|
||||
for e in psk_map]
|
||||
return out
|
||||
|
||||
|
||||
def _scp81_update_psk_map(body):
|
||||
"""Replace the PSK table of the running TLS listener. The card presets are
|
||||
the source of truth; the PWA pushes edits without a listener restart."""
|
||||
global _SCP81_PSKS, _SCP81_PSK_LEGACY
|
||||
table, err = _parse_psk_map((body or {}).get('psk_map'))
|
||||
if err:
|
||||
return {'ok': False, 'error': err}
|
||||
if not table:
|
||||
return {'ok': False, 'error': 'no usable PSK entries (identity + key required)'}
|
||||
if not isinstance(_SCP81_LISTENER, scp81.PskTlsServer):
|
||||
return {'ok': False, 'error': 'PSK TLS listener is not running'}
|
||||
_SCP81_LISTENER.set_psk_map(table)
|
||||
_SCP81_PSKS = dict(table)
|
||||
_SCP81_PSK_LEGACY = None
|
||||
_BIP.log('tls-psk-map', identities=sorted(table))
|
||||
return {'ok': True, 'identities': sorted(table),
|
||||
'listener': _scp81_listener_status()}
|
||||
|
||||
|
||||
def _scp81_gen_install(body):
|
||||
"""Generate the RAM APDU sequence for a .cap without touching the listener
|
||||
or the running script. The PWA uses it to turn an 'Install from .cap'
|
||||
script template into INSTALL [for load] / LOAD blocks / INSTALL [for
|
||||
install] APDUs; the .cap itself is never stored."""
|
||||
body = body or {}
|
||||
cap_hex = re.sub(r'\s', '', body.get('cap_hex') or '')
|
||||
if not cap_hex:
|
||||
return {'ok': False, 'error': 'No cap_hex provided'}
|
||||
try:
|
||||
loadfile_aid, module_aid, loadfile_data = _cap_parse(cap_hex)
|
||||
seq = _cap_apdu_sequence(
|
||||
loadfile_aid, module_aid, loadfile_data,
|
||||
sd_aid=re.sub(r'\s', '', body.get('sd_aid') or ''),
|
||||
privileges=re.sub(r'\s', '', body.get('privileges') or '') or '00',
|
||||
install_params=re.sub(r'\s', '', body.get('install_params') or ''),
|
||||
stk_params=re.sub(r'\s', '', body.get('stk_params') or ''),
|
||||
make_selectable=bool(body.get('make_selectable', True)))
|
||||
except Exception as e:
|
||||
return {'ok': False, 'error': 'cap parse failed: %s' % e}
|
||||
_BIP.log('gen-install', apdus=len(seq), load_file_aid=loadfile_aid,
|
||||
module_aid=module_aid)
|
||||
return {'ok': True, 'apdus': seq, 'load_file_aid': loadfile_aid,
|
||||
'module_aid': module_aid}
|
||||
|
||||
|
||||
def _scp81_bip_control(body):
|
||||
global _SCP81_LISTENER, _SCP81_PSK
|
||||
global _SCP81_SCRIPT, _SCP81_SCRIPT_SENT, _SCP81_SCRIPT_RESULTS
|
||||
global _SCP81_SCRIPT_INSERTED, _SCP81_PAGES, _SCP81_SCRIPT_KIND
|
||||
global _SCP81_LISTENER, _SCP81_PSKS, _SCP81_PSK_LEGACY
|
||||
global _SCP81_SCRIPT_TEMPLATE, _SCP81_SCRIPT_CR_TAG, _SCP81_NEXT_URI
|
||||
global _SCP81_LINK_EVENTS, _SCP81_TARGETED_APP, _SCP81_APACHE_HEADERS
|
||||
global _SCP81_CHUNKED
|
||||
@@ -1651,34 +1866,47 @@ def _scp81_bip_control(body):
|
||||
_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
|
||||
raw_map = body.get('psk_map')
|
||||
table, err = _parse_psk_map(raw_map)
|
||||
if err:
|
||||
return {'ok': False, 'error': err}
|
||||
if raw_map is not None and not table:
|
||||
return {'ok': False,
|
||||
'error': 'psk_map has no usable entries (identity + key required)'}
|
||||
psk = None
|
||||
identity = None
|
||||
psk_hex = body.get('psk_hex') or ''
|
||||
if table:
|
||||
pass # table sent by the PWA from the presets
|
||||
elif psk_hex:
|
||||
# Legacy single-key form (API/tests): psk_hex [+ psk_identity].
|
||||
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
|
||||
elif _SCP81_PSKS:
|
||||
table = dict(_SCP81_PSKS) # reuse the table of the last start
|
||||
elif _SCP81_PSK_LEGACY:
|
||||
psk, identity = _SCP81_PSK_LEGACY # reuse the last single key
|
||||
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]
|
||||
_SCP81_SCRIPT_KIND = 'custom'
|
||||
elif script in _SCP81_SCRIPTS:
|
||||
_SCP81_SCRIPT = list(_SCP81_SCRIPTS[script])
|
||||
_SCP81_SCRIPT_KIND = script
|
||||
else:
|
||||
return {'ok': False, 'error': 'unknown script preset: %s' % script}
|
||||
_SCP81_SCRIPT_SENT = 0
|
||||
_SCP81_SCRIPT_RESULTS = []
|
||||
_SCP81_SCRIPT_INSERTED = []
|
||||
_SCP81_PAGES = 0
|
||||
return {'ok': False, 'error': 'no PSK configured: send psk_map '
|
||||
'(card presets) or psk_hex'}
|
||||
script = body.get('script')
|
||||
if isinstance(script, str):
|
||||
if script in ('none', ''):
|
||||
_scp81_reset_script([], 'none')
|
||||
else:
|
||||
return {'ok': False, 'error': 'unknown script preset: %s; send an '
|
||||
'explicit APDU list or none' % script}
|
||||
elif isinstance(script, list):
|
||||
_scp81_reset_script(script, body.get('script_kind') or 'custom')
|
||||
# No 'script' key: keep the configured script and its run progress
|
||||
# (an explicit list starts a fresh run).
|
||||
template = body.get('script_template', 'indefinite')
|
||||
if template not in ('indefinite', 'definite'):
|
||||
return {'ok': False, 'error': 'script_template must be indefinite or definite'}
|
||||
@@ -1698,7 +1926,7 @@ def _scp81_bip_control(body):
|
||||
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,
|
||||
host, port, psk, identity=identity, psk_map=(table or None),
|
||||
responder=_scp81_script_responder,
|
||||
chunked=bool(body.get('chunked', True)),
|
||||
chunk_size=chunk_size,
|
||||
@@ -1711,10 +1939,14 @@ def _scp81_bip_control(body):
|
||||
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))
|
||||
_SCP81_PSKS = dict(_SCP81_LISTENER.psk_map)
|
||||
_SCP81_PSK_LEGACY = None if table else (psk, identity)
|
||||
_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,
|
||||
'script': list(_SCP81_SCRIPT_BASE),
|
||||
'script_kind': _SCP81_SCRIPT_KIND,
|
||||
'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,
|
||||
@@ -2782,10 +3014,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/scp81/script':
|
||||
self._log_req()
|
||||
resp = {'script': _SCP81_SCRIPT, 'sent': _SCP81_SCRIPT_SENT,
|
||||
'kind': _SCP81_SCRIPT_KIND,
|
||||
'template': _SCP81_SCRIPT_TEMPLATE, 'cr_tag': _SCP81_SCRIPT_CR_TAG,
|
||||
'results': _SCP81_SCRIPT_RESULTS}
|
||||
resp = _scp81_script_state()
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path.startswith('/api/'):
|
||||
@@ -3453,14 +3682,57 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
make_selectable = body.get('make_selectable', True)
|
||||
privileges_hex = body.get('privileges', '').replace(' ', '') or '00'
|
||||
|
||||
# LOAD block size: the default 240-byte payload cannot be sent
|
||||
# over SCP80 (pySim refuses a secured packet above one SMS).
|
||||
# Fit it automatically, or honour an explicit override capped
|
||||
# to what still encodes into a single SMS.
|
||||
block_size_req = body.get('load_block_size')
|
||||
if block_size_req in (None, ''):
|
||||
block_size_req = None
|
||||
else:
|
||||
try:
|
||||
block_size_req = int(block_size_req)
|
||||
except (TypeError, ValueError):
|
||||
block_size_req = -1
|
||||
if not 1 <= block_size_req <= 240:
|
||||
err = {'success': False,
|
||||
'error': 'load_block_size must be 1..240'}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
return
|
||||
max_block = _max_load_block_size(spi1, spi2, kic, kid, tar, cntr,
|
||||
kic_key, kid_key,
|
||||
requested=block_size_req or 240)
|
||||
if max_block < 1:
|
||||
err = {'success': False,
|
||||
'error': 'no LOAD block fits a single SMS with these '
|
||||
'SCP80 parameters'}
|
||||
self._send_json(err, 500)
|
||||
self._log_resp(err)
|
||||
return
|
||||
block_size = min(block_size_req, max_block) if block_size_req else max_block
|
||||
block_clamped = block_size_req is not None and block_size != block_size_req
|
||||
sys.stderr.write('RAM-INSTALL: LOAD block size %d bytes%s\n' % (
|
||||
block_size,
|
||||
(' (requested %d, clamped to fit one SMS)' % block_size_req)
|
||||
if block_clamped else ''))
|
||||
|
||||
steps = []
|
||||
encode_error = None
|
||||
include_cpi = body.get('includeCpi', True)
|
||||
spi2_val = int(spi2, 16)
|
||||
por_in_submit = bool(spi2_val & 0x20)
|
||||
|
||||
def _send_gp_apdu(apdu_hex, step_name):
|
||||
nonlocal cntr
|
||||
sp_hex, _ = _ota_reference(spi1, spi2, kic, kid, tar, cntr, apdu_hex, kic_key, kid_key)
|
||||
nonlocal cntr, encode_error
|
||||
try:
|
||||
sp_hex, _ = _ota_reference(spi1, spi2, kic, kid, tar, cntr, apdu_hex, kic_key, kid_key)
|
||||
except ValueError as e:
|
||||
encode_error = str(e)
|
||||
steps.append({'name': step_name, 'por_status': 'encode_error',
|
||||
'sw': encode_error})
|
||||
sys.stderr.write('RAM-INSTALL: %s encode failed: %s\n' % (step_name, e))
|
||||
return False
|
||||
sp_bytes = bytes.fromhex(sp_hex)
|
||||
max_chunk = 130
|
||||
chunks = [sp_bytes[i:i + max_chunk] for i in range(0, len(sp_bytes), max_chunk)]
|
||||
@@ -3514,7 +3786,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
loadfile_aid, module_aid, loadfile_data, sd_aid=sd_aid,
|
||||
privileges=privileges_hex,
|
||||
install_params=install_params_hex, stk_params=stk_params_hex,
|
||||
make_selectable=make_selectable)
|
||||
make_selectable=make_selectable, block_size=block_size)
|
||||
sys.stderr.write('RAM-INSTALL: %d APDUs (INSTALL / %d x LOAD / INSTALL) loadfile_aid=%s\n' % (
|
||||
len(seq), len(seq) - 2, loadfile_aid))
|
||||
for apdu_idx, gp_apdu in enumerate(seq):
|
||||
@@ -3526,14 +3798,20 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
step_name = 'LOAD (%d/%d)' % (apdu_idx, len(seq) - 2)
|
||||
if not _send_gp_apdu(gp_apdu, step_name):
|
||||
resp = {'success': False, 'steps': steps, 'failed_step': len(steps),
|
||||
'error': '%s failed' % step_name,
|
||||
'load_file_aid': loadfile_aid, 'module_aid': module_aid}
|
||||
'error': encode_error or ('%s failed' % step_name),
|
||||
'load_file_aid': loadfile_aid, 'module_aid': module_aid,
|
||||
'load_block_size': block_size,
|
||||
'load_block_size_requested': block_size_req,
|
||||
'load_block_size_clamped': block_clamped}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
return
|
||||
|
||||
resp = {'success': True, 'steps': steps, 'load_file_aid': loadfile_aid,
|
||||
'module_aid': module_aid, 'final_cntr': cntr}
|
||||
'module_aid': module_aid, 'final_cntr': cntr,
|
||||
'load_block_size': block_size,
|
||||
'load_block_size_requested': block_size_req,
|
||||
'load_block_size_clamped': block_clamped}
|
||||
sys.stderr.write('RAM-INSTALL: Complete — loadfile_aid=%s module_aid=%s cntr=%s\n' % (
|
||||
loadfile_aid, module_aid, cntr))
|
||||
self._send_json(resp)
|
||||
@@ -3545,14 +3823,23 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
self._log_resp(err)
|
||||
elif self.path == '/api/scp81/bip':
|
||||
body = self._read_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)
|
||||
# Never log the pre-shared keys.
|
||||
self._log_req(_redact_psk_fields(body))
|
||||
try:
|
||||
resp = _scp81_bip_control(body)
|
||||
except Exception as e:
|
||||
resp = {'ok': False, 'error': str(e)}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/scp81/psk-map':
|
||||
body = self._read_body()
|
||||
self._log_req(_redact_psk_fields(body))
|
||||
try:
|
||||
resp = _scp81_update_psk_map(body)
|
||||
except Exception as e:
|
||||
resp = {'ok': False, 'error': str(e)}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/scp81/queue':
|
||||
body = self._read_body()
|
||||
self._log_req(body)
|
||||
@@ -3572,34 +3859,13 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
'runs on the card next POST (push/trigger)')
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/scp81/ram-install':
|
||||
elif self.path == '/api/scp81/gen-install':
|
||||
body = self._read_body()
|
||||
self._log_req(body)
|
||||
cap_hex = (body.get('cap_hex') or '').replace(' ', '')
|
||||
if not cap_hex:
|
||||
resp = {'ok': False, 'error': 'No cap_hex provided'}
|
||||
elif _SCP81_LISTENER is None:
|
||||
resp = {'ok': False, 'error': 'SCP81 listener is not running'}
|
||||
else:
|
||||
try:
|
||||
loadfile_aid, module_aid, loadfile_data = _cap_parse(cap_hex)
|
||||
seq = _cap_apdu_sequence(
|
||||
loadfile_aid, module_aid, loadfile_data,
|
||||
sd_aid=(body.get('sd_aid') or '').replace(' ', ''),
|
||||
privileges=(body.get('privileges') or '').replace(' ', '') or '00',
|
||||
install_params=(body.get('install_params') or '').replace(' ', ''),
|
||||
stk_params=(body.get('stk_params') or '').replace(' ', ''),
|
||||
make_selectable=bool(body.get('make_selectable', True)))
|
||||
queued = _scp81_queue_script(seq, kind='ram-install',
|
||||
force=bool(body.get('force', False)))
|
||||
resp = dict(queued, ok=bool(queued.get('queued')),
|
||||
load_file_aid=loadfile_aid,
|
||||
module_aid=module_aid, apdus=len(seq))
|
||||
if queued.get('queued'):
|
||||
resp['note'] = ('queued as the SCP81 command script; '
|
||||
'runs on the card next POST (push/trigger)')
|
||||
except Exception as e:
|
||||
resp = {'ok': False, 'error': 'cap parse failed: %s' % e}
|
||||
try:
|
||||
resp = _scp81_gen_install(body)
|
||||
except Exception as e:
|
||||
resp = {'ok': False, 'error': str(e)}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/scp81/log-clear':
|
||||
|
||||
Reference in New Issue
Block a user