goldwarden/agent/bitwarden/auth.go
Bernd Schoolmann 30237e79b2
Initial commit
2023-07-17 03:23:26 +02:00

160 lines
4.9 KiB
Go

package bitwarden
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"net/url"
"runtime"
"strconv"
"strings"
"github.com/LlamaNite/llamalog"
"github.com/awnumar/memguard"
"github.com/quexten/goldwarden/agent/bitwarden/crypto"
"github.com/quexten/goldwarden/agent/config"
"github.com/quexten/goldwarden/agent/systemauth"
"github.com/quexten/goldwarden/agent/vault"
"golang.org/x/crypto/pbkdf2"
)
var authLog = llamalog.NewLogger("Goldwarden", "Auth")
type preLoginRequest struct {
Email string `json:"email"`
}
type preLoginResponse struct {
KDF int
KDFIterations int
KDFMemory int
KDFParallelism int
}
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"`
}
const (
deviceName = "goldwarden"
loginScope = "api offline_access"
loginApiKeyScope = "api"
)
func deviceType() string {
switch runtime.GOOS {
case "linux":
return "8"
case "darwin":
return "7"
case "windows":
return "6"
default:
return "14"
}
}
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{
Email: email,
}); err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not pre-login: %v", err)
}
var values url.Values
var masterKey crypto.MasterKey
var hashedPassword string
password, err := systemauth.GetPassword("Bitwarden Password", "Enter your Bitwarden password")
if err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", err
}
masterKey, err = crypto.DeriveMasterKey(*memguard.NewBufferFromBytes([]byte(strings.Clone(password))), email, crypto.KDFConfig{Type: crypto.KDFType(preLogin.KDF), Iterations: uint32(preLogin.KDFIterations), Memory: uint32(preLogin.KDFMemory), Parallelism: uint32(preLogin.KDFParallelism)})
if err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", err
}
hashedPassword = b64enc.EncodeToString(pbkdf2.Key(masterKey.GetBytes(), []byte(password), 1, 32, sha256.New))
values = urlValues(
"grant_type", "password",
"username", email,
"password", string(hashedPassword),
"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)
errsc, ok := err.(*errStatusCode)
if ok && bytes.Contains(errsc.body, []byte("TwoFactor")) {
var twoFactor TwoFactorResponse
if err := json.Unmarshal(errsc.body, &twoFactor); err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", err
}
provider, token, err := performSecondFactor(&twoFactor, cfg)
if err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not obtain two-factor auth token: %v", err)
}
values.Set("twoFactorProvider", strconv.Itoa(int(provider)))
values.Set("twoFactorToken", string(token))
values.Set("twoFactorRemember", "1")
loginResponseToken = LoginResponseToken{}
if err := authenticatedHTTPPost(ctx, cfg.ConfigFile.IdentityUrl+"/connect/token", &loginResponseToken, values); err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not login via two-factor: %v", err)
}
authLog.Info("2FA login successful")
} else if err != nil && strings.Contains(err.Error(), "Captcha required.") {
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("captcha required, please login via the web interface")
} else if err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not login via password: %v", err)
}
authLog.Info("Logged in")
return loginResponseToken, masterKey, hashedPassword, nil
}
func RefreshToken(ctx context.Context, cfg *config.Config) bool {
authLog.Info("Refreshing token")
token, err := cfg.GetToken()
if err != nil {
fmt.Println("Could not get refresh token: ", err)
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 {
fmt.Println("Could not refresh token: ", err)
return false
}
cfg.SetToken(config.LoginToken{
AccessToken: loginResponseToken.AccessToken,
RefreshToken: loginResponseToken.RefreshToken,
Key: loginResponseToken.Key,
TokenType: loginResponseToken.TokenType,
ExpiresIn: loginResponseToken.ExpiresIn,
})
authLog.Info("Token refreshed")
return true
}