Merge pysim-otaman-server into otaman (monorepo, v1.7.0)
- Move PWA into frontend/; server package at pysim_otaman_server/ - Server now serves the PWA (same origin -> no CORS/PNA): static file handler + --web-dir flag + injects window.PYSIM_EMBEDDED marker into index.html - Frontend defaults to relative API base when embedded (pysimBase = '') - Version 1.7.0 aligned: server.py VERSION, pyproject.toml, PWA header - SW cache bump otaman-v7 -> otaman-v8 - Docs: combined README + docs/api.md endpoint reference; update links - Fix stale package.json repository URL
@@ -1 +1,6 @@
|
||||
node_modules/
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
# OTAMan — APDU Helper & Secured Packet Builder, SIM OTA in PWA
|
||||
# OTAMan — SIM OTA toolkit: PWA + local card server
|
||||
|
||||
Standalone offline HTML/JS tool for building APDU commands for SIM, USIM, and GlobalPlatform RAM, assembling secure packets per ETSI TS 102 225, and constructing BER-TLV command scripts per ETSI TS 102 226.
|
||||
OTAMan is an offline HTML/JS PWA for building APDU commands (SIM, USIM, GlobalPlatform RAM), assembling SCP80 secured packets per ETSI TS 102 225, and constructing BER-TLV command scripts per ETSI TS 102 226. A bundled [`pysim-otaman-server`](pysim_otaman_server/) exposes a local HTTP API over pySim for live card operations: file manager, raw APDU, SIM Toolkit menu browsing, and OTA (SCP80) delivery.
|
||||
|
||||
Open `index.html` in any modern browser. No server required.
|
||||
**Demo:** [otaman.atroshin.ru](https://otaman.atroshin.ru) — the PWA alone, for experimenting. Install the server (below) for card-reader functions.
|
||||
|
||||
**Demo:** [otaman.atroshin.ru](https://otaman.atroshin.ru)
|
||||
## Quick start
|
||||
|
||||
**PWA only (client-side tools):** open `frontend/index.html` in any browser, or serve `frontend/` with any static server. No Python required.
|
||||
|
||||
**Full (PWA + card server):**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/anttro/otaman.git
|
||||
cd otaman
|
||||
./setup.sh # or setup.bat on Windows — creates .venv, installs pysim + server
|
||||
./start.sh # or start.bat — starts the server (it serves the PWA too)
|
||||
```
|
||||
|
||||
Then open http://127.0.0.1:8080 — the UI and API share one origin, so no CORS or browser-permission setup is needed.
|
||||
|
||||
## Build
|
||||
|
||||
Tailwind CSS is used for styling. After cloning, rebuild the CSS:
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Interface
|
||||
|
||||
Six tabs, each with a form and a "Сгенерировать" button.
|
||||
Five tabs, each with a form and a "Generate" button.
|
||||
|
||||
---
|
||||
|
||||
@@ -383,7 +397,7 @@ Swaps nibble pairs of an even-length hex string.
|
||||
|
||||
## Card Reader (pySim integration)
|
||||
|
||||
Connects to a local [pysim-otaman-server](https://github.com/anttro/pysim-otaman-server) for live card operations.
|
||||
Connects to the bundled [`pysim-otaman-server`](pysim_otaman_server/) for live card operations.
|
||||
|
||||
> **Browser restriction:** when the PWA is served from a public HTTPS host, reaching the local server (`http://127.0.0.1:8080`) requires two things: the server must send `Access-Control-Allow-Private-Network: true` (pysim-otaman-server ≥ 1.6.1 does this automatically), and the browser must be allowed to access the local network — in Chrome/Edge/Vivaldi: Site settings → Local network access → allow the site (or accept the permission prompt). Without the browser permission, the request to `127.0.0.1` is blocked before any preflight is sent.
|
||||
|
||||
@@ -459,6 +473,60 @@ OTAMan is a Progressive Web App and can be installed for offline use. Use the **
|
||||
- Service worker pre-caches all assets on first visit
|
||||
- App icons at 192×192 and 512×512
|
||||
|
||||
## Server (pysim-otaman-server)
|
||||
|
||||
The bundled Python server wraps [pySim](https://osmocom.org/projects/pysim/wiki) and serves both the OTAMan PWA (from `frontend/`) and a JSON API under `/api/*`.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Python 3.8+** with `pip`, and **Git**
|
||||
- **Smart card reader** (PC/SC or serial/FTDI) — PC/SC is preferable (`pcsc-lite` + `ccid` on Linux)
|
||||
- **Windows** — use Python 3.10–3.13 (3.13 recommended): `pyscard` ships precompiled wheels for these versions. On 3.9 / 3.14 it builds from source (needs MSVC C++ Build Tools). The SMPP bridge (`smpp.twisted3`) is intentionally skipped on Windows.
|
||||
|
||||
### Scripts
|
||||
|
||||
| Script | What it does |
|
||||
|--------|-------------|
|
||||
| `setup.sh` / `setup.bat` | Creates `.venv/`, installs pysim and the server. Run once after cloning. |
|
||||
| `start.sh` / `start.bat` | Starts the server from the venv (serves the PWA + API on `:8080`). |
|
||||
|
||||
`start.sh` auto-detects the reader (PC/SC if `pcscd` is running, else `/dev/ttyUSB0`); `start.bat` always uses `-p 0` (PC/SC is built into Windows). If no reader is found the server still starts ("Reader: none") — initialize the card later via the **Equip** button.
|
||||
|
||||
### Manual installation
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate # Linux/macOS (Windows: .venv\Scripts\activate)
|
||||
pip install git+https://github.com/osmocom/pysim.git
|
||||
pip install -e . # editable — serves frontend/ from the source tree
|
||||
pysim-otaman-server --http-port 8080
|
||||
```
|
||||
|
||||
### CLI options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--http-host` | Bind address (default: `127.0.0.1`) |
|
||||
| `--http-port` | TCP port (default: `8080`) |
|
||||
| `--web-dir` | Directory with PWA static files (default: `<repo>/frontend`) |
|
||||
| `-p` / `--pcsc-device` | PC/SC reader slot number |
|
||||
| `-d` / `--device` | Serial device path |
|
||||
| `--no-card-init` | Skip card init to preserve the CAT session (no file manager) |
|
||||
| `--apdu-trace` | Log APDU-level traces to stderr |
|
||||
| `--log-requests` | Log request/response payloads to stderr |
|
||||
| `--sms-oa` / `--sms-sm-sc` | SMS-DELIVER originating address / SM-SC for PoR-in-submit |
|
||||
| `--terminal-profile` | TERMINAL PROFILE payload hex (default 10-byte GSM profile) |
|
||||
| `--poll-interval` | Idle interval before automatic STATUS polling (default 30s) |
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- **"Failed to establish context: Access denied"** — `pcscd` isn't running or the user lacks permission: `sudo systemctl enable --now pcscd && sudo usermod -a -G pcscd $USER`.
|
||||
- **"device file /dev/ttyUSB0 does not exist"** — no serial reader; connect a USB reader or pass `-d` explicitly. The server still starts without a reader.
|
||||
|
||||
### API reference
|
||||
|
||||
See [docs/api.md](docs/api.md) for the full endpoint reference.
|
||||
|
||||
## Theme
|
||||
|
||||
Dark theme is supported. The app follows the OS preference on first visit, and a manual toggle button (🌙/☀️) at the top-right corner persists the choice in `localStorage`.
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
# OTAMan — APDU Helper & Secured Packet Builder, SIM OTA в PWA
|
||||
# OTAMan — SIM OTA toolkit: PWA + локальный сервер карт
|
||||
|
||||
Автономный offline-инструмент на HTML/JS для создания APDU-команд для SIM, USIM и GlobalPlatform RAM, сборки защищённых пакетов по ETSI TS 102 225 и построения BER-TLV скриптов по ETSI TS 102 226.
|
||||
OTAMan — автономный offline-PWA (HTML/JS) для создания APDU-команд (SIM, USIM, GlobalPlatform RAM), сборки защищённых пакетов SCP80 по ETSI TS 102 225 и построения BER-TLV скриптов по ETSI TS 102 226. В комплекте — [`pysim-otaman-server`](pysim_otaman_server/) — локальный HTTP-сервер поверх pySim для работы с картой: файловый менеджер, сырые APDU, меню SIM Toolkit и доставка OTA.
|
||||
|
||||
Откройте `index.html` в любом современном браузере. Сервер не требуется.
|
||||
**Демо:** [otaman.atroshin.ru](https://otaman.atroshin.ru) — только PWA, для экспериментов. Для функций картридера установите сервер (ниже).
|
||||
|
||||
**Демо:** [otaman.atroshin.ru](https://otaman.atroshin.ru)
|
||||
## Быстрый старт
|
||||
|
||||
**Только PWA (клиентские функции):** откройте `frontend/index.html` в любом браузере или раздайте `frontend/` любым статическим сервером. Python не нужен.
|
||||
|
||||
**Полная установка (PWA + сервер карт):**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/anttro/otaman.git
|
||||
cd otaman
|
||||
./setup.sh # или setup.bat в Windows — создаёт .venv, ставит pysim + сервер
|
||||
./start.sh # или start.bat — запускает сервер (он же раздаёт PWA)
|
||||
```
|
||||
|
||||
Затем откройте http://127.0.0.1:8080 — интерфейс и API на одном origin, поэтому CORS и разрешения браузера не нужны.
|
||||
|
||||
## Сборка
|
||||
|
||||
Для стилей используется Tailwind CSS. После клонирования пересоберите CSS:
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Интерфейс
|
||||
|
||||
Шесть вкладок, каждая с формой и кнопкой «Generate APDU».
|
||||
Пять вкладок, каждая с формой и кнопкой «Generate APDU».
|
||||
|
||||
---
|
||||
|
||||
@@ -382,7 +396,7 @@ PWA проверяет версию сервера при подключении
|
||||
|
||||
## Card Reader (интеграция с pySim)
|
||||
|
||||
Подключение к локальному [pysim-otaman-server](https://github.com/anttro/pysim-otaman-server) для работы с картой.
|
||||
Подключение к встроенному [`pysim-otaman-server`](pysim_otaman_server/) для работы с картой.
|
||||
|
||||
> **Ограничение браузера:** если PWA раздаётся с публичного HTTPS-хоста, для доступа к локальному серверу (`http://127.0.0.1:8080`) нужны два условия: сервер должен отправлять `Access-Control-Allow-Private-Network: true` (pysim-otaman-server ≥ 1.6.1 делает это автоматически), и браузеру должно быть разрешено обращаться к локальной сети — в Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → разрешить сайт (или подтвердить запрос). Без разрешения браузера запрос к `127.0.0.1` блокируется ещё до отправки preflight.
|
||||
|
||||
@@ -408,3 +422,55 @@ PWA проверяет версию сервера при подключении
|
||||
### Подсказки команд
|
||||
|
||||
Введите имя команды в **pySim command line**. Подсказки по использованию появляются через 300 мс. Автодополнение команд — над полем ввода.
|
||||
|
||||
## Сервер (pysim-otaman-server)
|
||||
|
||||
Встроенный Python-сервер оборачивает [pySim](https://osmocom.org/projects/pysim/wiki) и раздаёт как PWA (из `frontend/`), так и JSON API по `/api/*`.
|
||||
|
||||
### Требования
|
||||
|
||||
- **Python 3.8+** с `pip`, и **Git**
|
||||
- **Смарт-картридер** (PC/SC или serial/FTDI) — предпочтителен PC/SC (`pcsc-lite` + `ccid` на Linux)
|
||||
- **Windows** — используйте Python 3.10–3.13 (рекомендуется 3.13): у `pyscard` есть готовые wheel. На 3.9 / 3.14 он собирается из исходников (нужны MSVC C++ Build Tools). SMPP-мост (`smpp.twisted3`) в Windows сознательно не ставится.
|
||||
|
||||
### Скрипты
|
||||
|
||||
| Скрипт | Назначение |
|
||||
|--------|-------------|
|
||||
| `setup.sh` / `setup.bat` | Создаёт `.venv/`, ставит pysim и сервер. Запускать один раз после клонирования. |
|
||||
| `start.sh` / `start.bat` | Запускает сервер из venv (раздаёт PWA + API на `:8080`). |
|
||||
|
||||
`start.sh` автоопределяет ридер (PC/SC при работающем `pcscd`, иначе `/dev/ttyUSB0`); `start.bat` всегда использует `-p 0`. Без ридера сервер всё равно стартует («Reader: none») — карту можно инициализировать позже кнопкой **Equip**.
|
||||
|
||||
### Ручная установка
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate # Linux/macOS (Windows: .venv\Scripts\activate)
|
||||
pip install git+https://github.com/osmocom/pysim.git
|
||||
pip install -e . # editable — раздаёт frontend/ из исходного дерева
|
||||
pysim-otaman-server --http-port 8080
|
||||
```
|
||||
|
||||
### Параметры CLI
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|-------------|
|
||||
| `--http-host` | Адрес привязки (по умолчанию `127.0.0.1`) |
|
||||
| `--http-port` | Порт (по умолчанию `8080`) |
|
||||
| `--web-dir` | Каталог со статикой PWA (по умолчанию `<repo>/frontend`) |
|
||||
| `-p` / `--pcsc-device` | Номер слота PC/SC |
|
||||
| `-d` / `--device` | Путь к serial-устройству |
|
||||
| `--no-card-init` | Пропустить инициализацию карты (сохранить CAT-сессию) |
|
||||
| `--apdu-trace` | Лог APDU-трафика в stderr |
|
||||
| `--log-requests` | Лог запросов/ответов в stderr |
|
||||
| `--poll-interval` | Интервал автоопроса STATUS (по умолчанию 30с) |
|
||||
|
||||
### Устранение неполадок
|
||||
|
||||
- **"Failed to establish context: Access denied"** — `pcscd` не запущен или нет прав: `sudo systemctl enable --now pcscd && sudo usermod -a -G pcscd $USER`.
|
||||
- **"device file /dev/ttyUSB0 does not exist"** — нет serial-ридера; подключите USB-ридер или укажите `-d`. Сервер всё равно стартует без ридера.
|
||||
|
||||
### Справочник API
|
||||
|
||||
Полный справочник endpoints: [docs/api.md](docs/api.md).
|
||||
@@ -0,0 +1,268 @@
|
||||
# pysim-otaman-server — HTTP API reference
|
||||
|
||||
The server exposes a JSON HTTP API under `/api/*`. All responses carry
|
||||
`Access-Control-Allow-Origin: *` (plus `Access-Control-Allow-Private-Network: true`
|
||||
on the preflight), so the API is reachable from a separately-hosted PWA.
|
||||
|
||||
## Version compatibility
|
||||
|
||||
| Server | PWA (OTAMan) | Status |
|
||||
|--------|-------------|--------|
|
||||
| 1.x.x | 1.x.x | ✅ Compatible |
|
||||
| 0.x.x | 1.x.x | ❌ Outdated — update server |
|
||||
| 2.x.x+ | 1.x.x | ⚠️ Server newer — update PWA |
|
||||
|
||||
The server reports its version via `GET /api/version`. The PWA checks this on
|
||||
connect and warns if versions are incompatible.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/api/version` | GET | Server version string |
|
||||
| `/api/status` | GET | Card reader + card info + current selection |
|
||||
| `/api/command` | POST | pySim command (equip, status, tree, etc.) |
|
||||
| `/api/commands` | GET | List available pySim commands |
|
||||
| `/api/tree` | POST | File tree browser for given FID/name |
|
||||
| `/api/select` | POST | Select a file by name or FID |
|
||||
| `/api/read` | POST | Read file content |
|
||||
| `/api/write` | POST | Write raw hex data to a file |
|
||||
| `/api/apdu` | POST | Raw APDU send |
|
||||
| `/api/help` | POST | pySim help for a given command |
|
||||
| `/api/send-ota` | POST | SCP80 OTA secured packet delivery |
|
||||
| `/api/sp-verify` | POST | Verify secured packet against pySim reference |
|
||||
| `/api/menu` | GET | Current STK menu (title + items + active) |
|
||||
| `/api/menu-select` | POST | ENVELOPE(Menu Selection) with item_id |
|
||||
| `/api/menu-respond` | POST | TERMINAL RESPONSE for paused STK command |
|
||||
| `/api/stk-status` | GET | STK session state (active/pending/type) |
|
||||
| `/api/events` | GET | Event list from SET UP EVENT LIST |
|
||||
| `/api/event-send` | POST | Send ENVELOPE(Event Download) |
|
||||
| `/api/proactive-log` | GET | Last 50 proactive commands |
|
||||
| `/api/status-poll` | POST | Manual STATUS poll + FETCH if 91XX |
|
||||
| `/api/rescue` | POST | Re-send TERMINAL PROFILE to recover CAT session |
|
||||
| `/api/poll-status` | GET | Background STATUS polling state |
|
||||
| `/api/poll-toggle` | POST | Enable/disable background polling |
|
||||
| `/api/pli-qualifiers` | GET | List of qualifier codes with descriptions |
|
||||
| `/api/pli-dict` | GET | Current dictionary (hex values per qualifier) |
|
||||
| `/api/pli-dict` | POST | Update dictionary entries |
|
||||
|
||||
## Endpoint details
|
||||
|
||||
### `GET /api/version`
|
||||
|
||||
Returns server version for compatibility checking.
|
||||
|
||||
**Example response:**
|
||||
```json
|
||||
{"version": "1.7.0"}
|
||||
```
|
||||
|
||||
### `GET /api/status`
|
||||
|
||||
Card reader, card type, current selection, and card state.
|
||||
|
||||
### `GET /api/commands`
|
||||
|
||||
List all available shell commands for the current card profile.
|
||||
|
||||
### `POST /api/command`
|
||||
|
||||
Execute any pysim-shell command.
|
||||
|
||||
```json
|
||||
{"cmd": "select MF"}
|
||||
```
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{"output": "..."}
|
||||
```
|
||||
|
||||
### `POST /api/apdu`
|
||||
|
||||
Send a raw APDU to the card.
|
||||
|
||||
```json
|
||||
{"apdu": "00A4040000..."}
|
||||
```
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{"response": "...", "sw": "9000"}
|
||||
```
|
||||
|
||||
### `POST /api/help`
|
||||
|
||||
Get structured help for a shell command.
|
||||
|
||||
```json
|
||||
{"cmd": "apdu"}
|
||||
```
|
||||
|
||||
Returns:
|
||||
```json
|
||||
{"usage": "apdu [-h] [--expect-sw EXPECT_SW] [--raw] APDU", "description": "...", "args": [{"name": "APDU", "type": "positional", "help": "..."}]}
|
||||
```
|
||||
|
||||
### `POST /api/send-ota`
|
||||
|
||||
Send an OTA command (SCP80) to the card via SMS-PP-DOWNLOAD ENVELOPE.
|
||||
The secured packet is delivered in an SMS-DELIVER TPDU wrapped in an ENVELOPE command.
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"sp": "00201516011515b00000...",
|
||||
"spi1": "16",
|
||||
"spi2": "01",
|
||||
"kic": "15",
|
||||
"kid": "15",
|
||||
"tar": "b00000",
|
||||
"cntr": "0000000001",
|
||||
"kicKey": "D6FCC023...",
|
||||
"kidKey": "1B07E7E0..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response (delivery PoR):**
|
||||
```json
|
||||
{"success": true, "sw": "9000", "response_data": "027100000e0a...",
|
||||
"por": {"response_status": "por_ok", "tar": "B00000", "pcntr": 0,
|
||||
"decoded": {"number_of_commands": 1, "last_status_word": "6e00",
|
||||
"last_response_data": ""}}}
|
||||
```
|
||||
|
||||
**Response (submit PoR):** PoR is extracted from the SMS-SUBMIT TPDU
|
||||
fetched via a proactive command (FETCH). The response contains the
|
||||
same `por` structure if decoding succeeds.
|
||||
|
||||
The SPI2 `por_in_submit` bit (0x20) selects submit-mode PoR.
|
||||
|
||||
### `POST /api/sp-verify`
|
||||
|
||||
Cross-check a secured packet against pySim's `OtaDialectSms.encode_cmd`
|
||||
reference. Returns the JS-generated packet, pySim reference, a match flag,
|
||||
and the decoded SPI fields.
|
||||
|
||||
```json
|
||||
{"spi1": "16", "spi2": "01", "kic": "15", "kid": "15", "tar": "b00000",
|
||||
"cntr": "0000000001", "apdu": "00a40000023f00",
|
||||
"kicKey": "D6FCC023...", "kidKey": "1B07E7E0..."}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{"js_sp": "...", "py_sp": "...", "match": true,
|
||||
"diffs": [], "spi": {"counter": "counter_must_be_higher", ...}}
|
||||
```
|
||||
|
||||
### `GET /api/menu`
|
||||
|
||||
Returns the SIM Toolkit SETUP MENU captured from the card's TERMINAL PROFILE
|
||||
response at startup. Empty `{"items": []}` if the card didn't send a menu.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{"command_number": 1, "items": [{"id": 128, "text": "Настройки/Settings"}],
|
||||
"title": "Alfa Mobile", "active": false}
|
||||
```
|
||||
|
||||
### `POST /api/menu-select`
|
||||
|
||||
Sends an `ENVELOPE(MENU SELECTION)` with the selected item ID, then handles
|
||||
the card's proactive response (DISPLAY TEXT or SELECT ITEM).
|
||||
|
||||
```json
|
||||
{"item_id": 128}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{"type": "display_text", "text": "Hello", "sw": "9122"}
|
||||
```
|
||||
or
|
||||
```json
|
||||
{"type": "select_item", "items": [{"id": 1, "text": "Sub-menu"}], "sw": "9122"}
|
||||
```
|
||||
|
||||
### `POST /api/menu-respond`
|
||||
|
||||
Sends `TERMINAL RESPONSE` to the current proactive command with the given result
|
||||
code. Continues the proactive chain if the card responds with `91XX`.
|
||||
|
||||
```json
|
||||
{"result": "ok", "item_id": 1}
|
||||
```
|
||||
|
||||
| `result` | TERMINAL RESPONSE code | Meaning |
|
||||
|---|---|---|
|
||||
| `ok` | `0x00` | Command performed successfully |
|
||||
| `back` | `0x12` | Backward move requested |
|
||||
| `cancel` | `0x10` | Proactive session terminated |
|
||||
| `timeout` | `0x11` | No response from user |
|
||||
|
||||
### `GET /api/stk-status`
|
||||
|
||||
Returns the current STK session state.
|
||||
```json
|
||||
{"active": true, "pending": true, "pending_type": "select_item"}
|
||||
```
|
||||
|
||||
### `POST /api/read`
|
||||
|
||||
Read file content. Auto-detects transparent vs record files.
|
||||
|
||||
```json
|
||||
{"name": "EF.ICCID", "fid": "2FE2", "parent_sel": "3F00", "mode": "raw"}
|
||||
```
|
||||
|
||||
Returns:
|
||||
```json
|
||||
{"success": true, "sw": "9000", "file_type": "transparent", "data": "..."}
|
||||
```
|
||||
|
||||
### `POST /api/write`
|
||||
|
||||
Write raw hex data to a file.
|
||||
|
||||
```json
|
||||
{"name": "EF.ICCID", "fid": "2FE2", "data": "A0A1A2...", "parent_sel": "3F00"}
|
||||
```
|
||||
|
||||
For record files:
|
||||
```json
|
||||
{"name": "EF.ADN", "fid": "6F3A", "data": "A0A1...", "record_nr": 1, "parent_sel": "7F10"}
|
||||
```
|
||||
|
||||
Returns:
|
||||
```json
|
||||
{"success": true, "sw": "9000"}
|
||||
```
|
||||
|
||||
### `POST /api/select`
|
||||
|
||||
Select a file by name or FID, with optional parent selection.
|
||||
|
||||
```json
|
||||
{"name": "EF.ICCID", "fid": "2FE2", "parent_sel": "3F00"}
|
||||
```
|
||||
|
||||
Returns:
|
||||
```json
|
||||
{"name": "EF.ICCID", "fid": "2FE2", "file_type": "transparent", "exists": true}
|
||||
```
|
||||
|
||||
### `POST /api/tree`
|
||||
|
||||
Get directory listing with typed children.
|
||||
|
||||
```json
|
||||
{"name": "MF", "fid": "3F00"}
|
||||
```
|
||||
|
||||
Returns:
|
||||
```json
|
||||
{"exists": true, "name": "MF", "fid": "3F00", "file_type": "df", "children": [{"name": "EF.ICCID", "fid": "2fe2", "isDir": false}]}
|
||||
```
|
||||
@@ -236,7 +236,7 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Вкладка Card reader (pySim)</h2>
|
||||
<p class="mb-3">Подключение к локальному <a href="https://github.com/anttro/pysim-otaman-server" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> для работы с картой. Подвкладки: <strong>File manager</strong>, <strong>Custom files</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong> и <strong>Proactive UICC</strong>.</p>
|
||||
<p class="mb-3">Подключение к локальному <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> для работы с картой. Подвкладки: <strong>File manager</strong>, <strong>Custom files</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong> и <strong>Proactive UICC</strong>.</p>
|
||||
|
||||
<h3 id="file-manager" class="text-lg font-medium mb-2">6.1 File manager</h3>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
@@ -322,7 +322,7 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">7. Установка сервера</h2>
|
||||
<p class="mb-3">Для работы с картой (вкладка Card reader, Proactive UICC, доставка OTA) нужен локальный <a href="https://github.com/anttro/pysim-otaman-server" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> — небольшой HTTP-сервер, оборачивающий pySim и взаимодействующий с ридером через PC/SC или serial.</p>
|
||||
<p class="mb-3">Для работы с картой (вкладка Card reader, Proactive UICC, доставка OTA) нужен локальный <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> — встроенный в OTAMan HTTP-сервер, оборачивающий pySim, работающий с ридером через PC/SC или serial и раздающий сам PWA (откройте <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
|
||||
|
||||
<h3 id="prerequisites" class="text-lg font-medium mb-2">7.1 Требования</h3>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
@@ -333,17 +333,17 @@
|
||||
</ul>
|
||||
|
||||
<h3 id="quickstart-linux" class="text-lg font-medium mb-2">7.2 Быстрый старт — Linux / macOS</h3>
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/pysim-otaman-server.git
|
||||
cd pysim-otaman-server
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
|
||||
cd otaman
|
||||
chmod +x setup.sh start.sh
|
||||
./setup.sh # создаёт .venv, устанавливает pysim + сервер (однократно)
|
||||
./start.sh # запускает сервер (автоопределение ридера)</pre>
|
||||
./start.sh # запускает сервер (PWA + API, автоопределение ридера)</pre>
|
||||
|
||||
<h3 id="quickstart-windows" class="text-lg font-medium mb-2">7.3 Быстрый старт — Windows</h3>
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/pysim-otaman-server.git
|
||||
cd pysim-otaman-server
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
|
||||
cd otaman
|
||||
setup.bat # создаёт .venv, устанавливает pysim + сервер (однократно)
|
||||
start.bat # запускает сервер</pre>
|
||||
start.bat # запускает сервер (PWA + API)</pre>
|
||||
|
||||
<h3 id="helper-scripts" class="text-lg font-medium mb-2">7.4 Вспомогательные скрипты</h3>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
@@ -369,14 +369,14 @@ source .venv/bin/activate # Linux/macOS
|
||||
# .venv\Scripts\activate # Windows
|
||||
|
||||
# Установить pysim
|
||||
pip install git+https://gitea.osmocom.org/sim-card/pysim.git
|
||||
pip install git+https://github.com/osmocom/pysim.git
|
||||
|
||||
# Установить pysim-otaman-server
|
||||
pip install .
|
||||
# Установить pysim-otaman-server (editable — раздаёт встроенный PWA)
|
||||
pip install -e .
|
||||
|
||||
# Запустить сервер
|
||||
# Запустить сервер (PWA + API)
|
||||
pysim-otaman-server --http-port 8080</pre>
|
||||
<p class="text-sm mb-3">Подключите PC/SC-ридер с SIM-картой и укажите в PWA адрес <code class="font-mono text-sm">http://127.0.0.1:8080</code>.</p>
|
||||
<p class="text-sm mb-3">Подключите PC/SC-ридер с SIM-картой и откройте <code class="font-mono text-sm">http://127.0.0.1:8080</code> — PWA и API на одном origin, поэтому CORS не требуется.</p>
|
||||
<p class="text-sm mb-3">Если PWA раздаётся с публичного HTTPS-хоста (например, <code class="font-mono text-sm">https://otaman.example.com</code>), для доступа к локальному серверу карт нужны два условия: (1) сервер отвечает на preflight заголовком <code class="font-mono text-sm">Access-Control-Allow-Private-Network: true</code> (pysim-otaman-server ≥ 1.6.1 делает это автоматически), и (2) браузеру должно быть разрешено обращаться к локальной сети — в Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → разрешить сайт (или подтвердить запрос). Без разрешения браузера запрос к <code class="font-mono text-sm">127.0.0.1</code> блокируется ещё до отправки preflight.</p>
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Card reader (pySim) tab</h2>
|
||||
<p class="mb-3">Connects to a local <a href="https://github.com/anttro/pysim-otaman-server" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> for live card operations. Sub-tabs: <strong>File manager</strong>, <strong>Custom files</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong>, and <strong>Proactive UICC</strong>.</p>
|
||||
<p class="mb-3">Connects to a local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> for live card operations. Sub-tabs: <strong>File manager</strong>, <strong>Custom files</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong>, and <strong>Proactive UICC</strong>.</p>
|
||||
|
||||
<h3 id="file-manager" class="text-lg font-medium mb-2">6.1 File manager</h3>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
@@ -322,7 +322,7 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">7. Server installation</h2>
|
||||
<p class="mb-3">Live card operations (Card reader tab, Proactive UICC, OTA delivery) require the local <a href="https://github.com/anttro/pysim-otaman-server" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> — a small HTTP server that wraps pySim and talks to the reader over PC/SC or serial.</p>
|
||||
<p class="mb-3">Live card operations (Card reader tab, Proactive UICC, OTA delivery) require the local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> — a small HTTP server bundled with OTAMan that wraps pySim, talks to the reader over PC/SC or serial, and also serves the PWA itself (open <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
|
||||
|
||||
<h3 id="prerequisites" class="text-lg font-medium mb-2">7.1 Prerequisites</h3>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
@@ -333,17 +333,17 @@
|
||||
</ul>
|
||||
|
||||
<h3 id="quickstart-linux" class="text-lg font-medium mb-2">7.2 Quick start — Linux / macOS</h3>
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/pysim-otaman-server.git
|
||||
cd pysim-otaman-server
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
|
||||
cd otaman
|
||||
chmod +x setup.sh start.sh
|
||||
./setup.sh # creates .venv, installs pysim + server (run once)
|
||||
./start.sh # starts the server (auto-detects reader)</pre>
|
||||
./start.sh # starts the server (serves PWA + API, auto-detects reader)</pre>
|
||||
|
||||
<h3 id="quickstart-windows" class="text-lg font-medium mb-2">7.3 Quick start — Windows</h3>
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/pysim-otaman-server.git
|
||||
cd pysim-otaman-server
|
||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
|
||||
cd otaman
|
||||
setup.bat # creates .venv, installs pysim + server (run once)
|
||||
start.bat # starts the server</pre>
|
||||
start.bat # starts the server (serves PWA + API)</pre>
|
||||
|
||||
<h3 id="helper-scripts" class="text-lg font-medium mb-2">7.4 Helper scripts</h3>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
@@ -369,14 +369,14 @@ source .venv/bin/activate # Linux/macOS
|
||||
# .venv\Scripts\activate # Windows
|
||||
|
||||
# Install pysim
|
||||
pip install git+https://gitea.osmocom.org/sim-card/pysim.git
|
||||
pip install git+https://github.com/osmocom/pysim.git
|
||||
|
||||
# Install pysim-otaman-server
|
||||
pip install .
|
||||
# Install pysim-otaman-server (editable, so it serves the bundled PWA)
|
||||
pip install -e .
|
||||
|
||||
# Start the server
|
||||
# Start the server (serves PWA + API)
|
||||
pysim-otaman-server --http-port 8080</pre>
|
||||
<p class="text-sm mb-3">Connect a PC/SC reader with a SIM card, then point the PWA at <code class="font-mono text-sm">http://127.0.0.1:8080</code>.</p>
|
||||
<p class="text-sm mb-3">Connect a PC/SC reader with a SIM card, then open <code class="font-mono text-sm">http://127.0.0.1:8080</code> — the PWA and API share one origin, so no CORS is involved.</p>
|
||||
<p class="text-sm mb-3">If the PWA is served from a public HTTPS host (e.g. <code class="font-mono text-sm">https://otaman.example.com</code>), two things are required to reach a local card server: (1) the server must answer the preflight with <code class="font-mono text-sm">Access-Control-Allow-Private-Network: true</code> (pysim-otaman-server ≥ 1.6.1 does this automatically), and (2) the browser must be allowed to access the local network — in Chrome/Edge/Vivaldi: Site settings → Local network access → allow the site (or accept the permission prompt). Without the browser permission, the request to <code class="font-mono text-sm">127.0.0.1</code> is blocked before any preflight is sent.</p>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -17,7 +17,7 @@
|
||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.6.1</span></h1>
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.7.0</span></h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button>
|
||||
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
|
||||
@@ -846,7 +846,7 @@
|
||||
</div>
|
||||
<div id="pysim-connect-info" class="mb-3 text-xs text-gray-500 dark:text-slate-400 leading-relaxed">
|
||||
<p data-l10n="To access local card reader, you need pysim-otaman-server running locally.">To access local card reader, you need pysim-otaman-server running locally.</p>
|
||||
<p><span data-l10n="To connect, install">To connect, install</span> <a href="https://github.com/anttro/pysim-otaman-server" target="_blank" class="text-blue-600 dark:text-blue-400 underline">pysim-otaman-server</a>: <code class="font-mono bg-gray-100 dark:bg-slate-700 px-1 rounded">git clone https://github.com/anttro/pysim-otaman-server.git</code> <span data-l10n="and start it.">and start it.</span><br><span data-l10n="Refer to pysim-otaman-server README files for details.">Refer to pysim-otaman-server README files for details.</span> <span data-l10n="Click">Click</span> <b data-l10n="Connect">Connect</b> <span data-l10n="when pysim-otaman-server is ready">when pysim-otaman-server is ready</span></p>
|
||||
<p><span data-l10n="To connect, install">To connect, install</span> <a href="https://github.com/anttro/otaman" target="_blank" class="text-blue-600 dark:text-blue-400 underline">pysim-otaman-server</a>: <code class="font-mono bg-gray-100 dark:bg-slate-700 px-1 rounded">git clone https://github.com/anttro/otaman.git</code> <span data-l10n="and start it.">and start it.</span><br><span data-l10n="Refer to pysim-otaman-server README files for details.">Refer to pysim-otaman-server README files for details.</span> <span data-l10n="Click">Click</span> <b data-l10n="Connect">Connect</b> <span data-l10n="when pysim-otaman-server is ready">when pysim-otaman-server is ready</span></p>
|
||||
<div id="pysim-connect-msg" class="text-xs"></div>
|
||||
</div>
|
||||
<div id="pysim-connected-row" class="mb-3 hidden">
|
||||
@@ -2635,7 +2635,13 @@ function decodeResponse() {
|
||||
}
|
||||
|
||||
// ===== pysim integration =====
|
||||
let pysimBase = 'http://127.0.0.1:8080';
|
||||
// When served by pysim-otaman-server itself (same origin), the API is relative;
|
||||
// otherwise default to the local server on 127.0.0.1:8080.
|
||||
let pysimBase = window.PYSIM_EMBEDDED ? '' : 'http://127.0.0.1:8080';
|
||||
if (window.PYSIM_EMBEDDED) {
|
||||
const urlEl = document.getElementById('pysim-url');
|
||||
if (urlEl) { urlEl.value = ''; urlEl.placeholder = '(same origin)'; }
|
||||
}
|
||||
|
||||
function esc(str) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitea.atroshin.ru/catarrh/otaman.git"
|
||||
"url": "https://github.com/anttro/otaman.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"type": "commonjs",
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v7';
|
||||
const CACHE = 'otaman-v8';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
@@ -0,0 +1,15 @@
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-otaman-server"
|
||||
version = "1.7.0"
|
||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
# Declaring it here would make pip resolve its full tree (including the SMPP
|
||||
# bridge -> Twisted -> twisted-iocpsupport, which needs MSVC on Windows).
|
||||
|
||||
[project.scripts]
|
||||
pysim-otaman-server = "pysim_otaman_server.__main__:main"
|
||||
@@ -0,0 +1,154 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from http.server import HTTPServer
|
||||
from pySim.card_handler import CardHandler
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.log import PySimLogger
|
||||
from pySim.cards import UiccCardBase
|
||||
|
||||
from .shell import load_pysim_app
|
||||
from .server import PysimHandler, StderrApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session
|
||||
|
||||
|
||||
_server_start = 0
|
||||
|
||||
def _log_stdout(msg):
|
||||
elapsed = time.time() - _server_start
|
||||
os.write(1, ('[%8.3f] %s\n' % (elapsed, msg)).encode())
|
||||
|
||||
|
||||
def _default_web_dir():
|
||||
# <repo>/frontend, whether run from source or an editable install.
|
||||
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'frontend')
|
||||
|
||||
|
||||
def main():
|
||||
global _server_start
|
||||
_server_start = time.time()
|
||||
mod = load_pysim_app()
|
||||
parser = mod.option_parser
|
||||
parser.description = 'pysim-otaman-server — HTTP API for pysim'
|
||||
parser.add_argument('--http-host', default='127.0.0.1', help='Bind address (default: 127.0.0.1)')
|
||||
parser.add_argument('--http-port', type=int, default=8080, help='Bind port (default: 8080)')
|
||||
parser.add_argument('--web-dir', default=_default_web_dir(), metavar='PATH',
|
||||
help='Directory with the OTAMan PWA static files to serve (default: <repo>/frontend)')
|
||||
parser.add_argument('--log-requests', action='store_true', default=False, help='Log request/response payloads to stderr')
|
||||
parser.add_argument('--sms-oa', default='12345', metavar='DIGITS',
|
||||
help='TP-Originating-Address (SMSC number) for the SMS-DELIVER TPDU (default: 12345)')
|
||||
parser.add_argument('--sms-sm-sc', default='12345678912', metavar='DIGITS',
|
||||
help='SM-SC address for SMS-SUBMIT routing in PoR-in-submit mode (default: 12345678912)')
|
||||
parser.add_argument('--terminal-profile', default='7FFFFFFFFF0000CF02', metavar='HEX',
|
||||
help='TERMINAL PROFILE payload (default: 10-byte profile with SMS-PP download and event list)')
|
||||
parser.add_argument('--poll-interval', type=int, default=30, metavar='SECS',
|
||||
help='Idle interval before automatic STATUS polling (1-255 seconds, default: 30). Disable with --poll-interval 0')
|
||||
parser.add_argument('--no-card-init', action='store_true', default=False,
|
||||
help='Skip pysim card initialization (preserve CAT session — no file manager)')
|
||||
|
||||
opts = parser.parse_args()
|
||||
opts.skip_card_init = opts.no_card_init
|
||||
sl = None
|
||||
scc = None
|
||||
card = None
|
||||
rs = None
|
||||
sim_menu = None
|
||||
event_list = None
|
||||
try:
|
||||
kwargs = {}
|
||||
if opts.apdu_trace:
|
||||
kwargs['apdu_tracer'] = StderrApduTracer()
|
||||
sl = mod.init_reader(opts, **kwargs)
|
||||
scc = SimCardCommands(sl)
|
||||
scc.cat_cla = '80' # UICC CLA default; overridden for SIM after init_card
|
||||
scc._tp.proactive_handler = _DefaultProactiveHandler()
|
||||
sl.wait_for_card(3)
|
||||
rs, card = mod.init_card(sl, opts.skip_card_init)
|
||||
scc.cat_cla = '80' if isinstance(card, UiccCardBase) else 'a0'
|
||||
except Exception:
|
||||
print("Warning: reader/card initialization failed:", file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
ch = CardHandler(sl) if sl else None
|
||||
try:
|
||||
app = mod.PysimApp(verbose=opts.verbose, card=card, rs=rs, sl=sl, ch=ch)
|
||||
except Exception:
|
||||
print("Warning: PysimApp creation failed:", file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
app = None
|
||||
if scc and hasattr(scc, '_tp'):
|
||||
scc._tp.apdu_tracer = StderrApduTracer()
|
||||
try:
|
||||
_init_proactive_session()
|
||||
sys.stderr.write('INIT: sending TERMINAL PROFILE %s (CLA=%s)\n' % (opts.terminal_profile, scc.cat_cla))
|
||||
sm, el = _send_terminal_profile(scc, opts.terminal_profile)
|
||||
sys.stderr.write('INIT: TP done, menu=%s events=%s\n' % ('yes' if sm else 'no', 'yes' if el else 'no'))
|
||||
sim_menu = sm or sim_menu
|
||||
event_list = el or event_list
|
||||
for _ in range(3):
|
||||
st_data, st_sw = _send_status(scc)
|
||||
sys.stderr.write('INIT: drain STATUS -> %s\n' % st_sw)
|
||||
if not st_sw.startswith('91'):
|
||||
break
|
||||
_handle_proactive_chain(scc, st_sw)
|
||||
except Exception:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
if app is not None and opts.apdu_trace:
|
||||
# PysimApp.__init__ routes PySimLogger through app.poutput() (app.stdout)
|
||||
# and drops the root level to INFO. Re-route pysim's own APDU trace logging
|
||||
# directly to fd 1 so it survives the app.stdout/StringIO redirection in the
|
||||
# HTTP handlers and the INFO level suppression.
|
||||
PySimLogger.setup(print_callback=_log_stdout)
|
||||
PySimLogger.set_level(logging.DEBUG)
|
||||
# PysimApp.__init__ and every `equip` wipe the transport apdu_tracer
|
||||
# (_onchange_apdu_trace sets it to None). Re-attach our tracer and make
|
||||
# sure it stays attached across equip/re-equip.
|
||||
tracer = StderrApduTracer()
|
||||
def _reattach_tracer():
|
||||
if app.card:
|
||||
app.card._scc._tp.apdu_tracer = tracer
|
||||
_reattach_tracer()
|
||||
orig_onchange = app._onchange_apdu_trace
|
||||
def _onchange_apdu_trace(param_name, old, new):
|
||||
orig_onchange(param_name, old, new)
|
||||
_reattach_tracer()
|
||||
app._onchange_apdu_trace = _onchange_apdu_trace
|
||||
server = HTTPServer((opts.http_host, opts.http_port), PysimHandler)
|
||||
server.sl = sl
|
||||
server.scc = scc
|
||||
server.card = card
|
||||
server.rs = rs
|
||||
server.app = app
|
||||
server.sms_oa = opts.sms_oa
|
||||
server.sms_sc = opts.sms_sm_sc
|
||||
server.log_requests = opts.log_requests
|
||||
server.terminal_profile = opts.terminal_profile
|
||||
server.web_dir = opts.web_dir
|
||||
server.sim_menu = sim_menu
|
||||
server.event_list = event_list
|
||||
server.menu_active = False
|
||||
server.stk_pending = None
|
||||
# Set server reference for polling timer and mark card as connected
|
||||
import pysim_otaman_server.server
|
||||
pysim_otaman_server.server._server_ref = server
|
||||
pysim_otaman_server.server._CARD_CONNECTED = True
|
||||
if opts.poll_interval is not None:
|
||||
pysim_otaman_server.server._set_poll_interval(opts.poll_interval)
|
||||
# Auto-enable polling if card initialized successfully (unless interval is 0)
|
||||
if server.scc and server.card and opts.poll_interval != 0:
|
||||
pysim_otaman_server.server._poll_enable()
|
||||
print("─" * 70)
|
||||
print(" pysim-otaman-server v%s listening on http://%s:%s" % (VERSION, opts.http_host, opts.http_port))
|
||||
print(" Open http://%s:%s in your browser for the OTAMan UI (served by this server)."
|
||||
% (opts.http_host, opts.http_port))
|
||||
print("─" * 70)
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down...")
|
||||
server.shutdown()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,22 @@
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def load_pysim_app():
|
||||
import pySim
|
||||
pysim_dir = os.path.dirname(pySim.__file__)
|
||||
candidates = [
|
||||
os.path.join(os.path.dirname(pysim_dir), 'pySim-shell.py'),
|
||||
os.path.join(os.path.dirname(sys.executable), 'pySim-shell.py'),
|
||||
]
|
||||
for path in candidates:
|
||||
if os.path.exists(path):
|
||||
spec = importlib.util.spec_from_file_location("pySim_shell", path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
raise ImportError(
|
||||
"pySim-shell.py not found. Make sure pysim is installed "
|
||||
"(pip install pysim) and pySim-shell.py is on the PATH."
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
# pysim dependencies minus the SMPP bridge (smpp.twisted3). Used by setup.bat.
|
||||
# pySim-shell does not use SMPP; omitting smpp.twisted3 avoids the
|
||||
# twisted -> twisted-iocpsupport build chain on Windows.
|
||||
pyscard
|
||||
pyserial
|
||||
pytlv
|
||||
cmd2>=2.6.2,<4.0
|
||||
jsonpath-ng
|
||||
construct>=2.10.70
|
||||
bidict
|
||||
pyosmocom>=0.0.12
|
||||
pyyaml>=5.4
|
||||
termcolor
|
||||
colorlog
|
||||
pycryptodomex
|
||||
packaging
|
||||
smpp.pdu @ git+https://github.com/hologram-io/smpp.pdu
|
||||
asn1tools
|
||||
@@ -0,0 +1,86 @@
|
||||
@echo off
|
||||
REM pysim-otaman-server setup script for Windows
|
||||
REM Creates a venv and installs pysim and its dependencies.
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set VENV_DIR=%~dp0.venv
|
||||
|
||||
echo === pysim-otaman-server setup ===
|
||||
echo.
|
||||
|
||||
REM Check Python
|
||||
python --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Python is required but not found.
|
||||
echo Install Python 3.8+ from https://python.org
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Found Python
|
||||
python --version
|
||||
|
||||
REM Check Git
|
||||
git --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Git is required but not found.
|
||||
echo Install Git from https://git-scm.com
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Found Git
|
||||
git --version
|
||||
echo.
|
||||
|
||||
REM Create venv
|
||||
if not exist "%VENV_DIR%" (
|
||||
echo Creating virtual environment...
|
||||
python -m venv "%VENV_DIR%"
|
||||
)
|
||||
call "%VENV_DIR%\Scripts\activate.bat"
|
||||
|
||||
REM Upgrade pip
|
||||
python -m pip install --upgrade pip -q
|
||||
|
||||
REM pyscard has precompiled wheels for Python 3.10-3.13.
|
||||
REM On Python 3.9 / 3.14 pip builds it from source (requires Microsoft C++ Build Tools).
|
||||
echo Note: pyscard ships precompiled wheels for Python 3.10-3.13.
|
||||
echo Python 3.9 / 3.14 will build pyscard from source and require
|
||||
echo Microsoft C++ Build Tools ("Desktop development with C++").
|
||||
echo.
|
||||
|
||||
REM Install pysim dependencies first, then pysim itself (without the SMPP
|
||||
REM bridge - not needed by pysim-shell). Installing pysim with --no-deps after
|
||||
REM its dependencies avoids pip's resolver warning about the intentionally
|
||||
REM omitted smpp.twisted3 (SMPP bridge) dependency.
|
||||
echo === Installing pysim dependencies ===
|
||||
pip install -r "%~dp0requirements-pysim.txt"
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Failed to install pysim dependencies.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo === Installing pysim ===
|
||||
pip install --no-deps git+https://github.com/osmocom/pysim.git
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Failed to install pysim.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
|
||||
REM Install pysim-otaman-server
|
||||
echo === Installing pysim-otaman-server ===
|
||||
pip install -e "%~dp0."
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Failed to install pysim-otaman-server.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
|
||||
echo === Setup complete ===
|
||||
echo Run start.bat to start the server.
|
||||
pause
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# pysim-otaman-server setup script
|
||||
# Creates a venv and installs pysim and its dependencies.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
VENV_DIR="$SCRIPT_DIR/.venv"
|
||||
|
||||
# Check prerequisites
|
||||
command -v python3 >/dev/null 2>&1 || { echo "Error: Python 3 is required but not found. Install Python 3.8+ from https://python.org"; exit 1; }
|
||||
command -v git >/dev/null 2>&1 || { echo "Error: Git is required but not found. Install Git from https://git-scm.com"; exit 1; }
|
||||
|
||||
PY_VER=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||||
echo "Found Python $PY_VER"
|
||||
|
||||
# Create venv
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
python3 -m venv "$VENV_DIR"
|
||||
fi
|
||||
source "$VENV_DIR/bin/activate"
|
||||
PIP="$VENV_DIR/bin/pip"
|
||||
|
||||
# Upgrade pip
|
||||
$PIP install --upgrade pip -q
|
||||
|
||||
# Install pysim from the Osmocom repository
|
||||
echo ""
|
||||
echo "=== Installing pysim ==="
|
||||
$PIP install git+https://github.com/osmocom/pysim.git
|
||||
|
||||
# Install pysim-otaman-server
|
||||
echo ""
|
||||
echo "=== Installing pysim-otaman-server ==="
|
||||
$PIP install -e "$SCRIPT_DIR"
|
||||
|
||||
# Check for PC/SC
|
||||
echo ""
|
||||
if command -v pcscd >/dev/null 2>&1; then
|
||||
echo "=== PC/SC daemon found ==="
|
||||
if ! pgrep -x pcscd >/dev/null; then
|
||||
echo "Starting pcscd..."
|
||||
sudo pcscd || echo "Warning: could not start pcscd. Start it manually: sudo systemctl start pcscd"
|
||||
fi
|
||||
else
|
||||
echo "Note: PC/SC daemon not found. If you use a USB smart card reader,"
|
||||
echo "install pcsc-lite and ccid:"
|
||||
echo " Debian/Ubuntu: sudo apt install pcscd pcsc-tools"
|
||||
echo " Arch Linux: sudo pacman -S pcsc-lite ccid"
|
||||
echo " Fedora: sudo dnf install pcsc-lite pcsc-lite-ccid"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Setup complete ==="
|
||||
echo "Run ./start.sh to start the server."
|
||||
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
REM pysim-otaman-server start script for Windows
|
||||
REM Starts the server, preferring the venv if it exists.
|
||||
REM PC/SC is built into Windows, so defaults to reader 0.
|
||||
|
||||
set VENV_DIR=%~dp0.venv
|
||||
|
||||
if exist "%VENV_DIR%\Scripts\pysim-otaman-server.exe" (
|
||||
echo Starting pysim-otaman-server from venv on http://127.0.0.1:8080
|
||||
echo Press Ctrl+C to stop.
|
||||
"%VENV_DIR%\Scripts\pysim-otaman-server.exe" --http-port 8080 -p 0
|
||||
goto :eof
|
||||
)
|
||||
|
||||
if exist "%~dp0pysim_otaman_server\__main__.py" (
|
||||
echo Starting pysim-otaman-server from source on http://127.0.0.1:8080
|
||||
echo Press Ctrl+C to stop.
|
||||
python -m pysim_otaman_server --http-port 8080 -p 0
|
||||
goto :eof
|
||||
)
|
||||
|
||||
where pysim-otaman-server >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
echo Starting pysim-otaman-server on http://127.0.0.1:8080
|
||||
echo Press Ctrl+C to stop.
|
||||
pysim-otaman-server --http-port 8080 -p 0
|
||||
goto :eof
|
||||
)
|
||||
|
||||
echo Error: pysim-otaman-server not installed.
|
||||
echo Run setup.bat first or install manually:
|
||||
echo pip install pysim-otaman-server
|
||||
pause
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# pysim-otaman-server start script
|
||||
# Starts the server, preferring the venv if it exists.
|
||||
# Auto-detects PC/SC reader if available.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
VENV_DIR="$SCRIPT_DIR/.venv"
|
||||
|
||||
# Auto-detect reader
|
||||
READER_ARGS=""
|
||||
if command -v pcscd > /dev/null 2>&1 && pgrep -x pcscd > /dev/null 2>&1; then
|
||||
READER_ARGS="-p 0"
|
||||
elif [ -e /dev/ttyUSB0 ]; then
|
||||
READER_ARGS="-d /dev/ttyUSB0"
|
||||
fi
|
||||
|
||||
SERVER=""
|
||||
if [ -f "$VENV_DIR/bin/pysim-otaman-server" ]; then
|
||||
SERVER="$VENV_DIR/bin/pysim-otaman-server"
|
||||
elif command -v pysim-otaman-server &> /dev/null; then
|
||||
SERVER="pysim-otaman-server"
|
||||
elif [ -f "$SCRIPT_DIR/pysim_otaman_server/__main__.py" ]; then
|
||||
echo "Starting pysim-otaman-server from source on http://127.0.0.1:8080"
|
||||
cd "$SCRIPT_DIR" && python3 -m pysim_otaman_server --http-port 8080 $READER_ARGS
|
||||
exit $?
|
||||
else
|
||||
echo "Error: pysim-otaman-server not installed."
|
||||
echo "Run setup.sh first or install manually:"
|
||||
echo " pip install pysim-otaman-server"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting pysim-otaman-server on http://127.0.0.1:8080"
|
||||
echo "Press Ctrl+C to stop."
|
||||
$SERVER --http-port 8080 $READER_ARGS
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Unit tests for the OTA helper functions in pysim_otaman_server.server.
|
||||
|
||||
Reference vectors are key-free: synthetic dummy keys plus the already-public
|
||||
sysmocom sample-key vectors that ship in pySim's own tests/unittests/test_ota.py.
|
||||
No live/sample card keys and no ICCIDs appear here.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
# pySim checkout is a sibling of this repo; put it on sys.path so the server
|
||||
# module (which imports pySim at module level) can be exercised against it.
|
||||
PROJECTS = Path(__file__).resolve().parents[2]
|
||||
PY_SIM = PROJECTS / 'pysim'
|
||||
if str(PY_SIM) not in sys.path:
|
||||
sys.path.insert(0, str(PY_SIM))
|
||||
|
||||
from pysim_otaman_server.server import (
|
||||
_build_sms_tpdu,
|
||||
_decode_por,
|
||||
_ota_reference,
|
||||
_spi_from_bytes,
|
||||
)
|
||||
|
||||
# Synthetic dummy key material (no real card keys).
|
||||
K = '00112233445566778899AABBCCDDEEFF'
|
||||
# Public sysmocom sample keys from pySim tests/unittests/test_ota.py.
|
||||
KIC3 = 'C21DD66ACAC13CB3BC8B331B24AFB57B'
|
||||
KID3 = '12110C78E678C25408233076AA033615'
|
||||
# Public synthetic AES keys from pySim tests/unittests/test_ota.py.
|
||||
KIC_AES = '200102030405060708090a0b0c0d0e0f'
|
||||
KID_AES = '201102030405060708090a0b0c0d0e0f'
|
||||
|
||||
APDU = '00a40000023f00'
|
||||
|
||||
# (spi1, spi2) -> expected secured packet, generated with _ota_reference
|
||||
# against pySim's OtaDialectSms.encode_cmd and cross-checked with the JS genSp().
|
||||
REFERENCE_VECTORS = {
|
||||
('06', '09'):
|
||||
'00201506091515b00000c08f58c38860acb3a362fffe670ad13759a2a6b4c1a91116',
|
||||
('16', '01'):
|
||||
'00201516011515b00000e42573469e68a8462a57a505b0e2b1c09c1928c7a182311f',
|
||||
('02', '09'):
|
||||
'001d1502091515b0000000000000010085a8ca1a9828b0bb00a40000023f00',
|
||||
}
|
||||
|
||||
# AES-128 reference vectors (public synthetic keys from pySim test_ota.py).
|
||||
AES_APDU = '00a40004023f00'
|
||||
AES_REFERENCE_VECTORS = {
|
||||
('16', '19'):
|
||||
'00281516192222b000115a47655527e96e832f1a5c698655715d4331454a0d83952c0ed35245706976b1',
|
||||
('12', '09'):
|
||||
'001d1512092222b0001100000000110029826122c7a0b79500a40004023f00',
|
||||
('1e', '19'):
|
||||
'0028151e192222b0001118b202ee47a3203e7370861c383b4142e704157b36e5c0eb4bb33eb6036cbaf8',
|
||||
}
|
||||
|
||||
|
||||
class TestSpiFromBytes(unittest.TestCase):
|
||||
def test_06_09_ciphered_cc(self):
|
||||
spi = _spi_from_bytes(0x06, 0x09)
|
||||
self.assertEqual(spi, {
|
||||
'counter': 'no_counter',
|
||||
'ciphering': True,
|
||||
'rc_cc_ds': 'cc',
|
||||
'por_in_submit': False,
|
||||
'por_shall_be_ciphered': False,
|
||||
'por_rc_cc_ds': 'cc',
|
||||
'por': 'por_required',
|
||||
})
|
||||
|
||||
def test_16_01_counter_must_be_higher(self):
|
||||
spi = _spi_from_bytes(0x16, 0x01)
|
||||
self.assertEqual(spi['counter'], 'counter_must_be_higher')
|
||||
self.assertTrue(spi['ciphering'])
|
||||
self.assertEqual(spi['rc_cc_ds'], 'cc')
|
||||
self.assertEqual(spi['por_rc_cc_ds'], 'no_rc_cc_ds')
|
||||
|
||||
def test_02_09_unciphered_cc(self):
|
||||
spi = _spi_from_bytes(0x02, 0x09)
|
||||
self.assertFalse(spi['ciphering'])
|
||||
self.assertEqual(spi['rc_cc_ds'], 'cc')
|
||||
self.assertEqual(spi['por_rc_cc_ds'], 'cc')
|
||||
|
||||
def test_04_19_ciphered_no_cc(self):
|
||||
spi = _spi_from_bytes(0x04, 0x19)
|
||||
self.assertTrue(spi['ciphering'])
|
||||
self.assertEqual(spi['rc_cc_ds'], 'no_rc_cc_ds')
|
||||
self.assertTrue(spi['por_shall_be_ciphered'])
|
||||
self.assertEqual(spi['por_rc_cc_ds'], 'cc')
|
||||
|
||||
|
||||
class TestBuildSmsTpdu(unittest.TestCase):
|
||||
CHUNK = '00201506091515b00000c08f58c38860acb3a362fffe670ad13759a2a6b4c1a91116'
|
||||
SCTS = bytes.fromhex('24051215173000')
|
||||
|
||||
def _build(self, *args, **kwargs):
|
||||
with mock.patch('pysim_otaman_server.server._encode_scts', return_value=self.SCTS):
|
||||
return _build_sms_tpdu(*args, **kwargs)
|
||||
|
||||
def test_single_message_with_cpi(self):
|
||||
self.assertEqual(
|
||||
self._build(self.CHUNK, include_cpi=True),
|
||||
'4005812143f57ff6240512151730002502700000201506091515b00000'
|
||||
'c08f58c38860acb3a362fffe670ad13759a2a6b4c1a91116')
|
||||
|
||||
def test_single_message_without_cpi(self):
|
||||
self.assertEqual(
|
||||
self._build(self.CHUNK, include_cpi=False),
|
||||
'0405812143f57ff6240512151730002200201506091515b00000'
|
||||
'c08f58c38860acb3a362fffe670ad13759a2a6b4c1a91116')
|
||||
|
||||
def test_first_chunk_has_cpi(self):
|
||||
self.assertEqual(
|
||||
self._build(self.CHUNK, chunk_total=3, chunk_num=1, include_cpi=True),
|
||||
'4405812143f57ff6240512151730002a070003010301700000201506091515b00000'
|
||||
'c08f58c38860acb3a362fffe670ad13759a2a6b4c1a91116')
|
||||
|
||||
def test_later_chunk_concat_only(self):
|
||||
self.assertEqual(
|
||||
self._build(self.CHUNK, chunk_total=3, chunk_num=2, include_cpi=True),
|
||||
'4405812143f57ff6240512151730002805000301030200201506091515b00000'
|
||||
'c08f58c38860acb3a362fffe670ad13759a2a6b4c1a91116')
|
||||
|
||||
|
||||
class TestOtaReference(unittest.TestCase):
|
||||
def test_ciphered_spi_06_09(self):
|
||||
out, _ = _ota_reference('06', '09', '15', '15', 'b00000', '0000000001', APDU, K, K)
|
||||
self.assertEqual(out, REFERENCE_VECTORS[('06', '09')])
|
||||
|
||||
def test_ciphered_spi_16_01(self):
|
||||
out, _ = _ota_reference('16', '01', '15', '15', 'b00000', '0000000001', APDU, K, K)
|
||||
self.assertEqual(out, REFERENCE_VECTORS[('16', '01')])
|
||||
|
||||
def test_unciphered_spi_02_09(self):
|
||||
out, _ = _ota_reference('02', '09', '15', '15', 'b00000', '0000000001', APDU, K, K)
|
||||
self.assertEqual(out, REFERENCE_VECTORS[('02', '09')])
|
||||
|
||||
def test_unciphered_cpl_is_0x001d(self):
|
||||
# Regression: CPL counts octets from the CHL octet to the last octet
|
||||
# of the secured data (29 here), it must NOT be len(out)-2 (27/0x001b).
|
||||
out, _ = _ota_reference('02', '09', '15', '15', 'b00000', '0000000001', APDU, K, K)
|
||||
self.assertEqual(out[:4], '001d')
|
||||
self.assertEqual(len(out) // 2, 31)
|
||||
|
||||
def test_sysmocom_reference_vector(self):
|
||||
# Public vector from pySim tests/unittests/test_ota.py (test_cmd_3des_ciphered).
|
||||
out, _ = _ota_reference('04', '19', '35', '35', 'b00000', '0000000000', APDU, KIC3, KID3)
|
||||
self.assertEqual(out, '00180d04193535b00000e3ec80a849b554421276af3883927c20')
|
||||
|
||||
def test_returns_spi_dict(self):
|
||||
_, spi = _ota_reference('16', '01', '15', '15', 'b00000', '0000000001', APDU, K, K)
|
||||
self.assertEqual(spi['counter'], 'counter_must_be_higher')
|
||||
self.assertTrue(spi['ciphering'])
|
||||
|
||||
def test_aes128_ciphered_cc(self):
|
||||
out, _ = _ota_reference('16', '19', '22', '22', 'b00011', '0000000011', AES_APDU, KIC_AES, KID_AES)
|
||||
self.assertEqual(out, AES_REFERENCE_VECTORS[('16', '19')])
|
||||
|
||||
def test_aes128_unciphered_cc(self):
|
||||
out, _ = _ota_reference('12', '09', '22', '22', 'b00011', '0000000011', AES_APDU, KIC_AES, KID_AES)
|
||||
self.assertEqual(out, AES_REFERENCE_VECTORS[('12', '09')])
|
||||
|
||||
def test_aes128_counter_plus_one(self):
|
||||
out, spi = _ota_reference('1e', '19', '22', '22', 'b00011', '0000000011', AES_APDU, KIC_AES, KID_AES)
|
||||
self.assertEqual(out, AES_REFERENCE_VECTORS[('1e', '19')])
|
||||
self.assertEqual(spi['counter'], 'counter_must_be_lower')
|
||||
|
||||
|
||||
class TestDecodePor(unittest.TestCase):
|
||||
def test_plaintext_no_cc_synthetic(self):
|
||||
r = _decode_por('02', '01', '15', '15', '0000000001', K, K,
|
||||
'027100000e0ab0000000000000010000016e00')
|
||||
self.assertEqual(r['response_status'], 'por_ok')
|
||||
self.assertEqual(r['tar'], 'B00000')
|
||||
self.assertEqual(r['decoded']['last_status_word'], '6e00')
|
||||
|
||||
def test_sysmocom_signed(self):
|
||||
r = _decode_por('06', '09', '35', '35', '0000000001', KIC3, KID3,
|
||||
'027100001612b000110000000000000055f47118381175fb01612f')
|
||||
self.assertEqual(r['response_status'], 'por_ok')
|
||||
self.assertEqual(r['decoded']['last_status_word'], '612f')
|
||||
|
||||
def test_sysmocom_ciphered(self):
|
||||
r = _decode_por('06', '19', '35', '35', '0000000001', KIC3, KID3,
|
||||
'027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c')
|
||||
self.assertEqual(r['response_status'], 'por_ok')
|
||||
self.assertEqual(r['decoded']['last_status_word'], '612f')
|
||||
|
||||
def test_sysmocom_no_cc(self):
|
||||
r = _decode_por('06', '01', '35', '35', '0000000001', KIC3, KID3,
|
||||
'027100000e0ab000110000000000000001612f')
|
||||
self.assertEqual(r['response_status'], 'por_ok')
|
||||
self.assertEqual(r['decoded']['last_status_word'], '612f')
|
||||
|
||||
def test_sysmocom_bad_cc_returns_none(self):
|
||||
r = _decode_por('06', '09', '35', '35', '0000000001', KIC3, KID3,
|
||||
'027100001612b000110000000000000055f47118381175fb02612f')
|
||||
self.assertIsNone(r)
|
||||
|
||||
def test_aes128_ciphered(self):
|
||||
r = _decode_por('06', '19', '22', '22', '0000000001', KIC_AES, KID_AES,
|
||||
'027100002412b00011ebc6b497e2cad7aedf36ace0e3a29b38853f0fe9ccde81913be5702b73abce1f')
|
||||
self.assertEqual(r['response_status'], 'por_ok')
|
||||
self.assertEqual(r['decoded']['last_status_word'], '6132')
|
||||
|
||||
def test_malformed_returns_none(self):
|
||||
for bad in ['', '00', '00027100000e0a', '027100000e0ab00000', 'garbage', 'zz']:
|
||||
self.assertIsNone(
|
||||
_decode_por('02', '01', '15', '15', '0000000001', K, K, bad),
|
||||
msg='expected None for %r' % bad)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||