Login with device & virtual ipc
This commit is contained in:
parent
187ee16adf
commit
6e3859dd43
|
@ -28,7 +28,15 @@ func handleLogin(msg ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, cal
|
|||
req := msg.ParsedPayload().(ipc.DoLoginRequest)
|
||||
|
||||
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 {
|
||||
var payload = ipc.ActionResponse{
|
||||
Success: false,
|
||||
|
|
|
@ -4,16 +4,20 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/LlamaNite/llamalog"
|
||||
"github.com/awnumar/memguard"
|
||||
"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/systemauth"
|
||||
"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)
|
||||
errsc, ok := err.(*errStatusCode)
|
||||
if ok && bytes.Contains(errsc.body, []byte("TwoFactor")) {
|
||||
var twoFactor TwoFactorResponse
|
||||
var twoFactor twofactor.TwoFactorResponse
|
||||
if err := json.Unmarshal(errsc.body, &twoFactor); err != nil {
|
||||
return LoginResponseToken{}, crypto.MasterKey{}, "", err
|
||||
}
|
||||
provider, token, err := performSecondFactor(&twoFactor, cfg)
|
||||
provider, token, err := twofactor.PerformSecondFactor(&twoFactor, cfg)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
authLog.Info("Refreshing token")
|
||||
|
||||
|
@ -157,3 +217,16 @@ func RefreshToken(ctx context.Context, cfg *config.Config) bool {
|
|||
|
||||
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()
|
||||
|
|
|
@ -5,7 +5,9 @@ import (
|
|||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -53,6 +55,20 @@ func AssymmetricEncryptionKeyFromBytes(key []byte) (AsymmetricEncryptionKey, err
|
|||
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 {
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(message)
|
||||
|
|
|
@ -210,6 +210,20 @@ func EncryptWith(data []byte, typ EncStringType, key SymmetricEncryptionKey) (En
|
|||
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) {
|
||||
key, err := asymmetrickey.encKey.Open()
|
||||
if err != nil {
|
||||
|
|
|
@ -60,3 +60,7 @@ func DeriveMasterKey(password memguard.LockedBuffer, email string, kdfConfig KDF
|
|||
|
||||
return MasterKey{memguard.NewEnclave(key)}, nil
|
||||
}
|
||||
|
||||
func MasterKeyFromBytes(key []byte) MasterKey {
|
||||
return MasterKey{memguard.NewEnclave(key)}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,14 @@ import (
|
|||
"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 {
|
||||
CreationDate time.Time `json:"creationDate"`
|
||||
ID string `json:"id"`
|
||||
|
@ -80,3 +88,20 @@ func CreateAuthResponse(ctx context.Context, authRequest AuthRequestData, keyrin
|
|||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
package bitwarden
|
||||
//go:build !nofido2
|
||||
|
||||
package twofactor
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/keys-pub/go-libfido2"
|
||||
"github.com/quexten/goldwarden/agent/config"
|
||||
"github.com/quexten/goldwarden/agent/systemauth"
|
||||
)
|
||||
|
||||
const isFido2Enabled = true
|
||||
|
||||
type Fido2Response struct {
|
||||
Id string `json:"id"`
|
||||
RawId string `json:"rawId"`
|
||||
|
@ -50,7 +52,7 @@ func Fido2TwoFactor(challengeB64 string, credentials []string, config *config.Co
|
|||
for i, cred := range credentials {
|
||||
decodedPublicKey, err := base64.RawURLEncoding.DecodeString(cred)
|
||||
if err != nil {
|
||||
websocketLog.Fatal(err.Error())
|
||||
twofactorLog.Fatal(err.Error())
|
||||
}
|
||||
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")
|
||||
if err != nil {
|
||||
websocketLog.Fatal(err.Error())
|
||||
twofactorLog.Fatal(err.Error())
|
||||
}
|
||||
|
||||
assertion, err := device.Assertion(
|
||||
|
@ -101,71 +103,3 @@ func Fido2TwoFactor(challengeB64 string, credentials []string, config *config.Co
|
|||
respjson, err := json.Marshal(resp)
|
||||
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()
|
|
@ -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")
|
||||
}
|
|
@ -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{}
|
||||
}
|
|
@ -2,10 +2,13 @@ package systemauth
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/twpayne/go-pinentry"
|
||||
)
|
||||
|
||||
var authDisabled = false
|
||||
|
||||
func GetPassword(title string, description string) (string, error) {
|
||||
client, err := pinentry.NewClient(
|
||||
pinentry.WithBinaryNameFromGnuPGAgentConf(),
|
||||
|
@ -37,6 +40,11 @@ func GetPassword(title string, description string) (string, 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(
|
||||
pinentry.WithBinaryNameFromGnuPGAgentConf(),
|
||||
pinentry.WithGPGTTY(),
|
||||
|
@ -62,3 +70,10 @@ func GetApproval(title string, description string) (bool, error) {
|
|||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
envAuthDisabled := os.Getenv("GOLDWARDEN_SYSTEM_AUTH_DISABLED")
|
||||
if envAuthDisabled == "true" {
|
||||
authDisabled = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,6 +76,9 @@ func (a Approval) String() string {
|
|||
|
||||
func CheckBiometrics(approvalType Approval) bool {
|
||||
log.Info("Checking biometrics for %s", approvalType.String())
|
||||
if authDisabled {
|
||||
return true
|
||||
}
|
||||
|
||||
authority, err := polkit.NewAuthority()
|
||||
if err != nil {
|
||||
|
|
|
@ -102,7 +102,7 @@ type AgentState struct {
|
|||
config *config.ConfigFile
|
||||
}
|
||||
|
||||
func StartUnixAgent(path string) error {
|
||||
func StartUnixAgent(path string, websocket bool, sshAgent bool) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// check if exists
|
||||
|
@ -134,33 +134,37 @@ func StartUnixAgent(path string) error {
|
|||
}
|
||||
|
||||
disableDumpable()
|
||||
go bitwarden.RunWebsocketDaemon(ctx, vault, &cfg)
|
||||
if websocket {
|
||||
go bitwarden.RunWebsocketDaemon(ctx, vault, &cfg)
|
||||
}
|
||||
|
||||
vaultAgent := ssh.NewVaultAgent(vault)
|
||||
vaultAgent.SetUnlockRequestAction(func() bool {
|
||||
err := cfg.TryUnlock(vault)
|
||||
if err == nil {
|
||||
token, err := cfg.GetToken()
|
||||
if sshAgent {
|
||||
vaultAgent := ssh.NewVaultAgent(vault)
|
||||
vaultAgent.SetUnlockRequestAction(func() bool {
|
||||
err := cfg.TryUnlock(vault)
|
||||
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)
|
||||
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)
|
||||
err = bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, &protectedUserSymetricKey, true)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
go vaultAgent.Serve()
|
||||
return false
|
||||
})
|
||||
go vaultAgent.Serve()
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
|
@ -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
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
//go:build autofill
|
||||
//go:build !noautofill
|
||||
|
||||
package autofill
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//go:build autofill
|
||||
//go:build !noautofill
|
||||
|
||||
package autofill
|
||||
|
||||
|
|
|
@ -99,7 +99,7 @@ func handlePayloadMessage(msg PayloadMessage, appID string) {
|
|||
case "biometricUnlock":
|
||||
logging.Debugf("Biometric unlock requested")
|
||||
// logging.Debugf("Biometrics authorized: %t", isAuthorized)
|
||||
result, err := client.SendToAgent(ipc.GetBiometricsKeyRequest{})
|
||||
result, err := client.NewUnixSocketClient().SendToAgent(ipc.GetBiometricsKeyRequest{})
|
||||
if err != nil {
|
||||
logging.Errorf("Unable to send message to agent: %s", err.Error())
|
||||
return
|
||||
|
|
|
@ -1,56 +1,5 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"io"
|
||||
"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
|
||||
type Client interface {
|
||||
SendToAgent(request interface{}) (interface{}, error)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/quexten/goldwarden/client"
|
||||
"github.com/quexten/goldwarden/ipc"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -19,7 +18,7 @@ var setApiUrlCmd = &cobra.Command{
|
|||
request := ipc.SetApiURLRequest{}
|
||||
request.Value = url
|
||||
|
||||
result, err := client.SendToAgent(request)
|
||||
result, err := commandClient.SendToAgent(request)
|
||||
if err != nil {
|
||||
println("Error: " + err.Error())
|
||||
println("Is the daemon running?")
|
||||
|
@ -53,7 +52,7 @@ var setIdentityURLCmd = &cobra.Command{
|
|||
request := ipc.SetIdentityURLRequest{}
|
||||
request.Value = url
|
||||
|
||||
result, err := client.SendToAgent(request)
|
||||
result, err := commandClient.SendToAgent(request)
|
||||
if err != nil {
|
||||
println("Error: " + err.Error())
|
||||
println("Is the daemon running?")
|
||||
|
|
|
@ -15,6 +15,16 @@ var daemonizeCmd = &cobra.Command{
|
|||
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: 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() {
|
||||
signalChannel := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChannel, os.Interrupt)
|
||||
|
@ -25,7 +35,7 @@ var daemonizeCmd = &cobra.Command{
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = agent.StartUnixAgent(home + "/.goldwarden.sock")
|
||||
err = agent.StartUnixAgent(home+"/.goldwarden.sock", websocketDisabled, sshDisabled)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ Copyright © 2023 NAME HERE <EMAIL ADDRESS>
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/quexten/goldwarden/client"
|
||||
"github.com/quexten/goldwarden/ipc"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -18,8 +17,10 @@ var loginCmd = &cobra.Command{
|
|||
request := ipc.DoLoginRequest{}
|
||||
email, _ := cmd.Flags().GetString("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 {
|
||||
println("Error: " + err.Error())
|
||||
println("Is the daemon running?")
|
||||
|
@ -43,4 +44,5 @@ func init() {
|
|||
vaultCmd.AddCommand(loginCmd)
|
||||
loginCmd.PersistentFlags().String("email", "", "")
|
||||
loginCmd.MarkFlagRequired("email")
|
||||
loginCmd.PersistentFlags().Bool("passwordless", false, "")
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/quexten/goldwarden/client"
|
||||
"github.com/quexten/goldwarden/ipc"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -17,7 +16,7 @@ var setPinCmd = &cobra.Command{
|
|||
Short: "Set a new pin",
|
||||
Long: `Set a new pin. The pin is used to unlock the vault.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
result, err := client.SendToAgent(ipc.UpdateVaultPINRequest{})
|
||||
result, err := commandClient.SendToAgent(ipc.UpdateVaultPINRequest{})
|
||||
if err != nil {
|
||||
println("Error: " + err.Error())
|
||||
println("Is the daemon running?")
|
||||
|
@ -42,7 +41,7 @@ var pinStatusCmd = &cobra.Command{
|
|||
Short: "Check if a pin is set",
|
||||
Long: `Check if a pin is set. The pin is used to unlock the vault.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
result, err := client.SendToAgent(ipc.GetVaultPINRequest{})
|
||||
result, err := commandClient.SendToAgent(ipc.GetVaultPINRequest{})
|
||||
if err != nil {
|
||||
println("Error: " + err.Error())
|
||||
println("Is the daemon running?")
|
||||
|
|
12
cmd/root.go
12
cmd/root.go
|
@ -3,9 +3,13 @@ package cmd
|
|||
import (
|
||||
"os"
|
||||
|
||||
"github.com/quexten/goldwarden/agent"
|
||||
"github.com/quexten/goldwarden/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandClient client.Client
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "goldwarden",
|
||||
Short: "OS level integration for Bitwarden",
|
||||
|
@ -22,5 +26,13 @@ func Execute() {
|
|||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/quexten/goldwarden/client"
|
||||
"github.com/quexten/goldwarden/ipc"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -29,7 +28,7 @@ var runCmd = &cobra.Command{
|
|||
|
||||
env := []string{}
|
||||
|
||||
result, err := client.SendToAgent(ipc.GetCLICredentialsRequest{
|
||||
result, err := commandClient.SendToAgent(ipc.GetCLICredentialsRequest{
|
||||
ApplicationName: executable,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/quexten/goldwarden/client"
|
||||
"github.com/quexten/goldwarden/ipc"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -31,7 +30,7 @@ var sshAddCmd = &cobra.Command{
|
|||
name, _ := cmd.Flags().GetString("name")
|
||||
copyToClipboard, _ := cmd.Flags().GetBool("clipboard")
|
||||
|
||||
result, err := client.SendToAgent(ipc.CreateSSHKeyRequest{
|
||||
result, err := commandClient.SendToAgent(ipc.CreateSSHKeyRequest{
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -61,7 +60,7 @@ var listSSHCmd = &cobra.Command{
|
|||
Short: "Lists all SSH keys in your vault",
|
||||
Long: `Lists all SSH keys in your vault.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
result, err := client.SendToAgent(ipc.GetSSHKeysRequest{})
|
||||
result, err := commandClient.SendToAgent(ipc.GetSSHKeysRequest{})
|
||||
if err != nil {
|
||||
println("Error: " + err.Error())
|
||||
println("Is the daemon running?")
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/quexten/goldwarden/client"
|
||||
"github.com/quexten/goldwarden/ipc"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -19,7 +18,7 @@ var unlockCmd = &cobra.Command{
|
|||
Run: func(cmd *cobra.Command, args []string) {
|
||||
request := ipc.UnlockVaultRequest{}
|
||||
|
||||
result, err := client.SendToAgent(request)
|
||||
result, err := commandClient.SendToAgent(request)
|
||||
if err != nil {
|
||||
println("Error: " + err.Error())
|
||||
println("Is the daemon running?")
|
||||
|
@ -46,7 +45,7 @@ var lockCmd = &cobra.Command{
|
|||
Run: func(cmd *cobra.Command, args []string) {
|
||||
request := ipc.LockVaultRequest{}
|
||||
|
||||
result, err := client.SendToAgent(request)
|
||||
result, err := commandClient.SendToAgent(request)
|
||||
if err != nil {
|
||||
println("Error: " + err.Error())
|
||||
println("Is the daemon running?")
|
||||
|
@ -73,7 +72,7 @@ var purgeCmd = &cobra.Command{
|
|||
Run: func(cmd *cobra.Command, args []string) {
|
||||
request := ipc.WipeVaultRequest{}
|
||||
|
||||
result, err := client.SendToAgent(request)
|
||||
result, err := commandClient.SendToAgent(request)
|
||||
if err != nil {
|
||||
println("Error: " + err.Error())
|
||||
println("Is the daemon running?")
|
||||
|
|
|
@ -367,8 +367,9 @@ func IPCMessageFromPayload(payload interface{}) (IPCMessage, error) {
|
|||
}
|
||||
|
||||
type DoLoginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Passwordless bool `json:"passwordless"`
|
||||
}
|
||||
|
||||
type LockVaultRequest struct {
|
||||
|
|
Loading…
Reference in New Issue