Files
lippert-onecontrol/canbus/esphome/ids_can_auth.h
T
wesandClaude Fable 5 840cfaf5fc canbus: solve IDS-CAN command-auth cipher; add reference implementations
The CAN write gate (page-42/43 challenge/response) is a 32-round TEA/XTEA-family
Feistel keyed by a per-session 32-bit key; REMOTE_CONTROL = 0xB16B00B5. Verified
51/51 against captured challenge/response pairs across nodes 2A/61/75/F8 (one
global key, not per-node), so the CAN path can now actuate, not just sense.

- ids_can_auth.py         Python reference + self-test (51/51)
- esphome/ids_can_auth.h  C++ port for the ESP32 node (host-tested 8/8)
- sniff/analyze_auth.py   structural analysis (rules out affine; confirms keyed cipher)
- sniff/auth-pairs-multinode-2026-06-11.txt   +9 pairs across 4 nodes
- README                  document the cipher, session keys, unlock sequence

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:22:09 -04:00

55 lines
2.0 KiB
C++

#pragma once
// IDS-CAN command-auth response cipher for the OneControl write path.
//
// 32-round TEA/XTEA-family Feistel, delta 0x9E3779B9, keyed by a per-session
// 32-bit "Cypher" with the round constants baked in.
// Verified bit-exact against ids_can_auth.py and 51 captured bus pairs.
//
// uint32_t arithmetic wraps mod 2^32 by definition in C++, so no masking needed.
// ESP32 int is 32-bit (uint32_t == unsigned int): host g++ test is representative.
#include <cstdint>
#include <cstddef>
namespace ids_can_auth {
// Per-session keys ("Cypher"). REMOTE_CONTROL gates on/off/move.
enum SessionKey : uint32_t {
MANUFACTURING = 0xB16BA115u,
DIAGNOSTIC = 0xBABECAFEu,
REPROGRAMMING = 0xDEADBEEFu,
REMOTE_CONTROL = 0xB16B00B5u,
DAQ = 0x0B00B135u,
};
// response = Encrypt(challenge). seed = challenge word, cypher = session key.
inline uint32_t encrypt(uint32_t seed, uint32_t cypher) {
uint32_t num = cypher;
uint32_t sum = 0x9E3779B9u; // TEA golden-ratio delta
for (int rounds = 32;;) {
seed += ((num << 4) + 1131376761u) ^ (num + sum) ^ ((num >> 5) + 1919510376u);
if (--rounds <= 0) break;
num += ((seed << 4) + 1948272964u) ^ (seed + sum) ^ ((seed >> 5) + 1400073827u);
sum += 0x9E3779B9u;
}
return seed;
}
inline uint32_t remote_control_response(uint32_t challenge) {
return encrypt(challenge, REMOTE_CONTROL);
}
// 4 big-endian challenge bytes (as they arrive in the page-42 payload, after the
// "00 04" prefix) -> 4 big-endian response bytes (for the page-43 reply).
inline void remote_control_response_bytes(const uint8_t challenge[4], uint8_t response[4]) {
uint32_t c = (uint32_t)challenge[0] << 24 | (uint32_t)challenge[1] << 16 |
(uint32_t)challenge[2] << 8 | (uint32_t)challenge[3];
uint32_t r = remote_control_response(c);
response[0] = (uint8_t)(r >> 24);
response[1] = (uint8_t)(r >> 16);
response[2] = (uint8_t)(r >> 8);
response[3] = (uint8_t)(r);
}
} // namespace ids_can_auth