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"
|
2024-02-04 01:15:26 +01:00
|
|
|
|
"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()
|
2024-03-15 16:22:45 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
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...")
|
2023-12-26 21:52:49 +01:00
|
|
|
|
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
|
|
|
|
|
2023-12-28 01:05:15 +01: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
|
|
|
|
|
}
|
2023-12-26 21:52:49 +01:00
|
|
|
|
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)
|
|
|
|
|
}
|
2023-12-26 21:52:49 +01:00
|
|
|
|
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)
|
|
|
|
|
}
|
2023-12-26 21:52:49 +01:00
|
|
|
|
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)
|
|
|
|
|
|
2024-02-04 01:15:26 +01:00
|
|
|
|
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
|
2023-12-26 21:52:49 +01:00
|
|
|
|
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
|
|
|
|
|
}
|