//go:build portal /* * Copyright (c) 2018 Unrud * * This file is part of Remote-Touchpad. * * Remote-Touchpad is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Remote-Touchpad is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Remote-Touchpad. If not, see . */ package inputcontrol import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "errors" "fmt" "io" "log" "os" "path/filepath" "slices" "github.com/godbus/dbus/v5" "golang.org/x/crypto/hkdf" ) const ( deviceKeyboard uint32 = 1 devicePointer uint32 = 2 btnReleased uint32 = 0 btnPressed uint32 = 1 untilRevoked uint32 = 2 // linux/input-event-codes.h btnLeft int32 = 0x110 btnRight int32 = 0x111 btnMiddle int32 = 0x112 ) type portalController struct { bus *dbus.Conn remoteDesktop dbus.BusObject sessionHandle dbus.ObjectPath } func init() { RegisterController("RemoteDesktop portal", InitPortalController, 1) } func InitPortalController() (Controller, error) { bus, err := dbus.SessionBusPrivate() if err != nil { return nil, &UnsupportedPlatformError{err} } cleanupBus := true defer func() { if cleanupBus { bus.Close() } }() err = bus.Auth(nil) if err != nil { return nil, &UnsupportedPlatformError{err} } err = bus.Hello() if err != nil { return nil, &UnsupportedPlatformError{err} } remoteDesktop := bus.Object("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop") version, err := remoteDesktop.GetProperty( "org.freedesktop.portal.RemoteDesktop.version") if err != nil { return nil, &UnsupportedPlatformError{err} } restoreTokenStore, err := func() (*secretStore, error) { if version.Value().(uint32) < 2 { return nil, nil } 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") if err != nil { return nil, &UnsupportedPlatformError{err} } availableDeviceTypes, ok := availableDeviceTypesV.Value().(uint32) if !ok { return nil, &UnsupportedPlatformError{ errors.New("unexpected 'AvailableDeviceTypes' return type")} } if availableDeviceTypes&deviceKeyboard == 0 && availableDeviceTypes&devicePointer == 0 { return nil, &UnsupportedPlatformError{ errors.New("keyboard and pointer source type not supported")} } if availableDeviceTypes&deviceKeyboard == 0 { return nil, &UnsupportedPlatformError{ errors.New("keyboard source type not supported")} } if availableDeviceTypes&devicePointer == 0 { return nil, &UnsupportedPlatformError{ errors.New("pointer source type not supported")} } inVardict := make(map[string]dbus.Variant) inVardict["session_handle_token"] = dbus.MakeVariant("t") result, outVardict, err := getResponse(bus, remoteDesktop, "org.freedesktop.portal.RemoteDesktop.CreateSession", 0, inVardict) if err != nil { return nil, &UnsupportedPlatformError{err} } if result != 0 { return nil, &UnsupportedPlatformError{ fmt.Errorf("Calling 'CreateSession' failed (%v)", result)} } sessionHandleV, ok := outVardict["session_handle"] if !ok { return nil, &UnsupportedPlatformError{ errors.New("'session_handle' missing from 'CreateSession' return value")} } sessionHandleS, ok := sessionHandleV.Value().(string) if !ok { return nil, &UnsupportedPlatformError{ errors.New("unexpected 'session_handle' type in 'CreateSession' return value")} } sessionHandle := dbus.ObjectPath(sessionHandleS) inVardict = make(map[string]dbus.Variant) inVardict["types"] = dbus.MakeVariant(deviceKeyboard | devicePointer) 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) if err != nil { return nil, &UnsupportedPlatformError{err} } if result != 0 { return nil, &UnsupportedPlatformError{ fmt.Errorf("Calling 'SelectDevices' failed (%v)", result)} } inVardict = make(map[string]dbus.Variant) result, outVardict, err = getResponse(bus, remoteDesktop, "org.freedesktop.portal.RemoteDesktop.Start", 0, sessionHandle, "", inVardict) if err != nil { return nil, &UnsupportedPlatformError{err} } if result != 0 { return nil, errors.New("keyboard or pointer access denied") } 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"] if !ok { return nil, &UnsupportedPlatformError{ errors.New("'devices' missing from 'Start' return value")} } devices, ok := devicesV.Value().(uint32) if !ok { return nil, &UnsupportedPlatformError{ errors.New("unexpected 'devices' type in 'Start' return value")} } if devices&deviceKeyboard == 0 || devices&devicePointer == 0 { return nil, errors.New("keyboard or pointer access denied") } cleanupBus = false return &portalController{bus: bus, remoteDesktop: remoteDesktop, 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) bus.Signal(ch) defer bus.RemoveSignal(ch) var requestPath dbus.ObjectPath if err := object.Call(method, flags, args...).Store(&requestPath); err != nil { return 0, nil, err } for { s := <-ch if s.Path == requestPath && s.Name == "org.freedesktop.portal.Request.Response" { if len(s.Body) != 2 { return 0, nil, errors.New("unexpected 'Response' return length") } result, ok := s.Body[0].(uint32) if !ok { return 0, nil, errors.New("unexpected 'Response' return type") } outVardict, ok := s.Body[1].(map[string]dbus.Variant) if !ok { return 0, nil, errors.New("unexpected 'Response' return type") } return result, outVardict, nil } } } func (p *portalController) Close() error { return p.bus.Close() } func (p *portalController) keyboardKeys(keys []Keysym) error { inVardict := make(map[string]dbus.Variant) for _, keysym := range keys { for _, state := range [...]uint32{btnPressed, btnReleased} { if err := p.remoteDesktop.Call( "org.freedesktop.portal.RemoteDesktop.NotifyKeyboardKeysym", 0, p.sessionHandle, inVardict, keysym, state).Store(); err != nil { return err } } } return nil } func (p *portalController) KeyboardText(text string) error { keys := make([]Keysym, 0, len(text)) for _, runeValue := range text { keysym, err := RuneToKeysym(runeValue) if err != nil { return err } keys = append(keys, keysym) } return p.keyboardKeys(keys) } func (p *portalController) KeyboardKey(key Key) error { keysym, err := KeyToKeysym(key) if err != nil { return err } keys := [...]Keysym{keysym} return p.keyboardKeys(keys[:]) } func (p *portalController) PointerButton(button PointerButton, press bool) error { var btn int32 switch button { case PointerButtonLeft: btn = btnLeft case PointerButtonMiddle: btn = btnMiddle case PointerButtonRight: btn = btnRight default: return fmt.Errorf("unsupported pointer button: %#v", button) } state := btnReleased if press { state = btnPressed } inVardict := make(map[string]dbus.Variant) if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerButton", 0, p.sessionHandle, inVardict, btn, state).Store(); err != nil { return err } return nil } func (p *portalController) PointerMove(deltaX, deltaY int) error { inVardict := make(map[string]dbus.Variant) if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerMotion", 0, p.sessionHandle, inVardict, float64(deltaX), float64(deltaY)).Store(); err != nil { return err } return nil } func (p *portalController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error { inVardict := make(map[string]dbus.Variant) inVardict["finish"] = dbus.MakeVariant(finish) if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerAxis", 0, p.sessionHandle, inVardict, float64(deltaHorizontal), float64(deltaVertical)).Store(); err != nil { return err } return nil }