goldwarden-vaultwarden-bitw.../agent/config/config.go

585 lines
14 KiB
Go
Raw Normal View History

2023-07-17 03:23:26 +02:00
package config
import (
2023-12-22 12:01:21 +01:00
"bytes"
2023-07-17 03:23:26 +02:00
cryptoSubtle "crypto/subtle"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"os"
"runtime/debug"
2023-12-28 13:41:07 +01:00
"strings"
2023-07-17 03:23:26 +02:00
"sync"
2024-01-19 05:14:25 +01:00
"time"
2023-07-17 03:23:26 +02:00
"github.com/google/uuid"
"github.com/quexten/goldwarden/agent/bitwarden/crypto"
"github.com/quexten/goldwarden/agent/notify"
2024-02-09 20:48:44 +01:00
"github.com/quexten/goldwarden/agent/pincache"
2023-09-12 02:54:46 +02:00
"github.com/quexten/goldwarden/agent/systemauth/pinentry"
2023-07-17 03:23:26 +02:00
"github.com/quexten/goldwarden/agent/vault"
"github.com/tink-crypto/tink-go/v2/aead/subtle"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/sha3"
)
const (
2023-08-21 18:37:34 +02:00
KDFIterations = 2
KDFMemory = 2 * 1024 * 1024
KDFThreads = 8
2023-12-28 13:41:07 +01:00
DefaultConfigPath = "~/.config/goldwarden/goldwarden.json"
2023-07-17 03:23:26 +02:00
)
2023-08-21 18:37:34 +02:00
type RuntimeConfig struct {
2024-02-09 19:37:09 +01:00
AuthMethod string
DoNotPersistConfig bool
ConfigDirectory string
DisableSSHAgent bool
WebsocketDisabled bool
DeviceUUID string
User string
Password string
Pin string
UseMemguard bool
SSHAgentSocketPath string
GoldwardenSocketPath string
DaemonAuthToken string
2023-08-21 18:37:34 +02:00
}
2023-07-17 03:23:26 +02:00
type ConfigFile struct {
IdentityUrl string
ApiUrl string
2023-09-11 14:14:27 +02:00
NotificationsUrl string
VaultUrl string
2024-01-04 21:53:38 +01:00
EncryptedClientID string
EncryptedClientSecret string
2023-07-17 03:23:26 +02:00
DeviceUUID string
ConfigKeyHash string
EncryptedToken string
EncryptedUserSymmetricKey string
EncryptedMasterPasswordHash string
EncryptedMasterKey string
2023-08-21 18:37:34 +02:00
RuntimeConfig RuntimeConfig `json:"-"`
2023-07-17 03:23:26 +02:00
}
type LoginToken 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"`
}
type Config struct {
2023-12-22 12:01:21 +01:00
useMemguard bool
key *LockedBuffer
ConfigFile ConfigFile
mu sync.Mutex
2023-07-17 03:23:26 +02:00
}
2023-12-22 12:01:21 +01:00
func DefaultConfig(useMemguard bool) Config {
2023-07-17 03:23:26 +02:00
deviceUUID, _ := uuid.NewUUID()
2023-12-22 12:01:21 +01:00
keyBuffer := NewBuffer(32, useMemguard)
2023-07-17 03:23:26 +02:00
return Config{
2023-12-22 12:01:21 +01:00
useMemguard,
&keyBuffer,
2023-07-17 03:23:26 +02:00
ConfigFile{
2023-09-11 14:14:27 +02:00
IdentityUrl: "https://vault.bitwarden.com/identity",
ApiUrl: "https://vault.bitwarden.com/api",
NotificationsUrl: "https://notifications.bitwarden.com",
VaultUrl: "https://vault.bitwarden.com",
2024-01-04 21:53:38 +01:00
EncryptedClientID: "",
EncryptedClientSecret: "",
2023-07-17 03:23:26 +02:00
DeviceUUID: deviceUUID.String(),
ConfigKeyHash: "",
EncryptedToken: "",
EncryptedUserSymmetricKey: "",
EncryptedMasterPasswordHash: "",
EncryptedMasterKey: "",
2023-08-21 18:37:34 +02:00
RuntimeConfig: RuntimeConfig{},
2023-07-17 03:23:26 +02:00
},
sync.Mutex{},
}
}
func (c *Config) IsLocked() bool {
2023-12-22 12:01:21 +01:00
key := (*c.key).Bytes()
return bytes.Equal(key, make([]byte, 32)) && c.HasPin()
2023-07-17 03:23:26 +02:00
}
2023-07-17 05:42:21 +02:00
func (c *Config) IsLoggedIn() bool {
return c.ConfigFile.EncryptedMasterPasswordHash != ""
}
2023-07-17 03:23:26 +02:00
func (c *Config) Unlock(password string) bool {
c.mu.Lock()
defer c.mu.Unlock()
if !c.IsLocked() {
return true
}
key := argon2.Key([]byte(password), []byte(c.ConfigFile.DeviceUUID), KDFIterations, KDFMemory, KDFThreads, 32)
debug.FreeOSMemory()
keyHash := sha3.Sum256(key)
configKeyHash := hex.EncodeToString(keyHash[:])
if cryptoSubtle.ConstantTimeCompare([]byte(configKeyHash), []byte(c.ConfigFile.ConfigKeyHash)) != 1 {
return false
}
2023-12-22 12:01:21 +01:00
keyBuffer := NewBufferFromBytes(key, c.useMemguard)
c.key = &keyBuffer
2024-01-19 05:14:25 +01:00
notify.Notify("Goldwarden", "Vault Unlocked", "", 60*time.Second, func() {})
2024-02-09 20:48:44 +01:00
pincache.SetPin(c.useMemguard, []byte(password))
2023-07-17 03:23:26 +02:00
return true
}
2023-09-19 21:49:56 +02:00
func (c *Config) VerifyPin(password string) bool {
key := argon2.Key([]byte(password), []byte(c.ConfigFile.DeviceUUID), KDFIterations, KDFMemory, KDFThreads, 32)
debug.FreeOSMemory()
keyHash := sha3.Sum256(key)
configKeyHash := hex.EncodeToString(keyHash[:])
if cryptoSubtle.ConstantTimeCompare([]byte(configKeyHash), []byte(c.ConfigFile.ConfigKeyHash)) != 1 {
return false
} else {
return true
}
}
2023-07-17 03:23:26 +02:00
func (c *Config) Lock() {
c.mu.Lock()
defer c.mu.Unlock()
if c.IsLocked() {
return
}
2023-12-22 12:01:21 +01:00
(*c.key).Wipe()
2024-01-19 05:14:25 +01:00
notify.Notify("Goldwarden", "Vault Locked", "", 60*time.Second, func() {})
2023-07-17 03:23:26 +02:00
}
func (c *Config) Purge() {
c.mu.Lock()
defer c.mu.Unlock()
c.ConfigFile.EncryptedMasterPasswordHash = ""
c.ConfigFile.EncryptedToken = ""
c.ConfigFile.EncryptedUserSymmetricKey = ""
2024-01-04 23:40:07 +01:00
c.ConfigFile.EncryptedClientID = ""
c.ConfigFile.EncryptedClientSecret = ""
2023-07-17 03:23:26 +02:00
c.ConfigFile.ConfigKeyHash = ""
c.ConfigFile.EncryptedMasterKey = ""
2023-12-22 12:01:21 +01:00
key := NewBuffer(32, c.useMemguard)
c.key = &key
2023-07-17 03:23:26 +02:00
}
func (c *Config) HasPin() bool {
return c.ConfigFile.ConfigKeyHash != ""
}
func (c *Config) UpdatePin(password string, write bool) {
c.mu.Lock()
newKey := argon2.Key([]byte(password), []byte(c.ConfigFile.DeviceUUID), KDFIterations, KDFMemory, KDFThreads, 32)
keyHash := sha3.Sum256(newKey)
configKeyHash := hex.EncodeToString(keyHash[:])
debug.FreeOSMemory()
c.ConfigFile.ConfigKeyHash = configKeyHash
plaintextToken, err1 := c.decryptString(c.ConfigFile.EncryptedToken)
plaintextUserSymmetricKey, err3 := c.decryptString(c.ConfigFile.EncryptedUserSymmetricKey)
plaintextEncryptedMasterPasswordHash, err4 := c.decryptString(c.ConfigFile.EncryptedMasterPasswordHash)
plaintextMasterKey, err5 := c.decryptString(c.ConfigFile.EncryptedMasterKey)
2024-01-04 23:40:07 +01:00
plaintextClientID, err6 := c.decryptString(c.ConfigFile.EncryptedClientID)
plaintextClientSecret, err7 := c.decryptString(c.ConfigFile.EncryptedClientSecret)
2023-07-17 03:23:26 +02:00
2023-12-22 12:01:21 +01:00
key := NewBufferFromBytes(newKey, c.useMemguard)
c.key = &key
2023-07-17 03:23:26 +02:00
if err1 == nil {
c.ConfigFile.EncryptedToken, err1 = c.encryptString(plaintextToken)
}
if err3 == nil {
c.ConfigFile.EncryptedUserSymmetricKey, err3 = c.encryptString(plaintextUserSymmetricKey)
}
if err4 == nil {
c.ConfigFile.EncryptedMasterPasswordHash, err4 = c.encryptString(plaintextEncryptedMasterPasswordHash)
}
if err5 == nil {
c.ConfigFile.EncryptedMasterKey, err5 = c.encryptString(plaintextMasterKey)
}
2024-01-04 23:40:07 +01:00
if err6 == nil {
c.ConfigFile.EncryptedClientID, err6 = c.encryptString(plaintextClientID)
}
if err7 == nil {
c.ConfigFile.EncryptedClientSecret, err7 = c.encryptString(plaintextClientSecret)
}
2023-07-17 05:42:21 +02:00
c.mu.Unlock()
2023-07-17 03:23:26 +02:00
if write {
c.WriteConfig()
}
2024-02-09 20:48:44 +01:00
pincache.SetPin(c.useMemguard, []byte(password))
2023-07-17 03:23:26 +02:00
}
func (c *Config) GetToken() (LoginToken, error) {
if c.IsLocked() {
return LoginToken{}, errors.New("config is locked")
}
tokenJson, err := c.decryptString(c.ConfigFile.EncryptedToken)
if err != nil {
return LoginToken{}, err
}
var token LoginToken
err = json.Unmarshal([]byte(tokenJson), &token)
if err != nil {
return LoginToken{}, err
}
return token, nil
}
func (c *Config) SetToken(token LoginToken) error {
if c.IsLocked() {
return errors.New("config is locked")
}
tokenJson, err := json.Marshal(token)
encryptedToken, err := c.encryptString(string(tokenJson))
if err != nil {
return err
}
// c.mu.Lock()
c.ConfigFile.EncryptedToken = encryptedToken
// c.mu.Unlock()
c.WriteConfig()
return nil
}
2024-01-04 21:53:38 +01:00
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
}
2023-07-17 03:23:26 +02:00
func (c *Config) GetUserSymmetricKey() ([]byte, error) {
if c.IsLocked() {
return []byte{}, errors.New("config is locked")
}
decrypted, err := c.decryptString(c.ConfigFile.EncryptedUserSymmetricKey)
if err != nil {
return []byte{}, err
}
return []byte(decrypted), nil
}
func (c *Config) SetUserSymmetricKey(key []byte) error {
if c.IsLocked() {
return errors.New("config is locked")
}
encryptedKey, err := c.encryptString(string(key))
if err != nil {
return err
}
// c.mu.Lock()
c.ConfigFile.EncryptedUserSymmetricKey = encryptedKey
// c.mu.Unlock()
c.WriteConfig()
return nil
}
func (c *Config) GetMasterPasswordHash() ([]byte, error) {
if c.IsLocked() {
return []byte{}, errors.New("config is locked")
}
decrypted, err := c.decryptString(c.ConfigFile.EncryptedMasterPasswordHash)
if err != nil {
return []byte{}, err
}
return []byte(decrypted), nil
}
func (c *Config) SetMasterPasswordHash(hash []byte) error {
if c.IsLocked() {
return errors.New("config is locked")
}
encryptedHash, err := c.encryptString(string(hash))
if err != nil {
c.mu.Unlock()
return err
}
// c.mu.Lock()
c.ConfigFile.EncryptedMasterPasswordHash = encryptedHash
// c.mu.Unlock()
c.WriteConfig()
return nil
}
func (c *Config) GetMasterKey() ([]byte, error) {
if c.IsLocked() {
return []byte{}, errors.New("config is locked")
}
decrypted, err := c.decryptString(c.ConfigFile.EncryptedMasterKey)
if err != nil {
return []byte{}, err
}
return []byte(decrypted), nil
}
func (c *Config) SetMasterKey(key []byte) error {
if c.IsLocked() {
return errors.New("config is locked")
}
encryptedKey, err := c.encryptString(string(key))
if err != nil {
return err
}
// c.mu.Lock()
c.ConfigFile.EncryptedMasterKey = encryptedKey
// c.mu.Unlock()
c.WriteConfig()
return nil
}
2023-07-17 03:23:26 +02:00
func (c *Config) encryptString(data string) (string, error) {
if c.IsLocked() {
return "", errors.New("config is locked")
}
2023-12-22 12:01:21 +01:00
ca, err := subtle.NewChaCha20Poly1305((*c.key).Bytes())
2023-07-17 03:23:26 +02:00
if err != nil {
return "", err
}
result, err := ca.Encrypt([]byte(data), []byte{})
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(result), nil
}
func (c *Config) decryptString(data string) (string, error) {
if c.IsLocked() {
return "", errors.New("config is locked")
}
decoded, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return "", err
}
2023-12-22 12:01:21 +01:00
ca, err := subtle.NewChaCha20Poly1305((*c.key).Bytes())
2023-07-17 03:23:26 +02:00
if err != nil {
return "", err
}
result, err := ca.Decrypt(decoded, []byte{})
if err != nil {
return "", err
}
return string(result), nil
}
func (config *Config) WriteConfig() error {
2023-08-21 18:37:34 +02:00
if config.ConfigFile.RuntimeConfig.DoNotPersistConfig {
return nil
}
2023-07-17 03:23:26 +02:00
config.mu.Lock()
defer config.mu.Unlock()
jsonBytes, err := json.Marshal(config.ConfigFile)
if err != nil {
return err
}
// write to disk
2023-08-21 18:37:34 +02:00
os.Remove(config.ConfigFile.RuntimeConfig.ConfigDirectory)
2023-12-28 13:41:07 +01:00
parentDirectory := config.ConfigFile.RuntimeConfig.ConfigDirectory[:len(config.ConfigFile.RuntimeConfig.ConfigDirectory)-len("/goldwarden.json")]
if _, err := os.Stat(parentDirectory); os.IsNotExist(err) {
2024-02-03 23:15:29 +01:00
err := os.MkdirAll(parentDirectory, 0700)
if err != nil {
return err
}
2023-12-28 13:41:07 +01:00
}
2023-08-21 18:37:34 +02:00
file, err := os.OpenFile(config.ConfigFile.RuntimeConfig.ConfigDirectory, os.O_CREATE|os.O_WRONLY, 0600)
2023-07-17 03:23:26 +02:00
if err != nil {
return err
}
defer file.Close()
_, err = file.Write(jsonBytes)
if err != nil {
return err
}
return nil
}
2023-08-21 18:37:34 +02:00
func ReadConfig(rtCfg RuntimeConfig) (Config, error) {
2023-12-28 13:41:07 +01:00
userHome, _ := os.UserHomeDir()
oldPath := strings.ReplaceAll("~/.config/goldwarden.json", "~", userHome)
newPathParent := strings.ReplaceAll("~/.config/goldwarden", "~", userHome)
newPath := strings.ReplaceAll("~/.config/goldwarden/goldwarden.json", "~", userHome)
// Migrate old config
if _, err := os.Stat(oldPath); err == nil {
if _, err := os.Stat(newPath); err != nil {
if _, err := os.Stat(newPathParent); os.IsNotExist(err) {
os.Mkdir(newPathParent, 0700)
}
os.Rename(oldPath, newPath)
}
}
2023-08-21 18:37:34 +02:00
file, err := os.Open(rtCfg.ConfigDirectory)
2023-07-17 03:23:26 +02:00
if err != nil {
2023-12-22 12:01:21 +01:00
key := NewBuffer(32, rtCfg.UseMemguard)
2023-08-21 18:37:34 +02:00
return Config{
2023-12-22 12:01:21 +01:00
key: &key,
2023-08-21 18:37:34 +02:00
ConfigFile: ConfigFile{},
}, err
2023-07-17 03:23:26 +02:00
}
defer file.Close()
decoder := json.NewDecoder(file)
config := ConfigFile{}
err = decoder.Decode(&config)
if err != nil {
2023-12-22 12:01:21 +01:00
key := NewBuffer(32, rtCfg.UseMemguard)
2023-08-21 18:37:34 +02:00
return Config{
2023-12-22 12:01:21 +01:00
key: &key,
2023-08-21 18:37:34 +02:00
ConfigFile: ConfigFile{},
}, err
2023-07-17 03:23:26 +02:00
}
if config.ConfigKeyHash == "" {
2023-12-22 12:01:21 +01:00
key := NewBuffer(32, rtCfg.UseMemguard)
2023-08-21 18:37:34 +02:00
return Config{
2023-12-22 12:01:21 +01:00
key: &key,
2023-08-21 18:37:34 +02:00
ConfigFile: config,
}, nil
2023-07-17 03:23:26 +02:00
}
2023-12-22 12:01:21 +01:00
key := NewBuffer(32, rtCfg.UseMemguard)
2023-08-21 18:37:34 +02:00
return Config{
2023-12-22 12:01:21 +01:00
key: &key,
2023-08-21 18:37:34 +02:00
ConfigFile: config,
}, nil
2023-07-17 03:23:26 +02:00
}
func (cfg *Config) TryUnlock(vault *vault.Vault) error {
2024-02-09 20:48:44 +01:00
var pin string
if pincache.HasPin() {
pinBytes, err := pincache.GetPin()
if err != nil {
return err
}
pin = string(pinBytes)
} else {
var err error
pin, err = pinentry.GetPassword("Unlock Goldwarden", "Enter the vault PIN")
if err != nil {
return err
}
2023-07-17 03:23:26 +02:00
}
2024-02-09 20:48:44 +01:00
2023-07-17 05:42:21 +02:00
success := cfg.Unlock(pin)
if !success {
return errors.New("invalid PIN")
}
if cfg.IsLoggedIn() {
userKey, err := cfg.GetUserSymmetricKey()
if err == nil {
2023-12-22 08:02:23 +01:00
var key crypto.SymmetricEncryptionKey
var err error
if vault.Keyring.IsMemguard {
key, err = crypto.MemguardSymmetricEncryptionKeyFromBytes(userKey)
} else {
key, err = crypto.MemorySymmetricEncryptionKeyFromBytes(userKey)
}
2023-07-17 05:42:21 +02:00
if err != nil {
return err
}
2023-12-22 12:43:38 +01:00
vault.Keyring.UnlockWithAccountKey(key)
2023-07-17 05:42:21 +02:00
} else {
cfg.Lock()
2023-07-17 03:23:26 +02:00
return err
}
}
return nil
}