package bluez import ( "context" "errors" "fmt" "strconv" "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/godbus/dbus/v5" ) const ( bluezService = "org.bluez" agentManagerPath = "/org/bluez" agentManagerIface = "org.bluez.AgentManager1" agent1Iface = "org.bluez.Agent1" device1Iface = "org.bluez.Device1" agentPath = "/com/danklinux/bluez/agent" agentCapability = "KeyboardDisplay" ) const introspectXML = ` ` type BluezAgent struct { conn *dbus.Conn broker PromptBroker } func NewBluezAgent(broker PromptBroker) (*BluezAgent, error) { conn, err := dbus.ConnectSystemBus() if err != nil { return nil, fmt.Errorf("system bus connection failed: %w", err) } agent := &BluezAgent{ conn: conn, broker: broker, } if err := conn.Export(agent, dbus.ObjectPath(agentPath), agent1Iface); err != nil { conn.Close() return nil, fmt.Errorf("agent export failed: %w", err) } if err := conn.Export(agent, dbus.ObjectPath(agentPath), "org.freedesktop.DBus.Introspectable"); err != nil { conn.Close() return nil, fmt.Errorf("introspection export failed: %w", err) } mgr := conn.Object(bluezService, dbus.ObjectPath(agentManagerPath)) if err := mgr.Call(agentManagerIface+".RegisterAgent", 0, dbus.ObjectPath(agentPath), agentCapability).Err; err != nil { conn.Close() return nil, fmt.Errorf("agent registration failed: %w", err) } if err := mgr.Call(agentManagerIface+".RequestDefaultAgent", 0, dbus.ObjectPath(agentPath)).Err; err != nil { log.Debugf("[BluezAgent] not default agent: %v", err) } log.Infof("[BluezAgent] registered at %s with capability %s", agentPath, agentCapability) return agent, nil } func (a *BluezAgent) Close() { if a.conn == nil { return } mgr := a.conn.Object(bluezService, dbus.ObjectPath(agentManagerPath)) mgr.Call(agentManagerIface+".UnregisterAgent", 0, dbus.ObjectPath(agentPath)) a.conn.Close() } func (a *BluezAgent) Release() *dbus.Error { log.Infof("[BluezAgent] Release called") return nil } func (a *BluezAgent) RequestPinCode(device dbus.ObjectPath) (string, *dbus.Error) { log.Infof("[BluezAgent] RequestPinCode: device=%s", device) secrets, err := a.promptFor(device, "pin", []string{"pin"}, nil) if err != nil { log.Warnf("[BluezAgent] RequestPinCode failed: %v", err) return "", a.errorFrom(err) } pin := secrets["pin"] log.Infof("[BluezAgent] RequestPinCode returning PIN (len=%d)", len(pin)) return pin, nil } func (a *BluezAgent) RequestPasskey(device dbus.ObjectPath) (uint32, *dbus.Error) { log.Infof("[BluezAgent] RequestPasskey: device=%s", device) secrets, err := a.promptFor(device, "passkey", []string{"passkey"}, nil) if err != nil { log.Warnf("[BluezAgent] RequestPasskey failed: %v", err) return 0, a.errorFrom(err) } passkey, err := strconv.ParseUint(secrets["passkey"], 10, 32) if err != nil { log.Warnf("[BluezAgent] invalid passkey format: %v", err) return 0, dbus.MakeFailedError(fmt.Errorf("invalid passkey: %w", err)) } log.Infof("[BluezAgent] RequestPasskey returning: %d", passkey) return uint32(passkey), nil } func (a *BluezAgent) DisplayPinCode(device dbus.ObjectPath, pincode string) *dbus.Error { log.Infof("[BluezAgent] DisplayPinCode: device=%s, pin=%s", device, pincode) _, err := a.promptFor(device, "display-pin", []string{}, &pincode) if err != nil { log.Warnf("[BluezAgent] DisplayPinCode acknowledgment failed: %v", err) } return nil } func (a *BluezAgent) DisplayPasskey(device dbus.ObjectPath, passkey uint32, entered uint16) *dbus.Error { log.Infof("[BluezAgent] DisplayPasskey: device=%s, passkey=%06d, entered=%d", device, passkey, entered) if entered == 0 { passkeyStr := strconv.FormatUint(uint64(passkey), 10) _, err := a.promptFor(device, "display-passkey", []string{}, &passkeyStr) if err != nil { log.Warnf("[BluezAgent] DisplayPasskey acknowledgment failed: %v", err) } } return nil } func (a *BluezAgent) RequestConfirmation(device dbus.ObjectPath, passkey uint32) *dbus.Error { log.Infof("[BluezAgent] RequestConfirmation: device=%s, passkey=%06d", device, passkey) passkeyStr := strconv.FormatUint(uint64(passkey), 10) secrets, err := a.promptFor(device, "confirm", []string{"decision"}, &passkeyStr) if err != nil { log.Warnf("[BluezAgent] RequestConfirmation failed: %v", err) return a.errorFrom(err) } if secrets["decision"] != "yes" && secrets["decision"] != "accept" { log.Debugf("[BluezAgent] RequestConfirmation rejected by user") return dbus.NewError("org.bluez.Error.Rejected", nil) } log.Infof("[BluezAgent] RequestConfirmation accepted") return nil } func (a *BluezAgent) RequestAuthorization(device dbus.ObjectPath) *dbus.Error { log.Infof("[BluezAgent] RequestAuthorization: device=%s", device) secrets, err := a.promptFor(device, "authorize", []string{"decision"}, nil) if err != nil { log.Warnf("[BluezAgent] RequestAuthorization failed: %v", err) return a.errorFrom(err) } if secrets["decision"] != "yes" && secrets["decision"] != "accept" { log.Debugf("[BluezAgent] RequestAuthorization rejected by user") return dbus.NewError("org.bluez.Error.Rejected", nil) } log.Infof("[BluezAgent] RequestAuthorization accepted") return nil } func (a *BluezAgent) AuthorizeService(device dbus.ObjectPath, uuid string) *dbus.Error { log.Infof("[BluezAgent] AuthorizeService: device=%s, uuid=%s", device, uuid) secrets, err := a.promptFor(device, "authorize-service:"+uuid, []string{"decision"}, nil) if err != nil { log.Warnf("[BluezAgent] AuthorizeService failed: %v", err) return a.errorFrom(err) } if secrets["decision"] != "yes" && secrets["decision"] != "accept" { log.Debugf("[BluezAgent] AuthorizeService rejected by user") return dbus.NewError("org.bluez.Error.Rejected", nil) } log.Infof("[BluezAgent] AuthorizeService accepted") return nil } func (a *BluezAgent) Cancel() *dbus.Error { log.Infof("[BluezAgent] Cancel called") return nil } func (a *BluezAgent) Introspect() (string, *dbus.Error) { return introspectXML, nil } func (a *BluezAgent) promptFor(device dbus.ObjectPath, requestType string, fields []string, displayValue *string) (map[string]string, error) { if a.broker == nil { return nil, fmt.Errorf("broker not initialized") } deviceName, deviceAddr := a.getDeviceInfo(device) hints := []string{} if displayValue != nil { hints = append(hints, *displayValue) } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() var passkey *uint32 if requestType == "confirm" || requestType == "display-passkey" { if displayValue != nil { if pk, err := strconv.ParseUint(*displayValue, 10, 32); err == nil { pk32 := uint32(pk) passkey = &pk32 } } } token, err := a.broker.Ask(ctx, PromptRequest{ DevicePath: string(device), DeviceName: deviceName, DeviceAddr: deviceAddr, RequestType: requestType, Fields: fields, Hints: hints, Passkey: passkey, }) if err != nil { return nil, fmt.Errorf("prompt creation failed: %w", err) } log.Infof("[BluezAgent] waiting for user response (token=%s)", token) reply, err := a.broker.Wait(ctx, token) if err != nil { if errors.Is(err, errdefs.ErrSecretPromptTimeout) { return nil, err } if reply.Cancel || errors.Is(err, errdefs.ErrSecretPromptCancelled) { return nil, errdefs.ErrSecretPromptCancelled } return nil, err } if !reply.Accept && len(fields) > 0 { return nil, errdefs.ErrSecretPromptCancelled } return reply.Secrets, nil } func (a *BluezAgent) getDeviceInfo(device dbus.ObjectPath) (string, string) { obj := a.conn.Object(bluezService, device) var name, alias, addr string nameVar, err := obj.GetProperty(device1Iface + ".Name") if err == nil { if n, ok := nameVar.Value().(string); ok { name = n } } aliasVar, err := obj.GetProperty(device1Iface + ".Alias") if err == nil { if a, ok := aliasVar.Value().(string); ok { alias = a } } addrVar, err := obj.GetProperty(device1Iface + ".Address") if err == nil { if a, ok := addrVar.Value().(string); ok { addr = a } } if alias != "" { return alias, addr } if name != "" { return name, addr } return addr, addr } func (a *BluezAgent) errorFrom(err error) *dbus.Error { if errors.Is(err, errdefs.ErrSecretPromptTimeout) { return dbus.NewError("org.bluez.Error.Canceled", nil) } if errors.Is(err, errdefs.ErrSecretPromptCancelled) { return dbus.NewError("org.bluez.Error.Canceled", nil) } return dbus.MakeFailedError(err) }