--- name: feedback-yealink-api description: "Yealink T48S web API quirks — RSA/AES login, token-gated writes, correct page/field names for SIP account config" metadata: node_type: memory type: feedback originSessionId: 002fe81e-7e03-414d-b842-1f94f1390a22 --- Yealink T48S web API (firmware 66.86.0.15) — complete working flow: ## Login (PKCS1v15 + AES-CBC hybrid) 1. GET `/servlet?m=mod_listener&p=login&q=loginForm` — fetch RSA public key (`g_rsa_n`, `g_rsa_e`) and initial `JSESSIONID` cookie 2. Generate random 16-byte AES key + IV; encrypt plaintext `{rand};{JSESSIONID};{password}` with AES-128-CBC zero-padding (NOT PKCS7), base64 result → `pwd` 3. RSA-encrypt AES key hex string and IV hex string separately with PKCS1v15 → `rsakey`, `rsaiv` - **Critical**: encrypt the ASCII hex string (e.g. `"a1b2c3..."`) not raw bytes — Yealink's `pkcs1pad2` uses `charCodeAt` per character - **Critical**: AES key/IV hex must be **lowercase** 4. POST `username=admin&pwd=&rsakey=&rsaiv=` to `/servlet?m=mod_listener&p=login&q=login` with `JSESSIONID` cookie 5. Returns `{"authstatus":"done"}` on success; cookie jar updates with new JSESSIONID **Lockout**: 3 failed attempts → ~10 min lockout (polling the login page also resets the timer — stop ALL requests to the phone during lockout) ## SIP Account Config (account-register page) - **Page**: `account-register` (NOT `account-basic` — that page only has anonymous-call advanced fields) - **Load**: GET `/servlet?m=mod_data&p=account-register&q=load` - **Write**: POST `/servlet?m=mod_data&p=account-register&q=write&token=` - Token is **required** — without it returns 403; with it returns 200 + empty `_RES_INFO_` div (that empty response IS success) - Token comes from `g_strToken` variable in the loaded page HTML **Correct field names** for SIP account 1: | Field | Value | |-------|-------| | `var_accountID` | `0` (0-indexed) | | `AccountEnable` | `1` | | `AccountLabel` | display label | | `AccountDisplayName` | caller ID name | | `AccountRegisterName` | SIP auth username (e.g. `1000`) | | `AccountUserName` | SIP username (e.g. `1000`) | | `server1` | SIP server IP (e.g. `134.209.72.226`) | | `port1` | SIP port (e.g. `5080`) | | `Transport1` | `0` = UDP | | `Expires1` | registration expiry seconds | | `AccountPassword` | AES-encrypted password (same AES key/IV as login) | **Password encryption for writes**: Same AES-CBC approach as login — encrypt plaintext password bytes with zero-padding, base64 result → `AccountPassword`. Send same `rsakey` + `rsaiv` alongside. ## Autoprovision Trigger GET `/servlet?m=mod_data&p=settings-autop&q=autopnow&token=` → returns `{"ret":"ok","data":"3"}` on success ## Reboot POST `/servlet?m=mod_data&p=settings-upgrade&q=write&type=reboot` ## SIP Registration Status Load page contains JS: `ccStatus = g_json.ParseJSON(...)` with JSON like `{"Account1":"1000@134.209.72.226:2"}` — status codes: `0`=disabled, `1`=registered, `2`=registering, `3`=failed **Why:** All this was reverse-engineered from Yealink's `commonjs.js` (`pkcs1pad2` function) across multiple sessions after many failed approaches (textbook RSA, wrong plaintext format, wrong field names, missing token). **How to apply:** Use this as the reference any time we script Yealink T48S configuration via its web API. Scripts are saved at `/tmp/yfix_server.py` and `/tmp/ydiag_write.py` (on PVE1) as working examples.