diff --git a/CMakeLists.txt b/CMakeLists.txt index 1413372..d272ef5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ if(NOT CMAKE_GENERATOR MATCHES "^(Ninja|Xcode)$") endif() cmake_minimum_required(VERSION 3.21) -project(xone_macos VERSION 0.1.0 LANGUAGES CXX Swift) +project(xone_macos VERSION 0.1.1 LANGUAGES CXX Swift) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -35,11 +35,15 @@ target_compile_definitions(xone_usb PRIVATE ${BASE_DEFINITIONS}) # Auth + crypto (AES-CCMP key setup, ECDH, RSA — port from auth/) add_library(xone_auth STATIC "src/auth/crypto.cpp" + "src/auth/auth.cpp" ) target_include_directories(xone_auth PUBLIC "${CMAKE_SOURCE_DIR}/include") target_compile_features(xone_auth PRIVATE cxx_std_23) target_compile_options(xone_auth PRIVATE ${BASE_OPTIONS}) target_compile_definitions(xone_auth PRIVATE ${BASE_DEFINITIONS}) +find_library(XONE_SECURITY Security) +find_library(XONE_COREFOUNDATION CoreFoundation) +target_link_libraries(xone_auth PUBLIC "${XONE_SECURITY}" "${XONE_COREFOUNDATION}") # MT76 chip protocol (firmware load, EFUSE, radio init, beacons) add_library(xone_mt76 STATIC diff --git a/include/auth/auth.hpp b/include/auth/auth.hpp new file mode 100644 index 0000000..863a990 --- /dev/null +++ b/include/auth/auth.hpp @@ -0,0 +1,92 @@ +#pragma once + +// ============================================================================== +// GIP authentication handshake +// ============================================================================== +// Port target: medusalix/xone auth/auth.c + auth/auth.h. +// +// The kernel driver runs the RSA/ECDH exchanges on workqueues and speaks to +// the controller through gip_send_authenticate(). Here the exchanges run +// synchronously and the GIP layer is reached through the auth_sink callback +// interface, which the gip::client implements. +// ============================================================================== + +#include +#include +#include + +#include "auth/crypto.hpp" + +namespace xone::auth { + +using u8 = std::uint8_t; +using u16 = std::uint16_t; + +// Wire sizes from the upstream auth.h. +inline constexpr auto k_trailer_len = 8; +inline constexpr auto k_random_len = 32; +inline constexpr auto k_certificate_max_len = 1024; +inline constexpr auto k_pubkey_len = 270; // v1 RSA public key +inline constexpr auto k_secret_len = 48; +inline constexpr auto k_encrypted_pms_len = 256; +inline constexpr auto k_transcript_len = 32; +inline constexpr auto k_session_key_len = 16; +inline constexpr auto k_pubkey2_len = 64; // v2 EC public key (X || Y) +inline constexpr auto k_secret2_len = 32; + +// Callback interface implemented by the GIP layer. +class auth_sink { +public: + virtual ~auth_sink() = default; + + // Send an AUTHENTICATE command packet to the controller. + virtual auto send(std::span pkt, bool acknowledge) -> int = 0; + + // Install the derived AES-CCMP session key (16 bytes). + virtual auto set_encryption_key(std::span key) -> int = 0; +}; + +// Runs the handshake for one controller. Port of struct gip_auth. +class auth { +public: + explicit auth(auth_sink& sink); + + // Begin the handshake by sending the (v1) host hello. + auto start() -> int; + + // Process one incoming AUTHENTICATE packet from the controller. + auto process_pkt(std::span data) -> int; + +private: + auto send_pkt(u8 cmd, void const* pkt, std::size_t len) -> int; + auto request_pkt(u8 cmd, std::uint16_t len) -> int; + auto send_hello() -> int; + auto send_hello2() -> int; + auto send_finish(u8 cmd) -> int; + auto send_complete() -> int; + auto exchange_rsa() -> void; + auto exchange_ecdh() -> void; + auto complete_handshake() -> void; + auto handle_pkt_acknowledge() -> int; + auto handle_pkt_data(std::span data) -> int; + auto dispatch_pkt(u8 cmd, std::span data) -> int; + auto handle_hello(std::span data) -> int; + auto handle_certificate(std::span data) -> int; + auto handle_finish(std::span data) -> int; + auto handle_hello2(std::span data) -> int; + auto handle_certificate2(std::span data) -> int; + auto handle_pubkey(std::span data) -> int; + auto get_transcript() -> std::array; + + auth_sink& sink_; + sha256 transcript_; + u8 last_sent_command_ = 0; + + std::array random_host_{}; + std::array random_client_{}; + std::array pubkey_client_{}; + std::array pubkey_client2_{}; + std::array master_secret_{}; +}; + +} // namespace xone::auth diff --git a/include/auth/crypto.hpp b/include/auth/crypto.hpp index f1dc904..62f547f 100644 --- a/include/auth/crypto.hpp +++ b/include/auth/crypto.hpp @@ -1,16 +1,110 @@ #pragma once // ============================================================================== -// Auth + crypto +// Auth + crypto primitives // ============================================================================== -// Port target: medusalix/xone auth/auth.c + auth/crypto.c. -// ECDH (P-256) key agreement, RSA encryption, SHA transcript/PRF for the GIP -// authentication handshake. AES-CCMP frame encryption itself runs on-chip; -// this layer only derives and installs keys into the MT76 WCID registers. +// Port target: medusalix/xone auth/crypto.c + auth/crypto.h. +// +// The kernel driver uses the in-kernel crypto API (crypto_shash for +// SHA-256/HMAC, crypto_akcipher for PKCS#1 RSA, crypto_kpp for ECDH +// P-256). These are replaced here with user-space equivalents: +// - SHA-256 / HMAC-SHA256: CommonCrypto (part of libSystem) +// - RSA PKCS#1 v1.5: Security.framework +// - ECDH P-256: self-contained implementation (no public +// macOS C API exists for EC key agreement) +// - randomness: arc4random_buf +// +// AES-CCMP frame encryption itself runs on the MT76 chip; this layer +// only derives keys and installs them (see xone_mt76_set_client_key). // ============================================================================== +#include +#include +#include +#include + +#include + namespace xone::auth { -// TODO(phase 1): ECDH/RSA/SHA primitives (CommonCrypto / Security.framework). +using u8 = std::uint8_t; + +inline constexpr auto k_sha256_len = 32; + +// -------------------------------------------------------------------------- +// SHA-256 +// -------------------------------------------------------------------------- +// Incremental SHA-256. Instances are cheap to copy; a copy is an +// independent snapshot of the current hash state, which the handshake +// uses to compute the running transcript without losing progress. +class sha256 { +public: + sha256(); + ~sha256() = default; + sha256(sha256 const&) = default; + auto operator=(sha256 const&) -> sha256& = default; + + auto update(std::span data) -> void; + auto finalize(std::array& out) -> void; + +private: + CC_SHA256_CTX ctx_; +}; + +// -------------------------------------------------------------------------- +// HMAC-SHA256 +// -------------------------------------------------------------------------- +class hmac_sha256 { +public: + explicit hmac_sha256(std::span key); + + auto update(std::span data) -> void; + auto finalize(std::array& out) -> void; + +private: + sha256 inner_; + sha256 outer_; +}; + +// -------------------------------------------------------------------------- +// TLS P_SHA256 PRF +// -------------------------------------------------------------------------- +// Expands key + label + seed into out_len bytes, exactly like the kernel +// driver's gip_auth_compute_prf(). Used to derive the master secret, the +// handshake transcript checks, and the session key. +auto prf_sha256(std::span key, std::string_view label, + std::span seed, std::span out) -> void; + +// -------------------------------------------------------------------------- +// RSA (PKCS#1 v1.5) +// -------------------------------------------------------------------------- +// Encrypts plaintext with a DER RSAPublicKey (the 270-byte ASN.1 SEQUENCE +// that Microsoft controllers embed in their X.509 certificate). out must +// be at least the RSA modulus size (256 bytes for 2048-bit keys). +auto rsa_encrypt_pkcs1(std::span der_key, + std::span plaintext, std::span out) + -> bool; + +// -------------------------------------------------------------------------- +// ECDH (P-256) +// -------------------------------------------------------------------------- +using ec_scalar = std::array; // big-endian private key / x-coord +using ec_point = std::array; // X || Y, big-endian, no 0x04 prefix + +// q = d * G. +auto ec_base_point_multiply(ec_scalar const& d, ec_point& q) -> void; + +// shared = x-coordinate of d * peer (the standard P-256 shared secret). +// Returns false if peer is not a valid point on the curve. +auto ec_compute_shared(ec_scalar const& d, ec_point const& peer, + ec_scalar& shared) -> bool; + +// Generate a random keypair. +auto ec_generate_keypair(ec_scalar& d, ec_point& q) -> void; + +// -------------------------------------------------------------------------- +// Randomness +// -------------------------------------------------------------------------- +auto random_bytes(std::span out) -> void; } // namespace xone::auth diff --git a/include/common/log.hpp b/include/common/log.hpp new file mode 100644 index 0000000..d459c68 --- /dev/null +++ b/include/common/log.hpp @@ -0,0 +1,48 @@ +#pragma once + +// ============================================================================== +// Minimal printf-style logger +// ============================================================================== +// Replaces the kernel's dev_dbg/dev_err from medusalix/xone. Writes to +// stderr; the Swift app layer can redirect or swallow it later. +// ============================================================================== + +#include +#include + +namespace xone { + +enum class log_level { + debug = 0, + info, + warn, + error, +}; + +inline auto log_msg(log_level level, char const* fmt, ...) -> void +{ + char const* tag = "?"; + switch (level) { + case log_level::debug: + tag = "debug"; + break; + case log_level::info: + tag = "info"; + break; + case log_level::warn: + tag = "warn"; + break; + case log_level::error: + tag = "error"; + break; + } + + std::fprintf(stderr, "[%s] ", tag); + va_list ap; + va_start(ap, fmt); + std::vfprintf(stderr, fmt, ap); + va_end(ap); + std::fprintf(stderr, "\n"); +} + +} // namespace xone diff --git a/include/common/types.hpp b/include/common/types.hpp index c2f37f2..397cf27 100644 --- a/include/common/types.hpp +++ b/include/common/types.hpp @@ -10,6 +10,11 @@ #include +// Mark a struct as packed (no padding), matching the wire formats of the +// MT76 chip and the GIP protocol. clang-specific, like the rest of this +// macOS-only project. +#define XONE_PACKED __attribute__((packed)) + namespace xone { // 128-bit GUID used by the GIP protocol (port of the kernel guid_t). @@ -46,4 +51,18 @@ inline auto store_le32(void *p, std::uint32_t v) -> void b[3] = static_cast((v >> 24) & 0xFF); } +// Big-endian 16-bit load/store (used by the auth handshake headers). +inline auto load_be16(void const *p) -> std::uint16_t +{ + auto const *b = static_cast(p); + return static_cast((b[0] << 8) | b[1]); +} + +inline auto store_be16(void *p, std::uint16_t v) -> void +{ + auto *b = static_cast(p); + b[0] = static_cast((v >> 8) & 0xFF); + b[1] = static_cast(v & 0xFF); +} + } // namespace xone diff --git a/include/gip/protocol.hpp b/include/gip/protocol.hpp index ce537bc..01cafec 100644 --- a/include/gip/protocol.hpp +++ b/include/gip/protocol.hpp @@ -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 +#include +#include +#include +#include +#include + +#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 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 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) -> void {} + virtual auto on_hid_report(class client&, std::span) -> 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) -> 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 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 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 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 pkt, bool acknowledge) -> int override; + auto set_encryption_key(std::span key) -> int override; + + auto start_auth() -> int { return auth_.start(); } + auto process_auth(std::span 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 data; + }; + + std::unique_ptr client_commands_; + std::unique_ptr firmware_versions_; + std::unique_ptr audio_formats_; + std::unique_ptr capabilities_out_; + std::unique_ptr capabilities_in_; + std::vector classes_; + std::unique_ptr interfaces_; + std::unique_ptr 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 data; + }; + std::unique_ptr chunk_buf_out_; + std::unique_ptr 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 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& 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 data, u16 offset, int item_length, + std::unique_ptr& out) -> int; + auto parse_client_commands(client& c, u16 const offsets[8], + std::span data) -> int; + auto parse_firmware_versions(client& c, u16 const offsets[8], + std::span data) -> int; + auto parse_audio_formats(client& c, u16 const offsets[8], + std::span data) -> int; + auto parse_capabilities(client& c, u16 const offsets[8], + std::span data) -> int; + auto parse_classes(client& c, std::span data, u16 offset) -> int; + auto parse_interfaces(client& c, u16 const offsets[8], + std::span data) -> int; + auto parse_hid_descriptor(client& c, u16 const offsets[8], + std::span data) -> int; + + transport& transport_; + client_listener& listener_; + int audio_packet_count_; + + std::array, k_max_clients> clients_; + u8 data_sequence_ = 0; + u8 audio_sequence_ = 0; +}; } // namespace xone::gip diff --git a/src/auth/auth.cpp b/src/auth/auth.cpp new file mode 100644 index 0000000..65ebe6c --- /dev/null +++ b/src/auth/auth.cpp @@ -0,0 +1,483 @@ +// GIP authentication handshake (port of medusalix/xone auth/auth.c). + +#include "auth/auth.hpp" + +#include +#include + +#include + +#include "common/log.hpp" +#include "common/types.hpp" + +namespace xone::auth { + +namespace { + +enum : u8 { + ctx_handshake = 0x00, + ctx_control = 0x01, +}; + +enum : u8 { + cmd_host_hello = 0x01, + cmd_client_hello = 0x02, + cmd_client_certificate = 0x03, + cmd_host_secret = 0x05, + cmd_host_finish = 0x07, + cmd_client_finish = 0x08, + + cmd2_host_hello = 0x21, + cmd2_client_hello = 0x22, + cmd2_client_certificate = 0x23, + cmd2_client_pubkey = 0x24, + cmd2_host_pubkey = 0x25, + cmd2_host_finish = 0x26, + cmd2_client_finish = 0x27, +}; + +enum : u8 { + ctrl_complete = 0x00, + ctrl_reset = 0x01, +}; + +enum : u8 { + opt_acknowledge = 1 << 0, + opt_request = 1 << 1, + opt_from_host = 1 << 6, +}; + +struct header_handshake { + u8 context; + u8 options; + u8 error; + u8 command; + u16 length; +} XONE_PACKED; + +struct header_data { + u8 command; + u8 version; + u16 length; +} XONE_PACKED; + +struct header_full { + header_handshake handshake; + header_data data; +} XONE_PACKED; + +struct header_control { + u8 context; + u8 control; +} XONE_PACKED; + +struct pkt_request { + header_handshake header; + std::array trailer; +} XONE_PACKED; + +struct pkt_host_hello { + header_full header; + std::array random; + std::array unknown1; + std::array unknown2; + std::array trailer; +} XONE_PACKED; + +struct pkt_host_secret { + header_full header; + std::array encrypted_pms; + std::array trailer; +} XONE_PACKED; + +struct pkt_host_finish { + header_full header; + std::array transcript; + std::array trailer; +} XONE_PACKED; + +struct pkt_client_hello { + std::array random; + std::array unknown; +} XONE_PACKED; + +struct pkt_client_finish { + std::array transcript; + std::array unknown; +} XONE_PACKED; + +struct pkt2_host_hello { + header_full header; + std::array random; + std::array unknown; + std::array trailer; +} XONE_PACKED; + +struct pkt2_host_pubkey { + header_full header; + std::array pubkey; + std::array trailer; +} XONE_PACKED; + +struct pkt2_client_hello { + std::array random; + std::array unknown1; + std::array unknown2; +} XONE_PACKED; + +struct pkt2_client_cert { + std::array header; + std::array unknown1; + std::array chip; + std::array revision; + std::array unknown2; +} XONE_PACKED; + +struct pkt2_client_pubkey { + std::array pubkey; + std::array unknown; +} XONE_PACKED; + +} // namespace + +auth::auth(auth_sink& sink) + : sink_(sink) +{ +} + +auto auth::start() -> int +{ + return send_hello(); +} + +auto auth::process_pkt(std::span data) -> int +{ + auto const* hdr = reinterpret_cast(data.data()); + + if (data.size() < sizeof(*hdr)) + return -EINVAL; + + if (hdr->error) + return -EPROTO; + + if (hdr->options & opt_acknowledge) { + if (hdr->command == 0x01) + return handle_pkt_acknowledge(); + + log_msg(log_level::error, "auth: handshake failed: 0x%02x", hdr->command); + return -EPROTO; + } + + return handle_pkt_data(data); +} + +auto auth::send_pkt(u8 cmd, void const* pkt, std::size_t len) -> int +{ + auto* hdr = reinterpret_cast(const_cast(static_cast(pkt))); + u16 data_len = static_cast(len - sizeof(hdr->handshake) - k_trailer_len); + + hdr->handshake.context = ctx_handshake; + hdr->handshake.options = opt_acknowledge | opt_from_host; + hdr->handshake.error = 0; + hdr->handshake.command = cmd; + store_be16(&hdr->handshake.length, data_len); + + hdr->data.command = cmd; + hdr->data.version = cmd >= cmd2_host_hello ? 0x02 : 0x01; + store_be16(&hdr->data.length, static_cast(data_len - sizeof(hdr->data))); + + last_sent_command_ = cmd; + transcript_.update( + {reinterpret_cast(hdr) + sizeof(hdr->handshake), data_len}); + + return sink_.send({reinterpret_cast(hdr), len}, true); +} + +auto auth::request_pkt(u8 cmd, std::uint16_t len) -> int +{ + pkt_request req{}; + u16 data_len = static_cast(len + sizeof(header_data)); + + req.header.context = ctx_handshake; + req.header.options = opt_request | opt_from_host; + req.header.error = 0; + req.header.command = cmd; + store_be16(&req.header.length, data_len); + + return sink_.send({reinterpret_cast(&req), sizeof(req)}, true); +} + +auto auth::send_hello() -> int +{ + pkt_host_hello pkt{}; + random_bytes(random_host_); + std::memcpy(pkt.random.data(), random_host_.data(), k_random_len); + return send_pkt(cmd_host_hello, &pkt.header, sizeof(pkt)); +} + +auto auth::send_finish(u8 cmd) -> int +{ + pkt_host_finish pkt{}; + auto transcript = get_transcript(); + prf_sha256(master_secret_, "Host Finished", transcript, pkt.transcript); + return send_pkt(cmd, &pkt.header, sizeof(pkt)); +} + +auto auth::send_complete() -> int +{ + header_control hdr{}; + hdr.context = ctx_control; + hdr.control = ctrl_complete; + return sink_.send({reinterpret_cast(&hdr), sizeof(hdr)}, false); +} + +auto auth::get_transcript() -> std::array +{ + sha256 snap = transcript_; + std::array out{}; + snap.finalize(out); + return out; +} + +auto auth::exchange_rsa() -> void +{ + pkt_host_secret pkt{}; + std::array random{}; + std::array pms{}; + + std::copy(random_host_.begin(), random_host_.end(), random.begin()); + std::copy(random_client_.begin(), random_client_.end(), + random.begin() + k_random_len); + + random_bytes(pms); + + std::array encrypted{}; + if (!rsa_encrypt_pkcs1(pubkey_client_, pms, encrypted)) { + log_msg(log_level::error, "auth: encrypt RSA failed"); + return; + } + std::memcpy(pkt.encrypted_pms.data(), encrypted.data(), k_encrypted_pms_len); + + prf_sha256(pms, "Master Secret", random, master_secret_); + + if (send_pkt(cmd_host_secret, &pkt.header, sizeof(pkt))) + log_msg(log_level::error, "auth: send pkt failed"); +} + +auto auth::exchange_ecdh() -> void +{ + pkt2_host_pubkey pkt{}; + std::array random{}; + std::array secret{}; + + std::copy(random_host_.begin(), random_host_.end(), random.begin()); + std::copy(random_client_.begin(), random_client_.end(), + random.begin() + k_random_len); + + ec_scalar d{}; + ec_point q{}; + ec_generate_keypair(d, q); + std::memcpy(pkt.pubkey.data(), q.data(), k_pubkey2_len); + + ec_scalar shared_x{}; + if (!ec_compute_shared(d, pubkey_client2_, shared_x)) { + log_msg(log_level::error, "auth: compute ECDH failed"); + return; + } + + // The PRF key is SHA-256 of the raw shared x-coordinate. + sha256 h; + h.update(shared_x); + h.finalize(secret); + + prf_sha256(secret, "Master Secret", random, master_secret_); + + if (send_pkt(cmd2_host_pubkey, &pkt.header, sizeof(pkt))) + log_msg(log_level::error, "auth: send pkt failed"); +} + +auto auth::complete_handshake() -> void +{ + std::array random{}; + std::array key{}; + + std::copy(random_host_.begin(), random_host_.end(), random.begin()); + std::copy(random_client_.begin(), random_client_.end(), + random.begin() + k_random_len); + + prf_sha256(master_secret_, + "EXPORTER DAWN data channel session key for controller", random, + key); + + if (send_complete()) + log_msg(log_level::error, "auth: send complete failed"); + + if (sink_.set_encryption_key(key)) + log_msg(log_level::error, "auth: set encryption key failed"); +} + +auto auth::handle_pkt_acknowledge() -> int +{ + switch (last_sent_command_) { + case cmd2_host_hello: + return request_pkt(cmd2_client_hello, sizeof(pkt2_client_hello)); + case cmd2_host_pubkey: + return send_finish(cmd2_host_finish); + case cmd2_host_finish: + return request_pkt(cmd2_client_finish, sizeof(pkt_client_finish)); + case cmd_host_hello: + return request_pkt(cmd_client_hello, sizeof(pkt_client_hello)); + case cmd_host_secret: + return send_finish(cmd_host_finish); + case cmd_host_finish: + return request_pkt(cmd_client_finish, sizeof(pkt_client_finish)); + default: + return -EPROTO; + } +} + +auto auth::handle_pkt_data(std::span data) -> int +{ + auto const* hdr = reinterpret_cast(data.data()); + + if (data.size() < sizeof(*hdr)) + return -EINVAL; + + // The controller upgraded to auth v2: restart the handshake. + if (hdr->handshake.command != hdr->data.command) { + log_msg(log_level::debug, "auth: protocol upgrade to v2"); + transcript_ = sha256{}; + return send_hello2(); + } + + int err = dispatch_pkt(hdr->data.command, data.subspan(sizeof(*hdr))); + if (err) + return err; + + transcript_.update(data.subspan(sizeof(hdr->handshake))); + return 0; +} + +auto auth::dispatch_pkt(u8 cmd, std::span data) -> int +{ + switch (cmd) { + case cmd2_client_hello: + return handle_hello2(data); + case cmd2_client_certificate: + return handle_certificate2(data); + case cmd2_client_pubkey: + return handle_pubkey(data); + case cmd2_client_finish: + return handle_finish(data); + case cmd_client_hello: + return handle_hello(data); + case cmd_client_certificate: + return handle_certificate(data); + case cmd_client_finish: + return handle_finish(data); + default: + return -EPROTO; + } +} + +auto auth::handle_hello(std::span data) -> int +{ + auto const* pkt = reinterpret_cast(data.data()); + + if (data.size() < sizeof(*pkt)) + return -EINVAL; + + std::memcpy(random_client_.data(), pkt->random.data(), k_random_len); + return request_pkt(cmd_client_certificate, k_certificate_max_len); +} + +auto auth::handle_certificate(std::span data) -> int +{ + // The client cert embeds a DER RSAPublicKey: ASN.1 SEQUENCE of length + // 0x010a. Search for its header and copy the whole 270-byte key. + static constexpr u8 asn1_seq[] = {0x30, 0x82, 0x01, 0x0a}; + + if (data.size() > k_certificate_max_len) + return -EINVAL; + + for (std::size_t i = 0; i + sizeof(asn1_seq) <= data.size(); ++i) { + if (std::memcmp(data.data() + i, asn1_seq, sizeof(asn1_seq))) + continue; + + if (i + k_pubkey_len > data.size()) + return -EINVAL; + + std::memcpy(pubkey_client_.data(), data.data() + i, k_pubkey_len); + exchange_rsa(); + return 0; + } + + return -EPROTO; +} + +auto auth::handle_finish(std::span data) -> int +{ + auto const* pkt = reinterpret_cast(data.data()); + + if (data.size() < sizeof(*pkt)) + return -EINVAL; + + auto transcript = get_transcript(); + std::array finished{}; + prf_sha256(master_secret_, "Device Finished", transcript, finished); + + if (std::memcmp(pkt->transcript.data(), finished.data(), k_transcript_len)) { + log_msg(log_level::error, "auth: transcript mismatch"); + return -EPROTO; + } + + complete_handshake(); + return 0; +} + +auto auth::send_hello2() -> int +{ + pkt2_host_hello pkt{}; + random_bytes(random_host_); + std::memcpy(pkt.random.data(), random_host_.data(), k_random_len); + return send_pkt(cmd2_host_hello, &pkt.header, sizeof(pkt)); +} + +auto auth::handle_hello2(std::span data) -> int +{ + auto const* pkt = reinterpret_cast(data.data()); + + if (data.size() < sizeof(*pkt)) + return -EINVAL; + + std::memcpy(random_client_.data(), pkt->random.data(), k_random_len); + return request_pkt(cmd2_client_certificate, sizeof(pkt2_client_cert)); +} + +auto auth::handle_certificate2(std::span data) -> int +{ + auto const* pkt = reinterpret_cast(data.data()); + + if (data.size() < sizeof(*pkt)) + return -EINVAL; + + log_msg(log_level::debug, "auth: chip=%.*s, revision=%.*s", + static_cast(sizeof(pkt->chip)), pkt->chip.data(), + static_cast(sizeof(pkt->revision)), pkt->revision.data()); + + return request_pkt(cmd2_client_pubkey, sizeof(pkt2_client_pubkey)); +} + +auto auth::handle_pubkey(std::span data) -> int +{ + auto const* pkt = reinterpret_cast(data.data()); + + if (data.size() < sizeof(*pkt)) + return -EINVAL; + + std::memcpy(pubkey_client2_.data(), pkt->pubkey.data(), k_pubkey2_len); + exchange_ecdh(); + return 0; +} + +} // namespace xone::auth diff --git a/src/auth/crypto.cpp b/src/auth/crypto.cpp index f37d669..b5b1eb1 100644 --- a/src/auth/crypto.cpp +++ b/src/auth/crypto.cpp @@ -1,7 +1,554 @@ -// Auth + crypto -// TODO(phase 1): port from medusalix/xone auth/auth.c + auth/crypto.c. +// Auth + crypto primitives (CommonCrypto / Security.framework / P-256 ECDH) +// Port target: medusalix/xone auth/crypto.c. #include "auth/crypto.hpp" +#include +#include +#include +#include + +#include + +#include +#include + namespace xone::auth { + +namespace { + +// -------------------------------------------------------------------------- +// Field arithmetic over GF(p), p = P-256, in Montgomery form. +// -------------------------------------------------------------------------- +// Values are stored as v * R mod p with R = 2^256. Multiplication is a +// Montgomery product (schoolbook 4x4 -> 8 limbs, then REDC). Addition and +// subtraction work directly on the stored values with a single conditional +// p-subtract / p-add, which is valid for the Montgomery representation. +// -------------------------------------------------------------------------- +using u64 = std::uint64_t; +using u128 = unsigned __int128; +using s128 = __int128; + +struct fe { + u64 l[4]; +}; + +// Modulus p in normal form (used by REDC and the conditional reductions). +constexpr fe k_p = {{0xffffffffffffffffULL, 0x00000000ffffffffULL, + 0x0000000000000000ULL, 0xffffffff00000001ULL}}; + +constexpr fe k_zero = {{0, 0, 0, 0}}; + +// Montgomery form of small constants and curve parameters (precomputed). +constexpr fe k_one = {{0x0000000000000001ULL, 0xffffffff00000000ULL, + 0xffffffffffffffffULL, 0x00000000fffffffeULL}}; +constexpr fe k_two = {{0x0000000000000002ULL, 0xfffffffe00000000ULL, + 0xffffffffffffffffULL, 0x00000001fffffffdULL}}; +constexpr fe k_three = {{0x0000000000000003ULL, 0xfffffffd00000000ULL, + 0xffffffffffffffffULL, 0x00000002fffffffcULL}}; +constexpr fe k_four = {{0x0000000000000004ULL, 0xfffffffc00000000ULL, + 0xffffffffffffffffULL, 0x00000003fffffffbULL}}; +constexpr fe k_eight = {{0x0000000000000008ULL, 0xfffffff800000000ULL, + 0xffffffffffffffffULL, 0x00000007fffffff7ULL}}; +// R^2 mod p, for converting normal form into Montgomery form. +constexpr fe k_r2 = {{0x0000000000000003ULL, 0xfffffffbffffffffULL, + 0xfffffffffffffffeULL, 0x00000004fffffffdULL}}; +constexpr fe k_mgx = {{0x79e730d418a9143cULL, 0x75ba95fc5fedb601ULL, + 0x79fb732b77622510ULL, 0x18905f76a53755c6ULL}}; +constexpr fe k_mgy = {{0xddf25357ce95560aULL, 0x8b4ab8e4ba19e45cULL, + 0xd2e88688dd21f325ULL, 0x8571ff1825885d85ULL}}; +constexpr fe k_mb = {{0xd89cdf6229c4bddfULL, 0xacf005cd78843090ULL, + 0xe5a220abf7212ed6ULL, 0xdc30061d04874834ULL}}; + +auto fe_is_zero(fe const& a) -> bool +{ + return a.l[0] == 0 && a.l[1] == 0 && a.l[2] == 0 && a.l[3] == 0; +} + +auto fe_eq(fe const& a, fe const& b) -> bool +{ + return a.l[0] == b.l[0] && a.l[1] == b.l[1] && a.l[2] == b.l[2] && + a.l[3] == b.l[3]; +} + +// a >= b, as unsigned integers. +auto fe_ge(fe const& a, fe const& b) -> bool +{ + for (int i = 3; i >= 0; --i) { + if (a.l[i] > b.l[i]) + return true; + if (a.l[i] < b.l[i]) + return false; + } + return true; +} + +// a - b, modulo 2^256 (wraps if a < b). +auto fe_sub_raw(fe const& a, fe const& b) -> fe +{ + fe r{}; + s128 borrow = 0; + for (int i = 0; i < 4; ++i) { + s128 t = static_cast(a.l[i]) - static_cast(b.l[i]) - borrow; + if (t < 0) { + t += static_cast(static_cast(1) << 63) * 2; // + 2^64 + borrow = 1; + } else { + borrow = 0; + } + r.l[i] = static_cast(t); + } + return r; +} + +auto fe_add(fe const& a, fe const& b) -> fe +{ + fe r{}; + u64 carry = 0; + for (int i = 0; i < 4; ++i) { + u128 t = static_cast(a.l[i]) + b.l[i] + carry; + r.l[i] = static_cast(t); + carry = static_cast(t >> 64); + } + if (carry || fe_ge(r, k_p)) + r = fe_sub_raw(r, k_p); + return r; +} + +auto fe_sub(fe const& a, fe const& b) -> fe +{ + if (fe_ge(a, b)) + return fe_sub_raw(a, b); + return fe_sub_raw(k_p, fe_sub_raw(b, a)); +} + +// Montgomery reduction: r = t * 2^-256 mod p, t given as 9 limbs (t[8] is +// the possible 257th carry bit, initially zero). +auto fe_redc(u64 const t9[9]) -> fe +{ + u64 t[9]; + std::memcpy(t, t9, sizeof(t)); + + // n0 = -p^-1 mod 2^64 = 1 for P-256 (p mod 2^64 == -1). + for (int i = 0; i < 4; ++i) { + u64 m = t[i]; + u64 carry = 0; + for (int j = 0; j < 4; ++j) { + u128 x = static_cast(m) * k_p.l[j] + t[i + j] + carry; + t[i + j] = static_cast(x); + carry = static_cast(x >> 64); + } + int k = i + 4; + while (carry && k < 9) { + u128 x = static_cast(t[k]) + carry; + t[k] = static_cast(x); + carry = static_cast(x >> 64); + ++k; + } + } + + fe r{{t[4], t[5], t[6], t[7]}}; + if (t[8] != 0 || fe_ge(r, k_p)) + r = fe_sub_raw(r, k_p); + if (fe_ge(r, k_p)) // safety net, unreachable for well-formed inputs + r = fe_sub_raw(r, k_p); + return r; +} + +auto fe_mul(fe const& a, fe const& b) -> fe +{ + u64 t[9] = {0}; + for (int i = 0; i < 4; ++i) { + u64 carry = 0; + for (int j = 0; j < 4; ++j) { + u128 x = static_cast(a.l[i]) * b.l[j] + t[i + j] + carry; + t[i + j] = static_cast(x); + carry = static_cast(x >> 64); + } + t[i + 4] += carry; + } + return fe_redc(t); +} + +auto fe_sqr(fe const& a) -> fe +{ + return fe_mul(a, a); +} + +auto fe_inv(fe const& a) -> fe +{ + // a^(p-2) mod p via square-and-multiply (result stays in Montgomery form). + fe result = k_one; + fe base = a; + u64 e[4] = {0xfffffffffffffffdULL, 0x00000000ffffffffULL, 0x0ULL, + 0xffffffff00000001ULL}; + for (int i = 3; i >= 0; --i) { + for (int b = 63; b >= 0; --b) { + result = fe_sqr(result); + if ((e[i] >> b) & 1) + result = fe_mul(result, base); + } + } + return result; +} + +auto load_be64(u8 const* p) -> u64 +{ + u64 v = 0; + for (int i = 0; i < 8; ++i) + v = (v << 8) | p[i]; + return v; +} + +auto store_be64(u8* p, u64 v) -> void +{ + for (int i = 7; i >= 0; --i) { + p[i] = static_cast(v); + v >>= 8; + } +} + +// Normal form (big-endian bytes) -> Montgomery form. +auto fe_from_bytes(std::array const& b) -> fe +{ + fe n{}; + for (int i = 0; i < 4; ++i) + n.l[i] = load_be64(b.data() + (3 - i) * 8); + return fe_mul(n, k_r2); +} + +// Montgomery form -> normal form (big-endian bytes). +auto fe_from_mont(fe const& a) -> fe +{ + u64 t[9] = {a.l[0], a.l[1], a.l[2], a.l[3], 0, 0, 0, 0, 0}; + return fe_redc(t); +} + +auto fe_to_bytes(fe const& a) -> std::array +{ + fe n = fe_from_mont(a); + std::array out{}; + for (int i = 0; i < 4; ++i) + store_be64(out.data() + (3 - i) * 8, n.l[i]); + return out; +} + +// -------------------------------------------------------------------------- +// Jacobian-coordinate point arithmetic on y^2 = x^3 - 3x + b. +// -------------------------------------------------------------------------- +struct jac { + fe x, y, z; +}; + +struct aff { + fe x, y; +}; + +// Point doubling, a = -3 (EFD dbl-2001-b). +auto jac_dbl(jac const& p) -> jac +{ + if (fe_is_zero(p.z) || fe_is_zero(p.y)) + return {k_zero, k_zero, k_zero}; + + fe zz = fe_sqr(p.z); + fe yy = fe_sqr(p.y); + fe beta = fe_mul(p.x, yy); + fe alpha = fe_mul(k_three, fe_mul(fe_sub(p.x, zz), fe_add(p.x, zz))); + fe x3 = fe_sub(fe_sqr(alpha), fe_mul(k_eight, beta)); + fe z3 = fe_sub(fe_sub(fe_sqr(fe_add(p.y, p.z)), yy), zz); + fe y3 = fe_sub(fe_mul(alpha, fe_sub(fe_mul(k_four, beta), x3)), + fe_mul(k_eight, fe_sqr(yy))); + return {x3, y3, z3}; +} + +// Mixed Jacobian + affine addition, a = -3 (EFD madd-2007-bl). +auto jac_madd(jac const& p, aff const& q) -> jac +{ + if (fe_is_zero(p.z)) + return {q.x, q.y, k_one}; + + fe zz = fe_sqr(p.z); + fe u2 = fe_mul(q.x, zz); + fe s2 = fe_mul(q.y, fe_mul(p.z, zz)); + fe h = fe_sub(u2, p.x); + + if (fe_is_zero(h)) { + if (fe_eq(s2, p.y)) + return jac_dbl(p); // p == q + return {k_zero, k_zero, k_zero}; // p == -q + } + + fe hh = fe_sqr(h); + fe i4 = fe_mul(k_four, hh); + fe j = fe_mul(h, i4); + fe r = fe_mul(k_two, fe_sub(s2, p.y)); + fe v = fe_mul(p.x, i4); + fe x3 = fe_sub(fe_sub(fe_sqr(r), j), fe_mul(k_two, v)); + fe y3 = fe_sub(fe_mul(r, fe_sub(v, x3)), + fe_mul(fe_mul(k_two, p.y), j)); + fe z3 = fe_sub(fe_sub(fe_sqr(fe_add(p.z, h)), zz), hh); + return {x3, y3, z3}; +} + +auto jac_to_aff(jac const& p) -> aff +{ + if (fe_is_zero(p.z)) + return {k_zero, k_zero}; + fe zinv = fe_inv(p.z); + fe zinv2 = fe_sqr(zinv); + fe zinv3 = fe_mul(zinv2, zinv); + return {fe_mul(p.x, zinv2), fe_mul(p.y, zinv3)}; +} + +auto jac_scalar_mul(aff const& base, u64 const e[4]) -> jac +{ + jac r{k_zero, k_zero, k_zero}; + for (int i = 3; i >= 0; --i) { + for (int b = 63; b >= 0; --b) { + r = jac_dbl(r); + if ((e[i] >> b) & 1) + r = jac_madd(r, base); + } + } + return r; +} + +auto scalar_to_limbs(ec_scalar const& d, u64 e[4]) -> void +{ + for (int i = 0; i < 4; ++i) + e[i] = load_be64(d.data() + (3 - i) * 8); +} + +auto ec_point_is_valid(ec_point const& p) -> bool +{ + std::array px{}; + std::array py{}; + std::memcpy(px.data(), p.data(), 32); + std::memcpy(py.data(), p.data() + 32, 32); + + fe x = fe_from_bytes(px); + fe y = fe_from_bytes(py); + + // y^2 == x^3 - 3x + b + fe lhs = fe_sqr(y); + fe rhs = fe_add(fe_sub(fe_mul(fe_sqr(x), x), fe_mul(k_three, x)), k_mb); + return fe_eq(lhs, rhs); +} + +} // namespace + +// ========================================================================== +// SHA-256 +// ========================================================================== +sha256::sha256() +{ + CC_SHA256_Init(&ctx_); +} + +auto sha256::update(std::span data) -> void +{ + CC_SHA256_Update(&ctx_, data.data(), static_cast(data.size())); +} + +auto sha256::finalize(std::array& out) -> void +{ + CC_SHA256_Final(out.data(), &ctx_); +} + +// ========================================================================== +// HMAC-SHA256 +// ========================================================================== +hmac_sha256::hmac_sha256(std::span key) +{ + u8 block[64] = {}; + u8 key_block[64] = {}; + + if (key.size() > 64) { + std::array digest{}; + sha256 h; + h.update(key); + h.finalize(digest); + std::memcpy(key_block, digest.data(), digest.size()); + } else { + std::memcpy(key_block, key.data(), key.size()); + } + + for (int i = 0; i < 64; ++i) { + block[i] = static_cast(key_block[i] ^ 0x36); + inner_.update({&block[i], 1}); + } + for (int i = 0; i < 64; ++i) { + block[i] = static_cast(key_block[i] ^ 0x5c); + outer_.update({&block[i], 1}); + } +} + +auto hmac_sha256::update(std::span data) -> void +{ + inner_.update(data); +} + +auto hmac_sha256::finalize(std::array& out) -> void +{ + std::array inner_digest{}; + inner_.finalize(inner_digest); + outer_.update(inner_digest); + outer_.finalize(out); +} + +// ========================================================================== +// TLS P_SHA256 PRF +// ========================================================================== +auto prf_sha256(std::span key, std::string_view label, + std::span seed, std::span out) -> void +{ + std::span label_bytes{reinterpret_cast(label.data()), + label.size()}; + + std::array a{}; + { + hmac_sha256 h{key}; + h.update(label_bytes); + h.update(seed); + h.finalize(a); + } + + std::size_t off = 0; + while (off < out.size()) { + std::array block{}; + { + hmac_sha256 h{key}; + h.update(a); + h.update(label_bytes); + h.update(seed); + h.finalize(block); + } + + std::size_t n = std::min(k_sha256_len, out.size() - off); + std::memcpy(out.data() + off, block.data(), n); + off += n; + + hmac_sha256 h{key}; + h.update(a); + h.finalize(a); + } +} + +// ========================================================================== +// RSA (PKCS#1 v1.5) +// ========================================================================== +auto rsa_encrypt_pkcs1(std::span der_key, + std::span plaintext, std::span out) + -> bool +{ + CFDataRef key_data = CFDataCreate( + kCFAllocatorDefault, der_key.data(), static_cast(der_key.size())); + if (!key_data) + return false; + + void const* attr_keys[] = {kSecAttrKeyType, kSecAttrKeyClass}; + void const* attr_vals[] = {kSecAttrKeyTypeRSA, kSecAttrKeyClassPublic}; + CFDictionaryRef attrs = CFDictionaryCreate( + kCFAllocatorDefault, attr_keys, attr_vals, 2, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + if (!attrs) { + CFRelease(key_data); + return false; + } + + CFErrorRef error = nullptr; + SecKeyRef key = SecKeyCreateWithData(key_data, attrs, &error); + CFRelease(attrs); + CFRelease(key_data); + if (!key) { + if (error) + CFRelease(error); + return false; + } + + CFDataRef pt = CFDataCreate( + kCFAllocatorDefault, plaintext.data(), static_cast(plaintext.size())); + if (!pt) { + CFRelease(key); + return false; + } + + CFDataRef ct = SecKeyCreateEncryptedData(key, kSecKeyAlgorithmRSAEncryptionPKCS1, + pt, &error); + CFRelease(pt); + if (!ct) { + CFRelease(key); + if (error) + CFRelease(error); + return false; + } + + CFIndex len = CFDataGetLength(ct); + if (len > static_cast(out.size())) { + CFRelease(ct); + CFRelease(key); + return false; + } + + CFDataGetBytes(ct, CFRangeMake(0, len), out.data()); + CFRelease(ct); + CFRelease(key); + return true; +} + +// ========================================================================== +// ECDH (P-256) +// ========================================================================== +auto ec_base_point_multiply(ec_scalar const& d, ec_point& q) -> void +{ + u64 e[4]; + scalar_to_limbs(d, e); + aff g{k_mgx, k_mgy}; + aff r = jac_to_aff(jac_scalar_mul(g, e)); + + std::array bx = fe_to_bytes(r.x); + std::array by = fe_to_bytes(r.y); + std::memcpy(q.data(), bx.data(), 32); + std::memcpy(q.data() + 32, by.data(), 32); +} + +auto ec_compute_shared(ec_scalar const& d, ec_point const& peer, + ec_scalar& shared) -> bool +{ + if (!ec_point_is_valid(peer)) + return false; + + u64 e[4]; + scalar_to_limbs(d, e); + + std::array px{}; + std::array py{}; + std::memcpy(px.data(), peer.data(), 32); + std::memcpy(py.data(), peer.data() + 32, 32); + + aff r = jac_to_aff(jac_scalar_mul({fe_from_bytes(px), fe_from_bytes(py)}, e)); + if (fe_is_zero(r.y)) + return false; + + shared = fe_to_bytes(r.x); + return true; +} + +auto ec_generate_keypair(ec_scalar& d, ec_point& q) -> void +{ + do { + random_bytes(d); + } while (std::all_of(d.begin(), d.end(), [](u8 b) { return b == 0; })); + + ec_base_point_multiply(d, q); +} + +// ========================================================================== +// Randomness +// ========================================================================== +auto random_bytes(std::span out) -> void +{ + arc4random_buf(out.data(), out.size()); +} + } // namespace xone::auth diff --git a/src/gip/protocol.cpp b/src/gip/protocol.cpp index 1ea4e15..c5f0eaa 100644 --- a/src/gip/protocol.cpp +++ b/src/gip/protocol.cpp @@ -1,7 +1,941 @@ -// GIP protocol (Game Input Protocol) -// TODO(phase 1): port from medusalix/xone bus/protocol.c + bus/bus.c. +// GIP protocol (port of medusalix/xone bus/protocol.c + bus/bus.c). #include "gip/protocol.hpp" +#include +#include + +#include + +#include "common/log.hpp" +#include "common/types.hpp" + namespace xone::gip { + +namespace { + +constexpr int k_hdr_min_length = 3; +constexpr u8 k_vkey_left_win = 0x5b; + +auto encode_varint(u8* buf, u32 val) -> int +{ + int i; + for (i = 0; i < 4; ++i) { + buf[i] = static_cast(val); + if (val > 0x7f) + buf[i] |= 0x80; + val >>= 7; + if (!val) + break; + } + return i + 1; +} + +auto decode_varint(u8 const* data, int len, u32* val) -> int +{ + int i; + for (i = 0; i < 4 && i < len; ++i) { + *val |= static_cast(data[i] & 0x7f) << (i * 7); + if (!(data[i] & 0x80)) + break; + } + return i + 1; +} + +auto get_actual_header_length(gip_header const& hdr) -> int +{ + u32 pkt_len = hdr.packet_length; + u32 chunk_offset = hdr.chunk_offset; + int len = k_hdr_min_length; + + do { + ++len; + pkt_len >>= 7; + } while (pkt_len); + + if (hdr.options & GIP_OPT_CHUNK) { + while (chunk_offset) { + ++len; + chunk_offset >>= 7; + } + } + + return len; +} + +auto get_header_length(gip_header const& hdr) -> int +{ + int len = get_actual_header_length(hdr); + return len + (len % 2); // round up to even +} + +auto encode_header(gip_header const& hdr, u8* buf) -> void +{ + int hdr_len = 0; + + buf[hdr_len++] = hdr.command; + buf[hdr_len++] = hdr.options; + buf[hdr_len++] = hdr.sequence; + + hdr_len += encode_varint(buf + hdr_len, hdr.packet_length); + + // Header length must be even: pad by marking the last byte as a + // continuation and appending a zero byte (absorbed by the varint). + if (get_actual_header_length(hdr) % 2) { + buf[hdr_len - 1] |= 0x80; + buf[hdr_len++] = 0; + } + + if (hdr.options & GIP_OPT_CHUNK) + encode_varint(buf + hdr_len, hdr.chunk_offset); +} + +auto decode_header(gip_header& hdr, u8 const* data, int len) -> int +{ + int hdr_len = 0; + + hdr.command = data[hdr_len++]; + hdr.options = data[hdr_len++]; + hdr.sequence = data[hdr_len++]; + hdr.packet_length = 0; + hdr.chunk_offset = 0; + + hdr_len += decode_varint(data + hdr_len, len - hdr_len, &hdr.packet_length); + + if (hdr.options & GIP_OPT_CHUNK) + hdr_len += decode_varint(data + hdr_len, len - hdr_len, &hdr.chunk_offset); + + return hdr_len; +} + +} // namespace + +// ========================================================================== +// client +// ========================================================================== +client::client(adapter& adapter, u8 id) + : adapter_(adapter) + , id_(id) +{ +} + +auto client::has_interface(xone::guid_t const& guid) const -> bool +{ + if (!interfaces_) + return false; + + for (int i = 0; i < interfaces_->count; ++i) { + if (!std::memcmp(interfaces_->data.data() + i * sizeof(guid), guid.data, + sizeof(guid))) { + return true; + } + } + + return false; +} + +auto client::set_power_mode(u8 mode) -> int +{ + gip_header hdr{}; + hdr.command = GIP_CMD_POWER; + hdr.options = id_ | GIP_OPT_INTERNAL; + hdr.packet_length = 1; + return adapter_.send_pkt(*this, hdr, &mode); +} + +auto client::send_rumble(std::span pkt) -> int +{ + gip_header hdr{}; + hdr.command = GIP_CMD_RUMBLE; + hdr.options = id_; + hdr.packet_length = static_cast(pkt.size()); + return adapter_.send_pkt(*this, hdr, pkt.data()); +} + +auto client::set_led_mode(u8 mode, u8 brightness) -> int +{ + gip_header hdr{}; + hdr.command = GIP_CMD_LED; + hdr.options = id_ | GIP_OPT_INTERNAL; + hdr.packet_length = 3; + + u8 pkt[3] = {0, mode, brightness}; + return adapter_.send_pkt(*this, hdr, pkt); +} + +auto client::suggest_audio_format(u8 in, u8 out, bool chat) -> int +{ + int err; + if (chat) + err = adapter_.set_audio_format_chat(*this, GIP_AUD_FORMAT_CHAT_24KHZ); + else + err = adapter_.set_audio_format(*this, in, out); + + if (err) { + log_msg(log_level::error, "gip: set audio format failed: %d", err); + return err; + } + + audio_config_in_.format = in; + audio_config_out_.format = out; + return 0; +} + +auto client::set_audio_volume(u8 in, u8 chat, u8 out) -> int +{ + gip_header hdr{}; + hdr.command = GIP_CMD_AUDIO_CONTROL; + hdr.options = id_ | GIP_OPT_INTERNAL; + hdr.packet_length = 8; + + u8 pkt[8] = {0x03, 0x04, out, chat, in, 0, 0, 0}; + return adapter_.send_pkt(*this, hdr, pkt); +} + +auto client::send_audio_samples(std::span samples) -> int +{ + // TODO(phase 4): the MT76 transport batches audio packets; send one + // frame per packet until the transport exposes an audio buffer API. + auto const& cfg = audio_config_out_; + gip_header hdr{}; + hdr.command = GIP_CMD_AUDIO_SAMPLES; + hdr.options = id_ | GIP_OPT_INTERNAL; + hdr.packet_length = static_cast(cfg.fragment_size); + + int hdr_len = get_header_length(hdr); + + for (int i = 0; i < adapter_.audio_packet_count_; ++i) { + auto const* src = samples.data() + i * cfg.fragment_size; + std::vector frame(static_cast(hdr_len) + cfg.fragment_size); + + do { + hdr.sequence = adapter_.audio_sequence_++; + } while (!hdr.sequence); + + encode_header(hdr, frame.data()); + std::memcpy(frame.data() + hdr_len, src, cfg.fragment_size); + + int err = adapter_.transport_.send_frame(frame); + if (err) + return err; + } + + return 0; +} + +auto client::enable_audio() -> int +{ + // TODO(phase 4): audio transport is not wired up yet. + return 0; +} + +auto client::init_audio_in() -> int +{ + // TODO(phase 4): audio transport is not wired up yet. + return 0; +} + +auto client::init_audio_out() -> int +{ + // TODO(phase 4): audio transport is not wired up yet. + return 0; +} + +auto client::disable_audio() -> void +{ + // TODO(phase 4): audio transport is not wired up yet. +} + +auto client::send(std::span pkt, bool acknowledge) -> int +{ + gip_header hdr{}; + hdr.command = GIP_CMD_AUTHENTICATE; + hdr.options = id_ | GIP_OPT_INTERNAL; + hdr.packet_length = static_cast(pkt.size()); + + if (acknowledge) + hdr.options |= GIP_OPT_ACKNOWLEDGE; + + return adapter_.send_pkt(*this, hdr, pkt.data()); +} + +auto client::set_encryption_key(std::span key) -> int +{ + return adapter_.transport_.set_encryption_key(id_, key); +} + +// ========================================================================== +// adapter +// ========================================================================== +adapter::adapter(transport& transport, client_listener& listener, + int audio_packet_count) + : transport_(transport) + , listener_(listener) + , audio_packet_count_(audio_packet_count) +{ +} + +auto adapter::get_client(u8 id) -> client* +{ + if (id >= k_max_clients) + return nullptr; + + auto& slot = clients_[id]; + if (!slot) + slot = std::make_unique(*this, id); + + return slot.get(); +} + +auto adapter::client_count() const -> int +{ + int n = 0; + for (auto const& c : clients_) + if (c) + ++n; + return n; +} + +auto adapter::process_buffer(std::span data) -> int +{ + while (data.size() > k_hdr_min_length) { + gip_header hdr; + int hdr_len = decode_header(hdr, data.data(), + static_cast(data.size())); + if (data.size() < static_cast(hdr_len) + hdr.packet_length) + return -EINVAL; + + client* c = get_client(hdr.options & 0x0f); + if (!c) + return -ENODEV; + + int err = process_pkt(*c, hdr, data.data() + hdr_len); + if (err) + return err; + + data = data.subspan(static_cast(hdr_len) + hdr.packet_length); + } + + return 0; +} + +auto adapter::send_pkt_simple(gip_header& hdr, void const* data) -> int +{ + int hdr_len = get_header_length(hdr); + std::vector frame(static_cast(hdr_len) + hdr.packet_length); + + while (!hdr.sequence) + hdr.sequence = data_sequence_++; + + encode_header(hdr, frame.data()); + if (data && hdr.packet_length) + std::memcpy(frame.data() + hdr_len, data, hdr.packet_length); + + int err = transport_.send_frame(frame); + if (err) + log_msg(log_level::debug, "gip: send frame failed: %d", err); + + return err; +} + +auto adapter::send_pkt(client& c, gip_header& hdr, void const* data) -> int +{ + if (hdr.packet_length <= k_pkt_max_length) + return send_pkt_simple(hdr, data); + + // Split into chunks; the first chunk carries the total length. + hdr.options |= GIP_OPT_ACKNOWLEDGE | GIP_OPT_CHUNK_START | GIP_OPT_CHUNK; + hdr.chunk_offset = hdr.packet_length; + hdr.packet_length = k_pkt_max_length; + + int err = send_pkt_simple(hdr, data); + if (err) + return err; + + err = init_chunk_buffer(hdr, c.chunk_buf_in_); + if (err) + return err; + + std::memcpy(c.chunk_buf_in_->data.data(), data, hdr.chunk_offset); + return 0; +} + +auto adapter::init_chunk_buffer(gip_header const& hdr, + std::unique_ptr& buf) -> int +{ + if (hdr.chunk_offset > k_chunk_buf_max_length) + return -EINVAL; + + buf = std::make_unique(); + buf->header = hdr; + buf->header.options &= ~(GIP_OPT_ACKNOWLEDGE | GIP_OPT_CHUNK_START); + buf->length = hdr.chunk_offset; + buf->data.resize(hdr.chunk_offset); + return 0; +} + +auto adapter::send_remaining_chunks(client& c) -> int +{ + auto& buf = c.chunk_buf_in_; + gip_header hdr = buf->header; + u32 len = buf->length - k_pkt_max_length; + + while (len) { + if (len <= k_pkt_max_length) + hdr.options |= GIP_OPT_ACKNOWLEDGE; + + hdr.packet_length = std::min(len, k_pkt_max_length); + hdr.chunk_offset = buf->length - len; + + int err = send_pkt_simple(hdr, buf->data.data() + hdr.chunk_offset); + if (err) + return err; + + len -= hdr.packet_length; + } + + return 0; +} + +auto adapter::request_identification(client& c) -> int +{ + gip_header hdr{}; + hdr.command = GIP_CMD_IDENTIFY; + hdr.options = c.id_ | GIP_OPT_INTERNAL; + return send_pkt(c, hdr, nullptr); +} + +auto adapter::acknowledge_pkt(client& c, gip_header const& ack) -> int +{ + auto& buf = c.chunk_buf_out_; + gip_header hdr{}; + u8 pkt[9] = {}; + u32 len = ack.chunk_offset + ack.packet_length; + + hdr.command = GIP_CMD_ACKNOWLEDGE; + hdr.options = c.id_ | GIP_OPT_INTERNAL; + hdr.sequence = ack.sequence; + hdr.packet_length = sizeof(pkt); + + pkt[1] = ack.command; + pkt[2] = c.id_ | GIP_OPT_INTERNAL; + store_le16(pkt + 3, static_cast(len)); + + if ((ack.options & GIP_OPT_CHUNK) && buf) + store_le16(pkt + 7, static_cast(buf->length - len)); + + return send_pkt(c, hdr, pkt); +} + +auto adapter::remove_client(client& c) -> void +{ + listener_.on_client_removed(c.id_); + clients_[c.id_].reset(); +} + +auto adapter::free_client_info(client& c) -> void +{ + c.client_commands_.reset(); + c.firmware_versions_.reset(); + c.audio_formats_.reset(); + c.capabilities_out_.reset(); + c.capabilities_in_.reset(); + c.classes_.clear(); + c.interfaces_.reset(); + c.hid_descriptor_.reset(); +} + +auto adapter::add_client(client& c) -> void +{ + log_msg(log_level::info, "gip: client %u identified (vendor 0x%04x, " + "product 0x%04x)", + c.id_, c.hardware_.vendor, c.hardware_.product); + listener_.on_client_added(c); + c.start_auth(); +} + +auto adapter::make_audio_config(gip_audio_config& cfg) -> int +{ + switch (cfg.format) { + case GIP_AUD_FORMAT_16KHZ_MONO: + cfg.channels = 1; + cfg.sample_rate = 16000; + break; + case GIP_AUD_FORMAT_24KHZ_MONO: + cfg.channels = 1; + cfg.sample_rate = 24000; + break; + case GIP_AUD_FORMAT_48KHZ_STEREO: + cfg.channels = 2; + cfg.sample_rate = 48000; + break; + default: + log_msg(log_level::error, "gip: unknown audio format: 0x%02x", cfg.format); + return -EOPNOTSUPP; + } + + cfg.buffer_size = cfg.sample_rate * cfg.channels * 2 * k_audio_interval / 1000; + cfg.fragment_size = cfg.buffer_size / audio_packet_count_; + + gip_header hdr{}; + hdr.packet_length = static_cast(cfg.fragment_size); + cfg.packet_size = get_header_length(hdr) + cfg.fragment_size; + + return 0; +} + +auto adapter::set_audio_format(client& c, u8 in, u8 out) -> int +{ + gip_header hdr{}; + hdr.command = GIP_CMD_AUDIO_CONTROL; + hdr.options = c.id_ | GIP_OPT_INTERNAL; + hdr.packet_length = 3; + + u8 pkt[3] = {0x02, in, out}; + return send_pkt(c, hdr, pkt); +} + +auto adapter::set_audio_format_chat(client& c, u8 in_out) -> int +{ + gip_header hdr{}; + hdr.command = GIP_CMD_AUDIO_CONTROL; + hdr.options = c.id_ | GIP_OPT_INTERNAL; + hdr.packet_length = 2; + + u8 pkt[2] = {0x01, in_out}; + return send_pkt(c, hdr, pkt); +} + +auto adapter::process_pkt(client& c, gip_header& hdr, void const* data) -> int +{ + if (hdr.options & GIP_OPT_CHUNK_START) { + int err = init_chunk_buffer(hdr, c.chunk_buf_out_); + if (err) + return err; + hdr.chunk_offset = 0; + } + + if (hdr.options & GIP_OPT_ACKNOWLEDGE) { + int err = acknowledge_pkt(c, hdr); + if (err) + return err; + } + + if (hdr.options & GIP_OPT_CHUNK) + return process_pkt_chunked(c, hdr, data); + + return dispatch_pkt(c, hdr, data, hdr.packet_length); +} + +auto adapter::process_pkt_chunked(client& c, gip_header const& hdr, + void const* data) -> int +{ + auto& buf = c.chunk_buf_out_; + + if (!buf) { + // Older gamepads occasionally send spurious completions. + if (!hdr.packet_length) + return 0; + + log_msg(log_level::error, "gip: chunk buffer not allocated"); + return -EPROTO; + } + + if (hdr.command != buf->header.command) + return -EALREADY; + + if (buf->length < hdr.chunk_offset + hdr.packet_length) + return -EINVAL; + + if (hdr.packet_length) { + std::memcpy(buf->data.data() + hdr.chunk_offset, data, hdr.packet_length); + return 0; + } + + // Empty chunk signals the completion of the transfer. + int err = dispatch_pkt(c, hdr, buf->data.data(), buf->length); + buf.reset(); + return err; +} + +auto adapter::dispatch_pkt(client& c, gip_header const& hdr, void const* data, + u32 len) -> int +{ + if (hdr.options & GIP_OPT_INTERNAL) { + switch (hdr.command) { + case GIP_CMD_ACKNOWLEDGE: + return handle_pkt_acknowledge(c, data, len); + case GIP_CMD_ANNOUNCE: + return handle_pkt_announce(c, data, len); + case GIP_CMD_STATUS: + return handle_pkt_status(c, data, len); + case GIP_CMD_IDENTIFY: + return handle_pkt_identify(c, data, len); + case GIP_CMD_AUTHENTICATE: + return handle_pkt_authenticate(c, data, len); + case GIP_CMD_VIRTUAL_KEY: + return handle_pkt_virtual_key(c, data, len); + case GIP_CMD_AUDIO_CONTROL: + return handle_pkt_audio_control(c, data, len); + case GIP_CMD_HID_REPORT: + return handle_pkt_hid_report(c, data, len); + case GIP_CMD_AUDIO_SAMPLES: + return handle_pkt_audio_samples(c, data, len); + default: + return 0; + } + } + + if (hdr.command == GIP_CMD_INPUT) + return handle_pkt_input(c, data, len); + + return 0; +} + +auto adapter::handle_pkt_acknowledge(client& c, void const* data, u32 len) -> int +{ + if (len != 9) + return -EINVAL; + + auto const* p = static_cast(data); + auto& buf = c.chunk_buf_in_; + + if (!buf) + return 0; + + if (p[1] != buf->header.command) + return 0; + + if (load_le16(p + 3) < buf->length) + return send_remaining_chunks(c); + + // Empty chunk signals the completion of the transfer. + gip_header hdr = buf->header; + hdr.packet_length = 0; + hdr.chunk_offset = buf->length; + buf.reset(); + + return send_pkt_simple(hdr, nullptr); +} + +auto adapter::handle_pkt_announce(client& c, void const* data, u32 len) -> int +{ + if (len != 28) + return -EINVAL; + + auto const* p = static_cast(data); + u16 vendor = load_le16(p + 8); + u16 product = load_le16(p + 10); + u16 fw_major = load_le16(p + 12); + u16 fw_minor = load_le16(p + 14); + + if (!c.hardware_.vendor && !c.hardware_.product && !c.hardware_.version) { + c.hardware_.vendor = vendor; + c.hardware_.product = product; + c.hardware_.version = static_cast((fw_major << 8) | fw_minor); + } + + return request_identification(c); +} + +auto adapter::handle_pkt_status(client& c, void const* data, u32 len) -> int +{ + if (len < 4) + return -EINVAL; + + auto const* p = static_cast(data); + u8 status = p[0]; + + if (!(status & 0x80)) { + log_msg(log_level::debug, "gip: client %u disconnected", c.id_); + remove_client(c); + return 0; + } + + listener_.on_battery(c, (status >> 2) & 0x3, status & 0x3); + return 0; +} + +auto adapter::handle_pkt_identify(client& c, void const* data, u32 len) -> int +{ + if (len < 32) + return -EINVAL; + + if (!c.classes_.empty()) { + log_msg(log_level::warn, "gip: client already identified"); + return 0; + } + + auto const* base = static_cast(data); + u16 offsets[8]; + for (int i = 0; i < 8; ++i) + offsets[i] = load_le16(base + 16 + 2 * i); + + std::span info{base + 16, len - 16}; + + int err = parse_client_commands(c, offsets, info); + if (err) + goto err_free_info; + err = parse_firmware_versions(c, offsets, info); + if (err) + goto err_free_info; + err = parse_audio_formats(c, offsets, info); + if (err) + goto err_free_info; + err = parse_capabilities(c, offsets, info); + if (err) + goto err_free_info; + err = parse_classes(c, info, offsets[5]); + if (err) + goto err_free_info; + err = parse_interfaces(c, offsets, info); + if (err) + goto err_free_info; + err = parse_hid_descriptor(c, offsets, info); + if (err) + goto err_free_info; + + add_client(c); + return 0; + +err_free_info: + free_client_info(c); + return err; +} + +auto adapter::handle_pkt_authenticate(client& c, void const* data, u32 len) -> int +{ + return c.process_auth({static_cast(data), len}); +} + +auto adapter::handle_pkt_virtual_key(client& c, void const* data, u32 len) -> int +{ + if (len != 2) + return -EINVAL; + + auto const* p = static_cast(data); + if (p[1] != k_vkey_left_win) + return -EINVAL; + + listener_.on_guide_button(c, p[0] != 0); + return 0; +} + +auto adapter::handle_pkt_audio_control(client& c, void const* data, u32 len) -> int +{ + if (len < 1) + return -EINVAL; + + auto const* p = static_cast(data); + switch (p[0]) { + case 0x00: // volume chat + if (len != 5) + return -EINVAL; + listener_.on_audio_volume(c, p[4], p[3]); + return 0; + case 0x01: // format chat + if (len != 2) + return -EINVAL; + if (p[1] != GIP_AUD_FORMAT_CHAT_24KHZ || c.audio_config_in_.buffer_size || + c.audio_config_out_.buffer_size) { + return -EPROTO; + } + if (make_audio_config(c.audio_config_in_)) + return -EINVAL; + if (make_audio_config(c.audio_config_out_)) + return -EINVAL; + listener_.on_audio_ready(c); + return 0; + case 0x02: // format + if (len != 3) + return -EINVAL; + if (c.audio_config_in_.buffer_size || c.audio_config_out_.buffer_size) + return -EPROTO; + if (p[1] != c.audio_config_in_.format || p[2] != c.audio_config_out_.format) { + log_msg(log_level::warn, "gip: audio format rejected: 0x%02x/0x%02x", + c.audio_config_in_.format, c.audio_config_out_.format); + return c.suggest_audio_format(p[1], p[2], false); + } + if (make_audio_config(c.audio_config_in_)) + return -EINVAL; + if (make_audio_config(c.audio_config_out_)) + return -EINVAL; + listener_.on_audio_ready(c); + return 0; + case 0x03: // volume + if (len != 8) + return -EINVAL; + listener_.on_audio_volume(c, p[4], p[2]); + return 0; + default: + log_msg(log_level::error, "gip: unknown audio subcommand: 0x%02x", p[0]); + return -EPROTO; + } +} + +auto adapter::handle_pkt_hid_report(client& c, void const* data, u32 len) -> int +{ + listener_.on_hid_report(c, {static_cast(data), len}); + return 0; +} + +auto adapter::handle_pkt_input(client& c, void const* data, u32 len) -> int +{ + listener_.on_input(c, {static_cast(data), len}); + return 0; +} + +auto adapter::handle_pkt_audio_samples(client& c, void const* data, u32 len) -> int +{ + if (len < 2) + return -EINVAL; + + auto const* p = static_cast(data); + listener_.on_audio_samples(c, {p + 2, len - 2}); + return 0; +} + +auto adapter::parse_info_element(std::span data, u16 offset, + int item_length, + std::unique_ptr& out) -> int +{ + if (!offset) + return -EOPNOTSUPP; + if (data.size() < static_cast(offset) + 1) + return -EINVAL; + + u8 count = data[offset]; + ++offset; + if (!count) + return -EOPNOTSUPP; + + int total = count * item_length; + if (data.size() < static_cast(offset) + total) + return -EINVAL; + + auto elem = std::make_unique(); + elem->count = count; + elem->data.assign(data.begin() + offset, data.begin() + offset + total); + out = std::move(elem); + return 0; +} + +auto adapter::parse_client_commands(client& c, u16 const offsets[8], + std::span data) -> int +{ + std::unique_ptr cmds; + int err = parse_info_element(data, offsets[0], 23, cmds); + if (err) { + if (err == -EOPNOTSUPP) + return 0; + log_msg(log_level::error, "gip: parse client commands failed: %d", err); + return err; + } + c.client_commands_ = std::move(cmds); + return 0; +} + +auto adapter::parse_firmware_versions(client& c, u16 const offsets[8], + std::span data) -> int +{ + std::unique_ptr vers; + int err = parse_info_element(data, offsets[1], 4, vers); + if (err) { + log_msg(log_level::error, "gip: parse firmware versions failed: %d", err); + return err; + } + c.firmware_versions_ = std::move(vers); + return 0; +} + +auto adapter::parse_audio_formats(client& c, u16 const offsets[8], + std::span data) -> int +{ + std::unique_ptr fmts; + int err = parse_info_element(data, offsets[2], 2, fmts); + if (err) { + if (err == -EOPNOTSUPP) + return 0; + log_msg(log_level::error, "gip: parse audio formats failed: %d", err); + return err; + } + c.audio_formats_ = std::move(fmts); + return 0; +} + +auto adapter::parse_capabilities(client& c, u16 const offsets[8], + std::span data) -> int +{ + std::unique_ptr caps; + int err = parse_info_element(data, offsets[3], 1, caps); + if (err) { + log_msg(log_level::error, "gip: parse capabilities out failed: %d", err); + return err; + } + c.capabilities_out_ = std::move(caps); + + err = parse_info_element(data, offsets[4], 1, caps); + if (err) { + log_msg(log_level::error, "gip: parse capabilities in failed: %d", err); + return err; + } + c.capabilities_in_ = std::move(caps); + return 0; +} + +auto adapter::parse_classes(client& c, std::span data, u16 offset) -> int +{ + if (data.size() < static_cast(offset) + 1) + return -EINVAL; + + u8 count = data[offset]; + ++offset; + if (!count) + return -EINVAL; + + while (static_cast(c.classes_.size()) < count) { + if (data.size() < static_cast(offset) + 2) + return -EINVAL; + + u16 str_len = load_le16(data.data() + offset); + offset += 2; + if (!str_len || data.size() < static_cast(offset) + str_len) + return -EINVAL; + + c.classes_.emplace_back( + reinterpret_cast(data.data() + offset), str_len); + offset += str_len; + } + + return 0; +} + +auto adapter::parse_interfaces(client& c, u16 const offsets[8], + std::span data) -> int +{ + std::unique_ptr intfs; + int err = parse_info_element(data, offsets[6], sizeof(xone::guid_t), intfs); + if (err) { + log_msg(log_level::error, "gip: parse interfaces failed: %d", err); + return err; + } + c.interfaces_ = std::move(intfs); + return 0; +} + +auto adapter::parse_hid_descriptor(client& c, u16 const offsets[8], + std::span data) -> int +{ + std::unique_ptr desc; + int err = parse_info_element(data, offsets[7], 1, desc); + if (err) { + if (err == -EOPNOTSUPP) + return 0; + log_msg(log_level::error, "gip: parse hid descriptor failed: %d", err); + return err; + } + c.hid_descriptor_ = std::move(desc); + return 0; +} + } // namespace xone::gip diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index aacc4d3..821e684 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,6 +15,27 @@ target_compile_features(test_types PRIVATE cxx_std_23) add_test(NAME test_types COMMAND test_types) list(APPEND TEST_TARGETS test_types) +add_executable(test_crypto test_crypto.cpp) +target_include_directories(test_crypto PRIVATE "${CMAKE_SOURCE_DIR}/include") +target_link_libraries(test_crypto PRIVATE xone_auth Unity::Unity) +target_compile_features(test_crypto PRIVATE cxx_std_23) +add_test(NAME test_crypto COMMAND test_crypto) +list(APPEND TEST_TARGETS test_crypto) + +add_executable(test_auth test_auth.cpp) +target_include_directories(test_auth PRIVATE "${CMAKE_SOURCE_DIR}/include") +target_link_libraries(test_auth PRIVATE xone_auth Unity::Unity) +target_compile_features(test_auth PRIVATE cxx_std_23) +add_test(NAME test_auth COMMAND test_auth) +list(APPEND TEST_TARGETS test_auth) + +add_executable(test_gip test_gip.cpp) +target_include_directories(test_gip PRIVATE "${CMAKE_SOURCE_DIR}/include") +target_link_libraries(test_gip PRIVATE xone_gip Unity::Unity) +target_compile_features(test_gip PRIVATE cxx_std_23) +add_test(NAME test_gip COMMAND test_gip) +list(APPEND TEST_TARGETS test_gip) + add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_BINARY_DIR}" diff --git a/tests/test_auth.cpp b/tests/test_auth.cpp new file mode 100644 index 0000000..790f151 --- /dev/null +++ b/tests/test_auth.cpp @@ -0,0 +1,198 @@ +#include "unity.h" + +#include +#include +#include + +#include +#include + +#include "auth/auth.hpp" + +void setUp() {} +void tearDown() {} + +namespace { + +using u8 = std::uint8_t; +using u16 = std::uint16_t; + +class mock_sink : public xone::auth::auth_sink { +public: + struct sent { + std::vector data; + bool ack; + }; + + std::vector packets; + std::vector key; + + auto send(std::span pkt, bool ack) -> int override + { + packets.push_back({std::vector(pkt.begin(), pkt.end()), ack}); + return 0; + } + + auto set_encryption_key(std::span k) -> int override + { + key.assign(k.begin(), k.end()); + return 0; + } +}; + +// Build an auth data packet: full header + payload. +auto make_pkt(u8 cmd, std::vector const& payload) -> std::vector +{ + std::vector p; + u16 data_len = static_cast(payload.size()); + + p.push_back(0x00); // context = handshake + p.push_back(0x00); // options (client -> host) + p.push_back(0x00); // error + p.push_back(cmd); + p.push_back(static_cast(data_len >> 8)); + p.push_back(static_cast(data_len & 0xFF)); + + p.push_back(cmd); // data.command + p.push_back(0x01); // data.version + u16 data_body = static_cast(data_len - 4); + p.push_back(static_cast(data_body >> 8)); + p.push_back(static_cast(data_body & 0xFF)); + + p.insert(p.end(), payload.begin(), payload.end()); + return p; +} + +auto make_rsa_der() -> std::vector +{ + CFTypeRef keys[] = {kSecAttrKeyType, kSecAttrKeySizeInBits, + kSecAttrIsPermanent}; + CFNumberRef size = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, + (int[]){2048}); + CFTypeRef vals[] = {kSecAttrKeyTypeRSA, size, kCFBooleanFalse}; + CFDictionaryRef attrs = CFDictionaryCreate( + kCFAllocatorDefault, keys, vals, 3, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + CFErrorRef error = nullptr; + SecKeyRef priv = SecKeyCreateRandomKey(attrs, &error); + SecKeyRef pub = SecKeyCopyPublicKey(priv); + CFDataRef ext = SecKeyCopyExternalRepresentation(pub, &error); + std::vector der(CFDataGetLength(ext)); + CFDataGetBytes(ext, CFRangeMake(0, CFDataGetLength(ext)), der.data()); + CFRelease(ext); + CFRelease(pub); + CFRelease(priv); + CFRelease(size); + CFRelease(attrs); + return der; +} + +auto be16_at(std::vector const& v, std::size_t i) -> u16 +{ + return static_cast((v[i] << 8) | v[i + 1]); +} + +} // namespace + +void test_start_sends_host_hello(void) +{ + mock_sink sink; + xone::auth::auth auth{sink}; + + TEST_ASSERT_EQUAL_INT(0, auth.start()); + TEST_ASSERT_EQUAL_INT(1, sink.packets.size()); + TEST_ASSERT_TRUE(sink.packets[0].ack); + TEST_ASSERT_EQUAL_INT(58, sink.packets[0].data.size()); + + auto const& p = sink.packets[0].data; + TEST_ASSERT_EQUAL_UINT8(0x00, p[0]); // context + TEST_ASSERT_EQUAL_UINT8(0x41, p[1]); // ACK | FROM_HOST + TEST_ASSERT_EQUAL_UINT8(0x01, p[3]); // host hello + TEST_ASSERT_EQUAL_INT(44, be16_at(p, 4)); + TEST_ASSERT_EQUAL_UINT8(0x01, p[6]); // data.command + TEST_ASSERT_EQUAL_UINT8(0x01, p[7]); // data.version + TEST_ASSERT_EQUAL_INT(40, be16_at(p, 8)); +} + +void test_client_hello_requests_certificate(void) +{ + mock_sink sink; + xone::auth::auth auth{sink}; + auth.start(); + std::size_t base = sink.packets.size(); + + std::vector payload(80, 0x11); // client hello: random + unknown + TEST_ASSERT_EQUAL_INT(0, auth.process_pkt(make_pkt(0x02, payload))); + + TEST_ASSERT_EQUAL_INT(base + 1, sink.packets.size()); + auto const& p = sink.packets.back().data; + TEST_ASSERT_EQUAL_INT(14, p.size()); // request packet + TEST_ASSERT_TRUE(sink.packets.back().ack); + TEST_ASSERT_EQUAL_UINT8(0x42, p[1]); // REQUEST | FROM_HOST + TEST_ASSERT_EQUAL_UINT8(0x03, p[3]); // client certificate + TEST_ASSERT_EQUAL_INT(1028, be16_at(p, 4)); // cert max len + data hdr +} + +void test_certificate_triggers_rsa_exchange(void) +{ + mock_sink sink; + xone::auth::auth auth{sink}; + auth.start(); + + // client hello -> cert request + auth.process_pkt(make_pkt(0x02, std::vector(80, 0x22))); + std::size_t base = sink.packets.size(); + + // certificate: some prefix bytes + ASN.1 SEQUENCE + 270-byte DER + std::vector der = make_rsa_der(); + TEST_ASSERT_EQUAL_INT(270, der.size()); + TEST_ASSERT_EQUAL_UINT8(0x30, der[0]); + TEST_ASSERT_EQUAL_UINT8(0x82, der[1]); + TEST_ASSERT_EQUAL_UINT8(0x01, der[2]); + TEST_ASSERT_EQUAL_UINT8(0x0a, der[3]); + + std::vector cert(64, 0x33); + cert.insert(cert.end(), der.begin(), der.end()); + cert.insert(cert.end(), 32, 0x44); + + TEST_ASSERT_EQUAL_INT(0, auth.process_pkt(make_pkt(0x03, cert))); + + // host secret packet + TEST_ASSERT_EQUAL_INT(base + 1, sink.packets.size()); + auto const& p = sink.packets.back().data; + TEST_ASSERT_EQUAL_INT(274, p.size()); // header_full + 256 + trailer + TEST_ASSERT_TRUE(sink.packets.back().ack); + TEST_ASSERT_EQUAL_UINT8(0x05, p[3]); // host secret +} + +void test_acknowledge_sends_host_finish(void) +{ + mock_sink sink; + xone::auth::auth auth{sink}; + auth.start(); + auth.process_pkt(make_pkt(0x02, std::vector(80, 0x22))); + + std::vector der = make_rsa_der(); + std::vector cert(der.begin(), der.end()); + auth.process_pkt(make_pkt(0x03, cert)); + std::size_t base = sink.packets.size(); + + // ACK: handshake header with options=ACK, command=0x01 + std::vector ack{0x00, 0x01, 0x00, 0x01, 0x00, 0x00}; + TEST_ASSERT_EQUAL_INT(0, auth.process_pkt(ack)); + + TEST_ASSERT_EQUAL_INT(base + 1, sink.packets.size()); + auto const& p = sink.packets.back().data; + TEST_ASSERT_EQUAL_INT(50, p.size()); // header_full + 32 + trailer + TEST_ASSERT_EQUAL_UINT8(0x07, p[3]); // host finish +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_start_sends_host_hello); + RUN_TEST(test_client_hello_requests_certificate); + RUN_TEST(test_certificate_triggers_rsa_exchange); + RUN_TEST(test_acknowledge_sends_host_finish); + return UNITY_END(); +} diff --git a/tests/test_crypto.cpp b/tests/test_crypto.cpp new file mode 100644 index 0000000..d941c25 --- /dev/null +++ b/tests/test_crypto.cpp @@ -0,0 +1,284 @@ +#include "unity.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "auth/crypto.hpp" + +void setUp() {} +void tearDown() {} + +namespace { + +using u8 = std::uint8_t; + +auto hex(std::string_view s) -> std::vector +{ + std::vector out; + for (std::size_t i = 0; i + 1 < s.size(); i += 2) { + char buf[3] = {s[i], s[i + 1], 0}; + out.push_back(static_cast(std::strtoul(buf, nullptr, 16))); + } + return out; +} + +template +auto to_array(std::vector const& v) -> std::array +{ + std::array a{}; + TEST_ASSERT(v.size() == N); + std::copy(v.begin(), v.end(), a.begin()); + return a; +} + +auto span(std::vector const& v) -> std::span +{ + return {v.data(), v.size()}; +} + +} // namespace + +// -------------------------------------------------------------------------- +// SHA-256 +// -------------------------------------------------------------------------- +void test_sha256_abc(void) +{ + std::array out{}; + xone::auth::sha256 h; + std::string data = "abc"; + h.update({reinterpret_cast(data.data()), data.size()}); + h.finalize(out); + auto expect = hex("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expect.data(), out.data(), 32); +} + +void test_sha256_snapshot(void) +{ + // snapshot must not disturb continued hashing + std::array out{}; + xone::auth::sha256 h; + std::string a = "abc"; + h.update({reinterpret_cast(a.data()), a.size()}); + + xone::auth::sha256 snap = h; // snapshot + std::string b = "def"; + h.update({reinterpret_cast(b.data()), b.size()}); + h.finalize(out); + auto expect = hex("bef57ec7f53a6d40beb640a780a639c83bc29ac8a9816f1fc6c5c6dcd93c4721"); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expect.data(), out.data(), 32); + + // snapshot still hashes only "abc" + std::array snap_out{}; + snap.finalize(snap_out); + auto expect_snap = hex("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expect_snap.data(), snap_out.data(), 32); +} + +// -------------------------------------------------------------------------- +// HMAC-SHA256 (RFC 4231) +// -------------------------------------------------------------------------- +void test_hmac_rfc4231_case2(void) +{ + // key = "Jefe", data = "what do ya want for nothing?" + std::string key = "Jefe"; + std::string data = "what do ya want for nothing?"; + xone::auth::hmac_sha256 h{{reinterpret_cast(key.data()), key.size()}}; + h.update({reinterpret_cast(data.data()), data.size()}); + std::array out{}; + h.finalize(out); + auto expect = hex("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expect.data(), out.data(), 32); +} + +void test_hmac_rfc4231_case1(void) +{ + // key = 0x0b * 20, data = "Hi There" + std::vector key(20, 0x0b); + std::string data = "Hi There"; + xone::auth::hmac_sha256 h{span(key)}; + h.update({reinterpret_cast(data.data()), data.size()}); + std::array out{}; + h.finalize(out); + auto expect = hex("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expect.data(), out.data(), 32); +} + +// -------------------------------------------------------------------------- +// PRF (TLS P_SHA256) +// -------------------------------------------------------------------------- +void test_prf_expansion(void) +{ + // Long output forces multiple blocks; verify against Python-computed value. + std::vector key = hex("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"); + std::vector seed = hex("deadbeefcafebabe"); + std::array out{}; + xone::auth::prf_sha256(span(key), "Master Secret", span(seed), out); + auto expect = hex( + "78b92e5b14f8b98dc9f5ddd33668d3dd19e74467f38992009d71bdfd69b5dfa2" + "8ff26eaad240df9fe705798d6af784d8"); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expect.data(), out.data(), 48); +} + +// -------------------------------------------------------------------------- +// ECDH P-256 +// -------------------------------------------------------------------------- +void test_ec_small_multiples(void) +{ + auto two = hex("0000000000000000000000000000000000000000000000000000000000000002"); + auto three = hex("0000000000000000000000000000000000000000000000000000000000000003"); + + xone::auth::ec_point q{}; + xone::auth::ec_base_point_multiply(to_array<32>(two), q); + auto expect2g = hex( + "7cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc47669978" + "07775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d1"); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expect2g.data(), q.data(), 64); + + xone::auth::ec_base_point_multiply(to_array<32>(three), q); + auto expect3g = hex( + "5ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c" + "8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d5032"); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expect3g.data(), q.data(), 64); +} + +void test_ec_base_multiply(void) +{ + // vectors generated with OpenSSL (see test comments in commit) + auto d = hex("ca9aca63d14014baf4e37bcaeb90317557728b479ca42e126fcb9ae9a4f7f765"); + auto puba = hex( + "dca2dafb3baa46602e7512de4690ede383e390148b3c041b34e1beae2b9ec997" + "abf83c388bb7b53cd086a8d20810b22ac1f5d92484abf761f53b6725988604cd"); + + xone::auth::ec_point q{}; + xone::auth::ec_base_point_multiply(to_array<32>(d), q); + TEST_ASSERT_EQUAL_UINT8_ARRAY(puba.data(), q.data(), 64); +} + +void test_ec_shared_secret(void) +{ + auto d = hex("ca9aca63d14014baf4e37bcaeb90317557728b479ca42e126fcb9ae9a4f7f765"); + auto pubb = hex( + "5baeca87ad9b623f7d0cd33af316e5f57d17b711e821acc449a12e5427c95aa3" + "a0b86826cf4f028cb90bfd284e99db36cc19c640a914b97ef35dd965e2af19da"); + auto secret = hex("df01a8c296d70b09a91bc74f4bd25d877cffa0fe43287d24939e44cdc2e9ec25"); + + xone::auth::ec_scalar shared{}; + TEST_ASSERT_TRUE( + xone::auth::ec_compute_shared(to_array<32>(d), to_array<64>(pubb), shared)); + TEST_ASSERT_EQUAL_UINT8_ARRAY(secret.data(), shared.data(), 32); +} + +void test_ec_rejects_off_curve_point(void) +{ + auto d = hex("ca9aca63d14014baf4e37bcaeb90317557728b479ca42e126fcb9ae9a4f7f765"); + // point with x=1, y=1 is not on the curve + xone::auth::ec_point bad{}; + bad[31] = 1; + bad[63] = 1; + + xone::auth::ec_scalar shared{}; + TEST_ASSERT_FALSE( + xone::auth::ec_compute_shared(to_array<32>(d), bad, shared)); +} + +// -------------------------------------------------------------------------- +// RSA (PKCS#1 v1.5) round-trip against Security.framework +// -------------------------------------------------------------------------- +void test_rsa_roundtrip(void) +{ + CFErrorRef error = nullptr; + CFTypeRef attr_keys[] = {kSecAttrKeyType, kSecAttrKeySizeInBits, + kSecAttrIsPermanent}; + CFNumberRef size = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, + (int[]){2048}); + CFTypeRef attr_vals[] = {kSecAttrKeyTypeRSA, size, kCFBooleanFalse}; + CFDictionaryRef attrs = CFDictionaryCreate( + kCFAllocatorDefault, attr_keys, attr_vals, 3, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + TEST_ASSERT_NOT_NULL(attrs); + + SecKeyRef priv = SecKeyCreateRandomKey(attrs, &error); + CFRelease(size); + TEST_ASSERT_NOT_NULL(priv); + if (!priv) { + CFRelease(attrs); + return; + } + + SecKeyRef pub = SecKeyCopyPublicKey(priv); + TEST_ASSERT_NOT_NULL(pub); + if (!pub) { + CFRelease(priv); + CFRelease(attrs); + return; + } + + CFDataRef ext = SecKeyCopyExternalRepresentation(pub, &error); + TEST_ASSERT_NOT_NULL(ext); + if (!ext) { + CFRelease(pub); + CFRelease(priv); + CFRelease(attrs); + return; + } + + CFIndex key_len = CFDataGetLength(ext); + std::vector der(key_len); + CFDataGetBytes(ext, CFRangeMake(0, key_len), der.data()); + CFRelease(ext); + + // 2048-bit RSA PKCS#1 RSAPublicKey: 4-byte ASN.1 header + 266-byte body. + TEST_ASSERT_EQUAL_INT(270, key_len); + TEST_ASSERT_EQUAL_UINT8(0x30, der[0]); + TEST_ASSERT_EQUAL_UINT8(0x82, der[1]); + + std::vector plaintext(48, 0x5a); + std::vector ciphertext(256); + TEST_ASSERT_TRUE(xone::auth::rsa_encrypt_pkcs1(span(der), span(plaintext), + ciphertext)); + + // Decrypt to prove the round-trip. + CFDataRef ct = CFDataCreate(kCFAllocatorDefault, ciphertext.data(), + static_cast(ciphertext.size())); + CFDataRef dec = SecKeyCreateDecryptedData( + priv, kSecKeyAlgorithmRSAEncryptionPKCS1, ct, &error); + CFRelease(ct); + TEST_ASSERT_NOT_NULL(dec); + if (dec) { + CFIndex dec_len = CFDataGetLength(dec); + TEST_ASSERT_EQUAL_INT(48, dec_len); + std::vector dec_bytes(dec_len); + CFDataGetBytes(dec, CFRangeMake(0, dec_len), dec_bytes.data()); + TEST_ASSERT_EQUAL_UINT8_ARRAY(plaintext.data(), dec_bytes.data(), 48); + CFRelease(dec); + } + + CFRelease(priv); + CFRelease(pub); + CFRelease(attrs); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_sha256_abc); + RUN_TEST(test_sha256_snapshot); + RUN_TEST(test_hmac_rfc4231_case2); + RUN_TEST(test_hmac_rfc4231_case1); + RUN_TEST(test_prf_expansion); + RUN_TEST(test_ec_small_multiples); + RUN_TEST(test_ec_base_multiply); + RUN_TEST(test_ec_shared_secret); + RUN_TEST(test_ec_rejects_off_curve_point); + RUN_TEST(test_rsa_roundtrip); + return UNITY_END(); +} diff --git a/tests/test_gip.cpp b/tests/test_gip.cpp new file mode 100644 index 0000000..d293369 --- /dev/null +++ b/tests/test_gip.cpp @@ -0,0 +1,194 @@ +#include "unity.h" + +#include +#include +#include + +#include "gip/protocol.hpp" + +void setUp() {} +void tearDown() {} + +namespace { + +using u8 = std::uint8_t; +using u16 = std::uint16_t; +using u32 = std::uint32_t; + +class mock_transport : public xone::gip::transport { +public: + std::vector> frames; + + auto send_frame(std::span frame) -> int override + { + frames.emplace_back(frame.begin(), frame.end()); + return 0; + } +}; + +class mock_listener : public xone::gip::client_listener { +public: + int added = 0; + int removed = 0; + std::vector> inputs; + + auto on_client_added(xone::gip::client&) -> void override { ++added; } + auto on_client_removed(u8) -> void override { ++removed; } + auto on_input(xone::gip::client&, std::span data) -> void override + { + inputs.emplace_back(data.begin(), data.end()); + } +}; + +// Build a controller -> host GIP frame with a simple (single-byte varint) +// header. Payloads here stay below 128 bytes so the header is even without +// the pad byte. +auto make_frame(u8 command, u8 options, u8 sequence, + std::vector const& payload) -> std::vector +{ + std::vector f; + f.push_back(command); + f.push_back(options); + f.push_back(sequence); + u32 len = static_cast(payload.size()); + do { + u8 b = static_cast(len & 0x7f); + len >>= 7; + if (len) + b |= 0x80; + f.push_back(b); + } while (len); + f.insert(f.end(), payload.begin(), payload.end()); + return f; +} + +auto make_announce() -> std::vector +{ + std::vector p(28, 0); + p[8] = 0x5e; // vendor 0x045e (LE) + p[9] = 0x04; + p[10] = 0x12; // product 0x0b12 + p[11] = 0x0b; + p[12] = 0x05; // fw major + p[14] = 0x0f; // fw minor + return p; +} + +auto make_identify() -> std::vector +{ + // identify payload = 16 unknown bytes + 8 LE16 offsets + info elements. + // The offsets are relative to the base right after the unknown header + // (so they include the 16-byte offsets array itself: first element at 16). + auto put16 = [](std::vector& v, u16 x) { + v.push_back(static_cast(x & 0xFF)); + v.push_back(static_cast(x >> 8)); + }; + + std::vector elems; + // classes at info offset 16: count=1, str_len=10, "controller" + elems.push_back(1); + put16(elems, 10); + for (char c : std::string("controller")) + elems.push_back(static_cast(c)); + // firmware_versions at 29: count=1, {5, 15} + elems.push_back(1); + elems.push_back(5); + elems.push_back(0); + elems.push_back(15); + elems.push_back(0); + // capabilities_out at 34: count=1, 0x0f + elems.push_back(1); + elems.push_back(0x0f); + // capabilities_in at 36: count=1, 0x0f + elems.push_back(1); + elems.push_back(0x0f); + // interfaces at 38: count=1, 16-byte guid + elems.push_back(1); + for (int i = 0; i < 16; ++i) + elems.push_back(static_cast(i)); + + u16 offsets[8] = {0, 29, 0, 34, 36, 16, 38, 0}; + + std::vector payload(16, 0); // unknown header + for (u16 off : offsets) + put16(payload, off); + payload.insert(payload.end(), elems.begin(), elems.end()); + return payload; +} + +} // namespace + +void test_announce_requests_identification(void) +{ + mock_transport transport; + mock_listener listener; + xone::gip::adapter adap{transport, listener}; + + TEST_ASSERT_EQUAL_INT(0, adap.process_buffer(make_frame(0x02, 0x21, 1, make_announce()))); + + TEST_ASSERT_EQUAL_INT(1, transport.frames.size()); + auto const& f = transport.frames[0]; + TEST_ASSERT_EQUAL_INT(4, f.size()); // command + options + seq + len + TEST_ASSERT_EQUAL_UINT8(0x04, f[0]); // identify + TEST_ASSERT_EQUAL_UINT8(0x21, f[1]); // id 1 | internal + TEST_ASSERT_EQUAL_UINT8(0x01, f[2]); // sequence + TEST_ASSERT_EQUAL_UINT8(0x00, f[3]); // zero packet length +} + +void test_identify_adds_client_and_starts_auth(void) +{ + mock_transport transport; + mock_listener listener; + xone::gip::adapter adap{transport, listener}; + + TEST_ASSERT_EQUAL_INT(0, adap.process_buffer(make_frame(0x04, 0x21, 2, make_identify()))); + + TEST_ASSERT_EQUAL_INT(1, listener.added); + TEST_ASSERT_EQUAL_INT(1, adap.client_count()); + + // auth started: one AUTHENTICATE host hello frame was sent + TEST_ASSERT_EQUAL_INT(1, transport.frames.size()); + auto const& f = transport.frames[0]; + TEST_ASSERT_EQUAL_UINT8(0x06, f[0]); // authenticate + TEST_ASSERT_EQUAL_UINT8(0x31, f[1]); // id 1 | internal | ack + TEST_ASSERT_EQUAL_INT(62, f.size()); // 4-byte header + 58-byte hello +} + +void test_input_delivers_to_listener(void) +{ + mock_transport transport; + mock_listener listener; + xone::gip::adapter adap{transport, listener}; + + std::vector payload = {0x11, 0x22, 0x33, 0x44}; + TEST_ASSERT_EQUAL_INT(0, adap.process_buffer(make_frame(0x20, 0x01, 3, payload))); + + TEST_ASSERT_EQUAL_INT(1, listener.inputs.size()); + TEST_ASSERT_EQUAL_UINT8_ARRAY(payload.data(), listener.inputs[0].data(), 4); +} + +void test_status_disconnect_removes_client(void) +{ + mock_transport transport; + mock_listener listener; + xone::gip::adapter adap{transport, listener}; + + adap.get_client(1); + TEST_ASSERT_EQUAL_INT(1, adap.client_count()); + + std::vector payload = {0x00, 0, 0, 0}; // status, not connected + TEST_ASSERT_EQUAL_INT(0, adap.process_buffer(make_frame(0x03, 0x21, 4, payload))); + + TEST_ASSERT_EQUAL_INT(1, listener.removed); + TEST_ASSERT_EQUAL_INT(0, adap.client_count()); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_announce_requests_identification); + RUN_TEST(test_identify_adds_client_and_starts_auth); + RUN_TEST(test_input_delivers_to_listener); + RUN_TEST(test_status_disconnect_removes_client); + return UNITY_END(); +}