From 16dc47b5708bf0e526c175cea30b22da988dfc47 Mon Sep 17 00:00:00 2001 From: Unrud Date: Tue, 18 Mar 2025 09:18:15 +0100 Subject: [PATCH] Encrypt restore token --- flatpak/com.github.unrud.RemoteTouchpad.yaml | 5 + flatpak/modules.txt | 3 + go.mod | 1 + go.sum | 2 + inputcontrol/controller.go | 4 +- inputcontrol/controller_null.go | 2 +- inputcontrol/controller_portal.go | 158 ++++++++++++++----- inputcontrol/controller_uinput.go | 2 +- inputcontrol/controller_windows.go | 2 +- inputcontrol/controller_x11.go | 2 +- main.go | 5 +- 11 files changed, 137 insertions(+), 49 deletions(-) diff --git a/flatpak/com.github.unrud.RemoteTouchpad.yaml b/flatpak/com.github.unrud.RemoteTouchpad.yaml index 08934d7..55bb828 100644 --- a/flatpak/com.github.unrud.RemoteTouchpad.yaml +++ b/flatpak/com.github.unrud.RemoteTouchpad.yaml @@ -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 diff --git a/flatpak/modules.txt b/flatpak/modules.txt index 1ac92b8..6662b65 100644 --- a/flatpak/modules.txt +++ b/flatpak/modules.txt @@ -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 diff --git a/go.mod b/go.mod index c99d2aa..1f2549c 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 9cf18b6..a03440b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/inputcontrol/controller.go b/inputcontrol/controller.go index 3a64df0..06ed8e4 100644 --- a/inputcontrol/controller.go +++ b/inputcontrol/controller.go @@ -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 diff --git a/inputcontrol/controller_null.go b/inputcontrol/controller_null.go index 8b1ba67..6188cce 100644 --- a/inputcontrol/controller_null.go +++ b/inputcontrol/controller_null.go @@ -31,7 +31,7 @@ func init() { RegisterController("null", InitNullController, 1000) } -func InitNullController(saveRestoreToken bool) (Controller, error) { +func InitNullController() (Controller, error) { return &nullController{}, nil } diff --git a/inputcontrol/controller_portal.go b/inputcontrol/controller_portal.go index 439a7c5..b5a59d2 100644 --- a/inputcontrol/controller_portal.go +++ b/inputcontrol/controller_portal.go @@ -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) diff --git a/inputcontrol/controller_uinput.go b/inputcontrol/controller_uinput.go index b9723a2..df51e65 100644 --- a/inputcontrol/controller_uinput.go +++ b/inputcontrol/controller_uinput.go @@ -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" diff --git a/inputcontrol/controller_windows.go b/inputcontrol/controller_windows.go index 0a19556..58b5e9a 100644 --- a/inputcontrol/controller_windows.go +++ b/inputcontrol/controller_windows.go @@ -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} diff --git a/inputcontrol/controller_x11.go b/inputcontrol/controller_x11.go index 98fcbc4..bb37d37 100644 --- a/inputcontrol/controller_x11.go +++ b/inputcontrol/controller_x11.go @@ -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{ diff --git a/main.go b/main.go index 44cf381..3d05c7e 100644 --- a/main.go +++ b/main.go @@ -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 {