Login with device & virtual ipc

This commit is contained in:
Bernd Schoolmann 2023-08-21 13:52:06 +02:00
parent 187ee16adf
commit 6e3859dd43
No known key found for this signature in database
28 changed files with 565 additions and 174 deletions

View File

@ -28,7 +28,15 @@ func handleLogin(msg ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, cal
req := msg.ParsedPayload().(ipc.DoLoginRequest) req := msg.ParsedPayload().(ipc.DoLoginRequest)
ctx := context.Background() 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 { if err != nil {
var payload = ipc.ActionResponse{ var payload = ipc.ActionResponse{
Success: false, Success: false,

View File

@ -4,16 +4,20 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/rand"
"net/url" "net/url"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/LlamaNite/llamalog" "github.com/LlamaNite/llamalog"
"github.com/awnumar/memguard" "github.com/awnumar/memguard"
"github.com/quexten/goldwarden/agent/bitwarden/crypto" "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/config"
"github.com/quexten/goldwarden/agent/systemauth" "github.com/quexten/goldwarden/agent/systemauth"
"github.com/quexten/goldwarden/agent/vault" "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) err = authenticatedHTTPPost(ctx, cfg.ConfigFile.IdentityUrl+"/connect/token", &loginResponseToken, values)
errsc, ok := err.(*errStatusCode) errsc, ok := err.(*errStatusCode)
if ok && bytes.Contains(errsc.body, []byte("TwoFactor")) { if ok && bytes.Contains(errsc.body, []byte("TwoFactor")) {
var twoFactor TwoFactorResponse var twoFactor twofactor.TwoFactorResponse
if err := json.Unmarshal(errsc.body, &twoFactor); err != nil { if err := json.Unmarshal(errsc.body, &twoFactor); err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", err return LoginResponseToken{}, crypto.MasterKey{}, "", err
} }
provider, token, err := performSecondFactor(&twoFactor, cfg) provider, token, err := twofactor.PerformSecondFactor(&twoFactor, cfg)
if err != nil { if err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not obtain two-factor auth token: %v", err) 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 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 { func RefreshToken(ctx context.Context, cfg *config.Config) bool {
authLog.Info("Refreshing token") authLog.Info("Refreshing token")
@ -157,3 +217,16 @@ func RefreshToken(ctx context.Context, cfg *config.Config) bool {
return true 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()

View File

@ -5,7 +5,9 @@ import (
"crypto/cipher" "crypto/cipher"
"crypto/hmac" "crypto/hmac"
cryptorand "crypto/rand" cryptorand "crypto/rand"
"crypto/rsa"
"crypto/sha256" "crypto/sha256"
"crypto/x509"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io" "io"
@ -53,6 +55,20 @@ func AssymmetricEncryptionKeyFromBytes(key []byte) (AsymmetricEncryptionKey, err
return AsymmetricEncryptionKey{k}, nil 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 { func isMacValid(message, messageMAC, key []byte) bool {
mac := hmac.New(sha256.New, key) mac := hmac.New(sha256.New, key)
mac.Write(message) mac.Write(message)

View File

@ -210,6 +210,20 @@ func EncryptWith(data []byte, typ EncStringType, key SymmetricEncryptionKey) (En
return s, nil 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) { func DecryptWithAsymmetric(s []byte, asymmetrickey AsymmetricEncryptionKey) ([]byte, error) {
key, err := asymmetrickey.encKey.Open() key, err := asymmetrickey.encKey.Open()
if err != nil { if err != nil {

View File

@ -60,3 +60,7 @@ func DeriveMasterKey(password memguard.LockedBuffer, email string, kdfConfig KDF
return MasterKey{memguard.NewEnclave(key)}, nil return MasterKey{memguard.NewEnclave(key)}, nil
} }
func MasterKeyFromBytes(key []byte) MasterKey {
return MasterKey{memguard.NewEnclave(key)}
}

View File

@ -9,6 +9,14 @@ import (
"github.com/quexten/goldwarden/agent/config" "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 { type AuthRequestData struct {
CreationDate time.Time `json:"creationDate"` CreationDate time.Time `json:"creationDate"`
ID string `json:"id"` ID string `json:"id"`
@ -80,3 +88,20 @@ func CreateAuthResponse(ctx context.Context, authRequest AuthRequestData, keyrin
}) })
return authRequestResponse, err 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
}
}

View File

@ -1,19 +1,21 @@
package bitwarden //go:build !nofido2
package twofactor
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"net/url" "net/url"
"strconv"
"github.com/keys-pub/go-libfido2" "github.com/keys-pub/go-libfido2"
"github.com/quexten/goldwarden/agent/config" "github.com/quexten/goldwarden/agent/config"
"github.com/quexten/goldwarden/agent/systemauth" "github.com/quexten/goldwarden/agent/systemauth"
) )
const isFido2Enabled = true
type Fido2Response struct { type Fido2Response struct {
Id string `json:"id"` Id string `json:"id"`
RawId string `json:"rawId"` RawId string `json:"rawId"`
@ -50,7 +52,7 @@ func Fido2TwoFactor(challengeB64 string, credentials []string, config *config.Co
for i, cred := range credentials { for i, cred := range credentials {
decodedPublicKey, err := base64.RawURLEncoding.DecodeString(cred) decodedPublicKey, err := base64.RawURLEncoding.DecodeString(cred)
if err != nil { if err != nil {
websocketLog.Fatal(err.Error()) twofactorLog.Fatal(err.Error())
} }
creds[i] = decodedPublicKey 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") pin, err := systemauth.GetPassword("Fido2 PIN", "Enter your token's PIN")
if err != nil { if err != nil {
websocketLog.Fatal(err.Error()) twofactorLog.Fatal(err.Error())
} }
assertion, err := device.Assertion( assertion, err := device.Assertion(
@ -101,71 +103,3 @@ func Fido2TwoFactor(challengeB64 string, credentials []string, config *config.Co
respjson, err := json.Marshal(resp) respjson, err := json.Marshal(resp)
return string(respjson), nil 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()

View File

@ -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")
}

View File

@ -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{}
}

View File

@ -2,10 +2,13 @@ package systemauth
import ( import (
"errors" "errors"
"os"
"github.com/twpayne/go-pinentry" "github.com/twpayne/go-pinentry"
) )
var authDisabled = false
func GetPassword(title string, description string) (string, error) { func GetPassword(title string, description string) (string, error) {
client, err := pinentry.NewClient( client, err := pinentry.NewClient(
pinentry.WithBinaryNameFromGnuPGAgentConf(), pinentry.WithBinaryNameFromGnuPGAgentConf(),
@ -37,6 +40,11 @@ func GetPassword(title string, description string) (string, error) {
} }
func GetApproval(title string, description string) (bool, 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( client, err := pinentry.NewClient(
pinentry.WithBinaryNameFromGnuPGAgentConf(), pinentry.WithBinaryNameFromGnuPGAgentConf(),
pinentry.WithGPGTTY(), pinentry.WithGPGTTY(),
@ -62,3 +70,10 @@ func GetApproval(title string, description string) (bool, error) {
return true, nil return true, nil
} }
} }
func init() {
envAuthDisabled := os.Getenv("GOLDWARDEN_SYSTEM_AUTH_DISABLED")
if envAuthDisabled == "true" {
authDisabled = true
}
}

View File

@ -76,6 +76,9 @@ func (a Approval) String() string {
func CheckBiometrics(approvalType Approval) bool { func CheckBiometrics(approvalType Approval) bool {
log.Info("Checking biometrics for %s", approvalType.String()) log.Info("Checking biometrics for %s", approvalType.String())
if authDisabled {
return true
}
authority, err := polkit.NewAuthority() authority, err := polkit.NewAuthority()
if err != nil { if err != nil {

View File

@ -102,7 +102,7 @@ type AgentState struct {
config *config.ConfigFile config *config.ConfigFile
} }
func StartUnixAgent(path string) error { func StartUnixAgent(path string, websocket bool, sshAgent bool) error {
ctx := context.Background() ctx := context.Background()
// check if exists // check if exists
@ -134,33 +134,37 @@ func StartUnixAgent(path string) error {
} }
disableDumpable() disableDumpable()
go bitwarden.RunWebsocketDaemon(ctx, vault, &cfg) if websocket {
go bitwarden.RunWebsocketDaemon(ctx, vault, &cfg)
}
vaultAgent := ssh.NewVaultAgent(vault) if sshAgent {
vaultAgent.SetUnlockRequestAction(func() bool { vaultAgent := ssh.NewVaultAgent(vault)
err := cfg.TryUnlock(vault) vaultAgent.SetUnlockRequestAction(func() bool {
if err == nil { err := cfg.TryUnlock(vault)
token, err := cfg.GetToken()
if err == nil { if err == nil {
if token.AccessToken != "" { token, err := cfg.GetToken()
bitwarden.RefreshToken(ctx, &cfg) if err == nil {
userSymmetricKey, err := cfg.GetUserSymmetricKey() if token.AccessToken != "" {
if err != nil { bitwarden.RefreshToken(ctx, &cfg)
fmt.Println(err) userSymmetricKey, err := cfg.GetUserSymmetricKey()
} if err != nil {
protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(userSymmetricKey) fmt.Println(err)
}
protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(userSymmetricKey)
err = bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, &protectedUserSymetricKey, true) err = bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, &protectedUserSymetricKey, true)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
}
} }
} }
return true
} }
return true return false
} })
return false go vaultAgent.Serve()
}) }
go vaultAgent.Serve()
go func() { go func() {
for { for {

135
agent/virtualagent.go Normal file
View File

@ -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
}

View File

@ -1,4 +1,4 @@
//go:build autofill //go:build !noautofill
package autofill package autofill

View File

@ -1,4 +1,4 @@
//go:build autofill //go:build !noautofill
package autofill package autofill

View File

@ -99,7 +99,7 @@ func handlePayloadMessage(msg PayloadMessage, appID string) {
case "biometricUnlock": case "biometricUnlock":
logging.Debugf("Biometric unlock requested") logging.Debugf("Biometric unlock requested")
// logging.Debugf("Biometrics authorized: %t", isAuthorized) // logging.Debugf("Biometrics authorized: %t", isAuthorized)
result, err := client.SendToAgent(ipc.GetBiometricsKeyRequest{}) result, err := client.NewUnixSocketClient().SendToAgent(ipc.GetBiometricsKeyRequest{})
if err != nil { if err != nil {
logging.Errorf("Unable to send message to agent: %s", err.Error()) logging.Errorf("Unable to send message to agent: %s", err.Error())
return return

View File

@ -1,56 +1,5 @@
package client package client
import ( type Client interface {
"io" SendToAgent(request interface{}) (interface{}, error)
"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
} }

View File

@ -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
}

42
client/virtualclient.go Normal file
View File

@ -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
}

View File

@ -1,7 +1,6 @@
package cmd package cmd
import ( import (
"github.com/quexten/goldwarden/client"
"github.com/quexten/goldwarden/ipc" "github.com/quexten/goldwarden/ipc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -19,7 +18,7 @@ var setApiUrlCmd = &cobra.Command{
request := ipc.SetApiURLRequest{} request := ipc.SetApiURLRequest{}
request.Value = url request.Value = url
result, err := client.SendToAgent(request) result, err := commandClient.SendToAgent(request)
if err != nil { if err != nil {
println("Error: " + err.Error()) println("Error: " + err.Error())
println("Is the daemon running?") println("Is the daemon running?")
@ -53,7 +52,7 @@ var setIdentityURLCmd = &cobra.Command{
request := ipc.SetIdentityURLRequest{} request := ipc.SetIdentityURLRequest{}
request.Value = url request.Value = url
result, err := client.SendToAgent(request) result, err := commandClient.SendToAgent(request)
if err != nil { if err != nil {
println("Error: " + err.Error()) println("Error: " + err.Error())
println("Is the daemon running?") println("Is the daemon running?")

View File

@ -15,6 +15,16 @@ var daemonizeCmd = &cobra.Command{
Long: `Starts the agent as a daemon. The agent will run in the background and will 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 in the background until it is stopped.`,
Run: func(cmd *cobra.Command, args []string) { 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() { go func() {
signalChannel := make(chan os.Signal, 1) signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, os.Interrupt) signal.Notify(signalChannel, os.Interrupt)
@ -25,7 +35,7 @@ var daemonizeCmd = &cobra.Command{
if err != nil { if err != nil {
panic(err) panic(err)
} }
err = agent.StartUnixAgent(home + "/.goldwarden.sock") err = agent.StartUnixAgent(home+"/.goldwarden.sock", websocketDisabled, sshDisabled)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -4,7 +4,6 @@ Copyright © 2023 NAME HERE <EMAIL ADDRESS>
package cmd package cmd
import ( import (
"github.com/quexten/goldwarden/client"
"github.com/quexten/goldwarden/ipc" "github.com/quexten/goldwarden/ipc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -18,8 +17,10 @@ var loginCmd = &cobra.Command{
request := ipc.DoLoginRequest{} request := ipc.DoLoginRequest{}
email, _ := cmd.Flags().GetString("email") email, _ := cmd.Flags().GetString("email")
request.Email = 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 { if err != nil {
println("Error: " + err.Error()) println("Error: " + err.Error())
println("Is the daemon running?") println("Is the daemon running?")
@ -43,4 +44,5 @@ func init() {
vaultCmd.AddCommand(loginCmd) vaultCmd.AddCommand(loginCmd)
loginCmd.PersistentFlags().String("email", "", "") loginCmd.PersistentFlags().String("email", "", "")
loginCmd.MarkFlagRequired("email") loginCmd.MarkFlagRequired("email")
loginCmd.PersistentFlags().Bool("passwordless", false, "")
} }

View File

@ -1,7 +1,6 @@
package cmd package cmd
import ( import (
"github.com/quexten/goldwarden/client"
"github.com/quexten/goldwarden/ipc" "github.com/quexten/goldwarden/ipc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -17,7 +16,7 @@ var setPinCmd = &cobra.Command{
Short: "Set a new pin", Short: "Set a new pin",
Long: `Set a new pin. The pin is used to unlock the vault.`, Long: `Set a new pin. The pin is used to unlock the vault.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
result, err := client.SendToAgent(ipc.UpdateVaultPINRequest{}) result, err := commandClient.SendToAgent(ipc.UpdateVaultPINRequest{})
if err != nil { if err != nil {
println("Error: " + err.Error()) println("Error: " + err.Error())
println("Is the daemon running?") println("Is the daemon running?")
@ -42,7 +41,7 @@ var pinStatusCmd = &cobra.Command{
Short: "Check if a pin is set", Short: "Check if a pin is set",
Long: `Check if a pin is set. The pin is used to unlock the vault.`, Long: `Check if a pin is set. The pin is used to unlock the vault.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
result, err := client.SendToAgent(ipc.GetVaultPINRequest{}) result, err := commandClient.SendToAgent(ipc.GetVaultPINRequest{})
if err != nil { if err != nil {
println("Error: " + err.Error()) println("Error: " + err.Error())
println("Is the daemon running?") println("Is the daemon running?")

View File

@ -3,9 +3,13 @@ package cmd
import ( import (
"os" "os"
"github.com/quexten/goldwarden/agent"
"github.com/quexten/goldwarden/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var commandClient client.Client
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "goldwarden", Use: "goldwarden",
Short: "OS level integration for Bitwarden", Short: "OS level integration for Bitwarden",
@ -22,5 +26,13 @@ func Execute() {
} }
func init() { 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") rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
} }

View File

@ -7,7 +7,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"github.com/quexten/goldwarden/client"
"github.com/quexten/goldwarden/ipc" "github.com/quexten/goldwarden/ipc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -29,7 +28,7 @@ var runCmd = &cobra.Command{
env := []string{} env := []string{}
result, err := client.SendToAgent(ipc.GetCLICredentialsRequest{ result, err := commandClient.SendToAgent(ipc.GetCLICredentialsRequest{
ApplicationName: executable, ApplicationName: executable,
}) })
if err != nil { if err != nil {

View File

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"github.com/atotto/clipboard" "github.com/atotto/clipboard"
"github.com/quexten/goldwarden/client"
"github.com/quexten/goldwarden/ipc" "github.com/quexten/goldwarden/ipc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -31,7 +30,7 @@ var sshAddCmd = &cobra.Command{
name, _ := cmd.Flags().GetString("name") name, _ := cmd.Flags().GetString("name")
copyToClipboard, _ := cmd.Flags().GetBool("clipboard") copyToClipboard, _ := cmd.Flags().GetBool("clipboard")
result, err := client.SendToAgent(ipc.CreateSSHKeyRequest{ result, err := commandClient.SendToAgent(ipc.CreateSSHKeyRequest{
Name: name, Name: name,
}) })
if err != nil { if err != nil {
@ -61,7 +60,7 @@ var listSSHCmd = &cobra.Command{
Short: "Lists all SSH keys in your vault", Short: "Lists all SSH keys in your vault",
Long: `Lists all SSH keys in your vault.`, Long: `Lists all SSH keys in your vault.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
result, err := client.SendToAgent(ipc.GetSSHKeysRequest{}) result, err := commandClient.SendToAgent(ipc.GetSSHKeysRequest{})
if err != nil { if err != nil {
println("Error: " + err.Error()) println("Error: " + err.Error())
println("Is the daemon running?") println("Is the daemon running?")

View File

@ -1,7 +1,6 @@
package cmd package cmd
import ( import (
"github.com/quexten/goldwarden/client"
"github.com/quexten/goldwarden/ipc" "github.com/quexten/goldwarden/ipc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -19,7 +18,7 @@ var unlockCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
request := ipc.UnlockVaultRequest{} request := ipc.UnlockVaultRequest{}
result, err := client.SendToAgent(request) result, err := commandClient.SendToAgent(request)
if err != nil { if err != nil {
println("Error: " + err.Error()) println("Error: " + err.Error())
println("Is the daemon running?") println("Is the daemon running?")
@ -46,7 +45,7 @@ var lockCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
request := ipc.LockVaultRequest{} request := ipc.LockVaultRequest{}
result, err := client.SendToAgent(request) result, err := commandClient.SendToAgent(request)
if err != nil { if err != nil {
println("Error: " + err.Error()) println("Error: " + err.Error())
println("Is the daemon running?") println("Is the daemon running?")
@ -73,7 +72,7 @@ var purgeCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
request := ipc.WipeVaultRequest{} request := ipc.WipeVaultRequest{}
result, err := client.SendToAgent(request) result, err := commandClient.SendToAgent(request)
if err != nil { if err != nil {
println("Error: " + err.Error()) println("Error: " + err.Error())
println("Is the daemon running?") println("Is the daemon running?")

View File

@ -367,8 +367,9 @@ func IPCMessageFromPayload(payload interface{}) (IPCMessage, error) {
} }
type DoLoginRequest struct { type DoLoginRequest struct {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
Passwordless bool `json:"passwordless"`
} }
type LockVaultRequest struct { type LockVaultRequest struct {