From 65fed65bf2b75800f91ca3ab498767416f6bb468 Mon Sep 17 00:00:00 2001 From: Giuseppe Tufo Date: Sun, 8 Feb 2026 03:05:53 +0100 Subject: [PATCH] fix pairing --- go.mod | 2 +- internal/ble/agent.go | 139 +++++++++++++++++++++++++++++++++++++++++ internal/tui/logger.go | 5 ++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 internal/ble/agent.go diff --git a/go.mod b/go.mod index 03d2e1b..6658bee 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.6 require ( github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 + github.com/godbus/dbus/v5 v5.1.0 tinygo.org/x/bluetooth v0.14.0 ) @@ -19,7 +20,6 @@ require ( github.com/clipperhouse/uax29/v2 v2.5.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect diff --git a/internal/ble/agent.go b/internal/ble/agent.go new file mode 100644 index 0000000..b3d3790 --- /dev/null +++ b/internal/ble/agent.go @@ -0,0 +1,139 @@ +package ble + +import ( + "context" + "fmt" + "os/exec" + "time" + + "github.com/godbus/dbus/v5" +) + +const ( + agentPath = "/org/bluez/agent/noinputnooutput" + agentInterface = "org.bluez.Agent1" + agentManager = "org.bluez.AgentManager1" + bluezService = "org.bluez" + bluezPath = "/org/bluez" +) + +// NoInputNoOutputAgent implements org.bluez.Agent1 with NoInputNoOutput capability. +// This disables pairing prompts and allows "Just Works" pairing. +type NoInputNoOutputAgent struct{} + +// Release is called when the agent is unregistered. +func (a *NoInputNoOutputAgent) Release() *dbus.Error { + return nil +} + +// RequestPinCode returns an error as PIN codes are not supported with NoInputNoOutput. +func (a *NoInputNoOutputAgent) RequestPinCode(device dbus.ObjectPath) (string, *dbus.Error) { + return "", dbus.NewError("org.bluez.Error.Rejected", []any{"NoInputNoOutput agent"}) +} + +// DisplayPinCode does nothing as we have no display. +func (a *NoInputNoOutputAgent) DisplayPinCode(device dbus.ObjectPath, pincode string) *dbus.Error { + return nil +} + +// RequestPasskey returns an error as passkeys are not supported with NoInputNoOutput. +func (a *NoInputNoOutputAgent) RequestPasskey(device dbus.ObjectPath) (uint32, *dbus.Error) { + return 0, dbus.NewError("org.bluez.Error.Rejected", []any{"NoInputNoOutput agent"}) +} + +// DisplayPasskey does nothing as we have no display. +func (a *NoInputNoOutputAgent) DisplayPasskey(device dbus.ObjectPath, passkey uint32, entered uint16) *dbus.Error { + return nil +} + +// RequestConfirmation auto-accepts pairing for "Just Works" mode. +func (a *NoInputNoOutputAgent) RequestConfirmation(device dbus.ObjectPath, passkey uint32) *dbus.Error { + return nil // Accept pairing silently +} + +// RequestAuthorization auto-accepts authorization for "Just Works" mode. +func (a *NoInputNoOutputAgent) RequestAuthorization(device dbus.ObjectPath) *dbus.Error { + return nil // Accept authorization silently +} + +// AuthorizeService auto-accepts service authorization. +func (a *NoInputNoOutputAgent) AuthorizeService(device dbus.ObjectPath, uuid string) *dbus.Error { + return nil // Accept service authorization silently +} + +// Cancel is called when an operation is canceled. +func (a *NoInputNoOutputAgent) Cancel() *dbus.Error { + return nil +} + +// RegisterNoInputNoOutputAgent connects to system DBus and registers a NoInputNoOutput +// agent with BlueZ. This disables pairing prompts for BLE connections. +func RegisterNoInputNoOutputAgent() error { + conn, err := dbus.SystemBus() + if err != nil { + return fmt.Errorf("failed to connect to system DBus: %w", err) + } + + agent := &NoInputNoOutputAgent{} + + // Export the agent object on DBus + err = conn.Export(agent, agentPath, agentInterface) + if err != nil { + return fmt.Errorf("failed to export agent: %w", err) + } + + // Get the AgentManager interface + agentMgr := conn.Object(bluezService, bluezPath) + + // Register our agent with NoInputNoOutput capability + call := agentMgr.Call(agentManager+".RegisterAgent", 0, dbus.ObjectPath(agentPath), "NoInputNoOutput") + if call.Err != nil { + return fmt.Errorf("failed to register agent: %w", call.Err) + } + + // Request to be the default agent + call = agentMgr.Call(agentManager+".RequestDefaultAgent", 0, dbus.ObjectPath(agentPath)) + if call.Err != nil { + return fmt.Errorf("failed to set default agent: %w", call.Err) + } + + return nil +} + +// SetAdapterNotPairable sets the Pairable property to false on the default adapter. +// This is optional and prevents the adapter from initiating pairing. +func SetAdapterNotPairable() error { + conn, err := dbus.SystemBus() + if err != nil { + return fmt.Errorf("failed to connect to system DBus: %w", err) + } + + // Get the default adapter (hci0) + adapter := conn.Object(bluezService, "/org/bluez/hci0") + + // Set Pairable to false using org.freedesktop.DBus.Properties interface + call := adapter.Call("org.freedesktop.DBus.Properties.Set", 0, + "org.bluez.Adapter1", "Pairable", dbus.MakeVariant(false)) + if call.Err != nil { + return fmt.Errorf("failed to set Pairable: %w", call.Err) + } + + return nil +} + +// DisableBonding runs btmgmt to disable bonding on the default adapter. +// This prevents phones from initiating pairing requests. +func DisableBonding() error { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "btmgmt", "-i", "0", "bondable", "off") + output, err := cmd.CombinedOutput() + if ctx.Err() == context.DeadlineExceeded { + return fmt.Errorf("btmgmt timed out") + } + if err != nil { + return fmt.Errorf("failed to disable bonding: %w (output: %s)", err, output) + } + return nil +} diff --git a/internal/tui/logger.go b/internal/tui/logger.go index 747826a..095a698 100644 --- a/internal/tui/logger.go +++ b/internal/tui/logger.go @@ -70,6 +70,11 @@ func (l *LogBuffer) Err(format string, args ...any) { l.Log("ERR", fmt.Sprintf(format, args...)) } +// Warn logs a WARN level message +func (l *LogBuffer) Warn(format string, args ...any) { + l.Log("WARN", fmt.Sprintf(format, args...)) +} + // Entries returns a copy of all log entries func (l *LogBuffer) Entries() []LogEntry { l.mu.RLock()