From be11f59ed85549e19d8a6ba4f583c207c6f2d59c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 22 Dec 2023 08:02:23 +0100 Subject: [PATCH 01/14] Add no memguard mode --- agent/actions/actions.go | 11 ++- agent/actions/login.go | 7 +- agent/actions/vault.go | 9 ++- agent/bitwarden/auth.go | 2 +- agent/bitwarden/crypto/crypto.go | 108 ++++++++++++++++++++++--- agent/bitwarden/crypto/encstring.go | 51 ++++++------ agent/bitwarden/crypto/keyhierarchy.go | 38 ++++++--- agent/bitwarden/crypto/keyring.go | 19 +++-- agent/bitwarden/models/models.go | 2 +- agent/bitwarden/passwordless.go | 2 +- agent/config/config.go | 11 ++- agent/defaultmainloop.go | 2 +- agent/giouimainloop.go | 2 +- agent/ssh/keys.go | 14 ++-- agent/unixsocketagent.go | 23 +++++- agent/virtualagent.go | 16 +++- main.go | 1 + 17 files changed, 241 insertions(+), 77 deletions(-) diff --git a/agent/actions/actions.go b/agent/actions/actions.go index 961e30c..7fa2ab8 100644 --- a/agent/actions/actions.go +++ b/agent/actions/actions.go @@ -56,7 +56,16 @@ func sync(ctx context.Context, vault *vault.Vault, cfg *config.Config) bool { if err != nil { return false } - protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(userSymmetricKey) + + var protectedUserSymetricKey crypto.SymmetricEncryptionKey + if vault.Keyring.IsMemguard { + protectedUserSymetricKey, err = crypto.MemguardSymmetricEncryptionKeyFromBytes(userSymmetricKey) + } else { + protectedUserSymetricKey, err = crypto.MemorySymmetricEncryptionKeyFromBytes(userSymmetricKey) + } + if err != nil { + return false + } err = bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, cfg, &protectedUserSymetricKey, true) if err != nil { diff --git a/agent/actions/login.go b/agent/actions/login.go index a751a15..4398036 100644 --- a/agent/actions/login.go +++ b/agent/actions/login.go @@ -92,7 +92,12 @@ func handleLogin(msg messages.IPCMessage, cfg *config.Config, vault *vault.Vault cfg.SetUserSymmetricKey(vault.Keyring.AccountKey.Bytes()) cfg.SetMasterPasswordHash([]byte(masterpasswordHash)) cfg.SetMasterKey([]byte(masterKey.GetBytes())) - protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(vault.Keyring.AccountKey.Bytes()) + var protectedUserSymetricKey crypto.SymmetricEncryptionKey + if vault.Keyring.IsMemguard { + protectedUserSymetricKey, err = crypto.MemguardSymmetricEncryptionKeyFromBytes(vault.Keyring.AccountKey.Bytes()) + } else { + protectedUserSymetricKey, err = crypto.MemorySymmetricEncryptionKeyFromBytes(vault.Keyring.AccountKey.Bytes()) + } if err != nil { var payload = messages.ActionResponse{ Success: false, diff --git a/agent/actions/vault.go b/agent/actions/vault.go index a035f68..57bf923 100644 --- a/agent/actions/vault.go +++ b/agent/actions/vault.go @@ -64,10 +64,17 @@ func handleUnlockVault(request messages.IPCMessage, cfg *config.Config, vault *v if err != nil { fmt.Println(err) } - safeUserSymmkey, err := crypto.SymmetricEncryptionKeyFromBytes(userSymmkey) + + var safeUserSymmkey crypto.SymmetricEncryptionKey + if vault.Keyring.IsMemguard { + safeUserSymmkey, err = crypto.MemguardSymmetricEncryptionKeyFromBytes(userSymmkey) + } else { + safeUserSymmkey, err = crypto.MemorySymmetricEncryptionKeyFromBytes(userSymmkey) + } if err != nil { fmt.Println(err) } + err = bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, cfg, &safeUserSymmkey, true) if err != nil { fmt.Println(err) diff --git a/agent/bitwarden/auth.go b/agent/bitwarden/auth.go index 15f3617..d7ee9f5 100644 --- a/agent/bitwarden/auth.go +++ b/agent/bitwarden/auth.go @@ -127,7 +127,7 @@ func LoginWithDevice(ctx context.Context, email string, cfg *config.Config, vaul for i := 0; i < 25; i++ { accessCode += string(alphabet[rand.Intn(len(alphabet))]) } - publicKey, err := crypto.GenerateAsymmetric() + publicKey, err := crypto.GenerateAsymmetric(vault.Keyring.IsMemguard) if err != nil { return LoginResponseToken{}, crypto.MasterKey{}, "", err } diff --git a/agent/bitwarden/crypto/crypto.go b/agent/bitwarden/crypto/crypto.go index 42437e5..e1f37e6 100644 --- a/agent/bitwarden/crypto/crypto.go +++ b/agent/bitwarden/crypto/crypto.go @@ -18,24 +18,51 @@ import ( var b64enc = base64.StdEncoding.Strict() -type SymmetricEncryptionKey struct { +type SymmetricEncryptionKey interface { + Bytes() []byte + EncryptionKeyBytes() ([]byte, error) + MacKeyBytes() ([]byte, error) +} + +type MemorySymmetricEncryptionKey struct { + encKey []byte + macKey []byte +} + +type MemguardSymmetricEncryptionKey struct { encKey *memguard.Enclave macKey *memguard.Enclave } -type AsymmetricEncryptionKey struct { +type AsymmetricEncryptionKey interface { + PublicBytes() []byte + PrivateBytes() ([]byte, error) +} + +type MemoryAsymmetricEncryptionKey struct { + encKey []byte +} + +type MemguardAsymmetricEncryptionKey struct { encKey *memguard.Enclave } -func SymmetricEncryptionKeyFromBytes(key []byte) (SymmetricEncryptionKey, error) { +func MemguardSymmetricEncryptionKeyFromBytes(key []byte) (MemguardSymmetricEncryptionKey, error) { if len(key) != 64 { memguard.WipeBytes(key) - return SymmetricEncryptionKey{}, fmt.Errorf("invalid key length: %d", len(key)) + return MemguardSymmetricEncryptionKey{}, fmt.Errorf("invalid key length: %d", len(key)) } - return SymmetricEncryptionKey{memguard.NewEnclave(key[0:32]), memguard.NewEnclave(key[32:64])}, nil + return MemguardSymmetricEncryptionKey{memguard.NewEnclave(key[0:32]), memguard.NewEnclave(key[32:64])}, nil } -func (key SymmetricEncryptionKey) Bytes() []byte { +func MemorySymmetricEncryptionKeyFromBytes(key []byte) (MemorySymmetricEncryptionKey, error) { + if len(key) != 64 { + return MemorySymmetricEncryptionKey{}, fmt.Errorf("invalid key length: %d", len(key)) + } + return MemorySymmetricEncryptionKey{encKey: key[0:32], macKey: key[32:64]}, nil +} + +func (key MemguardSymmetricEncryptionKey) Bytes() []byte { k1, err := key.encKey.Open() if err != nil { panic(err) @@ -50,12 +77,61 @@ func (key SymmetricEncryptionKey) Bytes() []byte { return keyBytes } -func AssymmetricEncryptionKeyFromBytes(key []byte) (AsymmetricEncryptionKey, error) { - k := memguard.NewEnclave(key) - return AsymmetricEncryptionKey{k}, nil +func (key MemorySymmetricEncryptionKey) Bytes() []byte { + keyBytes := make([]byte, 64) + copy(keyBytes[0:32], key.encKey) + copy(keyBytes[32:64], key.macKey) + return keyBytes } -func (key AsymmetricEncryptionKey) PublicBytes() []byte { +func (key MemorySymmetricEncryptionKey) EncryptionKeyBytes() ([]byte, error) { + return key.encKey, nil +} + +func (key MemguardSymmetricEncryptionKey) EncryptionKeyBytes() ([]byte, error) { + k, err := key.encKey.Open() + if err != nil { + return nil, err + } + keyBytes := make([]byte, 32) + copy(keyBytes, k.Bytes()) + return keyBytes, nil +} + +func (key MemorySymmetricEncryptionKey) MacKeyBytes() ([]byte, error) { + return key.macKey, nil +} + +func (key MemguardSymmetricEncryptionKey) MacKeyBytes() ([]byte, error) { + k, err := key.macKey.Open() + if err != nil { + return nil, err + } + keyBytes := make([]byte, 32) + copy(keyBytes, k.Bytes()) + return keyBytes, nil +} + +func MemoryAssymmetricEncryptionKeyFromBytes(key []byte) (MemoryAsymmetricEncryptionKey, error) { + return MemoryAsymmetricEncryptionKey{key}, nil +} + +func MemguardAssymmetricEncryptionKeyFromBytes(key []byte) (MemguardAsymmetricEncryptionKey, error) { + k := memguard.NewEnclave(key) + return MemguardAsymmetricEncryptionKey{k}, nil +} + +func (key MemoryAsymmetricEncryptionKey) PublicBytes() []byte { + privateKey, err := x509.ParsePKCS8PrivateKey(key.encKey) + pub := (privateKey.(*rsa.PrivateKey)).Public() + publicKeyBytes, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + panic(err) + } + return publicKeyBytes +} + +func (key MemguardAsymmetricEncryptionKey) PublicBytes() []byte { buffer, err := key.encKey.Open() if err != nil { panic(err) @@ -69,6 +145,18 @@ func (key AsymmetricEncryptionKey) PublicBytes() []byte { return publicKeyBytes } +func (key MemoryAsymmetricEncryptionKey) PrivateBytes() ([]byte, error) { + return key.encKey, nil +} + +func (key MemguardAsymmetricEncryptionKey) PrivateBytes() ([]byte, error) { + buffer, err := key.encKey.Open() + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + func isMacValid(message, messageMAC, key []byte) bool { mac := hmac.New(sha256.New, key) mac.Write(message) diff --git a/agent/bitwarden/crypto/encstring.go b/agent/bitwarden/crypto/encstring.go index b25cb1c..3c297a1 100644 --- a/agent/bitwarden/crypto/encstring.go +++ b/agent/bitwarden/crypto/encstring.go @@ -15,6 +15,8 @@ import ( "fmt" "io" "strconv" + + "github.com/awnumar/memguard" ) type EncString struct { @@ -121,16 +123,13 @@ func b64encode(src []byte) []byte { } func DecryptWith(s EncString, key SymmetricEncryptionKey) ([]byte, error) { - encKeyData, err := key.encKey.Open() - if err != nil { - return nil, err - } - macKeyData, err := key.macKey.Open() + encKeyData, err := key.MacKeyBytes() + macKeyData, err := key.MacKeyBytes() if err != nil { return nil, err } - block, err := aes.NewCipher(encKeyData.Data()) + block, err := aes.NewCipher(encKeyData) if err != nil { return nil, err } @@ -143,13 +142,13 @@ func DecryptWith(s EncString, key SymmetricEncryptionKey) ([]byte, error) { } if s.Type == AesCbc256_HmacSha256_B64 { - if len(s.MAC) == 0 || len(macKeyData.Data()) == 0 { + if len(s.MAC) == 0 || len(macKeyData) == 0 { return nil, fmt.Errorf("decrypt: cipher string type expects a MAC") } var msg []byte msg = append(msg, s.IV...) msg = append(msg, s.CT...) - if !isMacValid(msg, s.MAC, macKeyData.Data()) { + if !isMacValid(msg, s.MAC, macKeyData) { return nil, fmt.Errorf("decrypt: MAC mismatch") } } @@ -165,14 +164,8 @@ func DecryptWith(s EncString, key SymmetricEncryptionKey) ([]byte, error) { } func EncryptWith(data []byte, typ EncStringType, key SymmetricEncryptionKey) (EncString, error) { - encKeyData, err := key.encKey.Open() - if err != nil { - return EncString{}, err - } - macKeyData, err := key.macKey.Open() - if err != nil { - return EncString{}, err - } + encKeyData, err := key.EncryptionKeyBytes() + macKeyData, err := key.MacKeyBytes() s := EncString{} switch typ { @@ -183,7 +176,7 @@ func EncryptWith(data []byte, typ EncStringType, key SymmetricEncryptionKey) (En s.Type = typ data = padPKCS7(data, aes.BlockSize) - block, err := aes.NewCipher(encKeyData.Bytes()) + block, err := aes.NewCipher(encKeyData) if err != nil { return s, err } @@ -196,13 +189,13 @@ func EncryptWith(data []byte, typ EncStringType, key SymmetricEncryptionKey) (En mode.CryptBlocks(s.CT, data) if typ == AesCbc256_HmacSha256_B64 { - if len(macKeyData.Bytes()) == 0 { + if len(macKeyData) == 0 { return s, fmt.Errorf("encrypt: cipher string type expects a MAC") } var macMessage []byte macMessage = append(macMessage, s.IV...) macMessage = append(macMessage, s.CT...) - mac := hmac.New(sha256.New, macKeyData.Bytes()) + mac := hmac.New(sha256.New, macKeyData) mac.Write(macMessage) s.MAC = mac.Sum(nil) } @@ -210,27 +203,31 @@ func EncryptWith(data []byte, typ EncStringType, key SymmetricEncryptionKey) (En return s, nil } -func GenerateAsymmetric() (AsymmetricEncryptionKey, error) { +func GenerateAsymmetric(useMemguard bool) (AsymmetricEncryptionKey, error) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - return AsymmetricEncryptionKey{}, err + return MemoryAsymmetricEncryptionKey{}, err } encKey, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { - return AsymmetricEncryptionKey{}, err + return MemoryAsymmetricEncryptionKey{}, err } - return AssymmetricEncryptionKeyFromBytes(encKey) + if useMemguard { + return MemguardAsymmetricEncryptionKey{memguard.NewEnclave(encKey)}, nil + } else { + return MemoryAsymmetricEncryptionKey{encKey}, nil + } } func DecryptWithAsymmetric(s []byte, asymmetrickey AsymmetricEncryptionKey) ([]byte, error) { - key, err := asymmetrickey.encKey.Open() + key, err := asymmetrickey.PrivateBytes() if err != nil { return nil, err } - parsedKey, err := x509.ParsePKCS8PrivateKey(key.Bytes()) + parsedKey, err := x509.ParsePKCS8PrivateKey(key) if err != nil { return nil, err } @@ -248,12 +245,12 @@ func DecryptWithAsymmetric(s []byte, asymmetrickey AsymmetricEncryptionKey) ([]b } func EncryptWithAsymmetric(s []byte, asymmbetrickey AsymmetricEncryptionKey) ([]byte, error) { - key, err := asymmbetrickey.encKey.Open() + key, err := asymmbetrickey.PrivateBytes() if err != nil { return nil, err } - parsedKey, err := x509.ParsePKIXPublicKey(key.Bytes()) + parsedKey, err := x509.ParsePKIXPublicKey(key) if err != nil { return nil, err } diff --git a/agent/bitwarden/crypto/keyhierarchy.go b/agent/bitwarden/crypto/keyhierarchy.go index 170fbcf..7deb82f 100644 --- a/agent/bitwarden/crypto/keyhierarchy.go +++ b/agent/bitwarden/crypto/keyhierarchy.go @@ -23,7 +23,7 @@ func InitKeyringFromMasterKey(keyring *Keyring, accountKey EncString, accountPri switch accountKey.Type { case AesCbc256_HmacSha256_B64: - stretchedMasterKey, err := stretchKey(masterKey) + stretchedMasterKey, err := stretchKey(masterKey, keyring.IsMemguard) if err != nil { return err } @@ -36,43 +36,56 @@ func InitKeyringFromMasterKey(keyring *Keyring, accountKey EncString, accountPri return fmt.Errorf("unsupported account key type: %d", accountKey.Type) } - accountSymmetricKey, err := SymmetricEncryptionKeyFromBytes(accountSymmetricKeyByteArray) + var accountSymmetricKey SymmetricEncryptionKey + var err error + if keyring.IsMemguard { + accountSymmetricKey, err = MemguardSymmetricEncryptionKeyFromBytes(accountSymmetricKeyByteArray) + } else { + accountSymmetricKey, err = MemorySymmetricEncryptionKeyFromBytes(accountSymmetricKeyByteArray) + } if err != nil { return err } - keyring.AccountKey = &accountSymmetricKey + keyring.AccountKey = accountSymmetricKey pkcs8PrivateKey, err := DecryptWith(accountPrivateKey, accountSymmetricKey) if err != nil { return err } - keyring.AsymmetricEncyryptionKey = AsymmetricEncryptionKey{memguard.NewEnclave(pkcs8PrivateKey)} + if keyring.IsMemguard { + keyring.AsymmetricEncyryptionKey = MemguardAsymmetricEncryptionKey{memguard.NewEnclave(pkcs8PrivateKey)} + } else { + keyring.AsymmetricEncyryptionKey = MemoryAsymmetricEncryptionKey{pkcs8PrivateKey} + } keyring.OrganizationKeys = orgKeys return nil } func InitKeyringFromUserSymmetricKey(keyring *Keyring, accountSymmetricKey SymmetricEncryptionKey, accountPrivateKey EncString, orgKeys map[string]string) error { - keyring.AccountKey = &accountSymmetricKey - + keyring.AccountKey = accountSymmetricKey pkcs8PrivateKey, err := DecryptWith(accountPrivateKey, accountSymmetricKey) if err != nil { return err } - keyring.AsymmetricEncyryptionKey = AsymmetricEncryptionKey{memguard.NewEnclave(pkcs8PrivateKey)} + if keyring.IsMemguard { + keyring.AsymmetricEncyryptionKey = MemguardAsymmetricEncryptionKey{memguard.NewEnclave(pkcs8PrivateKey)} + } else { + keyring.AsymmetricEncyryptionKey = MemoryAsymmetricEncryptionKey{pkcs8PrivateKey} + } keyring.OrganizationKeys = orgKeys return nil } -func stretchKey(masterKey MasterKey) (SymmetricEncryptionKey, error) { +func stretchKey(masterKey MasterKey, useMemguard bool) (SymmetricEncryptionKey, error) { key := make([]byte, 32) macKey := make([]byte, 32) buffer, err := masterKey.encKey.Open() if err != nil { - return SymmetricEncryptionKey{}, err + return MemorySymmetricEncryptionKey{}, err } var r io.Reader @@ -80,5 +93,10 @@ func stretchKey(masterKey MasterKey) (SymmetricEncryptionKey, error) { r.Read(key) r = hkdf.Expand(sha256.New, buffer.Data(), []byte("mac")) r.Read(macKey) - return SymmetricEncryptionKey{memguard.NewEnclave(key), memguard.NewEnclave(macKey)}, nil + + if useMemguard { + return MemguardSymmetricEncryptionKey{memguard.NewEnclave(key), memguard.NewEnclave(macKey)}, nil + } else { + return MemorySymmetricEncryptionKey{encKey: key, macKey: macKey}, nil + } } diff --git a/agent/bitwarden/crypto/keyring.go b/agent/bitwarden/crypto/keyring.go index 34aae83..73d8d6d 100644 --- a/agent/bitwarden/crypto/keyring.go +++ b/agent/bitwarden/crypto/keyring.go @@ -5,12 +5,19 @@ import ( ) type Keyring struct { - AccountKey *SymmetricEncryptionKey + AccountKey SymmetricEncryptionKey AsymmetricEncyryptionKey AsymmetricEncryptionKey + IsMemguard bool OrganizationKeys map[string]string } -func NewKeyring(accountKey *SymmetricEncryptionKey) Keyring { +func NewMemoryKeyring(accountKey *MemorySymmetricEncryptionKey) Keyring { + return Keyring{ + AccountKey: accountKey, + } +} + +func NewMemguardKeyring(accountKey *MemguardSymmetricEncryptionKey) Keyring { return Keyring{ AccountKey: accountKey, } @@ -22,7 +29,7 @@ func (keyring Keyring) IsLocked() bool { func (keyring *Keyring) Lock() { keyring.AccountKey = nil - keyring.AsymmetricEncyryptionKey = AsymmetricEncryptionKey{} + keyring.AsymmetricEncyryptionKey = MemoryAsymmetricEncryptionKey{} keyring.OrganizationKeys = nil } @@ -30,10 +37,10 @@ func (keyring *Keyring) GetSymmetricKeyForOrganization(uuid string) (SymmetricEn if key, ok := keyring.OrganizationKeys[uuid]; ok { decryptedOrgKey, err := DecryptWithAsymmetric([]byte(key), keyring.AsymmetricEncyryptionKey) if err != nil { - return SymmetricEncryptionKey{}, err + return MemorySymmetricEncryptionKey{}, err } - return SymmetricEncryptionKeyFromBytes(decryptedOrgKey) + return MemorySymmetricEncryptionKeyFromBytes(decryptedOrgKey) } - return SymmetricEncryptionKey{}, errors.New("no key found for organization") + return MemorySymmetricEncryptionKey{}, errors.New("no key found for organization") } diff --git a/agent/bitwarden/models/models.go b/agent/bitwarden/models/models.go index 231d23e..6bcc327 100644 --- a/agent/bitwarden/models/models.go +++ b/agent/bitwarden/models/models.go @@ -150,5 +150,5 @@ func (cipher Cipher) GetKeyForCipher(keyring crypto.Keyring) (crypto.SymmetricEn if cipher.OrganizationID != nil { return keyring.GetSymmetricKeyForOrganization(cipher.OrganizationID.String()) } - return *keyring.AccountKey, nil + return keyring.AccountKey, nil } diff --git a/agent/bitwarden/passwordless.go b/agent/bitwarden/passwordless.go index 40c7062..5029211 100644 --- a/agent/bitwarden/passwordless.go +++ b/agent/bitwarden/passwordless.go @@ -69,7 +69,7 @@ func CreateAuthResponse(ctx context.Context, authRequest AuthRequestData, keyrin //} publicKey, err := base64.StdEncoding.DecodeString(authRequest.PublicKey) - requesterKey, err := crypto.AssymmetricEncryptionKeyFromBytes(publicKey) + requesterKey, err := crypto.MemoryAssymmetricEncryptionKeyFromBytes(publicKey) encryptedUserSymmetricKey, err := crypto.EncryptWithAsymmetric(userSymmetricKey, requesterKey) if err != nil { diff --git a/agent/config/config.go b/agent/config/config.go index c3e0400..a1c5064 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -43,6 +43,7 @@ type RuntimeConfig struct { User string Password string Pin string + UseMemguard bool } type ConfigFile struct { @@ -419,11 +420,17 @@ func (cfg *Config) TryUnlock(vault *vault.Vault) error { if cfg.IsLoggedIn() { userKey, err := cfg.GetUserSymmetricKey() if err == nil { - key, err := crypto.SymmetricEncryptionKeyFromBytes(userKey) + var key crypto.SymmetricEncryptionKey + var err error + if vault.Keyring.IsMemguard { + key, err = crypto.MemguardSymmetricEncryptionKeyFromBytes(userKey) + } else { + key, err = crypto.MemorySymmetricEncryptionKeyFromBytes(userKey) + } if err != nil { return err } - vault.Keyring.AccountKey = &key + vault.Keyring.AccountKey = key } else { cfg.Lock() return err diff --git a/agent/defaultmainloop.go b/agent/defaultmainloop.go index c90ef4e..ace84b0 100644 --- a/agent/defaultmainloop.go +++ b/agent/defaultmainloop.go @@ -1,4 +1,4 @@ -//go:build !windows && !darwin && !linux +//go:build (!windows && !darwin && !linux) || noautofill package agent diff --git a/agent/giouimainloop.go b/agent/giouimainloop.go index ee802bc..678e024 100644 --- a/agent/giouimainloop.go +++ b/agent/giouimainloop.go @@ -1,4 +1,4 @@ -//go:build windows || darwin || linux +//go:build (windows || darwin || linux) && !noautofill package agent diff --git a/agent/ssh/keys.go b/agent/ssh/keys.go index f1f1c33..ead979c 100644 --- a/agent/ssh/keys.go +++ b/agent/ssh/keys.go @@ -30,13 +30,13 @@ func NewSSHKeyCipher(name string, keyring *crypto.Keyring) (models.Cipher, strin privatePEM := pem.EncodeToMemory(&privBlock) publicKey, err := ssh.NewPublicKey(pub) - encryptedName, _ := crypto.EncryptWith([]byte(name), crypto.AesCbc256_HmacSha256_B64, *keyring.AccountKey) - encryptedPublicKeyKey, _ := crypto.EncryptWith([]byte("public-key"), crypto.AesCbc256_HmacSha256_B64, *keyring.AccountKey) - encryptedPublicKeyValue, _ := crypto.EncryptWith([]byte(string(ssh.MarshalAuthorizedKey(publicKey))), crypto.AesCbc256_HmacSha256_B64, *keyring.AccountKey) - encryptedCustomTypeKey, _ := crypto.EncryptWith([]byte("custom-type"), crypto.AesCbc256_HmacSha256_B64, *keyring.AccountKey) - encryptedCustomTypeValue, _ := crypto.EncryptWith([]byte("ssh-key"), crypto.AesCbc256_HmacSha256_B64, *keyring.AccountKey) - encryptedPrivateKeyKey, _ := crypto.EncryptWith([]byte("private-key"), crypto.AesCbc256_HmacSha256_B64, *keyring.AccountKey) - encryptedPrivateKeyValue, _ := crypto.EncryptWith(privatePEM, crypto.AesCbc256_HmacSha256_B64, *keyring.AccountKey) + encryptedName, _ := crypto.EncryptWith([]byte(name), crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) + encryptedPublicKeyKey, _ := crypto.EncryptWith([]byte("public-key"), crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) + encryptedPublicKeyValue, _ := crypto.EncryptWith([]byte(string(ssh.MarshalAuthorizedKey(publicKey))), crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) + encryptedCustomTypeKey, _ := crypto.EncryptWith([]byte("custom-type"), crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) + encryptedCustomTypeValue, _ := crypto.EncryptWith([]byte("ssh-key"), crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) + encryptedPrivateKeyKey, _ := crypto.EncryptWith([]byte("private-key"), crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) + encryptedPrivateKeyValue, _ := crypto.EncryptWith(privatePEM, crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) cipher := models.Cipher{ Type: models.CipherNote, diff --git a/agent/unixsocketagent.go b/agent/unixsocketagent.go index b4e48e1..1ee9232 100644 --- a/agent/unixsocketagent.go +++ b/agent/unixsocketagent.go @@ -99,8 +99,13 @@ type AgentState struct { func StartUnixAgent(path string, runtimeConfig config.RuntimeConfig) error { ctx := context.Background() - // check if exists - keyring := crypto.NewKeyring(nil) + var keyring crypto.Keyring + if runtimeConfig.UseMemguard { + keyring = crypto.NewMemguardKeyring(nil) + } else { + keyring = crypto.NewMemoryKeyring(nil) + } + var vault = vault.NewVault(&keyring) cfg, err := config.ReadConfig(runtimeConfig) if err != nil { @@ -137,7 +142,12 @@ func StartUnixAgent(path string, runtimeConfig config.RuntimeConfig) error { time.Sleep(60 * time.Second) continue } - protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(userSymmetricKey) + var protectedUserSymetricKey crypto.SymmetricEncryptionKey + if vault.Keyring.IsMemguard { + protectedUserSymetricKey, err = crypto.MemguardSymmetricEncryptionKeyFromBytes(userSymmetricKey) + } else { + protectedUserSymetricKey, err = crypto.MemorySymmetricEncryptionKeyFromBytes(userSymmetricKey) + } err = bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, &protectedUserSymetricKey, true) if err != nil { @@ -170,7 +180,12 @@ func StartUnixAgent(path string, runtimeConfig config.RuntimeConfig) error { if err != nil { fmt.Println(err) } - protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(userSymmetricKey) + var protectedUserSymetricKey crypto.SymmetricEncryptionKey + if vault.Keyring.IsMemguard { + protectedUserSymetricKey, err = crypto.MemguardSymmetricEncryptionKeyFromBytes(userSymmetricKey) + } else { + protectedUserSymetricKey, err = crypto.MemorySymmetricEncryptionKeyFromBytes(userSymmetricKey) + } err = bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, &protectedUserSymetricKey, true) if err != nil { diff --git a/agent/virtualagent.go b/agent/virtualagent.go index f8b3e33..642a457 100644 --- a/agent/virtualagent.go +++ b/agent/virtualagent.go @@ -74,8 +74,13 @@ func serveVirtualAgent(recv chan []byte, send chan []byte, ctx context.Context, func StartVirtualAgent(runtimeConfig config.RuntimeConfig) (chan []byte, chan []byte) { ctx := context.Background() - // check if exists - keyring := crypto.NewKeyring(nil) + var keyring crypto.Keyring + if runtimeConfig.UseMemguard { + keyring = crypto.NewMemguardKeyring(nil) + } else { + keyring = crypto.NewMemoryKeyring(nil) + } + var vault = vault.NewVault(&keyring) cfg, err := config.ReadConfig(runtimeConfig) if err != nil { @@ -106,7 +111,12 @@ func StartVirtualAgent(runtimeConfig config.RuntimeConfig) (chan []byte, chan [] if err != nil { fmt.Println(err) } - protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(userSymmetricKey) + var protectedUserSymetricKey crypto.SymmetricEncryptionKey + if keyring.IsMemguard { + protectedUserSymetricKey, err = crypto.MemguardSymmetricEncryptionKeyFromBytes(userSymmetricKey) + } else { + protectedUserSymetricKey, err = crypto.MemorySymmetricEncryptionKeyFromBytes(userSymmetricKey) + } err = bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, &protectedUserSymetricKey, true) if err != nil { diff --git a/main.go b/main.go index e65aa5f..495c347 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,7 @@ func main() { User: os.Getenv("GOLDWARDEN_AUTH_USER"), Password: os.Getenv("GOLDWARDEN_AUTH_PASSWORD"), Pin: os.Getenv("GOLDWARDEN_PIN"), + UseMemguard: os.Getenv("GOLDWARDEN_NO_MEMGUARD") != "true", ConfigDirectory: configPath, } From 82d576d5460fcfbd943d82e44bb3c4b8b344f340 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 22 Dec 2023 10:44:21 +0100 Subject: [PATCH 02/14] Fix bug in decryption --- agent/bitwarden/crypto/encstring.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/agent/bitwarden/crypto/encstring.go b/agent/bitwarden/crypto/encstring.go index 3c297a1..1eca8d7 100644 --- a/agent/bitwarden/crypto/encstring.go +++ b/agent/bitwarden/crypto/encstring.go @@ -123,7 +123,10 @@ func b64encode(src []byte) []byte { } func DecryptWith(s EncString, key SymmetricEncryptionKey) ([]byte, error) { - encKeyData, err := key.MacKeyBytes() + encKeyData, err := key.EncryptionKeyBytes() + if err != nil { + return nil, err + } macKeyData, err := key.MacKeyBytes() if err != nil { return nil, err From 3a1e47ff673be229b9c15883f35e3661dec29938 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 22 Dec 2023 10:44:49 +0100 Subject: [PATCH 03/14] Add more logging --- agent/actions/browserbiometrics.go | 6 ++++++ agent/actions/vault.go | 10 ++++++++++ agent/bitwarden/crypto/keyring.go | 7 +++++++ agent/bitwarden/sync.go | 10 +++++++--- agent/bitwarden/websocket.go | 10 ++++++---- agent/processsecurity/unix.go | 5 ++--- agent/vault/vault.go | 30 ++++++++++++++++++------------ cmd/vault.go | 28 ++++++++++++++++++++++++++++ ipc/messages/vault.go | 29 +++++++++++++++++++++++++++++ 9 files changed, 113 insertions(+), 22 deletions(-) diff --git a/agent/actions/browserbiometrics.go b/agent/actions/browserbiometrics.go index 27995e9..5f4e4d3 100644 --- a/agent/actions/browserbiometrics.go +++ b/agent/actions/browserbiometrics.go @@ -15,28 +15,33 @@ import ( ) func handleGetBiometricsKey(request messages.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx *sockets.CallingContext) (response messages.IPCMessage, err error) { + actionsLog.Info("Browser Biometrics: Key requested, verifying biometrics...") if !(systemauth.VerifyPinSession(*ctx) || biometrics.CheckBiometrics(biometrics.BrowserBiometrics)) { response, err = messages.IPCMessageFromPayload(messages.ActionResponse{ Success: false, Message: "not approved", }) + actionsLog.Info("Browser Biometrics: Biometrics not approved %v", err) if err != nil { return messages.IPCMessage{}, err } return response, nil } + actionsLog.Info("Browser Biometrics: Biometrics verified, asking for approval...") if approved, err := pinentry.GetApproval("Approve Credential Access", fmt.Sprintf("%s on %s>%s>%s is trying to access your vault encryption key for browser biometric unlock.", ctx.UserName, ctx.GrandParentProcessName, ctx.ParentProcessName, ctx.ProcessName)); err != nil || !approved { response, err = messages.IPCMessageFromPayload(messages.ActionResponse{ Success: false, Message: "not approved", }) + actionsLog.Info("Browser Biometrics: Biometrics not approved %v", err) if err != nil { return messages.IPCMessage{}, err } return response, nil } + actionsLog.Info("Browser Biometrics: Approved, getting key...") masterKey, err := cfg.GetMasterKey() if err != nil { return messages.IPCMessage{}, err @@ -45,6 +50,7 @@ func handleGetBiometricsKey(request messages.IPCMessage, cfg *config.Config, vau response, err = messages.IPCMessageFromPayload(messages.GetBiometricsKeyResponse{ Key: masterKeyB64, }) + actionsLog.Info("Browser Biometrics: Sending key...") return response, err } diff --git a/agent/actions/vault.go b/agent/actions/vault.go index 57bf923..56d12c2 100644 --- a/agent/actions/vault.go +++ b/agent/actions/vault.go @@ -185,10 +185,20 @@ func handlePinStatus(request messages.IPCMessage, cfg *config.Config, vault *vau return } +func handleVaultStatus(request messages.IPCMessage, cfg *config.Config, vault *vault.Vault, callingContext *sockets.CallingContext) (response messages.IPCMessage, err error) { + var vaultStatus messages.VaultStatusResponse = messages.VaultStatusResponse{} + vaultStatus.Locked = cfg.IsLocked() + vaultStatus.NumberOfLogins = len(vault.GetLogins()) + vaultStatus.NumberOfNotes = len(vault.GetNotes()) + response, err = messages.IPCMessageFromPayload(vaultStatus) + return +} + func init() { AgentActionsRegistry.Register(messages.MessageTypeForEmptyPayload(messages.UnlockVaultRequest{}), handleUnlockVault) AgentActionsRegistry.Register(messages.MessageTypeForEmptyPayload(messages.LockVaultRequest{}), handleLockVault) AgentActionsRegistry.Register(messages.MessageTypeForEmptyPayload(messages.WipeVaultRequest{}), handleWipeVault) AgentActionsRegistry.Register(messages.MessageTypeForEmptyPayload(messages.UpdateVaultPINRequest{}), ensureBiometricsAuthorized(systemauth.AccessVault, handleUpdateVaultPin)) AgentActionsRegistry.Register(messages.MessageTypeForEmptyPayload(messages.GetVaultPINRequest{}), handlePinStatus) + AgentActionsRegistry.Register(messages.MessageTypeForEmptyPayload(messages.VaultStatusRequest{}), handleVaultStatus) } diff --git a/agent/bitwarden/crypto/keyring.go b/agent/bitwarden/crypto/keyring.go index 73d8d6d..f00b25f 100644 --- a/agent/bitwarden/crypto/keyring.go +++ b/agent/bitwarden/crypto/keyring.go @@ -2,8 +2,12 @@ package crypto import ( "errors" + + "github.com/quexten/goldwarden/logging" ) +var keyringLog = logging.GetLogger("Goldwarden", "Keyring") + type Keyring struct { AccountKey SymmetricEncryptionKey AsymmetricEncyryptionKey AsymmetricEncryptionKey @@ -12,12 +16,14 @@ type Keyring struct { } func NewMemoryKeyring(accountKey *MemorySymmetricEncryptionKey) Keyring { + keyringLog.Info("Creating new memory keyring") return Keyring{ AccountKey: accountKey, } } func NewMemguardKeyring(accountKey *MemguardSymmetricEncryptionKey) Keyring { + keyringLog.Info("Creating new memguard keyring") return Keyring{ AccountKey: accountKey, } @@ -28,6 +34,7 @@ func (keyring Keyring) IsLocked() bool { } func (keyring *Keyring) Lock() { + keyringLog.Info("Locking keyring") keyring.AccountKey = nil keyring.AsymmetricEncyryptionKey = MemoryAsymmetricEncryptionKey{} keyring.OrganizationKeys = nil diff --git a/agent/bitwarden/sync.go b/agent/bitwarden/sync.go index 17cc978..767efd8 100644 --- a/agent/bitwarden/sync.go +++ b/agent/bitwarden/sync.go @@ -30,35 +30,39 @@ func Sync(ctx context.Context, config *config.Config) (models.SyncData, error) { func DoFullSync(ctx context.Context, vault *vault.Vault, config *config.Config, userSymmetricKey *crypto.SymmetricEncryptionKey, allowCache bool) error { log.Info("Performing full sync...") - sync, err := Sync(ctx, config) if err != nil { + log.Error("Could not sync: %v", err) if allowCache { home, _ := os.UserHomeDir() sync, err = ReadVault(home + path) } else { return err } + } else { + log.Info("Sync successful, initializing keyring and vault...") } var orgKeys map[string]string = make(map[string]string) + log.Info("Initializing %d org keys...", len(sync.Profile.Organizations)) for _, org := range sync.Profile.Organizations { orgId := org.Id.String() orgKeys[orgId] = org.Key } if userSymmetricKey != nil { + log.Info("Initializing keyring from user symmetric key...") crypto.InitKeyringFromUserSymmetricKey(vault.Keyring, *userSymmetricKey, sync.Profile.PrivateKey, orgKeys) } + log.Info("Clearing vault...") vault.Clear() + log.Info("Adding %d ciphers to vault...", len(sync.Ciphers)) for _, cipher := range sync.Ciphers { switch cipher.Type { case models.CipherLogin: vault.AddOrUpdateLogin(cipher) - break case models.CipherNote: vault.AddOrUpdateSecureNote(cipher) - break } } diff --git a/agent/bitwarden/websocket.go b/agent/bitwarden/websocket.go index 3c2a4e7..711a4d8 100644 --- a/agent/bitwarden/websocket.go +++ b/agent/bitwarden/websocket.go @@ -3,7 +3,6 @@ package bitwarden import ( "bytes" "context" - "fmt" "net/url" "os" "os/signal" @@ -49,7 +48,7 @@ const ( ) const ( - WEBSOCKET_SLEEP_DURATION_SECONDS = 5 + WEBSOCKET_SLEEP_DURATION_SECONDS = 60 ) func RunWebsocketDaemon(ctx context.Context, vault *vault.Vault, cfg *config.Config) { @@ -95,12 +94,15 @@ func connectToWebsocket(ctx context.Context, vault *vault.Vault, cfg *config.Con go func() { defer close(done) for { - mt, message, err := c.ReadMessage() - fmt.Println(mt) + _, message, err := c.ReadMessage() if err != nil { websocketLog.Error("Error reading websocket message %s", err) return } + if len(message) < 3 { + //ignore empty messages + continue + } if messageType, cipherid, success := websocketMessageType(message); success { var mt1 = NotificationMessageType(messageType) diff --git a/agent/processsecurity/unix.go b/agent/processsecurity/unix.go index f3579fc..43a1fee 100644 --- a/agent/processsecurity/unix.go +++ b/agent/processsecurity/unix.go @@ -2,8 +2,7 @@ package processsecurity -import "golang.org/x/sys/unix" - func DisableDumpable() error { - return unix.Prctl(unix.PR_SET_DUMPABLE, 0, 0, 0, 0) + // return unix.Prctl(unix.PR_SET_DUMPABLE, 0, 0, 0, 0) + return nil } diff --git a/agent/vault/vault.go b/agent/vault/vault.go index b6dec7f..8431865 100644 --- a/agent/vault/vault.go +++ b/agent/vault/vault.go @@ -7,10 +7,12 @@ import ( "github.com/quexten/goldwarden/agent/bitwarden/crypto" "github.com/quexten/goldwarden/agent/bitwarden/models" - "github.com/rs/zerolog/log" + "github.com/quexten/goldwarden/logging" "golang.org/x/exp/slices" ) +var vaultLog = logging.GetLogger("Goldwarden", "Vault") + type Vault struct { Keyring *crypto.Keyring logins map[string]models.Cipher @@ -94,7 +96,7 @@ func (vault *Vault) isEnv(cipher models.Cipher) (string, bool) { key, err := cipher.GetKeyForCipher(*vault.Keyring) if err != nil { - log.Warn().Err(err).Msg("Failed to get key for cipher " + cipher.ID.String()) + vaultLog.Error("Failed to get key for cipher "+cipher.ID.String(), err.Error()) return "", false } @@ -132,7 +134,7 @@ func (vault *Vault) isSSHKey(cipher models.Cipher) bool { key, err := cipher.GetKeyForCipher(*vault.Keyring) if err != nil { - log.Warn().Err(err).Msg("Failed to get key for cipher " + cipher.ID.String()) + vaultLog.Error("Failed to get key for cipher "+cipher.ID.String(), err.Error()) return false } @@ -140,8 +142,12 @@ func (vault *Vault) isSSHKey(cipher models.Cipher) bool { fieldName, err := crypto.DecryptWith(field.Name, key) if err != nil { cipherID := cipher.ID.String() - orgID := cipher.OrganizationID.String() - log.Warn().Err(err).Msg("Failed to decrypt field name with on cipher " + cipherID + " in organization " + orgID) + if cipher.OrganizationID != nil { + orgID := cipher.OrganizationID.String() + vaultLog.Error("Failed to decrypt field name with on cipher "+cipherID+" in organization "+orgID, err.Error()) + } else { + vaultLog.Error("Failed to decrypt field name with on cipher "+cipherID, err.Error()) + } continue } fieldValue, err := crypto.DecryptWith(field.Value, key) @@ -229,7 +235,7 @@ func (vault *Vault) GetEnvCredentialForExecutable(executableName string) (map[st if id, ok := vault.envCredentials[executableName]; ok { key, err := vault.secureNotes[id].GetKeyForCipher(*vault.Keyring) if err != nil { - log.Warn().Err(err).Msg("Failed to get key for cipher " + id) + vaultLog.Error("Failed to get key for cipher " + id) return make(map[string]string), false } @@ -302,7 +308,7 @@ func (vault *Vault) GetLoginByFilter(uuid string, orgId string, name string, use key, err := cipher.GetKeyForCipher(*vault.Keyring) if err != nil { - log.Warn().Err(err).Msg("Failed to get key for cipher " + cipher.ID.String()) + vaultLog.Error("Failed to get key for cipher " + cipher.ID.String()) continue } if name != "" { @@ -312,7 +318,7 @@ func (vault *Vault) GetLoginByFilter(uuid string, orgId string, name string, use decryptedName, err := crypto.DecryptWith(cipher.Name, key) if err != nil { - log.Warn().Err(err).Msg("Failed to decrypt name for cipher " + cipher.ID.String()) + vaultLog.Error("Failed to decrypt name for cipher " + cipher.ID.String()) continue } if name != "" && string(decryptedName) != name { @@ -327,7 +333,7 @@ func (vault *Vault) GetLoginByFilter(uuid string, orgId string, name string, use decryptedUsername, err := crypto.DecryptWith(cipher.Login.Username, key) if err != nil { - log.Warn().Err(err).Msg("Failed to decrypt username for cipher " + cipher.ID.String()) + vaultLog.Error("Failed to decrypt username for cipher " + cipher.ID.String()) continue } if username != "" && string(decryptedUsername) != username { @@ -338,7 +344,7 @@ func (vault *Vault) GetLoginByFilter(uuid string, orgId string, name string, use return cipher, nil } - return models.Cipher{}, errors.New("Cipher not found") + return models.Cipher{}, errors.New("cipher not found") } func (vault *Vault) GetNoteByFilter(uuid string, orgId string, name string) (models.Cipher, error) { @@ -355,12 +361,12 @@ func (vault *Vault) GetNoteByFilter(uuid string, orgId string, name string) (mod key, err := cipher.GetKeyForCipher(*vault.Keyring) if err != nil { - log.Warn().Err(err).Msg("Failed to get key for cipher " + cipher.ID.String()) + vaultLog.Error("Failed to get key for cipher "+cipher.ID.String(), err.Error()) continue } decryptedName, err := crypto.DecryptWith(cipher.Name, key) if err != nil { - log.Warn().Err(err).Msg("Failed to decrypt name for cipher " + cipher.ID.String()) + vaultLog.Error("Failed to decrypt name for cipher "+cipher.ID.String(), err.Error()) continue } if name != "" && string(decryptedName) != name { diff --git a/cmd/vault.go b/cmd/vault.go index 7665a96..397a13c 100644 --- a/cmd/vault.go +++ b/cmd/vault.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/quexten/goldwarden/ipc/messages" "github.com/spf13/cobra" ) @@ -89,9 +91,35 @@ var purgeCmd = &cobra.Command{ }, } +var statusCmd = &cobra.Command{ + Use: "status", + Short: "Shows the vault status", + Long: `Shows the vault status.`, + Run: func(cmd *cobra.Command, args []string) { + request := messages.VaultStatusRequest{} + + result, err := commandClient.SendToAgent(request) + if err != nil { + handleSendToAgentError(err) + return + } + + switch result.(type) { + case messages.VaultStatusResponse: + status := result.(messages.VaultStatusResponse) + fmt.Println("Locked: ", status.Locked) + fmt.Println("Number of logins: ", status.NumberOfLogins) + fmt.Println("Number of notes: ", status.NumberOfNotes) + default: + println("Wrong response type") + } + }, +} + func init() { rootCmd.AddCommand(vaultCmd) vaultCmd.AddCommand(unlockCmd) vaultCmd.AddCommand(lockCmd) vaultCmd.AddCommand(purgeCmd) + vaultCmd.AddCommand(statusCmd) } diff --git a/ipc/messages/vault.go b/ipc/messages/vault.go index 166a8cf..dff39a6 100644 --- a/ipc/messages/vault.go +++ b/ipc/messages/vault.go @@ -17,6 +17,17 @@ type WipeVaultRequest struct { type GetVaultPINRequest struct { } +type VaultStatusRequest struct { +} + +type VaultStatusResponse struct { + Locked bool + NumberOfLogins int + NumberOfNotes int + // todo websocket status + // todo last synced +} + func init() { registerPayloadParser(func(payload []byte) (interface{}, error) { var req LockVaultRequest @@ -62,4 +73,22 @@ func init() { } return req, nil }, GetVaultPINRequest{}) + + registerPayloadParser(func(payload []byte) (interface{}, error) { + var req VaultStatusRequest + err := json.Unmarshal(payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req, nil + }, VaultStatusRequest{}) + + registerPayloadParser(func(payload []byte) (interface{}, error) { + var req VaultStatusResponse + err := json.Unmarshal(payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req, nil + }, VaultStatusResponse{}) } From 189b1511f8a8d8503aff3157f4f78b3109bd71be Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 22 Dec 2023 12:01:21 +0100 Subject: [PATCH 04/14] Make memguard fully optional --- agent/bitwarden/auth.go | 3 +- agent/bitwarden/crypto/kdf.go | 8 +- agent/bitwarden/crypto/keyhierarchy.go | 2 +- agent/bitwarden/sync.go | 5 +- agent/bitwarden/websocket.go | 6 +- agent/config/config.go | 45 +++++---- agent/config/lockedbuffer.go | 48 +++++++++ agent/unixsocketagent.go | 2 +- agent/virtualagent.go | 2 +- go.mod | 44 ++++----- go.sum | 131 +++++++++++-------------- 11 files changed, 169 insertions(+), 127 deletions(-) create mode 100644 agent/config/lockedbuffer.go diff --git a/agent/bitwarden/auth.go b/agent/bitwarden/auth.go index d7ee9f5..a6e614a 100644 --- a/agent/bitwarden/auth.go +++ b/agent/bitwarden/auth.go @@ -14,7 +14,6 @@ import ( "strings" "time" - "github.com/awnumar/memguard" "github.com/quexten/goldwarden/agent/bitwarden/crypto" "github.com/quexten/goldwarden/agent/bitwarden/twofactor" "github.com/quexten/goldwarden/agent/config" @@ -81,7 +80,7 @@ func LoginWithMasterpassword(ctx context.Context, email string, cfg *config.Conf 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)}) + masterKey, err = crypto.DeriveMasterKey([]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 } diff --git a/agent/bitwarden/crypto/kdf.go b/agent/bitwarden/crypto/kdf.go index 10c4bad..a368a59 100644 --- a/agent/bitwarden/crypto/kdf.go +++ b/agent/bitwarden/crypto/kdf.go @@ -42,21 +42,19 @@ func (masterKey MasterKey) GetBytes() []byte { return bytes.Clone(buffer.Bytes()) } -func DeriveMasterKey(password memguard.LockedBuffer, email string, kdfConfig KDFConfig) (MasterKey, error) { +func DeriveMasterKey(password []byte, email string, kdfConfig KDFConfig) (MasterKey, error) { defer debug.FreeOSMemory() var key []byte switch kdfConfig.Type { case PBKDF2: - key = pbkdf2.Key(password.Bytes(), []byte(strings.ToLower(email)), int(kdfConfig.Iterations), 32, sha256.New) + key = pbkdf2.Key(password, []byte(strings.ToLower(email)), int(kdfConfig.Iterations), 32, sha256.New) case Argon2ID: var salt [32]byte = sha256.Sum256([]byte(strings.ToLower(email))) - key = argon2.IDKey(password.Bytes(), salt[:], kdfConfig.Iterations, kdfConfig.Memory*1024, uint8(kdfConfig.Parallelism), 32) + key = argon2.IDKey(password, salt[:], kdfConfig.Iterations, kdfConfig.Memory*1024, uint8(kdfConfig.Parallelism), 32) default: - password.Destroy() return MasterKey{}, fmt.Errorf("unsupported KDF type %d", kdfConfig.Type) } - password.Destroy() return MasterKey{memguard.NewEnclave(key)}, nil } diff --git a/agent/bitwarden/crypto/keyhierarchy.go b/agent/bitwarden/crypto/keyhierarchy.go index 7deb82f..8ae472a 100644 --- a/agent/bitwarden/crypto/keyhierarchy.go +++ b/agent/bitwarden/crypto/keyhierarchy.go @@ -9,7 +9,7 @@ import ( "golang.org/x/crypto/hkdf" ) -func InitKeyringFromMasterPassword(keyring *Keyring, accountKey EncString, accountPrivateKey EncString, orgKeys map[string]string, password memguard.LockedBuffer, email string, kdfConfig KDFConfig) error { +func InitKeyringFromMasterPassword(keyring *Keyring, accountKey EncString, accountPrivateKey EncString, orgKeys map[string]string, password []byte, email string, kdfConfig KDFConfig) error { masterKey, err := DeriveMasterKey(password, email, kdfConfig) if err != nil { return err diff --git a/agent/bitwarden/sync.go b/agent/bitwarden/sync.go index 767efd8..33da748 100644 --- a/agent/bitwarden/sync.go +++ b/agent/bitwarden/sync.go @@ -36,6 +36,9 @@ func DoFullSync(ctx context.Context, vault *vault.Vault, config *config.Config, if allowCache { home, _ := os.UserHomeDir() sync, err = ReadVault(home + path) + if err != nil { + return err + } } else { return err } @@ -44,7 +47,7 @@ func DoFullSync(ctx context.Context, vault *vault.Vault, config *config.Config, } var orgKeys map[string]string = make(map[string]string) - log.Info("Initializing %d org keys...", len(sync.Profile.Organizations)) + log.Info("Reading %d org keys...", len(sync.Profile.Organizations)) for _, org := range sync.Profile.Organizations { orgId := org.Id.String() orgKeys[orgId] = org.Key diff --git a/agent/bitwarden/websocket.go b/agent/bitwarden/websocket.go index 711a4d8..42ccfdf 100644 --- a/agent/bitwarden/websocket.go +++ b/agent/bitwarden/websocket.go @@ -171,7 +171,11 @@ func connectToWebsocket(ctx context.Context, vault *vault.Vault, cfg *config.Con break case LogOut: websocketLog.Info("LogOut received. Wiping vault and exiting...") - memguard.SafeExit(0) + if vault.Keyring.IsMemguard { + memguard.SafeExit(0) + } else { + os.Exit(0) + } case AuthRequest: websocketLog.Info("AuthRequest received" + string(cipherid)) authRequest, err := GetAuthRequest(context.WithValue(ctx, AuthToken{}, token.AccessToken), cipherid, cfg) diff --git a/agent/config/config.go b/agent/config/config.go index a1c5064..fe15453 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -1,6 +1,7 @@ package config import ( + "bytes" cryptoSubtle "crypto/subtle" "encoding/base64" "encoding/hex" @@ -10,7 +11,6 @@ import ( "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" @@ -68,15 +68,18 @@ type LoginToken struct { } type Config struct { - key *memguard.LockedBuffer - ConfigFile ConfigFile - mu sync.Mutex + useMemguard bool + key *LockedBuffer + ConfigFile ConfigFile + mu sync.Mutex } -func DefaultConfig() Config { +func DefaultConfig(useMemguard bool) Config { deviceUUID, _ := uuid.NewUUID() + keyBuffer := NewBuffer(32, useMemguard) return Config{ - memguard.NewBuffer(32), + useMemguard, + &keyBuffer, ConfigFile{ IdentityUrl: "https://vault.bitwarden.com/identity", ApiUrl: "https://vault.bitwarden.com/api", @@ -94,7 +97,8 @@ func DefaultConfig() Config { } func (c *Config) IsLocked() bool { - return c.key.EqualTo(make([]byte, 32)) && c.HasPin() + key := (*c.key).Bytes() + return bytes.Equal(key, make([]byte, 32)) && c.HasPin() } func (c *Config) IsLoggedIn() bool { @@ -117,7 +121,8 @@ func (c *Config) Unlock(password string) bool { return false } - c.key = memguard.NewBufferFromBytes(key) + keyBuffer := NewBufferFromBytes(key, c.useMemguard) + c.key = &keyBuffer return true } @@ -140,7 +145,7 @@ func (c *Config) Lock() { if c.IsLocked() { return } - c.key.Wipe() + (*c.key).Wipe() } func (c *Config) Purge() { @@ -152,7 +157,8 @@ func (c *Config) Purge() { c.ConfigFile.EncryptedUserSymmetricKey = "" c.ConfigFile.ConfigKeyHash = "" c.ConfigFile.EncryptedMasterKey = "" - c.key = memguard.NewBuffer(32) + key := NewBuffer(32, c.useMemguard) + c.key = &key } func (c *Config) HasPin() bool { @@ -174,7 +180,8 @@ func (c *Config) UpdatePin(password string, write bool) { plaintextEncryptedMasterPasswordHash, err4 := c.decryptString(c.ConfigFile.EncryptedMasterPasswordHash) plaintextMasterKey, err5 := c.decryptString(c.ConfigFile.EncryptedMasterKey) - c.key = memguard.NewBufferFromBytes(newKey) + key := NewBufferFromBytes(newKey, c.useMemguard) + c.key = &key if err1 == nil { c.ConfigFile.EncryptedToken, err1 = c.encryptString(plaintextToken) @@ -315,7 +322,7 @@ func (c *Config) encryptString(data string) (string, error) { if c.IsLocked() { return "", errors.New("config is locked") } - ca, err := subtle.NewChaCha20Poly1305(c.key.Bytes()) + ca, err := subtle.NewChaCha20Poly1305((*c.key).Bytes()) if err != nil { return "", err } @@ -337,7 +344,7 @@ func (c *Config) decryptString(data string) (string, error) { return "", err } - ca, err := subtle.NewChaCha20Poly1305(c.key.Bytes()) + ca, err := subtle.NewChaCha20Poly1305((*c.key).Bytes()) if err != nil { return "", err } @@ -379,8 +386,9 @@ func (config *Config) WriteConfig() error { func ReadConfig(rtCfg RuntimeConfig) (Config, error) { file, err := os.Open(rtCfg.ConfigDirectory) if err != nil { + key := NewBuffer(32, rtCfg.UseMemguard) return Config{ - key: memguard.NewBuffer(32), + key: &key, ConfigFile: ConfigFile{}, }, err } @@ -390,19 +398,22 @@ func ReadConfig(rtCfg RuntimeConfig) (Config, error) { config := ConfigFile{} err = decoder.Decode(&config) if err != nil { + key := NewBuffer(32, rtCfg.UseMemguard) return Config{ - key: memguard.NewBuffer(32), + key: &key, ConfigFile: ConfigFile{}, }, err } if config.ConfigKeyHash == "" { + key := NewBuffer(32, rtCfg.UseMemguard) return Config{ - key: memguard.NewBuffer(32), + key: &key, ConfigFile: config, }, nil } + key := NewBuffer(32, rtCfg.UseMemguard) return Config{ - key: memguard.NewBuffer(32), + key: &key, ConfigFile: config, }, nil } diff --git a/agent/config/lockedbuffer.go b/agent/config/lockedbuffer.go new file mode 100644 index 0000000..bf27b9f --- /dev/null +++ b/agent/config/lockedbuffer.go @@ -0,0 +1,48 @@ +package config + +import "github.com/awnumar/memguard" + +type LockedBuffer interface { + Bytes() []byte + Wipe() +} + +func NewBuffer(size int, useMemguard bool) LockedBuffer { + if useMemguard { + return MemGuardLockedBuffer{memguard.NewBuffer(size)} + } + return MemoryLockedBuffer{make([]byte, size)} +} + +func NewBufferFromBytes(bytes []byte, useMemguard bool) LockedBuffer { + if useMemguard { + return MemGuardLockedBuffer{memguard.NewBufferFromBytes(bytes)} + } + return MemoryLockedBuffer{bytes} +} + +type MemGuardLockedBuffer struct { + buffer *memguard.LockedBuffer +} + +func (b MemGuardLockedBuffer) Wipe() { + b.buffer.Destroy() +} + +func (b MemGuardLockedBuffer) Bytes() []byte { + return b.buffer.Bytes() +} + +type MemoryLockedBuffer struct { + buffer []byte +} + +func (b MemoryLockedBuffer) Wipe() { + for i := range b.buffer { + b.buffer[i] = 0 + } +} + +func (b MemoryLockedBuffer) Bytes() []byte { + return b.buffer +} diff --git a/agent/unixsocketagent.go b/agent/unixsocketagent.go index 1ee9232..5fdc220 100644 --- a/agent/unixsocketagent.go +++ b/agent/unixsocketagent.go @@ -110,7 +110,7 @@ func StartUnixAgent(path string, runtimeConfig config.RuntimeConfig) error { cfg, err := config.ReadConfig(runtimeConfig) if err != nil { log.Warn("Could not read config: %s", err.Error()) - cfg = config.DefaultConfig() + cfg = config.DefaultConfig(runtimeConfig.UseMemguard) cfg.ConfigFile.RuntimeConfig = runtimeConfig cfg.WriteConfig() } diff --git a/agent/virtualagent.go b/agent/virtualagent.go index 642a457..d14562c 100644 --- a/agent/virtualagent.go +++ b/agent/virtualagent.go @@ -84,7 +84,7 @@ func StartVirtualAgent(runtimeConfig config.RuntimeConfig) (chan []byte, chan [] var vault = vault.NewVault(&keyring) cfg, err := config.ReadConfig(runtimeConfig) if err != nil { - var cfg = config.DefaultConfig() + var cfg = config.DefaultConfig(runtimeConfig.UseMemguard) cfg.WriteConfig() } cfg.ConfigFile.RuntimeConfig = runtimeConfig diff --git a/go.mod b/go.mod index 21b8c7a..59ce90b 100644 --- a/go.mod +++ b/go.mod @@ -7,22 +7,21 @@ require ( github.com/LlamaNite/llamalog v0.2.1 github.com/amenzhinsky/go-polkit v0.0.0-20210519083301-ee6a51849123 github.com/atotto/clipboard v0.1.4 - github.com/awnumar/memguard v0.22.3 - github.com/bendahl/uinput v1.6.2 - github.com/google/uuid v1.3.0 - github.com/gorilla/websocket v1.5.0 + github.com/awnumar/memguard v0.22.4 + github.com/bendahl/uinput v1.7.0 + github.com/google/uuid v1.5.0 + github.com/gorilla/websocket v1.5.1 github.com/lox/go-touchid v0.0.0-20170712105233-619cc8e578d0 github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a github.com/mitchellh/go-ps v1.0.0 github.com/pquerna/otp v1.4.0 - github.com/shirou/gopsutil/v3 v3.23.6 - github.com/spf13/cobra v1.7.0 - github.com/tink-crypto/tink-go/v2 v2.0.0 - github.com/twpayne/go-pinentry v0.2.0 - github.com/vmihailenco/msgpack/v5 v5.3.5 - golang.org/x/crypto v0.13.0 - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 - golang.org/x/sys v0.12.0 + github.com/shirou/gopsutil/v3 v3.23.11 + github.com/spf13/cobra v1.8.0 + github.com/tink-crypto/tink-go/v2 v2.1.0 + github.com/twpayne/go-pinentry v0.3.0 + github.com/vmihailenco/msgpack/v5 v5.4.1 + golang.org/x/crypto v0.17.0 + golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 ) require ( @@ -34,25 +33,26 @@ require ( github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/tklauser/go-sysconf v0.3.11 // indirect - github.com/tklauser/numcpus v0.6.0 // indirect + github.com/tklauser/go-sysconf v0.3.13 // indirect + github.com/tklauser/numcpus v0.7.0 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect - golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 // indirect - golang.org/x/image v0.5.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/exp/shiny v0.0.0-20231219180239-dc181d75b848 // indirect + golang.org/x/image v0.14.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect ) require ( - github.com/awnumar/memcall v0.1.2 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/awnumar/memcall v0.2.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/keys-pub/go-libfido2 v1.5.3 github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rs/zerolog v1.29.1 + github.com/rs/zerolog v1.31.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - go.uber.org/multierr v1.11.0 // indirect inet.af/peercred v0.0.0-20210906144145-0893ea02156a ) diff --git a/go.sum b/go.sum index c43b618..3ebe52e 100644 --- a/go.sum +++ b/go.sum @@ -8,20 +8,22 @@ gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y= gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= github.com/LlamaNite/llamalog v0.2.1 h1:k9XugHmyQqJhCrogca808Jl2rrEKIWMtWyLKX+xX9Mg= github.com/LlamaNite/llamalog v0.2.1/go.mod h1:zopgmWk8utZPfZCPa/uvQkv99Lan3pRrw/9inbIYZeo= +github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/amenzhinsky/go-polkit v0.0.0-20210519083301-ee6a51849123 h1:VdNhe94PF9yn6KudYnpcBb6bH7l+wsEy9yn6Ulm1/j8= github.com/amenzhinsky/go-polkit v0.0.0-20210519083301-ee6a51849123/go.mod h1:CdMR3dsiNi5M2BbtFlMo85mRbNt6LiMw04UBzJmoVEU= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/awnumar/memcall v0.1.2 h1:7gOfDTL+BJ6nnbtAp9+HQzUFjtP1hEseRQq8eP055QY= -github.com/awnumar/memcall v0.1.2/go.mod h1:S911igBPR9CThzd/hYQQmTc9SWNu3ZHIlCGaWsWsoJo= -github.com/awnumar/memguard v0.22.3 h1:b4sgUXtbUjhrGELPbuC62wU+BsPQy+8lkWed9Z+pj0Y= -github.com/awnumar/memguard v0.22.3/go.mod h1:mmGunnffnLHlxE5rRgQc3j+uwPZ27eYb61ccr8Clz2Y= -github.com/bendahl/uinput v1.6.2 h1:tIz52QyKDx1i1nObUkts3AZa/bULfLhPA5a+xKGlRPI= -github.com/bendahl/uinput v1.6.2/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8= +github.com/awnumar/memcall v0.2.0 h1:sRaogqExTOOkkNwO9pzJsL8jrOV29UuUW7teRMfbqtI= +github.com/awnumar/memcall v0.2.0/go.mod h1:S911igBPR9CThzd/hYQQmTc9SWNu3ZHIlCGaWsWsoJo= +github.com/awnumar/memguard v0.22.4 h1:1PLgKcgGPeExPHL8dCOWGVjIbQUBgJv9OL0F/yE1PqQ= +github.com/awnumar/memguard v0.22.4/go.mod h1:+APmZGThMBWjnMlKiSM1X7MVpbIVewen2MTkqWkA/zE= +github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg= +github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -35,12 +37,14 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/keys-pub/go-libfido2 v1.5.3 h1:vtgHxlSB43u6lj0TSuA3VvT6z3E7VI+L1a2hvMFdECk= @@ -49,13 +53,12 @@ github.com/lox/go-touchid v0.0.0-20170712105233-619cc8e578d0 h1:m81erW+1MD5vl3lK github.com/lox/go-touchid v0.0.0-20170712105233-619cc8e578d0/go.mod h1:EHbIQzfC3kdWFI81pLOFjssnolF+ALfmVf8PUdWBxo4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= @@ -68,18 +71,18 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= -github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v3 v3.23.6 h1:5y46WPI9QBKBbK7EEccUPNXpJpNrvPuTD0O2zHEHT08= -github.com/shirou/gopsutil/v3 v3.23.6/go.mod h1:j7QX50DrXYggrpN30W0Mo+I4/8U2UUIQrnrhqUeWrAU= +github.com/shirou/gopsutil/v3 v3.23.11 h1:i3jP9NjCPUz7FiZKxlMnODZkdSIp2gnzfrvsu9CuWEQ= +github.com/shirou/gopsutil/v3 v3.23.11/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -87,77 +90,53 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/tink-crypto/tink-go/v2 v2.0.0 h1:LutFJapahsM0i/6hKfOkzSYTVeshmFs+jloZXqe9z9s= -github.com/tink-crypto/tink-go/v2 v2.0.0/go.mod h1:QAbyq9LZncomYnScxlfaHImbV4ieNIe6bnu/Xcqqox4= -github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= -github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= -github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= -github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= -github.com/twpayne/go-pinentry v0.2.0 h1:hS5NEJiilop9xP9pBX/1NYduzDlGGMdg1KamTBTrOWw= -github.com/twpayne/go-pinentry v0.2.0/go.mod h1:r6buhMwARxnnL0VRBqfd1tE6Fadk1kfP00GRMutEspY= -github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/tink-crypto/tink-go/v2 v2.1.0 h1:QXFBguwMwTIaU17EgZpEJWsUSc60b1BAGTzBIoMdmok= +github.com/tink-crypto/tink-go/v2 v2.1.0/go.mod h1:y1TnYFt1i2eZVfx4OGc+C+EMp4CoKWAw2VSEuoicHHI= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= +github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= +github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= +github.com/twpayne/go-pinentry v0.3.0 h1:Rr+fEOZXmeItOb4thjeVaBWJKB9Xa/eojolycyF/26c= +github.com/twpayne/go-pinentry v0.3.0/go.mod h1:iOIZD+9np/2V24OdCGos7Y1/xX90wc6VEAZsgb+r9D4= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0= -golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8= -golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= -golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp/shiny v0.0.0-20231219180239-dc181d75b848 h1:LnDWUUS+bxOesHc+QBvFFmS4v0ZH+Vtg0EncMANwN9Q= +golang.org/x/exp/shiny v0.0.0-20231219180239-dc181d75b848/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= +golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= +golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From ebe3dd44b538217a37a25edfef0406a0b05dbafb Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 22 Dec 2023 12:09:50 +0100 Subject: [PATCH 05/14] Update websocket backoff behaviour --- agent/bitwarden/websocket.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/agent/bitwarden/websocket.go b/agent/bitwarden/websocket.go index 42ccfdf..5330a01 100644 --- a/agent/bitwarden/websocket.go +++ b/agent/bitwarden/websocket.go @@ -52,10 +52,10 @@ const ( ) func RunWebsocketDaemon(ctx context.Context, vault *vault.Vault, cfg *config.Config) { + time.Sleep(5 * time.Second) for { - time.Sleep(WEBSOCKET_SLEEP_DURATION_SECONDS * time.Second) - if cfg.IsLocked() { + time.Sleep(5 * time.Second) continue } @@ -65,6 +65,8 @@ func RunWebsocketDaemon(ctx context.Context, vault *vault.Vault, cfg *config.Con websocketLog.Error("Websocket error %s", err) } } + + time.Sleep(WEBSOCKET_SLEEP_DURATION_SECONDS * time.Second) } } From 6311d6fbac393b32ac525d873b166d4b87b890bc Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 22 Dec 2023 12:43:38 +0100 Subject: [PATCH 06/14] Fix keyring locked detection --- agent/actions/login.go | 6 +++--- agent/bitwarden/crypto/keyhierarchy.go | 4 ++-- agent/bitwarden/crypto/keyring.go | 24 +++++++++++++++++++----- agent/bitwarden/models/models.go | 2 +- agent/config/config.go | 2 +- agent/ssh/keys.go | 14 +++++++------- 6 files changed, 33 insertions(+), 19 deletions(-) diff --git a/agent/actions/login.go b/agent/actions/login.go index 4398036..dbe50de 100644 --- a/agent/actions/login.go +++ b/agent/actions/login.go @@ -89,14 +89,14 @@ func handleLogin(msg messages.IPCMessage, cfg *config.Config, vault *vault.Vault return } - cfg.SetUserSymmetricKey(vault.Keyring.AccountKey.Bytes()) + cfg.SetUserSymmetricKey(vault.Keyring.GetAccountKey().Bytes()) cfg.SetMasterPasswordHash([]byte(masterpasswordHash)) cfg.SetMasterKey([]byte(masterKey.GetBytes())) var protectedUserSymetricKey crypto.SymmetricEncryptionKey if vault.Keyring.IsMemguard { - protectedUserSymetricKey, err = crypto.MemguardSymmetricEncryptionKeyFromBytes(vault.Keyring.AccountKey.Bytes()) + protectedUserSymetricKey, err = crypto.MemguardSymmetricEncryptionKeyFromBytes(vault.Keyring.GetAccountKey().Bytes()) } else { - protectedUserSymetricKey, err = crypto.MemorySymmetricEncryptionKeyFromBytes(vault.Keyring.AccountKey.Bytes()) + protectedUserSymetricKey, err = crypto.MemorySymmetricEncryptionKeyFromBytes(vault.Keyring.GetAccountKey().Bytes()) } if err != nil { var payload = messages.ActionResponse{ diff --git a/agent/bitwarden/crypto/keyhierarchy.go b/agent/bitwarden/crypto/keyhierarchy.go index 8ae472a..d0dc398 100644 --- a/agent/bitwarden/crypto/keyhierarchy.go +++ b/agent/bitwarden/crypto/keyhierarchy.go @@ -47,7 +47,7 @@ func InitKeyringFromMasterKey(keyring *Keyring, accountKey EncString, accountPri return err } - keyring.AccountKey = accountSymmetricKey + keyring.UnlockWithAccountKey(accountSymmetricKey) pkcs8PrivateKey, err := DecryptWith(accountPrivateKey, accountSymmetricKey) if err != nil { @@ -64,7 +64,7 @@ func InitKeyringFromMasterKey(keyring *Keyring, accountKey EncString, accountPri } func InitKeyringFromUserSymmetricKey(keyring *Keyring, accountSymmetricKey SymmetricEncryptionKey, accountPrivateKey EncString, orgKeys map[string]string) error { - keyring.AccountKey = accountSymmetricKey + keyring.UnlockWithAccountKey(accountSymmetricKey) pkcs8PrivateKey, err := DecryptWith(accountPrivateKey, accountSymmetricKey) if err != nil { return err diff --git a/agent/bitwarden/crypto/keyring.go b/agent/bitwarden/crypto/keyring.go index f00b25f..58acd33 100644 --- a/agent/bitwarden/crypto/keyring.go +++ b/agent/bitwarden/crypto/keyring.go @@ -9,7 +9,8 @@ import ( var keyringLog = logging.GetLogger("Goldwarden", "Keyring") type Keyring struct { - AccountKey SymmetricEncryptionKey + isLocked bool + accountKey SymmetricEncryptionKey AsymmetricEncyryptionKey AsymmetricEncryptionKey IsMemguard bool OrganizationKeys map[string]string @@ -18,28 +19,41 @@ type Keyring struct { func NewMemoryKeyring(accountKey *MemorySymmetricEncryptionKey) Keyring { keyringLog.Info("Creating new memory keyring") return Keyring{ - AccountKey: accountKey, + isLocked: accountKey == nil, + accountKey: accountKey, } } func NewMemguardKeyring(accountKey *MemguardSymmetricEncryptionKey) Keyring { keyringLog.Info("Creating new memguard keyring") return Keyring{ - AccountKey: accountKey, + isLocked: accountKey == nil, + accountKey: accountKey, } } func (keyring Keyring) IsLocked() bool { - return keyring.AccountKey == nil + return keyring.isLocked } func (keyring *Keyring) Lock() { keyringLog.Info("Locking keyring") - keyring.AccountKey = nil + keyring.isLocked = true + keyring.accountKey = nil keyring.AsymmetricEncyryptionKey = MemoryAsymmetricEncryptionKey{} keyring.OrganizationKeys = nil } +func (keyring *Keyring) UnlockWithAccountKey(accountKey SymmetricEncryptionKey) { + keyringLog.Info("Unlocking keyring with account key") + keyring.isLocked = false + keyring.accountKey = accountKey +} + +func (keyring *Keyring) GetAccountKey() SymmetricEncryptionKey { + return keyring.accountKey +} + func (keyring *Keyring) GetSymmetricKeyForOrganization(uuid string) (SymmetricEncryptionKey, error) { if key, ok := keyring.OrganizationKeys[uuid]; ok { decryptedOrgKey, err := DecryptWithAsymmetric([]byte(key), keyring.AsymmetricEncyryptionKey) diff --git a/agent/bitwarden/models/models.go b/agent/bitwarden/models/models.go index 6bcc327..5e7075e 100644 --- a/agent/bitwarden/models/models.go +++ b/agent/bitwarden/models/models.go @@ -150,5 +150,5 @@ func (cipher Cipher) GetKeyForCipher(keyring crypto.Keyring) (crypto.SymmetricEn if cipher.OrganizationID != nil { return keyring.GetSymmetricKeyForOrganization(cipher.OrganizationID.String()) } - return keyring.AccountKey, nil + return keyring.GetAccountKey(), nil } diff --git a/agent/config/config.go b/agent/config/config.go index fe15453..d70dc14 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -441,7 +441,7 @@ func (cfg *Config) TryUnlock(vault *vault.Vault) error { if err != nil { return err } - vault.Keyring.AccountKey = key + vault.Keyring.UnlockWithAccountKey(key) } else { cfg.Lock() return err diff --git a/agent/ssh/keys.go b/agent/ssh/keys.go index ead979c..235a3f0 100644 --- a/agent/ssh/keys.go +++ b/agent/ssh/keys.go @@ -30,13 +30,13 @@ func NewSSHKeyCipher(name string, keyring *crypto.Keyring) (models.Cipher, strin privatePEM := pem.EncodeToMemory(&privBlock) publicKey, err := ssh.NewPublicKey(pub) - encryptedName, _ := crypto.EncryptWith([]byte(name), crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) - encryptedPublicKeyKey, _ := crypto.EncryptWith([]byte("public-key"), crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) - encryptedPublicKeyValue, _ := crypto.EncryptWith([]byte(string(ssh.MarshalAuthorizedKey(publicKey))), crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) - encryptedCustomTypeKey, _ := crypto.EncryptWith([]byte("custom-type"), crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) - encryptedCustomTypeValue, _ := crypto.EncryptWith([]byte("ssh-key"), crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) - encryptedPrivateKeyKey, _ := crypto.EncryptWith([]byte("private-key"), crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) - encryptedPrivateKeyValue, _ := crypto.EncryptWith(privatePEM, crypto.AesCbc256_HmacSha256_B64, keyring.AccountKey) + encryptedName, _ := crypto.EncryptWith([]byte(name), crypto.AesCbc256_HmacSha256_B64, keyring.GetAccountKey()) + encryptedPublicKeyKey, _ := crypto.EncryptWith([]byte("public-key"), crypto.AesCbc256_HmacSha256_B64, keyring.GetAccountKey()) + encryptedPublicKeyValue, _ := crypto.EncryptWith([]byte(string(ssh.MarshalAuthorizedKey(publicKey))), crypto.AesCbc256_HmacSha256_B64, keyring.GetAccountKey()) + encryptedCustomTypeKey, _ := crypto.EncryptWith([]byte("custom-type"), crypto.AesCbc256_HmacSha256_B64, keyring.GetAccountKey()) + encryptedCustomTypeValue, _ := crypto.EncryptWith([]byte("ssh-key"), crypto.AesCbc256_HmacSha256_B64, keyring.GetAccountKey()) + encryptedPrivateKeyKey, _ := crypto.EncryptWith([]byte("private-key"), crypto.AesCbc256_HmacSha256_B64, keyring.GetAccountKey()) + encryptedPrivateKeyValue, _ := crypto.EncryptWith(privatePEM, crypto.AesCbc256_HmacSha256_B64, keyring.GetAccountKey()) cipher := models.Cipher{ Type: models.CipherNote, From dcc2fb8bdc38a05aeca974c0a35c1695717a15f4 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 22 Dec 2023 15:29:58 +0100 Subject: [PATCH 07/14] Add debug logging --- .gitignore | 1 - browserbiometrics/communication.go | 2 +- browserbiometrics/logging/debuglogger.go | 32 ++++++++++++++++++++++++ browserbiometrics/logging/nooplogger.go | 2 +- browserbiometrics/main.go | 4 +++ browserbiometrics/protocol.go | 2 ++ 6 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 browserbiometrics/logging/debuglogger.go diff --git a/.gitignore b/.gitignore index 26bab9a..034f749 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ .vscode -*debug* goldwarden* \ No newline at end of file diff --git a/browserbiometrics/communication.go b/browserbiometrics/communication.go index 534cc6d..89d95b4 100644 --- a/browserbiometrics/communication.go +++ b/browserbiometrics/communication.go @@ -52,7 +52,7 @@ func readMessageLength(msg []byte) int { func send(msg SendMessage) { byteMsg := dataToBytes(msg) - logging.Debugf("Sending message: " + string(byteMsg)) + logging.Debugf("[SENSITIVE] Sending message: " + string(byteMsg)) writeMessageLength(byteMsg) var msgBuf bytes.Buffer diff --git a/browserbiometrics/logging/debuglogger.go b/browserbiometrics/logging/debuglogger.go new file mode 100644 index 0000000..9b6966c --- /dev/null +++ b/browserbiometrics/logging/debuglogger.go @@ -0,0 +1,32 @@ +//go:build !nooplogging + +package logging + +import ( + "fmt" + "os" + "time" +) + +func writeLog(level string, format string, args ...interface{}) { + file, _ := os.OpenFile("/tmp/DELETE_ME.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + // log with date and time + file.WriteString("[" + level + "] ") + file.WriteString(time.Now().Format("2006-01-02 15:04:05") + " ") + file.WriteString(fmt.Sprintf(format, args...)) + file.WriteString("\n") + file.Close() +} + +func Debugf(format string, args ...interface{}) { + writeLog("DEBUG", format, args...) +} + +func Errorf(format string, args ...interface{}) { + writeLog("ERROR", format, args...) +} + +func Panicf(format string, args ...interface{}) { + writeLog("PANIC", format, args...) + panic("") +} diff --git a/browserbiometrics/logging/nooplogger.go b/browserbiometrics/logging/nooplogger.go index a769ab2..7a5bdd7 100644 --- a/browserbiometrics/logging/nooplogger.go +++ b/browserbiometrics/logging/nooplogger.go @@ -1,4 +1,4 @@ -//go:build !logging +//go:build nooplogging package logging diff --git a/browserbiometrics/main.go b/browserbiometrics/main.go index d3b7bab..be3617e 100644 --- a/browserbiometrics/main.go +++ b/browserbiometrics/main.go @@ -5,6 +5,8 @@ import ( "os" "path/filepath" "strings" + + "github.com/quexten/goldwarden/browserbiometrics/logging" ) const appID = "com.quexten.bw-bio-handler" @@ -25,7 +27,9 @@ func Main() { return } + logging.Debugf("Starting browserbiometrics") transportKey = generateTransportKey() + logging.Debugf("Generated transport key") setupCommunication() readLoop() diff --git a/browserbiometrics/protocol.go b/browserbiometrics/protocol.go index 050b16e..ecf97b0 100644 --- a/browserbiometrics/protocol.go +++ b/browserbiometrics/protocol.go @@ -18,11 +18,13 @@ func readLoop() { lengthBytes := make([]byte, 4) lengthNum := int(0) + logging.Debugf("Sending connected message") send(SendMessage{ Command: "connected", AppID: appID, }) + logging.Debugf("Starting read loop") for b, err := s.Read(lengthBytes); b > 0 && err == nil; b, err = s.Read(lengthBytes) { lengthNum = readMessageLength(lengthBytes) From 6f30d75b081509179dc994ea16c08fce1c3062de Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 23 Dec 2023 07:18:30 +0100 Subject: [PATCH 08/14] Add prototype GTK UI --- .gitignore | 3 +- Readme.md | 4 +- agent/actions/logins.go | 12 +- autofill/autofill.go | 78 ---- autofill/autotype/uinput/dvorak.go | 384 ----------------- autofill/autotype/uinput/qwerty.go | 387 ------------------ autofill/autotype/uinput/uinput.go | 175 -------- autofill/autotype/uinputautotype.go | 9 - autofill/autotype/unimplemented.go | 7 - autofill/gioAutofillDialog.go | 336 --------------- .../libportalautotype.go | 4 +- cmd/autofill.go | 19 +- cmd/logins.go | 61 +++ cmd/vault.go | 8 +- go.mod | 3 + go.sum | 6 + ui/autotype/autotype.py | 101 +++++ ui/clipboard.py | 5 + ui/goldwarden.py | 87 ++++ ui/main.py | 286 +++++++++++++ ui/monitors/dbus_autofill_monitor.py | 23 ++ ui/monitors/global_shortcut_portal.py | 1 + 22 files changed, 605 insertions(+), 1394 deletions(-) delete mode 100644 autofill/autofill.go delete mode 100644 autofill/autotype/uinput/dvorak.go delete mode 100644 autofill/autotype/uinput/qwerty.go delete mode 100644 autofill/autotype/uinput/uinput.go delete mode 100644 autofill/autotype/uinputautotype.go delete mode 100644 autofill/autotype/unimplemented.go delete mode 100644 autofill/gioAutofillDialog.go rename {autofill/autotype => autotype}/libportalautotype.go (96%) create mode 100644 ui/autotype/autotype.py create mode 100644 ui/clipboard.py create mode 100644 ui/goldwarden.py create mode 100644 ui/main.py create mode 100644 ui/monitors/dbus_autofill_monitor.py create mode 100644 ui/monitors/global_shortcut_portal.py diff --git a/.gitignore b/.gitignore index 034f749..db73830 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .vscode -goldwarden* \ No newline at end of file +goldwarden +__pycache__ \ No newline at end of file diff --git a/Readme.md b/Readme.md index 9b821e4..35c6e65 100644 --- a/Readme.md +++ b/Readme.md @@ -85,9 +85,9 @@ Run a command with injected environment variables goldwarden run -- ``` -Autofill +Autofill (Flatpak only?) ``` -goldwarden autofill --layout +dbus-send --type=method_call --dest=com.quexten.goldwarden /com/quexten/goldwarden com.quexten.goldwarden.Autofill.autofill ``` (Create a hotkey for this depending on your desktop environment) diff --git a/agent/actions/logins.go b/agent/actions/logins.go index 0c6b52b..a53a662 100644 --- a/agent/actions/logins.go +++ b/agent/actions/logins.go @@ -101,7 +101,7 @@ func handleGetLoginCipher(request messages.IPCMessage, cfg *config.Config, vault } func handleListLoginsRequest(request messages.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx *sockets.CallingContext) (response messages.IPCMessage, err error) { - if approved, err := pinentry.GetApproval("Approve List Credentials", fmt.Sprintf("%s on %s>%s>%s is trying to list credentials (name & username)", ctx.UserName, ctx.GrandParentProcessName, ctx.ParentProcessName, ctx.ProcessName)); err != nil || !approved { + if approved, err := pinentry.GetApproval("Approve List Credentials", fmt.Sprintf("%s on %s>%s>%s is trying access all credentials", ctx.UserName, ctx.GrandParentProcessName, ctx.ParentProcessName, ctx.ProcessName)); err != nil || !approved { response, err = messages.IPCMessageFromPayload(messages.ActionResponse{ Success: false, Message: "not approved", @@ -123,6 +123,7 @@ func handleListLoginsRequest(request messages.IPCMessage, cfg *config.Config, va var decryptedName []byte = []byte{} var decryptedUsername []byte = []byte{} + var decryptedPassword []byte = []byte{} if !login.Name.IsNull() { decryptedName, err = crypto.DecryptWith(login.Name, key) @@ -140,10 +141,19 @@ func handleListLoginsRequest(request messages.IPCMessage, cfg *config.Config, va } } + if !login.Login.Password.IsNull() { + decryptedPassword, err = crypto.DecryptWith(login.Login.Password, key) + if err != nil { + actionsLog.Warn("Could not decrypt login:" + err.Error()) + continue + } + } + decryptedLoginCiphers = append(decryptedLoginCiphers, messages.DecryptedLoginCipher{ Name: string(decryptedName), Username: string(decryptedUsername), UUID: login.ID.String(), + Password: string(decryptedPassword), }) // prevent deadlock from enclaves diff --git a/autofill/autofill.go b/autofill/autofill.go deleted file mode 100644 index 527ed14..0000000 --- a/autofill/autofill.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build !noautofill - -package autofill - -import ( - "errors" - - "github.com/atotto/clipboard" - "github.com/quexten/goldwarden/autofill/autotype" - "github.com/quexten/goldwarden/client" - "github.com/quexten/goldwarden/ipc/messages" -) - -func GetLoginByUUID(uuid string, client client.Client) (messages.DecryptedLoginCipher, error) { - resp, err := client.SendToAgent(messages.GetLoginRequest{ - UUID: uuid, - }) - if err != nil { - return messages.DecryptedLoginCipher{}, err - } - - switch resp.(type) { - case messages.GetLoginResponse: - castedResponse := (resp.(messages.GetLoginResponse)) - return castedResponse.Result, nil - case messages.ActionResponse: - castedResponse := (resp.(messages.ActionResponse)) - return messages.DecryptedLoginCipher{}, errors.New("Error: " + castedResponse.Message) - default: - return messages.DecryptedLoginCipher{}, errors.New("Wrong response type") - } -} - -func ListLogins(client client.Client) ([]messages.DecryptedLoginCipher, error) { - resp, err := client.SendToAgent(messages.ListLoginsRequest{}) - if err != nil { - return []messages.DecryptedLoginCipher{}, err - } - - switch resp.(type) { - case messages.GetLoginsResponse: - castedResponse := (resp.(messages.GetLoginsResponse)) - return castedResponse.Result, nil - case messages.ActionResponse: - castedResponse := (resp.(messages.ActionResponse)) - return []messages.DecryptedLoginCipher{}, errors.New("Error: " + castedResponse.Message) - default: - return []messages.DecryptedLoginCipher{}, errors.New("Wrong response type") - } -} - -func Run(layout string, client client.Client) { - logins, err := ListLogins(client) - if err != nil { - panic(err) - } - - autofillEntries := []AutofillEntry{} - for _, login := range logins { - autofillEntries = append(autofillEntries, AutofillEntry{ - Name: login.Name, - Username: login.Username, - UUID: login.UUID, - }) - } - - RunAutofill(autofillEntries, func(uuid string, c chan bool) { - login, err := GetLoginByUUID(uuid, client) - if err != nil { - panic(err) - } - - autotype.TypeString(string(login.Username)+"\t"+string(login.Password), layout) - - clipboard.WriteAll(login.TwoFactorCode) - c <- true - }) -} diff --git a/autofill/autotype/uinput/dvorak.go b/autofill/autotype/uinput/dvorak.go deleted file mode 100644 index 7dbf68f..0000000 --- a/autofill/autotype/uinput/dvorak.go +++ /dev/null @@ -1,384 +0,0 @@ -package uinput - -import ( - "errors" - "fmt" - - "github.com/bendahl/uinput" -) - -type Dvorak struct { -} - -func (d Dvorak) TypeKey(key Key, keyboard uinput.Keyboard) error { - var err error - switch key { - case KeyA: - err = keyboard.KeyPress(uinput.KeyA) - break - case KeyAUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyA) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyB: - err = keyboard.KeyPress(uinput.KeyN) - break - case KeyBUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyN) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyC: - err = keyboard.KeyPress(uinput.KeyI) - break - case KeyCUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyI) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyD: - err = keyboard.KeyPress(uinput.KeyH) - break - case KeyDUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyH) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyE: - err = keyboard.KeyPress(uinput.KeyD) - break - case KeyEUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - err = keyboard.KeyPress(uinput.KeyD) - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyF: - err = keyboard.KeyPress(uinput.KeyY) - break - case KeyFUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyY) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyG: - err = keyboard.KeyPress(uinput.KeyU) - break - case KeyGUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyU) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyH: - err = keyboard.KeyPress(uinput.KeyJ) - break - case KeyHUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyJ) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyI: - err = keyboard.KeyPress(uinput.KeyG) - break - case KeyIUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyG) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyJ: - err = keyboard.KeyPress(uinput.KeyC) - break - case KeyJUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyC) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyK: - err = keyboard.KeyPress(uinput.KeyV) - break - case KeyKUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyV) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyL: - err = keyboard.KeyPress(uinput.KeyP) - break - case KeyLUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyP) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyM: - err = keyboard.KeyPress(uinput.KeyM) - break - case KeyMUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyM) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyN: - err = keyboard.KeyPress(uinput.KeyL) - break - case KeyNUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyL) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyO: - err = keyboard.KeyPress(uinput.KeyS) - break - case KeyOUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyS) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyP: - err = keyboard.KeyPress(uinput.KeyR) - break - case KeyPUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyR) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyQ: - err = keyboard.KeyPress(uinput.KeyX) - break - case KeyQUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyX) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyR: - err = keyboard.KeyPress(uinput.KeyO) - break - case KeyRUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyO) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyS: - err = keyboard.KeyPress(uinput.KeySemicolon) - break - case KeySUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeySemicolon) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyT: - err = keyboard.KeyPress(uinput.KeyK) - break - case KeyTUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyK) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyU: - err = keyboard.KeyPress(uinput.KeyF) - break - case KeyUUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyF) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyV: - err = keyboard.KeyPress(uinput.KeyDot) - break - case KeyVUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyDot) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyW: - err = keyboard.KeyPress(uinput.KeyComma) - break - case KeyWUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyComma) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyX: - err = keyboard.KeyPress(uinput.KeyB) - break - case KeyXUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyB) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyY: - err = keyboard.KeyPress(uinput.KeyT) - break - case KeyYUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyT) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyZ: - err = keyboard.KeyPress(uinput.KeySlash) - break - case KeyZUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeySlash) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case Key1: - err = keyboard.KeyPress(uinput.Key1) - break - case Key2: - err = keyboard.KeyPress(uinput.Key2) - break - case Key3: - err = keyboard.KeyPress(uinput.Key3) - break - case Key4: - err = keyboard.KeyPress(uinput.Key4) - break - case Key5: - err = keyboard.KeyPress(uinput.Key5) - break - case Key6: - err = keyboard.KeyPress(uinput.Key6) - break - case Key7: - err = keyboard.KeyPress(uinput.Key7) - break - case Key8: - err = keyboard.KeyPress(uinput.Key8) - break - case Key9: - err = keyboard.KeyPress(uinput.Key9) - break - case Key0: - err = keyboard.KeyPress(uinput.Key0) - break - case KeyTab: - err = keyboard.KeyPress(uinput.KeyTab) - break - case KeyHyphen: - err = keyboard.KeyPress(uinput.KeyApostrophe) - break - case KeyExclamationMark: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key1) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyAtSign: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key2) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyHash: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key3) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyDollar: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key4) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyPercent: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key5) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyCaret: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key6) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyAmpersand: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key7) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyAsterisk: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key8) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyDot: - err = keyboard.KeyPress(uinput.KeyE) - break - case KeyComma: - err = keyboard.KeyPress(uinput.KeyW) - break - case KeyQuestionMark: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyLeftbrace) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeySemicolon: - err = keyboard.KeyPress(uinput.KeyZ) - break - case KeyColon: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyZ) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeySlash: - err = keyboard.KeyPress(uinput.KeyLeftbrace) - break - case KeyApostrophe: - err = keyboard.KeyPress(uinput.KeyQ) - break - case KeySpace: - err = keyboard.KeyPress(uinput.KeySpace) - break - - default: - fmt.Println("Unknown key: ", key) - fmt.Println("Please add it to the dvorak layout") - return errors.New("Unknown key") - } - - return err -} - -func init() { - DefaultLayoutRegistry.Register("dvorak", Dvorak{}) -} diff --git a/autofill/autotype/uinput/qwerty.go b/autofill/autotype/uinput/qwerty.go deleted file mode 100644 index 79cc88b..0000000 --- a/autofill/autotype/uinput/qwerty.go +++ /dev/null @@ -1,387 +0,0 @@ -package uinput - -import ( - "errors" - "fmt" - - "github.com/bendahl/uinput" -) - -type Qwerty struct { -} - -func (d Qwerty) TypeKey(key Key, keyboard uinput.Keyboard) error { - var err error - - switch key { - case KeyA: - err = keyboard.KeyPress(uinput.KeyA) - break - case KeyAUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyA) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyB: - err = keyboard.KeyPress(uinput.KeyB) - break - case KeyBUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyB) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyC: - err = keyboard.KeyPress(uinput.KeyC) - break - case KeyCUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyC) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyD: - err = keyboard.KeyPress(uinput.KeyD) - break - case KeyDUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyD) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyE: - err = keyboard.KeyPress(uinput.KeyE) - break - case KeyEUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyE) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyF: - err = keyboard.KeyPress(uinput.KeyF) - break - case KeyFUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyF) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyG: - err = keyboard.KeyPress(uinput.KeyG) - break - case KeyGUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyG) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyH: - err = keyboard.KeyPress(uinput.KeyH) - break - case KeyHUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyH) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyI: - err = keyboard.KeyPress(uinput.KeyI) - break - case KeyIUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyI) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyJ: - err = keyboard.KeyPress(uinput.KeyJ) - break - case KeyJUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyJ) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyK: - err = keyboard.KeyPress(uinput.KeyK) - break - case KeyKUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyK) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyL: - err = keyboard.KeyPress(uinput.KeyL) - break - case KeyLUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyL) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyM: - err = keyboard.KeyPress(uinput.KeyM) - break - case KeyMUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyM) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyN: - err = keyboard.KeyPress(uinput.KeyN) - break - case KeyNUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyN) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyO: - err = keyboard.KeyPress(uinput.KeyO) - break - case KeyOUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyO) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyP: - err = keyboard.KeyPress(uinput.KeyP) - break - case KeyPUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyP) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyQ: - err = keyboard.KeyPress(uinput.KeyQ) - break - case KeyQUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyQ) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyR: - err = keyboard.KeyPress(uinput.KeyR) - break - case KeyRUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyR) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyS: - err = keyboard.KeyPress(uinput.KeyS) - break - case KeySUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyS) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyT: - err = keyboard.KeyPress(uinput.KeyT) - break - case KeyTUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyT) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyU: - err = keyboard.KeyPress(uinput.KeyU) - break - case KeyUUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyU) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyV: - err = keyboard.KeyPress(uinput.KeyV) - break - case KeyVUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyV) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyW: - err = keyboard.KeyPress(uinput.KeyW) - break - case KeyWUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyW) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyX: - err = keyboard.KeyPress(uinput.KeyX) - break - case KeyXUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyX) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyY: - err = keyboard.KeyPress(uinput.KeyY) - break - case KeyYUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyY) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyZ: - err = keyboard.KeyPress(uinput.KeyZ) - break - case KeyZUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyZ) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case Key1: - err = keyboard.KeyPress(uinput.Key1) - break - case Key2: - err = keyboard.KeyPress(uinput.Key2) - break - case Key3: - err = keyboard.KeyPress(uinput.Key3) - break - case Key4: - err = keyboard.KeyPress(uinput.Key4) - break - case Key5: - err = keyboard.KeyPress(uinput.Key5) - break - case Key6: - err = keyboard.KeyPress(uinput.Key6) - break - case Key7: - err = keyboard.KeyPress(uinput.Key7) - break - case Key8: - err = keyboard.KeyPress(uinput.Key8) - break - case Key9: - err = keyboard.KeyPress(uinput.Key9) - break - case Key0: - err = keyboard.KeyPress(uinput.Key0) - break - case KeyTab: - err = keyboard.KeyPress(uinput.KeyTab) - break - case KeyHyphen: - err = keyboard.KeyPress(uinput.KeyMinus) - break - case KeyExclamationMark: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key1) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyAtSign: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key2) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyHash: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key3) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyDollar: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key4) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyPercent: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key5) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyCaret: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key6) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyAmpersand: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key7) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyAsterisk: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key8) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyDot: - err = keyboard.KeyPress(uinput.KeyDot) - break - case KeyComma: - err = keyboard.KeyPress(uinput.KeyW) - break - case KeyQuestionMark: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeySlash) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeySemicolon: - err = keyboard.KeyPress(uinput.KeySemicolon) - break - case KeyColon: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeySemicolon) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeySlash: - err = keyboard.KeyPress(uinput.KeySlash) - break - case KeyApostrophe: - err = keyboard.KeyPress(uinput.KeyApostrophe) - break - - case KeySpace: - err = keyboard.KeyPress(uinput.KeySpace) - break - - default: - fmt.Println("Unknown key: ", key) - fmt.Println("Please add it to the QWERTY layout") - return errors.New("Unknown key") - } - return err -} - -func init() { - DefaultLayoutRegistry.Register("qwerty", Qwerty{}) -} diff --git a/autofill/autotype/uinput/uinput.go b/autofill/autotype/uinput/uinput.go deleted file mode 100644 index 8e32fb5..0000000 --- a/autofill/autotype/uinput/uinput.go +++ /dev/null @@ -1,175 +0,0 @@ -package uinput - -import ( - "errors" - "fmt" - "time" - - "github.com/bendahl/uinput" -) - -type Layout interface { - TypeKey(key Key, keyboard uinput.Keyboard) error -} - -type Key string - -const ( - KeyA Key = "a" - KeyB Key = "b" - KeyC Key = "c" - KeyD Key = "d" - KeyE Key = "e" - KeyF Key = "f" - KeyG Key = "g" - KeyH Key = "h" - KeyI Key = "i" - KeyJ Key = "j" - KeyK Key = "k" - KeyL Key = "l" - KeyM Key = "m" - KeyN Key = "n" - KeyO Key = "o" - KeyP Key = "p" - KeyQ Key = "q" - KeyR Key = "r" - KeyS Key = "s" - KeyT Key = "t" - KeyU Key = "u" - KeyV Key = "v" - KeyW Key = "w" - KeyX Key = "x" - KeyY Key = "y" - KeyZ Key = "z" - KeyAUpper Key = "A" - KeyBUpper Key = "B" - KeyCUpper Key = "C" - KeyDUpper Key = "D" - KeyEUpper Key = "E" - KeyFUpper Key = "F" - KeyGUpper Key = "G" - KeyHUpper Key = "H" - KeyIUpper Key = "I" - KeyJUpper Key = "J" - KeyKUpper Key = "K" - KeyLUpper Key = "L" - KeyMUpper Key = "M" - KeyNUpper Key = "N" - KeyOUpper Key = "O" - KeyPUpper Key = "P" - KeyQUpper Key = "Q" - KeyRUpper Key = "R" - KeySUpper Key = "S" - KeyTUpper Key = "T" - KeyUUpper Key = "U" - KeyVUpper Key = "V" - KeyWUpper Key = "W" - KeyXUpper Key = "X" - KeyYUpper Key = "Y" - KeyZUpper Key = "Z" - Key0 Key = "0" - Key1 Key = "1" - Key2 Key = "2" - Key3 Key = "3" - Key4 Key = "4" - Key5 Key = "5" - Key6 Key = "6" - Key7 Key = "7" - Key8 Key = "8" - Key9 Key = "9" - - KeyHyphen Key = "-" - - KeySpace Key = " " - KeyExclamationMark Key = "!" - KeyAtSign Key = "@" - KeyHash Key = "#" - KeyDollar Key = "$" - KeyPercent Key = "%" - KeyCaret Key = "^" - KeyAmpersand Key = "&" - KeyAsterisk Key = "*" - - KeyDot Key = "." - KeyComma Key = "," - KeySlash Key = "/" - KeyBackslash Key = "\\" - KeyQuestionMark Key = "?" - KeySemicolon Key = ";" - KeyColon Key = ":" - KeyApostrophe Key = "'" - - KeyTab Key = "\t" -) - -type LayoutRegistry struct { - layouts map[string]Layout -} - -func NewLayoutRegistry() *LayoutRegistry { - return &LayoutRegistry{ - layouts: make(map[string]Layout), - } -} - -var DefaultLayoutRegistry = NewLayoutRegistry() - -func (r *LayoutRegistry) Register(name string, layout Layout) { - r.layouts[name] = layout -} - -func TypeString(text string, layout string) error { - if layout == "" { - layout = "qwerty" - } - - if _, ok := DefaultLayoutRegistry.layouts[layout]; !ok { - return errors.New("layout not found") - } - - keyboard, err := uinput.CreateKeyboard("/dev/uinput", []byte("testkeyboard")) - if err != nil { - return err - } - - for _, c := range text { - key := Key(string(c)) - err := DefaultLayoutRegistry.layouts[layout].TypeKey(key, keyboard) - if err != nil { - fmt.Println(err) - } - time.Sleep(10 * time.Millisecond) - } - - err = keyboard.Close() - if err != nil { - return err - } - - return nil -} - -func Paste(layout string) error { - if layout == "" { - layout = "qwerty" - } - - if _, ok := DefaultLayoutRegistry.layouts[layout]; !ok { - return errors.New("layout not found") - } - - keyboard, err := uinput.CreateKeyboard("/dev/uinput", []byte("Goldwarden Autotype")) - if err != nil { - return err - } - keyboard.KeyDown(uinput.KeyLeftctrl) - time.Sleep(100 * time.Millisecond) - DefaultLayoutRegistry.layouts[layout].TypeKey(KeyV, keyboard) - time.Sleep(100 * time.Millisecond) - keyboard.KeyUp(uinput.KeyLeftctrl) - return nil -} - -func Sleep() { - time.Sleep(20 * time.Millisecond) -} diff --git a/autofill/autotype/uinputautotype.go b/autofill/autotype/uinputautotype.go deleted file mode 100644 index 50bb452..0000000 --- a/autofill/autotype/uinputautotype.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build linux && uinput - -package autotype - -import "github.com/quexten/goldwarden/autofill/autotype/uinput" - -func TypeString(text string, layout string) error { - return uinput.TypeString(text, layout) -} diff --git a/autofill/autotype/unimplemented.go b/autofill/autotype/unimplemented.go deleted file mode 100644 index a7d8c73..0000000 --- a/autofill/autotype/unimplemented.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !linux - -package autotype - -func TypeString(text string, layout string) error { - return errors.New("Not implemented") -} diff --git a/autofill/gioAutofillDialog.go b/autofill/gioAutofillDialog.go deleted file mode 100644 index f3ce170..0000000 --- a/autofill/gioAutofillDialog.go +++ /dev/null @@ -1,336 +0,0 @@ -//go:build !noautofill - -package autofill - -import ( - "fmt" - "image" - "image/color" - "log" - "math" - "os" - "os/user" - "sort" - "strings" - - "gioui.org/app" - "gioui.org/font/gofont" - "gioui.org/io/key" - "gioui.org/io/system" - "gioui.org/layout" - "gioui.org/op" - "gioui.org/op/clip" - "gioui.org/op/paint" - "gioui.org/unit" - "gioui.org/widget" - "gioui.org/widget/material" - "github.com/shirou/gopsutil/v3/process" -) - -type AutofillEntry struct { - Username string - Name string - UUID string -} - -var autofillEntries = []AutofillEntry{} -var onAutofill func(string, chan bool) -var selectedEntry = 0 - -type scoredAutofillEntry struct { - autofillEntry AutofillEntry - score int -} - -func getUserProcs() []string { - procNames := []string{} - - procs, err := process.Processes() - if err != nil { - return []string{} - } - for _, proc := range procs { - user, err := user.Current() - if err != nil { - continue - } - - procuser, err := proc.Username() - if err != nil { - continue - } - if procuser == user.Username { - procName, err := proc.Name() - if err != nil { - continue - } - procNames = append(procNames, strings.ToLower(procName)) - } - - } - - return procNames -} - -func GetFilteredAutofillEntries(entries []AutofillEntry, filter string) []AutofillEntry { - if len(filter) == 0 { - return []AutofillEntry{} - } - - filter = strings.ToLower(filter) - - // filter entrien by whether they contain the filter string - filteredEntries := []AutofillEntry{} - for _, entry := range entries { - name := strings.ToLower(entry.Name) - if strings.HasPrefix(name, filter) { - filteredEntries = append(filteredEntries, entry) - } - } - - processes := getUserProcs() - - scoredEntries := []scoredAutofillEntry{} - for _, entry := range filteredEntries { - score := 0 - name := strings.ToLower(entry.Name) - filter = strings.ToLower(filter) - - maxProcessScore := 0 - - for _, process := range processes { - score := 0 - - sharedPrefixLen := 0 - for i := 0; i < len(process) && i < len(entry.Name); i++ { - if process[i] == name[i] { - sharedPrefixLen++ - } else { - break - } - } - sharedPrefixLenPercent := float32(sharedPrefixLen) / float32(math.Min(float64(len(process)), float64(len(name)))) - - if sharedPrefixLen > 0 { - score += int(sharedPrefixLenPercent * 7) - } - - if score > maxProcessScore { - maxProcessScore = score - } - } - - score += maxProcessScore - scoredEntries = append(scoredEntries, scoredAutofillEntry{entry, score}) - } - - sort.Slice(scoredEntries, func(i, j int) bool { - return scoredEntries[i].score > scoredEntries[j].score - }) - - var filteredEntries1 []AutofillEntry - for _, scoredEntry := range scoredEntries { - if scoredEntry.score == 0 { - continue - } - filteredEntries1 = append(filteredEntries1, scoredEntry.autofillEntry) - } - - return filteredEntries1 -} - -func RunAutofill(entries []AutofillEntry, onAutofillFunc func(string, chan bool)) { - autofillEntries = entries - onAutofill = onAutofillFunc - - go func() { - w := app.NewWindow() - w.Option(app.Size(unit.Dp(600), unit.Dp(800))) - w.Option(app.Decorated(false)) - w.Perform(system.ActionCenter) - w.Perform(system.ActionRaise) - lineEditor.Focus() - if err := loop(w); err != nil { - log.Fatal(err) - } - }() - app.Main() -} - -var lineEditor = &widget.Editor{ - SingleLine: true, - Submit: true, -} - -var ( - unselected = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} - unselectedText = color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} - background = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} - selected = color.NRGBA{R: 0x65, G: 0x1F, B: 0xFF, A: 0xFF} - selectedText = color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} -) - -var th = material.NewTheme(gofont.Collection()) -var list = layout.List{Axis: layout.Vertical} - -func doLayout(gtx layout.Context) layout.Dimensions { - var filteredEntries []AutofillEntry = GetFilteredAutofillEntries(autofillEntries, lineEditor.Text()) - - if selectedEntry >= 10 || selectedEntry >= len(filteredEntries) { - selectedEntry = 0 - } - - return Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - searchBox := material.Editor(th, lineEditor, "Search query") - searchBox.Color = selectedText - border := widget.Border{Color: selectedText, CornerRadius: unit.Dp(8), Width: unit.Dp(2)} - return border.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(unit.Dp(8)).Layout(gtx, searchBox.Layout) - }) - }) - }) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - - return list.Layout(gtx, len(filteredEntries), func(gtx layout.Context, i int) layout.Dimensions { - entry := filteredEntries[i] - - return layout.Inset{Bottom: unit.Dp(10)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - isSelected := i == selectedEntry - var color color.NRGBA - if isSelected { - color = selected - } else { - color = unselected - } - - return Background{Color: color, CornerRadius: unit.Dp(8)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - dimens := layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - t := material.H6(th, entry.Name) - if isSelected { - t.Color = selectedText - } else { - t.Color = unselectedText - } - return t.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - t := material.Body1(th, entry.Username) - if isSelected { - t.Color = selectedText - } else { - t.Color = unselectedText - } - - return t.Layout(gtx) - }), - ) - return dimens - }) - }) - }) - }) - }) - })) - }) -} - -func loop(w *app.Window) error { - var ops op.Ops - for { - e := <-w.Events() - switch e := e.(type) { - case system.DestroyEvent: - return e.Err - case system.FrameEvent: - gtx := layout.NewContext(&ops, e) - - key.InputOp{ - Keys: key.Set(key.NameReturn + "|" + key.NameEscape + "|" + key.NameDownArrow), - Tag: 0, - }.Add(gtx.Ops) - t := lineEditor.Events() - for _, ev := range t { - switch ev.(type) { - case widget.SubmitEvent: - entries := GetFilteredAutofillEntries(autofillEntries, lineEditor.Text()) - if len(entries) == 0 { - fmt.Println("no entries") - continue - } else { - w.Perform(system.ActionMinimize) - c := make(chan bool) - go onAutofill(entries[selectedEntry].UUID, c) - go func() { - <-c - os.Exit(0) - }() - } - } - } - - test := gtx.Events(0) - for _, ev := range test { - switch ev := ev.(type) { - case key.Event: - switch ev.Name { - case key.NameReturn: - fmt.Println("uncaught submit") - return nil - case key.NameDownArrow: - if ev.State == key.Press { - selectedEntry++ - if selectedEntry >= 10 { - selectedEntry = 0 - } - } - case key.NameEscape: - os.Exit(0) - } - } - } - - doLayout(gtx) - e.Frame(gtx.Ops) - } - } -} - -type Background struct { - Color color.NRGBA - CornerRadius unit.Dp -} - -func (b Background) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { - m := op.Record(gtx.Ops) - dims := w(gtx) - size := dims.Size - call := m.Stop() - if r := gtx.Dp(b.CornerRadius); r > 0 { - defer clip.RRect{ - Rect: image.Rect(0, 0, size.X, size.Y), - NE: r, NW: r, SE: r, SW: r, - }.Push(gtx.Ops).Pop() - } - fill{b.Color}.Layout(gtx, size) - call.Add(gtx.Ops) - return dims -} - -type fill struct { - col color.NRGBA -} - -func (f fill) Layout(gtx layout.Context, sz image.Point) layout.Dimensions { - defer clip.Rect(image.Rectangle{Max: sz}).Push(gtx.Ops).Pop() - paint.ColorOp{Color: f.col}.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - return layout.Dimensions{Size: sz} -} diff --git a/autofill/autotype/libportalautotype.go b/autotype/libportalautotype.go similarity index 96% rename from autofill/autotype/libportalautotype.go rename to autotype/libportalautotype.go index 64ad3fb..fbd09b2 100644 --- a/autofill/autotype/libportalautotype.go +++ b/autotype/libportalautotype.go @@ -1,4 +1,4 @@ -//go:build linux && !uinput +//go:build linux package autotype @@ -13,7 +13,7 @@ var globalID = 0 const autoTypeDelay = 1 * time.Millisecond -func TypeString(textToType string, layout string) { +func TypeString(textToType string) { bus, err := dbus.SessionBus() if err != nil { panic(err) diff --git a/cmd/autofill.go b/cmd/autofill.go index 1710720..e14a1b3 100644 --- a/cmd/autofill.go +++ b/cmd/autofill.go @@ -1,24 +1,25 @@ -//go:build !noautofill +//go:build linux package cmd import ( - "github.com/quexten/goldwarden/autofill" + "github.com/quexten/goldwarden/autotype" "github.com/spf13/cobra" ) var autofillCmd = &cobra.Command{ - Use: "autofill", - Short: "Autofill credentials", - Long: `Autofill credentials`, + Use: "autotype", + Short: "Autotype credentials", + Long: `Autotype credentials`, Run: func(cmd *cobra.Command, args []string) { - layout := cmd.Flag("layout").Value.String() - autofill.Run(layout, commandClient) + username, _ := cmd.Flags().GetString("username") + password, _ := cmd.Flags().GetString("password") + autotype.TypeString(username + "\t" + password) }, } func init() { rootCmd.AddCommand(autofillCmd) - autofillCmd.PersistentFlags().String("layout", "qwerty", "") - autofillCmd.PersistentFlags().Bool("use-copy-paste", false, "") + autofillCmd.PersistentFlags().String("username", "", "") + autofillCmd.PersistentFlags().String("password", "", "") } diff --git a/cmd/logins.go b/cmd/logins.go index 7d4d530..4251fac 100644 --- a/cmd/logins.go +++ b/cmd/logins.go @@ -1,8 +1,13 @@ package cmd import ( + "encoding/json" + "errors" "fmt" + "strings" + "github.com/icza/gox/stringsx" + "github.com/quexten/goldwarden/client" "github.com/quexten/goldwarden/ipc/messages" "github.com/spf13/cobra" ) @@ -54,6 +59,61 @@ var getLoginCmd = &cobra.Command{ }, } +var listLoginsCmd = &cobra.Command{ + Use: "list", + Short: "Lists all logins in your vault", + Long: `Lists all logins in your vault.`, + Run: func(cmd *cobra.Command, args []string) { + loginIfRequired() + + logins, err := ListLogins(commandClient) + if err != nil { + handleSendToAgentError(err) + return + } + + fmt.Println("[") + for index, login := range logins { + data := map[string]string{ + "name": stringsx.Clean(login.Name), + "uuid": stringsx.Clean(login.UUID), + "username": stringsx.Clean(login.Username), + "password": stringsx.Clean(strings.ReplaceAll(login.Password, "\"", "\\\"")), + } + jsonString, err := json.Marshal(data) + if err != nil { + handleSendToAgentError(err) + return + } + fmt.Print(string(jsonString)) + if index != len(logins)-1 { + fmt.Println(",") + } else { + fmt.Println() + } + } + fmt.Println("]") + }, +} + +func ListLogins(client client.Client) ([]messages.DecryptedLoginCipher, error) { + resp, err := client.SendToAgent(messages.ListLoginsRequest{}) + if err != nil { + return []messages.DecryptedLoginCipher{}, err + } + + switch resp.(type) { + case messages.GetLoginsResponse: + castedResponse := (resp.(messages.GetLoginsResponse)) + return castedResponse.Result, nil + case messages.ActionResponse: + castedResponse := (resp.(messages.ActionResponse)) + return []messages.DecryptedLoginCipher{}, errors.New("Error: " + castedResponse.Message) + default: + return []messages.DecryptedLoginCipher{}, errors.New("Wrong response type") + } +} + func init() { rootCmd.AddCommand(baseLoginCmd) baseLoginCmd.AddCommand(getLoginCmd) @@ -61,4 +121,5 @@ func init() { getLoginCmd.PersistentFlags().String("username", "", "") getLoginCmd.PersistentFlags().String("uuid", "", "") getLoginCmd.PersistentFlags().Bool("full", false, "") + baseLoginCmd.AddCommand(listLoginsCmd) } diff --git a/cmd/vault.go b/cmd/vault.go index 397a13c..2668ede 100644 --- a/cmd/vault.go +++ b/cmd/vault.go @@ -107,9 +107,11 @@ var statusCmd = &cobra.Command{ switch result.(type) { case messages.VaultStatusResponse: status := result.(messages.VaultStatusResponse) - fmt.Println("Locked: ", status.Locked) - fmt.Println("Number of logins: ", status.NumberOfLogins) - fmt.Println("Number of notes: ", status.NumberOfNotes) + fmt.Println("{") + fmt.Println(" \"locked\":", status.Locked, ",") + fmt.Println(" \"loginEntries\":", status.NumberOfLogins, ",") + fmt.Println(" \"noteEntries\":", status.NumberOfNotes) + fmt.Println("}") default: println("Wrong response type") } diff --git a/go.mod b/go.mod index 59ce90b..a1d92ce 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,9 @@ require ( github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433 // indirect + github.com/icza/gox v0.0.0-20230924165045-adcb03233bb5 // indirect + github.com/k0kubun/pp v2.4.0+incompatible // indirect + github.com/k0kubun/pp/v3 v3.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect diff --git a/go.sum b/go.sum index 3ebe52e..6046d5d 100644 --- a/go.sum +++ b/go.sum @@ -45,8 +45,14 @@ github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/icza/gox v0.0.0-20230924165045-adcb03233bb5 h1:K7KEFpKgVcjj98jOu2Z3xMBTtTwfYVT90Zmo3ZuWmbE= +github.com/icza/gox v0.0.0-20230924165045-adcb03233bb5/go.mod h1:VbcN86fRkkUMPX2ufM85Um8zFndLZswoIW1eYtpAcVk= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/k0kubun/pp v2.4.0+incompatible h1:M9iQzcejGfiBjDa7+Tc0rJgR7WFKP6rim/Q0DDrAT3g= +github.com/k0kubun/pp v2.4.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs= +github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA= github.com/keys-pub/go-libfido2 v1.5.3 h1:vtgHxlSB43u6lj0TSuA3VvT6z3E7VI+L1a2hvMFdECk= github.com/keys-pub/go-libfido2 v1.5.3/go.mod h1:P0V19qHwJNY0htZwZDe9Ilvs/nokGhdFX7faKFyZ6+U= github.com/lox/go-touchid v0.0.0-20170712105233-619cc8e578d0 h1:m81erW+1MD5vl3lKQ/+TYPHJ6Y9/C1COqxXPE51FkDk= diff --git a/ui/autotype/autotype.py b/ui/autotype/autotype.py new file mode 100644 index 0000000..2f75c12 --- /dev/null +++ b/ui/autotype/autotype.py @@ -0,0 +1,101 @@ +# TODO??!?!? for now using golang implementation + +import dbus +import dbus.mainloop.glib +from dbus.mainloop.glib import DBusGMainLoop + +from gi.repository import GLib + +import random +import time + +step = 0 + +def typestring(text): + step = 0 + handle = "" + + def handler(*args, **kwargs): + global step + if step == 0: + handle_xdp_session_created(*args, **kwargs) + elif step == 1: + handle_xdp_devices_selected(*args, **kwargs) + elif step == 2: + handle_session_start(*args, **kwargs) + else: + print(args, kwargs) + step += 1 + + def handle_session_start(code, results, object_path): + global handle + + if code != 0: + raise Exception("Could not start session") + + for sym in text: + if sym == "\t": + inter.NotifyKeyboardKeycode(handle, {}, 15, 1) + time.sleep(0.001) + inter.NotifyKeyboardKeycode(handle, {}, 15, 0) + time.sleep(0.001) + else: + inter.NotifyKeyboardKeysym(handle, {}, ord(sym), 1) + time.sleep(0.001) + inter.NotifyKeyboardKeysym(handle, {}, ord(sym), 0) + time.sleep(0.001) + + bus + + def handle_xdp_devices_selected(code, results, object_path): + global handle + + if code != 0: + raise Exception("Could not select devices") + + start_options = { + "handle_token": "krfb" + str(random.randint(0, 999999999)) + } + reply = inter.Start(handle, "", start_options) + print(reply) + + def handle_xdp_session_created(code, results, object_path): + global handle + + if code != 0: + raise Exception("Could not create session") + print(results) + handle = results["session_handle"] + + # select sources for the session + selection_options = { + "types": dbus.UInt32(7), # request all (KeyBoard, Pointer, TouchScreen) + "handle_token": "krfb" + str(random.randint(0, 999999999)) + } + selector_reply = inter.SelectDevices(handle, selection_options) + print(selector_reply) + + def main(): + global bus + global inter + loop = GLib.MainLoop() + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + bus = dbus.SessionBus() + obj = bus.get_object("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop") + inter = dbus.Interface(obj, "org.freedesktop.portal.RemoteDesktop") + + bus.add_signal_receiver( + handler, + signal_name="Response", + dbus_interface="org.freedesktop.portal.Request", + bus_name="org.freedesktop.portal.Desktop", + path_keyword="object_path") + + print(inter) + result = inter.CreateSession({ + "session_handle_token": "sessionhandletoken" + }) + print(result) + loop.run() + + main() diff --git a/ui/clipboard.py b/ui/clipboard.py new file mode 100644 index 0000000..818da99 --- /dev/null +++ b/ui/clipboard.py @@ -0,0 +1,5 @@ +import subprocess + +def write(text): + process = subprocess.Popen(["/bin/sh", "-c", "wl-copy"], stdin=subprocess.PIPE) + process.communicate(text.encode('utf-8')) \ No newline at end of file diff --git a/ui/goldwarden.py b/ui/goldwarden.py new file mode 100644 index 0000000..6e4e90a --- /dev/null +++ b/ui/goldwarden.py @@ -0,0 +1,87 @@ +import subprocess +import json + +BINARY_PATH = "/home/quexten/go/src/github.com/quexten/goldwarden/goldwarden" + +def set_api_url(url): + restic_cmd = f"{BINARY_PATH} config set-api-url {url}" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + +def set_identity_url(url): + restic_cmd = f"{BINARY_PATH} config set-identity-url {url}" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + +def set_notification_url(url): + restic_cmd = f"{BINARY_PATH} config set-notifications-url {url}" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + +def login_with_password(email, password): + restic_cmd = f"{BINARY_PATH} vault login --email {email}" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + if len(result.stderr.strip()) > 0: + print(result.stderr) + if "password" in result.stderr: + return "badpass" + else: + if "Logged in" in result.stderr: + print("ok") + return "ok" + return "error" + print("ok") + return "ok" + +def login_passwordless(email): + restic_cmd = f"{BINARY_PATH} vault login --email {email} --passwordless" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + +def is_pin_enabled(): + restic_cmd = f"{BINARY_PATH} vault pin" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + # check if contains enabled + return "enabled" in result.stdout + +def enable_pin(): + restic_cmd = f"{BINARY_PATH} vault pin set" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + +def get_vault_status(): + restic_cmd = f"{BINARY_PATH} vault status" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + try: + return json.loads(result.stdout) + except Exception as e: + print(e) + return None + +def get_vault_logins(): + restic_cmd = f"{BINARY_PATH} logins list" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + try: + return json.loads(result.stdout) + except Exception as e: + print(e) + return None + +def autotype(username, password): + restic_cmd = f"{BINARY_PATH} autotype --username {username} --password {password}" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) \ No newline at end of file diff --git a/ui/main.py b/ui/main.py new file mode 100644 index 0000000..293b4c4 --- /dev/null +++ b/ui/main.py @@ -0,0 +1,286 @@ +#!/usr/bin/python +import sys +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +import gc + +from gi.repository import Gtk, Adw, Gdk, Graphene, Gsk, Gio, GLib, GObject +import monitors.dbus_autofill_monitor +import goldwarden +import clipboard +import time +from threading import Thread + +class MainWindow(Gtk.ApplicationWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.stack = Gtk.Stack() + self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) + self.set_child(self.stack) + + self.box = Gtk.Box() + self.box.set_orientation(Gtk.Orientation.VERTICAL) + self.stack.add_named(self.box, "box") + + + self.text_view = Adw.EntryRow() + self.text_view.set_title("Search") + # on type func + def on_type(entry): + if len(entry.get_text()) > 1: + self.history_list.show() + else: + self.history_list.hide() + + while self.history_list.get_first_child() != None: + self.history_list.remove(self.history_list.get_first_child()) + + self.filtered_logins = list(filter(lambda i: entry.get_text().lower() in i["name"].lower(), self.logins)) + if len( self.filtered_logins) > 10: + self.filtered_logins = self.filtered_logins[0:10] + self.starts_with_logins = list(filter(lambda i: i["name"].lower().startswith(entry.get_text().lower()), self.logins)) + self.other_logins = list(filter(lambda i: i not in self.starts_with_logins , self.filtered_logins)) + self.filtered_logins = None + + for i in self.starts_with_logins + self.other_logins : + action_row = Adw.ActionRow() + action_row.set_title(i["name"]) + action_row.set_subtitle(i["username"]) + action_row.set_icon_name("dialog-password") + action_row.set_activatable(True) + action_row.password = i["password"] + action_row.username = i["username"] + self.history_list.append(action_row) + self.starts_with_logins = None + self.other_logins = None + self.text_view.connect("changed", lambda entry: on_type(entry)) + self.box.append(self.text_view) + + self.history_list = Gtk.ListBox() + # margin' + self.history_list.set_margin_start(10) + self.history_list.set_margin_end(10) + self.history_list.set_margin_top(10) + self.history_list.set_margin_bottom(10) + self.history_list.hide() + + keycont = Gtk.EventControllerKey() + def handle_keypress(controller, keyval, keycode, state, user_data): + if keycode == 36: + print("enter") + self.hide() + def do_autotype(username, password): + time.sleep(0.5) + goldwarden.autotype(username, password) + GLib.idle_add(lambda: self.show()) + autotypeThread = Thread(target=do_autotype, args=(self.history_list.get_selected_row().username, self.history_list.get_selected_row().password,)) + autotypeThread.start() + print(self.history_list.get_selected_row().get_title()) + if keyval == 112: + print("copy password") + clipboard.write(self.history_list.get_selected_row().password) + elif keyval == 117: + print("copy username") + clipboard.write(self.history_list.get_selected_row().username) + + keycont.connect('key-pressed', handle_keypress, self) + self.add_controller(keycont) + + self.history_list.get_style_context().add_class("boxed-list") + self.box.append(self.history_list) + self.set_default_size(700, 700) + self.set_title("Goldwarden") + + def on_close(window): + while self.history_list.get_first_child() != None: + self.history_list.remove(self.history_list.get_first_child()) + window.hide() + gc.collect() + return True + self.connect("close-request", on_close) + + def show(self): + for i in range(0, 5): + action_row = Adw.ActionRow() + action_row.set_title("aaa") + action_row.set_subtitle("Test") + self.history_list.append(action_row) + + +class SettingsWinvdow(Gtk.ApplicationWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.stack = Gtk.Stack() + self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) + self.set_child(self.stack) + + self.preferences_page = Adw.PreferencesPage() + self.preferences_page.set_title("General") + self.stack.add_named(self.preferences_page, "preferences_page") + + self.preferences_group = Adw.PreferencesGroup() + self.preferences_group.set_title("Services") + self.preferences_page.add(self.preferences_group) + + self.ssh_row = Adw.ActionRow() + self.ssh_row.set_title("SSH Daemon") + self.ssh_row.set_subtitle("Listening at ~/.goldwarden-ssh-agent.sock") + self.preferences_group.add(self.ssh_row) + + self.login_with_device = Adw.ActionRow() + self.login_with_device.set_title("Login with device") + self.login_with_device.set_subtitle("Waiting for requests...") + self.preferences_group.add(self.login_with_device) + + self.autofill_row = Adw.ActionRow() + self.autofill_row.set_title("Autofill Shortcut") + self.autofill_row.set_subtitle("Unavailable") + self.preferences_group.add(self.autofill_row) + + self.status_row = Adw.ActionRow() + self.status_row.set_title("DBUS Service") + self.status_row.set_subtitle("Listening") + self.preferences_group.add(self.status_row) + + self.status_row = Adw.ActionRow() + self.status_row.set_title("Vault Status") + self.status_row.set_subtitle("Locked") + self.preferences_group.add(self.status_row) + + self.login_row = Adw.ActionRow() + self.login_row.set_title("Vault Login Entries") + self.login_row.set_subtitle("0") + self.preferences_group.add(self.login_row) + + self.notes_row = Adw.ActionRow() + self.notes_row.set_title("Vault Notes") + self.notes_row.set_subtitle("0") + self.preferences_group.add(self.notes_row) + + self.login_button = Gtk.Button() + self.login_button.set_label("Login") + self.login_button.connect("clicked", lambda button: show_login()) + self.login_button.set_margin_top(10) + self.preferences_group.add(self.login_button) + + self.set_pin_button = Gtk.Button() + self.set_pin_button.set_label("Set Pin") + def set_pin(): + set_pin_thread = Thread(target=goldwarden.enable_pin) + set_pin_thread.start() + self.set_pin_button.connect("clicked", lambda button: set_pin()) + self.set_pin_button.set_margin_top(10) + self.preferences_group.add(self.set_pin_button) + + self.unlock_button = Gtk.Button() + self.unlock_button.set_label("Unlock") + self.unlock_button.set_margin_top(10) + self.preferences_group.add(self.unlock_button) + + def update_labels(): + status = goldwarden.get_vault_status() + self.status_row.set_subtitle(str("Unlocked" if status["locked"] == False else "Locked")) + self.login_row.set_subtitle(str(status["loginEntries"])) + self.notes_row.set_subtitle(str(status["noteEntries"])) + GLib.timeout_add(1000, update_labels) + + GLib.timeout_add(1000, update_labels) + self.set_default_size(400, 700) + self.set_title("Goldwarden") + + + #add title buttons + self.title_bar = Gtk.HeaderBar() + self.set_titlebar(self.title_bar) + +class MyApp(Adw.Application): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.connect('activate', self.on_activate) + + def on_activate(self, app): + if hasattr(self, "win") is False: + app.hold() + self.win = MainWindow(application=app) + self.win.set_hide_on_close(True) + self.win.hide() + self.settings_win = SettingsWinvdow(application=app) + self.settings_win.set_hide_on_close(True) + self.settings_win.present() + +app = MyApp(application_id="com.quexten.Goldwarden") +def on_autofill(): + logins = goldwarden.get_vault_logins() + if logins == None: + return + app.win.logins = logins + app.win.show() + app.win.present() +monitors.dbus_autofill_monitor.on_autofill = on_autofill + +def show_login(): + dialog = Gtk.Dialog(title="Goldwarden") + preference_group = Adw.PreferencesGroup() + preference_group.set_title("Config") + dialog.get_content_area().append(preference_group) + + api_url_entry = Adw.EntryRow() + api_url_entry.set_title("API Url") + # set value + api_url_entry.set_text("https://api.bitwarden.com/") + preference_group.add(api_url_entry) + + identity_url_entry = Adw.EntryRow() + identity_url_entry.set_title("Identity Url") + identity_url_entry.set_text("https://identity.bitwarden.com/") + preference_group.add(identity_url_entry) + + notification_url_entry = Adw.EntryRow() + notification_url_entry.set_title("Notification URL") + notification_url_entry.set_text("https://notifications.bitwarden.com/") + preference_group.add(notification_url_entry) + + auth_preference_group = Adw.PreferencesGroup() + auth_preference_group.set_title("Authentication") + dialog.get_content_area().append(auth_preference_group) + + email_entry = Adw.EntryRow() + email_entry.set_title("Email") + email_entry.set_text("") + auth_preference_group.add(email_entry) + + dialog.add_button("Login", Gtk.ResponseType.OK) + def on_save(res): + if res != Gtk.ResponseType.OK: + return + goldwarden.set_api_url(api_url_entry.get_text()) + goldwarden.set_identity_url(identity_url_entry.get_text()) + goldwarden.set_notification_url(notification_url_entry.get_text()) + def login(): + res = goldwarden.login_with_password(email_entry.get_text(), "password") + def handle_res(): + print("handle res", res) + if res == "ok": + dialog.close() + print("ok") + elif res == "badpass": + bad_pass_diag = Gtk.MessageDialog(transient_for=dialog, modal=True, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text="Bad password") + bad_pass_diag.connect("response", lambda dialog, response: bad_pass_diag.close()) + bad_pass_diag.present() + GLib.idle_add(handle_res) + + login_thread = Thread(target=login) + login_thread.start() + + #ok response + dialog.connect("response", lambda dialog, response: on_save(response)) + dialog.set_default_size(400, 200) + dialog.set_modal(True) + dialog.present() + + +app.run(sys.argv) + diff --git a/ui/monitors/dbus_autofill_monitor.py b/ui/monitors/dbus_autofill_monitor.py new file mode 100644 index 0000000..b32788f --- /dev/null +++ b/ui/monitors/dbus_autofill_monitor.py @@ -0,0 +1,23 @@ +#Python DBUS Test Server +#runs until the Quit() method is called via DBUS + +from gi.repository import Gtk +import dbus +import dbus.service +from dbus.mainloop.glib import DBusGMainLoop +from threading import Thread + +on_autofill = lambda: None + +class GoldwardenDBUSService(dbus.service.Object): + def __init__(self): + bus_name = dbus.service.BusName('com.quexten.goldwarden', bus=dbus.SessionBus()) + dbus.service.Object.__init__(self, bus_name, '/com/quexten/goldwarden') + + @dbus.service.method('com.quexten.goldwarden.Autofill') + def autofill(self): + on_autofill() + return "" + +DBusGMainLoop(set_as_default=True) +service = GoldwardenDBUSService() diff --git a/ui/monitors/global_shortcut_portal.py b/ui/monitors/global_shortcut_portal.py new file mode 100644 index 0000000..99f1089 --- /dev/null +++ b/ui/monitors/global_shortcut_portal.py @@ -0,0 +1 @@ +# todo \ No newline at end of file From 5a41145d4b710de82fe5df094517e6a45d06f588 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 23 Dec 2023 08:33:07 +0100 Subject: [PATCH 09/14] Add ui functionality --- com.quexten.Goldwarden.yml | 28 ++++++++++++++++++++++++++++ ui/com.quexten.Goldwarden.desktop | 10 ++++++++++ ui/goldwarden.py | 22 ++++++++++++++++++++-- ui/main.py | 25 +++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 com.quexten.Goldwarden.yml create mode 100644 ui/com.quexten.Goldwarden.desktop diff --git a/com.quexten.Goldwarden.yml b/com.quexten.Goldwarden.yml new file mode 100644 index 0000000..d571d52 --- /dev/null +++ b/com.quexten.Goldwarden.yml @@ -0,0 +1,28 @@ +id: com.quexten.Goldwarden +runtime: org.gnome.Platform +runtime-version: '45' +sdk: org.gnome.Sdk +command: main.py +finish-args: + - --share=ipc + - --share=network + - --socket=wayland + - --socket=fallback-x11 + - --filesystem=home +modules: + - name: goldwarden + buildsystem: simple + build-options: + build-args: + - "--share=network" + build-commands: + - pip3 install --prefix=/app timeago secretstorage pycryptodome chacha20 + - install -D main.py /app/bin/main.py + - install -D com.quexten.Goldwarden.desktop /app/share/applications/com.quexten.Goldwarden.desktop + - install -D goldwarden.svg /app/share/icons/hicolor/scalable/apps/com.quexten.Goldwarden.svg + - cp -R ui/ /app/bin/gui/ + - cp -R backend/ /app/bin/backend/ + - cp -R event_monitors/ /app/bin/event_monitors/ + sources: + - type: dir + path: ui \ No newline at end of file diff --git a/ui/com.quexten.Goldwarden.desktop b/ui/com.quexten.Goldwarden.desktop new file mode 100644 index 0000000..1f6cf1e --- /dev/null +++ b/ui/com.quexten.Goldwarden.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=Goldwarden +Comment=A Bitwarden compatible desktop password manager +Keywords=backup;recovery; +Exec=main.py +Terminal=false +Type=Application +Icon=com.quexten.goldwarden.svg +StartupNotify=true +Categories=GNOME;GTK;Password Manager; \ No newline at end of file diff --git a/ui/goldwarden.py b/ui/goldwarden.py index 6e4e90a..657c3ee 100644 --- a/ui/goldwarden.py +++ b/ui/goldwarden.py @@ -45,12 +45,12 @@ def login_passwordless(email): raise Exception("Failed to initialize repository, err", result.stderr) def is_pin_enabled(): - restic_cmd = f"{BINARY_PATH} vault pin" + restic_cmd = f"{BINARY_PATH} vault pin status" result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) if result.returncode != 0: raise Exception("Failed to initialize repository, err", result.stderr) # check if contains enabled - return "enabled" in result.stdout + return "enabled" in result.stderr def enable_pin(): restic_cmd = f"{BINARY_PATH} vault pin set" @@ -58,6 +58,24 @@ def enable_pin(): if result.returncode != 0: raise Exception("Failed to initialize repository, err", result.stderr) +def unlock(): + restic_cmd = f"{BINARY_PATH} vault unlock" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + +def lock(): + restic_cmd = f"{BINARY_PATH} vault lock" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + +def purge(): + restic_cmd = f"{BINARY_PATH} vault purge" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + def get_vault_status(): restic_cmd = f"{BINARY_PATH} vault status" result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) diff --git a/ui/main.py b/ui/main.py index 293b4c4..4961adc 100644 --- a/ui/main.py +++ b/ui/main.py @@ -137,7 +137,7 @@ class SettingsWinvdow(Gtk.ApplicationWindow): self.autofill_row = Adw.ActionRow() self.autofill_row.set_title("Autofill Shortcut") - self.autofill_row.set_subtitle("Unavailable") + self.autofill_row.set_subtitle("Unavailable, please set up a shortcut in your desktop environment (README)") self.preferences_group.add(self.autofill_row) self.status_row = Adw.ActionRow() @@ -163,6 +163,7 @@ class SettingsWinvdow(Gtk.ApplicationWindow): self.login_button = Gtk.Button() self.login_button.set_label("Login") self.login_button.connect("clicked", lambda button: show_login()) + self.login_button.set_sensitive(False) self.login_button.set_margin_top(10) self.preferences_group.add(self.login_button) @@ -173,18 +174,38 @@ class SettingsWinvdow(Gtk.ApplicationWindow): set_pin_thread.start() self.set_pin_button.connect("clicked", lambda button: set_pin()) self.set_pin_button.set_margin_top(10) + self.set_pin_button.set_sensitive(False) self.preferences_group.add(self.set_pin_button) self.unlock_button = Gtk.Button() self.unlock_button.set_label("Unlock") self.unlock_button.set_margin_top(10) + def unlock_button_clicked(): + action = goldwarden.unlock if self.unlock_button.get_label() == "Unlock" else goldwarden.lock + unlock_thread = Thread(target=action) + unlock_thread.start() + self.unlock_button.connect("clicked", lambda button: unlock_button_clicked()) + # set disabled + self.unlock_button.set_sensitive(False) self.preferences_group.add(self.unlock_button) + + self.logout_button = Gtk.Button() + self.logout_button.set_label("Logout") + self.logout_button.set_margin_top(10) + self.logout_button.connect("clicked", lambda button: goldwarden.purge()) + self.preferences_group.add(self.logout_button) def update_labels(): + pin_set = goldwarden.is_pin_enabled() status = goldwarden.get_vault_status() - self.status_row.set_subtitle(str("Unlocked" if status["locked"] == False else "Locked")) + locked = status["locked"] + self.login_button.set_sensitive(pin_set and not locked) + self.set_pin_button.set_sensitive(not pin_set or not locked) + self.status_row.set_subtitle(str("Unlocked" if not locked else "Locked")) self.login_row.set_subtitle(str(status["loginEntries"])) self.notes_row.set_subtitle(str(status["noteEntries"])) + self.unlock_button.set_sensitive(True) + self.unlock_button.set_label("Unlock" if locked else "Lock") GLib.timeout_add(1000, update_labels) GLib.timeout_add(1000, update_labels) From 0717e3479615d81f4412caad723b66ce6fc4d176 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 23 Dec 2023 12:26:01 +0100 Subject: [PATCH 10/14] Fix flatpak support; split up code; add autostart --- .gitignore | 3 +- cmd/autofill.go | 6 +- com.quexten.Goldwarden.yml | 8 +- ui/autofill.py | 108 ++++++++++ ui/background.py | 58 ++++++ ui/com.quexten.Goldwarden.desktop | 2 +- ui/goldwarden.py | 8 +- ui/goldwarden.svg | 115 +++++++++++ ui/main.py | 318 ++---------------------------- ui/settings.py | 216 ++++++++++++++++++++ 10 files changed, 534 insertions(+), 308 deletions(-) create mode 100644 ui/autofill.py create mode 100644 ui/background.py create mode 100644 ui/goldwarden.svg create mode 100644 ui/settings.py diff --git a/.gitignore b/.gitignore index db73830..53cdaf5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vscode goldwarden -__pycache__ \ No newline at end of file +__pycache__ +.flatpak-builder \ No newline at end of file diff --git a/cmd/autofill.go b/cmd/autofill.go index e14a1b3..c569e70 100644 --- a/cmd/autofill.go +++ b/cmd/autofill.go @@ -3,6 +3,8 @@ package cmd import ( + "os" + "github.com/quexten/goldwarden/autotype" "github.com/spf13/cobra" ) @@ -13,7 +15,8 @@ var autofillCmd = &cobra.Command{ Long: `Autotype credentials`, Run: func(cmd *cobra.Command, args []string) { username, _ := cmd.Flags().GetString("username") - password, _ := cmd.Flags().GetString("password") + // get pasword from env + password := os.Getenv("PASSWORD") autotype.TypeString(username + "\t" + password) }, } @@ -21,5 +24,4 @@ var autofillCmd = &cobra.Command{ func init() { rootCmd.AddCommand(autofillCmd) autofillCmd.PersistentFlags().String("username", "", "") - autofillCmd.PersistentFlags().String("password", "", "") } diff --git a/com.quexten.Goldwarden.yml b/com.quexten.Goldwarden.yml index d571d52..a5994ab 100644 --- a/com.quexten.Goldwarden.yml +++ b/com.quexten.Goldwarden.yml @@ -9,6 +9,8 @@ finish-args: - --socket=wayland - --socket=fallback-x11 - --filesystem=home + # todo remove this + - --socket=session-bus modules: - name: goldwarden buildsystem: simple @@ -16,13 +18,11 @@ modules: build-args: - "--share=network" build-commands: - - pip3 install --prefix=/app timeago secretstorage pycryptodome chacha20 + - pip3 install --prefix=/app tendo - install -D main.py /app/bin/main.py - install -D com.quexten.Goldwarden.desktop /app/share/applications/com.quexten.Goldwarden.desktop - install -D goldwarden.svg /app/share/icons/hicolor/scalable/apps/com.quexten.Goldwarden.svg - - cp -R ui/ /app/bin/gui/ - - cp -R backend/ /app/bin/backend/ - - cp -R event_monitors/ /app/bin/event_monitors/ + - cp -R ./ /app/bin/ sources: - type: dir path: ui \ No newline at end of file diff --git a/ui/autofill.py b/ui/autofill.py new file mode 100644 index 0000000..4836769 --- /dev/null +++ b/ui/autofill.py @@ -0,0 +1,108 @@ +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +import gc +import time +from gi.repository import Gtk, Adw, GLib +import goldwarden +import clipboard +from threading import Thread +import sys +import os + +class MyApp(Adw.Application): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.connect('activate', self.on_activate) + + def on_activate(self, app): + self.autofill_window = MainWindow(application=app) + self.autofill_window.logins = [] + self.autofill_window.present() + logins = goldwarden.get_vault_logins() + if logins == None: + return + app.autofill_window.logins = logins + +class MainWindow(Gtk.ApplicationWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.stack = Gtk.Stack() + self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) + self.set_child(self.stack) + + self.box = Gtk.Box() + self.box.set_orientation(Gtk.Orientation.VERTICAL) + self.stack.add_named(self.box, "box") + + self.text_view = Adw.EntryRow() + self.text_view.set_title("Search") + # on type func + def on_type(entry): + if len(entry.get_text()) > 1: + self.history_list.show() + else: + self.history_list.hide() + + while self.history_list.get_first_child() != None: + self.history_list.remove(self.history_list.get_first_child()) + + self.filtered_logins = list(filter(lambda i: entry.get_text().lower() in i["name"].lower(), self.logins)) + if len( self.filtered_logins) > 10: + self.filtered_logins = self.filtered_logins[0:10] + self.starts_with_logins = list(filter(lambda i: i["name"].lower().startswith(entry.get_text().lower()), self.logins)) + self.other_logins = list(filter(lambda i: i not in self.starts_with_logins , self.filtered_logins)) + self.filtered_logins = None + + for i in self.starts_with_logins + self.other_logins : + action_row = Adw.ActionRow() + action_row.set_title(i["name"]) + action_row.set_subtitle(i["username"]) + action_row.set_icon_name("dialog-password") + action_row.set_activatable(True) + action_row.password = i["password"] + action_row.username = i["username"] + self.history_list.append(action_row) + self.starts_with_logins = None + self.other_logins = None + self.text_view.connect("changed", lambda entry: on_type(entry)) + self.box.append(self.text_view) + + self.history_list = Gtk.ListBox() + # margin' + self.history_list.set_margin_start(10) + self.history_list.set_margin_end(10) + self.history_list.set_margin_top(10) + self.history_list.set_margin_bottom(10) + self.history_list.hide() + + keycont = Gtk.EventControllerKey() + def handle_keypress(cotroller, keyval, keycode, state, user_data): + if keycode == 36: + print("enter") + self.hide() + def do_autotype(username, password): + time.sleep(0.5) + goldwarden.autotype(username, password) + GLib.idle_add(lambda: self.show()) + autotypeThread = Thread(target=do_autotype, args=(self.history_list.get_selected_row().username, self.history_list.get_selected_row().password,)) + autotypeThread.start() + print(self.history_list.get_selected_row().get_title()) + if keyval == 112: + print("copy password") + clipboard.write(self.history_list.get_selected_row().password) + elif keyval == 117: + print("copy username") + clipboard.write(self.history_list.get_selected_row().username) + + keycont.connect('key-pressed', handle_keypress, self) + self.add_controller(keycont) + + self.history_list.get_style_context().add_class("boxed-list") + self.box.append(self.history_list) + self.set_default_size(700, 700) + self.set_title("Goldwarden") + +app = MyApp(application_id="com.quexten.Goldwarden.autofill") +app.run(sys.argv) \ No newline at end of file diff --git a/ui/background.py b/ui/background.py new file mode 100644 index 0000000..f684b4f --- /dev/null +++ b/ui/background.py @@ -0,0 +1,58 @@ +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +import gc +from gi.repository import Gtk, Adw, GLib, Gio +from random import randint +import time + + +def receive_autostart(self, *args): + print("autostart enabled..!?") + print(args) + +def request_autostart(): + bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) + proxy = Gio.DBusProxy.new_sync( + bus, + Gio.DBusProxyFlags.NONE, + None, + 'org.freedesktop.portal.Desktop', + '/org/freedesktop/portal/desktop', + 'org.freedesktop.portal.Background', + None, + ) + + token = 0 + randint(10000000, 20000000) + options = { + 'handle_token': GLib.Variant('s', f'com/quexten/Goldwarden/{token}'), + 'reason': GLib.Variant('s', ('Autostart Goldwarden in the background.')), + 'autostart': GLib.Variant('b', True), + 'commandline': GLib.Variant('as', ['main.py', '--hidden']), + 'dbus-activatable': GLib.Variant('b', False), + } + + try: + request = proxy.RequestBackground('(sa{sv})', "", options) + if request is None: + raise Exception( + "Registering with background portal failed." + ) + + bus.signal_subscribe( + 'org.freedesktop.portal.Desktop', + 'org.freedesktop.portal.Request', + 'Response', + request, + None, + Gio.DBusSignalFlags.NO_MATCH_RULE, + receive_autostart, + None, + ) + except Exception as e: + print(e) + +request_autostart() + +loop = GLib.MainLoop() +loop.run() diff --git a/ui/com.quexten.Goldwarden.desktop b/ui/com.quexten.Goldwarden.desktop index 1f6cf1e..6f154c5 100644 --- a/ui/com.quexten.Goldwarden.desktop +++ b/ui/com.quexten.Goldwarden.desktop @@ -5,6 +5,6 @@ Keywords=backup;recovery; Exec=main.py Terminal=false Type=Application -Icon=com.quexten.goldwarden.svg +Icon=com.quexten.Goldwarden.svg StartupNotify=true Categories=GNOME;GTK;Password Manager; \ No newline at end of file diff --git a/ui/goldwarden.py b/ui/goldwarden.py index 657c3ee..72ef2f5 100644 --- a/ui/goldwarden.py +++ b/ui/goldwarden.py @@ -1,5 +1,6 @@ import subprocess import json +import os BINARY_PATH = "/home/quexten/go/src/github.com/quexten/goldwarden/goldwarden" @@ -99,7 +100,10 @@ def get_vault_logins(): return None def autotype(username, password): - restic_cmd = f"{BINARY_PATH} autotype --username {username} --password {password}" - result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + # environment + env = os.environ.copy() + env["PASSWORD"] = password + restic_cmd = f"{BINARY_PATH} autotype --username {username}" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True, env=env) if result.returncode != 0: raise Exception("Failed to initialize repository, err", result.stderr) \ No newline at end of file diff --git a/ui/goldwarden.svg b/ui/goldwarden.svg new file mode 100644 index 0000000..c26e20f --- /dev/null +++ b/ui/goldwarden.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/main.py b/ui/main.py index 4961adc..c3480a3 100644 --- a/ui/main.py +++ b/ui/main.py @@ -1,307 +1,29 @@ #!/usr/bin/python -import sys -import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') -import gc - -from gi.repository import Gtk, Adw, Gdk, Graphene, Gsk, Gio, GLib, GObject -import monitors.dbus_autofill_monitor -import goldwarden -import clipboard import time -from threading import Thread +import subprocess +from tendo import singleton +import monitors.dbus_autofill_monitor +import sys -class MainWindow(Gtk.ApplicationWindow): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) +try: + subprocess.Popen(["python3", "/app/bin/background.py"], start_new_session=True) +except: + pass - self.stack = Gtk.Stack() - self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) - self.set_child(self.stack) +if "--hidden" not in sys.argv: + try: + subprocess.Popen(["python3", "/app/bin/settings.py"], start_new_session=True) + except: + pass - self.box = Gtk.Box() - self.box.set_orientation(Gtk.Orientation.VERTICAL) - self.stack.add_named(self.box, "box") +try: + me = singleton.SingleInstance() +except: + exit() - - self.text_view = Adw.EntryRow() - self.text_view.set_title("Search") - # on type func - def on_type(entry): - if len(entry.get_text()) > 1: - self.history_list.show() - else: - self.history_list.hide() - - while self.history_list.get_first_child() != None: - self.history_list.remove(self.history_list.get_first_child()) - - self.filtered_logins = list(filter(lambda i: entry.get_text().lower() in i["name"].lower(), self.logins)) - if len( self.filtered_logins) > 10: - self.filtered_logins = self.filtered_logins[0:10] - self.starts_with_logins = list(filter(lambda i: i["name"].lower().startswith(entry.get_text().lower()), self.logins)) - self.other_logins = list(filter(lambda i: i not in self.starts_with_logins , self.filtered_logins)) - self.filtered_logins = None - - for i in self.starts_with_logins + self.other_logins : - action_row = Adw.ActionRow() - action_row.set_title(i["name"]) - action_row.set_subtitle(i["username"]) - action_row.set_icon_name("dialog-password") - action_row.set_activatable(True) - action_row.password = i["password"] - action_row.username = i["username"] - self.history_list.append(action_row) - self.starts_with_logins = None - self.other_logins = None - self.text_view.connect("changed", lambda entry: on_type(entry)) - self.box.append(self.text_view) - - self.history_list = Gtk.ListBox() - # margin' - self.history_list.set_margin_start(10) - self.history_list.set_margin_end(10) - self.history_list.set_margin_top(10) - self.history_list.set_margin_bottom(10) - self.history_list.hide() - - keycont = Gtk.EventControllerKey() - def handle_keypress(controller, keyval, keycode, state, user_data): - if keycode == 36: - print("enter") - self.hide() - def do_autotype(username, password): - time.sleep(0.5) - goldwarden.autotype(username, password) - GLib.idle_add(lambda: self.show()) - autotypeThread = Thread(target=do_autotype, args=(self.history_list.get_selected_row().username, self.history_list.get_selected_row().password,)) - autotypeThread.start() - print(self.history_list.get_selected_row().get_title()) - if keyval == 112: - print("copy password") - clipboard.write(self.history_list.get_selected_row().password) - elif keyval == 117: - print("copy username") - clipboard.write(self.history_list.get_selected_row().username) - - keycont.connect('key-pressed', handle_keypress, self) - self.add_controller(keycont) - - self.history_list.get_style_context().add_class("boxed-list") - self.box.append(self.history_list) - self.set_default_size(700, 700) - self.set_title("Goldwarden") - - def on_close(window): - while self.history_list.get_first_child() != None: - self.history_list.remove(self.history_list.get_first_child()) - window.hide() - gc.collect() - return True - self.connect("close-request", on_close) - - def show(self): - for i in range(0, 5): - action_row = Adw.ActionRow() - action_row.set_title("aaa") - action_row.set_subtitle("Test") - self.history_list.append(action_row) - - -class SettingsWinvdow(Gtk.ApplicationWindow): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.stack = Gtk.Stack() - self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) - self.set_child(self.stack) - - self.preferences_page = Adw.PreferencesPage() - self.preferences_page.set_title("General") - self.stack.add_named(self.preferences_page, "preferences_page") - - self.preferences_group = Adw.PreferencesGroup() - self.preferences_group.set_title("Services") - self.preferences_page.add(self.preferences_group) - - self.ssh_row = Adw.ActionRow() - self.ssh_row.set_title("SSH Daemon") - self.ssh_row.set_subtitle("Listening at ~/.goldwarden-ssh-agent.sock") - self.preferences_group.add(self.ssh_row) - - self.login_with_device = Adw.ActionRow() - self.login_with_device.set_title("Login with device") - self.login_with_device.set_subtitle("Waiting for requests...") - self.preferences_group.add(self.login_with_device) - - self.autofill_row = Adw.ActionRow() - self.autofill_row.set_title("Autofill Shortcut") - self.autofill_row.set_subtitle("Unavailable, please set up a shortcut in your desktop environment (README)") - self.preferences_group.add(self.autofill_row) - - self.status_row = Adw.ActionRow() - self.status_row.set_title("DBUS Service") - self.status_row.set_subtitle("Listening") - self.preferences_group.add(self.status_row) - - self.status_row = Adw.ActionRow() - self.status_row.set_title("Vault Status") - self.status_row.set_subtitle("Locked") - self.preferences_group.add(self.status_row) - - self.login_row = Adw.ActionRow() - self.login_row.set_title("Vault Login Entries") - self.login_row.set_subtitle("0") - self.preferences_group.add(self.login_row) - - self.notes_row = Adw.ActionRow() - self.notes_row.set_title("Vault Notes") - self.notes_row.set_subtitle("0") - self.preferences_group.add(self.notes_row) - - self.login_button = Gtk.Button() - self.login_button.set_label("Login") - self.login_button.connect("clicked", lambda button: show_login()) - self.login_button.set_sensitive(False) - self.login_button.set_margin_top(10) - self.preferences_group.add(self.login_button) - - self.set_pin_button = Gtk.Button() - self.set_pin_button.set_label("Set Pin") - def set_pin(): - set_pin_thread = Thread(target=goldwarden.enable_pin) - set_pin_thread.start() - self.set_pin_button.connect("clicked", lambda button: set_pin()) - self.set_pin_button.set_margin_top(10) - self.set_pin_button.set_sensitive(False) - self.preferences_group.add(self.set_pin_button) - - self.unlock_button = Gtk.Button() - self.unlock_button.set_label("Unlock") - self.unlock_button.set_margin_top(10) - def unlock_button_clicked(): - action = goldwarden.unlock if self.unlock_button.get_label() == "Unlock" else goldwarden.lock - unlock_thread = Thread(target=action) - unlock_thread.start() - self.unlock_button.connect("clicked", lambda button: unlock_button_clicked()) - # set disabled - self.unlock_button.set_sensitive(False) - self.preferences_group.add(self.unlock_button) - - self.logout_button = Gtk.Button() - self.logout_button.set_label("Logout") - self.logout_button.set_margin_top(10) - self.logout_button.connect("clicked", lambda button: goldwarden.purge()) - self.preferences_group.add(self.logout_button) - - def update_labels(): - pin_set = goldwarden.is_pin_enabled() - status = goldwarden.get_vault_status() - locked = status["locked"] - self.login_button.set_sensitive(pin_set and not locked) - self.set_pin_button.set_sensitive(not pin_set or not locked) - self.status_row.set_subtitle(str("Unlocked" if not locked else "Locked")) - self.login_row.set_subtitle(str(status["loginEntries"])) - self.notes_row.set_subtitle(str(status["noteEntries"])) - self.unlock_button.set_sensitive(True) - self.unlock_button.set_label("Unlock" if locked else "Lock") - GLib.timeout_add(1000, update_labels) - - GLib.timeout_add(1000, update_labels) - self.set_default_size(400, 700) - self.set_title("Goldwarden") - - - #add title buttons - self.title_bar = Gtk.HeaderBar() - self.set_titlebar(self.title_bar) - -class MyApp(Adw.Application): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.connect('activate', self.on_activate) - - def on_activate(self, app): - if hasattr(self, "win") is False: - app.hold() - self.win = MainWindow(application=app) - self.win.set_hide_on_close(True) - self.win.hide() - self.settings_win = SettingsWinvdow(application=app) - self.settings_win.set_hide_on_close(True) - self.settings_win.present() - -app = MyApp(application_id="com.quexten.Goldwarden") def on_autofill(): - logins = goldwarden.get_vault_logins() - if logins == None: - return - app.win.logins = logins - app.win.show() - app.win.present() + subprocess.Popen(["python3", "/app/bin/autofill.py"], start_new_session=True) monitors.dbus_autofill_monitor.on_autofill = on_autofill -def show_login(): - dialog = Gtk.Dialog(title="Goldwarden") - preference_group = Adw.PreferencesGroup() - preference_group.set_title("Config") - dialog.get_content_area().append(preference_group) - - api_url_entry = Adw.EntryRow() - api_url_entry.set_title("API Url") - # set value - api_url_entry.set_text("https://api.bitwarden.com/") - preference_group.add(api_url_entry) - - identity_url_entry = Adw.EntryRow() - identity_url_entry.set_title("Identity Url") - identity_url_entry.set_text("https://identity.bitwarden.com/") - preference_group.add(identity_url_entry) - - notification_url_entry = Adw.EntryRow() - notification_url_entry.set_title("Notification URL") - notification_url_entry.set_text("https://notifications.bitwarden.com/") - preference_group.add(notification_url_entry) - - auth_preference_group = Adw.PreferencesGroup() - auth_preference_group.set_title("Authentication") - dialog.get_content_area().append(auth_preference_group) - - email_entry = Adw.EntryRow() - email_entry.set_title("Email") - email_entry.set_text("") - auth_preference_group.add(email_entry) - - dialog.add_button("Login", Gtk.ResponseType.OK) - def on_save(res): - if res != Gtk.ResponseType.OK: - return - goldwarden.set_api_url(api_url_entry.get_text()) - goldwarden.set_identity_url(identity_url_entry.get_text()) - goldwarden.set_notification_url(notification_url_entry.get_text()) - def login(): - res = goldwarden.login_with_password(email_entry.get_text(), "password") - def handle_res(): - print("handle res", res) - if res == "ok": - dialog.close() - print("ok") - elif res == "badpass": - bad_pass_diag = Gtk.MessageDialog(transient_for=dialog, modal=True, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text="Bad password") - bad_pass_diag.connect("response", lambda dialog, response: bad_pass_diag.close()) - bad_pass_diag.present() - GLib.idle_add(handle_res) - - login_thread = Thread(target=login) - login_thread.start() - - #ok response - dialog.connect("response", lambda dialog, response: on_save(response)) - dialog.set_default_size(400, 200) - dialog.set_modal(True) - dialog.present() - - -app.run(sys.argv) - +while True: + time.sleep(60) \ No newline at end of file diff --git a/ui/settings.py b/ui/settings.py new file mode 100644 index 0000000..2c823f4 --- /dev/null +++ b/ui/settings.py @@ -0,0 +1,216 @@ +#!/usr/bin/python +import sys +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +import gc + +from gi.repository import Gtk, Adw, GLib +import goldwarden +from threading import Thread + +class SettingsWinvdow(Gtk.ApplicationWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.stack = Gtk.Stack() + self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) + self.set_child(self.stack) + + self.preferences_page = Adw.PreferencesPage() + self.preferences_page.set_title("General") + self.stack.add_named(self.preferences_page, "preferences_page") + + self.preferences_group = Adw.PreferencesGroup() + self.preferences_group.set_title("Services") + self.preferences_page.add(self.preferences_group) + + self.ssh_row = Adw.ActionRow() + self.ssh_row.set_title("SSH Daemon") + self.ssh_row.set_subtitle("Listening at ~/.goldwarden-ssh-agent.sock") + self.preferences_group.add(self.ssh_row) + + self.login_with_device = Adw.ActionRow() + self.login_with_device.set_title("Login with device") + self.login_with_device.set_subtitle("Waiting for requests...") + self.preferences_group.add(self.login_with_device) + + self.status_row = Adw.ActionRow() + self.status_row.set_title("DBUS Service") + self.status_row.set_subtitle("Listening") + self.preferences_group.add(self.status_row) + + self.shortcut_preferences_group = Adw.PreferencesGroup() + self.shortcut_preferences_group.set_title("Shortcuts") + self.preferences_page.add(self.shortcut_preferences_group) + + self.autofill_row = Adw.ActionRow() + self.autofill_row.set_title("Autofill Shortcut") + self.autofill_row.set_subtitle("Unavailable, please set up a shortcut in your desktop environment (README)") + self.shortcut_preferences_group.add(self.autofill_row) + + self.copy_username_shortcut_row = Adw.ActionRow() + self.copy_username_shortcut_row.set_title("Copy Username Shortcut") + self.copy_username_shortcut_row.set_subtitle("U") + self.shortcut_preferences_group.add(self.copy_username_shortcut_row) + + self.copy_password_shortcut_row = Adw.ActionRow() + self.copy_password_shortcut_row.set_title("Copy Password Shortcut") + self.copy_password_shortcut_row.set_subtitle("P") + self.shortcut_preferences_group.add(self.copy_password_shortcut_row) + + self.vault_status_preferences_group = Adw.PreferencesGroup() + self.vault_status_preferences_group.set_title("Vault Status") + self.preferences_page.add(self.vault_status_preferences_group) + + self.status_row = Adw.ActionRow() + self.status_row.set_title("Vault Status") + self.status_row.set_subtitle("Locked") + self.vault_status_preferences_group.add(self.status_row) + + self.login_row = Adw.ActionRow() + self.login_row.set_title("Vault Login Entries") + self.login_row.set_subtitle("0") + self.vault_status_preferences_group.add(self.login_row) + + self.notes_row = Adw.ActionRow() + self.notes_row.set_title("Vault Notes") + self.notes_row.set_subtitle("0") + self.vault_status_preferences_group.add(self.notes_row) + + self.action_preferences_group = Adw.PreferencesGroup() + self.action_preferences_group.set_title("Actions") + self.preferences_page.add(self.action_preferences_group) + + self.login_button = Gtk.Button() + self.login_button.set_label("Login") + self.login_button.connect("clicked", lambda button: show_login()) + self.login_button.set_sensitive(False) + self.login_button.set_margin_top(10) + self.login_button.get_style_context().add_class("suggested-action") + self.action_preferences_group.add(self.login_button) + + self.set_pin_button = Gtk.Button() + self.set_pin_button.set_label("Set Pin") + def set_pin(): + set_pin_thread = Thread(target=goldwarden.enable_pin) + set_pin_thread.start() + self.set_pin_button.connect("clicked", lambda button: set_pin()) + self.set_pin_button.set_margin_top(10) + self.set_pin_button.set_sensitive(False) + self.set_pin_button.get_style_context().add_class("suggested-action") + self.action_preferences_group.add(self.set_pin_button) + + self.unlock_button = Gtk.Button() + self.unlock_button.set_label("Unlock") + self.unlock_button.set_margin_top(10) + def unlock_button_clicked(): + action = goldwarden.unlock if self.unlock_button.get_label() == "Unlock" else goldwarden.lock + unlock_thread = Thread(target=action) + unlock_thread.start() + self.unlock_button.connect("clicked", lambda button: unlock_button_clicked()) + # set disabled + self.unlock_button.set_sensitive(False) + self.action_preferences_group.add(self.unlock_button) + + self.logout_button = Gtk.Button() + self.logout_button.set_label("Logout") + self.logout_button.set_margin_top(10) + self.logout_button.connect("clicked", lambda button: goldwarden.purge()) + self.logout_button.get_style_context().add_class("destructive-action") + self.action_preferences_group.add(self.logout_button) + + def update_labels(): + pin_set = goldwarden.is_pin_enabled() + status = goldwarden.get_vault_status() + locked = status["locked"] + self.login_button.set_sensitive(pin_set and not locked) + self.set_pin_button.set_sensitive(not pin_set or not locked) + self.status_row.set_subtitle(str("Unlocked" if not locked else "Locked")) + self.login_row.set_subtitle(str(status["loginEntries"])) + self.notes_row.set_subtitle(str(status["noteEntries"])) + self.unlock_button.set_sensitive(True) + self.unlock_button.set_label("Unlock" if locked else "Lock") + GLib.timeout_add(1000, update_labels) + + GLib.timeout_add(1000, update_labels) + self.set_default_size(400, 700) + self.set_title("Goldwarden") + + + #add title buttons + self.title_bar = Gtk.HeaderBar() + self.set_titlebar(self.title_bar) + +class MyApp(Adw.Application): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.connect('activate', self.on_activate) + + def on_activate(self, app): + self.settings_win = SettingsWinvdow(application=app) + self.settings_win.present() + +app = MyApp(application_id="com.quexten.Goldwarden") + +def show_login(): + dialog = Gtk.Dialog(title="Goldwarden") + preference_group = Adw.PreferencesGroup() + preference_group.set_title("Config") + dialog.get_content_area().append(preference_group) + + api_url_entry = Adw.EntryRow() + api_url_entry.set_title("API Url") + # set value + api_url_entry.set_text("https://api.bitwarden.com/") + preference_group.add(api_url_entry) + + identity_url_entry = Adw.EntryRow() + identity_url_entry.set_title("Identity Url") + identity_url_entry.set_text("https://identity.bitwarden.com/") + preference_group.add(identity_url_entry) + + notification_url_entry = Adw.EntryRow() + notification_url_entry.set_title("Notification URL") + notification_url_entry.set_text("https://notifications.bitwarden.com/") + preference_group.add(notification_url_entry) + + auth_preference_group = Adw.PreferencesGroup() + auth_preference_group.set_title("Authentication") + dialog.get_content_area().append(auth_preference_group) + + email_entry = Adw.EntryRow() + email_entry.set_title("Email") + email_entry.set_text("") + auth_preference_group.add(email_entry) + + dialog.add_button("Login", Gtk.ResponseType.OK) + def on_save(res): + if res != Gtk.ResponseType.OK: + return + goldwarden.set_api_url(api_url_entry.get_text()) + goldwarden.set_identity_url(identity_url_entry.get_text()) + goldwarden.set_notification_url(notification_url_entry.get_text()) + def login(): + res = goldwarden.login_with_password(email_entry.get_text(), "password") + def handle_res(): + print("handle res", res) + if res == "ok": + dialog.close() + print("ok") + elif res == "badpass": + bad_pass_diag = Gtk.MessageDialog(transient_for=dialog, modal=True, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text="Bad password") + bad_pass_diag.connect("response", lambda dialog, response: bad_pass_diag.close()) + bad_pass_diag.present() + GLib.idle_add(handle_res) + + login_thread = Thread(target=login) + login_thread.start() + + #ok response + dialog.connect("response", lambda dialog, response: on_save(response)) + dialog.set_default_size(400, 200) + dialog.set_modal(True) + dialog.present() + +app.run(sys.argv) \ No newline at end of file From b9be947d9435bb4933948fcd95dcb40d994bbaa6 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 23 Dec 2023 13:24:46 +0100 Subject: [PATCH 11/14] Spawn goldwarden daemon automatically on boot; fix biometrics --- agent/sockets/callingcontext.go | 26 +++++++++++++++++++++----- agent/systemauth/biometrics/polkit.go | 3 +++ agent/systemauth/systemauth.go | 1 + com.quexten.Goldwarden.yml | 7 +++++-- ui/goldwarden.py | 22 +++++++++++++++++++++- ui/main.py | 15 +++++++++++++++ ui/settings.py | 21 +++++++++++++-------- 7 files changed, 79 insertions(+), 16 deletions(-) diff --git a/agent/sockets/callingcontext.go b/agent/sockets/callingcontext.go index 76d3063..4a725f0 100644 --- a/agent/sockets/callingcontext.go +++ b/agent/sockets/callingcontext.go @@ -3,6 +3,7 @@ package sockets import ( "net" "os/user" + "time" gops "github.com/mitchellh/go-ps" "inet.af/peercred" @@ -20,11 +21,26 @@ type CallingContext struct { func GetCallingContext(connection net.Conn) CallingContext { creds, err := peercred.Get(connection) + errorContext := CallingContext{ + UserName: "unknown user", + ProcessName: "unknown process", + ParentProcessName: "unknown parent", + GrandParentProcessName: "unknown grandparent", + ProcessPid: time.Now().UTC().Nanosecond(), + ParentProcessPid: time.Now().UTC().Nanosecond(), + GrandParentProcessPid: time.Now().UTC().Nanosecond(), + } if err != nil { - panic(err) + return errorContext } pid, _ := creds.PID() process, err := gops.FindProcess(pid) + if err != nil { + return errorContext + } + if process == nil { + return errorContext + } // git is epheremal and spawns ssh-keygen and ssh so we need to anchor to git if process.Executable() == "ssh-keygen" || process.Executable() == "ssh" { @@ -38,22 +54,22 @@ func GetCallingContext(connection net.Conn) CallingContext { uid, _ := creds.UserID() ppid := process.PPid() if err != nil { - panic(err) + return errorContext } parentProcess, err := gops.FindProcess(ppid) if err != nil { - panic(err) + return errorContext } parentParentProcess, err := gops.FindProcess(parentProcess.PPid()) if err != nil { - panic(err) + return errorContext } username, err := user.LookupId(uid) if err != nil { - panic(err) + return errorContext } return CallingContext{ diff --git a/agent/systemauth/biometrics/polkit.go b/agent/systemauth/biometrics/polkit.go index dcb7565..7a08647 100644 --- a/agent/systemauth/biometrics/polkit.go +++ b/agent/systemauth/biometrics/polkit.go @@ -78,15 +78,18 @@ func BiometricsWorking() bool { authority, err := polkit.NewAuthority() if err != nil { + log.Warn("Failed to create polkit authority: %s", err.Error()) return false } result, err := authority.EnumerateActions("en") if err != nil { + log.Warn("Failed to enumerate polkit actions: %s", err.Error()) return false } if len(result) == 0 { + log.Warn("No polkit actions found") return false } diff --git a/agent/systemauth/systemauth.go b/agent/systemauth/systemauth.go index 931a5cc..5ebf567 100644 --- a/agent/systemauth/systemauth.go +++ b/agent/systemauth/systemauth.go @@ -87,6 +87,7 @@ func GetPermission(sessionType SessionType, ctx sockets.CallingContext, config * return false, nil } } else { + log.Warn("Biometrics is not available, asking for pin") pin, err := pinentry.GetPassword("Enter PIN", "Biometrics is not available. Enter your pin to authorize this action. "+message) if err != nil { return false, err diff --git a/com.quexten.Goldwarden.yml b/com.quexten.Goldwarden.yml index a5994ab..434dbe4 100644 --- a/com.quexten.Goldwarden.yml +++ b/com.quexten.Goldwarden.yml @@ -8,9 +8,12 @@ finish-args: - --share=network - --socket=wayland - --socket=fallback-x11 - - --filesystem=home - # todo remove this + - --device=dri + # - --filesystem=home + - --filesystem=host + # polkit & locking.. should filter this later on - --socket=session-bus + - --socket=system-bus modules: - name: goldwarden buildsystem: simple diff --git a/ui/goldwarden.py b/ui/goldwarden.py index 72ef2f5..dca8047 100644 --- a/ui/goldwarden.py +++ b/ui/goldwarden.py @@ -106,4 +106,24 @@ def autotype(username, password): restic_cmd = f"{BINARY_PATH} autotype --username {username}" result = subprocess.run(restic_cmd.split(), capture_output=True, text=True, env=env) if result.returncode != 0: - raise Exception("Failed to initialize repository, err", result.stderr) \ No newline at end of file + raise Exception("Failed to initialize repository, err", result.stderr) + +def is_daemon_running(): + restic_cmd = f"{BINARY_PATH} vault status" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + return False + daemon_not_running = ("daemon running?" in result.stderr or "daemon running" in result.stderr) + return not daemon_not_running + +def run_daemon(): + restic_cmd = f"{BINARY_PATH} daemonize" + # print while running + result = subprocess.Popen(restic_cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + if result.returncode != 0: + print("Failed err", result.stderr) + for line in result.stdout: + print(line.decode("utf-8")) + result.wait() + print("quitting goldwarden daemon") + return result.returncode \ No newline at end of file diff --git a/ui/main.py b/ui/main.py index c3480a3..627dccd 100644 --- a/ui/main.py +++ b/ui/main.py @@ -4,6 +4,8 @@ import subprocess from tendo import singleton import monitors.dbus_autofill_monitor import sys +import goldwarden +from threading import Thread try: subprocess.Popen(["python3", "/app/bin/background.py"], start_new_session=True) @@ -21,6 +23,19 @@ try: except: exit() +def run_daemon(): + # todo: do a proper check + time.sleep(20) + if not goldwarden.is_daemon_running(): + goldwarden.run_daemon() + +if not goldwarden.is_daemon_running(): + print("daemon not running.. autostarting") + daemonThread = Thread(target=run_daemon) + daemonThread.start() + +print("starting autofill monitor") + def on_autofill(): subprocess.Popen(["python3", "/app/bin/autofill.py"], start_new_session=True) monitors.dbus_autofill_monitor.on_autofill = on_autofill diff --git a/ui/settings.py b/ui/settings.py index 2c823f4..343ccab 100644 --- a/ui/settings.py +++ b/ui/settings.py @@ -123,14 +123,19 @@ class SettingsWinvdow(Gtk.ApplicationWindow): def update_labels(): pin_set = goldwarden.is_pin_enabled() status = goldwarden.get_vault_status() - locked = status["locked"] - self.login_button.set_sensitive(pin_set and not locked) - self.set_pin_button.set_sensitive(not pin_set or not locked) - self.status_row.set_subtitle(str("Unlocked" if not locked else "Locked")) - self.login_row.set_subtitle(str(status["loginEntries"])) - self.notes_row.set_subtitle(str(status["noteEntries"])) - self.unlock_button.set_sensitive(True) - self.unlock_button.set_label("Unlock" if locked else "Lock") + if status != None: + locked = status["locked"] + self.login_button.set_sensitive(pin_set and not locked) + self.set_pin_button.set_sensitive(not pin_set or not locked) + self.status_row.set_subtitle(str("Unlocked" if not locked else "Locked")) + self.login_row.set_subtitle(str(status["loginEntries"])) + self.notes_row.set_subtitle(str(status["noteEntries"])) + self.unlock_button.set_sensitive(True) + self.unlock_button.set_label("Unlock" if locked else "Lock") + else: + is_daemon_running = goldwarden.is_daemon_running() + if not is_daemon_running: + self.status_row.set_subtitle("Daemon not running") GLib.timeout_add(1000, update_labels) GLib.timeout_add(1000, update_labels) From f68dd3e973f752fc7a3b7b0ec7c8096f45764abb Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 23 Dec 2023 13:44:16 +0100 Subject: [PATCH 12/14] Improve usability of callingcontext & approval --- agent/sockets/callingcontext.go | 15 +++++++-------- agent/systemauth/systemauth.go | 8 ++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/agent/sockets/callingcontext.go b/agent/sockets/callingcontext.go index 4a725f0..3541094 100644 --- a/agent/sockets/callingcontext.go +++ b/agent/sockets/callingcontext.go @@ -3,7 +3,6 @@ package sockets import ( "net" "os/user" - "time" gops "github.com/mitchellh/go-ps" "inet.af/peercred" @@ -22,13 +21,13 @@ type CallingContext struct { func GetCallingContext(connection net.Conn) CallingContext { creds, err := peercred.Get(connection) errorContext := CallingContext{ - UserName: "unknown user", - ProcessName: "unknown process", - ParentProcessName: "unknown parent", - GrandParentProcessName: "unknown grandparent", - ProcessPid: time.Now().UTC().Nanosecond(), - ParentProcessPid: time.Now().UTC().Nanosecond(), - GrandParentProcessPid: time.Now().UTC().Nanosecond(), + UserName: "unknown", + ProcessName: "unknown", + ParentProcessName: "unknown", + GrandParentProcessName: "unknown", + ProcessPid: 0, + ParentProcessPid: 0, + GrandParentProcessPid: 0, } if err != nil { return errorContext diff --git a/agent/systemauth/systemauth.go b/agent/systemauth/systemauth.go index 5ebf567..78e2995 100644 --- a/agent/systemauth/systemauth.go +++ b/agent/systemauth/systemauth.go @@ -97,10 +97,10 @@ func GetPermission(sessionType SessionType, ctx sockets.CallingContext, config * } } - approval, err := pinentry.GetApproval("Goldwarden authorization", message) - if err != nil || !approval { - return false, err - } + // approval, err := pinentry.GetApproval("Goldwarden authorization", message) + // if err != nil || !approval { + // return false, err + // } log.Info("Permission granted, creating session") sessionStore.CreateSession(ctx.ProcessPid, ctx.ParentProcessPid, ctx.GrandParentProcessPid, sessionType) From 0136dac0086f3731785730e85677a0b1aa4b754a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 23 Dec 2023 13:44:41 +0100 Subject: [PATCH 13/14] Make daemon start instantly when not started in background --- ui/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/main.py b/ui/main.py index 627dccd..a2c6c91 100644 --- a/ui/main.py +++ b/ui/main.py @@ -12,7 +12,9 @@ try: except: pass -if "--hidden" not in sys.argv: +is_hidden = "--hidden" in sys.argv + +if not is_hidden: try: subprocess.Popen(["python3", "/app/bin/settings.py"], start_new_session=True) except: @@ -25,7 +27,8 @@ except: def run_daemon(): # todo: do a proper check - time.sleep(20) + if is_hidden: + time.sleep(20) if not goldwarden.is_daemon_running(): goldwarden.run_daemon() From 16968de5bb7e2f442cba58eeec8d5d1aa981d878 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 23 Dec 2023 13:56:15 +0100 Subject: [PATCH 14/14] Update readme --- Readme.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Readme.md b/Readme.md index 35c6e65..1ec2790 100644 --- a/Readme.md +++ b/Readme.md @@ -24,6 +24,11 @@ On Linux, you need at least a working Polkit installation and a pinentry agent. ### Installation +#### Flatpak (WIP) +There is a flatpak that includes a small UI, autotype functionality and autostarting of the daemon. +**Not yet on flathub** + +#### CLI On Arch linux, or other distributions with access to the AUR, simply: ``` yay -S goldwarden @@ -85,7 +90,7 @@ Run a command with injected environment variables goldwarden run -- ``` -Autofill (Flatpak only?) +Autofill (Flatpak Only) ``` dbus-send --type=method_call --dest=com.quexten.goldwarden /com/quexten/goldwarden com.quexten.goldwarden.Autofill.autofill ``` @@ -176,6 +181,9 @@ restic backup [goldwarden_autofill.webm](https://github.com/quexten/goldwarden/assets/11866552/6ac7cdc2-0cd7-42fd-9fd0-cfff26e2ceee) You can bind this to a hotkey in your desktop environment (i.e i3/sway config file, Gnome custom shortcuts, etc). +``` +dbus-send --type=method_call --dest=com.quexten.goldwarden /com/quexten/goldwarden com.quexten.goldwarden.Autofill.autofill +``` #### XDG-RemoteDesktop-Portal @@ -183,13 +191,6 @@ By default, the remote desktop portal is used. As long as your desktop environme this enables autotyping without having to modify permissions. `goldwarden autofill` -#### (Legacy) Uinput -If your desktop environment does not implement the remotedesktop portal, your only other option is uinput based autotype. This requires your user -to have access to the input group to use uinput to autotype. This needs a keyboardlayout to map the letters to -keycodes. Currently supported are qwerty and dvorak. -`goldwarden autofill --layout qwerty` -`goldwarden autofill --layout dvorak` - ### Login with device Approving other devices works out of the box and is enabled by default. If the agent is unlocked, you will be prompted to approve the device. If you want to log into goldwarden using another device, add the "--passwordless" parameter to the login command.