package config import ( cryptoSubtle "crypto/subtle" "encoding/base64" "encoding/hex" "encoding/json" "errors" "os" "runtime/debug" "sync" "github.com/awnumar/memguard" "github.com/google/uuid" "github.com/quexten/goldwarden/agent/bitwarden/crypto" "github.com/quexten/goldwarden/agent/systemauth/pinentry" "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 ( KDFIterations = 2 KDFMemory = 2 * 1024 * 1024 KDFThreads = 8 DefaultConfigPath = "~/.config/goldwarden.json" ) type RuntimeConfig struct { DisableAuth bool DisablePinRequirement bool AuthMethod string DoNotPersistConfig bool ConfigDirectory string DisableSSHAgent bool WebsocketDisabled bool ApiURI string IdentityURI string NotificationsURI string SingleProcess bool DeviceUUID string User string Password string Pin string } type ConfigFile struct { IdentityUrl string ApiUrl string NotificationsUrl string DeviceUUID string ConfigKeyHash string EncryptedToken string EncryptedUserSymmetricKey string EncryptedMasterPasswordHash string EncryptedMasterKey string RuntimeConfig RuntimeConfig `json:"-"` } 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 { key *memguard.LockedBuffer ConfigFile ConfigFile mu sync.Mutex } func DefaultConfig() Config { deviceUUID, _ := uuid.NewUUID() return Config{ memguard.NewBuffer(32), ConfigFile{ IdentityUrl: "https://vault.bitwarden.com/identity", ApiUrl: "https://vault.bitwarden.com/api", NotificationsUrl: "https://notifications.bitwarden.com", DeviceUUID: deviceUUID.String(), ConfigKeyHash: "", EncryptedToken: "", EncryptedUserSymmetricKey: "", EncryptedMasterPasswordHash: "", EncryptedMasterKey: "", RuntimeConfig: RuntimeConfig{}, }, sync.Mutex{}, } } func (c *Config) IsLocked() bool { return c.key.EqualTo(make([]byte, 32)) && c.HasPin() } func (c *Config) IsLoggedIn() bool { return c.ConfigFile.EncryptedMasterPasswordHash != "" } 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 } c.key = memguard.NewBufferFromBytes(key) return true } 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 } } func (c *Config) Lock() { c.mu.Lock() defer c.mu.Unlock() if c.IsLocked() { return } c.key.Wipe() } func (c *Config) Purge() { c.mu.Lock() defer c.mu.Unlock() c.ConfigFile.EncryptedMasterPasswordHash = "" c.ConfigFile.EncryptedToken = "" c.ConfigFile.EncryptedUserSymmetricKey = "" c.ConfigFile.ConfigKeyHash = "" c.ConfigFile.EncryptedMasterKey = "" c.key = memguard.NewBuffer(32) } 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) c.key = memguard.NewBufferFromBytes(newKey) 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) } c.mu.Unlock() if write { c.WriteConfig() } } 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 } 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 } func (c *Config) encryptString(data string) (string, error) { if c.IsLocked() { return "", errors.New("config is locked") } ca, err := subtle.NewChaCha20Poly1305(c.key.Bytes()) 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 } ca, err := subtle.NewChaCha20Poly1305(c.key.Bytes()) 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 { if config.ConfigFile.RuntimeConfig.DoNotPersistConfig { return nil } config.mu.Lock() defer config.mu.Unlock() jsonBytes, err := json.Marshal(config.ConfigFile) if err != nil { return err } // write to disk os.Remove(config.ConfigFile.RuntimeConfig.ConfigDirectory) file, err := os.OpenFile(config.ConfigFile.RuntimeConfig.ConfigDirectory, os.O_CREATE|os.O_WRONLY, 0600) if err != nil { return err } defer file.Close() _, err = file.Write(jsonBytes) if err != nil { return err } return nil } func ReadConfig(rtCfg RuntimeConfig) (Config, error) { file, err := os.Open(rtCfg.ConfigDirectory) if err != nil { return Config{ key: memguard.NewBuffer(32), ConfigFile: ConfigFile{}, }, err } defer file.Close() decoder := json.NewDecoder(file) config := ConfigFile{} err = decoder.Decode(&config) if err != nil { return Config{ key: memguard.NewBuffer(32), ConfigFile: ConfigFile{}, }, err } if config.ConfigKeyHash == "" { return Config{ key: memguard.NewBuffer(32), ConfigFile: config, }, nil } return Config{ key: memguard.NewBuffer(32), ConfigFile: config, }, nil } func (cfg *Config) TryUnlock(vault *vault.Vault) error { pin, err := pinentry.GetPassword("Unlock Goldwarden", "Enter the vault PIN") if err != nil { return err } success := cfg.Unlock(pin) if !success { return errors.New("invalid PIN") } if cfg.IsLoggedIn() { userKey, err := cfg.GetUserSymmetricKey() if err == nil { key, err := crypto.SymmetricEncryptionKeyFromBytes(userKey) if err != nil { return err } vault.Keyring.AccountKey = &key } else { cfg.Lock() return err } } return nil }