feat: port GIP protocol and auth stack

Port the GIP protocol and authentication layers from medusalix/xone
into the C++ stack:

- crypto: SHA-256/HMAC (CommonCrypto), RSA PKCS#1 (Security.framework),
  and a self-contained P-256 ECDH validated against OpenSSL vectors
- auth: v1 (RSA) and v2 (ECDH) handshake state machine
- gip: header/varint/chunk handling and packet dispatch, with the kernel
  device model replaced by transport + client_listener interfaces

Adds test_crypto, test_auth, and test_gip suites.

Co-Authored-By: deepseek (deepseek/deepseek-v4-pro-0813): ported crypto, auth, and GIP
This commit is contained in:
portersky
2026-08-17 15:04:44 +02:00
parent 95e958a7e8
commit 2d4366f454
13 changed files with 3217 additions and 14 deletions
+288 -3
View File
@@ -4,12 +4,297 @@
// GIP protocol (Game Input Protocol)
// ==============================================================================
// Port target: medusalix/xone bus/protocol.c + bus/bus.c.
// Client lifecycle, ANNOUNCE/IDENTIFY handshake, auth handshake, status
// reports (battery, connected), HID report pass-through.
//
// The kernel driver's device model (struct device, sysfs, driver
// registration) is gone. The adapter owns up to 16 clients; the transport
// interface abstracts the MT76 data path (Phase 3), and client_listener
// delivers input/battery/audio events to the HID layer (Phase 4). The
// adapter is driven single-threaded from the USB read callback.
// ==============================================================================
#include <array>
#include <cstdint>
#include <memory>
#include <span>
#include <string>
#include <vector>
#include "auth/auth.hpp"
#include "common/types.hpp"
namespace xone::gip {
// TODO(phase 1): GIP frame types, client lifecycle, handshake state machine.
using u8 = std::uint8_t;
using u16 = std::uint16_t;
using u32 = std::uint32_t;
inline constexpr u16 k_vid_microsoft = 0x045e;
inline constexpr int k_audio_interval = 8; // ms between audio packets
inline constexpr u32 k_pkt_max_length = 58;
inline constexpr u32 k_chunk_buf_max_length = 0xffff;
inline constexpr int k_max_clients = 16;
enum : u8 {
GIP_CMD_ACKNOWLEDGE = 0x01,
GIP_CMD_ANNOUNCE = 0x02,
GIP_CMD_STATUS = 0x03,
GIP_CMD_IDENTIFY = 0x04,
GIP_CMD_POWER = 0x05,
GIP_CMD_AUTHENTICATE = 0x06,
GIP_CMD_VIRTUAL_KEY = 0x07,
GIP_CMD_AUDIO_CONTROL = 0x08,
GIP_CMD_RUMBLE = 0x09,
GIP_CMD_LED = 0x0a,
GIP_CMD_HID_REPORT = 0x0b,
GIP_CMD_FIRMWARE = 0x0c,
GIP_CMD_SERIAL_NUMBER = 0x1e,
GIP_CMD_INPUT = 0x20,
GIP_CMD_AUDIO_SAMPLES = 0x60,
};
enum : u8 {
GIP_OPT_ACKNOWLEDGE = 1 << 4,
GIP_OPT_INTERNAL = 1 << 5,
GIP_OPT_CHUNK_START = 1 << 6,
GIP_OPT_CHUNK = 1 << 7,
};
enum : u8 {
GIP_BATT_TYPE_NONE = 0x00,
GIP_BATT_TYPE_STANDARD = 0x01,
GIP_BATT_TYPE_KIT = 0x02,
};
enum : u8 {
GIP_BATT_LEVEL_LOW = 0x00,
GIP_BATT_LEVEL_NORMAL = 0x01,
GIP_BATT_LEVEL_HIGH = 0x02,
GIP_BATT_LEVEL_FULL = 0x03,
};
enum : u8 {
GIP_PWR_ON = 0x00,
GIP_PWR_SLEEP = 0x01,
GIP_PWR_OFF = 0x04,
GIP_PWR_RESET = 0x07,
};
enum : u8 {
GIP_LED_OFF = 0x00,
GIP_LED_ON = 0x01,
GIP_LED_BLINK_FAST = 0x02,
GIP_LED_BLINK_NORMAL = 0x03,
GIP_LED_BLINK_SLOW = 0x04,
GIP_LED_FADE_SLOW = 0x08,
GIP_LED_FADE_FAST = 0x09,
};
enum : u8 {
GIP_AUD_FORMAT_16KHZ_MONO = 0x05,
GIP_AUD_FORMAT_24KHZ_MONO = 0x09,
GIP_AUD_FORMAT_48KHZ_STEREO = 0x10,
};
enum : u8 {
GIP_AUD_FORMAT_CHAT_24KHZ = 0x04,
GIP_AUD_FORMAT_CHAT_16KHZ = 0x05,
};
// Decoded wire header (variable length, not packed).
struct gip_header {
u8 command = 0;
u8 options = 0;
u8 sequence = 0;
u32 packet_length = 0;
u32 chunk_offset = 0;
};
struct gip_hardware {
u16 vendor = 0;
u16 product = 0;
u16 version = 0;
};
struct gip_audio_config {
u8 format = 0;
int channels = 0;
int sample_rate = 0;
int buffer_size = 0;
int fragment_size = 0;
int packet_size = 0;
};
// Low-level data path to the MT76 chip (implemented by Phase 3).
class transport {
public:
virtual ~transport() = default;
// Send one framed GIP data buffer to the chip.
virtual auto send_frame(std::span<u8 const> frame) -> int = 0;
// Install the AES-CCMP session key for a client (no-op by default).
virtual auto set_encryption_key(u8 client_id, std::span<u8 const> key) -> int
{
(void)client_id;
(void)key;
return 0;
}
};
// Events delivered to the HID layer (Phase 4).
class client_listener {
public:
virtual ~client_listener() = default;
virtual auto on_client_added(class client&) -> void {}
virtual auto on_client_removed(u8) -> void {}
virtual auto on_battery(class client&, u8, u8) -> void {}
virtual auto on_guide_button(class client&, bool) -> void {}
virtual auto on_input(class client&, std::span<u8 const>) -> void {}
virtual auto on_hid_report(class client&, std::span<u8 const>) -> void {}
virtual auto on_audio_ready(class client&) -> void {}
virtual auto on_audio_volume(class client&, u8, u8) -> void {}
virtual auto on_audio_samples(class client&, std::span<u8 const>) -> void {}
};
class adapter;
// One connected controller. Also implements the auth_sink so the auth
// handshake can send AUTHENTICATE packets back through the GIP layer.
class client : public xone::auth::auth_sink {
public:
client(adapter& adapter, u8 id);
auto id() const -> u8 { return id_; }
auto hardware() const -> gip_hardware const& { return hardware_; }
auto audio_config_in() const -> gip_audio_config const& { return audio_config_in_; }
auto audio_config_out() const -> gip_audio_config const& { return audio_config_out_; }
auto classes() const -> std::vector<std::string> const& { return classes_; }
auto has_interface(xone::guid_t const& guid) const -> bool;
auto set_power_mode(u8 mode) -> int;
auto send_rumble(std::span<u8 const> pkt) -> int;
auto set_led_mode(u8 mode, u8 brightness) -> int;
auto suggest_audio_format(u8 in, u8 out, bool chat) -> int;
auto set_audio_volume(u8 in, u8 chat, u8 out) -> int;
auto send_audio_samples(std::span<u8 const> samples) -> int;
auto enable_audio() -> int;
auto init_audio_in() -> int;
auto init_audio_out() -> int;
auto disable_audio() -> void;
// auth_sink implementation.
auto send(std::span<u8 const> pkt, bool acknowledge) -> int override;
auto set_encryption_key(std::span<u8 const> key) -> int override;
auto start_auth() -> int { return auth_.start(); }
auto process_auth(std::span<u8 const> data) -> int
{
return auth_.process_pkt(data);
}
private:
friend class adapter;
adapter& adapter_;
u8 id_;
gip_hardware hardware_{};
struct info_element {
u8 count = 0;
std::vector<u8> data;
};
std::unique_ptr<info_element> client_commands_;
std::unique_ptr<info_element> firmware_versions_;
std::unique_ptr<info_element> audio_formats_;
std::unique_ptr<info_element> capabilities_out_;
std::unique_ptr<info_element> capabilities_in_;
std::vector<std::string> classes_;
std::unique_ptr<info_element> interfaces_;
std::unique_ptr<info_element> hid_descriptor_;
gip_audio_config audio_config_in_;
gip_audio_config audio_config_out_;
// Chunk reassembly buffers (large packets are split into chunks).
struct chunk_buffer {
gip_header header;
u32 length = 0;
std::vector<u8> data;
};
std::unique_ptr<chunk_buffer> chunk_buf_out_;
std::unique_ptr<chunk_buffer> chunk_buf_in_;
xone::auth::auth auth_{*this};
};
class adapter {
public:
adapter(transport& transport, client_listener& listener,
int audio_packet_count = 8);
// Dispatch one USB data buffer (may contain multiple GIP packets).
auto process_buffer(std::span<u8 const> data) -> int;
auto get_client(u8 id) -> client*;
auto client_count() const -> int;
private:
friend class client;
auto send_pkt(client& c, gip_header& hdr, void const* data) -> int;
auto send_pkt_simple(gip_header& hdr, void const* data) -> int;
auto init_chunk_buffer(gip_header const& hdr,
std::unique_ptr<client::chunk_buffer>& buf) -> int;
auto send_remaining_chunks(client& c) -> int;
auto request_identification(client& c) -> int;
auto acknowledge_pkt(client& c, gip_header const& ack) -> int;
auto remove_client(client& c) -> void;
auto free_client_info(client& c) -> void;
auto add_client(client& c) -> void;
auto make_audio_config(gip_audio_config& cfg) -> int;
auto set_audio_format(client& c, u8 in, u8 out) -> int;
auto set_audio_format_chat(client& c, u8 in_out) -> int;
auto dispatch_pkt(client& c, gip_header const& hdr, void const* data, u32 len) -> int;
auto process_pkt(client& c, gip_header& hdr, void const* data) -> int;
auto process_pkt_chunked(client& c, gip_header const& hdr, void const* data) -> int;
auto handle_pkt_acknowledge(client& c, void const* data, u32 len) -> int;
auto handle_pkt_announce(client& c, void const* data, u32 len) -> int;
auto handle_pkt_status(client& c, void const* data, u32 len) -> int;
auto handle_pkt_identify(client& c, void const* data, u32 len) -> int;
auto handle_pkt_authenticate(client& c, void const* data, u32 len) -> int;
auto handle_pkt_virtual_key(client& c, void const* data, u32 len) -> int;
auto handle_pkt_audio_control(client& c, void const* data, u32 len) -> int;
auto handle_pkt_hid_report(client& c, void const* data, u32 len) -> int;
auto handle_pkt_input(client& c, void const* data, u32 len) -> int;
auto handle_pkt_audio_samples(client& c, void const* data, u32 len) -> int;
auto parse_info_element(std::span<u8 const> data, u16 offset, int item_length,
std::unique_ptr<client::info_element>& out) -> int;
auto parse_client_commands(client& c, u16 const offsets[8],
std::span<u8 const> data) -> int;
auto parse_firmware_versions(client& c, u16 const offsets[8],
std::span<u8 const> data) -> int;
auto parse_audio_formats(client& c, u16 const offsets[8],
std::span<u8 const> data) -> int;
auto parse_capabilities(client& c, u16 const offsets[8],
std::span<u8 const> data) -> int;
auto parse_classes(client& c, std::span<u8 const> data, u16 offset) -> int;
auto parse_interfaces(client& c, u16 const offsets[8],
std::span<u8 const> data) -> int;
auto parse_hid_descriptor(client& c, u16 const offsets[8],
std::span<u8 const> data) -> int;
transport& transport_;
client_listener& listener_;
int audio_packet_count_;
std::array<std::unique_ptr<client>, k_max_clients> clients_;
u8 data_sequence_ = 0;
u8 audio_sequence_ = 0;
};
} // namespace xone::gip