goldwarden-vaultwarden-bitw.../agent/bitwarden/websocket.go

287 lines
8.5 KiB
Go
Raw Normal View History

2023-07-17 03:23:26 +02:00
package bitwarden
import (
"bytes"
"context"
"net/url"
"os"
"time"
"github.com/awnumar/memguard"
"github.com/gorilla/websocket"
"github.com/quexten/goldwarden/agent/bitwarden/models"
"github.com/quexten/goldwarden/agent/config"
"github.com/quexten/goldwarden/agent/notify"
2023-09-12 01:22:48 +02:00
"github.com/quexten/goldwarden/agent/systemauth/biometrics"
2023-09-12 02:54:46 +02:00
"github.com/quexten/goldwarden/agent/systemauth/pinentry"
2023-07-17 03:23:26 +02:00
"github.com/quexten/goldwarden/agent/vault"
2023-08-21 18:37:34 +02:00
"github.com/quexten/goldwarden/logging"
2023-07-17 03:23:26 +02:00
"github.com/vmihailenco/msgpack/v5"
)
2023-08-21 18:37:34 +02:00
var websocketLog = logging.GetLogger("Goldwarden", "Websocket")
2023-07-17 03:23:26 +02:00
type NotificationMessageType int64
const (
SyncCipherUpdate NotificationMessageType = 0
SyncCipherCreate NotificationMessageType = 1
SyncLoginDelete NotificationMessageType = 2
SyncFolderDelete NotificationMessageType = 3
SyncCiphers NotificationMessageType = 4
SyncVault NotificationMessageType = 5
SyncOrgKeys NotificationMessageType = 6
SyncFolderCreate NotificationMessageType = 7
SyncFolderUpdate NotificationMessageType = 8
SyncCipherDelete NotificationMessageType = 9
SyncSettings NotificationMessageType = 10
LogOut NotificationMessageType = 11
SyncSendCreate NotificationMessageType = 12
SyncSendUpdate NotificationMessageType = 13
SyncSendDelete NotificationMessageType = 14
AuthRequest NotificationMessageType = 15
AuthRequestResponse NotificationMessageType = 16
)
const (
2023-12-22 10:44:49 +01:00
WEBSOCKET_SLEEP_DURATION_SECONDS = 60
2023-07-17 03:23:26 +02:00
)
func RunWebsocketDaemon(ctx context.Context, vault *vault.Vault, cfg *config.Config) {
for {
if cfg.IsLocked() {
2023-12-22 12:09:50 +01:00
time.Sleep(5 * time.Second)
2023-07-17 03:23:26 +02:00
continue
}
if token, err := cfg.GetToken(); err == nil && token.AccessToken != "" {
err := connectToWebsocket(ctx, vault, cfg)
if err != nil {
websocketLog.Error("Websocket error %s", err)
}
}
2023-12-22 12:09:50 +01:00
time.Sleep(WEBSOCKET_SLEEP_DURATION_SECONDS * time.Second)
2023-07-17 03:23:26 +02:00
}
}
func connectToWebsocket(ctx context.Context, vault *vault.Vault, cfg *config.Config) error {
2023-09-11 14:14:27 +02:00
url, err := url.Parse(cfg.ConfigFile.NotificationsUrl)
2023-07-17 03:23:26 +02:00
if err != nil {
return err
}
token, err := cfg.GetToken()
2023-09-11 14:14:27 +02:00
var websocketURL = "wss://" + url.Host + url.Path + "/hub?access_token=" + token.AccessToken
2023-07-17 03:23:26 +02:00
c, _, err := websocket.DefaultDialer.Dial(websocketURL, nil)
if err != nil {
return err
}
defer c.Close()
websocketLog.Info("Connected to websocket server...")
vault.SetWebsocketConnected(true)
2023-07-17 03:23:26 +02:00
done := make(chan struct{})
2023-09-11 14:14:27 +02:00
//handshake required for official bitwarden implementation
2024-03-03 01:38:11 +01:00
err = c.WriteMessage(1, []byte(`{"protocol":"messagepack","version":1}`))
if err != nil {
return err
}
2023-07-17 03:23:26 +02:00
go func() {
for {
time.Sleep(5 * time.Second)
if vault.Keyring.IsLocked() || cfg.IsLocked() || !cfg.IsLoggedIn() {
c.Close()
return
}
}
}()
2023-07-17 03:23:26 +02:00
go func() {
defer close(done)
for {
2023-12-22 10:44:49 +01:00
_, message, err := c.ReadMessage()
2023-07-17 03:23:26 +02:00
if err != nil {
websocketLog.Error("Error reading websocket message %s", err)
return
}
if len(message) < 5 {
2023-12-22 10:44:49 +01:00
//ignore empty messages
continue
}
2023-07-17 03:23:26 +02:00
if messageType, cipherid, success := websocketMessageType(message); success {
var mt1 = NotificationMessageType(messageType)
switch mt1 {
case SyncCiphers, SyncVault:
websocketLog.Warn("SyncCiphers requested")
token, err := cfg.GetToken()
if err != nil {
websocketLog.Error("Error getting token %s", err)
break
}
2024-03-03 01:38:11 +01:00
err = DoFullSync(context.WithValue(ctx, AuthToken{}, token.AccessToken), vault, cfg, nil, false)
if err != nil {
log.Error("could not perform full sync: %s", err.Error())
return
}
2023-07-17 03:23:26 +02:00
case SyncCipherDelete:
websocketLog.Warn("Delete requested for cipher " + cipherid)
vault.DeleteCipher(cipherid)
case SyncCipherUpdate:
websocketLog.Warn("Update requested for cipher " + cipherid)
token, err := cfg.GetToken()
if err != nil {
websocketLog.Error("Error getting token %s", err)
break
}
cipher, err := GetCipher(context.WithValue(ctx, AuthToken{}, token.AccessToken), cipherid, cfg)
if err != nil {
websocketLog.Error("Error getting cipher %s", err)
break
}
if !cipher.DeletedDate.IsZero() {
websocketLog.Info("Cipher moved to trash " + cipherid)
vault.DeleteCipher(cipherid)
break
}
if cipher.Type == models.CipherNote {
vault.AddOrUpdateSecureNote(cipher)
} else {
vault.AddOrUpdateLogin(cipher)
}
vault.SetLastSynced(time.Now().Unix())
2023-07-17 03:23:26 +02:00
case SyncCipherCreate:
websocketLog.Warn("Create requested for cipher " + cipherid)
token, err := cfg.GetToken()
if err != nil {
websocketLog.Error("Error getting token %s", err)
break
}
cipher, err := GetCipher(context.WithValue(ctx, AuthToken{}, token.AccessToken), cipherid, cfg)
if err != nil {
websocketLog.Error("Error getting cipher %s", err)
break
}
if cipher.Type == models.CipherNote {
vault.AddOrUpdateSecureNote(cipher)
} else {
vault.AddOrUpdateLogin(cipher)
}
vault.SetLastSynced(time.Now().Unix())
2023-07-17 03:23:26 +02:00
case SyncSendCreate, SyncSendUpdate, SyncSendDelete:
websocketLog.Warn("SyncSend requested: sends are not supported")
case LogOut:
websocketLog.Info("LogOut received. Wiping vault and exiting...")
2023-12-22 12:01:21 +01:00
if vault.Keyring.IsMemguard {
memguard.SafeExit(0)
} else {
os.Exit(0)
}
2023-07-17 03:23:26 +02:00
case AuthRequest:
websocketLog.Info("AuthRequest received" + string(cipherid))
authRequest, err := GetAuthRequest(context.WithValue(ctx, AuthToken{}, token.AccessToken), cipherid, cfg)
if err != nil {
websocketLog.Error("Error getting auth request %s", err)
break
}
websocketLog.Info("AuthRequest details " + authRequest.RequestIpAddress + " " + authRequest.RequestDeviceType)
notify.Notify("Passwordless Login Request", authRequest.RequestIpAddress+" - "+authRequest.RequestDeviceType, "", 0, func() {
var message = "Do you want to allow " + authRequest.RequestIpAddress + " (" + authRequest.RequestDeviceType + ") to login to your account?"
if approved, err := pinentry.GetApproval("Paswordless Login Request", message); err != nil || !approved {
websocketLog.Info("AuthRequest denied")
return
}
if !biometrics.CheckBiometrics(biometrics.AccessVault) {
websocketLog.Info("AuthRequest denied - biometrics required")
return
}
_, err = CreateAuthResponse(context.WithValue(ctx, AuthToken{}, token.AccessToken), authRequest, vault.Keyring, cfg)
if err != nil {
websocketLog.Error("Error creating auth response %s", err)
}
})
2023-07-17 03:23:26 +02:00
case AuthRequestResponse:
websocketLog.Info("AuthRequestResponse received")
case SyncFolderDelete, SyncFolderCreate, SyncFolderUpdate:
websocketLog.Warn("SyncFolder requested: folders are not supported")
case SyncOrgKeys, SyncSettings:
websocketLog.Warn("SyncOrgKeys requested: orgs / settings are not supported")
default:
websocketLog.Warn("Unknown message type received %d", mt1)
}
}
}
}()
<-done
vault.SetWebsocketConnected(false)
2023-07-17 03:23:26 +02:00
return nil
}
func websocketMessageType(message []byte) (int8, string, bool) {
lenBufferLen := 0
for i := 0; i < len(message); i++ {
if (message[i] & 0x80) == 0 {
lenBufferLen = i + 1
break
}
}
msgPackMessage := message[lenBufferLen:]
return parseMessageTypeFromMessagePack(msgPackMessage)
}
func parseMessageTypeFromMessagePack(messagePack []byte) (int8, string, bool) {
msgPackBuffer := bytes.NewBuffer(messagePack)
dec := msgpack.NewDecoder(msgPackBuffer)
value, err := dec.DecodeSlice()
if value == nil || err != nil {
return 0, "", false
}
if len(value) < 5 {
websocketLog.Warn("Invalid message received, length too short")
return 0, "", false
}
value, success := value[4].([]interface{})
if len(value) < 1 || !success {
websocketLog.Warn("Invalid message received, length too short")
return 0, "", false
}
value1, success := value[0].(map[string]interface{})
if !success {
websocketLog.Warn("Invalid message received, value is not a map")
return 0, "", false
}
if _, ok := value1["Type"]; !ok {
websocketLog.Warn("Invalid message received, no type")
return 0, "", false
}
messagePayloadType, success := value1["Type"].(int8)
if !success {
websocketLog.Warn("Invalid message received, type is not an int")
return 0, "", false
}
payload, success := value1["Payload"].(map[string]interface{})
if !success {
return messagePayloadType, "", true
}
if _, ok := payload["Id"]; !ok {
websocketLog.Warn("Invalid message received, no id")
return 0, "", false
}
return messagePayloadType, payload["Id"].(string), true
}