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:
+288
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user