+ Finished authentication
* Cleaned up prose * Moved dependencies to their own file
This commit is contained in:
parent
a3852405e6
commit
b38c67a269
13
.vscode/tasks.json
vendored
Normal file
13
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the tasks.json format
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "wrap and tangle when changed",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "while true; do inotifywait --event modify security/*.md *.md; make tangle; done",
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
55
Dependencies.md
Normal file
55
Dependencies.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Project Dependencies
|
||||||
|
|
||||||
|
This project has the following dependencies, excluding the Go standard library:
|
||||||
|
|
||||||
|
|
||||||
|
# uuid
|
||||||
|
|
||||||
|
We use uuidv4s to generate authentication tokens
|
||||||
|
|
||||||
|
--- uuid import string
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# xdg
|
||||||
|
|
||||||
|
We use the xdg package to get the user's config directory.
|
||||||
|
|
||||||
|
--- xdg import string
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# sha3
|
||||||
|
|
||||||
|
We use sha3 to hash authentication tokens. It is not in the crypto standard library.
|
||||||
|
|
||||||
|
--- sha3 import string
|
||||||
|
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# keybd_event
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
--- keybd_event import string
|
||||||
|
|
||||||
|
"github.com/micmonay/keybd_event"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# gorilla/websocket
|
||||||
|
|
||||||
|
We also rely on gorilla/websocket for the websocket server that processes keyboard input.
|
||||||
|
|
||||||
|
--- gorilla/websocket import string
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
|
||||||
|
---
|
@ -1,5 +1,21 @@
|
|||||||
# GoSmartKeyboard Environment Variables
|
# GoSmartKeyboard Environment Variables
|
||||||
|
|
||||||
|
|
||||||
|
## Authentication token file
|
||||||
|
|
||||||
|
The authentication token configuration file is set by the environment variable `KEYBOARD_AUTH_TOKEN_FILE`, but defaults to
|
||||||
|
`XDG_CONFIG_HOME/smartkeyboard/auth-token`.
|
||||||
|
|
||||||
|
|
||||||
|
``` go
|
||||||
|
--- get authTokenFile from environment
|
||||||
|
|
||||||
|
authTokenFile, authTokenFileIsSet := os.LookupEnv("KEYBOARD_AUTH_TOKEN_FILE")
|
||||||
|
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## HTTP Bind Settings
|
## HTTP Bind Settings
|
||||||
|
|
||||||
|
|
||||||
@ -12,6 +28,7 @@ One should prefer a unix socket if their reverse proxy supports it and is on the
|
|||||||
same machine.
|
same machine.
|
||||||
|
|
||||||
--- unixSocketPath
|
--- unixSocketPath
|
||||||
|
|
||||||
unixSocketPath, unixSocketPathExists := os.LookupEnv("KEYBOARD_UNIX_SOCKET_PATH")
|
unixSocketPath, unixSocketPathExists := os.LookupEnv("KEYBOARD_UNIX_SOCKET_PATH")
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -20,9 +37,11 @@ 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:
|
The TCP socket is configured by the following environment variables:
|
||||||
|
|
||||||
--- TCPBindAddress
|
--- TCPBindAddress
|
||||||
|
|
||||||
tcpBindAddress, tcpBindAddressExists := os.LookupEnv("KEYBOARD_TCP_BIND_ADDRESS")
|
tcpBindAddress, tcpBindAddressExists := os.LookupEnv("KEYBOARD_TCP_BIND_ADDRESS")
|
||||||
---
|
---
|
||||||
|
|
||||||
--- TCPBindPort
|
--- TCPBindPort
|
||||||
|
|
||||||
tcpBindPort, tcpBindPortExists := os.LookupEnv("KEYBOARD_TCP_BIND_PORT")
|
tcpBindPort, tcpBindPortExists := os.LookupEnv("KEYBOARD_TCP_BIND_PORT")
|
||||||
---
|
---
|
||||||
|
4
Makefile
4
Makefile
@ -1,7 +1,7 @@
|
|||||||
weave:
|
weave:
|
||||||
srcweave --formatter srcweave-format --weave docs/ ReadMe.go.md security/Authentication.md
|
srcweave --formatter srcweave-format --weave docs/ ReadMe.go.md security/Authentication.md Dependencies.md
|
||||||
tangle:
|
tangle:
|
||||||
srcweave --formatter srcweave-format --tangle smartkeyboard/ ReadMe.go.md security/Authentication.md
|
srcweave --formatter srcweave-format --tangle smartkeyboard/ ReadMe.go.md security/Authentication.md EnvironmentVariables.md Dependencies.md
|
||||||
clean:
|
clean:
|
||||||
rm -rf docs
|
rm -rf docs
|
||||||
find smartkeyboard/ -type f -not -name "*_test.go" -delete
|
find smartkeyboard/ -type f -not -name "*_test.go" -delete
|
||||||
|
@ -46,54 +46,33 @@ A smart keyboard could, for example, be used for the following:
|
|||||||
First, we call
|
First, we call
|
||||||
|
|
||||||
--- entrypoint
|
--- entrypoint
|
||||||
func main(){
|
|
||||||
fmt.Println("Hello, World!")
|
func main(){
|
||||||
}
|
fmt.Println("Hello, World!")
|
||||||
|
}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
--- /main.go
|
--- /main.go
|
||||||
@{includes}
|
|
||||||
|
|
||||||
@{set network bind globals}
|
package main
|
||||||
|
|
||||||
@{entrypoint}
|
@{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
|
--- set network bind globals
|
||||||
var string unixSocketPath
|
|
||||||
var bool unixSocketPathExists
|
var string unixSocketPath
|
||||||
var string tcpBindAddress
|
var bool unixSocketPathExists
|
||||||
var bool tcpBindAddressExists
|
var string tcpBindAddress
|
||||||
var string tcpBindPort
|
var bool tcpBindAddressExists
|
||||||
var bool tcpBindPortExists
|
var string tcpBindPort
|
||||||
|
var bool tcpBindPortExists
|
||||||
|
|
||||||
---
|
---
|
@ -1,40 +1,44 @@
|
|||||||
# Keyboard connection authentication
|
# Keyboard connection authentication
|
||||||
|
|
||||||
Keyboarding obviously has a ton of sensitive data, so we need to both encrypt and
|
Keyboarding is a very sensitive activity, so this app naturally needs to encrypt and authenticate connections.
|
||||||
authenticate the connections.
|
|
||||||
|
|
||||||
All connections are encrypted using an external TLS proxy (e.g. Caddy) outside the
|
All connections are encrypted using an external TLS proxy (e.g. [Caddy](https://caddyserver.com)) outside the
|
||||||
scope of this project, but we perform application level authentication using two
|
scope of this project, but we perform application level authentication using two
|
||||||
randomly generated UUIDv4s in a manner similar to a passphrase. @{token generation}
|
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
|
We hash the token using sha3-256 to avoid accidentally exposing the token to a
|
||||||
readonly attacker.
|
readonly attacker. Since the token is very high entropy, we do not need a salt or
|
||||||
|
KDF.
|
||||||
|
|
||||||
## Generating the authentication token
|
|
||||||
|
|
||||||
``` go
|
``` go
|
||||||
--- token generation
|
--- token generation
|
||||||
id := uuid.New() + uuid.New()
|
authToken = uuid.New().String() + uuid.New().String()
|
||||||
hashedID := sha3.Sum256([]byte(id))
|
hashedID := sha3.Sum256([]byte(authToken))
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
## Storing the authentication token
|
|
||||||
|
|
||||||
We then store the token in a file, which is set
|
## Authentication token file
|
||||||
|
|
||||||
|
We store the token in a file, which is set
|
||||||
by the environment variable `KEYBOARD_AUTH_TOKEN_FILE`, but defaults to
|
by the environment variable `KEYBOARD_AUTH_TOKEN_FILE`, but defaults to
|
||||||
'XDG_CONFIG_HOME/smartkeyboard/auth-token.' @{defineProvisionTokenFile}
|
'XDG_CONFIG_HOME/smartkeyboard/auth-token.'
|
||||||
|
|
||||||
|
The following is used when we need to get the token file path:
|
||||||
|
|
||||||
``` go
|
``` go
|
||||||
--- store token
|
|
||||||
if err := os.WriteFile(KEYBOARD_AUTH_TOKEN_FILE, hashedID, 0666); err != nil {
|
--- define authentication token file
|
||||||
log.Fatal(err)
|
|
||||||
|
@{get authTokenFile from environment}
|
||||||
|
|
||||||
|
if authTokenFileIsSet == false {
|
||||||
|
authTokenFile = filepath.Join(xdg.ConfigHome, "smartkeyboard", "auth-token")
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Checking authentication
|
## Checking authentication
|
||||||
|
|
||||||
When a client connects, the [websocket server](Server.md) checks the token they send against the stored token.
|
When a client connects, the [websocket server](Server.md) checks the token they send against the stored token.
|
||||||
@ -42,57 +46,55 @@ When a client connects, the [websocket server](Server.md) checks the token they
|
|||||||
``` go
|
``` go
|
||||||
--- check token
|
--- check token
|
||||||
func checkAuthToken(token string) error {
|
func checkAuthToken(token string) error {
|
||||||
|
@{define authentication token file}
|
||||||
|
// compare sha3_256 hash to hash in file
|
||||||
hashedToken := sha3.Sum256([]byte(token))
|
hashedToken := sha3.Sum256([]byte(token))
|
||||||
storedToken, err := os.ReadFile(KEYBOARD_AUTH_TOKEN_FILE)
|
storedToken, err := os.ReadFile(authTokenFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare(hashedToken[:], storedToken) != 1 {
|
if subtle.ConstantTimeCompare(hashedToken[:], storedToken) != 1 {
|
||||||
return errors.New("invalid token")
|
return errors.New("invalid token")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
--- /auth/auth.go
|
--- /auth/auth.go
|
||||||
package main
|
package auth
|
||||||
|
|
||||||
|
import(
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"errors"
|
||||||
|
"crypto/subtle"
|
||||||
|
@{sha3 import string}
|
||||||
|
@{uuid import string}
|
||||||
|
@{xdg import string}
|
||||||
|
)
|
||||||
|
|
||||||
|
var authToken = ""
|
||||||
|
|
||||||
|
func provisionToken() (error){
|
||||||
|
@{define authentication token file}
|
||||||
|
|
||||||
|
if _, err := os.Stat(authTokenFile); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@{token generation}
|
||||||
|
|
||||||
|
fo, err := os.Create(authTokenFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fo.Write(hashedID[:])
|
||||||
|
fo.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
@{auth imports}
|
|
||||||
@{check token}
|
@{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}
|
|
||||||
}
|
|
||||||
---
|
|
||||||
```
|
```
|
@ -1,16 +1,58 @@
|
|||||||
package main
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAuthPasswordHashBad(t *testing.T) {
|
func TestAuthPasswordHashBad(t *testing.T) {
|
||||||
|
os.RemoveAll("test-auth-token")
|
||||||
|
os.Setenv("KEYBOARD_AUTH_TOKEN_FILE", "test-auth-token")
|
||||||
|
os.WriteFile("test-auth-token", []byte("junk"), 0644)
|
||||||
t.Log("TestAuthPasswordHash")
|
t.Log("TestAuthPasswordHash")
|
||||||
expectedResult := error(nil) // OK
|
|
||||||
password := "wrong password"
|
password := "wrong password"
|
||||||
|
|
||||||
result := checkAuthToken(password)
|
result := checkAuthToken(password)
|
||||||
if result != expectedResult {
|
if result == nil {
|
||||||
t.Errorf("Expected %s, got %s", expectedResult, result)
|
t.Errorf("Expected error, got nil")
|
||||||
}
|
}
|
||||||
|
os.RemoveAll("test-auth-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthPasswordEmpty(t *testing.T) {
|
||||||
|
os.RemoveAll("test-auth-token")
|
||||||
|
os.Setenv("KEYBOARD_AUTH_TOKEN_FILE", "test-auth-token")
|
||||||
|
os.WriteFile("test-auth-token", []byte("c0067d4af4e87f00dbac63b6156828237059172d1bbeac67427345d6a9fda484"), 0644)
|
||||||
|
t.Log("TestAuthPasswordHash")
|
||||||
|
|
||||||
|
password := ""
|
||||||
|
|
||||||
|
result := checkAuthToken(password)
|
||||||
|
if result == nil {
|
||||||
|
t.Errorf("Expected error, got nil")
|
||||||
|
}
|
||||||
|
os.RemoveAll("test-auth-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthPasswordHashGood(t *testing.T) {
|
||||||
|
os.RemoveAll("test-auth-token")
|
||||||
|
os.Setenv("KEYBOARD_AUTH_TOKEN_FILE", "test-auth-token")
|
||||||
|
//os.WriteFile("test-auth-token", []byte("c0067d4af4e87f00dbac63b6156828237059172d1bbeac67427345d6a9fda484"), 0644)
|
||||||
|
expectedHash := sha3.Sum256([]byte("password"))
|
||||||
|
fo, err := os.Create("test-auth-token")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fo.Write(expectedHash[:])
|
||||||
|
t.Log("TestAuthPasswordHash")
|
||||||
|
|
||||||
|
password := "password"
|
||||||
|
|
||||||
|
result := checkAuthToken(password)
|
||||||
|
if result != nil {
|
||||||
|
t.Errorf("Expected nil, got error")
|
||||||
|
}
|
||||||
|
os.RemoveAll("test-auth-token")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user