diff --git a/agent/actions/login.go b/agent/actions/login.go index 21ca8fd..903918f 100644 --- a/agent/actions/login.go +++ b/agent/actions/login.go @@ -28,7 +28,15 @@ func handleLogin(msg ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, cal req := msg.ParsedPayload().(ipc.DoLoginRequest) ctx := context.Background() - token, masterKey, masterpasswordHash, err := bitwarden.LoginWithMasterpassword(ctx, req.Email, cfg, vault) + var token bitwarden.LoginResponseToken + var masterKey crypto.MasterKey + var masterpasswordHash string + + if req.Passwordless { + token, masterKey, masterpasswordHash, err = bitwarden.LoginWithDevice(ctx, req.Email, cfg, vault) + } else { + token, masterKey, masterpasswordHash, err = bitwarden.LoginWithMasterpassword(ctx, req.Email, cfg, vault) + } if err != nil { var payload = ipc.ActionResponse{ Success: false, diff --git a/agent/bitwarden/auth.go b/agent/bitwarden/auth.go index 09101e8..23f07fc 100644 --- a/agent/bitwarden/auth.go +++ b/agent/bitwarden/auth.go @@ -4,16 +4,20 @@ import ( "bytes" "context" "crypto/sha256" + "encoding/base64" "encoding/json" "fmt" + "math/rand" "net/url" "runtime" "strconv" "strings" + "time" "github.com/LlamaNite/llamalog" "github.com/awnumar/memguard" "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/systemauth" "github.com/quexten/goldwarden/agent/vault" @@ -99,11 +103,11 @@ func LoginWithMasterpassword(ctx context.Context, email string, cfg *config.Conf err = authenticatedHTTPPost(ctx, cfg.ConfigFile.IdentityUrl+"/connect/token", &loginResponseToken, values) errsc, ok := err.(*errStatusCode) if ok && bytes.Contains(errsc.body, []byte("TwoFactor")) { - var twoFactor TwoFactorResponse + var twoFactor twofactor.TwoFactorResponse if err := json.Unmarshal(errsc.body, &twoFactor); err != nil { return LoginResponseToken{}, crypto.MasterKey{}, "", err } - provider, token, err := performSecondFactor(&twoFactor, cfg) + provider, token, err := twofactor.PerformSecondFactor(&twoFactor, cfg) if err != nil { return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not obtain two-factor auth token: %v", err) } @@ -126,6 +130,62 @@ func LoginWithMasterpassword(ctx context.Context, email string, cfg *config.Conf return loginResponseToken, masterKey, hashedPassword, nil } +func LoginWithDevice(ctx context.Context, email string, cfg *config.Config, vault *vault.Vault) (LoginResponseToken, crypto.MasterKey, string, error) { + timeout := 120 * time.Second + + // 25 random letters & numbers + alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + accessCode := "" + for i := 0; i < 25; i++ { + accessCode += string(alphabet[rand.Intn(len(alphabet))]) + } + publicKey, err := crypto.GenerateAsymmetric() + if err != nil { + return LoginResponseToken{}, crypto.MasterKey{}, "", err + } + data, err := CreateAuthRequest(ctx, accessCode, cfg.ConfigFile.DeviceUUID, email, base64.StdEncoding.EncodeToString(publicKey.PublicBytes()), cfg) + + timeoutChan := make(chan bool) + go func() { + time.Sleep(timeout) + timeoutChan <- true + }() + + for { + select { + case <-timeoutChan: + return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("timed out waiting for device to be authorized") + default: + authRequestData, err := GetAuthRequest(ctx, data.ID, cfg) + if err != nil { + log.Error("Could not get auth request: ", err) + } + if authRequestData.RequestApproved { + masterKey, err := crypto.DecryptWithAsymmetric([]byte(authRequestData.Key), publicKey) + masterPasswordHash, err := crypto.DecryptWithAsymmetric([]byte(authRequestData.MasterPasswordHash), publicKey) + values := urlValues( + "grant_type", "password", + "username", email, + "password", string(accessCode), + "authRequest", authRequestData.ID, + "scope", loginScope, + "client_id", "connector", + "deviceType", deviceType(), + "deviceName", deviceName, + "deviceIdentifier", cfg.ConfigFile.DeviceUUID, + ) + + var loginResponseToken LoginResponseToken + err = authenticatedHTTPPost(ctx, cfg.ConfigFile.IdentityUrl+"/connect/token", &loginResponseToken, values) + if err != nil { + return LoginResponseToken{}, crypto.MasterKey{}, "", err + } + return loginResponseToken, crypto.MasterKeyFromBytes(masterKey), string(masterPasswordHash), nil + } + } + } +} + func RefreshToken(ctx context.Context, cfg *config.Config) bool { authLog.Info("Refreshing token") @@ -157,3 +217,16 @@ func RefreshToken(ctx context.Context, cfg *config.Config) bool { return true } + +func urlValues(pairs ...string) url.Values { + if len(pairs)%2 != 0 { + panic("pairs must be of even length") + } + vals := make(url.Values) + for i := 0; i < len(pairs); i += 2 { + vals.Set(pairs[i], pairs[i+1]) + } + return vals +} + +var b64enc = base64.StdEncoding.Strict() diff --git a/agent/bitwarden/crypto/crypto.go b/agent/bitwarden/crypto/crypto.go index 4b45501..42437e5 100644 --- a/agent/bitwarden/crypto/crypto.go +++ b/agent/bitwarden/crypto/crypto.go @@ -5,7 +5,9 @@ import ( "crypto/cipher" "crypto/hmac" cryptorand "crypto/rand" + "crypto/rsa" "crypto/sha256" + "crypto/x509" "encoding/base64" "fmt" "io" @@ -53,6 +55,20 @@ func AssymmetricEncryptionKeyFromBytes(key []byte) (AsymmetricEncryptionKey, err return AsymmetricEncryptionKey{k}, nil } +func (key AsymmetricEncryptionKey) PublicBytes() []byte { + buffer, err := key.encKey.Open() + if err != nil { + panic(err) + } + privateKey, err := x509.ParsePKCS8PrivateKey(buffer.Bytes()) + pub := (privateKey.(*rsa.PrivateKey)).Public() + publicKeyBytes, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + panic(err) + } + return publicKeyBytes +} + func isMacValid(message, messageMAC, key []byte) bool { mac := hmac.New(sha256.New, key) mac.Write(message) diff --git a/agent/bitwarden/crypto/encstring.go b/agent/bitwarden/crypto/encstring.go index 7544136..b25cb1c 100644 --- a/agent/bitwarden/crypto/encstring.go +++ b/agent/bitwarden/crypto/encstring.go @@ -210,6 +210,20 @@ func EncryptWith(data []byte, typ EncStringType, key SymmetricEncryptionKey) (En return s, nil } +func GenerateAsymmetric() (AsymmetricEncryptionKey, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return AsymmetricEncryptionKey{}, err + } + + encKey, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return AsymmetricEncryptionKey{}, err + } + + return AssymmetricEncryptionKeyFromBytes(encKey) +} + func DecryptWithAsymmetric(s []byte, asymmetrickey AsymmetricEncryptionKey) ([]byte, error) { key, err := asymmetrickey.encKey.Open() if err != nil { diff --git a/agent/bitwarden/crypto/kdf.go b/agent/bitwarden/crypto/kdf.go index 83b4c7d..10c4bad 100644 --- a/agent/bitwarden/crypto/kdf.go +++ b/agent/bitwarden/crypto/kdf.go @@ -60,3 +60,7 @@ func DeriveMasterKey(password memguard.LockedBuffer, email string, kdfConfig KDF return MasterKey{memguard.NewEnclave(key)}, nil } + +func MasterKeyFromBytes(key []byte) MasterKey { + return MasterKey{memguard.NewEnclave(key)} +} diff --git a/agent/bitwarden/paswordless.go b/agent/bitwarden/paswordless.go index d59f3ff..6a59647 100644 --- a/agent/bitwarden/paswordless.go +++ b/agent/bitwarden/paswordless.go @@ -9,6 +9,14 @@ import ( "github.com/quexten/goldwarden/agent/config" ) +type CreateAuthRequestData struct { + AccessCode string `json:"accessCode"` + DeviceIdentifier string `json:"deviceIdentifier"` + Email string `json:"email"` + PublicKey string `json:"publicKey"` + Type int `json:"type"` +} + type AuthRequestData struct { CreationDate time.Time `json:"creationDate"` ID string `json:"id"` @@ -80,3 +88,20 @@ func CreateAuthResponse(ctx context.Context, authRequest AuthRequestData, keyrin }) return authRequestResponse, err } + +func CreateAuthRequest(ctx context.Context, code string, deviceIdentifier string, email string, publicKey string, config *config.Config) (AuthRequestData, error) { + var authrequestData AuthRequestData + err := authenticatedHTTPPost(ctx, config.ConfigFile.ApiUrl+"/auth-requests/", &authrequestData, &CreateAuthRequestData{ + AccessCode: code, + DeviceIdentifier: deviceIdentifier, + Email: email, + PublicKey: publicKey, + Type: 0, + }) + + if err != nil { + return AuthRequestData{}, err + } else { + return authrequestData, nil + } +} diff --git a/agent/bitwarden/twofactor.go b/agent/bitwarden/twofactor/fido2twofactor.go similarity index 52% rename from agent/bitwarden/twofactor.go rename to agent/bitwarden/twofactor/fido2twofactor.go index 3a33a2d..1cdf7e3 100644 --- a/agent/bitwarden/twofactor.go +++ b/agent/bitwarden/twofactor/fido2twofactor.go @@ -1,19 +1,21 @@ -package bitwarden +//go:build !nofido2 + +package twofactor import ( "crypto/sha256" "encoding/base64" "encoding/json" "errors" - "fmt" "net/url" - "strconv" "github.com/keys-pub/go-libfido2" "github.com/quexten/goldwarden/agent/config" "github.com/quexten/goldwarden/agent/systemauth" ) +const isFido2Enabled = true + type Fido2Response struct { Id string `json:"id"` RawId string `json:"rawId"` @@ -50,7 +52,7 @@ func Fido2TwoFactor(challengeB64 string, credentials []string, config *config.Co for i, cred := range credentials { decodedPublicKey, err := base64.RawURLEncoding.DecodeString(cred) if err != nil { - websocketLog.Fatal(err.Error()) + twofactorLog.Fatal(err.Error()) } creds[i] = decodedPublicKey } @@ -61,7 +63,7 @@ func Fido2TwoFactor(challengeB64 string, credentials []string, config *config.Co pin, err := systemauth.GetPassword("Fido2 PIN", "Enter your token's PIN") if err != nil { - websocketLog.Fatal(err.Error()) + twofactorLog.Fatal(err.Error()) } assertion, err := device.Assertion( @@ -101,71 +103,3 @@ func Fido2TwoFactor(challengeB64 string, credentials []string, config *config.Co respjson, err := json.Marshal(resp) return string(respjson), nil } - -func performSecondFactor(resp *TwoFactorResponse, cfg *config.Config) (TwoFactorProvider, []byte, error) { - if resp.TwoFactorProviders2[WebAuthn] != nil { - chall := resp.TwoFactorProviders2[WebAuthn]["challenge"].(string) - - var creds []string - for _, credential := range resp.TwoFactorProviders2[WebAuthn]["allowCredentials"].([]interface{}) { - publicKey := credential.(map[string]interface{})["id"].(string) - creds = append(creds, publicKey) - } - - result, err := Fido2TwoFactor(chall, creds, cfg) - if err != nil { - return WebAuthn, nil, err - } - return WebAuthn, []byte(result), err - } - if resp.TwoFactorProviders2[Authenticator] != nil { - token, err := systemauth.GetPassword("Authenticator Second Factor", "Enter your two-factor auth code") - return Authenticator, []byte(token), err - } - if resp.TwoFactorProviders2[Email] != nil { - token, err := systemauth.GetPassword("Email Second Factor", "Enter your two-factor auth code") - return Email, []byte(token), err - } - - return Authenticator, []byte{}, errors.New("no second factor available") -} - -type TwoFactorProvider int - -const ( - Authenticator TwoFactorProvider = 0 - Email TwoFactorProvider = 1 - Duo TwoFactorProvider = 2 //Not supported - YubiKey TwoFactorProvider = 3 //Not supported - U2f TwoFactorProvider = 4 //Not supported - Remember TwoFactorProvider = 5 //Not supported - OrganizationDuo TwoFactorProvider = 6 //Not supported - WebAuthn TwoFactorProvider = 7 - _TwoFactorProviderMax = 8 //Not supported -) - -func (t *TwoFactorProvider) UnmarshalText(text []byte) error { - i, err := strconv.Atoi(string(text)) - if err != nil || i < 0 || i >= _TwoFactorProviderMax { - return fmt.Errorf("invalid two-factor auth provider: %q", text) - } - *t = TwoFactorProvider(i) - return nil -} - -type TwoFactorResponse struct { - TwoFactorProviders2 map[TwoFactorProvider]map[string]interface{} -} - -func urlValues(pairs ...string) url.Values { - if len(pairs)%2 != 0 { - panic("pairs must be of even length") - } - vals := make(url.Values) - for i := 0; i < len(pairs); i += 2 { - vals.Set(pairs[i], pairs[i+1]) - } - return vals -} - -var b64enc = base64.StdEncoding.Strict() diff --git a/agent/bitwarden/twofactor/fido2twofactorunsupported.go b/agent/bitwarden/twofactor/fido2twofactorunsupported.go new file mode 100644 index 0000000..4bbd1f7 --- /dev/null +++ b/agent/bitwarden/twofactor/fido2twofactorunsupported.go @@ -0,0 +1,14 @@ +//go:build nofido2 + +package twofactor + +import ( + "errors" + "github.com/quexten/goldwarden/agent/config" +) + +const isFido2Enabled = false + +func Fido2TwoFactor(challengeB64 string, credentials []string, config *config.Config) (string, error) { + return "", errors.New("Fido2 is not enabled") +} diff --git a/agent/bitwarden/twofactor/twofactor.go b/agent/bitwarden/twofactor/twofactor.go new file mode 100644 index 0000000..1a0efd7 --- /dev/null +++ b/agent/bitwarden/twofactor/twofactor.go @@ -0,0 +1,72 @@ +package twofactor + +import ( + "errors" + "fmt" + "strconv" + + "github.com/LlamaNite/llamalog" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/systemauth" +) + +var twofactorLog = llamalog.NewLogger("Goldwarden", "TwoFactor") + +func PerformSecondFactor(resp *TwoFactorResponse, cfg *config.Config) (TwoFactorProvider, []byte, error) { + if resp.TwoFactorProviders2[WebAuthn] != nil { + if isFido2Enabled { + chall := resp.TwoFactorProviders2[WebAuthn]["challenge"].(string) + + var creds []string + for _, credential := range resp.TwoFactorProviders2[WebAuthn]["allowCredentials"].([]interface{}) { + publicKey := credential.(map[string]interface{})["id"].(string) + creds = append(creds, publicKey) + } + + result, err := Fido2TwoFactor(chall, creds, cfg) + if err != nil { + return WebAuthn, nil, err + } + return WebAuthn, []byte(result), err + } else { + twofactorLog.Warn("WebAuthn is enabled for the account but goldwarden is not compiled with FIDO2 support") + } + } + if resp.TwoFactorProviders2[Authenticator] != nil { + token, err := systemauth.GetPassword("Authenticator Second Factor", "Enter your two-factor auth code") + return Authenticator, []byte(token), err + } + if resp.TwoFactorProviders2[Email] != nil { + token, err := systemauth.GetPassword("Email Second Factor", "Enter your two-factor auth code") + return Email, []byte(token), err + } + + return Authenticator, []byte{}, errors.New("no second factor available") +} + +type TwoFactorProvider int + +const ( + Authenticator TwoFactorProvider = 0 + Email TwoFactorProvider = 1 + Duo TwoFactorProvider = 2 //Not supported + YubiKey TwoFactorProvider = 3 //Not supported + U2f TwoFactorProvider = 4 //Not supported + Remember TwoFactorProvider = 5 //Not supported + OrganizationDuo TwoFactorProvider = 6 //Not supported + WebAuthn TwoFactorProvider = 7 + _TwoFactorProviderMax = 8 //Not supported +) + +func (t *TwoFactorProvider) UnmarshalText(text []byte) error { + i, err := strconv.Atoi(string(text)) + if err != nil || i < 0 || i >= _TwoFactorProviderMax { + return fmt.Errorf("invalid two-factor auth provider: %q", text) + } + *t = TwoFactorProvider(i) + return nil +} + +type TwoFactorResponse struct { + TwoFactorProviders2 map[TwoFactorProvider]map[string]interface{} +} diff --git a/agent/systemauth/pinentry.go b/agent/systemauth/pinentry.go index 6f8c761..feebcb5 100644 --- a/agent/systemauth/pinentry.go +++ b/agent/systemauth/pinentry.go @@ -2,10 +2,13 @@ package systemauth import ( "errors" + "os" "github.com/twpayne/go-pinentry" ) +var authDisabled = false + func GetPassword(title string, description string) (string, error) { client, err := pinentry.NewClient( pinentry.WithBinaryNameFromGnuPGAgentConf(), @@ -37,6 +40,11 @@ func GetPassword(title string, description string) (string, error) { } func GetApproval(title string, description string) (bool, error) { + if authDisabled { + log.Info("Skipping approval because system auth is disabled") + return true, nil + } + client, err := pinentry.NewClient( pinentry.WithBinaryNameFromGnuPGAgentConf(), pinentry.WithGPGTTY(), @@ -62,3 +70,10 @@ func GetApproval(title string, description string) (bool, error) { return true, nil } } + +func init() { + envAuthDisabled := os.Getenv("GOLDWARDEN_SYSTEM_AUTH_DISABLED") + if envAuthDisabled == "true" { + authDisabled = true + } +} diff --git a/agent/systemauth/polkitbiometrics.go b/agent/systemauth/polkitbiometrics.go index 5f42dca..9a31392 100644 --- a/agent/systemauth/polkitbiometrics.go +++ b/agent/systemauth/polkitbiometrics.go @@ -76,6 +76,9 @@ func (a Approval) String() string { func CheckBiometrics(approvalType Approval) bool { log.Info("Checking biometrics for %s", approvalType.String()) + if authDisabled { + return true + } authority, err := polkit.NewAuthority() if err != nil { diff --git a/agent/agent.go b/agent/unixsocketagent.go similarity index 81% rename from agent/agent.go rename to agent/unixsocketagent.go index 579a1c4..5f59c82 100644 --- a/agent/agent.go +++ b/agent/unixsocketagent.go @@ -102,7 +102,7 @@ type AgentState struct { config *config.ConfigFile } -func StartUnixAgent(path string) error { +func StartUnixAgent(path string, websocket bool, sshAgent bool) error { ctx := context.Background() // check if exists @@ -134,33 +134,37 @@ func StartUnixAgent(path string) error { } disableDumpable() - go bitwarden.RunWebsocketDaemon(ctx, vault, &cfg) + if websocket { + go bitwarden.RunWebsocketDaemon(ctx, vault, &cfg) + } - vaultAgent := ssh.NewVaultAgent(vault) - vaultAgent.SetUnlockRequestAction(func() bool { - err := cfg.TryUnlock(vault) - if err == nil { - token, err := cfg.GetToken() + if sshAgent { + vaultAgent := ssh.NewVaultAgent(vault) + vaultAgent.SetUnlockRequestAction(func() bool { + err := cfg.TryUnlock(vault) if err == nil { - if token.AccessToken != "" { - bitwarden.RefreshToken(ctx, &cfg) - userSymmetricKey, err := cfg.GetUserSymmetricKey() - if err != nil { - fmt.Println(err) - } - protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(userSymmetricKey) + token, err := cfg.GetToken() + if err == nil { + if token.AccessToken != "" { + bitwarden.RefreshToken(ctx, &cfg) + userSymmetricKey, err := cfg.GetUserSymmetricKey() + if err != nil { + fmt.Println(err) + } + protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(userSymmetricKey) - err = bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, &protectedUserSymetricKey, true) - if err != nil { - fmt.Println(err) + err = bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, &protectedUserSymetricKey, true) + if err != nil { + fmt.Println(err) + } } } + return true } - return true - } - return false - }) - go vaultAgent.Serve() + return false + }) + go vaultAgent.Serve() + } go func() { for { diff --git a/agent/virtualagent.go b/agent/virtualagent.go new file mode 100644 index 0000000..b3174f9 --- /dev/null +++ b/agent/virtualagent.go @@ -0,0 +1,135 @@ +package agent + +import ( + "context" + "encoding/json" + "fmt" + "os/user" + "time" + + "github.com/quexten/goldwarden/agent/actions" + "github.com/quexten/goldwarden/agent/bitwarden" + "github.com/quexten/goldwarden/agent/bitwarden/crypto" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/sockets" + "github.com/quexten/goldwarden/agent/vault" + "github.com/quexten/goldwarden/ipc" +) + +func writeErrorToLog(err error) { + log.Error(err.Error()) +} + +func serveVirtualAgent(recv chan []byte, send chan []byte, ctx context.Context, vault *vault.Vault, cfg *config.Config) { + for { + data := <-recv + + var msg ipc.IPCMessage + err := json.Unmarshal(data, &msg) + if err != nil { + writeErrorToLog(err) + continue + } + + responseBytes := []byte{} + if action, actionFound := actions.AgentActionsRegistry.Get(msg.Type); actionFound { + user, _ := user.Current() + process := "goldwarden" + parent := "SINGLE_PROC_MODE" + grandparent := "SINGLE_PROC_MODE" + callingContext := sockets.CallingContext{ + UserName: user.Name, + ProcessName: process, + ParentProcessName: parent, + GrandParentProcessName: grandparent, + } + payload, err := action(msg, cfg, vault, callingContext) + if err != nil { + writeErrorToLog(err) + continue + } + responseBytes, err = json.Marshal(payload) + if err != nil { + writeErrorToLog(err) + continue + } + } else { + payload := ipc.ActionResponse{ + Success: false, + Message: "Action not found", + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + writeErrorToLog(err) + continue + } + responseBytes = payloadBytes + } + + send <- responseBytes + } +} + +func StartVirtualAgent() (chan []byte, chan []byte) { + ctx := context.Background() + + // check if exists + keyring := crypto.NewKeyring(nil) + var vault = vault.NewVault(&keyring) + cfg, err := config.ReadConfig() + if err != nil { + var cfg = config.DefaultConfig() + cfg.WriteConfig() + } + if !cfg.IsLocked() { + log.Warn("Config is not locked. SET A PIN!!") + token, err := cfg.GetToken() + if err == nil { + if token.AccessToken != "" { + bitwarden.RefreshToken(ctx, &cfg) + userSymmetricKey, err := cfg.GetUserSymmetricKey() + if err != nil { + fmt.Println(err) + } + protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(userSymmetricKey) + + err = bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, &protectedUserSymetricKey, true) + if err != nil { + fmt.Println(err) + } + } + } + } + disableDumpable() + go func() { + for { + time.Sleep(TokenRefreshInterval) + if !cfg.IsLocked() { + bitwarden.RefreshToken(ctx, &cfg) + } + } + }() + + go func() { + for { + time.Sleep(FullSyncInterval) + if !cfg.IsLocked() { + token, err := cfg.GetToken() + if err != nil { + log.Warn("Could not get token: %s", err.Error()) + continue + } + + bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token), vault, &cfg, nil, false) + } + } + }() + + recv := make(chan []byte) + send := make(chan []byte) + + go func() { + go serveVirtualAgent(recv, send, ctx, vault, &cfg) + }() + return recv, send +} diff --git a/autofill/autofill.go b/autofill/autofill.go index d664d6e..981e1a8 100644 --- a/autofill/autofill.go +++ b/autofill/autofill.go @@ -1,4 +1,4 @@ -//go:build autofill +//go:build !noautofill package autofill diff --git a/autofill/gioAutofillDialog.go b/autofill/gioAutofillDialog.go index 359241b..f3ce170 100644 --- a/autofill/gioAutofillDialog.go +++ b/autofill/gioAutofillDialog.go @@ -1,4 +1,4 @@ -//go:build autofill +//go:build !noautofill package autofill diff --git a/browserbiometrics/protocol.go b/browserbiometrics/protocol.go index 7f3203f..ea7793f 100644 --- a/browserbiometrics/protocol.go +++ b/browserbiometrics/protocol.go @@ -99,7 +99,7 @@ func handlePayloadMessage(msg PayloadMessage, appID string) { case "biometricUnlock": logging.Debugf("Biometric unlock requested") // logging.Debugf("Biometrics authorized: %t", isAuthorized) - result, err := client.SendToAgent(ipc.GetBiometricsKeyRequest{}) + result, err := client.NewUnixSocketClient().SendToAgent(ipc.GetBiometricsKeyRequest{}) if err != nil { logging.Errorf("Unable to send message to agent: %s", err.Error()) return diff --git a/client/client.go b/client/client.go index 084fe56..c0ce142 100644 --- a/client/client.go +++ b/client/client.go @@ -1,56 +1,5 @@ package client -import ( - "io" - "log" - "net" - "os" - - "github.com/quexten/goldwarden/ipc" -) - -const READ_BUFFER = 1 * 1024 * 1024 // 1MB - -func reader(r io.Reader) interface{} { - buf := make([]byte, READ_BUFFER) - for { - n, err := r.Read(buf[:]) - if err != nil { - return nil - } - message, err := ipc.UnmarshalJSON(buf[0:n]) - if err != nil { - panic(err) - } - return message - } -} - -func SendToAgent(request interface{}) (interface{}, error) { - home, err := os.UserHomeDir() - if err != nil { - panic(err) - } - - c, err := net.Dial("unix", home+"/.goldwarden.sock") - if err != nil { - return nil, err - } - defer c.Close() - - message, err := ipc.IPCMessageFromPayload(request) - if err != nil { - panic(err) - } - messageJson, err := message.MarshallToJson() - if err != nil { - panic(err) - } - - _, err = c.Write(messageJson) - if err != nil { - log.Fatal("write error:", err) - } - result := reader(c) - return result.(ipc.IPCMessage).ParsedPayload(), nil +type Client interface { + SendToAgent(request interface{}) (interface{}, error) } diff --git a/client/unixsocketclient.go b/client/unixsocketclient.go new file mode 100644 index 0000000..d8065a2 --- /dev/null +++ b/client/unixsocketclient.go @@ -0,0 +1,63 @@ +package client + +import ( + "io" + "log" + "net" + "os" + + "github.com/quexten/goldwarden/ipc" +) + +const READ_BUFFER = 1 * 1024 * 1024 // 1MB + +type UnixSocketClient struct { +} + +func NewUnixSocketClient() UnixSocketClient { + return UnixSocketClient{} +} + +func reader(r io.Reader) interface{} { + buf := make([]byte, READ_BUFFER) + for { + n, err := r.Read(buf[:]) + if err != nil { + return nil + } + message, err := ipc.UnmarshalJSON(buf[0:n]) + if err != nil { + panic(err) + } + return message + } +} + +func (client UnixSocketClient) SendToAgent(request interface{}) (interface{}, error) { + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + + c, err := net.Dial("unix", home+"/.goldwarden.sock") + if err != nil { + return nil, err + } + defer c.Close() + + message, err := ipc.IPCMessageFromPayload(request) + if err != nil { + panic(err) + } + messageJson, err := message.MarshallToJson() + if err != nil { + panic(err) + } + + _, err = c.Write(messageJson) + if err != nil { + log.Fatal("write error:", err) + } + result := reader(c) + return result.(ipc.IPCMessage).ParsedPayload(), nil +} diff --git a/client/virtualclient.go b/client/virtualclient.go new file mode 100644 index 0000000..25b7b50 --- /dev/null +++ b/client/virtualclient.go @@ -0,0 +1,42 @@ +package client + +import ( + "github.com/quexten/goldwarden/ipc" +) + +func NewVirtualClient(recv chan []byte, send chan []byte) VirtualClient { + return VirtualClient{ + recv, + send, + } +} + +type VirtualClient struct { + recv chan []byte + send chan []byte +} + +func virtualReader(recv chan []byte) interface{} { + for { + message, err := ipc.UnmarshalJSON(<-recv) + if err != nil { + panic(err) + } + return message + } +} + +func (client VirtualClient) SendToAgent(request interface{}) (interface{}, error) { + message, err := ipc.IPCMessageFromPayload(request) + if err != nil { + panic(err) + } + messageJson, err := message.MarshallToJson() + if err != nil { + panic(err) + } + + client.send <- messageJson + result := virtualReader(client.recv) + return result.(ipc.IPCMessage).ParsedPayload(), nil +} diff --git a/cmd/config.go b/cmd/config.go index 295919e..cebb641 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/quexten/goldwarden/client" "github.com/quexten/goldwarden/ipc" "github.com/spf13/cobra" ) @@ -19,7 +18,7 @@ var setApiUrlCmd = &cobra.Command{ request := ipc.SetApiURLRequest{} request.Value = url - result, err := client.SendToAgent(request) + result, err := commandClient.SendToAgent(request) if err != nil { println("Error: " + err.Error()) println("Is the daemon running?") @@ -53,7 +52,7 @@ var setIdentityURLCmd = &cobra.Command{ request := ipc.SetIdentityURLRequest{} request.Value = url - result, err := client.SendToAgent(request) + result, err := commandClient.SendToAgent(request) if err != nil { println("Error: " + err.Error()) println("Is the daemon running?") diff --git a/cmd/daemonize.go b/cmd/daemonize.go index 05cd890..3335ef8 100644 --- a/cmd/daemonize.go +++ b/cmd/daemonize.go @@ -15,6 +15,16 @@ var daemonizeCmd = &cobra.Command{ Long: `Starts the agent as a daemon. The agent will run in the background and will run in the background until it is stopped.`, Run: func(cmd *cobra.Command, args []string) { + websocketDisabled := os.Getenv("GOLDWARDEN_WEBSOCKET_DISABLED") == "true" + if websocketDisabled { + println("Websocket disabled") + } + + sshDisabled := os.Getenv("GOLDWARDEN_SSH_DISABLED") == "true" + if sshDisabled { + println("SSH agent disabled") + } + go func() { signalChannel := make(chan os.Signal, 1) signal.Notify(signalChannel, os.Interrupt) @@ -25,7 +35,7 @@ var daemonizeCmd = &cobra.Command{ if err != nil { panic(err) } - err = agent.StartUnixAgent(home + "/.goldwarden.sock") + err = agent.StartUnixAgent(home+"/.goldwarden.sock", websocketDisabled, sshDisabled) if err != nil { panic(err) } diff --git a/cmd/login.go b/cmd/login.go index 1bc7fed..28f299c 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -4,7 +4,6 @@ Copyright © 2023 NAME HERE package cmd import ( - "github.com/quexten/goldwarden/client" "github.com/quexten/goldwarden/ipc" "github.com/spf13/cobra" ) @@ -18,8 +17,10 @@ var loginCmd = &cobra.Command{ request := ipc.DoLoginRequest{} email, _ := cmd.Flags().GetString("email") request.Email = email + passwordless, _ := cmd.Flags().GetBool("passwordless") + request.Passwordless = passwordless - result, err := client.SendToAgent(request) + result, err := commandClient.SendToAgent(request) if err != nil { println("Error: " + err.Error()) println("Is the daemon running?") @@ -43,4 +44,5 @@ func init() { vaultCmd.AddCommand(loginCmd) loginCmd.PersistentFlags().String("email", "", "") loginCmd.MarkFlagRequired("email") + loginCmd.PersistentFlags().Bool("passwordless", false, "") } diff --git a/cmd/pin.go b/cmd/pin.go index e0f006f..562cc55 100644 --- a/cmd/pin.go +++ b/cmd/pin.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/quexten/goldwarden/client" "github.com/quexten/goldwarden/ipc" "github.com/spf13/cobra" ) @@ -17,7 +16,7 @@ var setPinCmd = &cobra.Command{ Short: "Set a new pin", Long: `Set a new pin. The pin is used to unlock the vault.`, Run: func(cmd *cobra.Command, args []string) { - result, err := client.SendToAgent(ipc.UpdateVaultPINRequest{}) + result, err := commandClient.SendToAgent(ipc.UpdateVaultPINRequest{}) if err != nil { println("Error: " + err.Error()) println("Is the daemon running?") @@ -42,7 +41,7 @@ var pinStatusCmd = &cobra.Command{ Short: "Check if a pin is set", Long: `Check if a pin is set. The pin is used to unlock the vault.`, Run: func(cmd *cobra.Command, args []string) { - result, err := client.SendToAgent(ipc.GetVaultPINRequest{}) + result, err := commandClient.SendToAgent(ipc.GetVaultPINRequest{}) if err != nil { println("Error: " + err.Error()) println("Is the daemon running?") diff --git a/cmd/root.go b/cmd/root.go index 2c39673..f522bcd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,9 +3,13 @@ package cmd import ( "os" + "github.com/quexten/goldwarden/agent" + "github.com/quexten/goldwarden/client" "github.com/spf13/cobra" ) +var commandClient client.Client + var rootCmd = &cobra.Command{ Use: "goldwarden", Short: "OS level integration for Bitwarden", @@ -22,5 +26,13 @@ func Execute() { } func init() { + goldwardenSingleProcess := os.Getenv("GOLDWARDEN_SINGLE_PROCESS") + if goldwardenSingleProcess == "true" { + recv, send := agent.StartVirtualAgent() + commandClient = client.NewVirtualClient(send, recv) + } else { + commandClient = client.NewUnixSocketClient() + } + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/run.go b/cmd/run.go index f206c6d..cf2ee2a 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -7,7 +7,6 @@ import ( "os" "os/exec" - "github.com/quexten/goldwarden/client" "github.com/quexten/goldwarden/ipc" "github.com/spf13/cobra" ) @@ -29,7 +28,7 @@ var runCmd = &cobra.Command{ env := []string{} - result, err := client.SendToAgent(ipc.GetCLICredentialsRequest{ + result, err := commandClient.SendToAgent(ipc.GetCLICredentialsRequest{ ApplicationName: executable, }) if err != nil { diff --git a/cmd/ssh.go b/cmd/ssh.go index b6901b3..6265c1c 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -7,7 +7,6 @@ import ( "fmt" "github.com/atotto/clipboard" - "github.com/quexten/goldwarden/client" "github.com/quexten/goldwarden/ipc" "github.com/spf13/cobra" ) @@ -31,7 +30,7 @@ var sshAddCmd = &cobra.Command{ name, _ := cmd.Flags().GetString("name") copyToClipboard, _ := cmd.Flags().GetBool("clipboard") - result, err := client.SendToAgent(ipc.CreateSSHKeyRequest{ + result, err := commandClient.SendToAgent(ipc.CreateSSHKeyRequest{ Name: name, }) if err != nil { @@ -61,7 +60,7 @@ var listSSHCmd = &cobra.Command{ Short: "Lists all SSH keys in your vault", Long: `Lists all SSH keys in your vault.`, Run: func(cmd *cobra.Command, args []string) { - result, err := client.SendToAgent(ipc.GetSSHKeysRequest{}) + result, err := commandClient.SendToAgent(ipc.GetSSHKeysRequest{}) if err != nil { println("Error: " + err.Error()) println("Is the daemon running?") diff --git a/cmd/vault.go b/cmd/vault.go index 9a734a4..4936f31 100644 --- a/cmd/vault.go +++ b/cmd/vault.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/quexten/goldwarden/client" "github.com/quexten/goldwarden/ipc" "github.com/spf13/cobra" ) @@ -19,7 +18,7 @@ var unlockCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { request := ipc.UnlockVaultRequest{} - result, err := client.SendToAgent(request) + result, err := commandClient.SendToAgent(request) if err != nil { println("Error: " + err.Error()) println("Is the daemon running?") @@ -46,7 +45,7 @@ var lockCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { request := ipc.LockVaultRequest{} - result, err := client.SendToAgent(request) + result, err := commandClient.SendToAgent(request) if err != nil { println("Error: " + err.Error()) println("Is the daemon running?") @@ -73,7 +72,7 @@ var purgeCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { request := ipc.WipeVaultRequest{} - result, err := client.SendToAgent(request) + result, err := commandClient.SendToAgent(request) if err != nil { println("Error: " + err.Error()) println("Is the daemon running?") diff --git a/ipc/ipc.go b/ipc/ipc.go index d40f2af..25b55c4 100644 --- a/ipc/ipc.go +++ b/ipc/ipc.go @@ -367,8 +367,9 @@ func IPCMessageFromPayload(payload interface{}) (IPCMessage, error) { } type DoLoginRequest struct { - Email string `json:"email"` - Password string `json:"password"` + Email string `json:"email"` + Password string `json:"password"` + Passwordless bool `json:"passwordless"` } type LockVaultRequest struct {