Detect host OS based on USB fingerprint (#18463)

Co-authored-by: Drashna Jaelre <drashna@live.com>
Co-authored-by: Nick Brassel <nick@tzarc.org>
master
Ruslan Sayfutdinov 2022-12-08 16:45:30 +00:00 committed by GitHub
parent e06f50c489
commit 85ee55ff3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 448 additions and 7 deletions

View File

@ -62,6 +62,7 @@ include $(PLATFORM_PATH)/common.mk
include $(TMK_PATH)/protocol.mk
include $(QUANTUM_PATH)/debounce/tests/rules.mk
include $(QUANTUM_PATH)/encoder/tests/rules.mk
include $(QUANTUM_PATH)/os_detection/tests/rules.mk
include $(QUANTUM_PATH)/sequencer/tests/rules.mk
include $(QUANTUM_PATH)/wear_leveling/tests/rules.mk
include $(QUANTUM_PATH)/logging/print.mk

View File

@ -907,3 +907,11 @@ ifeq ($(strip $(ENCODER_ENABLE)), yes)
OPT_DEFS += -DENCODER_MAP_ENABLE
endif
endif
ifeq ($(strip $(OS_DETECTION_ENABLE)), yes)
SRC += $(QUANTUM_DIR)/os_detection.c
OPT_DEFS += -DOS_DETECTION_ENABLE
ifeq ($(strip $(OS_DETECTION_DEBUG_ENABLE)), yes)
OPT_DEFS += -DOS_DETECTION_DEBUG_ENABLE
endif
endif

View File

@ -3,6 +3,7 @@ FULL_TESTS := $(notdir $(TEST_LIST))
include $(QUANTUM_PATH)/debounce/tests/testlist.mk
include $(QUANTUM_PATH)/encoder/tests/testlist.mk
include $(QUANTUM_PATH)/os_detection/tests/testlist.mk
include $(QUANTUM_PATH)/sequencer/tests/testlist.mk
include $(QUANTUM_PATH)/wear_leveling/tests/testlist.mk
include $(PLATFORM_PATH)/test/testlist.mk

View File

@ -85,6 +85,7 @@
* [Key Overrides](feature_key_overrides.md)
* [Layers](feature_layers.md)
* [One Shot Keys](one_shot_keys.md)
* [OS Detection](feature_os_detection.md)
* [Raw HID](feature_rawhid.md)
* [Secure](feature_secure.md)
* [Send String](feature_send_string.md)

View File

@ -0,0 +1,77 @@
# OS Detection
This feature makes a best guess at the host OS based on OS specific behavior during USB setup. It may not always get the correct OS, and shouldn't be relied on as for critical functionality.
Using it you can have OS specific key mappings or combos which work differently on different devices.
It is available for keyboards which use ChibiOS, LUFA and V-USB.
## Usage
In your `rules.mk` add:
```make
OS_DETECTION_ENABLE = yes
```
Include `"os_detection.h"` in your `keymap.c`.
It declares `os_variant_t detected_host_os(void);` which you can call to get detected OS.
It returns one of the following values:
```c
enum {
OS_UNSURE,
OS_LINUX,
OS_WINDOWS,
OS_MACOS,
OS_IOS,
} os_variant_t;
```
?> Note that it takes some time after firmware is booted to detect the OS.
This time is quite short, probably hundreds of milliseconds, but this data may be not ready in keyboard and layout setup functions which run very early during firmware startup.
## Debug
If OS is guessed incorrectly, you may want to collect data about USB setup packets to refine the detection logic.
To do so in your `rules.mk` add:
```make
OS_DETECTION_DEBUG_ENABLE = yes
CONSOLE_ENABLE = yes
```
And also include `"os_detection.h"` in your `keymap.c`.
Then you can define custom keycodes to store data about USB setup packets in EEPROM (persistent memory) and to print it later on host where you can run `qmk console`:
```c
enum custom_keycodes {
STORE_SETUPS = SAFE_RANGE,
PRINT_SETUPS,
};
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case STORE_SETUPS:
if (record->event.pressed) {
store_setups_in_eeprom();
}
return false;
case PRINT_SETUPS:
if (record->event.pressed) {
print_stored_setups();
}
return false;
}
}
```
Then please open an issue on Github with this information and tell what OS was not detected correctly and if you have any intermediate devices between keyboard and your computer.
## Credits
Original idea is coming from [FingerprintUSBHost](https://github.com/keyboardio/FingerprintUSBHost) project.

129
quantum/os_detection.c Normal file
View File

@ -0,0 +1,129 @@
/* Copyright 2022 Ruslan Sayfutdinov (@KapJI)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "os_detection.h"
#include <string.h>
#ifdef OS_DETECTION_DEBUG_ENABLE
# include "eeconfig.h"
# include "eeprom.h"
# include "print.h"
# define STORED_USB_SETUPS 50
# define EEPROM_USER_OFFSET (uint8_t*)EECONFIG_SIZE
uint16_t usb_setups[STORED_USB_SETUPS];
#endif
#ifdef OS_DETECTION_ENABLE
struct setups_data_t {
uint8_t count;
uint8_t cnt_02;
uint8_t cnt_04;
uint8_t cnt_ff;
uint16_t last_wlength;
os_variant_t detected_os;
};
struct setups_data_t setups_data = {
.count = 0,
.cnt_02 = 0,
.cnt_04 = 0,
.cnt_ff = 0,
.detected_os = OS_UNSURE,
};
// Some collected sequences of wLength can be found in tests.
void make_guess(void) {
if (setups_data.count < 3) {
return;
}
if (setups_data.cnt_ff >= 2 && setups_data.cnt_04 >= 1) {
setups_data.detected_os = OS_WINDOWS;
return;
}
if (setups_data.count == setups_data.cnt_ff) {
// Linux has 3 packets with 0xFF.
setups_data.detected_os = OS_LINUX;
return;
}
if (setups_data.count == 5 && setups_data.last_wlength == 0xFF && setups_data.cnt_ff == 1 && setups_data.cnt_02 == 2) {
setups_data.detected_os = OS_MACOS;
return;
}
if (setups_data.count == 4 && setups_data.cnt_ff == 0 && setups_data.cnt_02 == 2) {
// iOS and iPadOS don't have the last 0xFF packet.
setups_data.detected_os = OS_IOS;
return;
}
if (setups_data.cnt_ff == 0 && setups_data.cnt_02 == 3 && setups_data.cnt_04 == 1) {
// This is actually PS5.
setups_data.detected_os = OS_LINUX;
return;
}
if (setups_data.cnt_ff >= 1 && setups_data.cnt_02 == 0 && setups_data.cnt_04 == 0) {
// This is actually Quest 2 or Nintendo Switch.
setups_data.detected_os = OS_LINUX;
return;
}
}
void process_wlength(const uint16_t w_length) {
# ifdef OS_DETECTION_DEBUG_ENABLE
usb_setups[setups_data.count] = w_length;
# endif
setups_data.count++;
setups_data.last_wlength = w_length;
if (w_length == 0x2) {
setups_data.cnt_02++;
} else if (w_length == 0x4) {
setups_data.cnt_04++;
} else if (w_length == 0xFF) {
setups_data.cnt_ff++;
}
make_guess();
}
os_variant_t detected_host_os(void) {
return setups_data.detected_os;
}
void erase_wlength_data(void) {
memset(&setups_data, 0, sizeof(setups_data));
}
#endif // OS_DETECTION_ENABLE
#ifdef OS_DETECTION_DEBUG_ENABLE
void print_stored_setups(void) {
# ifdef CONSOLE_ENABLE
uint8_t cnt = eeprom_read_byte(EEPROM_USER_OFFSET);
for (uint16_t i = 0; i < cnt; ++i) {
uint16_t* addr = (uint16_t*)EEPROM_USER_OFFSET + i * sizeof(uint16_t) + sizeof(uint8_t);
xprintf("i: %d, wLength: 0x%02X\n", i, eeprom_read_word(addr));
}
# endif
}
void store_setups_in_eeprom(void) {
eeprom_update_byte(EEPROM_USER_OFFSET, setups_data.count);
for (uint16_t i = 0; i < setups_data.count; ++i) {
uint16_t* addr = (uint16_t*)EEPROM_USER_OFFSET + i * sizeof(uint16_t) + sizeof(uint8_t);
eeprom_update_word(addr, usb_setups[i]);
}
}
#endif // OS_DETECTION_DEBUG_ENABLE

38
quantum/os_detection.h Normal file
View File

@ -0,0 +1,38 @@
/* Copyright 2022 Ruslan Sayfutdinov (@KapJI)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <stdint.h>
#ifdef OS_DETECTION_ENABLE
typedef enum {
OS_UNSURE,
OS_LINUX,
OS_WINDOWS,
OS_MACOS,
OS_IOS,
} os_variant_t;
void process_wlength(const uint16_t w_length);
os_variant_t detected_host_os(void);
void erase_wlength_data(void);
#endif
#ifdef OS_DETECTION_DEBUG_ENABLE
void print_stored_setups(void);
void store_setups_in_eeprom(void);
#endif

View File

@ -0,0 +1,164 @@
/* Copyright 2022 Ruslan Sayfutdinov (@KapJI)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "gtest/gtest.h"
extern "C" {
#include "os_detection.h"
}
class OsDetectionTest : public ::testing::Test {
protected:
void SetUp() override {
erase_wlength_data();
}
};
os_variant_t check_sequence(const std::vector<uint16_t> &w_lengths) {
for (auto &w_length : w_lengths) {
process_wlength(w_length);
}
return detected_host_os();
}
/* Some collected data.
ChibiOS:
Windows 10: [FF, FF, 4, 24, 4, 24, 4, FF, 24, FF, 4, FF, 24, 4, 24, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A, 20A]
Windows 10 (another host): [FF, FF, 4, 24, 4, 24, 4, 24, 4, 24, 4, 24]
macOS 12.5: [2, 24, 2, 28, FF]
iOS/iPadOS 15.6: [2, 24, 2, 28]
Linux (including Android, Raspberry Pi and WebOS TV): [FF, FF, FF]
PS5: [2, 4, 2, 28, 2, 24]
Nintendo Switch: [82, FF, 40, 40, FF, 40, 40, FF, 40, 40, FF, 40, 40, FF, 40, 40]
Quest 2: [FF, FF, FF, FE, FF, FE, FF, FE, FF, FE, FF]
LUFA:
Windows 10 (first connect): [12, FF, FF, 4, 10, FF, FF, FF, 4, 10, 20A, 20A, 20A, 20A, 20A, 20A]
Windows 10 (subsequent connect): [FF, FF, 4, 10, FF, 4, FF, 10, FF, 20A, 20A, 20A, 20A, 20A, 20A]
Windows 10 (another host): [FF, FF, 4, 10, 4, 10]
macOS: [2, 10, 2, E, FF]
iOS/iPadOS: [2, 10, 2, E]
Linux: [FF, FF, FF]
PS5: [2, 4, 2, E, 2, 10]
Nintendo Switch: [82, FF, 40, 40, FF, 40, 40]
V-USB:
Windows 10: [FF, FF, 4, E, FF]
Windows 10 (another host): [FF, FF, 4, E, 4]
macOS: [2, E, 2, E, FF]
iOS/iPadOS: [2, E, 2, E]
Linux: [FF, FF, FF]
PS5: [2, 4, 2, E, 2]
Nintendo Switch: [82, FF, 40, 40]
Quest 2: [FF, FF, FF, FE]
Common parts:
Windows: [..., FF, FF, 4, ...]
macOS: [2, _, 2, _, FF]
iOS/iPadOS: [2, _, 2, _]
Linux: [FF, FF, FF]
PS5: [2, 4, 2, _, 2, ...]
Nintendo Switch: [82, FF, 40, 40, ...]
Quest 2: [FF, FF, FF, FE, ...]
*/
TEST_F(OsDetectionTest, TestLinux) {
EXPECT_EQ(check_sequence({0xFF, 0xFF, 0xFF}), OS_LINUX);
}
TEST_F(OsDetectionTest, TestChibiosMacos) {
EXPECT_EQ(check_sequence({0x2, 0x24, 0x2, 0x28, 0xFF}), OS_MACOS);
}
TEST_F(OsDetectionTest, TestLufaMacos) {
EXPECT_EQ(check_sequence({0x2, 0x10, 0x2, 0xE, 0xFF}), OS_MACOS);
}
TEST_F(OsDetectionTest, TestVusbMacos) {
EXPECT_EQ(check_sequence({0x2, 0xE, 0x2, 0xE, 0xFF}), OS_MACOS);
}
TEST_F(OsDetectionTest, TestChibiosIos) {
EXPECT_EQ(check_sequence({0x2, 0x24, 0x2, 0x28}), OS_IOS);
}
TEST_F(OsDetectionTest, TestLufaIos) {
EXPECT_EQ(check_sequence({0x2, 0x10, 0x2, 0xE}), OS_IOS);
}
TEST_F(OsDetectionTest, TestVusbIos) {
EXPECT_EQ(check_sequence({0x2, 0xE, 0x2, 0xE}), OS_IOS);
}
TEST_F(OsDetectionTest, TestChibiosWindows10) {
EXPECT_EQ(check_sequence({0xFF, 0xFF, 0x4, 0x24, 0x4, 0x24, 0x4, 0xFF, 0x24, 0xFF, 0x4, 0xFF, 0x24, 0x4, 0x24, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A}), OS_WINDOWS);
}
TEST_F(OsDetectionTest, TestChibiosWindows10_2) {
EXPECT_EQ(check_sequence({0xFF, 0xFF, 0x4, 0x24, 0x4, 0x24, 0x4, 0x24, 0x4, 0x24, 0x4, 0x24}), OS_WINDOWS);
}
TEST_F(OsDetectionTest, TestLufaWindows10) {
EXPECT_EQ(check_sequence({0x12, 0xFF, 0xFF, 0x4, 0x10, 0xFF, 0xFF, 0xFF, 0x4, 0x10, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A}), OS_WINDOWS);
}
TEST_F(OsDetectionTest, TestLufaWindows10_2) {
EXPECT_EQ(check_sequence({0xFF, 0xFF, 0x4, 0x10, 0xFF, 0x4, 0xFF, 0x10, 0xFF, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A}), OS_WINDOWS);
}
TEST_F(OsDetectionTest, TestLufaWindows10_3) {
EXPECT_EQ(check_sequence({0xFF, 0xFF, 0x4, 0x10, 0x4, 0x10}), OS_WINDOWS);
}
TEST_F(OsDetectionTest, TestVusbWindows10) {
EXPECT_EQ(check_sequence({0xFF, 0xFF, 0x4, 0xE, 0xFF}), OS_WINDOWS);
}
TEST_F(OsDetectionTest, TestVusbWindows10_2) {
EXPECT_EQ(check_sequence({0xFF, 0xFF, 0x4, 0xE, 0x4}), OS_WINDOWS);
}
TEST_F(OsDetectionTest, TestChibiosPs5) {
EXPECT_EQ(check_sequence({0x2, 0x4, 0x2, 0x28, 0x2, 0x24}), OS_LINUX);
}
TEST_F(OsDetectionTest, TestLufaPs5) {
EXPECT_EQ(check_sequence({0x2, 0x4, 0x2, 0xE, 0x2, 0x10}), OS_LINUX);
}
TEST_F(OsDetectionTest, TestVusbPs5) {
EXPECT_EQ(check_sequence({0x2, 0x4, 0x2, 0xE, 0x2}), OS_LINUX);
}
TEST_F(OsDetectionTest, TestChibiosNintendoSwitch) {
EXPECT_EQ(check_sequence({0x82, 0xFF, 0x40, 0x40, 0xFF, 0x40, 0x40, 0xFF, 0x40, 0x40, 0xFF, 0x40, 0x40, 0xFF, 0x40, 0x40}), OS_LINUX);
}
TEST_F(OsDetectionTest, TestLufaNintendoSwitch) {
EXPECT_EQ(check_sequence({0x82, 0xFF, 0x40, 0x40, 0xFF, 0x40, 0x40}), OS_LINUX);
}
TEST_F(OsDetectionTest, TestVusbNintendoSwitch) {
EXPECT_EQ(check_sequence({0x82, 0xFF, 0x40, 0x40}), OS_LINUX);
}
TEST_F(OsDetectionTest, TestChibiosQuest2) {
EXPECT_EQ(check_sequence({0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF}), OS_LINUX);
}
TEST_F(OsDetectionTest, TestVusbQuest2) {
EXPECT_EQ(check_sequence({0xFF, 0xFF, 0xFF, 0xFE}), OS_LINUX);
}

View File

@ -0,0 +1,5 @@
os_detection_DEFS := -DOS_DETECTION_ENABLE
os_detection_SRC := \
$(QUANTUM_PATH)/os_detection/tests/os_detection.cpp \
$(QUANTUM_PATH)/os_detection.c

View File

@ -0,0 +1 @@
TEST_LIST += os_detection

View File

@ -111,9 +111,10 @@ uint8_t extra_report_blank[3] = {0};
static const USBDescriptor *usb_get_descriptor_cb(USBDriver *usbp, uint8_t dtype, uint8_t dindex, uint16_t wIndex) {
(void)usbp;
static USBDescriptor desc;
uint16_t wValue = ((uint16_t)dtype << 8) | dindex;
desc.ud_string = NULL;
desc.ud_size = get_usb_descriptor(wValue, wIndex, (const void **const) & desc.ud_string);
uint16_t wValue = ((uint16_t)dtype << 8) | dindex;
uint16_t wLength = ((uint16_t)usbp->setup[7] << 8) | usbp->setup[6];
desc.ud_string = NULL;
desc.ud_size = get_usb_descriptor(wValue, wIndex, wLength, (const void **const) & desc.ud_string);
if (desc.ud_string == NULL)
return NULL;
else
@ -585,7 +586,8 @@ static uint16_t get_hword(uint8_t *p) {
*/
static uint8_t set_report_buf[2] __attribute__((aligned(4)));
static void set_led_transfer_cb(USBDriver *usbp) {
static void set_led_transfer_cb(USBDriver *usbp) {
if (usbp->setup[6] == 2) { /* LSB(wLength) */
uint8_t report_id = set_report_buf[0];
if ((report_id == REPORT_ID_KEYBOARD) || (report_id == REPORT_ID_NKRO)) {

View File

@ -901,5 +901,5 @@ void protocol_post_task(void) {
}
uint16_t CALLBACK_USB_GetDescriptor(const uint16_t wValue, const uint16_t wIndex, const void **const DescriptorAddress) {
return get_usb_descriptor(wValue, wIndex, DescriptorAddress);
return get_usb_descriptor(wValue, wIndex, USB_ControlRequest.wLength, DescriptorAddress);
}

View File

@ -45,6 +45,10 @@
# include "joystick.h"
#endif
#ifdef OS_DETECTION_ENABLE
# include "os_detection.h"
#endif
// clang-format off
/*
@ -1092,7 +1096,7 @@ const USB_Descriptor_String_t PROGMEM SerialNumberString = {
* is called so that the descriptor details can be passed back and the appropriate descriptor sent back to the
* USB host.
*/
uint16_t get_usb_descriptor(const uint16_t wValue, const uint16_t wIndex, const void** const DescriptorAddress) {
uint16_t get_usb_descriptor(const uint16_t wValue, const uint16_t wIndex, const uint16_t wLength, const void** const DescriptorAddress) {
const uint8_t DescriptorType = (wValue >> 8);
const uint8_t DescriptorIndex = (wValue & 0xFF);
const void* Address = NULL;
@ -1134,6 +1138,9 @@ uint16_t get_usb_descriptor(const uint16_t wValue, const uint16_t wIndex, const
break;
#endif
}
#ifdef OS_DETECTION_ENABLE
process_wlength(wLength);
#endif
break;
case HID_DTYPE_HID:

View File

@ -308,4 +308,4 @@ enum usb_endpoints {
#define JOYSTICK_EPSIZE 8
#define DIGITIZER_EPSIZE 8
uint16_t get_usb_descriptor(const uint16_t wValue, const uint16_t wIndex, const void** const DescriptorAddress);
uint16_t get_usb_descriptor(const uint16_t wValue, const uint16_t wIndex, const uint16_t wLength, const void** const DescriptorAddress);

View File

@ -44,6 +44,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
# include "ring_buffer.h"
#endif
#ifdef OS_DETECTION_ENABLE
# include "os_detection.h"
#endif
#define NEXT_INTERFACE __COUNTER__
/*
@ -1013,6 +1017,9 @@ USB_PUBLIC usbMsgLen_t usbFunctionDescriptor(struct usbRequest *rq) {
break;
#endif
}
#ifdef OS_DETECTION_ENABLE
process_wlength(rq->wLength.word);
#endif
break;
case USBDESCR_HID:
switch (rq->wValue.bytes[0]) {