Correct wire format to disassembly: raw S1||crypt(S2), radix 32, int snno/seconds, decryptData response parser, --probe
- envelope: 7 fixed keys + conditional dataArea, no data key; int
type/snno, seconds timestamp, data setter auto len/crc (CRC32 upper);
SequenceNumber 1..9999 (increaseSN)
- crypto: encrypt is plaintext-header-first a||transform(b), key radix 32
at both tryParse sites; decrypt_data ports decryptData (dual-0x58
markers, s+'}' equipNo probe, s+','+T return); decrypt_request kept
for the device-side known-header split
- socket: raw send (no length prefix), accumulate-and-try receive
- battery: queryAPowerData shape (data={'update':2}), radix default 32,
--probe prints exact TX/RX bytes per variant; device_local.send_probe
- docs: RE_DART_FINDINGS ss3/5/8 + open items, BATTERY_POC host recipe
- tests: 19 new protocol tests, suite 44/44 green
This commit is contained in:
1 parent
2438a04ed5
commit
9e69f07636
10 files changed
+744
-182
No files matched your search
+77
-29
@@ -77,20 +77,36 @@ esp_provisioning_ble/src/... # ESP pr
|
||||
|
||||
## 3. Wire envelope — `CommunicationObject`
|
||||
|
||||
JSON keys, in the exact insertion/inverse order used by `toJson`:
|
||||
JSON keys, in the exact insertion order used by `toJson`:
|
||||
|
||||
```
|
||||
cmdType, equipNo, type, timeStamp, snno, len, crc, dataArea, data
|
||||
cmdType, equipNo, type, timeStamp, snno, len, crc[, dataArea]
|
||||
```
|
||||
|
||||
Fields present: `_Mint field_8`, `_OneByteString field_10`, `_Mint field_14`
|
||||
(+ `_Mint`/`_OneByteString` further). `data` is a setter; string const `"00000000"` is a
|
||||
default (likely `crc`/`dataArea` placeholder). `cmdType` = `CommunicationCmd` raw,
|
||||
`type` = `CommunicationOperate` raw, `equipNo` = device SN.
|
||||
`dataArea` is appended **only when the payload is present and non-empty**;
|
||||
there is **no** `data` key on the wire. `cmdType` = `CommunicationCmd` raw,
|
||||
`type` = `CommunicationOperate` raw (always 0 on the `communicate`-factory
|
||||
path — the ctor zeroes it), `equipNo` = device SN (`"00000000"` fallback
|
||||
when empty), `timeStamp` = whole **seconds** (`micros ~/ 1000 ~/ 1000`),
|
||||
`snno` = `int` sequence (see below, **not** the serial string), `len` =
|
||||
UTF-8 byte length of the compact JSON of the payload, `crc` =
|
||||
`Crc32.calculate(jsonEncode(data))` as **uppercase** unpadded hex.
|
||||
Null/empty payload ⇒ `len == 0`, `crc == ""`, no `dataArea` key.
|
||||
|
||||
- `CommunicationOperate`: `query=0, set=1, command=2, modify=5`.
|
||||
- `Crc32::calculate` is used in `fromJsonString` (integrity check of the payload).
|
||||
- `toJsonString()` produces the string that is then transformed (see §5).
|
||||
- `toJsonString()` = `jsonEncode(toJson())` — compact JSON, the input of
|
||||
`encryptObject` (see §5). `toString()` likewise returns `toJsonString()`.
|
||||
- `CommunicationObject(cmd, equipNo, {data})` ctor zeroes `type`/`snno`,
|
||||
stamps seconds, and routes `data` through the `data=` setter (which
|
||||
recomputes `len`/`crc`).
|
||||
- `sendObject` stamps `snno = increaseSN()` first: a 1-based counter that
|
||||
wraps to 1 past 9999 (manager field starts at 0, so the first request
|
||||
carries `snno == 1`).
|
||||
- The `communicate` factory (`0xc9264c`) builds the `data` map from the
|
||||
caller's `arguments` (e.g. `queryAPowerData` passes `{"update": 2}` with
|
||||
cmd `deviceApowerInfo`/`0x19`, operate `query`) and calls the ctor with
|
||||
a named `data:` argument.
|
||||
|
||||
## 4. `CommunicationCmd` (raw → name)
|
||||
|
||||
@@ -119,20 +135,37 @@ default (likely `crc`/`dataArea` placeholder). `cmdType` = `CommunicationCmd` ra
|
||||
## 5. Encryption — `communication_center/core/encryption.dart`
|
||||
|
||||
**`generateSecretKey(String equipNo)`** = hex of `(sum(utf8(equipNo)) & 0xFF)` (via
|
||||
`reduce(sum)` → `&0xFF` → `_toPow2String(16)`), i.e. a 1-byte key rendered as hex.
|
||||
`reduce(sum)` → `&0xFF` → `_toPow2String(16)`), i.e. a 1-byte key rendered as hex,
|
||||
unpadded. E.g. SN `A03X2516013210060005` → byte sum `1052 & 0xFF = 28` → `"1c"`.
|
||||
|
||||
**`encryptObject(obj, key)`**:
|
||||
1. `s = obj.toJsonString()`; split on `","`.
|
||||
2. `a` = UTF-8 of `part[0] + "," + part[1]` (i.e. `{"cmdType":..` and `"equipNo":..`).
|
||||
3. `b` = UTF-8 of `join(",", parts[2..])` (the rest).
|
||||
4. `keyInt = int.tryParse(key, radix: …)`.
|
||||
5. `out[i] = (b[i] + i + len(a) + keyInt) & 0xFF` for all i, then append `a` **unchanged**.
|
||||
**`encryptObject(object, key)`** (object in arg slot 0, key in slot 1):
|
||||
1. `s = object.toJsonString()`; split on `","`.
|
||||
2. `S1 = parts[0] + "," + parts[1] + ","` (header, *with* trailing comma);
|
||||
`a = UTF-8(S1)`.
|
||||
3. `S2 = join(",", parts[2..])` (the rest); `b = UTF-8(S2)`.
|
||||
4. `keyInt = int.tryParse(key, radix: 32)` (literal `0x20` at the call site;
|
||||
null → 0).
|
||||
5. `out[i] = (b[i] + i + len(a) + keyInt) & 0xFF` for all i (so each tail
|
||||
byte is shifted by its *absolute* JSON index plus the key).
|
||||
6. Result = `a ++ out` — the plaintext header goes on the wire **first**,
|
||||
transformed tail after (`addAll` onto the header copy).
|
||||
|
||||
So on the wire the encrypted body is `transform(b) || a` (header/keys appended at the end).
|
||||
`decryptData` is the inverse.
|
||||
**`decryptData(bytes)`** (single arg, the accumulated receive buffer):
|
||||
1. `i = indexWhere(byte == 0x58)`, `j = indexWhere(byte == 0x58, start: i+1)`;
|
||||
either missing → return `""`.
|
||||
2. `s = utf8(wire[0..j])`; `probe = jsonDecode(s + "}")`, null → `""`.
|
||||
3. `equipNo = probe["equipNo"]`; `key = generateSecretKey(equipNo)`;
|
||||
`keyInt = int.tryParse(key, radix: 32) ?? 0`.
|
||||
4. `tail = wire[j+1..]`; `out[k] = (tail[k] - k - len(utf8(s + ",")) - keyInt) & 0xFF`.
|
||||
5. Return `s + "," + utf8(out)` (the `","` re-inserts the comma the `0x58`
|
||||
separator replaced). The caller (`_onSocketReceiveData`) `jsonDecode`s
|
||||
that, builds `CommunicationObject.fromJson`, logs
|
||||
`Socket Manager receive object: …`, and clears the buffer; `""`/parse
|
||||
failures keep accumulating ("receive incomplete data").
|
||||
|
||||
> This is a bespoke additive/KSA stream; the exact radix of the key parse and the trailing
|
||||
> `a` handling should be confirmed against a captured frame before trusting it byte-for-byte.
|
||||
So requests are `S1 || transform(S2)` (split at the computable header
|
||||
length) while responses are `s || 0x58 || transform(tail)` (split at the
|
||||
second `0x58` marker). Both halves share the one additive stream.
|
||||
|
||||
## 6. Checksums & byte order
|
||||
|
||||
@@ -191,7 +224,16 @@ authS2C=2 "fwh/s2c/auth/" mngC2S=5 "fwh/c2s/mng/" deviceS2C "fwh/s2c/
|
||||
- host = stored host, else **`"10.100.1.1"`** (device hotspot gateway);
|
||||
- port constant **`0x4650 = 18000`** (candidate local port),
|
||||
- a `Duration` and retry loop (up to ~2 retries) are involved.
|
||||
- `BytesToInt16/Int16ToBytes` LE framing; `Socket Manager send object` / `receive object`.
|
||||
- `sendObject`: stamps `snno = increaseSN()`, logs
|
||||
`Socket Manager send object: <json>`, derives the key with
|
||||
`generateSecretKey(equipNo)`, calls `encryptObject(object, key)`, then
|
||||
`_sendData`, which checks `connected` status and calls `ClientSocket.send`
|
||||
→ **`dart:io _Socket.add` with the bytes as-is: raw send, no length
|
||||
prefix, no markers, no handshake** — then clears the receive buffer.
|
||||
- Receive: socket `listen` chunk → `ClientSocket._onSocketReceiveData` →
|
||||
`SocketManager._onSocketReceiveData` → buffer `addAll` → `decryptData`
|
||||
in try/catch (`""` = keep accumulating) → `jsonDecode` →
|
||||
`CommunicationObject.fromJson` → `Socket Manager receive object: …` log.
|
||||
|
||||
### Empirical — live aGate on its SoftAP (2026-10-13, from a client on `10.100.1.0/24`)
|
||||
|
||||
@@ -206,11 +248,11 @@ Full TCP sweep of `10.100.1.1` (ports 1–65535): exactly **two** ports open —
|
||||
`16 03` = int16LE `790`, so the decoder waits for a 790-byte frame that never
|
||||
completes, then closes on idle;
|
||||
- cleartext HTTP `GET` (`47 45` = `17735`) hangs forever (read timeout).
|
||||
- A *complete* 147-byte frame (int16LE length + encrypted `QUERY_DEVICE_INFO`
|
||||
envelope, `equipNo = X25160132`, key from `sum(utf8(sn))&0xFF`) is accepted
|
||||
but met with **zero bytes** — every command tried (`0x00`/`0x01`/`0x02`/`0x06`)
|
||||
and both key radices (16/32). Content-level rejection; the leading hypothesis is
|
||||
`equipNo` validation against the real serial. **Pending: retry with the true SN.**
|
||||
- Earlier probes are superseded: they used an int16LE length prefix (the
|
||||
socket layer sends **raw**), the old `transform(b) || a` order (correct
|
||||
is `a || transform(b)` — §5), a truncated `equipNo`, and default radix
|
||||
16. All are corrected in the current PoC; see `BATTERY_POC.md` for the
|
||||
host probe recipe with the true SN.
|
||||
|
||||
## 9. Bluetooth (BLE) transport — `communication_center/bluetooth`
|
||||
|
||||
@@ -341,16 +383,22 @@ keepAlive 10 s, LWT, QoS 0, event names), `MethodHandlerManager` handler list,
|
||||
Still open:
|
||||
|
||||
1. Exact BLE frame field order & the 2-byte magic semantics (confirm with `btsnoop`).
|
||||
2. Exact `encryptObject` radix + trailing-header handling.
|
||||
2. ~~Exact `encryptObject` radix + trailing-header handling~~ — **resolved
|
||||
from disassembly (see §5): radix 32, plaintext header first
|
||||
(`S1 || transform(S2)`), `decryptData` marker model. Remaining: first
|
||||
confirmed live round-trip.**
|
||||
3. ~~Local HTTP port + TLS behavior for `https://IP:PORT`~~ — **revised (2026-10-13,
|
||||
§8 empirical):** no HTTP/TLS port exists on the device at all (only 53 + 9000).
|
||||
Either local mode is socket-only on this firmware, or the HTTP service is
|
||||
state-dependent and wasn't running. Revisit if a `/terminal` response is ever
|
||||
captured from the SoftAP.
|
||||
4. ~~Local socket port confirmation~~ — **resolved on one unit (2026-10-13): `9000`**
|
||||
(§8 empirical). The `0x4650` constant is not listening; the port may be
|
||||
firmware-/device-dependent. Remaining: first confirmed RPC round-trip
|
||||
(blocked on the real SN — see §8 empirical).
|
||||
4. ~~Local socket port confirmation~~ — **partially resolved: `9000` open on
|
||||
one unit** (§8 empirical); the `0x4650` constant is not listening there,
|
||||
so the port may be firmware-/device-dependent. Remaining: first confirmed
|
||||
RPC round-trip — the corrected request bytes (raw `S1 || transform(S2)`,
|
||||
radix 32, int `snno`, seconds `timeStamp`, true SN
|
||||
`A03X2516013210060005`) are ready via
|
||||
`battery --probe`; needs a host on `10.100.1.0/24`.
|
||||
5. Which class in `classes5.dex` sets the `X-Device-*` headers and their exact values
|
||||
(string-pool location confirmed; class binding in progress — §13).
|
||||
6. (Tooling, optional) Enable REA's DEX tools: obtain a **jadx-headless-mcp 0.7.1** JAR
|
||||
|
||||
Reference in new issue
Block a user