diff --git a/applications/main/bad_usb/helpers/ducky_script.c b/applications/main/bad_usb/helpers/ducky_script.c index c4aa91062297..46cfd70a16a8 100644 --- a/applications/main/bad_usb/helpers/ducky_script.c +++ b/applications/main/bad_usb/helpers/ducky_script.c @@ -8,13 +8,11 @@ #include "ducky_script.h" #include "ducky_script_i.h" #include +#include "helpers/keyboard.h" #define TAG "BadUsb" #define WORKER_TAG TAG "Worker" -#define BADUSB_ASCII_TO_KEY(script, x) \ - (((uint8_t)x < 128) ? (script->layout[(uint8_t)x]) : HID_KEYBOARD_NONE) - typedef enum { WorkerEvtStartStop = (1 << 0), WorkerEvtPauseResume = (1 << 1), @@ -57,7 +55,7 @@ uint16_t ducky_get_keycode(BadUsbScript* bad_usb, const char* param, bool accept } if((accept_chars) && (strlen(param) > 0)) { - return (BADUSB_ASCII_TO_KEY(bad_usb, param[0]) & 0xFF); + return (bad_usb_keyboard_get_key(bad_usb->layout, param[0]) & 0xFF); } return 0; } @@ -134,44 +132,27 @@ int32_t ducky_error(BadUsbScript* bad_usb, const char* text, ...) { } bool ducky_string(BadUsbScript* bad_usb, const char* param) { - uint32_t i = 0; + bad_usb_keyboard_type_string(bad_usb->layout, param); - while(param[i] != '\0') { - if(param[i] != '\n') { - uint16_t keycode = BADUSB_ASCII_TO_KEY(bad_usb, param[i]); - if(keycode != HID_KEYBOARD_NONE) { - furi_hal_hid_kb_press(keycode); - furi_hal_hid_kb_release(keycode); - } - } else { - furi_hal_hid_kb_press(HID_KEYBOARD_RETURN); - furi_hal_hid_kb_release(HID_KEYBOARD_RETURN); - } - i++; - } bad_usb->stringdelay = 0; return true; } static bool ducky_string_next(BadUsbScript* bad_usb) { - if(bad_usb->string_print_pos >= furi_string_size(bad_usb->string_print)) { - return true; - } + FuriStringUTF8State utf8state = FuriStringUTF8StateStarting; + FuriStringUnicodeValue codepoint; + do { + if(bad_usb->string_print_pos >= furi_string_size(bad_usb->string_print)) { + return true; + } - char print_char = furi_string_get_char(bad_usb->string_print, bad_usb->string_print_pos); + char print_char = furi_string_get_char(bad_usb->string_print, bad_usb->string_print_pos); + furi_string_utf8_decode(print_char, &utf8state, &codepoint); - if(print_char != '\n') { - uint16_t keycode = BADUSB_ASCII_TO_KEY(bad_usb, print_char); - if(keycode != HID_KEYBOARD_NONE) { - furi_hal_hid_kb_press(keycode); - furi_hal_hid_kb_release(keycode); - } - } else { - furi_hal_hid_kb_press(HID_KEYBOARD_RETURN); - furi_hal_hid_kb_release(HID_KEYBOARD_RETURN); - } + bad_usb->string_print_pos++; + } while(utf8state != FuriStringUTF8StateStarting); - bad_usb->string_print_pos++; + bad_usb_keyboard_type_codepoint(bad_usb->layout, codepoint); return false; } @@ -637,19 +618,13 @@ static int32_t bad_usb_worker(void* context) { return 0; } -static void bad_usb_script_set_default_keyboard_layout(BadUsbScript* bad_usb) { - furi_assert(bad_usb); - memset(bad_usb->layout, HID_KEYBOARD_NONE, sizeof(bad_usb->layout)); - memcpy(bad_usb->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(bad_usb->layout))); -} - BadUsbScript* bad_usb_script_open(FuriString* file_path) { furi_assert(file_path); BadUsbScript* bad_usb = malloc(sizeof(BadUsbScript)); bad_usb->file_path = furi_string_alloc(); furi_string_set(bad_usb->file_path, file_path); - bad_usb_script_set_default_keyboard_layout(bad_usb); + bad_usb->layout = bad_usb_keyboard_alloc_default(); bad_usb->st.state = BadUsbStateInit; bad_usb->st.error[0] = '\0'; @@ -680,14 +655,16 @@ void bad_usb_script_set_keyboard_layout(BadUsbScript* bad_usb, FuriString* layou if(!furi_string_empty(layout_path)) { //-V1051 if(storage_file_open( layout_file, furi_string_get_cstr(layout_path), FSAM_READ, FSOM_OPEN_EXISTING)) { - uint16_t layout[128]; - if(storage_file_read(layout_file, layout, sizeof(layout)) == sizeof(layout)) { - memcpy(bad_usb->layout, layout, sizeof(layout)); + bad_usb_keyboard_free(bad_usb->layout); + bad_usb->layout = bad_usb_keyboard_alloc_read(layout_file); + if(!bad_usb->layout) { + bad_usb->layout = bad_usb_keyboard_alloc_default(); } } storage_file_close(layout_file); } else { - bad_usb_script_set_default_keyboard_layout(bad_usb); + bad_usb_keyboard_free(bad_usb->layout); + bad_usb->layout = bad_usb_keyboard_alloc_default(); } storage_file_free(layout_file); } diff --git a/applications/main/bad_usb/helpers/ducky_script_i.h b/applications/main/bad_usb/helpers/ducky_script_i.h index 84c7ef9de6cf..8631b515900b 100644 --- a/applications/main/bad_usb/helpers/ducky_script_i.h +++ b/applications/main/bad_usb/helpers/ducky_script_i.h @@ -7,6 +7,7 @@ extern "C" { #include #include #include "ducky_script.h" +#include "keyboard.h" #define SCRIPT_STATE_ERROR (-1) #define SCRIPT_STATE_END (-2) @@ -30,7 +31,7 @@ struct BadUsbScript { uint32_t defdelay; uint32_t stringdelay; - uint16_t layout[128]; + BadUsbKeyboard* layout; FuriString* line; FuriString* line_prev; diff --git a/applications/main/bad_usb/helpers/keyboard.c b/applications/main/bad_usb/helpers/keyboard.c new file mode 100644 index 000000000000..86ffcf84d964 --- /dev/null +++ b/applications/main/bad_usb/helpers/keyboard.c @@ -0,0 +1,97 @@ +#include +#include +#include +#include "keyboard.h" +#include "keyboard_i.h" + +const uint8_t header_magic[3] = {0xFF, 'K', 'L'}; + +static int determine_version(File* layout_file) { + struct SharedHeader shared_header = {0}; + storage_file_read(layout_file, &shared_header, sizeof(shared_header)); + storage_file_seek(layout_file, 0, true); + + if(memcmp( + shared_header.magic, + header_magic, + MIN(sizeof(shared_header.magic), sizeof(header_magic))) != 0) { + // Backwards compat with v1 + return 1; + } + return shared_header.version; +} + +BadUsbKeyboard* bad_usb_keyboard_alloc_read(File* layout_file) { + switch(determine_version(layout_file)) { + case 1: + return bad_usb_keyboard_v1_alloc_read(layout_file); + case 2: + return bad_usb_keyboard_v2_alloc_read(layout_file); + default: + return NULL; + } +} + +BadUsbKeyboard* bad_usb_keyboard_alloc(void* data, const struct BadUsbKeyboardVTable* vtable) { + // vtable->get_key_sequence is optional + furi_assert(vtable); + furi_assert(vtable->get_key); + furi_assert(vtable->free); + + BadUsbKeyboard* kb = malloc(sizeof(BadUsbKeyboard)); + kb->data = data; + kb->vtable = vtable; + return kb; +} + +void bad_usb_keyboard_free(BadUsbKeyboard* keyboard) { + furi_assert(keyboard); + + keyboard->vtable->free(keyboard->data); + free(keyboard); +} + +void bad_usb_keyboard_type_codepoint(BadUsbKeyboard* keyboard, FuriStringUnicodeValue codepoint) { + furi_assert(keyboard); + + if(keyboard->vtable->get_key_sequence) { + uint16_t* key_sequence = keyboard->vtable->get_key_sequence(keyboard->data, codepoint); + + if(key_sequence == NULL) { + return; + } + + for(size_t i = 0; key_sequence[i] != HID_KEYBOARD_NONE; i++) { + furi_hal_hid_kb_press(key_sequence[i]); + furi_hal_hid_kb_release(key_sequence[i]); + } + } else { + uint16_t key = keyboard->vtable->get_key(keyboard->data, codepoint); + if(key != HID_KEYBOARD_NONE) { + furi_hal_hid_kb_press(key); + furi_hal_hid_kb_release(key); + } + } +} + +void bad_usb_keyboard_type_string(BadUsbKeyboard* keyboard, const char* string) { + furi_assert(keyboard); + + FuriStringUTF8State utf8_state = FuriStringUTF8StateStarting; + FuriStringUnicodeValue codepoint; + + for(size_t i = 0; string[i] != '\0'; i++) { + furi_string_utf8_decode(string[i], &utf8_state, &codepoint); + if(utf8_state != FuriStringUTF8StateStarting) { + continue; + } + + bad_usb_keyboard_type_codepoint(keyboard, codepoint); + } +} + +uint16_t bad_usb_keyboard_get_key(BadUsbKeyboard* keyboard, FuriStringUnicodeValue codepoint) { + furi_assert(keyboard); + + return keyboard->vtable->get_key(keyboard->data, codepoint); +} diff --git a/applications/main/bad_usb/helpers/keyboard.h b/applications/main/bad_usb/helpers/keyboard.h new file mode 100644 index 000000000000..b6cf0338d4ed --- /dev/null +++ b/applications/main/bad_usb/helpers/keyboard.h @@ -0,0 +1,23 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +typedef struct BadUsbKeyboard BadUsbKeyboard; + +BadUsbKeyboard* bad_usb_keyboard_alloc_default(); +BadUsbKeyboard* bad_usb_keyboard_alloc_read(File* layout_file); +void bad_usb_keyboard_free(BadUsbKeyboard* keyboard); + +void bad_usb_keyboard_type_string(BadUsbKeyboard* keyboard, const char* string); +void bad_usb_keyboard_type_codepoint(BadUsbKeyboard* keyboard, FuriStringUnicodeValue codepoint); + +uint16_t bad_usb_keyboard_get_key(BadUsbKeyboard* keyboard, FuriStringUnicodeValue codepoint); + +#ifdef __cplusplus +} +#endif diff --git a/applications/main/bad_usb/helpers/keyboard_default.c b/applications/main/bad_usb/helpers/keyboard_default.c new file mode 100644 index 000000000000..d58e0a907136 --- /dev/null +++ b/applications/main/bad_usb/helpers/keyboard_default.c @@ -0,0 +1,25 @@ +#include +#include +#include +#include "keyboard_i.h" + +static uint16_t get_key(void*, FuriStringUnicodeValue codepoint) { + if(codepoint >= sizeof(hid_asciimap) / sizeof(*hid_asciimap)) { + return HID_KEYBOARD_NONE; + } + + return hid_asciimap[codepoint]; +} + +static void free_data(void*) { + // No-op. +} + +static const struct BadUsbKeyboardVTable vtable = { + .get_key = get_key, + .free = free_data, +}; + +BadUsbKeyboard* bad_usb_keyboard_alloc_default() { + return bad_usb_keyboard_alloc(NULL, &vtable); +} diff --git a/applications/main/bad_usb/helpers/keyboard_i.h b/applications/main/bad_usb/helpers/keyboard_i.h new file mode 100644 index 000000000000..177ef3da103a --- /dev/null +++ b/applications/main/bad_usb/helpers/keyboard_i.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include "keyboard.h" + +struct BadUsbKeyboardVTable { + /** + * @brief Gets the key associated the codepoint. + * + * This gets the key that needs to be pressed to enter the character + * associated with codepoint. The key is represented by a 2 byte value with + * the USB HID Keyboard/Keypad Page Usage ID in the low byte and the + * modifier keys in the high byte. It is suitable for passing to + * furi_hal_hid_kb_press. + * + * This function is required to be implemented. + * + * @returns The keycode if the codepoint can meaningfully be entered by + * pressing a single key. Otherwise returns HID_KEYBOARD_NONE. + */ + uint16_t (*get_key)(void* data, FuriStringUnicodeValue codepoint); + + /** + * @brief Gets the sequence of keys associated with the codepoint. + * + * This gets the key sequence that needs to be pressed to enter the + * character associated with the codepoint. For example, to enter the + * codepoint U+00E9 “é” LATIN SMALL LETTER E WITH ACUTE on the + * US-International keyboard layout, one first needs to type + * HID_KEYBOARD_APOSTROPHE (0x34), followed by HID_KEYBOARD_E (0x08). + * + * This function is optional. It may be set to NULL, in which case get_key + * will be used instead. + * + * @returns A pointer to a sequence of keycodes, terminated by + * HID_KEYBOARD_NONE. NULL may be returned if no sequence could be found. + */ + uint16_t* (*get_key_sequence)(void* data, FuriStringUnicodeValue codepoint); + + /** + * @brief Called when the data structure needs to be freed. + */ + void (*free)(void* data); +}; + +struct BadUsbKeyboard { + const struct BadUsbKeyboardVTable* vtable; + void* data; +}; + +BadUsbKeyboard* bad_usb_keyboard_alloc(void* data, const struct BadUsbKeyboardVTable* vtable); + +BadUsbKeyboard* bad_usb_keyboard_layout_default_alloc(); +BadUsbKeyboard* bad_usb_keyboard_v1_alloc_read(File* layout); +BadUsbKeyboard* bad_usb_keyboard_v2_alloc_read(File* layout); + +struct SharedHeader { + uint8_t magic[3]; + uint8_t version; +} FURI_PACKED; + +#ifdef __cplusplus +} +#endif diff --git a/applications/main/bad_usb/helpers/keyboard_v1.c b/applications/main/bad_usb/helpers/keyboard_v1.c new file mode 100644 index 000000000000..faa7f8849a49 --- /dev/null +++ b/applications/main/bad_usb/helpers/keyboard_v1.c @@ -0,0 +1,33 @@ +#include +#include +#include "keyboard.h" +#include "keyboard_i.h" + +struct Layout { + uint16_t keycodes[128]; +}; + +static uint16_t get_key(void* data, FuriStringUnicodeValue codepoint) { + struct Layout* layout = data; + + if(codepoint >= sizeof(layout->keycodes) / sizeof(*layout->keycodes)) { + return HID_KEYBOARD_NONE; + } + + return layout->keycodes[codepoint]; +} + +static const struct BadUsbKeyboardVTable imp_v1 = { + .get_key = get_key, + .free = free, +}; + +BadUsbKeyboard* bad_usb_keyboard_v1_alloc_read(File* layout) { + struct Layout* data = malloc(sizeof(*data)); + if(storage_file_read(layout, data, sizeof(*data)) != sizeof(*data)) { + free(data); + return NULL; + } + + return bad_usb_keyboard_alloc(data, &imp_v1); +} diff --git a/applications/main/bad_usb/helpers/keyboard_v2.c b/applications/main/bad_usb/helpers/keyboard_v2.c new file mode 100644 index 000000000000..53829c7e3041 --- /dev/null +++ b/applications/main/bad_usb/helpers/keyboard_v2.c @@ -0,0 +1,169 @@ +#include +#include +#include +#include +#include +#include "keyboard.h" +#include "keyboard_i.h" + +struct Header { + struct SharedHeader shared; + uint16_t codepoint_range_len; + uint16_t key_sequences_len; +} FURI_PACKED; + +static_assert(sizeof(struct Header) % sizeof(uint32_t) == 0); + +#define CODEPOINTS_IN_CODEPOINT_RANGE 0x100 + +struct CodepointRange { + uint32_t range; + uint16_t key_offsets[CODEPOINTS_IN_CODEPOINT_RANGE]; +} FURI_PACKED; + +static_assert(sizeof(struct CodepointRange) % sizeof(uint32_t) == 0); + +struct Layout { + struct CodepointRange* codepoint_range; + size_t codepoint_range_len; + + uint16_t* key_sequences; + size_t key_sequences_len; +}; + +#define CODEPOINT_LOCAL_MASK 0x000000ff +#define CODEPOINT_RANGE_MASK 0xffffff00 + +static_assert(CODEPOINT_LOCAL_MASK < CODEPOINTS_IN_CODEPOINT_RANGE); + +static struct CodepointRange* + get_codepoint_range(struct Layout* layout, FuriStringUnicodeValue codepoint) { + uint32_t wanted_codepoint_range = codepoint & CODEPOINT_RANGE_MASK; + + for(size_t i = 0; i < layout->codepoint_range_len; i++) { + if(layout->codepoint_range[i].range == wanted_codepoint_range) { + return &layout->codepoint_range[i]; + } + } + + return NULL; +} + +static size_t key_sequence_len(uint16_t* keycode_sequence) { + size_t i = 0; + while(keycode_sequence[i] != 0) { + i++; + } + return i; +} + +static uint16_t* get_key_sequence(void* data, FuriStringUnicodeValue codepoint) { + struct Layout* layout = data; + + struct CodepointRange* codepoint_range = get_codepoint_range(layout, codepoint); + if(codepoint_range == NULL) { + return NULL; + } + + uint32_t codepoint_local = codepoint & CODEPOINT_LOCAL_MASK; + uint16_t offset = codepoint_range->key_offsets[codepoint_local]; + + if(offset >= layout->key_sequences_len) { + return NULL; + } + + return &layout->key_sequences[offset]; +} + +static uint16_t get_single_key(void* data, FuriStringUnicodeValue codepoint) { + struct Layout* layout = data; + + uint16_t* keycode_sequence = get_key_sequence(layout, codepoint); + if(key_sequence_len(keycode_sequence) != 1) { + return 0; + } + + return keycode_sequence[0]; +} + +static void free_layout(void* data) { + struct Layout* layout = data; + + free(layout->key_sequences); + free(layout->codepoint_range); + free(layout); +} + +static const struct BadUsbKeyboardVTable vtable = { + .get_key = get_single_key, + .get_key_sequence = get_key_sequence, + .free = free_layout, +}; + +static bool is_layout_valid(struct Layout* layout) { + for(size_t i = 0; i < layout->codepoint_range_len; i++) { + struct CodepointRange* codepoint_range = &layout->codepoint_range[i]; + if((codepoint_range->range & CODEPOINT_LOCAL_MASK) != 0) { + return false; + } + + for(size_t j = 0; j < CODEPOINTS_IN_CODEPOINT_RANGE; j++) { + if(codepoint_range->key_offsets[j] >= layout->key_sequences_len) { + return false; + } + } + } + + // Safety check to prevent us trying to read off the edge + if(layout->key_sequences[layout->key_sequences_len - 1] != 0x0000) { + return false; + } + + return true; +} + +BadUsbKeyboard* bad_usb_keyboard_v2_alloc_read(File* layout_file) { + struct Header header; + if(storage_file_read(layout_file, &header, sizeof(header)) != sizeof(header)) { + return NULL; + } + + struct Layout* layout = malloc(sizeof(*layout)); + + size_t codepoint_range_size = header.codepoint_range_len * sizeof(*layout->codepoint_range); + layout->codepoint_range = malloc(codepoint_range_size); + layout->codepoint_range_len = header.codepoint_range_len; + + size_t key_sequences_size = header.key_sequences_len * sizeof(*layout->key_sequences); + layout->key_sequences = malloc(key_sequences_size); + layout->key_sequences_len = header.key_sequences_len; + + if(storage_file_read(layout_file, layout->codepoint_range, codepoint_range_size) != + codepoint_range_size) { + free(layout->key_sequences); + free(layout->codepoint_range); + free(layout); + return NULL; + } + + if(storage_file_read(layout_file, layout->key_sequences, key_sequences_size) != + key_sequences_size) { + free(layout->key_sequences); + free(layout->codepoint_range); + free(layout); + return NULL; + } + + if(!is_layout_valid(layout)) { + free(layout->key_sequences); + free(layout->codepoint_range); + free(layout); + return NULL; + } + + BadUsbKeyboard* keyboard = malloc(sizeof(BadUsbKeyboard)); + keyboard->data = layout; + keyboard->vtable = &vtable; + + return keyboard; +} diff --git a/applications/main/bad_usb/resources/badusb/assets/layouts/en-US-intl.kl b/applications/main/bad_usb/resources/badusb/assets/layouts/en-US-intl.kl new file mode 100644 index 000000000000..45981b6d4f19 Binary files /dev/null and b/applications/main/bad_usb/resources/badusb/assets/layouts/en-US-intl.kl differ diff --git a/documentation/file_formats/BadUsbKeyboardFormat.md b/documentation/file_formats/BadUsbKeyboardFormat.md new file mode 100644 index 000000000000..0011484259dd --- /dev/null +++ b/documentation/file_formats/BadUsbKeyboardFormat.md @@ -0,0 +1,103 @@ +# Bad USB `.kl` Format + +The `.kl` files tell Bad USB what keys to press to make the correct characters +appear on screen. There are two formats, version 1 and version 2. + +## Version 1 + +Version 1 has no file header. The entire format is a table of 127 16-bit +[Keys](#key-format). The Keys are layed out so that a 7-bit ASCII character +code can be used to index the table and find the relevant Key. To indicate that +a character is not mapped, two zero bytes (`00 00`) may be used. This format is +in little-endian. + +## Version 2 + +Version 2 attempts to lift the limitations of version 1, by allowing non-ASCII +characters to be defined, and allowing to define that a character should be +entered by a sequence of keys, rather than being limited to just a single key. + +For example, to enter the codepoint U+00E9 “é” LATIN SMALL LETTER E WITH ACUTE +on the US-International keyboard layout, one first needs to first the +apostrophe key (here key 51), followed by the “E” key (here key 8). This file +format allows such characters to be defined. + +The layout of the version 2 file format is as follows: + + +========+==================+=================+ + | Header | Codepoint Ranges | Sequences Table | + +========+====================================+ + +All data is stored in little-endian. + +### Header + +The Header starts with the magic byte sequence FF 4B 4C, followed by the +version number, with is 02. The FF is to disambiguate it with the first file +format, as that is not a valid Usage ID on the USB HID Keyboard/Keypad Page. +After that, the amount of Codepoint Ranges are specified in an unsigned 16-bit +integer, and the length of the Sequences Tables is specified in an unsigned +16-bit integer. + + +------------+------------+------------+------------+ + | Header Magic Bytes: FF 4B 4C | Version: 2 | + +------------+------------+------------+------------+ + | # of Codepoint Ranges | Sequences Table length | + +------------+------------+------------+------------+ + +### Codepoint Ranges + +The Codepoint Ranges section of any number of Codepoint Range definitions. The +amount of Codepoint Ranges is specified in the header. + +A Codepoint Range starts with the Range Specifier, a 32-bit unsigned integer +that specifies which unicode codepoints it handles. The Codepoint Range handles +the codepoints starting at Range Specifier and ending at Range Specifier + +0x100, exclusive. The least significant byte of Range Specifier must always be +00. + +It is then followed by 256 Key Offsets, which are 16-bit unsigned integers that +indicate at which offset in the Sequences Table the Key Sequence can be found. + + +------+------+------+------+------+------+------+------+ +------+------+ + | Range Specifier | Key Offset | Key Offset | ... | Key Offset | + +------+------+------+------+------+------+------+------+ +------+------+ + +To indicate that a codepoint is unmapped, it is recommended is to store a +zero-length Key Sequence at offset zero of the Sequences Table, and point to +that. + +### Sequences Table + +The Sequences Table consist of any number of Key Sequences. A Key Sequence is +an array of Keys, as described by [Key Format](#key-format), terminated by a +16-bit null. + + +---+---+---+---+ +---+---+---+---+---+---+---+---+ +---+---+---+---+ + | Key | Key | ... | Key | 00 00 | Key | Key | ... | Key | 00 00 | ... + +---+---+---+---+ +---+---+---+---+---+---+---+---+ +---+---+---+---+ + +## Key Format + + +----------------+----------------+ + | Usage ID | Modifier Flags | + +----------------+----------------+ + +A key is two bytes wide, where the low byte is the Usage ID as found in the +[HID USB Keyboard/Keypad Page][HID Usage Tables]. In the high byte, the +modifier keys that need to be pressed for that key to be typed are found: + +```c +enum HidKeyboardMods { + KEY_MOD_LEFT_CTRL = (1 << 8), + KEY_MOD_LEFT_SHIFT = (1 << 9), + KEY_MOD_LEFT_ALT = (1 << 10), + KEY_MOD_LEFT_GUI = (1 << 11), + KEY_MOD_RIGHT_CTRL = (1 << 12), + KEY_MOD_RIGHT_SHIFT = (1 << 13), + KEY_MOD_RIGHT_ALT = (1 << 14), + KEY_MOD_RIGHT_GUI = (1 << 15), +}; +``` + +[HID Usage Tables]: https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf