Skip to content

Security: MikeInNs/GhostInput

Security

docs/SECURITY.md

GhostInput Security

GhostInput uses a deliberately small security layer for devices on a trusted private network. It prevents unknown devices from using the Host and Client management APIs or starting streams without adding per-packet cryptography to the latency-sensitive HID and audio paths.

This is not intended to provide enterprise or internet-facing security. Keep GhostInput on a trusted LAN and do not expose its ports directly to the internet.

End-User Overview

What GhostInput Protects

Studio creates one installation identity that is shared by the GhostInput devices it provisions. Host and Client management actions must prove that they belong to that installation before the device accepts them.

In normal use this happens automatically:

  1. Studio provisions the first Host or Client and creates the installation identity.
  2. Studio stores the installation secret in Windows Credential Manager.
  3. Each provisioned Pi receives the same installation identity plus its own device ID.
  4. Studio and Clients authenticate before using protected control APIs.
  5. Successful authentication produces a temporary access token. Tokens are refreshed automatically after they expire or a service restarts.

There is no separate pairing screen, certificate approval, or password prompt for normal GhostInput connections.

The security layer protects actions such as:

  • reading device inventories and profiles
  • changing routes or device settings
  • starting and stopping Host streams
  • restarting or updating GhostInput services
  • using Bluetooth, network-test, and diagnostic management APIs

Basic discovery information remains public on the LAN so Studio can find Hosts and Clients before authenticating.

What It Does Not Protect

GhostInput control APIs use HTTP rather than encrypted HTTPS. The challenge-response procedure does not send the installation secret over the network, but subsequent access tokens and API content are not encrypted. A device capable of passively monitoring the trusted LAN could capture that traffic.

Realtime HID and audio use UDP and are not encrypted or cryptographically authenticated. Existing session IDs and sequence checks continue to reject stale or unrelated traffic, but a session ID is not a security secret.

All devices in one GhostInput installation share the same installation secret. A compromised Studio account or provisioned Pi should therefore be treated as a compromise of that GhostInput installation.

GhostInput does not currently provide:

  • HTTPS or certificate management
  • encrypted HID or audio streams
  • per-device permissions within one installation
  • automatic secret rotation or individual-device revocation
  • protection for manually installed devices that have no provisioned identity

These limitations are an intentional tradeoff for a small private-LAN gaming and simulator tool. Adding TLS, certificate recovery, or per-packet authentication would substantially increase setup and troubleshooting complexity without improving normal joystick, keyboard, mouse, or audio relaying.

Recommended Network Setup

  • Run GhostInput only on a private network you trust.
  • Do not forward Host, Client, control, HID, or audio ports from the internet.
  • Keep guest and untrusted IoT networks separate where practical.
  • Use a trusted VPN rather than port forwarding for remote access.
  • Protect the Windows account running Studio and the SSH accounts on each Pi.
  • Remove or change temporary provisioning passwords when they are no longer needed.

Reinstalling Studio or Reimaging the Windows PC

The installation identity is stored for the current Windows installation in Windows Credential Manager under GhostInput/Installation. It is not restored automatically on a newly imaged PC.

If at least one provisioned GhostInput Pi remains:

  1. Install Studio on the new PC.
  2. Open Provisioning and connect to that existing Pi with its SSH credentials.
  3. Before installation, Studio recovers the existing installation identity from the Pi over the authenticated SSH connection.
  4. Studio stores the recovered identity in Windows Credential Manager.
  5. Studio continues the normal update/reprovisioning workflow for that Pi.

Once recovered, Studio can authenticate to the other devices that use the same installation identity.

If neither Windows Credential Manager nor any provisioned Pi retains the old identity, it cannot be reconstructed. Studio creates a new identity during provisioning, and the GhostInput devices must be provisioned with that new identity before they can communicate as one installation.

Reprovisioning Devices

Reprovisioning a Pi from the same Studio installation reuses the installation identity. If the Pi already contains a valid identity for that installation, its existing device ID is preserved.

Provisioning a device from a Studio installation with a different identity moves that device into the new GhostInput installation. Update or reprovision the other devices as well if they should continue communicating with it.

Developer Design

Security Goals

The implemented layer is designed to:

  • reject unauthenticated control and stream-start requests
  • avoid transmitting the shared installation secret
  • prevent replay of authentication proofs
  • issue short-lived, role-scoped access tokens
  • recover automatically from expired tokens and process restarts
  • preserve the existing realtime UDP packet formats and performance
  • retain discovery and recovery paths during provisioning

It does not attempt to secure a hostile or monitored network.

Provisioned Identity

Each installation has:

  • installation_id: canonical UUID shared by Studio, Hosts, and Clients
  • installation_secret: 32 cryptographically random bytes, encoded as 43 unpadded base64url characters

Each Pi also has:

  • device_id: canonical UUID identifying that provisioned device

On Clients, this non-secret per-device UUID also supplies the stable local USB gadget identity. The Client formats it as GI plus 32 uppercase hexadecimal characters when creating the ConfigFS gadget. The shared installation_id, installation secret, hostname, profile client_id, and physical source-device serial are not used for this purpose. This keeps copied profiles from producing duplicate Windows USB serials on different Clients. A deliberate identity reset or OS reimage can generate a new device_id and therefore a new Windows USB device instance.

Studio uses its own caller device UUID, stored as:

%LOCALAPPDATA%\GhostInput\studio-device-id

This UUID is an identifier, not a secret.

Studio stores the installation ID and secret as a generic Windows credential:

Target: GhostInput/Installation
Username: <installation UUID>
Secret: <base64url installation secret>
Persistence: local machine

The Pi stores its identity at:

/etc/ghostinput/security/installation.json

The installed file is owned by root:ghostinput with mode 0640; its parent directory is mode 0750. Provisioning stages the identity in a mode 0700 temporary directory, installs it with elevated privileges, and removes the staged copy.

The document shape is:

{
  "installation_id": "00000000-0000-0000-0000-000000000000",
  "device_id": "00000000-0000-0000-0000-000000000000",
  "installation_secret": "<43-character base64url value>"
}

Do not print, log, commit, or include the real installation_secret in diagnostics.

Provisioning and Recovery Flow

When provisioning begins, Studio resolves an installation identity in this order:

  1. Load GhostInput/Installation from Windows Credential Manager.
  2. If it is absent, attempt to read the identity already installed on the target Pi over SSH with sudo.
  3. If the target has no identity, generate a new installation UUID and 256-bit secret.
  4. Save the resolved installation identity in Windows Credential Manager.
  5. Create or preserve the target Pi's device UUID and install the device identity document.

Recovery trusts the authenticated SSH connection and the user's sudo credentials. SSH host-key verification and credential hygiene remain part of the provisioning trust boundary.

Challenge-Response Authentication

Host and Client expose:

POST /security/challenge
POST /security/authenticate

The challenge endpoint returns a random 128-bit challenge ID and a random 256-bit nonce. A challenge is valid for 30 seconds by default.

The caller computes:

HMAC-SHA-256(
    installation_secret,
    "ghostinput-control-v1\n" +
    challenge_id + "\n" +
    nonce + "\n" +
    installation_id + "\n" +
    caller_device_id + "\n" +
    caller_role
)

The proof is encoded with unpadded base64url. Supported caller roles are studio, host, and client.

The service compares the proof in constant time. Every challenge is consumed by the first authentication attempt, including an invalid attempt, so the same proof cannot be replayed.

Successful authentication returns a random 256-bit bearer token. Tokens:

  • are registered only in the Host or Client service's process memory
  • record the authenticated role and caller device ID
  • expire after one hour by default
  • disappear immediately when the Host or Client service restarts
  • are limited to 128 active entries per service by default

The service also limits pending challenges to 64 entries.

Authorization Policy

Public endpoints:

  • Host and Client /summary
  • Host /time
  • /security/challenge
  • /security/authenticate

Protected Host endpoints:

  • /streams and /streams/stop: client or studio
  • all other management endpoints: studio

Protected Client endpoints:

  • all non-public management endpoints: studio

Requests without a bearer token receive 401 Unauthorized with authentication_required. Invalid, expired, or wrong-role tokens receive 401 Unauthorized with invalid_access_token.

If a Host or Client has no valid installed identity, its authentication service is not created and it remains in legacy compatibility mode. This keeps manual or older deployments reachable during migration, but it also means those devices do not receive API authentication until they are provisioned or updated with an identity.

Token Handling and Restart Recovery

Studio caches tokens in process memory per API authority, installation ID, and Studio device ID. The Client caches Host tokens in process memory per Host endpoint. Both renew before token expiry.

When a protected request returns 401 after a service restart:

  1. discard the cached token
  2. perform a new challenge-response exchange
  3. retry the original request once

This avoids persistent failures while also preventing unbounded retry loops. The Client applies the same process before retrying a Host stream request.

Realtime Streams

Authentication applies only to the HTTP control request that creates or manages a stream. The negotiated HID and audio streams then use their assigned session IDs, while liveness and active-state messages use the dedicated compact control channel. The wire formats are documented in GhostInput Protocol.

No authentication field, MAC, nonce, certificate, or encryption overhead is added to:

  • HID UDP headers or payloads
  • downstream HID output reports
  • speaker/headphone audio packets
  • microphone audio packets
  • realtime control-channel packets

This is intentional. Authentication does not add overhead to the realtime data path, avoiding additional packet size or cryptographic work on small Raspberry Pi devices.

Main Implementation Files

Area Files
Studio identity storage admin/GhostInput.Studio.Core/Security/
Provisioning and SSH recovery admin/GhostInput.Studio.Core/Provisioning/ProvisioningIdentityCoordinator.cs, SshPiProvisioningService.cs, and ProvisioningSecurityInstaller.cs
Native identity loading shared/GhostInput.Common/src/InstallationSecurityIdentity.cpp
HMAC, challenges, and tokens shared/GhostInput.Common/src/ControlAuthentication.cpp
Authentication HTTP endpoints and bearer parsing shared/GhostInput.Common/src/ControlAuthenticationApi.cpp
Native authentication caller shared/GhostInput.Common/src/ControlAuthenticationClient.cpp
Studio authentication caller admin/GhostInput.Studio.Core/Api/StudioControlAuthenticationHandler.cs
Host authorization host/GhostInput.Host/src/Api/ControlApiCommand.cpp
Client authorization client/GhostInput.Client/src/Api/ClientControlServer.cpp
Client-to-Host stream authentication client/GhostInput.Client/src/Transport/HostStreamClient.cpp

Verification

Public endpoints should remain reachable:

curl -i http://HOST:47172/summary
curl -i http://HOST:47172/time
curl -i http://CLIENT:47173/summary

Protected endpoints should reject anonymous requests:

curl -i http://HOST:47172/devices
curl -i http://CLIENT:47173/profile

Expected result:

HTTP/1.1 401 Unauthorized

Normal Studio operations and Client stream requests should authenticate automatically. After restarting a Host, the Client log should show a fresh authentication followed by a successful stream request:

sudo journalctl -u ghostinput-client.service -b --no-pager -l |
  grep -E 'Host control authentication|Requested Host stream|401|Unauthorized'

Automated coverage is provided by:

  • GhostInput.Common.Tests
  • GhostInput.Host.Tests
  • GhostInput.Client.Tests
  • GhostInput.Studio.Core.Tests

There aren't any published security advisories