mirror of
https://github.com/quexten/goldwarden.git
synced 2024-12-19 00:22:02 +03:00
160 lines
4.9 KiB
Go
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
|
|
}
|