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.
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:
- Studio provisions the first Host or Client and creates the installation identity.
- Studio stores the installation secret in Windows Credential Manager.
- Each provisioned Pi receives the same installation identity plus its own device ID.
- Studio and Clients authenticate before using protected control APIs.
- 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.
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.
- 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.
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:
- Install Studio on the new PC.
- Open Provisioning and connect to that existing Pi with its SSH credentials.
- Before installation, Studio recovers the existing installation identity from the Pi over the authenticated SSH connection.
- Studio stores the recovered identity in Windows Credential Manager.
- 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 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.
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.
Each installation has:
installation_id: canonical UUID shared by Studio, Hosts, and Clientsinstallation_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.
When provisioning begins, Studio resolves an installation identity in this order:
- Load
GhostInput/Installationfrom Windows Credential Manager. - If it is absent, attempt to read the identity already installed on the
target Pi over SSH with
sudo. - If the target has no identity, generate a new installation UUID and 256-bit secret.
- Save the resolved installation identity in Windows Credential Manager.
- 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.
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.
Public endpoints:
- Host and Client
/summary - Host
/time /security/challenge/security/authenticate
Protected Host endpoints:
/streamsand/streams/stop:clientorstudio- 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.
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:
- discard the cached token
- perform a new challenge-response exchange
- 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.
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.
| 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 |
Public endpoints should remain reachable:
curl -i http://HOST:47172/summary
curl -i http://HOST:47172/time
curl -i http://CLIENT:47173/summaryProtected endpoints should reject anonymous requests:
curl -i http://HOST:47172/devices
curl -i http://CLIENT:47173/profileExpected 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.TestsGhostInput.Host.TestsGhostInput.Client.TestsGhostInput.Studio.Core.Tests