Files
samplez/tunie/research/FINDINGS.md
uhryniuk 4c44933b5d Add tunie: read-only KWP2000 diagnostics for Triumph Keihin ECU
A Python tool to safely read the Keihin ECU on a 2010 Bonneville T100 over
K-Line (KKL cable) or a Bluetooth ELM327, plus the reverse-engineering research
behind it. Phase 1 (read-only comms) of an open tuning toolchain to replace the
closed TuneECU app.

Read-only by construction: safety.assert_read_only() runs on every outbound
request before it hits the wire and refuses all write/flash services (0x27,
0x31, 0x34/0x36, 0x35, 0x37, 0x14, 0x11, 0x2E) and programming sessions, so a
bug cannot brick the ECU. Verified frames match TuneECU byte-for-byte in
tests/verify_protocol.py.

Protocol constants recovered from the TuneECU APK (not guessed): ECU address
0xD5, K-Line tester 0xF5, format byte 0x80|len, additive mod-256 checksum.
Includes the full TuneECU map catalogue (1811 entries) extracted to maps.json,
searchable and filterable by ECU type and mechanical-vs-LCD odometer.

research/ documents the Security Access seed/key algorithm, recovered as
standard AES-128 (three embedded keys), with a self-testing reference impl
verified against FIPS-197. This is write-path material, kept outside the
read-only package.

STATUS.md and README.md capture full context, the risk register, and where we
left off: comms built but not yet run against the bike; next step is wiring the
VAG KKL cable to the Triumph connector and running the first scan.
2026-08-10 14:29:50 -05:00

83 lines
3.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Seed/key recovery — findings
## Question
Is the Triumph Keihin KWP2000 Security Access (service `0x27`) seed/key algorithm
recoverable, and does the logging APK help?
## Answer
**Yes, fully — and from the *static* decompile, so the logging build isn't
needed for it.** The algorithm is **AES-128**, and all key material is embedded
in the app. RESEARCH.md's prediction ("Honda-style XOR / bit-shift with a factory
constant, possibly brute-forced") was wrong about the primitive.
## Where it lives
`com/tuneecu/m.java`, method `Vb()` (jadx), = `Lcom/tuneecu/m;->Vb()I` (smali).
| Evidence in `Vb()` | What it proves |
|---|---|
| `sArr = {1,2,4,8,16,32,64,128,27,54}` | AES Rcon (0x01,02,04,08,10,20,40,80,**1B,36**) |
| 4 table lookups/column at `+0,+256,+512,+768` | AES T-tables (Te0–Te3) |
| 10-iteration loop building 44 words | AES-128 key expansion |
| 9 rounds + 1 S-box-only final round | AES-128 encryption |
| `iArr3 = w.f` first entry `0xA56363C6` | `(3·S0, S0, S0, 2·S0)` with S0=0x63 → standard S-box + MixColumns |
**Verified:** all 256 entries of `w.f` match the standard AES Te0 table exactly
(`keihin_seedkey.py` cross-checks them). Our reference AES also passes the
FIPS-197 Appendix-B known-answer test. This is unmodified AES-128.
## What `Vb()` does
1. Loads a 128-bit seed block into `Ie[0..3]` from the ECU's `0x27` seed response.
2. AES-128-encrypts it with one of **three** embedded keys (`iArr4`, m.java:5351),
selected by `MainActivity.h7 ∈ {0,1,2}`.
3. Returns the first ciphertext word; `Gd()` sends it as `27 02 <4 bytes>`.
The three keys (little-endian, as the cipher consumes them):
```
h7=0: ef704ca051b800cc9287df6a3511a978
h7=1: d4b15ff4c92ab7f098316e7a5b11ac39
h7=2: dc9fdba46f2fad18a4b8e1123c7183c2
```
`h7` is looked up from an ECU calibration-code string (`MainActivity.java:6221`).
## Still open (framing, not crypto — one captured pair resolves both)
1. **Which key** applies to a mechanical-odometer 865 twin. `h7` defaults toward
0 for the older no-code Keihin maps, but confirm rather than assume.
2. **Exact seed-block layout.** `Vb()` encrypts `Ie[0..3]`; m.java fills `Ie[1..3]`
from the seed response and pads two bytes with fixed constants (`0x03`, `0x01`),
with `Ie[0]` set on an earlier path. A single real (seed, key) pair pins this
down exactly.
## The logging APK
`work/TuneECU-logging.apk` is a clean apktool rebuild of the original (the ~17 KB
size delta is re-signing/recompression). No custom instrumentation was found —
every `Log` call is TuneECU's own, gated behind its debug boolean, and no
`debuggable` flag is set. So it does not currently capture seed/key at runtime.
It's still the right tool for the **validation** step above: make the app perform
one real Security Access against the bike (or a bench ECU) and capture the `27 01`
seed and `27 02` key off the wire, then check `compute_key(seed, h7)` reproduces
it. That either confirms the port outright or tells us the exact `h7`/padding.
## Reference implementation
`research/keihin_seedkey.py` — pure-Python, self-testing. Run it:
```sh
python3 research/keihin_seedkey.py
```
## Boundary
This is **write-path** material. It is kept in `research/`, outside the read-only
`tunie` package, and `tunie` does not import it. Reproducing the key does not make
flashing safe — it's one of several pieces (checksum patching, a verified stock
dump, a recovery plan, ideally a spare ECU) that all have to line up first.