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>
55 lines
2.0 KiB
C++
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
|