Skip to main content

blekeyboard (Linux)

Linux implementation of blekeyboard, using BlueZ's HCI user channel to obtain raw access to a local Bluetooth controller.

See ../windows for the Windows implementation. The HCI packet construction layer (emulator.py) is common to both platforms; only the transport layer (hijack.py) differs.

Project status

Alpha, and currently the only functional implementation - the Windows package is an early transport-layer prototype and cannot yet pair or type. The full stack is implemented and verified against real hardware: the controller is claimed, reset and configured, advertises as a discoverable HID keyboard, is paired through the host operating system's own Bluetooth settings, enrols as a keyboard via HID over GATT, and delivers key reports over the encrypted link. This has been confirmed end to end on iOS, including actual on-screen typing, not just a successfully encrypted link.

Pairing implements both LE Legacy and LE Secure Connections, both restricted to the Just Works association model - the only one available to a device with no display and no keypad. Whichever a peer offers is used automatically; SC is preferred when available and is what several hosts, iOS included, require before treating the peripheral as a genuinely trusted input device rather than merely an encrypted one. The ECDH key agreement SC needs runs in pure Python rather than through the controller's own P-256/DHKey commands, since at least one common controller wedges its command queue partway through that exchange; AES still runs through the controller's LE Encrypt command. Neither adds a runtime dependency. A formed bond is persisted to ~/.local/state/blekeyboard/bonds.json (owner-readable only), so a host reconnecting after this process restarts resumes the encrypted session without pairing again.

A host will always prompt to confirm pairing with a new device before accepting input from it; this is a platform-level gate BLE HID has no way around. See the project roadmap and the BLE injection limitation noted there.

How it works

Windows requires replacing the vendor driver with WinUSB to reach the controller. Linux exposes equivalent access natively through BlueZ's HCI user channel: binding an AF_BLUETOOTH / BTPROTO_HCI socket with HCI_CHANNEL_USER grants exclusive raw HCI command and event access, detaching the adapter from bluetoothd and the kernel Bluetooth stack for as long as the socket remains open.

As a result, the Linux implementation has no external dependencies and communicates with the adapter using only the Python standard library (socket for the transport, secrets for cryptographic randomness).

Requirements

  • Python 3.10 or later
  • A Bluetooth Low Energy 4.2 or later controller recognised by BlueZ, listed as hci0, hci1, and so on by bluetoothctl list or btmgmt info
  • CAP_NET_ADMIN, granted either by running as root or by assigning the capability to the interpreter

Installation

pip install blekeyboard

For a local checkout instead:

pip install -e .

Usage

Preparing the adapter

The HCI user channel requires the target adapter to be down before it can be claimed:

sudo btmgmt --index 0 power off

This suspends normal Bluetooth functionality on that adapter, including connected peripherals such as mice and headsets, until it is restored.

Note Older documentation refers to sudo hciconfig hci0 down. The hciconfig and hcitool utilities are deprecated and no longer shipped with current BlueZ releases; use btmgmt or bluetoothctl power off instead.

Granting permissions

Run as root:

sudo python3 -m blekeyboard

Alternatively, grant the capability to the interpreter to avoid running the process fully privileged. Do this against a virtual environment's own interpreter, not a shared system Python: setcap targets a specific binary, and a venv created with --copies (not the default symlink) gets its own physical copy, so the grant does not extend to every other script that Python ever runs.

python3 -m venv --copies ~/.venvs/blekeyboard
~/.venvs/blekeyboard/bin/pip install blekeyboard
sudo setcap cap_net_admin+eip ~/.venvs/blekeyboard/bin/python3
~/.venvs/blekeyboard/bin/python3 -m blekeyboard

Command line

sudo python -m blekeyboard

Advertises as a keyboard and waits for a host to pair and subscribe, then drops into an interactive prompt: Enter types a demonstration string, t <text> types anything else, run <path> runs a Ducky Script file, who prints a best-effort guess at the connected host's OS, l sends the iOS input-language-switch shortcut, and r releases every key (useful if a host still believes one is held after a dropped notification). Holds the adapter until interrupted with Ctrl+C.

Scripting API

from blekeyboard import Keyboard

keyboard = Keyboard()
keyboard.connect()  # blocks until a host has paired and subscribed

keyboard.press(Keyboard.KEY_GUI, "r")  # open Run on Windows
keyboard.release_all()

keyboard.print("notepad\n")

connect() brings the adapter up, advertises, and blocks until a host has paired and subscribed to notifications; pass timeout= to give up after a limited wait instead of blocking indefinitely. press()/release() accept single characters or raw keycodes such as the Keyboard.KEY_* modifier constants, and accumulate into a held combination across calls. write() types one character; print() (aliased as type()) types a string.

API reference

Method Description
Keyboard.connect(timeout=None) Brings up the adapter, advertises, and waits for a host to pair and subscribe. Returns whether that happened.
Keyboard.is_connected() Whether a host is currently paired and subscribed.
Keyboard.disconnect() Stops advertising and releases the adapter.
Keyboard.press(*keys) Adds each key to the held combination and sends the resulting report.
Keyboard.release(*keys) Removes each key from the held combination and sends the resulting report.
Keyboard.release_all() Releases every held key.
Keyboard.write(char) Presses and releases a single character, preserving any keys held via press().
Keyboard.print(text) / Keyboard.type(text) Types a string one character at a time.
Keyboard.tap(*keys) Presses a combination, then releases it the way physical hardware sequences a keystroke - ordinary keys before modifiers - so a dropped notification can't strand a modifier on the host.
Keyboard.switch_input_language() Sends Ctrl+Space, the iOS shortcut to cycle the hardware-keyboard input language. A HID keyboard can only send key positions, never choose the host's layout - iOS ignores the HID country code entirely - so this is the only lever available when the host's active layout doesn't match the payload.
Keyboard.host_guess A best-effort HostGuess for the connected peer - see Host detection below. None before any connection.

Ducky Script

A second, deliberately restricted input syntax alongside the scripting API above, styled after the USB Rubber Ducky's payload format:

from blekeyboard import Keyboard, run_duckyscript

keyboard = Keyboard()
keyboard.connect()

run_duckyscript(keyboard, """
    REM opens a run dialog and types a command
    STRINGLN notepad.exe
    DELAY 500
    STRING done
""")

Supported, one command per line: STRING/STRINGLN <text>, DELAY <milliseconds>, REM <comment>, and single named keys on their own line (ENTER, TAB, ESCAPE, the arrow keys, F1-F12, and similar). Key combinations (GUI r, CTRL ALT DEL) and a key held across lines (HOLD/RELEASE) are not implemented yet; a line naming one raises DuckyScriptError rather than being silently misinterpreted. run_duckyscript_file(keyboard, path) reads and runs a script from disk - this is what the CLI's run <path> command uses.

Host detection

Keyboard.host_guess returns a HostGuess - a HostOS (IOS, ANDROID, WINDOWS, MACOS, LINUX, or UNKNOWN), a confidence ("none", "low", or "medium", deliberately never higher), and reasons explaining the guess. This is necessarily a hint, not a fact: BLE has no field where a central announces its operating system, so this is inferred from a handful of observable signals during pairing (the peer's address type and its Pairing Request's auth_req/CT2 pattern) cross-referenced against per-platform tendencies this project has actually observed. Windows, Linux and macOS are not yet distinguishable from each other and are reported as UNKNOWN rather than guessed at random.

Low-level API

The scripting API is built on HCITransport, BLEBroadcaster, and Link, which remain available directly for anything the high-level API does not cover:

import time

from blekeyboard.emulator import BLEBroadcaster
from blekeyboard.hijack import HCITransport

transport = HCITransport(dev_id=0)
broadcaster = BLEBroadcaster(transport)

try:
    # Claim exclusive raw HCI access to the adapter.
    transport.connect()

    # A newly claimed controller is uninitialised and must be reset first.
    broadcaster.reset_controller()

    broadcaster.configure_advertising(interval_ms=400)
    broadcaster.set_advertising_payload("BLE-Ducky")
    broadcaster.set_state(enable=True)

    # Periodic informational queries keep the controller from idling.
    while True:
        time.sleep(10)
        broadcaster.send_keepalive_ping()

finally:
    broadcaster.set_state(enable=False)
    transport.release()

Low-level API reference

Method Description
HCITransport.connect() Claims the adapter's HCI user channel.
HCITransport.send_control_packet(packet) Writes an HCI command packet to the controller.
HCITransport.send_acl_payload(handle, payload) Writes a payload to a connection, fragmenting it to the controller's ACL capacity.
HCITransport.read_packet(timeout_ms) Reads one HCI packet of any type, returning an empty list on timeout.
HCITransport.configure_acl_buffers(payload_length, total_packets) Adopts the ACL capacity reported by the controller.
HCITransport.credit_acl_packets(count) Returns buffer slots released by a Number Of Completed Packets event.
HCITransport.release() Closes the socket and returns the adapter to the kernel.
BLEBroadcaster.reset_controller() Issues HCI Reset. Required after claiming the adapter.
BLEBroadcaster.set_event_mask() Enables LE Meta event delivery, without which connection events are withheld.
BLEBroadcaster.set_le_event_mask() Selects the LE subevents the controller reports.
BLEBroadcaster.read_le_buffer_size() Queries the controller's ACL payload size and buffer count.
BLEBroadcaster.read_bd_addr() Reads the controller's own public address, which pairing mixes into the confirm value.
BLEBroadcaster.le_encrypt(key, plaintext) Runs one AES-128 block through the controller's engine.
BLEBroadcaster.le_rand() Requests eight random octets from the controller.
BLEBroadcaster.le_long_term_key_request_reply(handle, key) Supplies the key that encrypts a link.
BLEBroadcaster.le_long_term_key_request_negative_reply(handle) Declines to supply a key, aborting encryption.
BLEBroadcaster.configure_advertising(interval_ms) Sets advertising parameters. Accepts 20 ms to 10240 ms.
BLEBroadcaster.set_advertising_payload(name, service_uuids=None) Sets advertising data: flags, the complete local name, and optionally a list of 16-bit service UUIDs.
BLEBroadcaster.set_state(enable) Enables or disables advertising.
BLEBroadcaster.send_keepalive_ping() Reads local version information as a controller liveness check.

Restoring normal Bluetooth operation

Closing the transport releases the user channel automatically. To return the adapter to BlueZ:

sudo btmgmt --index 0 power on

Normal desktop Bluetooth functionality resumes immediately; no reboot is required.

Testing

pip install -e ".[dev]"
pytest

Disclaimer

This project is intended for authorized security testing and research. Use it only against systems you own or have explicit written permission to test. Behaviour depends heavily on controller and driver support and may vary across hardware and operating system configurations.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

blekeyboard-0.1.2.tar.gz (91.2 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

blekeyboard-0.1.2-py3-none-win_amd64.whl (9.4 kB view details)

Uploaded Python 3Windows x86-64

blekeyboard-0.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (60.8 kB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

File details

Details for the file blekeyboard-0.1.2.tar.gz.

File metadata

  • Download URL: blekeyboard-0.1.2.tar.gz
  • Upload date:
  • Size: 91.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for blekeyboard-0.1.2.tar.gz
Algorithm Hash digest
SHA256 39aaf7792bab722877db24c70024c9d7b95c1c4466e9ab3c1b140de64b18d942
MD5 b99d611ff6c05414498e695616f84bd4
BLAKE2b-256 8ac3711b0713530e87399d78d3774265e4b8f27fd995c22c4bbf8f8467c3ed6b

See more details on using hashes here.

File details

Details for the file blekeyboard-0.1.2-py3-none-win_amd64.whl.

File metadata

  • Download URL: blekeyboard-0.1.2-py3-none-win_amd64.whl
  • Upload date:
  • Size: 9.4 kB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for blekeyboard-0.1.2-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 19e19b7d7492c5575ed27e33f26ddfcb72f64d415816b622d3810ae88e371923
MD5 45c41be898ef9c0cf01dea1a228046fe
BLAKE2b-256 0bb035e47e31499c743dcbd94c5bf03a1e16bbaeea79603dd9835120496f77ef

See more details on using hashes here.

File details

Details for the file blekeyboard-0.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.

File metadata

File hashes

Hashes for blekeyboard-0.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 f63094f492d7e8836f884573ae2bcc30b335a186c7364c51fa2c5754420f85a5
MD5 55f4b443b518fdc88c99c7019b4d33a8
BLAKE2b-256 d4809adb4747c8003b44fdb7b3afb709aafebaa90f71a743fbc15c4623ab5e7d

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.2 This release

3 files

0.1.1

2 files

0.1.0

2 files

0.0.1

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page