remote-touchpad/inputcontrol/controller_portal.go
2025-03-18 09:18:15 +01:00

397 lines
11 KiB
Go

//go:build portal
/*
* Copyright (c) 2018 Unrud <unrud@outlook.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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
}