diff --git a/agent/bitwarden/auth.go b/agent/bitwarden/auth.go index a6e614a..5d2146f 100644 --- a/agent/bitwarden/auth.go +++ b/agent/bitwarden/auth.go @@ -17,6 +17,7 @@ import ( "github.com/quexten/goldwarden/agent/bitwarden/crypto" "github.com/quexten/goldwarden/agent/bitwarden/twofactor" "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/notify" "github.com/quexten/goldwarden/agent/systemauth/pinentry" "github.com/quexten/goldwarden/agent/vault" "github.com/quexten/goldwarden/logging" @@ -68,6 +69,7 @@ func LoginWithMasterpassword(ctx context.Context, email string, cfg *config.Conf if err := authenticatedHTTPPost(ctx, cfg.ConfigFile.ApiUrl+"/accounts/prelogin", &preLogin, preLoginRequest{ Email: email, }); err != nil { + notify.Notify("Goldwarden", fmt.Sprintf("Could not pre-login: %v", err), "", func() {}) return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not pre-login: %v", err) } @@ -77,11 +79,13 @@ func LoginWithMasterpassword(ctx context.Context, email string, cfg *config.Conf password, err := pinentry.GetPassword("Bitwarden Password", "Enter your Bitwarden password") if err != nil { + notify.Notify("Goldwarden", fmt.Sprintf("Could not get password: %v", err), "", func() {}) return LoginResponseToken{}, crypto.MasterKey{}, "", err } masterKey, err = crypto.DeriveMasterKey([]byte(strings.Clone(password)), email, crypto.KDFConfig{Type: crypto.KDFType(preLogin.KDF), Iterations: uint32(preLogin.KDFIterations), Memory: uint32(preLogin.KDFMemory), Parallelism: uint32(preLogin.KDFParallelism)}) if err != nil { + notify.Notify("Goldwarden", fmt.Sprintf("Could not derive master key: %v", err), "", func() {}) return LoginResponseToken{}, crypto.MasterKey{}, "", err } @@ -104,12 +108,14 @@ func LoginWithMasterpassword(ctx context.Context, email string, cfg *config.Conf if ok && bytes.Contains(errsc.body, []byte("TwoFactor")) { loginResponseToken, err = Perform2FA(values, errsc, cfg, ctx) if err != nil { + notify.Notify("Goldwarden", fmt.Sprintf("Could not login via two-factor: %v", err), "", func() {}) return LoginResponseToken{}, crypto.MasterKey{}, "", err } } else if err != nil && strings.Contains(err.Error(), "Captcha required.") { + notify.Notify("Goldwarden", fmt.Sprintf("Captcha required"), "", func() {}) return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("captcha required, please login via the web interface") - } else if err != nil { + notify.Notify("Goldwarden", fmt.Sprintf("Could not login via password: %v", err), "", func() {}) return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not login via password: %v", err) } diff --git a/agent/bitwarden/sync.go b/agent/bitwarden/sync.go index bbe1a15..e306b06 100644 --- a/agent/bitwarden/sync.go +++ b/agent/bitwarden/sync.go @@ -10,6 +10,7 @@ import ( "github.com/quexten/goldwarden/agent/bitwarden/crypto" "github.com/quexten/goldwarden/agent/bitwarden/models" "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/notify" "github.com/quexten/goldwarden/agent/vault" "github.com/quexten/goldwarden/logging" ) @@ -34,6 +35,7 @@ func DoFullSync(ctx context.Context, vault *vault.Vault, config *config.Config, sync, err := Sync(ctx, config) if err != nil { log.Error("Could not sync: %v", err) + notify.Notify("Goldwarden", "Could not sync", "", func() {}) if allowCache { home, _ := os.UserHomeDir() sync, err = ReadVault(home + path) diff --git a/agent/config/config.go b/agent/config/config.go index d70dc14..89474ec 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -13,6 +13,7 @@ import ( "github.com/google/uuid" "github.com/quexten/goldwarden/agent/bitwarden/crypto" + "github.com/quexten/goldwarden/agent/notify" "github.com/quexten/goldwarden/agent/systemauth/pinentry" "github.com/quexten/goldwarden/agent/vault" "github.com/tink-crypto/tink-go/v2/aead/subtle" @@ -146,6 +147,7 @@ func (c *Config) Lock() { return } (*c.key).Wipe() + notify.Notify("Goldwarden", "Vault Locked", "", func() {}) } func (c *Config) Purge() { diff --git a/agent/notify/dbus.go b/agent/notify/dbus.go new file mode 100644 index 0000000..8cb4b3f --- /dev/null +++ b/agent/notify/dbus.go @@ -0,0 +1,69 @@ +//go:build linux || freebsd + +package notify + +import ( + "github.com/godbus/dbus/v5" +) + +var closeListenerMap = make(map[uint32]func()) + +func Notify(title string, body string, actionName string, onclose func()) error { + bus, err := dbus.SessionBus() + if err != nil { + return err + } + obj := bus.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") + actions := []string{} + if actionName != "" { + actions = append(actions, actionName) + } + call := obj.Call("org.freedesktop.Notifications.Notify", 0, "goldwarden", uint32(0), "", title, body, actions, map[string]dbus.Variant{}, int32(60000)) + if call.Err != nil { + return call.Err + } + if len(call.Body) < 1 { + return nil + } + id := call.Body[0].(uint32) + closeListenerMap[id] = onclose + + return nil +} + +func ListenForNotifications() error { + bus, err := dbus.SessionBus() + if err != nil { + return err + } + err = bus.AddMatchSignal(dbus.WithMatchInterface("org.freedesktop.Notifications")) + if err != nil { + return err + } + + signals := make(chan *dbus.Signal, 10) + bus.Signal(signals) + for { + select { + case message := <-signals: + if message.Name == "org.freedesktop.Notifications.NotificationClosed" { + if len(message.Body) < 1 { + continue + } + id, ok := message.Body[0].(uint32) + if !ok { + continue + } + if id == 0 { + continue + } + if closeListener, ok := closeListenerMap[id]; ok { + delete(closeListenerMap, id) + closeListener() + } + } + } + } + + return nil +} diff --git a/agent/notify/unimplemented.go b/agent/notify/unimplemented.go new file mode 100644 index 0000000..889525c --- /dev/null +++ b/agent/notify/unimplemented.go @@ -0,0 +1,12 @@ +//go:build windows || darwin + +package notify + +func Notify(title string, body string) error { + // no notifications on windows or darwin + return nil +} + +func ListenForNotifications() error { + return nil +} diff --git a/agent/ssh/ssh.go b/agent/ssh/ssh.go index 3f6fcf8..01dafd6 100644 --- a/agent/ssh/ssh.go +++ b/agent/ssh/ssh.go @@ -9,6 +9,7 @@ import ( "os" "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/notify" "github.com/quexten/goldwarden/agent/sockets" "github.com/quexten/goldwarden/agent/systemauth" "github.com/quexten/goldwarden/agent/systemauth/pinentry" @@ -113,6 +114,7 @@ func (vaultAgent vaultAgent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signatur var rand = rand.Reader log.Info("Sign Request for key: %s %s accepted", ssh.FingerprintSHA256(key), sshKey.Name) + notify.Notify("Goldwarden", fmt.Sprintf("SSH Key Signing Request Approved for %s", sshKey.Name), "", func() {}) return signer.Sign(rand, data) } diff --git a/agent/unixsocketagent.go b/agent/unixsocketagent.go index a5a34fc..6a8ef70 100644 --- a/agent/unixsocketagent.go +++ b/agent/unixsocketagent.go @@ -12,6 +12,7 @@ import ( "github.com/quexten/goldwarden/agent/bitwarden" "github.com/quexten/goldwarden/agent/bitwarden/crypto" "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/notify" "github.com/quexten/goldwarden/agent/processsecurity" "github.com/quexten/goldwarden/agent/sockets" "github.com/quexten/goldwarden/agent/ssh" @@ -181,6 +182,12 @@ func StartUnixAgent(path string, runtimeConfig config.RuntimeConfig) error { log.Warn("Could not monitor idle: %s", err.Error()) } }() + go func() { + err = notify.ListenForNotifications() + if err != nil { + log.Warn("Could not listen for notifications: %s", err.Error()) + } + }() go func() { if !runtimeConfig.WebsocketDisabled {