Files
wesandClaude Opus 4.8 742ef49c8a canbus: confirm command path live + frame docs as device integration
Command path proven end to end on the bus (node F8 interior lights, on/off/on),
each answering a distinct fresh challenge; bare opcodes without the exchange are
ignored. ids_can_auth.h verified bit-exact against ids_can_auth.py and the
captured/live pairs.

- idscan_cmd.py: stdlib socketcan tool running the full page-42/43 exchange
- esphome/onecontrol-canbus.yaml: correct IDS-CAN read dispatch (was stale RV-C
  DGN code) + command path wired to the auth header
- README/memory: document the read map + command authentication; rename
  sniff/ -> captures/; neutral device-integration framing throughout

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 11:20:12 -04:00

55 lines
2.0 KiB
C++

#pragma once
// IDS-CAN command authentication response for the OneControl integration.
//
// 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 is the session for 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