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>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user