goldwarden-vaultwarden-bitw.../cli/agent/bitwarden/auth.go

371 lines
13 KiB
Go

package bitwarden
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"math/rand"
"net/url"
"runtime"
"strconv"
"strings"
"time"
"github.com/quexten/goldwarden/cli/agent/bitwarden/crypto"
"github.com/quexten/goldwarden/cli/agent/bitwarden/twofactor"
"github.com/quexten/goldwarden/cli/agent/config"
"github.com/quexten/goldwarden/cli/agent/notify"
"github.com/quexten/goldwarden/cli/agent/systemauth/pinentry"
"github.com/quexten/goldwarden/cli/agent/vault"
"github.com/quexten/goldwarden/cli/logging"
"golang.org/x/crypto/pbkdf2"
)
var authLog = logging.GetLogger("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"`
Kdf int `json:"Kdf"`
KdfIterations int `json:"KdfIterations"`
KdfMemory int `json:"KdfMemory"`
KdfParallelism int `json:"KdfParallelism"`
}
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 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), "", 0, 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), "", 0, 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), "", 0, 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), "", 0, 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), "", 0, 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{
Email: email,
}); err != nil {
notify.Notify("Goldwarden", fmt.Sprintf("Could not pre-login: %v", err), "", 0, func() {})
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 := pinentry.GetPassword("Bitwarden Password", "Enter your Bitwarden password")
if err != nil {
notify.Notify("Goldwarden", fmt.Sprintf("Could not get password: %v", err), "", 0, func() {})
return LoginResponseToken{}, crypto.MasterKey{}, "", err
}
masterKey, err = crypto.DeriveMasterKey([]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 {
notify.Notify("Goldwarden", fmt.Sprintf("Could not derive master key: %v", err), "", 0, func() {})
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")) {
loginResponseToken, err = Perform2FA(values, errsc, cfg, ctx)
if err != nil {
notify.Notify("Goldwarden", fmt.Sprintf("Could not login via two-factor: %v", err), "", 0, func() {})
return LoginResponseToken{}, crypto.MasterKey{}, "", err
}
} else if err != nil && strings.Contains(err.Error(), "Captcha required.") {
notify.Notify("Goldwarden", "Captcha required", "", 0, func() {})
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("captcha required, please login via the web interface")
} else if err != nil {
notify.Notify("Goldwarden", fmt.Sprintf("Could not login via password: %s", err.Error()), "", 0, func() {})
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not login via password: %v", err)
}
authLog.Info("Logged in")
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(vault.Keyring.IsMemguard)
if err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", err
}
data, err := CreateAuthRequest(ctx, accessCode, cfg.ConfigFile.DeviceUUID, email, base64.StdEncoding.EncodeToString(publicKey.PublicBytes()), cfg)
if err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", err
}
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 := GetAuthResponse(ctx, accessCode, data.ID, cfg)
if err != nil {
log.Error("Could not get auth request: %s", err.Error())
}
if authRequestData.RequestApproved {
masterKey, err := crypto.DecryptWithAsymmetric([]byte(authRequestData.Key), publicKey)
if err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not decrypt key with asymmetric key: %s", err.Error())
}
masterPasswordHash, err := crypto.DecryptWithAsymmetric([]byte(authRequestData.MasterPasswordHash), publicKey)
if err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not decrypt master password hash with asymmetric key: %s", err.Error())
}
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)
errsc, ok := err.(*errStatusCode)
if ok && bytes.Contains(errsc.body, []byte("TwoFactor")) {
loginResponseToken, err = Perform2FA(values, errsc, cfg, ctx)
if err != nil {
return LoginResponseToken{}, crypto.MasterKey{}, "", err
}
} 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: %s", err.Error())
}
return loginResponseToken, crypto.MasterKeyFromBytes(masterKey), string(masterPasswordHash), nil
}
time.Sleep(1 * time.Second)
}
}
}
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
}
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
}
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), "", 0, func() {})
return false
}
err = cfg.SetToken(config.LoginToken{
AccessToken: loginResponseToken.AccessToken,
RefreshToken: "",
Key: loginResponseToken.Key,
TokenType: loginResponseToken.TokenType,
ExpiresIn: loginResponseToken.ExpiresIn,
})
if err != nil {
authLog.Error("Could not set token: %s", err.Error())
return false
}
} 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), "", 0, func() {})
return false
}
err = cfg.SetToken(config.LoginToken{
AccessToken: loginResponseToken.AccessToken,
RefreshToken: loginResponseToken.RefreshToken,
Key: loginResponseToken.Key,
TokenType: loginResponseToken.TokenType,
ExpiresIn: loginResponseToken.ExpiresIn,
})
if err != nil {
authLog.Error("Could not set token: %s", err.Error())
return false
}
}
authLog.Info("Token refreshed")
return true
}
func Perform2FA(values url.Values, errsc *errStatusCode, cfg *config.Config, ctx context.Context) (LoginResponseToken, error) {
var twoFactor twofactor.TwoFactorResponse
if err := json.Unmarshal(errsc.body, &twoFactor); err != nil {
return LoginResponseToken{}, err
}
provider, token, err := twofactor.PerformSecondFactor(&twoFactor, cfg)
if err != nil {
return LoginResponseToken{}, 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{}, fmt.Errorf("could not login via two-factor: %v", err)
}
authLog.Info("2FA login successful")
return loginResponseToken, nil
}
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()