Skip to content

Commit

Permalink
Add api-key based login
Browse files Browse the repository at this point in the history
  • Loading branch information
quexten committed Jan 4, 2024
1 parent f36f994 commit 5af0780
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 63 deletions.
21 changes: 16 additions & 5 deletions agent/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ func sync(ctx context.Context, vault *vault.Vault, cfg *config.Config) bool {
token, err := cfg.GetToken()
if err == nil {
if token.AccessToken != "" {
bitwarden.RefreshToken(ctx, cfg)
refreshed := bitwarden.RefreshToken(ctx, cfg)
if !refreshed {
return false
}

userSymmetricKey, err := cfg.GetUserSymmetricKey()
if err != nil {
return false
Expand Down Expand Up @@ -83,10 +87,17 @@ func ensureIsNotLocked(action Action) Action {
ctx1 := context.Background()
success := sync(ctx1, vault, cfg)
if err != nil || !success {
return messages.IPCMessageFromPayload(messages.ActionResponse{
Success: false,
Message: err.Error(),
})
if err != nil {
return messages.IPCMessageFromPayload(messages.ActionResponse{
Success: false,
Message: err.Error(),
})
} else {
return messages.IPCMessageFromPayload(messages.ActionResponse{
Success: false,
Message: "Could not sync vault",
})
}
}

systemauth.CreatePinSession(*ctx)
Expand Down
34 changes: 34 additions & 0 deletions agent/actions/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,38 @@ func handleSetNotifications(request messages.IPCMessage, cfg *config.Config, vau
})
}

func handleSetClientID(request messages.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx *sockets.CallingContext) (response messages.IPCMessage, err error) {
clientID := messages.ParsePayload(request).(messages.SetClientIDRequest).Value
cfg.SetClientID(clientID)
err = cfg.WriteConfig()
if err != nil {
return messages.IPCMessageFromPayload(messages.ActionResponse{
Success: false,
Message: err.Error(),
})
}

return messages.IPCMessageFromPayload(messages.ActionResponse{
Success: true,
})
}

func handleSetClientSecret(request messages.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx *sockets.CallingContext) (response messages.IPCMessage, err error) {
clientSecret := messages.ParsePayload(request).(messages.SetClientSecretRequest).Value
cfg.SetClientSecret(clientSecret)
err = cfg.WriteConfig()
if err != nil {
return messages.IPCMessageFromPayload(messages.ActionResponse{
Success: false,
Message: err.Error(),
})
}

return messages.IPCMessageFromPayload(messages.ActionResponse{
Success: true,
})
}

func handleGetRuntimeConfig(request messages.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx *sockets.CallingContext) (response messages.IPCMessage, err error) {
return messages.IPCMessageFromPayload(messages.GetRuntimeConfigResponse{
UseMemguard: cfg.ConfigFile.RuntimeConfig.UseMemguard,
Expand All @@ -68,4 +100,6 @@ func init() {
AgentActionsRegistry.Register(messages.MessageTypeForEmptyPayload(messages.SetApiURLRequest{}), handleSetApiURL)
AgentActionsRegistry.Register(messages.MessageTypeForEmptyPayload(messages.SetNotificationsURLRequest{}), handleSetNotifications)
AgentActionsRegistry.Register(messages.MessageTypeForEmptyPayload(messages.GetRuntimeConfigRequest{}), handleGetRuntimeConfig)
AgentActionsRegistry.Register(messages.MessageTypeForEmptyPayload(messages.SetClientIDRequest{}), handleSetClientID)
AgentActionsRegistry.Register(messages.MessageTypeForEmptyPayload(messages.SetClientSecretRequest{}), handleSetClientSecret)
}
4 changes: 3 additions & 1 deletion agent/actions/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ func handleLogin(msg messages.IPCMessage, cfg *config.Config, vault *vault.Vault
var masterKey crypto.MasterKey
var masterpasswordHash string

if req.Passwordless {
if secret, err := cfg.GetClientSecret(); err == nil && secret != "" {
token, masterKey, masterpasswordHash, err = bitwarden.LoginWithApiKey(ctx, req.Email, cfg, vault)
} else 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)
Expand Down
139 changes: 117 additions & 22 deletions agent/bitwarden/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ type preLoginResponse struct {
}

type LoginResponseToken struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
RefreshToken string `json:"refresh_token"`
Key string `json:"key"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
RefreshToken string `json:"refresh_token"`
Key string `json:"key"`
Kdf int `json:"Kdf"`
KdfIterations int `json:"KdfIterations"`
KdfMemory int `json:"KdfMemory"`
KdfParallelism int `json:"KdfParallelism"`
}

const (
Expand All @@ -64,6 +68,52 @@ func deviceType() string {
}
}

func LoginWithApiKey(ctx context.Context, email string, cfg *config.Config, vault *vault.Vault) (LoginResponseToken, crypto.MasterKey, string, error) {
clientID, err := cfg.GetClientID()
if err != nil {
notify.Notify("Goldwarden", fmt.Sprintf("Could not get client ID: %v", err), "", func() {})
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not get client ID: %v", err)
}
clientSecret, err := cfg.GetClientSecret()
if err != nil {
notify.Notify("Goldwarden", fmt.Sprintf("Could not get client secret: %v", err), "", func() {})
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not get client secret: %v", err)
}

values := urlValues(
"client_id", clientID,
"client_secret", clientSecret,
"scope", loginApiKeyScope,
"grant_type", "client_credentials",
"deviceType", deviceType(),
"deviceName", deviceName,
"deviceIdentifier", cfg.ConfigFile.DeviceUUID,
)

var loginResponseToken LoginResponseToken
err = authenticatedHTTPPost(ctx, cfg.ConfigFile.IdentityUrl+"/connect/token", &loginResponseToken, values)
if err != nil {
notify.Notify("Goldwarden", fmt.Sprintf("Could not login via API key: %v", err), "", func() {})
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not login via API key: %v", err)
}

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(loginResponseToken.Kdf), Iterations: uint32(loginResponseToken.KdfIterations), Memory: uint32(loginResponseToken.KdfMemory), Parallelism: uint32(loginResponseToken.KdfParallelism)})
if err != nil {
notify.Notify("Goldwarden", fmt.Sprintf("Could not derive master key: %v", err), "", func() {})
return LoginResponseToken{}, crypto.MasterKey{}, "", err
}
hashedPassword := b64enc.EncodeToString(pbkdf2.Key(masterKey.GetBytes(), []byte(password), 1, 32, sha256.New))

authLog.Info("Logged in")
return loginResponseToken, masterKey, hashedPassword, nil
}

func LoginWithMasterpassword(ctx context.Context, email string, cfg *config.Config, vault *vault.Vault) (LoginResponseToken, crypto.MasterKey, string, error) {
var preLogin preLoginResponse
if err := authenticatedHTTPPost(ctx, cfg.ConfigFile.ApiUrl+"/accounts/prelogin", &preLogin, preLoginRequest{
Expand Down Expand Up @@ -200,25 +250,70 @@ func RefreshToken(ctx context.Context, cfg *config.Config) bool {
fmt.Println("Could not get refresh token: ", err)
return false
}
if token.RefreshToken == "" {
authLog.Info("Refreshing using API Key")
clientID, err := cfg.GetClientID()
if err != nil {
authLog.Error("Could not get client ID: %s", err.Error())
return false
}
clientSecret, err := cfg.GetClientSecret()
if err != nil {
authLog.Error("Could not get client secret: %s", err.Error())
return false
}

var loginResponseToken LoginResponseToken
err = authenticatedHTTPPost(ctx, cfg.ConfigFile.IdentityUrl+"/connect/token", &loginResponseToken, urlValues(
"grant_type", "refresh_token",
"refresh_token", token.RefreshToken,
"client_id", "connector",
))
if err != nil {
authLog.Error("Could not refresh token: %s", err.Error())
notify.Notify("Goldwarden", fmt.Sprintf("Could not refresh token: %v", err), "", func() {})
return false
if clientID != "" && clientSecret != "" {
values := urlValues(
"client_id", clientID,
"client_secret", clientSecret,
"scope", loginApiKeyScope,
"grant_type", "client_credentials",
"deviceType", deviceType(),
"deviceName", deviceName,
"deviceIdentifier", cfg.ConfigFile.DeviceUUID,
)

var loginResponseToken LoginResponseToken
err = authenticatedHTTPPost(ctx, cfg.ConfigFile.IdentityUrl+"/connect/token", &loginResponseToken, values)
if err != nil {
authLog.Error("Could not refresh token: %s", err.Error())
notify.Notify("Goldwarden", fmt.Sprintf("Could not refresh token: %v", err), "", func() {})
return false
}

cfg.SetToken(config.LoginToken{
AccessToken: loginResponseToken.AccessToken,
RefreshToken: "",
Key: loginResponseToken.Key,
TokenType: loginResponseToken.TokenType,
ExpiresIn: loginResponseToken.ExpiresIn,
})
} else {
authLog.Info("No api credentials set")
}
} else {
authLog.Info("Refreshing using refresh token")

var loginResponseToken LoginResponseToken
err = authenticatedHTTPPost(ctx, cfg.ConfigFile.IdentityUrl+"/connect/token", &loginResponseToken, urlValues(
"grant_type", "refresh_token",
"refresh_token", token.RefreshToken,
"client_id", "connector",
))
if err != nil {
authLog.Error("Could not refresh token: %s", err.Error())
notify.Notify("Goldwarden", fmt.Sprintf("Could not refresh token: %v", err), "", func() {})
return false
}
cfg.SetToken(config.LoginToken{
AccessToken: loginResponseToken.AccessToken,
RefreshToken: loginResponseToken.RefreshToken,
Key: loginResponseToken.Key,
TokenType: loginResponseToken.TokenType,
ExpiresIn: loginResponseToken.ExpiresIn,
})
}
cfg.SetToken(config.LoginToken{
AccessToken: loginResponseToken.AccessToken,
RefreshToken: loginResponseToken.RefreshToken,
Key: loginResponseToken.Key,
TokenType: loginResponseToken.TokenType,
ExpiresIn: loginResponseToken.ExpiresIn,
})

authLog.Info("Token refreshed")

Expand Down
80 changes: 80 additions & 0 deletions agent/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ type ConfigFile struct {
IdentityUrl string
ApiUrl string
NotificationsUrl string
EncryptedClientID string
EncryptedClientSecret string
DeviceUUID string
ConfigKeyHash string
EncryptedToken string
Expand Down Expand Up @@ -88,6 +90,8 @@ func DefaultConfig(useMemguard bool) Config {
IdentityUrl: "https://vault.bitwarden.com/identity",
ApiUrl: "https://vault.bitwarden.com/api",
NotificationsUrl: "https://notifications.bitwarden.com",
EncryptedClientID: "",
EncryptedClientSecret: "",
DeviceUUID: deviceUUID.String(),
ConfigKeyHash: "",
EncryptedToken: "",
Expand Down Expand Up @@ -241,6 +245,82 @@ func (c *Config) SetToken(token LoginToken) error {
return nil
}

func (c *Config) GetClientID() (string, error) {
if c.IsLocked() {
return "", errors.New("config is locked")
}

if c.ConfigFile.EncryptedClientID == "" {
return "", nil
}

decrypted, err := c.decryptString(c.ConfigFile.EncryptedClientID)
if err != nil {
return "", err
}
return decrypted, nil
}

func (c *Config) SetClientID(clientID string) error {
if c.IsLocked() {
return errors.New("config is locked")
}

if clientID == "" {
c.ConfigFile.EncryptedClientID = ""
c.WriteConfig()
return nil
}

encryptedClientID, err := c.encryptString(clientID)
if err != nil {
return err
}
// c.mu.Lock()
c.ConfigFile.EncryptedClientID = encryptedClientID
// c.mu.Unlock()
c.WriteConfig()
return nil
}

func (c *Config) GetClientSecret() (string, error) {
if c.IsLocked() {
return "", errors.New("config is locked")
}

if c.ConfigFile.EncryptedClientSecret == "" {
return "", nil
}

decrypted, err := c.decryptString(c.ConfigFile.EncryptedClientSecret)
if err != nil {
return "", err
}
return decrypted, nil
}

func (c *Config) SetClientSecret(clientSecret string) error {
if c.IsLocked() {
return errors.New("config is locked")
}

if clientSecret == "" {
c.ConfigFile.EncryptedClientSecret = ""
c.WriteConfig()
return nil
}

encryptedClientSecret, err := c.encryptString(clientSecret)
if err != nil {
return err
}
// c.mu.Lock()
c.ConfigFile.EncryptedClientSecret = encryptedClientSecret
// c.mu.Unlock()
c.WriteConfig()
return nil
}

func (c *Config) GetUserSymmetricKey() ([]byte, error) {
if c.IsLocked() {
return []byte{}, errors.New("config is locked")
Expand Down
3 changes: 2 additions & 1 deletion agent/unixsocketagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (

const (
FullSyncInterval = 60 * time.Minute
TokenRefreshInterval = 30 * time.Minute
TokenRefreshInterval = 10 * time.Minute
)

var log = logging.GetLogger("Goldwarden", "Agent")
Expand Down Expand Up @@ -248,6 +248,7 @@ func StartUnixAgent(path string, runtimeConfig config.RuntimeConfig) error {
for {
time.Sleep(FullSyncInterval)
if !cfg.IsLocked() {
bitwarden.RefreshToken(ctx, &cfg)
token, err := cfg.GetToken()
if err != nil {
log.Warn("Could not get token: %s", err.Error())
Expand Down
Loading

0 comments on commit 5af0780

Please sign in to comment.