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:
+5
-1
@@ -6,7 +6,7 @@ if(NOT CMAKE_GENERATOR MATCHES "^(Ninja|Xcode)$")
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.21)
|
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)
|
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/)
|
# Auth + crypto (AES-CCMP key setup, ECDH, RSA — port from auth/)
|
||||||
add_library(xone_auth STATIC
|
add_library(xone_auth STATIC
|
||||||
"src/auth/crypto.cpp"
|
"src/auth/crypto.cpp"
|
||||||
|
"src/auth/auth.cpp"
|
||||||
)
|
)
|
||||||
target_include_directories(xone_auth PUBLIC "${CMAKE_SOURCE_DIR}/include")
|
target_include_directories(xone_auth PUBLIC "${CMAKE_SOURCE_DIR}/include")
|
||||||
target_compile_features(xone_auth PRIVATE cxx_std_23)
|
target_compile_features(xone_auth PRIVATE cxx_std_23)
|
||||||
target_compile_options(xone_auth PRIVATE ${BASE_OPTIONS})
|
target_compile_options(xone_auth PRIVATE ${BASE_OPTIONS})
|
||||||
target_compile_definitions(xone_auth PRIVATE ${BASE_DEFINITIONS})
|
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)
|
# MT76 chip protocol (firmware load, EFUSE, radio init, beacons)
|
||||||
add_library(xone_mt76 STATIC
|
add_library(xone_mt76 STATIC
|
||||||
|
|||||||
@@ -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 <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
#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<u8 const> pkt, bool acknowledge) -> int = 0;
|
||||||
|
|
||||||
|
// Install the derived AES-CCMP session key (16 bytes).
|
||||||
|
virtual auto set_encryption_key(std::span<u8 const> 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<u8 const> 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<u8 const> data) -> int;
|
||||||
|
auto dispatch_pkt(u8 cmd, std::span<u8 const> data) -> int;
|
||||||
|
auto handle_hello(std::span<u8 const> data) -> int;
|
||||||
|
auto handle_certificate(std::span<u8 const> data) -> int;
|
||||||
|
auto handle_finish(std::span<u8 const> data) -> int;
|
||||||
|
auto handle_hello2(std::span<u8 const> data) -> int;
|
||||||
|
auto handle_certificate2(std::span<u8 const> data) -> int;
|
||||||
|
auto handle_pubkey(std::span<u8 const> data) -> int;
|
||||||
|
auto get_transcript() -> std::array<u8, k_transcript_len>;
|
||||||
|
|
||||||
|
auth_sink& sink_;
|
||||||
|
sha256 transcript_;
|
||||||
|
u8 last_sent_command_ = 0;
|
||||||
|
|
||||||
|
std::array<u8, k_random_len> random_host_{};
|
||||||
|
std::array<u8, k_random_len> random_client_{};
|
||||||
|
std::array<u8, k_pubkey_len> pubkey_client_{};
|
||||||
|
std::array<u8, k_pubkey2_len> pubkey_client2_{};
|
||||||
|
std::array<u8, k_secret_len> master_secret_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace xone::auth
|
||||||
+100
-6
@@ -1,16 +1,110 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
// Auth + crypto
|
// Auth + crypto primitives
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
// Port target: medusalix/xone auth/auth.c + auth/crypto.c.
|
// Port target: medusalix/xone auth/crypto.c + auth/crypto.h.
|
||||||
// ECDH (P-256) key agreement, RSA encryption, SHA transcript/PRF for the GIP
|
//
|
||||||
// authentication handshake. AES-CCMP frame encryption itself runs on-chip;
|
// The kernel driver uses the in-kernel crypto API (crypto_shash for
|
||||||
// this layer only derives and installs keys into the MT76 WCID registers.
|
// 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 <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <CommonCrypto/CommonDigest.h>
|
||||||
|
|
||||||
namespace xone::auth {
|
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<u8 const> data) -> void;
|
||||||
|
auto finalize(std::array<u8, k_sha256_len>& out) -> void;
|
||||||
|
|
||||||
|
private:
|
||||||
|
CC_SHA256_CTX ctx_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// HMAC-SHA256
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
class hmac_sha256 {
|
||||||
|
public:
|
||||||
|
explicit hmac_sha256(std::span<u8 const> key);
|
||||||
|
|
||||||
|
auto update(std::span<u8 const> data) -> void;
|
||||||
|
auto finalize(std::array<u8, k_sha256_len>& 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<u8 const> key, std::string_view label,
|
||||||
|
std::span<u8 const> seed, std::span<u8> 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<u8 const> der_key,
|
||||||
|
std::span<u8 const> plaintext, std::span<u8> out)
|
||||||
|
-> bool;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// ECDH (P-256)
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
using ec_scalar = std::array<u8, 32>; // big-endian private key / x-coord
|
||||||
|
using ec_point = std::array<u8, 64>; // 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<u8> out) -> void;
|
||||||
|
|
||||||
} // namespace xone::auth
|
} // namespace xone::auth
|
||||||
|
|||||||
@@ -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 <cstdarg>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -10,6 +10,11 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
// 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 {
|
namespace xone {
|
||||||
|
|
||||||
// 128-bit GUID used by the GIP protocol (port of the kernel guid_t).
|
// 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<std::uint8_t>((v >> 24) & 0xFF);
|
b[3] = static_cast<std::uint8_t>((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<std::uint8_t const *>(p);
|
||||||
|
return static_cast<std::uint16_t>((b[0] << 8) | b[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto store_be16(void *p, std::uint16_t v) -> void
|
||||||
|
{
|
||||||
|
auto *b = static_cast<std::uint8_t *>(p);
|
||||||
|
b[0] = static_cast<std::uint8_t>((v >> 8) & 0xFF);
|
||||||
|
b[1] = static_cast<std::uint8_t>(v & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace xone
|
} // namespace xone
|
||||||
|
|||||||
+288
-3
@@ -4,12 +4,297 @@
|
|||||||
// GIP protocol (Game Input Protocol)
|
// GIP protocol (Game Input Protocol)
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
// Port target: medusalix/xone bus/protocol.c + bus/bus.c.
|
// 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 {
|
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
|
} // namespace xone::gip
|
||||||
|
|||||||
@@ -0,0 +1,483 @@
|
|||||||
|
// GIP authentication handshake (port of medusalix/xone auth/auth.c).
|
||||||
|
|
||||||
|
#include "auth/auth.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#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<u8, k_trailer_len> trailer;
|
||||||
|
} XONE_PACKED;
|
||||||
|
|
||||||
|
struct pkt_host_hello {
|
||||||
|
header_full header;
|
||||||
|
std::array<u8, k_random_len> random;
|
||||||
|
std::array<u8, 4> unknown1;
|
||||||
|
std::array<u8, 4> unknown2;
|
||||||
|
std::array<u8, k_trailer_len> trailer;
|
||||||
|
} XONE_PACKED;
|
||||||
|
|
||||||
|
struct pkt_host_secret {
|
||||||
|
header_full header;
|
||||||
|
std::array<u8, k_encrypted_pms_len> encrypted_pms;
|
||||||
|
std::array<u8, k_trailer_len> trailer;
|
||||||
|
} XONE_PACKED;
|
||||||
|
|
||||||
|
struct pkt_host_finish {
|
||||||
|
header_full header;
|
||||||
|
std::array<u8, k_transcript_len> transcript;
|
||||||
|
std::array<u8, k_trailer_len> trailer;
|
||||||
|
} XONE_PACKED;
|
||||||
|
|
||||||
|
struct pkt_client_hello {
|
||||||
|
std::array<u8, k_random_len> random;
|
||||||
|
std::array<u8, 48> unknown;
|
||||||
|
} XONE_PACKED;
|
||||||
|
|
||||||
|
struct pkt_client_finish {
|
||||||
|
std::array<u8, k_transcript_len> transcript;
|
||||||
|
std::array<u8, 32> unknown;
|
||||||
|
} XONE_PACKED;
|
||||||
|
|
||||||
|
struct pkt2_host_hello {
|
||||||
|
header_full header;
|
||||||
|
std::array<u8, k_random_len> random;
|
||||||
|
std::array<u8, 4> unknown;
|
||||||
|
std::array<u8, k_trailer_len> trailer;
|
||||||
|
} XONE_PACKED;
|
||||||
|
|
||||||
|
struct pkt2_host_pubkey {
|
||||||
|
header_full header;
|
||||||
|
std::array<u8, k_pubkey2_len> pubkey;
|
||||||
|
std::array<u8, k_trailer_len> trailer;
|
||||||
|
} XONE_PACKED;
|
||||||
|
|
||||||
|
struct pkt2_client_hello {
|
||||||
|
std::array<u8, k_random_len> random;
|
||||||
|
std::array<u8, 108> unknown1;
|
||||||
|
std::array<u8, 32> unknown2;
|
||||||
|
} XONE_PACKED;
|
||||||
|
|
||||||
|
struct pkt2_client_cert {
|
||||||
|
std::array<char, 4> header;
|
||||||
|
std::array<u8, 136> unknown1;
|
||||||
|
std::array<char, 32> chip;
|
||||||
|
std::array<char, 20> revision;
|
||||||
|
std::array<u8, 576> unknown2;
|
||||||
|
} XONE_PACKED;
|
||||||
|
|
||||||
|
struct pkt2_client_pubkey {
|
||||||
|
std::array<u8, k_pubkey2_len> pubkey;
|
||||||
|
std::array<u8, 64> unknown;
|
||||||
|
} XONE_PACKED;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auth::auth(auth_sink& sink)
|
||||||
|
: sink_(sink)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
auto auth::start() -> int
|
||||||
|
{
|
||||||
|
return send_hello();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto auth::process_pkt(std::span<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
auto const* hdr = reinterpret_cast<header_handshake const*>(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<header_full*>(const_cast<u8*>(static_cast<u8 const*>(pkt)));
|
||||||
|
u16 data_len = static_cast<u16>(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<u16>(data_len - sizeof(hdr->data)));
|
||||||
|
|
||||||
|
last_sent_command_ = cmd;
|
||||||
|
transcript_.update(
|
||||||
|
{reinterpret_cast<u8 const*>(hdr) + sizeof(hdr->handshake), data_len});
|
||||||
|
|
||||||
|
return sink_.send({reinterpret_cast<u8 const*>(hdr), len}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto auth::request_pkt(u8 cmd, std::uint16_t len) -> int
|
||||||
|
{
|
||||||
|
pkt_request req{};
|
||||||
|
u16 data_len = static_cast<u16>(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<u8 const*>(&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<u8 const*>(&hdr), sizeof(hdr)}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto auth::get_transcript() -> std::array<u8, k_transcript_len>
|
||||||
|
{
|
||||||
|
sha256 snap = transcript_;
|
||||||
|
std::array<u8, k_transcript_len> out{};
|
||||||
|
snap.finalize(out);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto auth::exchange_rsa() -> void
|
||||||
|
{
|
||||||
|
pkt_host_secret pkt{};
|
||||||
|
std::array<u8, k_random_len * 2> random{};
|
||||||
|
std::array<u8, k_secret_len> 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<u8, k_encrypted_pms_len> 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<u8, k_random_len * 2> random{};
|
||||||
|
std::array<u8, k_secret2_len> 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<u8, k_random_len * 2> random{};
|
||||||
|
std::array<u8, k_session_key_len> 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<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
auto const* hdr = reinterpret_cast<header_full const*>(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<u8 const> 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<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
auto const* pkt = reinterpret_cast<pkt_client_hello const*>(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<u8 const> 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<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
auto const* pkt = reinterpret_cast<pkt_client_finish const*>(data.data());
|
||||||
|
|
||||||
|
if (data.size() < sizeof(*pkt))
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
auto transcript = get_transcript();
|
||||||
|
std::array<u8, k_transcript_len> 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<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
auto const* pkt = reinterpret_cast<pkt2_client_hello const*>(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<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
auto const* pkt = reinterpret_cast<pkt2_client_cert const*>(data.data());
|
||||||
|
|
||||||
|
if (data.size() < sizeof(*pkt))
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
log_msg(log_level::debug, "auth: chip=%.*s, revision=%.*s",
|
||||||
|
static_cast<int>(sizeof(pkt->chip)), pkt->chip.data(),
|
||||||
|
static_cast<int>(sizeof(pkt->revision)), pkt->revision.data());
|
||||||
|
|
||||||
|
return request_pkt(cmd2_client_pubkey, sizeof(pkt2_client_pubkey));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto auth::handle_pubkey(std::span<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
auto const* pkt = reinterpret_cast<pkt2_client_pubkey const*>(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
|
||||||
+549
-2
@@ -1,7 +1,554 @@
|
|||||||
// Auth + crypto
|
// Auth + crypto primitives (CommonCrypto / Security.framework / P-256 ECDH)
|
||||||
// TODO(phase 1): port from medusalix/xone auth/auth.c + auth/crypto.c.
|
// Port target: medusalix/xone auth/crypto.c.
|
||||||
|
|
||||||
#include "auth/crypto.hpp"
|
#include "auth/crypto.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include <Security/Security.h>
|
||||||
|
|
||||||
namespace xone::auth {
|
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<s128>(a.l[i]) - static_cast<s128>(b.l[i]) - borrow;
|
||||||
|
if (t < 0) {
|
||||||
|
t += static_cast<s128>(static_cast<u64>(1) << 63) * 2; // + 2^64
|
||||||
|
borrow = 1;
|
||||||
|
} else {
|
||||||
|
borrow = 0;
|
||||||
|
}
|
||||||
|
r.l[i] = static_cast<u64>(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<u128>(a.l[i]) + b.l[i] + carry;
|
||||||
|
r.l[i] = static_cast<u64>(t);
|
||||||
|
carry = static_cast<u64>(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<u128>(m) * k_p.l[j] + t[i + j] + carry;
|
||||||
|
t[i + j] = static_cast<u64>(x);
|
||||||
|
carry = static_cast<u64>(x >> 64);
|
||||||
|
}
|
||||||
|
int k = i + 4;
|
||||||
|
while (carry && k < 9) {
|
||||||
|
u128 x = static_cast<u128>(t[k]) + carry;
|
||||||
|
t[k] = static_cast<u64>(x);
|
||||||
|
carry = static_cast<u64>(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<u128>(a.l[i]) * b.l[j] + t[i + j] + carry;
|
||||||
|
t[i + j] = static_cast<u64>(x);
|
||||||
|
carry = static_cast<u64>(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<u8>(v);
|
||||||
|
v >>= 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal form (big-endian bytes) -> Montgomery form.
|
||||||
|
auto fe_from_bytes(std::array<u8, 32> 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<u8, 32>
|
||||||
|
{
|
||||||
|
fe n = fe_from_mont(a);
|
||||||
|
std::array<u8, 32> 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<u8, 32> px{};
|
||||||
|
std::array<u8, 32> 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<u8 const> data) -> void
|
||||||
|
{
|
||||||
|
CC_SHA256_Update(&ctx_, data.data(), static_cast<CC_LONG>(data.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto sha256::finalize(std::array<u8, k_sha256_len>& out) -> void
|
||||||
|
{
|
||||||
|
CC_SHA256_Final(out.data(), &ctx_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// HMAC-SHA256
|
||||||
|
// ==========================================================================
|
||||||
|
hmac_sha256::hmac_sha256(std::span<u8 const> key)
|
||||||
|
{
|
||||||
|
u8 block[64] = {};
|
||||||
|
u8 key_block[64] = {};
|
||||||
|
|
||||||
|
if (key.size() > 64) {
|
||||||
|
std::array<u8, 32> 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<u8>(key_block[i] ^ 0x36);
|
||||||
|
inner_.update({&block[i], 1});
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 64; ++i) {
|
||||||
|
block[i] = static_cast<u8>(key_block[i] ^ 0x5c);
|
||||||
|
outer_.update({&block[i], 1});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto hmac_sha256::update(std::span<u8 const> data) -> void
|
||||||
|
{
|
||||||
|
inner_.update(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto hmac_sha256::finalize(std::array<u8, k_sha256_len>& out) -> void
|
||||||
|
{
|
||||||
|
std::array<u8, k_sha256_len> inner_digest{};
|
||||||
|
inner_.finalize(inner_digest);
|
||||||
|
outer_.update(inner_digest);
|
||||||
|
outer_.finalize(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// TLS P_SHA256 PRF
|
||||||
|
// ==========================================================================
|
||||||
|
auto prf_sha256(std::span<u8 const> key, std::string_view label,
|
||||||
|
std::span<u8 const> seed, std::span<u8> out) -> void
|
||||||
|
{
|
||||||
|
std::span<u8 const> label_bytes{reinterpret_cast<u8 const*>(label.data()),
|
||||||
|
label.size()};
|
||||||
|
|
||||||
|
std::array<u8, k_sha256_len> 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<u8, k_sha256_len> block{};
|
||||||
|
{
|
||||||
|
hmac_sha256 h{key};
|
||||||
|
h.update(a);
|
||||||
|
h.update(label_bytes);
|
||||||
|
h.update(seed);
|
||||||
|
h.finalize(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t n = std::min<std::size_t>(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<u8 const> der_key,
|
||||||
|
std::span<u8 const> plaintext, std::span<u8> out)
|
||||||
|
-> bool
|
||||||
|
{
|
||||||
|
CFDataRef key_data = CFDataCreate(
|
||||||
|
kCFAllocatorDefault, der_key.data(), static_cast<CFIndex>(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<CFIndex>(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<CFIndex>(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<u8, 32> bx = fe_to_bytes(r.x);
|
||||||
|
std::array<u8, 32> 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<u8, 32> px{};
|
||||||
|
std::array<u8, 32> 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<u8> out) -> void
|
||||||
|
{
|
||||||
|
arc4random_buf(out.data(), out.size());
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace xone::auth
|
} // namespace xone::auth
|
||||||
|
|||||||
+936
-2
@@ -1,7 +1,941 @@
|
|||||||
// GIP protocol (Game Input Protocol)
|
// GIP protocol (port of medusalix/xone bus/protocol.c + bus/bus.c).
|
||||||
// TODO(phase 1): port from medusalix/xone bus/protocol.c + bus/bus.c.
|
|
||||||
|
|
||||||
#include "gip/protocol.hpp"
|
#include "gip/protocol.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#include "common/log.hpp"
|
||||||
|
#include "common/types.hpp"
|
||||||
|
|
||||||
namespace xone::gip {
|
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<u8>(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<u32>(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<u8 const> pkt) -> int
|
||||||
|
{
|
||||||
|
gip_header hdr{};
|
||||||
|
hdr.command = GIP_CMD_RUMBLE;
|
||||||
|
hdr.options = id_;
|
||||||
|
hdr.packet_length = static_cast<u32>(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<u8 const> 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<u32>(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<u8> frame(static_cast<std::size_t>(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<u8 const> pkt, bool acknowledge) -> int
|
||||||
|
{
|
||||||
|
gip_header hdr{};
|
||||||
|
hdr.command = GIP_CMD_AUTHENTICATE;
|
||||||
|
hdr.options = id_ | GIP_OPT_INTERNAL;
|
||||||
|
hdr.packet_length = static_cast<u32>(pkt.size());
|
||||||
|
|
||||||
|
if (acknowledge)
|
||||||
|
hdr.options |= GIP_OPT_ACKNOWLEDGE;
|
||||||
|
|
||||||
|
return adapter_.send_pkt(*this, hdr, pkt.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto client::set_encryption_key(std::span<u8 const> 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<client>(*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<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
while (data.size() > k_hdr_min_length) {
|
||||||
|
gip_header hdr;
|
||||||
|
int hdr_len = decode_header(hdr, data.data(),
|
||||||
|
static_cast<int>(data.size()));
|
||||||
|
if (data.size() < static_cast<std::size_t>(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<std::size_t>(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<u8> frame(static_cast<std::size_t>(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<client::chunk_buffer>& buf) -> int
|
||||||
|
{
|
||||||
|
if (hdr.chunk_offset > k_chunk_buf_max_length)
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
buf = std::make_unique<client::chunk_buffer>();
|
||||||
|
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<u16>(len));
|
||||||
|
|
||||||
|
if ((ack.options & GIP_OPT_CHUNK) && buf)
|
||||||
|
store_le16(pkt + 7, static_cast<u16>(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<u32>(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<u8 const*>(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<u8 const*>(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<u16>((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<u8 const*>(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<u8 const*>(data);
|
||||||
|
u16 offsets[8];
|
||||||
|
for (int i = 0; i < 8; ++i)
|
||||||
|
offsets[i] = load_le16(base + 16 + 2 * i);
|
||||||
|
|
||||||
|
std::span<u8 const> 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<u8 const*>(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<u8 const*>(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<u8 const*>(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<u8 const*>(data), len});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto adapter::handle_pkt_input(client& c, void const* data, u32 len) -> int
|
||||||
|
{
|
||||||
|
listener_.on_input(c, {static_cast<u8 const*>(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<u8 const*>(data);
|
||||||
|
listener_.on_audio_samples(c, {p + 2, len - 2});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto adapter::parse_info_element(std::span<u8 const> data, u16 offset,
|
||||||
|
int item_length,
|
||||||
|
std::unique_ptr<client::info_element>& out) -> int
|
||||||
|
{
|
||||||
|
if (!offset)
|
||||||
|
return -EOPNOTSUPP;
|
||||||
|
if (data.size() < static_cast<std::size_t>(offset) + 1)
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
u8 count = data[offset];
|
||||||
|
++offset;
|
||||||
|
if (!count)
|
||||||
|
return -EOPNOTSUPP;
|
||||||
|
|
||||||
|
int total = count * item_length;
|
||||||
|
if (data.size() < static_cast<std::size_t>(offset) + total)
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
auto elem = std::make_unique<client::info_element>();
|
||||||
|
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<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
std::unique_ptr<client::info_element> 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<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
std::unique_ptr<client::info_element> 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<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
std::unique_ptr<client::info_element> 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<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
std::unique_ptr<client::info_element> 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<u8 const> data, u16 offset) -> int
|
||||||
|
{
|
||||||
|
if (data.size() < static_cast<std::size_t>(offset) + 1)
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
u8 count = data[offset];
|
||||||
|
++offset;
|
||||||
|
if (!count)
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
while (static_cast<int>(c.classes_.size()) < count) {
|
||||||
|
if (data.size() < static_cast<std::size_t>(offset) + 2)
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
u16 str_len = load_le16(data.data() + offset);
|
||||||
|
offset += 2;
|
||||||
|
if (!str_len || data.size() < static_cast<std::size_t>(offset) + str_len)
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
c.classes_.emplace_back(
|
||||||
|
reinterpret_cast<char const*>(data.data() + offset), str_len);
|
||||||
|
offset += str_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto adapter::parse_interfaces(client& c, u16 const offsets[8],
|
||||||
|
std::span<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
std::unique_ptr<client::info_element> 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<u8 const> data) -> int
|
||||||
|
{
|
||||||
|
std::unique_ptr<client::info_element> 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
|
} // namespace xone::gip
|
||||||
|
|||||||
@@ -15,6 +15,27 @@ target_compile_features(test_types PRIVATE cxx_std_23)
|
|||||||
add_test(NAME test_types COMMAND test_types)
|
add_test(NAME test_types COMMAND test_types)
|
||||||
list(APPEND TEST_TARGETS 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
|
add_custom_target(check
|
||||||
COMMAND ${CMAKE_CTEST_COMMAND}
|
COMMAND ${CMAKE_CTEST_COMMAND}
|
||||||
--test-dir "${CMAKE_BINARY_DIR}"
|
--test-dir "${CMAKE_BINARY_DIR}"
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
#include "unity.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include <Security/Security.h>
|
||||||
|
|
||||||
|
#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<u8> data;
|
||||||
|
bool ack;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<sent> packets;
|
||||||
|
std::vector<u8> key;
|
||||||
|
|
||||||
|
auto send(std::span<u8 const> pkt, bool ack) -> int override
|
||||||
|
{
|
||||||
|
packets.push_back({std::vector<u8>(pkt.begin(), pkt.end()), ack});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto set_encryption_key(std::span<u8 const> 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<u8> const& payload) -> std::vector<u8>
|
||||||
|
{
|
||||||
|
std::vector<u8> p;
|
||||||
|
u16 data_len = static_cast<u16>(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<u8>(data_len >> 8));
|
||||||
|
p.push_back(static_cast<u8>(data_len & 0xFF));
|
||||||
|
|
||||||
|
p.push_back(cmd); // data.command
|
||||||
|
p.push_back(0x01); // data.version
|
||||||
|
u16 data_body = static_cast<u16>(data_len - 4);
|
||||||
|
p.push_back(static_cast<u8>(data_body >> 8));
|
||||||
|
p.push_back(static_cast<u8>(data_body & 0xFF));
|
||||||
|
|
||||||
|
p.insert(p.end(), payload.begin(), payload.end());
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto make_rsa_der() -> std::vector<u8>
|
||||||
|
{
|
||||||
|
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<u8> 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<u8> const& v, std::size_t i) -> u16
|
||||||
|
{
|
||||||
|
return static_cast<u16>((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<u8> 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<u8>(80, 0x22)));
|
||||||
|
std::size_t base = sink.packets.size();
|
||||||
|
|
||||||
|
// certificate: some prefix bytes + ASN.1 SEQUENCE + 270-byte DER
|
||||||
|
std::vector<u8> 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<u8> 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<u8>(80, 0x22)));
|
||||||
|
|
||||||
|
std::vector<u8> der = make_rsa_der();
|
||||||
|
std::vector<u8> 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<u8> 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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
#include "unity.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include <Security/Security.h>
|
||||||
|
|
||||||
|
#include "auth/crypto.hpp"
|
||||||
|
|
||||||
|
void setUp() {}
|
||||||
|
void tearDown() {}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using u8 = std::uint8_t;
|
||||||
|
|
||||||
|
auto hex(std::string_view s) -> std::vector<u8>
|
||||||
|
{
|
||||||
|
std::vector<u8> 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<u8>(std::strtoul(buf, nullptr, 16)));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <std::size_t N>
|
||||||
|
auto to_array(std::vector<u8> const& v) -> std::array<u8, N>
|
||||||
|
{
|
||||||
|
std::array<u8, N> a{};
|
||||||
|
TEST_ASSERT(v.size() == N);
|
||||||
|
std::copy(v.begin(), v.end(), a.begin());
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto span(std::vector<u8> const& v) -> std::span<u8 const>
|
||||||
|
{
|
||||||
|
return {v.data(), v.size()};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// SHA-256
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
void test_sha256_abc(void)
|
||||||
|
{
|
||||||
|
std::array<u8, 32> out{};
|
||||||
|
xone::auth::sha256 h;
|
||||||
|
std::string data = "abc";
|
||||||
|
h.update({reinterpret_cast<u8 const*>(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<u8, 32> out{};
|
||||||
|
xone::auth::sha256 h;
|
||||||
|
std::string a = "abc";
|
||||||
|
h.update({reinterpret_cast<u8 const*>(a.data()), a.size()});
|
||||||
|
|
||||||
|
xone::auth::sha256 snap = h; // snapshot
|
||||||
|
std::string b = "def";
|
||||||
|
h.update({reinterpret_cast<u8 const*>(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<u8, 32> 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<u8 const*>(key.data()), key.size()}};
|
||||||
|
h.update({reinterpret_cast<u8 const*>(data.data()), data.size()});
|
||||||
|
std::array<u8, 32> 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<u8> key(20, 0x0b);
|
||||||
|
std::string data = "Hi There";
|
||||||
|
xone::auth::hmac_sha256 h{span(key)};
|
||||||
|
h.update({reinterpret_cast<u8 const*>(data.data()), data.size()});
|
||||||
|
std::array<u8, 32> 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<u8> key = hex("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff");
|
||||||
|
std::vector<u8> seed = hex("deadbeefcafebabe");
|
||||||
|
std::array<u8, 48> 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<u8> 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<u8> plaintext(48, 0x5a);
|
||||||
|
std::vector<u8> 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<CFIndex>(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<u8> 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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
#include "unity.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<std::vector<u8>> frames;
|
||||||
|
|
||||||
|
auto send_frame(std::span<u8 const> 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<std::vector<u8>> 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<u8 const> 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<u8> const& payload) -> std::vector<u8>
|
||||||
|
{
|
||||||
|
std::vector<u8> f;
|
||||||
|
f.push_back(command);
|
||||||
|
f.push_back(options);
|
||||||
|
f.push_back(sequence);
|
||||||
|
u32 len = static_cast<u32>(payload.size());
|
||||||
|
do {
|
||||||
|
u8 b = static_cast<u8>(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<u8>
|
||||||
|
{
|
||||||
|
std::vector<u8> 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<u8>
|
||||||
|
{
|
||||||
|
// 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<u8>& v, u16 x) {
|
||||||
|
v.push_back(static_cast<u8>(x & 0xFF));
|
||||||
|
v.push_back(static_cast<u8>(x >> 8));
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<u8> 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<u8>(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<u8>(i));
|
||||||
|
|
||||||
|
u16 offsets[8] = {0, 29, 0, 34, 36, 16, 38, 0};
|
||||||
|
|
||||||
|
std::vector<u8> 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<u8> 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<u8> 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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user