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.
3.4 KiB
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
- Loads a 128-bit seed block into
Ie[0..3]from the ECU's0x27seed response. - AES-128-encrypts it with one of three embedded keys (
iArr4, m.java:5351), selected byMainActivity.h7 ∈ {0,1,2}. - Returns the first ciphertext word;
Gd()sends it as27 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)
- Which key applies to a mechanical-odometer 865 twin.
h7defaults toward 0 for the older no-code Keihin maps, but confirm rather than assume. - Exact seed-block layout.
Vb()encryptsIe[0..3]; m.java fillsIe[1..3]from the seed response and pads two bytes with fixed constants (0x03,0x01), withIe[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:
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.