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

420 lines
9.6 KiB
Go
Raw Permalink Normal View History

2023-07-17 03:23:26 +02:00
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"
"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
DefaultConfigPath = "~/.config/goldwarden.json"
2023-07-17 03:23:26 +02:00
)
2023-08-21 18:37:34 +02:00
type RuntimeConfig struct {
DisableAuth bool
DisablePinRequirement bool
AuthMethod string
DoNotPersistConfig bool
ConfigDirectory string
DisableSSHAgent bool
WebsocketDisabled bool
ApiURI string
IdentityURI string
SingleProcess bool
DeviceUUID string
User string
Password string
Pin string
}
2023-07-17 03:23:26 +02:00
type ConfigFile struct {
IdentityUrl string
ApiUrl string
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 {
key *memguard.LockedBuffer
ConfigFile ConfigFile
mu sync.Mutex
}
func DefaultConfig() Config {
deviceUUID, _ := uuid.NewUUID()
return Config{
memguard.NewBuffer(32),
ConfigFile{
IdentityUrl: "https://identity.bitwarden.com/",
ApiUrl: "https://identity.bitwarden.com/",
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-08-21 18:37:34 +02:00
return c.key.EqualTo(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
}
c.key = memguard.NewBufferFromBytes(key)
return true
}
func (c *Config) Lock() {
c.mu.Lock()
defer c.mu.Unlock()
if c.IsLocked() {
return
}
2023-08-21 18:37:34 +02:00
c.key.Wipe()
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 = ""
c.ConfigFile.ConfigKeyHash = ""
c.ConfigFile.EncryptedMasterKey = ""
2023-07-17 03:23:26 +02:00
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)
2023-07-17 03:23:26 +02:00
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)
}
2023-07-17 05:42:21 +02:00
c.mu.Unlock()
2023-07-17 03:23:26 +02:00
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
}
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")
}
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 {
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)
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) {
file, err := os.Open(rtCfg.ConfigDirectory)
2023-07-17 03:23:26 +02:00
if err != nil {
2023-08-21 18:37:34 +02:00
return Config{
key: memguard.NewBuffer(32),
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-08-21 18:37:34 +02:00
return Config{
key: memguard.NewBuffer(32),
ConfigFile: ConfigFile{},
}, err
2023-07-17 03:23:26 +02:00
}
if config.ConfigKeyHash == "" {
2023-08-21 18:37:34 +02:00
return Config{
key: memguard.NewBuffer(32),
ConfigFile: config,
}, nil
2023-07-17 03:23:26 +02:00
}
2023-08-21 18:37:34 +02:00
return Config{
key: memguard.NewBuffer(32),
ConfigFile: config,
}, nil
2023-07-17 03:23:26 +02:00
}
func (cfg *Config) TryUnlock(vault *vault.Vault) error {
pin, err := systemauth.GetPassword("Unlock Goldwarden", "Enter the vault PIN")
if err != nil {
return err
}
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 {
key, err := crypto.SymmetricEncryptionKeyFromBytes(userKey)
if err != nil {
return err
}
vault.Keyring.AccountKey = &key
} else {
cfg.Lock()
2023-07-17 03:23:26 +02:00
return err
}
}
return nil
}