Compare commits
5 Commits
2789545bd7
...
a3852405e6
Author | SHA1 | Date | |
---|---|---|---|
|
a3852405e6 | ||
|
ca5c748478 | ||
|
f2b572946a | ||
|
8e5f0ef73f | ||
|
cb27d192c2 |
28
EnvironmentVariables.md
Normal file
28
EnvironmentVariables.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# GoSmartKeyboard Environment Variables
|
||||||
|
|
||||||
|
## HTTP Bind Settings
|
||||||
|
|
||||||
|
|
||||||
|
GoSmartKeyboard supports both standard TCP sockets and unix sockets for the
|
||||||
|
HTTP server.
|
||||||
|
|
||||||
|
First, we check for a unix socket path.
|
||||||
|
|
||||||
|
One should prefer a unix socket if their reverse proxy supports it and is on the
|
||||||
|
same machine.
|
||||||
|
|
||||||
|
--- unixSocketPath
|
||||||
|
unixSocketPath, unixSocketPathExists := os.LookupEnv("KEYBOARD_UNIX_SOCKET_PATH")
|
||||||
|
---
|
||||||
|
|
||||||
|
If the unix socket path is set, we use it. Otherwise, we use the TCP socket.
|
||||||
|
|
||||||
|
The TCP socket is configured by the following environment variables:
|
||||||
|
|
||||||
|
--- TCPBindAddress
|
||||||
|
tcpBindAddress, tcpBindAddressExists := os.LookupEnv("KEYBOARD_TCP_BIND_ADDRESS")
|
||||||
|
---
|
||||||
|
|
||||||
|
--- TCPBindPort
|
||||||
|
tcpBindPort, tcpBindPortExists := os.LookupEnv("KEYBOARD_TCP_BIND_PORT")
|
||||||
|
---
|
99
ReadMe.go.md
Normal file
99
ReadMe.go.md
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# GoSmartKeyboard Daemon
|
||||||
|
|
||||||
|
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
This is a simple websocket server meant to accept a single connection, authenticate it, and stream UTF16 characters and send them as key strokes into the window manager.
|
||||||
|
|
||||||
|
The goal of this particular daemon is not to perfectly emulate a HID, so it may trip up on Windows UAC or game anticheat systems.
|
||||||
|
|
||||||
|
|
||||||
|
Some points about the design of this project:
|
||||||
|
|
||||||
|
* Written in [literate go](https://github.com/justinmeiners/srcweave), so this
|
||||||
|
markdown book is actually the source code
|
||||||
|
* The project is test-driven
|
||||||
|
* KISS principle above All
|
||||||
|
* Small and light core
|
||||||
|
* Advanced features provided via plugins
|
||||||
|
* Well defined [threat model](ThreatModel.md)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Why a smart keyboard?
|
||||||
|
|
||||||
|
Keyboards have been an essential element of computing since the beginning, however they have not evolved much. Everything has a smart variant, so why not keyboards?
|
||||||
|
|
||||||
|
A smart keyboard could, for example, be used for the following:
|
||||||
|
|
||||||
|
* Typical macros
|
||||||
|
* Buffer typed text before sending it to the client, preventing invalid commands or input. (This would also save some CPU on low power machines, this is how many early teletype systems worked)
|
||||||
|
* Clever CLI tricks, think `vim` or `cowsay` on your keyboard!
|
||||||
|
* Isolated password manager
|
||||||
|
* One Time Passwords
|
||||||
|
* Virtual keyboard switch or communicating with multiple daemons at once
|
||||||
|
* Easily attach to VMs
|
||||||
|
* Text storage, such as configuration or SSH pubkeys
|
||||||
|
* On-the-fly spell checking or translation
|
||||||
|
* On-the-fly encryption (ex: PGP sign every message you type), isolated from the perhaps untrusted computer
|
||||||
|
* Easy layout configuration
|
||||||
|
* Delay keystrokes by a few dozen or so milliseconds to reduce [key stroke timing biometrics](https://en.wikipedia.org/wiki/Keystroke_dynamics)
|
||||||
|
|
||||||
|
|
||||||
|
# Daemon Entrypoint
|
||||||
|
|
||||||
|
|
||||||
|
First, we call
|
||||||
|
|
||||||
|
--- entrypoint
|
||||||
|
func main(){
|
||||||
|
fmt.Println("Hello, World!")
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
--- /main.go
|
||||||
|
@{includes}
|
||||||
|
|
||||||
|
@{set network bind globals}
|
||||||
|
|
||||||
|
@{entrypoint}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
In order to avoid coding key press simulation for every major platform, we use [keybd_event](https://github.com/micmonay/keybd_event). This is a cross-platform library that uses the OS's native key press simulation.
|
||||||
|
|
||||||
|
|
||||||
|
--- include keybd event
|
||||||
|
import(
|
||||||
|
"github.com/micmonay/keybd_event"
|
||||||
|
)
|
||||||
|
---
|
||||||
|
|
||||||
|
We also rely on gorilla/websocket for the websocket server:
|
||||||
|
|
||||||
|
--- include gorilla websocket
|
||||||
|
import(
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
---
|
||||||
|
|
||||||
|
--- includes
|
||||||
|
package main
|
||||||
|
import(
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
--- set network bind globals
|
||||||
|
var string unixSocketPath
|
||||||
|
var bool unixSocketPathExists
|
||||||
|
var string tcpBindAddress
|
||||||
|
var bool tcpBindAddressExists
|
||||||
|
var string tcpBindPort
|
||||||
|
var bool tcpBindPortExists
|
||||||
|
---
|
98
security/Authentication.md
Normal file
98
security/Authentication.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# Keyboard connection authentication
|
||||||
|
|
||||||
|
Keyboarding obviously has a ton of sensitive data, so we need to both encrypt and
|
||||||
|
authenticate the connections.
|
||||||
|
|
||||||
|
All connections are encrypted using an external TLS proxy (e.g. Caddy) outside the
|
||||||
|
scope of this project, but we perform application level authentication using two
|
||||||
|
randomly generated UUIDv4s in a manner similar to a passphrase. @{token generation}
|
||||||
|
|
||||||
|
We hash the token using sha3-256 to avoid accidentally exposing the token to a
|
||||||
|
readonly attacker.
|
||||||
|
|
||||||
|
|
||||||
|
## Generating the authentication token
|
||||||
|
|
||||||
|
``` go
|
||||||
|
--- token generation
|
||||||
|
id := uuid.New() + uuid.New()
|
||||||
|
hashedID := sha3.Sum256([]byte(id))
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storing the authentication token
|
||||||
|
|
||||||
|
We then store the token in a file, which is set
|
||||||
|
by the environment variable `KEYBOARD_AUTH_TOKEN_FILE`, but defaults to
|
||||||
|
'XDG_CONFIG_HOME/smartkeyboard/auth-token.' @{defineProvisionTokenFile}
|
||||||
|
|
||||||
|
|
||||||
|
``` go
|
||||||
|
--- store token
|
||||||
|
if err := os.WriteFile(KEYBOARD_AUTH_TOKEN_FILE, hashedID, 0666); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checking authentication
|
||||||
|
|
||||||
|
When a client connects, the [websocket server](Server.md) checks the token they send against the stored token.
|
||||||
|
|
||||||
|
``` go
|
||||||
|
--- check token
|
||||||
|
func checkAuthToken(token string) error {
|
||||||
|
hashedToken := sha3.Sum256([]byte(token))
|
||||||
|
storedToken, err := os.ReadFile(KEYBOARD_AUTH_TOKEN_FILE)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare(hashedToken[:], storedToken) != 1 {
|
||||||
|
return errors.New("invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
--- /auth/auth.go
|
||||||
|
package main
|
||||||
|
|
||||||
|
@{auth imports}
|
||||||
|
@{check token}
|
||||||
|
---
|
||||||
|
|
||||||
|
--- auth imports
|
||||||
|
|
||||||
|
import(
|
||||||
|
//"github.com/google/uuid"
|
||||||
|
//"encoding/base64"
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
"os"
|
||||||
|
"errors"
|
||||||
|
"crypto/subtle"
|
||||||
|
)
|
||||||
|
---
|
||||||
|
|
||||||
|
``` go
|
||||||
|
--- defineProvisionTokenFile
|
||||||
|
provisionTokenFile, keyboardAuthExists := os.Getenv("KEYBOARD_AUTH_TOKEN_FILE")
|
||||||
|
|
||||||
|
if keyboardAuthExists == false {
|
||||||
|
provisionTokenFile = filepath.Join(xdg.ConfigHome, "smartkeyboard", "auth-token")
|
||||||
|
}
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
--- provisionToken
|
||||||
|
func provisionToken() (error){
|
||||||
|
@{defineProvisionTokenFile}
|
||||||
|
@{token generation}
|
||||||
|
@{store token}
|
||||||
|
}
|
||||||
|
---
|
||||||
|
```
|
0
security/ThreatModel.md
Normal file
0
security/ThreatModel.md
Normal file
16
smartkeyboard/auth/auth_test.go
Normal file
16
smartkeyboard/auth/auth_test.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuthPasswordHashBad(t *testing.T) {
|
||||||
|
t.Log("TestAuthPasswordHash")
|
||||||
|
expectedResult := error(nil) // OK
|
||||||
|
password := "wrong password"
|
||||||
|
|
||||||
|
result := checkAuthToken(password)
|
||||||
|
if result != expectedResult {
|
||||||
|
t.Errorf("Expected %s, got %s", expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
13
util/removefencedcode.py
Executable file
13
util/removefencedcode.py
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Remove fenced code blocks from generated HTML files
|
||||||
|
# We need to do this because srcweave does not support fenced code blocks
|
||||||
|
|
||||||
|
import glob
|
||||||
|
for f in glob.glob('docs/*.html'):
|
||||||
|
with open(f, 'r') as fin:
|
||||||
|
lines = fin.readlines()
|
||||||
|
with open(f, 'w') as fout:
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith('<p>```'):
|
||||||
|
continue
|
||||||
|
fout.write(line)
|
Loading…
Reference in New Issue
Block a user