# pylint: disable=unused-wildcard-import, wildcard-import
#
# This file is part of Facedancer.
#
import asyncio
from typing import Iterable
from . import default_main
from .. import *
from ..classes.hid.usage import *
from ..classes.hid.descriptor import *
from ..classes.hid.keyboard import *
# Specifies how many simultaneously keys we want to support.
KEY_ROLLOVER = 8
[docs]
@use_inner_classes_automatically
class USBKeyboardDevice(USBDevice):
""" Simple USB keyboard device. """
name : str = "USB keyboard device"
product_string : str = "Non-suspicious Keyboard"
class KeyboardConfiguration(USBConfiguration):
""" Primary USB configuration: act as a keyboard. """
class KeyboardInterface(USBInterface):
""" Core HID interface for our keyboard. """
name : str = "USB keyboard interface"
class_number : int = 3
class KeyEventEndpoint(USBEndpoint):
number : int = 3
direction : USBDirection = USBDirection.IN
transfer_type : USBTransferType = USBTransferType.INTERRUPT
interval : int = 10
#
# Raw descriptors -- TODO: build these from their component parts.
#
class HIDDescriptor(USBDescriptor):
number : int = 0
type_number : int = USBDescriptorTypeNumber.HID
raw : bytes = b'\x09\x21\x10\x01\x00\x01\x22\x2b\x00'
include_in_config : bool = True
class ReportDescriptor(HIDReportDescriptor):
number : int = 0
fields : tuple = (
# Identify ourselves as a keyboard.
USAGE_PAGE (HIDUsagePage.GENERIC_DESKTOP),
USAGE (HIDGenericDesktopUsage.KEYBOARD),
COLLECTION (HIDCollection.APPLICATION),
USAGE_PAGE (HIDUsagePage.KEYBOARD),
# Modifier keys.
# These span the full range of modifier key codes (left control to right meta),
# and each has two possible values (0 = unpressed, 1 = pressed).
USAGE_MINIMUM (KeyboardKeys.LEFTCTRL),
USAGE_MAXIMUM (KeyboardKeys.RIGHTMETA),
LOGICAL_MINIMUM (0),
LOGICAL_MAXIMUM (1),
REPORT_SIZE (1),
REPORT_COUNT (KeyboardKeys.RIGHTMETA - KeyboardKeys.LEFTCTRL + 1),
INPUT (variable=True),
# One byte of constant zero-padding.
# This is required for compliance; and Windows will ignore this report
# if the zero byte isn't present.
REPORT_SIZE (8),
REPORT_COUNT (1),
INPUT (constant=True),
# Capture our actual, pressed keyboard keys.
# Support a standard, 101-key keyboard; which has
# keycodes from 0 (NONE) to 101 (COMPOSE).
#
# We provide the capability to press up to eight keys
# simultaneously. Setting the REPORT_COUNT effectively
# sets the key rollover; so 8 reports means we can have
# up to eight keys pressed at once.
USAGE_MINIMUM (KeyboardKeys.NONE),
USAGE_MAXIMUM (KeyboardKeys.COMPOSE),
LOGICAL_MINIMUM (KeyboardKeys.NONE),
LOGICAL_MAXIMUM (KeyboardKeys.COMPOSE),
REPORT_SIZE (8),
REPORT_COUNT (KEY_ROLLOVER),
INPUT (),
# End the report.
END_COLLECTION (),
)
@class_request_handler(number=USBStandardRequests.GET_INTERFACE)
@to_this_interface
def handle_get_interface_request(self, request):
# Silently stall GET_INTERFACE class requests.
request.stall()
def __post_init__(self):
super().__post_init__()
# Keep track of any pressed keys, and any pressed modifiers.
self.active_keys = set()
self.modifiers = 0
def _generate_hid_report(self) -> bytes:
""" Generates a single HID report for the given keyboard state. """
# If we have active keypresses, compose a set of scancodes from them.
scancodes = \
list(self.active_keys)[:KEY_ROLLOVER] + \
[0] * (KEY_ROLLOVER - len(self.active_keys))
return bytes([self.modifiers, 0, *scancodes])
[docs]
def handle_data_requested(self, endpoint: USBEndpoint):
""" Provide data once per host request. """
report = self._generate_hid_report()
endpoint.send(report)
#
# User-facing API.
#
[docs]
def key_down(self, code: KeyboardKeys):
""" Marks a given key as pressed; should be a scancode from KeyboardKeys. """
self.active_keys.add(code)
[docs]
def key_up(self, code: KeyboardKeys):
""" Marks a given key as released; should be a scancode from KeyboardKeys. """
self.active_keys.remove(code)
[docs]
def modifier_down(self, code: KeyboardModifiers):
""" Marks a given modifier as pressed; should be a flag from KeyboardModifiers. """
if code is not None:
self.modifiers |= code
[docs]
def modifier_up(self, code: KeyboardModifiers):
""" Marks a given modifier as released; should be a flag from KeyboardModifiers. """
if code is not None:
self.modifiers &= ~code
[docs]
async def type_scancode(self, code: KeyboardKeys, duration: float = 0.1, modifiers: KeyboardModifiers = None):
""" Presses, and then releases, a single key.
Parameters:
code -- The keyboard key to be pressed's scancode.
duration -- How long the given key should be pressed, in seconds.
modifiers -- Any modifier keys that should be held while typing.
"""
self.modifier_down(modifiers)
self.key_down(code)
await asyncio.sleep(duration)
self.key_up(code)
self.modifier_up(modifiers)
await asyncio.sleep(duration)
[docs]
async def type_scancodes(self, *codes: Iterable[KeyboardKeys], duration: float = 0.1):
""" Presses, and then releases, a collection of keys, in order.
Parameters:
*code -- The keyboard keys to be pressed's scancodes.
duration -- How long each key should be pressed, in seconds.
"""
for code in codes:
await self.type_scancode(code, duration=duration)
[docs]
async def type_letter(self, letter: str, duration: float = 0.1, modifiers: KeyboardModifiers = None):
""" Attempts to type a single letter, based on its ASCII string representation.
Parameters:
letter -- A single-character string literal, to be typed.
duration -- How long each key should be pressed, in seconds.
modifiers -- Any modifier keys that should be held while typing.
"""
shift, code = KeyboardKeys.get_scancode_for_ascii(letter)
modifiers = shift if modifiers is None else modifiers | shift
await self.type_scancode(code, modifiers=modifiers, duration=duration)
[docs]
async def type_letters(self, *letters: Iterable[str], duration:float = 0.1):
""" Attempts to type a string of letters, based on ASCII string representations.
Parameters:
*letters -- A collection of single-character string literal, to be typed in order.
duration -- How long each key should be pressed, in seconds.
"""
for letter in letters:
await self.type_letter(letter, duration=duration)
[docs]
async def type_string(self, to_type: str, *, duration:float = 0.1, modifiers: KeyboardModifiers = None):
""" Attempts to type a python string into the remote host.
Parameters:
letter -- A collection of single-character string literal, to be typed in order.
duration -- How long each key should be pressed, in seconds.
modifiers -- Any modifier keys that should be held while typing.
"""
self.modifier_down(modifiers)
for letter in to_type:
await self.type_letter(letter, duration=duration)
self.modifier_up(modifiers)
[docs]
def all_keys_up(self, *, include_modifiers: bool = True):
""" Releases all keys currently pressed.
Parameters:
include_modifiers -- If set to false, modifiers will be left at their current states.
"""
self.active_keys.clear()
if include_modifiers:
self.all_modifiers_up()
[docs]
def all_modifiers_up(self):
""" Releases all modifiers currently held. """
self.modifiers = 0
if __name__ == "__main__":
default_main(USBKeyboardDevice)