Encrypt restore token

This commit is contained in:
Unrud 2025-03-18 09:18:15 +01:00
parent abd9fd4773
commit 16dc47b570
11 changed files with 137 additions and 49 deletions

View file

@ -29,6 +29,11 @@ modules:
tag: v0.37.0
commit: 99b3ae0643f9a2f9d820fcbba5f9e4c83b23bd48
dest: vendor/golang.org/x/net
- type: git
url: https://github.com/golang/crypto
tag: v0.36.0
commit: 49bf5b80c8108983f588ecabd7bf996e6e63a515
dest: vendor/golang.org/x/crypto
- type: file
path: modules.txt
dest: vendor

View file

@ -9,6 +9,9 @@ github.com/godbus/dbus/v5
github.com/skip2/go-qrcode
github.com/skip2/go-qrcode/bitset
github.com/skip2/go-qrcode/reedsolomon
# golang.org/x/crypto v0.36.0
## explicit; go 1.23.0
golang.org/x/crypto/hkdf
# golang.org/x/net v0.37.0
## explicit; go 1.23.0
golang.org/x/net/websocket

1
go.mod
View file

@ -8,5 +8,6 @@ require (
github.com/bendahl/uinput v1.7.0
github.com/godbus/dbus/v5 v5.1.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
golang.org/x/crypto v0.36.0
golang.org/x/net v0.37.0
)

2
go.sum
View file

@ -4,5 +4,7 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=

View file

@ -55,14 +55,14 @@ const (
type ControllerInfo struct {
Name string
Init func(bool) (Controller, error)
Init func() (Controller, error)
priority int
}
var Controllers []ControllerInfo
func RegisterController(name string, init func(bool) (Controller, error), priority int) {
func RegisterController(name string, init func() (Controller, error), priority int) {
Controllers = append(Controllers, ControllerInfo{name, init, priority})
sort.SliceStable(Controllers, func(i, j int) bool {
return Controllers[i].priority < Controllers[j].priority

View file

@ -31,7 +31,7 @@ func init() {
RegisterController("null", InitNullController, 1000)
}
func InitNullController(saveRestoreToken bool) (Controller, error) {
func InitNullController() (Controller, error) {
return &nullController{}, nil
}

View file

@ -22,12 +22,20 @@
package inputcontrol
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"github.com/godbus/dbus/v5"
"io"
"log"
"os"
"path/filepath"
"slices"
"github.com/godbus/dbus/v5"
"golang.org/x/crypto/hkdf"
)
const (
@ -55,7 +63,7 @@ func init() {
RegisterController("RemoteDesktop portal", InitPortalController, 1)
}
func InitPortalController(saveRestoreToken bool) (Controller, error) {
func InitPortalController() (Controller, error) {
bus, err := dbus.SessionBusPrivate()
if err != nil {
return nil, &UnsupportedPlatformError{err}
@ -81,25 +89,26 @@ func InitPortalController(saveRestoreToken bool) (Controller, error) {
if err != nil {
return nil, &UnsupportedPlatformError{err}
}
supportsRestoreTokens := version.Value().(uint32) >= 2
var restoreTokenFilePath string
var cacheDirectory string
var restoreToken string
if supportsRestoreTokens {
cacheDirectory, err = os.UserCacheDir()
if err != nil {
log.Printf("Cannot get user cache directory: %s. Therefore cannot get restore token file path in order to read or save the restore token.\n", err)
} else {
restoreTokenFilePath = filepath.Join(cacheDirectory, "remote_touchpad_portals_restore_token")
restoreTokenBytes, err := os.ReadFile(restoreTokenFilePath)
if err != nil {
log.Printf("Failed to read restore token file: %s\n", err)
} else {
restoreToken = string(restoreTokenBytes)
}
restoreTokenStore, err := func() (*secretStore, error) {
if version.Value().(uint32) < 2 {
return nil, nil
}
} else {
log.Println("Portals implementation does not support restore tokens")
cacheDirectory, err := os.UserCacheDir()
if err != nil {
return nil, err
}
if err := os.MkdirAll(cacheDirectory, 0700); err != nil {
return nil, err
}
secret, err := retrieveSecret(bus)
if err != nil {
return nil, err
}
return newSecretStore(secret,
filepath.Join(cacheDirectory, "remote-touchpad.portal-restore-token.bin"))
}()
if err != nil {
log.Printf("Skipping restore token: %v", err)
}
availableDeviceTypesV, err := remoteDesktop.GetProperty(
"org.freedesktop.portal.RemoteDesktop.AvailableDeviceTypes")
@ -148,13 +157,15 @@ func InitPortalController(saveRestoreToken bool) (Controller, error) {
sessionHandle := dbus.ObjectPath(sessionHandleS)
inVardict = make(map[string]dbus.Variant)
inVardict["types"] = dbus.MakeVariant(deviceKeyboard | devicePointer)
if supportsRestoreTokens {
if restoreToken != "" {
inVardict["restore_token"] = dbus.MakeVariant(restoreToken)
}
if saveRestoreToken {
inVardict["persist_mode"] = dbus.MakeVariant(untilRevoked)
if restoreTokenStore != nil {
if restoreToken, err := restoreTokenStore.Load(); err != nil {
if !errors.Is(err, os.ErrNotExist) {
log.Printf("Failed to load restore token: %v", err)
}
} else if len(restoreToken) > 0 {
inVardict["restore_token"] = dbus.MakeVariant(string(restoreToken))
}
inVardict["persist_mode"] = dbus.MakeVariant(untilRevoked)
}
result, outVardict, err = getResponse(bus, remoteDesktop,
"org.freedesktop.portal.RemoteDesktop.SelectDevices", 0, sessionHandle, inVardict)
@ -174,20 +185,9 @@ func InitPortalController(saveRestoreToken bool) (Controller, error) {
if result != 0 {
return nil, errors.New("keyboard or pointer access denied")
}
if supportsRestoreTokens && saveRestoreToken {
restoreToken, ok := outVardict["restore_token"].Value().(string)
if !ok {
log.Println("Failed to get new restore token")
} else if restoreTokenFilePath != "" {
err := os.MkdirAll(cacheDirectory, 0700)
if err != nil {
log.Printf("Failed to create cache directory for restore token: %s\n", err)
} else {
err := os.WriteFile(restoreTokenFilePath, []byte(restoreToken), 0600)
if err != nil {
log.Printf("Failed to write restore token: %s\n", err)
}
}
if restoreToken, _ := outVardict["restore_token"].Value().(string); restoreTokenStore != nil {
if err := restoreTokenStore.Store([]byte(restoreToken)); err != nil {
log.Printf("Failed to store restore token: %v", err)
}
}
devicesV, ok := outVardict["devices"]
@ -208,6 +208,84 @@ func InitPortalController(saveRestoreToken bool) (Controller, error) {
sessionHandle: sessionHandle}, nil
}
func retrieveSecret(bus *dbus.Conn) ([]byte, error) {
portalDesktop := bus.Object("org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop")
secretReader, secretWriter, err := os.Pipe()
if err != nil {
return nil, err
}
defer secretReader.Close()
defer secretWriter.Close()
if result, _, err := getResponse(bus, portalDesktop,
"org.freedesktop.portal.Secret.RetrieveSecret", 0,
dbus.UnixFD(secretWriter.Fd()),
map[string]dbus.Variant{},
); err != nil {
return nil, err
} else if result != 0 {
return nil, fmt.Errorf("calling 'RetrieveSecret' failed (%v)", result)
}
if err := secretWriter.Close(); err != nil {
return nil, err
}
secret, err := io.ReadAll(secretReader)
if err != nil {
return nil, err
}
if len(secret) < 16 {
return nil, fmt.Errorf("'RetrieveSecret' returned too few bytes (%d)", len(secret))
}
return secret, err
}
type secretStore struct {
aesgcm cipher.AEAD
filename string
}
func newSecretStore(key []byte, filename string) (*secretStore, error) {
hkdf := hkdf.New(sha256.New, key, nil, nil)
derivedKey := make([]byte, 32)
if _, err := io.ReadFull(hkdf, derivedKey); err != nil {
return nil, err
}
block, err := aes.NewCipher(derivedKey)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
return &secretStore{
aesgcm: aesgcm,
filename: filename,
}, nil
}
func (s *secretStore) Load() ([]byte, error) {
data, err := os.ReadFile(s.filename)
if err != nil {
return nil, err
}
if len(data) < s.aesgcm.NonceSize() {
return nil, errors.New("invalid ciphertext")
}
nonce := data[:s.aesgcm.NonceSize()]
ciphertext := data[len(nonce):]
return s.aesgcm.Open(nil, nonce, ciphertext, nil)
}
func (s *secretStore) Store(data []byte) error {
nonce := make([]byte, s.aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return err
}
ciphertext := s.aesgcm.Seal(nil, nonce, data, nil)
return os.WriteFile(s.filename, slices.Concat(nonce, ciphertext), 0600)
}
func getResponse(bus *dbus.Conn, object dbus.BusObject, method string,
flags dbus.Flags, args ...interface{}) (uint32, map[string]dbus.Variant, error) {
ch := make(chan *dbus.Signal, 512)

View file

@ -43,7 +43,7 @@ func init() {
RegisterController("uinput", InitUinputController, 10)
}
func InitUinputController(saveRestoreToken bool) (Controller, error) {
func InitUinputController() (Controller, error) {
keymapName, keymapSet := os.LookupEnv("REMOTE_TOUCHPAD_UINPUT_KEYMAP")
if !keymapSet {
keymapName = "defkeymap"

View file

@ -95,7 +95,7 @@ func init() {
RegisterController("Windows", InitWindowsController, 0)
}
func InitWindowsController(saveRestoreToken bool) (Controller, error) {
func InitWindowsController() (Controller, error) {
p := &windowsController{}
if err := sendInputProc.Find(); err != nil {
return nil, &UnsupportedPlatformError{err}

View file

@ -59,7 +59,7 @@ func init() {
RegisterController("X11", InitX11Controller, 0)
}
func InitX11Controller(saveRestoreToken bool) (Controller, error) {
func InitX11Controller() (Controller, error) {
display := C.XOpenDisplay(nil)
if display == nil {
return nil, &UnsupportedPlatformError{

View file

@ -152,7 +152,7 @@ func secureRandBase64(length int) string {
func main() {
terminal.SetTitle(prettyAppName)
var bind, certFile, keyFile, secret string
var showVersion, savePlaintextRestoreToken bool
var showVersion bool
var config config
flag.BoolVar(&showVersion, "version", false, "show program's version number and exit")
flag.StringVar(&bind, "bind", defaultBind, "bind server to [HOSTNAME]:PORT")
@ -164,7 +164,6 @@ func main() {
flag.Float64Var(&config.ScrollSpeed, "scroll-speed", 1, "scroll speed multiplier")
flag.Float64Var(&config.MouseMoveSpeed, "mouse-move-speed", 1, "mouse move speed multiplier")
flag.Float64Var(&config.MouseScrollSpeed, "mouse-scroll-speed", 1, "mouse scroll speed multiplier")
flag.BoolVar(&savePlaintextRestoreToken, "save-plaintext-restore-token", false, "save a restore token in plaintext in order to avoid the confirmation dialogue that appears with some implementations of the portals backend on wayland on linux from appearing on the next run of remote-touchpad")
flag.Parse()
if showVersion {
fmt.Println(version)
@ -189,7 +188,7 @@ func main() {
for _, controllerInfo := range inputcontrol.Controllers {
controllerName = controllerInfo.Name
var err error
controller, err = controllerInfo.Init(savePlaintextRestoreToken)
controller, err = controllerInfo.Init()
if err == nil {
break
} else {