397 lines
11 KiB
Go
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
|
|
}
|