From 30237e79b2a9dbf02eba8c8dabee587570eae2d8 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 17 Jul 2023 03:23:26 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + LICENSE | 21 ++ Readme.md | 211 +++++++++++ agent/actions/actions.go | 77 ++++ agent/actions/config.go | 45 +++ agent/actions/getclicredentials.go | 48 +++ agent/actions/login.go | 100 ++++++ agent/actions/logins.go | 145 ++++++++ agent/actions/ssh.go | 62 ++++ agent/actions/vault.go | 174 +++++++++ agent/agent.go | 211 +++++++++++ agent/bitwarden/auth.go | 159 +++++++++ agent/bitwarden/ciphers.go | 32 ++ agent/bitwarden/crypto/crypto.go | 124 +++++++ agent/bitwarden/crypto/encstring.go | 256 +++++++++++++ agent/bitwarden/crypto/kdf.go | 62 ++++ agent/bitwarden/crypto/keyhierarchy.go | 84 +++++ agent/bitwarden/crypto/keyring.go | 39 ++ agent/bitwarden/http.go | 120 +++++++ agent/bitwarden/models/models.go | 154 ++++++++ agent/bitwarden/paswordless.go | 82 +++++ agent/bitwarden/sync.go | 54 +++ agent/bitwarden/twofactor.go | 171 +++++++++ agent/bitwarden/websocket.go | 263 ++++++++++++++ agent/config/config.go | 351 ++++++++++++++++++ agent/sockets/callingcontext.go | 51 +++ agent/ssh/keys.go | 71 ++++ agent/ssh/ssh.go | 178 +++++++++ agent/systemauth/biometrics.go | 43 +++ agent/systemauth/pinentry.go | 64 ++++ agent/vault/vault.go | 391 ++++++++++++++++++++ autofill/autofill.go | 80 +++++ autofill/gioAutofillDialog.go | 239 +++++++++++++ autofill/uinput/dvorak.go | 259 ++++++++++++++ autofill/uinput/qwerty.go | 253 +++++++++++++ autofill/uinput/uinput.go | 170 +++++++++ client/client.go | 56 +++ cmd/autofill.go | 23 ++ cmd/config.go | 87 +++++ cmd/daemonize.go | 37 ++ cmd/login.go | 46 +++ cmd/pin.go | 65 ++++ cmd/root.go | 26 ++ cmd/run.go | 64 ++++ cmd/ssh.go | 84 +++++ cmd/vault.go | 101 ++++++ go.mod | 48 +++ go.sum | 135 +++++++ ipc/ipc.go | 456 ++++++++++++++++++++++++ main.go | 9 + resources/com.quexten.goldwarden.policy | 43 +++ 51 files changed, 6127 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Readme.md create mode 100644 agent/actions/actions.go create mode 100644 agent/actions/config.go create mode 100644 agent/actions/getclicredentials.go create mode 100644 agent/actions/login.go create mode 100644 agent/actions/logins.go create mode 100644 agent/actions/ssh.go create mode 100644 agent/actions/vault.go create mode 100644 agent/agent.go create mode 100644 agent/bitwarden/auth.go create mode 100644 agent/bitwarden/ciphers.go create mode 100644 agent/bitwarden/crypto/crypto.go create mode 100644 agent/bitwarden/crypto/encstring.go create mode 100644 agent/bitwarden/crypto/kdf.go create mode 100644 agent/bitwarden/crypto/keyhierarchy.go create mode 100644 agent/bitwarden/crypto/keyring.go create mode 100644 agent/bitwarden/http.go create mode 100644 agent/bitwarden/models/models.go create mode 100644 agent/bitwarden/paswordless.go create mode 100644 agent/bitwarden/sync.go create mode 100644 agent/bitwarden/twofactor.go create mode 100644 agent/bitwarden/websocket.go create mode 100644 agent/config/config.go create mode 100644 agent/sockets/callingcontext.go create mode 100644 agent/ssh/keys.go create mode 100644 agent/ssh/ssh.go create mode 100644 agent/systemauth/biometrics.go create mode 100644 agent/systemauth/pinentry.go create mode 100644 agent/vault/vault.go create mode 100644 autofill/autofill.go create mode 100644 autofill/gioAutofillDialog.go create mode 100644 autofill/uinput/dvorak.go create mode 100644 autofill/uinput/qwerty.go create mode 100644 autofill/uinput/uinput.go create mode 100644 client/client.go create mode 100644 cmd/autofill.go create mode 100644 cmd/config.go create mode 100644 cmd/daemonize.go create mode 100644 cmd/login.go create mode 100644 cmd/pin.go create mode 100644 cmd/root.go create mode 100644 cmd/run.go create mode 100644 cmd/ssh.go create mode 100644 cmd/vault.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 ipc/ipc.go create mode 100644 main.go create mode 100644 resources/com.quexten.goldwarden.policy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26bab9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode +*debug* +goldwarden* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3f16ec1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Bernd Schoolmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..0e48379 --- /dev/null +++ b/Readme.md @@ -0,0 +1,211 @@ +## Goldwarden + +Goldwarden is a Bitwarden compatible CLI tool written in Go. It focuses on features for Desktop integration, and enhanced security measures that other tools do not provide, such as: + +- Support for SSH Agent (Git signing and SSH login) +- Support for injecting environment variables into the environment of a cli command +- System wide autofill +- Biometric authentication (via Polkit) for each credential access +- Vault content is held encrypted in memory and only briefly decrypted when needed +- Kernel level memory protection for keys (via the memguard library) +- Additional measures to protect against memory dumps +- Passwordless login (Approval of other login) +- Fido2 (Webauthn) support +- more to come...? + +The current goal is not to provide a full featured Bitwarden CLI, but to provide specific features that are not available in other tools. +If you want an officially supported way to manage your Bitwarden vault, you should use the Bitwarden CLI (or a regular client). +If you are looking to manage secrets for machine to machine communication, you should use bitwarden secret manager or something like +hashicorp vault. + + +Parts of the code still need major refactor, and the code needs more testing. Expect some features to break. +Setup is a bit inloved atm. + +### Requirements +Right now, Goldwarden is only tested on Linux. It should be possible to port to mac / bsd, I'm open to PRs. +On Linux, you need at least a working Polkit installation, and a pinentry agent are required. + +### Installation + +To build, you will need libfido2-dev. And a go toolchain. + +Additionally, if you want the autofill feature you will need some dependencies. Everything from https://gioui.org/doc/install linux and wl-clipboard (or xclipboard) should be installed. + +Run: +``` +go install github.com/quexten/goldwarden@latest +go install -tags autofill github.com/quexten/goldwarden@latest +``` + +or: +``` +go build +go build -tags autofill +``` + +Make sure you have the binary in your path. +Next, you have to set up the polkit policy. Copy com.quexten.goldwarden.policy to /usr/share/polkit-1/actions/. +Consider having your shell source the goldwarden.env file, and edit it to your needs. + +Finally, make the daemon auto start: +``` + ~/.config/systemd/user/goldwarden.service + + [Unit] +Description="Goldwarden daemon" + +[Service] +ExecStart=/home/quexten/go/bin/goldwarden daemonize +``` + +and enable it: +``` +systemctl --user enable goldwarden +systemctl --user start goldwarden +``` + +### Design +The tool is split into CLI and daemon, which communicate via a unix socket. + +The vault is never written to disk and is only kept in encrypted form in memory, it is re-downloaded upon startup. The encryption keys are stored in secure enclaves (using the memguard library) and only decrypted briefly when needed. This protects from memory dumps. Vault entries are also only decrypted when needed. + +When entries change, the daemon gets notified via websockets and updates automatically. + +The sensitive parts of the config file are encrypted using a pin. The key is derrived using argon2, and the encryption used is chacha20poly1305. The config is also only held in memory in encrypted form and decrypted using key stored in kernel secured memory when needed. + +When accessing a vault entry, the daemon will authenticate against a polkit policy. This allows using biometrics. + +### Usage + +Start the daemon: +``` +goldwarden daemon +``` + +Set a pin +``` +goldwarden set pin +``` + +Login +``` +goldwarden login --email +``` + +Create an ssh key +``` +goldwarden ssh add --name +``` + +Run a command with injected environment variables +``` +goldwarden run -- +``` + +Autofill +``` +goldwarden autofill --layout +``` +(Create a hotkey for this depending on your desktop environment) + +#### SSH Agent +The SSH agent listens on a socket on `~/.goldwarden-ssh-agent.sock`. This can be used f.e by doing: +``` +SSH_AUTH_SOCK=~/.goldwarden-ssh-agent.sock ssh-add -l +``` + +Beware that some applications do ssh requests in the background, so you might need to set the env variable in your shell config. + +To add a key to your vault, run: +``` +goldwardens ssh add --name "my key" +``` + +Alternatively, use one of the gui clients. Create an ed25519 key: +``` +ssh-keygen -t ed25519 -f ./id_ed25519 +``` +Then create a secure note in bitwarden: +``` +custom-type: ssh-key +private-key: (hidden field) +public-key: +``` + +Then add the private key to bitwarden. The public key can be added to your github account f.e. + +##### Git Signing +To use the SSH agent for git signing, you need to add the following to your git config: +``` +[user] + email = + name = + signingKey = +[commit] + gpgsign = true +[gpg] + format = ssh +``` + +### Environment Variables +Goldwarden can inject environment variables into the environment of a cli command. + +First, create a secure note in bitwarden, and add the following custom fields (using restic as an example): +``` +custom-type: env +executable: name_of_executable +# env variables +AWS_ACCESS_KEY_ID: +AWS_SECRET_ACCESS_KEY: (hidden) +RESTIC_PASSWORD: (hidden) +# optional +RESTIC_REPOSITORY: +... +``` + +Then, run the command: +``` +goldwarden run -- +``` +I.e +``` +goldwarden run -- restic backup +``` + +You can also alias the commands, such that every time you run them, the environment variables are injected: +``` +alias restic="goldwarden run -- restic" +``` + +And then just run the command as usual: +``` +restic backup +``` + +### Autofill +The autofill feature is a bit experimental. It copies the credentials to the clipboard, then creates paste events +using uinput. + +### 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. + +### Future Plans +Some things that I consider adding (depending on time and personal need): + +- Support browser biometrics (similar to my bw-bio-handler tool) +- Paswordless sign in +- Regular cli managment (add, delete, update, of logins / secure notes) +- Scripts to properly set up the policies + +If you have other interesting ideas, feel free to open an issue. I can't +promise that I will implement it, but I'm open to suggestions. + +### Unsuported +Some things that are unsupported and not likely to develop myself: +- MacOS / BSD support (should not be too much work, most things should work out of the box, some adjustments for pinentry and polkit would be needed) +- Windows support (probably a lot of work, unix sockets don't really exist, and pinentry / polkit would have to be implemented otherwise. There might be go libraries for that, but I don't know) +- Send support +- Attachments +- Credit cards / Identities \ No newline at end of file diff --git a/agent/actions/actions.go b/agent/actions/actions.go new file mode 100644 index 0000000..d3934b5 --- /dev/null +++ b/agent/actions/actions.go @@ -0,0 +1,77 @@ +package actions + +import ( + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/sockets" + "github.com/quexten/goldwarden/agent/systemauth" + "github.com/quexten/goldwarden/agent/vault" + "github.com/quexten/goldwarden/ipc" +) + +var AgentActionsRegistry = newActionsRegistry() + +type Action func(ipc.IPCMessage, *config.Config, *vault.Vault, sockets.CallingContext) (interface{}, error) +type ActionsRegistry struct { + actions map[ipc.IPCMessageType]Action +} + +func newActionsRegistry() *ActionsRegistry { + return &ActionsRegistry{ + actions: make(map[ipc.IPCMessageType]Action), + } +} + +func (registry *ActionsRegistry) Register(messageType ipc.IPCMessageType, action Action) { + registry.actions[messageType] = action +} + +func (registry *ActionsRegistry) Get(messageType ipc.IPCMessageType) (Action, bool) { + action, ok := registry.actions[messageType] + return action, ok +} + +func ensureIsLoggedIn(action Action) Action { + return func(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx sockets.CallingContext) (interface{}, error) { + if hash, err := cfg.GetMasterPasswordHash(); err != nil || len(hash) == 0 { + return ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: "Not logged in", + }) + } + + return action(request, cfg, vault, ctx) + } +} + +func ensureIsNotLocked(action Action) Action { + return func(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx sockets.CallingContext) (interface{}, error) { + if cfg.IsLocked() { + err := cfg.TryUnlock(vault) + if err != nil { + return ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: err.Error(), + }) + } + } + + return action(request, cfg, vault, ctx) + } +} + +func ensureBiometricsAuthorized(approvalType systemauth.Approval, action Action) Action { + return func(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx sockets.CallingContext) (interface{}, error) { + if !systemauth.CheckBiometrics(approvalType) { + return ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: "Polkit authorization failed required", + }) + } + + return action(request, cfg, vault, ctx) + } +} + +func ensureEverything(approvalType systemauth.Approval, action Action) Action { + return ensureIsNotLocked(ensureIsLoggedIn(ensureBiometricsAuthorized(approvalType, action))) +} diff --git a/agent/actions/config.go b/agent/actions/config.go new file mode 100644 index 0000000..eb9485e --- /dev/null +++ b/agent/actions/config.go @@ -0,0 +1,45 @@ +package actions + +import ( + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/sockets" + "github.com/quexten/goldwarden/agent/vault" + "github.com/quexten/goldwarden/ipc" +) + +func handleSetApiURL(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx sockets.CallingContext) (response interface{}, err error) { + apiURL := request.ParsedPayload().(ipc.SetApiURLRequest).Value + cfg.ConfigFile.ApiUrl = apiURL + err = cfg.WriteConfig() + if err != nil { + return ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: err.Error(), + }) + } + + return ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: true, + }) +} + +func handleSetIdentity(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx sockets.CallingContext) (response interface{}, err error) { + identity := request.ParsedPayload().(ipc.SetIdentityURLRequest).Value + cfg.ConfigFile.IdentityUrl = identity + err = cfg.WriteConfig() + if err != nil { + return ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: err.Error(), + }) + } + + return ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: true, + }) +} + +func init() { + AgentActionsRegistry.Register(ipc.IPCMessageTypeSetIdentityURLRequest, handleSetIdentity) + AgentActionsRegistry.Register(ipc.IPCMessageTypeSetAPIUrlRequest, handleSetApiURL) +} diff --git a/agent/actions/getclicredentials.go b/agent/actions/getclicredentials.go new file mode 100644 index 0000000..379f19c --- /dev/null +++ b/agent/actions/getclicredentials.go @@ -0,0 +1,48 @@ +package actions + +import ( + "fmt" + + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/sockets" + "github.com/quexten/goldwarden/agent/systemauth" + "github.com/quexten/goldwarden/agent/vault" + "github.com/quexten/goldwarden/ipc" +) + +func handleGetCliCredentials(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx sockets.CallingContext) (response interface{}, err error) { + req := request.ParsedPayload().(ipc.GetCLICredentialsRequest) + + if approved, err := systemauth.GetApproval("Approve Credential Access", fmt.Sprintf("%s on %s>%s>%s is trying to access credentials for %s", ctx.UserName, ctx.GrandParentProcessName, ctx.ParentProcessName, ctx.ProcessName, req.ApplicationName)); err != nil || !approved { + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: "not approved", + }) + if err != nil { + return nil, err + } + return response, nil + } + + env, found := vault.GetEnvCredentialForExecutable(req.ApplicationName) + if !found { + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: "no credentials found for " + req.ApplicationName, + }) + if err != nil { + return nil, err + } + return response, nil + } + + response, err = ipc.IPCMessageFromPayload(ipc.GetCLICredentialsResponse{ + Env: env, + }) + + return +} + +func init() { + AgentActionsRegistry.Register(ipc.IPCMessageTypeGetCLICredentialsRequest, ensureEverything(systemauth.AccessCredential, handleGetCliCredentials)) +} diff --git a/agent/actions/login.go b/agent/actions/login.go new file mode 100644 index 0000000..5c9a4ed --- /dev/null +++ b/agent/actions/login.go @@ -0,0 +1,100 @@ +package actions + +import ( + "context" + "fmt" + + "github.com/quexten/goldwarden/agent/bitwarden" + "github.com/quexten/goldwarden/agent/bitwarden/crypto" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/sockets" + "github.com/quexten/goldwarden/agent/vault" + "github.com/quexten/goldwarden/ipc" +) + +func handleLogin(msg ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, callingContext sockets.CallingContext) (response interface{}, err error) { + req := msg.ParsedPayload().(ipc.DoLoginRequest) + + ctx := context.Background() + token, masterKey, masterpasswordHash, err := bitwarden.LoginWithMasterpassword(ctx, req.Email, cfg, vault) + if err != nil { + var payload = ipc.ActionResponse{ + Success: false, + Message: fmt.Sprintf("Could not login: %s", err.Error()), + } + response, err = ipc.IPCMessageFromPayload(payload) + if err != nil { + return nil, err + } + return + } + + cfg.SetToken(config.LoginToken{ + AccessToken: token.AccessToken, + ExpiresIn: token.ExpiresIn, + TokenType: token.TokenType, + RefreshToken: token.RefreshToken, + Key: token.Key, + }) + + profile, err := bitwarden.Sync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), cfg) + if err != nil { + var payload = ipc.ActionResponse{ + Success: false, + Message: fmt.Sprintf("Could not sync vault: %s", err.Error()), + } + response, err = ipc.IPCMessageFromPayload(payload) + if err != nil { + return nil, err + } + return + } + + var orgKeys map[string]string = make(map[string]string) + for _, org := range profile.Profile.Organizations { + orgId := org.Id.String() + orgKeys[orgId] = org.Key + } + + err = crypto.InitKeyringFromMasterKey(vault.Keyring, profile.Profile.Key, profile.Profile.PrivateKey, orgKeys, masterKey) + if err != nil { + var payload = ipc.ActionResponse{ + Success: false, + Message: fmt.Sprintf("Could not sync vault: %s", err.Error()), + } + response, err = ipc.IPCMessageFromPayload(payload) + if err != nil { + return nil, err + } + return + } + + cfg.SetUserSymmetricKey(vault.Keyring.AccountKey.Bytes()) + cfg.SetMasterPasswordHash([]byte(masterpasswordHash)) + protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(vault.Keyring.AccountKey.Bytes()) + if err != nil { + var payload = ipc.ActionResponse{ + Success: false, + Message: fmt.Sprintf("Could not sync vault: %s", err.Error()), + } + response, err = ipc.IPCMessageFromPayload(payload) + if err != nil { + return nil, err + } + return + } + err = bitwarden.SyncToVault(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, cfg, &protectedUserSymetricKey) + + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: true, + }) + if err != nil { + panic(err) + } + + return +} + +func init() { + AgentActionsRegistry.Register(ipc.IPCMessageTypeDoLoginRequest, ensureIsNotLocked(handleLogin)) +} diff --git a/agent/actions/logins.go b/agent/actions/logins.go new file mode 100644 index 0000000..4064490 --- /dev/null +++ b/agent/actions/logins.go @@ -0,0 +1,145 @@ +package actions + +import ( + "fmt" + "runtime/debug" + + "github.com/quexten/goldwarden/agent/bitwarden/crypto" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/sockets" + "github.com/quexten/goldwarden/agent/systemauth" + "github.com/quexten/goldwarden/agent/vault" + "github.com/quexten/goldwarden/ipc" +) + +func handleGetLoginCipher(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx sockets.CallingContext) (response interface{}, err error) { + req := request.ParsedPayload().(ipc.GetLoginRequest) + login, err := vault.GetLoginByFilter(req.UUID, req.OrgId, req.Name, req.Username) + if err != nil { + return ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: "login not found", + }) + } + + cipherKey, err := login.GetKeyForCipher(*vault.Keyring) + if err != nil { + return ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: "could not get cipher key", + }) + } + + decryptedLogin := ipc.DecryptedLoginCipher{ + Name: "NO NAME FOUND", + } + decryptedLogin.UUID = login.ID.String() + if login.OrganizationID != nil { + decryptedLogin.OrgaizationID = login.OrganizationID.String() + } + + if !login.Name.IsNull() { + decryptedName, err := crypto.DecryptWith(login.Name, cipherKey) + if err == nil { + decryptedLogin.Name = string(decryptedName) + } + } + + if !login.Login.Username.IsNull() { + decryptedUsername, err := crypto.DecryptWith(login.Login.Username, cipherKey) + if err == nil { + decryptedLogin.Username = string(decryptedUsername) + } + } + + if !login.Login.Password.IsNull() { + decryptedPassword, err := crypto.DecryptWith(login.Login.Password, cipherKey) + if err == nil { + decryptedLogin.Password = string(decryptedPassword) + } + } + + if !(login.Notes == nil) && !login.Notes.IsNull() { + decryptedNotes, err := crypto.DecryptWith(*login.Notes, cipherKey) + if err == nil { + decryptedLogin.Notes = string(decryptedNotes) + } + } + + if approved, err := systemauth.GetApproval("Approve Credential Access", fmt.Sprintf("%s on %s>%s>%s is trying to access credentials for user %s on entry %s", ctx.UserName, ctx.GrandParentProcessName, ctx.ParentProcessName, ctx.ProcessName, decryptedLogin.Username, decryptedLogin.Name)); err != nil || !approved { + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: "not approved", + }) + if err != nil { + return nil, err + } + return response, nil + } + + return ipc.IPCMessageFromPayload(ipc.GetLoginResponse{ + Found: true, + Result: decryptedLogin, + }) +} + +func handleListLoginsRequest(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx sockets.CallingContext) (response interface{}, err error) { + if approved, err := systemauth.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 { + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: "not approved", + }) + if err != nil { + return nil, err + } + return response, nil + } + + logins := vault.GetLogins() + decryptedLoginCiphers := make([]ipc.DecryptedLoginCipher, 0) + for _, login := range logins { + key, err := login.GetKeyForCipher(*vault.Keyring) + if err != nil { + actionsLog.Warn("Could not decrypt login:" + err.Error()) + continue + } + + var decryptedName []byte = []byte{} + var decryptedUsername []byte = []byte{} + + if !login.Name.IsNull() { + decryptedName, err = crypto.DecryptWith(login.Name, key) + if err != nil { + actionsLog.Warn("Could not decrypt login:" + err.Error()) + continue + } + } + + if !login.Login.Username.IsNull() { + decryptedUsername, err = crypto.DecryptWith(login.Login.Username, key) + if err != nil { + actionsLog.Warn("Could not decrypt login:" + err.Error()) + continue + } + } + + decryptedLoginCiphers = append(decryptedLoginCiphers, ipc.DecryptedLoginCipher{ + Name: string(decryptedName), + Username: string(decryptedUsername), + UUID: login.ID.String(), + }) + + // prevent deadlock from enclaves + debug.FreeOSMemory() + } + + return ipc.IPCMessageFromPayload(ipc.GetLoginsResponse{ + Found: len(decryptedLoginCiphers) > 0, + Result: decryptedLoginCiphers, + }) +} + +func init() { + AgentActionsRegistry.Register(ipc.IPCMessageGetLoginRequest, ensureEverything(systemauth.AccessCredential, handleGetLoginCipher)) + AgentActionsRegistry.Register(ipc.IPCMessageListLoginsRequest, ensureEverything(systemauth.AccessCredential, handleListLoginsRequest)) +} diff --git a/agent/actions/ssh.go b/agent/actions/ssh.go new file mode 100644 index 0000000..b4890cb --- /dev/null +++ b/agent/actions/ssh.go @@ -0,0 +1,62 @@ +package actions + +import ( + "context" + "strings" + + "github.com/LlamaNite/llamalog" + "github.com/quexten/goldwarden/agent/bitwarden" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/sockets" + "github.com/quexten/goldwarden/agent/ssh" + "github.com/quexten/goldwarden/agent/systemauth" + "github.com/quexten/goldwarden/agent/vault" + "github.com/quexten/goldwarden/ipc" +) + +var actionsLog = llamalog.NewLogger("Goldwarden", "Actions") + +func handleAddSSH(msg ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, callingContext sockets.CallingContext) (response interface{}, err error) { + req := msg.ParsedPayload().(ipc.CreateSSHKeyRequest) + + cipher, publicKey := ssh.NewSSHKeyCipher(req.Name, vault.Keyring) + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: true, + }) + if err != nil { + panic(err) + } + + token, err := cfg.GetToken() + ctx := context.WithValue(context.TODO(), bitwarden.AuthToken{}, token.AccessToken) + ciph, err := bitwarden.PostCipher(ctx, cipher, cfg) + if err == nil { + vault.AddOrUpdateSecureNote(ciph) + } else { + actionsLog.Warn("Error posting ssh key cipher: " + err.Error()) + } + + response, err = ipc.IPCMessageFromPayload(ipc.CreateSSHKeyResponse{ + Digest: strings.ReplaceAll(publicKey, "\n", "") + " " + req.Name, + }) + + return +} + +func handleListSSH(msg ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, callingContext sockets.CallingContext) (response interface{}, err error) { + keys := vault.GetSSHKeys() + keyStrings := make([]string, 0) + for _, key := range keys { + keyStrings = append(keyStrings, strings.ReplaceAll(key.PublicKey+" "+key.Name, "\n", "")) + } + + response, err = ipc.IPCMessageFromPayload(ipc.GetSSHKeysResponse{ + Keys: keyStrings, + }) + return +} + +func init() { + AgentActionsRegistry.Register(ipc.IPCMessageTypeCreateSSHKeyRequest, ensureEverything(systemauth.SSHKey, handleAddSSH)) + AgentActionsRegistry.Register(ipc.IPCMessageTypeGetSSHKeysRequest, ensureIsNotLocked(ensureIsLoggedIn(handleListSSH))) +} diff --git a/agent/actions/vault.go b/agent/actions/vault.go new file mode 100644 index 0000000..1fd8845 --- /dev/null +++ b/agent/actions/vault.go @@ -0,0 +1,174 @@ +package actions + +import ( + "context" + "fmt" + + "github.com/quexten/goldwarden/agent/bitwarden" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/sockets" + "github.com/quexten/goldwarden/agent/systemauth" + "github.com/quexten/goldwarden/agent/vault" + "github.com/quexten/goldwarden/ipc" +) + +func handleUnlockVault(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, callingContext sockets.CallingContext) (response interface{}, err error) { + if !cfg.HasPin() { + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: "No pin set", + }) + if err != nil { + panic(err) + } + + return + } + + if !cfg.IsLocked() { + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: true, + Message: "Unlocked", + }) + if err != nil { + panic(err) + } + + return + } + + err = cfg.TryUnlock(vault) + if err != nil { + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: "wrong pin", + }) + if err != nil { + panic(err) + } + + return + } + + token, err := cfg.GetToken() + if err == nil { + if token.AccessToken != "" { + ctx := context.Background() + bitwarden.RefreshToken(ctx, cfg) + token, err := cfg.GetToken() + err = bitwarden.SyncToVault(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, cfg, nil) + if err != nil { + fmt.Println(err) + } + } + } + + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: true, + }) + if err != nil { + panic(err) + } + + return +} + +func handleLockVault(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, callingContext sockets.CallingContext) (response interface{}, err error) { + if !cfg.HasPin() { + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: "No pin set", + }) + if err != nil { + panic(err) + } + + return + } + + if cfg.IsLocked() { + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: true, + Message: "Locked", + }) + if err != nil { + panic(err) + } + + return + } + + cfg.Lock() + vault.Clear() + vault.Keyring.Lock() + + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: true, + }) + if err != nil { + panic(err) + } + + return +} + +func handleWipeVault(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, callingContext sockets.CallingContext) (response interface{}, err error) { + cfg.Purge() + cfg.WriteConfig() + vault.Clear() + + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: true, + }) + if err != nil { + panic(err) + } + + return +} + +func handleUpdateVaultPin(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, callingContext sockets.CallingContext) (response interface{}, err error) { + pin, err := systemauth.GetPassword("Pin Change", "Enter your desired pin") + if err != nil { + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: false, + Message: err.Error(), + }) + if err != nil { + return nil, err + } else { + return response, nil + } + } + cfg.UpdatePin(pin, true) + + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: true, + }) + + return +} + +func handlePinStatus(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, callingContext sockets.CallingContext) (response interface{}, err error) { + var pinStatus string + if cfg.HasPin() { + pinStatus = "enabled" + } else { + pinStatus = "disabled" + } + + response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{ + Success: true, + Message: pinStatus, + }) + + return +} + +func init() { + AgentActionsRegistry.Register(ipc.IPCMessageTypeUnlockVaultRequest, handleUnlockVault) + AgentActionsRegistry.Register(ipc.IPCMessageTypeLockVaultRequest, handleLockVault) + AgentActionsRegistry.Register(ipc.IPCMessageTypeWipeVaultRequest, handleWipeVault) + AgentActionsRegistry.Register(ipc.IPCMessageTypeUpdateVaultPINRequest, ensureBiometricsAuthorized(systemauth.ChangePin, handleUpdateVaultPin)) + AgentActionsRegistry.Register(ipc.IPCMessageTypeGetVaultPINStatusRequest, handlePinStatus) +} diff --git a/agent/agent.go b/agent/agent.go new file mode 100644 index 0000000..267be84 --- /dev/null +++ b/agent/agent.go @@ -0,0 +1,211 @@ +package agent + +import ( + "context" + "encoding/json" + "fmt" + "net" + "os" + "time" + + "github.com/LlamaNite/llamalog" + "github.com/quexten/goldwarden/agent/actions" + "github.com/quexten/goldwarden/agent/bitwarden" + "github.com/quexten/goldwarden/agent/bitwarden/crypto" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/sockets" + "github.com/quexten/goldwarden/agent/ssh" + "github.com/quexten/goldwarden/agent/vault" + "github.com/quexten/goldwarden/ipc" + "golang.org/x/sys/unix" +) + +const ( + FullSyncInterval = 60 * time.Minute + TokenRefreshInterval = 30 * time.Minute +) + +var log = llamalog.NewLogger("Goldwarden", "Agent") + +func writeError(c net.Conn, errMsg error) error { + payload := ipc.ActionResponse{ + Success: false, + Message: errMsg.Error(), + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + return err + } + _, err = c.Write(payloadBytes) + if err != nil { + return err + } + return nil +} + +func serveAgentSession(c net.Conn, ctx context.Context, vault *vault.Vault, cfg *config.Config) { + for { + buf := make([]byte, 1024*1024) + nr, err := c.Read(buf) + if err != nil { + return + } + + data := buf[0:nr] + + var msg ipc.IPCMessage + err = json.Unmarshal(data, &msg) + if err != nil { + writeError(c, err) + continue + } + + responseBytes := []byte{} + if action, actionFound := actions.AgentActionsRegistry.Get(msg.Type); actionFound { + callingContext := sockets.GetCallingContext(c) + payload, err := action(msg, cfg, vault, callingContext) + if err != nil { + writeError(c, err) + continue + } + responseBytes, err = json.Marshal(payload) + if err != nil { + writeError(c, err) + continue + } + } else { + payload := ipc.ActionResponse{ + Success: false, + Message: "Action not found", + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + writeError(c, err) + continue + } + responseBytes = payloadBytes + } + + _, err = c.Write(responseBytes) + if err != nil { + log.Error("Failed writing to socket " + err.Error()) + } + } +} + +func disableDumpable() error { + return unix.Prctl(unix.PR_SET_DUMPABLE, 0, 0, 0, 0) +} + +type AgentState struct { + vault *vault.Vault + config *config.ConfigFile +} + +func StartUnixAgent(path string) error { + ctx := context.Background() + + // check if exists + keyring := crypto.NewKeyring(nil) + var vault = vault.NewVault(&keyring) + cfg, err := config.ReadConfig() + if err != nil { + var cfg = config.DefaultConfig() + cfg.WriteConfig() + } + if !cfg.IsLocked() { + log.Warn("Config is not locked. PLEASE SET A PIN!!") + token, err := cfg.GetToken() + if err == nil { + if token.AccessToken != "" { + bitwarden.RefreshToken(ctx, &cfg) + userSymmetricKey, err := cfg.GetUserSymmetricKey() + if err != nil { + fmt.Println(err) + } + protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(userSymmetricKey) + + err = bitwarden.SyncToVault(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, &protectedUserSymetricKey) + if err != nil { + fmt.Println(err) + } + } + } + } + + disableDumpable() + go bitwarden.RunWebsocketDaemon(ctx, vault, &cfg) + + vaultAgent := ssh.NewVaultAgent(vault) + vaultAgent.SetUnlockRequestAction(func() bool { + err := cfg.TryUnlock(vault) + if err == nil { + token, err := cfg.GetToken() + if err == nil { + if token.AccessToken != "" { + bitwarden.RefreshToken(ctx, &cfg) + userSymmetricKey, err := cfg.GetUserSymmetricKey() + if err != nil { + fmt.Println(err) + } + protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(userSymmetricKey) + + err = bitwarden.SyncToVault(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, &protectedUserSymetricKey) + if err != nil { + fmt.Println(err) + } + } + } + return true + } + return false + }) + go vaultAgent.Serve() + + go func() { + for { + time.Sleep(TokenRefreshInterval) + if !cfg.IsLocked() { + bitwarden.RefreshToken(ctx, &cfg) + } + } + }() + + go func() { + for { + time.Sleep(FullSyncInterval) + if !cfg.IsLocked() { + token, err := cfg.GetToken() + if err != nil { + log.Warn("Could not get token: %s", err.Error()) + continue + } + + bitwarden.SyncToVault(context.WithValue(ctx, bitwarden.AuthToken{}, token), vault, &cfg, nil) + } + } + }() + + if _, err := os.Stat(path); err == nil { + if err := os.Remove(path); err != nil { + return err + } + } + + l, err := net.Listen("unix", path) + if err != nil { + println("listen error", err.Error()) + return err + } + log.Info("Agent listening on %s...", path) + + for { + fd, err := l.Accept() + if err != nil { + println("accept error", err.Error()) + return err + } + + go serveAgentSession(fd, ctx, vault, &cfg) + } +} diff --git a/agent/bitwarden/auth.go b/agent/bitwarden/auth.go new file mode 100644 index 0000000..09101e8 --- /dev/null +++ b/agent/bitwarden/auth.go @@ -0,0 +1,159 @@ +package bitwarden + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "net/url" + "runtime" + "strconv" + "strings" + + "github.com/LlamaNite/llamalog" + "github.com/awnumar/memguard" + "github.com/quexten/goldwarden/agent/bitwarden/crypto" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/systemauth" + "github.com/quexten/goldwarden/agent/vault" + "golang.org/x/crypto/pbkdf2" +) + +var authLog = llamalog.NewLogger("Goldwarden", "Auth") + +type preLoginRequest struct { + Email string `json:"email"` +} + +type preLoginResponse struct { + KDF int + KDFIterations int + KDFMemory int + KDFParallelism int +} + +type LoginResponseToken struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + RefreshToken string `json:"refresh_token"` + Key string `json:"key"` +} + +const ( + deviceName = "goldwarden" + loginScope = "api offline_access" + loginApiKeyScope = "api" +) + +func deviceType() string { + switch runtime.GOOS { + case "linux": + return "8" + case "darwin": + return "7" + case "windows": + return "6" + default: + return "14" + } +} + +func LoginWithMasterpassword(ctx context.Context, email string, cfg *config.Config, vault *vault.Vault) (LoginResponseToken, crypto.MasterKey, string, error) { + var preLogin preLoginResponse + if err := authenticatedHTTPPost(ctx, cfg.ConfigFile.ApiUrl+"/accounts/prelogin", &preLogin, preLoginRequest{ + Email: email, + }); err != nil { + return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not pre-login: %v", err) + } + + var values url.Values + var masterKey crypto.MasterKey + var hashedPassword string + + password, err := systemauth.GetPassword("Bitwarden Password", "Enter your Bitwarden password") + if err != nil { + 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)}) + if err != nil { + return LoginResponseToken{}, crypto.MasterKey{}, "", err + } + + hashedPassword = b64enc.EncodeToString(pbkdf2.Key(masterKey.GetBytes(), []byte(password), 1, 32, sha256.New)) + + values = urlValues( + "grant_type", "password", + "username", email, + "password", string(hashedPassword), + "scope", loginScope, + "client_id", "connector", + "deviceType", deviceType(), + "deviceName", deviceName, + "deviceIdentifier", cfg.ConfigFile.DeviceUUID, + ) + + var loginResponseToken LoginResponseToken + err = authenticatedHTTPPost(ctx, cfg.ConfigFile.IdentityUrl+"/connect/token", &loginResponseToken, values) + errsc, ok := err.(*errStatusCode) + if ok && bytes.Contains(errsc.body, []byte("TwoFactor")) { + var twoFactor TwoFactorResponse + if err := json.Unmarshal(errsc.body, &twoFactor); err != nil { + return LoginResponseToken{}, crypto.MasterKey{}, "", err + } + provider, token, err := performSecondFactor(&twoFactor, cfg) + if err != nil { + return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not obtain two-factor auth token: %v", err) + } + values.Set("twoFactorProvider", strconv.Itoa(int(provider))) + values.Set("twoFactorToken", string(token)) + values.Set("twoFactorRemember", "1") + loginResponseToken = LoginResponseToken{} + if err := authenticatedHTTPPost(ctx, cfg.ConfigFile.IdentityUrl+"/connect/token", &loginResponseToken, values); err != nil { + return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not login via two-factor: %v", err) + } + authLog.Info("2FA login successful") + } else if err != nil && strings.Contains(err.Error(), "Captcha required.") { + return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("captcha required, please login via the web interface") + + } else if err != nil { + return LoginResponseToken{}, crypto.MasterKey{}, "", fmt.Errorf("could not login via password: %v", err) + } + + authLog.Info("Logged in") + return loginResponseToken, masterKey, hashedPassword, nil +} + +func RefreshToken(ctx context.Context, cfg *config.Config) bool { + authLog.Info("Refreshing token") + + token, err := cfg.GetToken() + if err != nil { + fmt.Println("Could not get refresh token: ", err) + return false + } + + var loginResponseToken LoginResponseToken + err = authenticatedHTTPPost(ctx, cfg.ConfigFile.IdentityUrl+"/connect/token", &loginResponseToken, urlValues( + "grant_type", "refresh_token", + "refresh_token", token.RefreshToken, + "client_id", "connector", + )) + if err != nil { + fmt.Println("Could not refresh token: ", err) + return false + } + cfg.SetToken(config.LoginToken{ + AccessToken: loginResponseToken.AccessToken, + RefreshToken: loginResponseToken.RefreshToken, + Key: loginResponseToken.Key, + TokenType: loginResponseToken.TokenType, + ExpiresIn: loginResponseToken.ExpiresIn, + }) + + authLog.Info("Token refreshed") + + return true +} diff --git a/agent/bitwarden/ciphers.go b/agent/bitwarden/ciphers.go new file mode 100644 index 0000000..19187da --- /dev/null +++ b/agent/bitwarden/ciphers.go @@ -0,0 +1,32 @@ +package bitwarden + +import ( + "context" + + "github.com/quexten/goldwarden/agent/bitwarden/models" + "github.com/quexten/goldwarden/agent/config" +) + +func PostCipher(ctx context.Context, cipher models.Cipher, cfg *config.Config) (models.Cipher, error) { + var resultingCipher models.Cipher + err := authenticatedHTTPPost(ctx, cfg.ConfigFile.ApiUrl+"/ciphers", &resultingCipher, cipher) + return resultingCipher, err +} + +func GetCipher(ctx context.Context, uuid string, cfg *config.Config) (models.Cipher, error) { + var cipher models.Cipher + err := authenticatedHTTPGet(ctx, cfg.ConfigFile.ApiUrl+"/ciphers/"+uuid, &cipher) + return cipher, err +} + +func DeleteCipher(ctx context.Context, uuid string, cfg *config.Config) error { + var result interface{} + err := authenticatedHTTPDelete(ctx, cfg.ConfigFile.ApiUrl+"/ciphers/"+uuid, &result) + return err +} + +func PutCipher(ctx context.Context, uuid string, cipher models.Cipher, cfg *config.Config) (models.Cipher, error) { + var resultingCipher models.Cipher + err := authenticatedHTTPPut(ctx, cfg.ConfigFile.ApiUrl+"/ciphers/"+uuid, &resultingCipher, cipher) + return resultingCipher, err +} diff --git a/agent/bitwarden/crypto/crypto.go b/agent/bitwarden/crypto/crypto.go new file mode 100644 index 0000000..4b45501 --- /dev/null +++ b/agent/bitwarden/crypto/crypto.go @@ -0,0 +1,124 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + cryptorand "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" + "io" + "math" + + "github.com/awnumar/memguard" +) + +var b64enc = base64.StdEncoding.Strict() + +type SymmetricEncryptionKey struct { + encKey *memguard.Enclave + macKey *memguard.Enclave +} + +type AsymmetricEncryptionKey struct { + encKey *memguard.Enclave +} + +func SymmetricEncryptionKeyFromBytes(key []byte) (SymmetricEncryptionKey, error) { + if len(key) != 64 { + memguard.WipeBytes(key) + return SymmetricEncryptionKey{}, fmt.Errorf("invalid key length: %d", len(key)) + } + return SymmetricEncryptionKey{memguard.NewEnclave(key[0:32]), memguard.NewEnclave(key[32:64])}, nil +} + +func (key SymmetricEncryptionKey) Bytes() []byte { + k1, err := key.encKey.Open() + if err != nil { + panic(err) + } + k2, err := key.macKey.Open() + if err != nil { + panic(err) + } + keyBytes := make([]byte, 64) + copy(keyBytes[0:32], k1.Bytes()) + copy(keyBytes[32:64], k2.Bytes()) + return keyBytes +} + +func AssymmetricEncryptionKeyFromBytes(key []byte) (AsymmetricEncryptionKey, error) { + k := memguard.NewEnclave(key) + return AsymmetricEncryptionKey{k}, nil +} + +func isMacValid(message, messageMAC, key []byte) bool { + mac := hmac.New(sha256.New, key) + mac.Write(message) + expectedMAC := mac.Sum(nil) + return hmac.Equal(messageMAC, expectedMAC) +} + +func encryptAESCBC256(data, key []byte) (iv, ciphertext []byte, _ error) { + data = padPKCS7(data, aes.BlockSize) + block, err := aes.NewCipher(key) + if err != nil { + return nil, nil, err + } + ivSize := aes.BlockSize + iv = make([]byte, ivSize) + ciphertext = make([]byte, len(data)) + if _, err := io.ReadFull(cryptorand.Reader, iv); err != nil { + return nil, nil, err + } + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(ciphertext, data) + return iv, ciphertext, nil +} + +func decryptAESCBC256(iv, ciphertext, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + if len(iv) != aes.BlockSize { + return nil, fmt.Errorf("iv length does not match AES block size") + } + if len(ciphertext)%aes.BlockSize != 0 { + return nil, fmt.Errorf("ciphertext is not a multiple of AES block size") + } + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(ciphertext, ciphertext) // decrypt in-place + data, err := unpadPKCS7(ciphertext, aes.BlockSize) + if err != nil { + return nil, err + } + return data, nil +} + +func unpadPKCS7(src []byte, size int) ([]byte, error) { + n := src[len(src)-1] + if len(src)%size != 0 { + return nil, fmt.Errorf("expected PKCS7 padding for block size %d, but have %d bytes", size, len(src)) + } + if len(src) <= int(n) { + return nil, fmt.Errorf("cannot unpad %d bytes out of a total of %d", n, len(src)) + } + src = src[:len(src)-int(n)] + return src, nil +} + +func padPKCS7(src []byte, size int) []byte { + rem := len(src) % size + n := size - rem + if n > math.MaxUint8 { + panic(fmt.Sprintf("cannot pad over %d bytes, but got %d", math.MaxUint8, n)) + } + padded := make([]byte, len(src)+n) + copy(padded, src) + for i := len(src); i < len(padded); i++ { + padded[i] = byte(n) + } + return padded +} diff --git a/agent/bitwarden/crypto/encstring.go b/agent/bitwarden/crypto/encstring.go new file mode 100644 index 0000000..7544136 --- /dev/null +++ b/agent/bitwarden/crypto/encstring.go @@ -0,0 +1,256 @@ +package crypto + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + cryptorand "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "errors" + "fmt" + "io" + "strconv" +) + +type EncString struct { + Type EncStringType + IV, CT, MAC []byte +} + +type EncStringType int + +const ( + AesCbc256_B64 EncStringType = 0 + AesCbc128_HmacSha256_B64 EncStringType = 1 + AesCbc256_HmacSha256_B64 EncStringType = 2 + Rsa2048_OaepSha256_B64 EncStringType = 3 + Rsa2048_OaepSha1_B64 EncStringType = 4 + Rsa2048_OaepSha256_HmacSha256_B64 EncStringType = 5 + Rsa2048_OaepSha1_HmacSha256_B64 EncStringType = 6 +) + +func (t EncStringType) HasMAC() bool { + return t != AesCbc256_B64 +} + +func (s *EncString) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + i := bytes.IndexByte(data, '.') + if i < 0 { + return errors.New("invalid cipher string format") + } + + typStr := string(data[:i]) + var err error + if t, err := strconv.Atoi(typStr); err != nil { + return errors.New("invalid cipher string type") + } else { + s.Type = EncStringType(t) + } + + switch s.Type { + case AesCbc128_HmacSha256_B64, AesCbc256_HmacSha256_B64, AesCbc256_B64: + default: + return errors.New("invalid cipher string type") + } + + data = data[i+1:] + parts := bytes.Split(data, []byte("|")) + if len(parts) != 3 { + return errors.New("invalid cipher string format") + } + + if s.IV, err = b64decode(parts[0]); err != nil { + return err + } + if s.CT, err = b64decode(parts[1]); err != nil { + return err + } + if s.Type.HasMAC() { + if s.MAC, err = b64decode(parts[2]); err != nil { + return err + } + } + return nil +} + +func (s EncString) MarshalText() ([]byte, error) { + if s.Type == 0 { + return nil, nil + } + + var buf bytes.Buffer + buf.WriteString(strconv.Itoa(int(s.Type))) + buf.WriteByte('.') + buf.Write(b64encode(s.IV)) + buf.WriteByte('|') + buf.Write(b64encode(s.CT)) + if s.Type.HasMAC() { + buf.WriteByte('|') + buf.Write(b64encode(s.MAC)) + } + return buf.Bytes(), nil +} + +func (s EncString) IsNull() bool { + return len(s.IV) == 0 && len(s.CT) == 0 && len(s.MAC) == 0 +} + +func b64decode(src []byte) ([]byte, error) { + dst := make([]byte, b64enc.DecodedLen(len(src))) + n, err := b64enc.Decode(dst, src) + if err != nil { + return nil, err + } + dst = dst[:n] + return dst, nil +} + +func b64encode(src []byte) []byte { + dst := make([]byte, b64enc.EncodedLen(len(src))) + b64enc.Encode(dst, src) + return dst +} + +func DecryptWith(s EncString, key SymmetricEncryptionKey) ([]byte, error) { + encKeyData, err := key.encKey.Open() + if err != nil { + return nil, err + } + macKeyData, err := key.macKey.Open() + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(encKeyData.Data()) + if err != nil { + return nil, err + } + + switch s.Type { + case AesCbc256_B64, AesCbc256_HmacSha256_B64: + break + default: + return nil, fmt.Errorf("decrypt: unsupported cipher type %q", s.Type) + } + + if s.Type == AesCbc256_HmacSha256_B64 { + if len(s.MAC) == 0 || len(macKeyData.Data()) == 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()) { + return nil, fmt.Errorf("decrypt: MAC mismatch") + } + } + + mode := cipher.NewCBCDecrypter(block, s.IV) + dst := make([]byte, len(s.CT)) + mode.CryptBlocks(dst, s.CT) + dst, err = unpadPKCS7(dst, aes.BlockSize) + if err != nil { + return nil, err + } + return dst, nil +} + +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 + } + + s := EncString{} + switch typ { + case AesCbc256_B64, AesCbc256_HmacSha256_B64: + default: + return s, fmt.Errorf("encrypt: unsupported cipher type %q", s.Type) + } + s.Type = typ + data = padPKCS7(data, aes.BlockSize) + + block, err := aes.NewCipher(encKeyData.Bytes()) + if err != nil { + return s, err + } + s.IV = make([]byte, aes.BlockSize) + if _, err := io.ReadFull(cryptorand.Reader, s.IV); err != nil { + return s, err + } + s.CT = make([]byte, len(data)) + mode := cipher.NewCBCEncrypter(block, s.IV) + mode.CryptBlocks(s.CT, data) + + if typ == AesCbc256_HmacSha256_B64 { + if len(macKeyData.Bytes()) == 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.Write(macMessage) + s.MAC = mac.Sum(nil) + } + + return s, nil +} + +func DecryptWithAsymmetric(s []byte, asymmetrickey AsymmetricEncryptionKey) ([]byte, error) { + key, err := asymmetrickey.encKey.Open() + if err != nil { + return nil, err + } + + parsedKey, err := x509.ParsePKCS8PrivateKey(key.Bytes()) + if err != nil { + return nil, err + } + + rawKey, err := b64decode(s[2:]) + if err != nil { + return nil, err + } + + res, err := rsa.DecryptOAEP(sha1.New(), rand.Reader, parsedKey.(*rsa.PrivateKey), rawKey, nil) + if err != nil { + return nil, err + } + return res, nil +} + +func EncryptWithAsymmetric(s []byte, asymmbetrickey AsymmetricEncryptionKey) ([]byte, error) { + key, err := asymmbetrickey.encKey.Open() + if err != nil { + return nil, err + } + + parsedKey, err := x509.ParsePKIXPublicKey(key.Bytes()) + if err != nil { + return nil, err + } + + res, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, parsedKey.(*rsa.PublicKey), s, nil) + if err != nil { + return nil, err + } + + resB64 := b64encode(res) + res = append([]byte("4."), resB64...) + + return res, nil +} diff --git a/agent/bitwarden/crypto/kdf.go b/agent/bitwarden/crypto/kdf.go new file mode 100644 index 0000000..83b4c7d --- /dev/null +++ b/agent/bitwarden/crypto/kdf.go @@ -0,0 +1,62 @@ +package crypto + +import ( + "bytes" + "crypto/sha256" + "fmt" + "runtime/debug" + "strings" + + "github.com/awnumar/memguard" + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/pbkdf2" +) + +type KDFType int + +const ( + PBKDF2 KDFType = 0 + Argon2ID KDFType = 1 +) + +type KDFConfig struct { + Type KDFType + Iterations uint32 + Memory uint32 + Parallelism uint32 +} + +type MasterKey struct { + encKey *memguard.Enclave +} + +func (masterKey MasterKey) GetBytes() []byte { + defer debug.FreeOSMemory() + + buffer, err := masterKey.encKey.Open() + if err != nil { + panic(err) + } + defer buffer.Destroy() + + return bytes.Clone(buffer.Bytes()) +} + +func DeriveMasterKey(password memguard.LockedBuffer, 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) + 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) + 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 new file mode 100644 index 0000000..170fbcf --- /dev/null +++ b/agent/bitwarden/crypto/keyhierarchy.go @@ -0,0 +1,84 @@ +package crypto + +import ( + "crypto/sha256" + "fmt" + "io" + + "github.com/awnumar/memguard" + "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 { + masterKey, err := DeriveMasterKey(password, email, kdfConfig) + if err != nil { + return err + } + + return InitKeyringFromMasterKey(keyring, accountKey, accountPrivateKey, orgKeys, masterKey) +} + +func InitKeyringFromMasterKey(keyring *Keyring, accountKey EncString, accountPrivateKey EncString, orgKeys map[string]string, masterKey MasterKey) error { + var accountSymmetricKeyByteArray []byte + + switch accountKey.Type { + case AesCbc256_HmacSha256_B64: + stretchedMasterKey, err := stretchKey(masterKey) + if err != nil { + return err + } + + accountSymmetricKeyByteArray, err = DecryptWith(accountKey, stretchedMasterKey) + if err != nil { + return err + } + default: + return fmt.Errorf("unsupported account key type: %d", accountKey.Type) + } + + accountSymmetricKey, err := SymmetricEncryptionKeyFromBytes(accountSymmetricKeyByteArray) + if err != nil { + return err + } + + keyring.AccountKey = &accountSymmetricKey + + pkcs8PrivateKey, err := DecryptWith(accountPrivateKey, accountSymmetricKey) + if err != nil { + return err + } + keyring.AsymmetricEncyryptionKey = AsymmetricEncryptionKey{memguard.NewEnclave(pkcs8PrivateKey)} + keyring.OrganizationKeys = orgKeys + + return nil +} + +func InitKeyringFromUserSymmetricKey(keyring *Keyring, accountSymmetricKey SymmetricEncryptionKey, accountPrivateKey EncString, orgKeys map[string]string) error { + keyring.AccountKey = &accountSymmetricKey + + pkcs8PrivateKey, err := DecryptWith(accountPrivateKey, accountSymmetricKey) + if err != nil { + return err + } + keyring.AsymmetricEncyryptionKey = AsymmetricEncryptionKey{memguard.NewEnclave(pkcs8PrivateKey)} + keyring.OrganizationKeys = orgKeys + + return nil +} + +func stretchKey(masterKey MasterKey) (SymmetricEncryptionKey, error) { + key := make([]byte, 32) + macKey := make([]byte, 32) + + buffer, err := masterKey.encKey.Open() + if err != nil { + return SymmetricEncryptionKey{}, err + } + + var r io.Reader + r = hkdf.Expand(sha256.New, buffer.Data(), []byte("enc")) + r.Read(key) + r = hkdf.Expand(sha256.New, buffer.Data(), []byte("mac")) + r.Read(macKey) + return SymmetricEncryptionKey{memguard.NewEnclave(key), memguard.NewEnclave(macKey)}, nil +} diff --git a/agent/bitwarden/crypto/keyring.go b/agent/bitwarden/crypto/keyring.go new file mode 100644 index 0000000..6cebd68 --- /dev/null +++ b/agent/bitwarden/crypto/keyring.go @@ -0,0 +1,39 @@ +package crypto + +import ( + "errors" +) + +type Keyring struct { + AccountKey *SymmetricEncryptionKey + AsymmetricEncyryptionKey AsymmetricEncryptionKey + OrganizationKeys map[string]string +} + +func NewKeyring(accountKey *SymmetricEncryptionKey) Keyring { + return Keyring{ + AccountKey: accountKey, + } +} + +func (keyring Keyring) IsLocked() bool { + return keyring.AccountKey == nil +} + +func (keyring *Keyring) Lock() { + keyring.AccountKey = nil + keyring.AsymmetricEncyryptionKey = AsymmetricEncryptionKey{} + keyring.OrganizationKeys = nil +} + +func (keyring Keyring) GetSymmetricKeyForOrganization(uuid string) (SymmetricEncryptionKey, error) { + if key, ok := keyring.OrganizationKeys[uuid]; ok { + decryptedOrgKey, err := DecryptWithAsymmetric([]byte(key), keyring.AsymmetricEncyryptionKey) + if err != nil { + return SymmetricEncryptionKey{}, err + } + + return SymmetricEncryptionKeyFromBytes(decryptedOrgKey) + } + return SymmetricEncryptionKey{}, errors.New("no key found for organization") +} diff --git a/agent/bitwarden/http.go b/agent/bitwarden/http.go new file mode 100644 index 0000000..9d06e79 --- /dev/null +++ b/agent/bitwarden/http.go @@ -0,0 +1,120 @@ +package bitwarden + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +var httpClient = &http.Client{ + Timeout: 20 * time.Second, +} + +type errStatusCode struct { + code int + body []byte +} + +func (e *errStatusCode) Error() string { + return fmt.Sprintf("%s: %s", http.StatusText(e.code), e.body) +} + +type AuthToken struct{} + +func authenticatedHTTPPost(ctx context.Context, urlstr string, recv, send interface{}) error { + var r io.Reader + contentType := "application/json" + authEmail := "" + if values, ok := send.(url.Values); ok { + r = strings.NewReader(values.Encode()) + contentType = "application/x-www-form-urlencoded" + if email := values.Get("username"); email != "" && values.Get("scope") != "" { + authEmail = email + } + } else { + buf := new(bytes.Buffer) + if err := json.NewEncoder(buf).Encode(send); err != nil { + return err + } + r = buf + } + req, err := http.NewRequest("POST", urlstr, r) + if err != nil { + return err + } + req.Header.Set("Content-Type", contentType) + if authEmail != "" { + req.Header.Set("Auth-Email", base64.URLEncoding.EncodeToString([]byte(authEmail))) + } + return makeAuthenticatedHTTPRequest(ctx, req, recv) +} + +func authenticatedHTTPGet(ctx context.Context, urlstr string, recv interface{}) error { + req, err := http.NewRequest("GET", urlstr, nil) + if err != nil { + return err + } + return makeAuthenticatedHTTPRequest(ctx, req, recv) +} + +func authenticatedHTTPDelete(ctx context.Context, urlstr string, recv interface{}) error { + req, err := http.NewRequest("DELETE", urlstr, nil) + if err != nil { + return err + } + return makeAuthenticatedHTTPRequest(ctx, req, recv) +} + +func authenticatedHTTPPut(ctx context.Context, urlstr string, recv, send interface{}) error { + var r io.Reader + contentType := "application/json" + if values, ok := send.(url.Values); ok { + r = strings.NewReader(values.Encode()) + contentType = "application/x-www-form-urlencoded" + } else { + buf := new(bytes.Buffer) + if err := json.NewEncoder(buf).Encode(send); err != nil { + return err + } + r = buf + } + req, err := http.NewRequest("PUT", urlstr, r) + if err != nil { + return err + } + req.Header.Set("Content-Type", contentType) + return makeAuthenticatedHTTPRequest(ctx, req, recv) +} + +func makeAuthenticatedHTTPRequest(ctx context.Context, req *http.Request, recv interface{}) error { + if token, ok := ctx.Value(AuthToken{}).(string); ok { + req.Header.Set("Authorization", "Bearer "+token) + } + + res, err := httpClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + if res.StatusCode != 200 { + return &errStatusCode{res.StatusCode, body} + } + if err := json.Unmarshal(body, recv); err != nil { + fmt.Fprintln(os.Stderr, string(body)) + return err + } + return nil +} diff --git a/agent/bitwarden/models/models.go b/agent/bitwarden/models/models.go new file mode 100644 index 0000000..a066db8 --- /dev/null +++ b/agent/bitwarden/models/models.go @@ -0,0 +1,154 @@ +package models + +import ( + "time" + + "github.com/google/uuid" + "github.com/quexten/goldwarden/agent/bitwarden/crypto" +) + +type SyncData struct { + Profile Profile + Folders []Folder + Ciphers []Cipher +} + +type Organization struct { + Object string + Id uuid.UUID + Name string + UseGroups bool + UseDirectory bool + UseEvents bool + UseTotp bool + Use2fa bool + UseApi bool + UsersGetPremium bool + SelfHost bool + Seats int + MaxCollections int + MaxStorageGb int + Key string + Status int + Type int + Enabled bool +} + +type Profile struct { + ID uuid.UUID + Name string + Email string + EmailVerified bool + Premium bool + MasterPasswordHint string + Culture string + TwoFactorEnabled bool + Key crypto.EncString + PrivateKey crypto.EncString + SecurityStamp string + Organizations []Organization +} + +type Folder struct { + ID uuid.UUID + Name string + RevisionDate time.Time +} + +type Cipher struct { + Type CipherType + ID *uuid.UUID `json:",omitempty"` + Name crypto.EncString + Edit bool + RevisionDate time.Time + DeletedDate time.Time + + FolderID *uuid.UUID `json:",omitempty"` + OrganizationID *uuid.UUID `json:",omitempty"` + Favorite bool `json:",omitempty"` + Attachments interface{} `json:",omitempty"` + OrganizationUseTotp bool `json:",omitempty"` + CollectionIDs []string `json:",omitempty"` + Fields []Field `json:",omitempty"` + + Card *Card `json:",omitempty"` + Identity *Identity `json:",omitempty"` + Login *LoginCipher `json:",omitempty"` + Notes *crypto.EncString `json:",omitempty"` + SecureNote *SecureNoteCipher `json:",omitempty"` +} + +type CipherType int + +const ( + _ CipherType = iota + CipherLogin = 1 + CipherCard = 3 + CipherIdentity = 4 + CipherNote = 2 +) + +type Card struct { + CardholderName crypto.EncString + Brand crypto.EncString + Number crypto.EncString + ExpMonth crypto.EncString + ExpYear crypto.EncString + Code crypto.EncString +} + +type Identity struct { + Title crypto.EncString + FirstName crypto.EncString + MiddleName crypto.EncString + LastName crypto.EncString + + Username crypto.EncString + Company crypto.EncString + SSN crypto.EncString + PassportNumber crypto.EncString + LicenseNumber crypto.EncString + + Email crypto.EncString + Phone crypto.EncString + Address1 crypto.EncString + Address2 crypto.EncString + Address3 crypto.EncString + City crypto.EncString + State crypto.EncString + PostalCode crypto.EncString + Country crypto.EncString +} + +type FieldType int +type Field struct { + Type FieldType + Name crypto.EncString + Value crypto.EncString +} + +type LoginCipher struct { + Password crypto.EncString + URI crypto.EncString + URIs []URI + Username crypto.EncString `json:",omitempty"` + Totp string `json:",omitempty"` +} + +type URIMatch int +type URI struct { + URI string + Match URIMatch +} + +type SecureNoteType int +type SecureNoteCipher struct { + Type SecureNoteType +} + +func (cipher Cipher) GetKeyForCipher(keyring crypto.Keyring) (crypto.SymmetricEncryptionKey, error) { + if cipher.OrganizationID != nil { + return keyring.GetSymmetricKeyForOrganization(cipher.OrganizationID.String()) + } + return *keyring.AccountKey, nil +} diff --git a/agent/bitwarden/paswordless.go b/agent/bitwarden/paswordless.go new file mode 100644 index 0000000..d59f3ff --- /dev/null +++ b/agent/bitwarden/paswordless.go @@ -0,0 +1,82 @@ +package bitwarden + +import ( + "context" + "encoding/base64" + "time" + + "github.com/quexten/goldwarden/agent/bitwarden/crypto" + "github.com/quexten/goldwarden/agent/config" +) + +type AuthRequestData struct { + CreationDate time.Time `json:"creationDate"` + ID string `json:"id"` + Key string `json:"key"` + MasterPasswordHash string `json:"masterPasswordHash"` + Object string `json:"object"` + Origin string `json:"origin"` + PublicKey string `json:"publicKey"` + RequestApproved bool `json:"requestApproved"` + RequestDeviceType string `json:"requestDeviceType"` + RequestIpAddress string `json:"requestIpAddress"` + ResponseDate time.Time `json:"responseDate"` +} + +type AuthRequestResponseData struct { + DeviceIdentifier string `json:"deviceIdentifier"` + Key string `json:"key"` + MasterPasswordHash string `json:"masterPasswordHash"` + Requestapproved bool `json:"requestApproved"` +} + +func GetAuthRequest(ctx context.Context, requestUUID string, config *config.Config) (AuthRequestData, error) { + var authRequest AuthRequestData + err := authenticatedHTTPGet(ctx, config.ConfigFile.ApiUrl+"/auth-requests/"+requestUUID, &authRequest) + return authRequest, err +} + +func GetAuthRequests(ctx context.Context, config *config.Config) ([]AuthRequestData, error) { + var authRequests []AuthRequestData + err := authenticatedHTTPGet(ctx, config.ConfigFile.ApiUrl+"/auth-requests", &authRequests) + return authRequests, err +} + +func PutAuthRequest(ctx context.Context, requestUUID string, authRequest AuthRequestData, config *config.Config) error { + var response interface{} + err := authenticatedHTTPPut(ctx, config.ConfigFile.ApiUrl+"/auth-requests/"+requestUUID, &response, authRequest) + return err +} + +func CreateAuthResponse(ctx context.Context, authRequest AuthRequestData, keyring *crypto.Keyring, config *config.Config) (AuthRequestResponseData, error) { + var authRequestResponse AuthRequestResponseData + + userSymmetricKey, err := config.GetUserSymmetricKey() + if err != nil { + return authRequestResponse, err + } + masterPasswordHash, err := config.GetMasterPasswordHash() + if err != nil { + return authRequestResponse, err + } + + publicKey, err := base64.StdEncoding.DecodeString(authRequest.PublicKey) + requesterKey, err := crypto.AssymmetricEncryptionKeyFromBytes(publicKey) + + encryptedUserSymmetricKey, err := crypto.EncryptWithAsymmetric(userSymmetricKey, requesterKey) + if err != nil { + panic(err) + } + encryptedMasterPasswordHash, err := crypto.EncryptWithAsymmetric(masterPasswordHash, requesterKey) + if err != nil { + panic(err) + } + + err = authenticatedHTTPPut(ctx, config.ConfigFile.ApiUrl+"/auth-requests/"+authRequest.ID, &authRequestResponse, AuthRequestResponseData{ + DeviceIdentifier: config.ConfigFile.DeviceUUID, + Key: string(encryptedUserSymmetricKey), + MasterPasswordHash: string(encryptedMasterPasswordHash), + Requestapproved: true, + }) + return authRequestResponse, err +} diff --git a/agent/bitwarden/sync.go b/agent/bitwarden/sync.go new file mode 100644 index 0000000..70743b4 --- /dev/null +++ b/agent/bitwarden/sync.go @@ -0,0 +1,54 @@ +package bitwarden + +import ( + "context" + "fmt" + + "github.com/LlamaNite/llamalog" + "github.com/quexten/goldwarden/agent/bitwarden/crypto" + "github.com/quexten/goldwarden/agent/bitwarden/models" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/vault" +) + +var log = llamalog.NewLogger("Goldwarden", "Bitwarden API") + +func Sync(ctx context.Context, config *config.Config) (models.SyncData, error) { + var sync models.SyncData + if err := authenticatedHTTPGet(ctx, config.ConfigFile.ApiUrl+"/sync", &sync); err != nil { + return models.SyncData{}, fmt.Errorf("could not sync: %v", err) + } + return sync, nil +} + +func SyncToVault(ctx context.Context, vault *vault.Vault, config *config.Config, masterKey *crypto.SymmetricEncryptionKey) error { + log.Info("Performing full sync...") + + sync, err := Sync(ctx, config) + if err != nil { + return err + } + + if masterKey != nil { + var orgKeys map[string]string = make(map[string]string) + for _, org := range sync.Profile.Organizations { + orgId := org.Id.String() + orgKeys[orgId] = org.Key + } + crypto.InitKeyringFromUserSymmetricKey(vault.Keyring, *masterKey, sync.Profile.PrivateKey, orgKeys) + } + + vault.Clear() + for _, cipher := range sync.Ciphers { + switch cipher.Type { + case models.CipherLogin: + vault.AddOrUpdateLogin(cipher) + break + case models.CipherNote: + vault.AddOrUpdateSecureNote(cipher) + break + } + } + + return nil +} diff --git a/agent/bitwarden/twofactor.go b/agent/bitwarden/twofactor.go new file mode 100644 index 0000000..3a33a2d --- /dev/null +++ b/agent/bitwarden/twofactor.go @@ -0,0 +1,171 @@ +package bitwarden + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/url" + "strconv" + + "github.com/keys-pub/go-libfido2" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/systemauth" +) + +type Fido2Response struct { + Id string `json:"id"` + RawId string `json:"rawId"` + Type_ string `json:"type"` + Extensions struct { + Appid bool `json:"appid"` + } `json:"extensions"` + Response struct { + AuthenticatorData string `json:"authenticatorData"` + ClientDataJSON string `json:"clientDataJson"` + Signature string `json:"signature"` + } `json:"response"` +} + +func Fido2TwoFactor(challengeB64 string, credentials []string, config *config.Config) (string, error) { + url, err := url.Parse(config.ConfigFile.ApiUrl) + rpid := url.Host + + locs, err := libfido2.DeviceLocations() + if err != nil { + return "", err + } + if len(locs) == 0 { + return "", errors.New("no devices found") + } + + path := locs[0].Path + device, err := libfido2.NewDevice(path) + if err != nil { + return "", err + } + + creds := make([][]byte, len(credentials)) + for i, cred := range credentials { + decodedPublicKey, err := base64.RawURLEncoding.DecodeString(cred) + if err != nil { + websocketLog.Fatal(err.Error()) + } + creds[i] = decodedPublicKey + } + + clientDataJson := "{\"type\":\"webauthn.get\",\"challenge\":\"" + challengeB64 + "\",\"origin\":\"https://" + rpid + "\",\"crossOrigin\":false}" + clientDataHash := sha256.Sum256([]byte(clientDataJson)) + clientDataJson = base64.URLEncoding.EncodeToString([]byte(clientDataJson)) + + pin, err := systemauth.GetPassword("Fido2 PIN", "Enter your token's PIN") + if err != nil { + websocketLog.Fatal(err.Error()) + } + + assertion, err := device.Assertion( + rpid, + clientDataHash[:], + creds, + pin, + &libfido2.AssertionOpts{ + Extensions: []libfido2.Extension{}, + UV: libfido2.False, + }, + ) + + authDataRaw := assertion.AuthDataCBOR[2:] // first 2 bytes seem to be from cbor, don't have a proper decoder ATM but this works + authData := base64.URLEncoding.EncodeToString(authDataRaw) + sig := base64.URLEncoding.EncodeToString(assertion.Sig) + credential := base64.URLEncoding.EncodeToString(assertion.CredentialID) + + resp := Fido2Response{ + Id: credential, + RawId: credential, + Type_: "public-key", + Extensions: struct { + Appid bool `json:"appid"` + }{Appid: false}, + Response: struct { + AuthenticatorData string `json:"authenticatorData"` + ClientDataJSON string `json:"clientDataJson"` + Signature string `json:"signature"` + }{ + AuthenticatorData: authData, + ClientDataJSON: clientDataJson, + Signature: sig, + }, + } + + respjson, err := json.Marshal(resp) + return string(respjson), nil +} + +func performSecondFactor(resp *TwoFactorResponse, cfg *config.Config) (TwoFactorProvider, []byte, error) { + if resp.TwoFactorProviders2[WebAuthn] != nil { + chall := resp.TwoFactorProviders2[WebAuthn]["challenge"].(string) + + var creds []string + for _, credential := range resp.TwoFactorProviders2[WebAuthn]["allowCredentials"].([]interface{}) { + publicKey := credential.(map[string]interface{})["id"].(string) + creds = append(creds, publicKey) + } + + result, err := Fido2TwoFactor(chall, creds, cfg) + if err != nil { + return WebAuthn, nil, err + } + return WebAuthn, []byte(result), err + } + if resp.TwoFactorProviders2[Authenticator] != nil { + token, err := systemauth.GetPassword("Authenticator Second Factor", "Enter your two-factor auth code") + return Authenticator, []byte(token), err + } + if resp.TwoFactorProviders2[Email] != nil { + token, err := systemauth.GetPassword("Email Second Factor", "Enter your two-factor auth code") + return Email, []byte(token), err + } + + return Authenticator, []byte{}, errors.New("no second factor available") +} + +type TwoFactorProvider int + +const ( + Authenticator TwoFactorProvider = 0 + Email TwoFactorProvider = 1 + Duo TwoFactorProvider = 2 //Not supported + YubiKey TwoFactorProvider = 3 //Not supported + U2f TwoFactorProvider = 4 //Not supported + Remember TwoFactorProvider = 5 //Not supported + OrganizationDuo TwoFactorProvider = 6 //Not supported + WebAuthn TwoFactorProvider = 7 + _TwoFactorProviderMax = 8 //Not supported +) + +func (t *TwoFactorProvider) UnmarshalText(text []byte) error { + i, err := strconv.Atoi(string(text)) + if err != nil || i < 0 || i >= _TwoFactorProviderMax { + return fmt.Errorf("invalid two-factor auth provider: %q", text) + } + *t = TwoFactorProvider(i) + return nil +} + +type TwoFactorResponse struct { + TwoFactorProviders2 map[TwoFactorProvider]map[string]interface{} +} + +func urlValues(pairs ...string) url.Values { + if len(pairs)%2 != 0 { + panic("pairs must be of even length") + } + vals := make(url.Values) + for i := 0; i < len(pairs); i += 2 { + vals.Set(pairs[i], pairs[i+1]) + } + return vals +} + +var b64enc = base64.StdEncoding.Strict() diff --git a/agent/bitwarden/websocket.go b/agent/bitwarden/websocket.go new file mode 100644 index 0000000..62e0d00 --- /dev/null +++ b/agent/bitwarden/websocket.go @@ -0,0 +1,263 @@ +package bitwarden + +import ( + "bytes" + "context" + "net/url" + "os" + "os/signal" + "time" + + "github.com/LlamaNite/llamalog" + "github.com/awnumar/memguard" + "github.com/gorilla/websocket" + "github.com/quexten/goldwarden/agent/bitwarden/models" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/systemauth" + "github.com/quexten/goldwarden/agent/vault" + "github.com/vmihailenco/msgpack/v5" +) + +var websocketLog = llamalog.NewLogger("Goldwarden", "Websocket") + +type NotificationMessageType int64 + +const ( + SyncCipherUpdate NotificationMessageType = 0 + SyncCipherCreate NotificationMessageType = 1 + SyncLoginDelete NotificationMessageType = 2 + SyncFolderDelete NotificationMessageType = 3 + SyncCiphers NotificationMessageType = 4 + + SyncVault NotificationMessageType = 5 + SyncOrgKeys NotificationMessageType = 6 + SyncFolderCreate NotificationMessageType = 7 + SyncFolderUpdate NotificationMessageType = 8 + SyncCipherDelete NotificationMessageType = 9 + SyncSettings NotificationMessageType = 10 + + LogOut NotificationMessageType = 11 + + SyncSendCreate NotificationMessageType = 12 + SyncSendUpdate NotificationMessageType = 13 + SyncSendDelete NotificationMessageType = 14 + + AuthRequest NotificationMessageType = 15 + AuthRequestResponse NotificationMessageType = 16 +) + +const ( + WEBSOCKET_SLEEP_DURATION_SECONDS = 5 +) + +func RunWebsocketDaemon(ctx context.Context, vault *vault.Vault, cfg *config.Config) { + for { + time.Sleep(WEBSOCKET_SLEEP_DURATION_SECONDS * time.Second) + + if cfg.IsLocked() { + continue + } + + if token, err := cfg.GetToken(); err == nil && token.AccessToken != "" { + err := connectToWebsocket(ctx, vault, cfg) + if err != nil { + websocketLog.Error("Websocket error %s", err) + } + } + } +} + +func connectToWebsocket(ctx context.Context, vault *vault.Vault, cfg *config.Config) error { + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + url, err := url.Parse(cfg.ConfigFile.ApiUrl) + if err != nil { + return err + } + + token, err := cfg.GetToken() + var websocketURL = "wss://" + url.Host + "/notifications/hub?access_token=" + token.AccessToken + c, _, err := websocket.DefaultDialer.Dial(websocketURL, nil) + if err != nil { + return err + } + defer c.Close() + + websocketLog.Info("Connected to websocket server...") + + done := make(chan struct{}) + + go func() { + defer close(done) + for { + _, message, err := c.ReadMessage() + if err != nil { + websocketLog.Error("Error reading websocket message %s", err) + return + } + + if messageType, cipherid, success := websocketMessageType(message); success { + var mt1 = NotificationMessageType(messageType) + switch mt1 { + case SyncCiphers, SyncVault: + websocketLog.Warn("SyncCiphers requested") + token, err := cfg.GetToken() + if err != nil { + websocketLog.Error("Error getting token %s", err) + break + } + SyncToVault(context.WithValue(ctx, AuthToken{}, token.AccessToken), vault, cfg, nil) + break + case SyncCipherDelete: + websocketLog.Warn("Delete requested for cipher " + cipherid) + vault.DeleteCipher(cipherid) + break + case SyncCipherUpdate: + websocketLog.Warn("Update requested for cipher " + cipherid) + token, err := cfg.GetToken() + if err != nil { + websocketLog.Error("Error getting token %s", err) + break + } + + cipher, err := GetCipher(context.WithValue(ctx, AuthToken{}, token.AccessToken), cipherid, cfg) + if err != nil { + websocketLog.Error("Error getting cipher %s", err) + break + } + if !cipher.DeletedDate.IsZero() { + websocketLog.Info("Cipher moved to trash " + cipherid) + vault.DeleteCipher(cipherid) + break + } + + if cipher.Type == models.CipherNote { + vault.AddOrUpdateSecureNote(cipher) + } else { + vault.AddOrUpdateLogin(cipher) + } + break + case SyncCipherCreate: + websocketLog.Warn("Create requested for cipher " + cipherid) + token, err := cfg.GetToken() + if err != nil { + websocketLog.Error("Error getting token %s", err) + break + } + + cipher, err := GetCipher(context.WithValue(ctx, AuthToken{}, token.AccessToken), cipherid, cfg) + if err != nil { + websocketLog.Error("Error getting cipher %s", err) + break + } + + if cipher.Type == models.CipherNote { + vault.AddOrUpdateSecureNote(cipher) + } else { + vault.AddOrUpdateLogin(cipher) + } + + break + case SyncSendCreate, SyncSendUpdate, SyncSendDelete: + websocketLog.Warn("SyncSend requested: sends are not supported") + break + case LogOut: + websocketLog.Info("LogOut received. Wiping vault and exiting...") + memguard.SafeExit(0) + case AuthRequest: + websocketLog.Info("AuthRequest received" + string(cipherid)) + authRequest, err := GetAuthRequest(context.WithValue(ctx, AuthToken{}, token.AccessToken), cipherid, cfg) + if err != nil { + websocketLog.Error("Error getting auth request %s", err) + break + } + websocketLog.Info("AuthRequest details " + authRequest.RequestIpAddress + " " + authRequest.RequestDeviceType) + + if approved, err := systemauth.GetApproval("Paswordless Login Request", "Do you want to allow "+authRequest.RequestIpAddress+" ("+authRequest.RequestDeviceType+") to login to your account?"); err != nil || !approved { + websocketLog.Info("AuthRequest denied") + break + } + if !systemauth.CheckBiometrics(systemauth.AccessCredential) { + websocketLog.Info("AuthRequest denied - biometrics required") + break + } + + _, err = CreateAuthResponse(context.WithValue(ctx, AuthToken{}, token.AccessToken), authRequest, vault.Keyring, cfg) + if err != nil { + websocketLog.Error("Error creating auth response %s", err) + } + break + case AuthRequestResponse: + websocketLog.Info("AuthRequestResponse received") + break + case SyncFolderDelete, SyncFolderCreate, SyncFolderUpdate: + websocketLog.Warn("SyncFolder requested: folders are not supported") + break + case SyncOrgKeys, SyncSettings: + websocketLog.Warn("SyncOrgKeys requested: orgs / settings are not supported") + break + default: + websocketLog.Warn("Unknown message type received %d", mt1) + } + } + } + }() + + <-done + return nil +} + +func websocketMessageType(message []byte) (int8, string, bool) { + lenBufferLen := 0 + for i := 0; i < len(message); i++ { + if (message[i] & 0x80) == 0 { + lenBufferLen = i + 1 + break + } + } + msgPackMessage := message[lenBufferLen:] + return parseMessageTypeFromMessagePack(msgPackMessage) +} + +func parseMessageTypeFromMessagePack(messagePack []byte) (int8, string, bool) { + msgPackBuffer := bytes.NewBuffer(messagePack) + dec := msgpack.NewDecoder(msgPackBuffer) + value, err := dec.DecodeSlice() + if value == nil || err != nil { + return 0, "", false + } + if len(value) < 5 { + websocketLog.Warn("Invalid message received, length too short") + return 0, "", false + } + value, success := value[4].([]interface{}) + if len(value) < 1 || !success { + websocketLog.Warn("Invalid message received, length too short") + return 0, "", false + } + value1, success := value[0].(map[string]interface{}) + if !success { + websocketLog.Warn("Invalid message received, value is not a map") + return 0, "", false + } + if _, ok := value1["Type"]; !ok { + websocketLog.Warn("Invalid message received, no type") + return 0, "", false + } + messagePayloadType, success := value1["Type"].(int8) + if !success { + websocketLog.Warn("Invalid message received, type is not an int") + return 0, "", false + } + payload, success := value1["Payload"].(map[string]interface{}) + if !success { + return messagePayloadType, "", true + } + if _, ok := payload["Id"]; !ok { + websocketLog.Warn("Invalid message received, no id") + return 0, "", false + } + + return messagePayloadType, payload["Id"].(string), true +} diff --git a/agent/config/config.go b/agent/config/config.go new file mode 100644 index 0000000..874a13c --- /dev/null +++ b/agent/config/config.go @@ -0,0 +1,351 @@ +package config + +import ( + cryptoSubtle "crypto/subtle" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "os" + "runtime/debug" + "sync" + + "github.com/awnumar/memguard" + "github.com/google/uuid" + "github.com/quexten/goldwarden/agent/bitwarden/crypto" + "github.com/quexten/goldwarden/agent/systemauth" + "github.com/quexten/goldwarden/agent/vault" + "github.com/tink-crypto/tink-go/v2/aead/subtle" + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/sha3" +) + +const ( + KDFIterations = 2 + KDFMemory = 2 * 1024 * 1024 + KDFThreads = 8 + ConfigPath = "/.config/goldwarden.json" +) + +type ConfigFile struct { + IdentityUrl string + ApiUrl string + DeviceUUID string + ConfigKeyHash string + EncryptedToken string + EncryptedUserSymmetricKey string + EncryptedMasterPasswordHash string +} + +type LoginToken struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + RefreshToken string `json:"refresh_token"` + Key string `json:"key"` +} + +type Config struct { + key *memguard.LockedBuffer + ConfigFile ConfigFile + mu sync.Mutex +} + +func DefaultConfig() Config { + deviceUUID, _ := uuid.NewUUID() + return Config{ + memguard.NewBuffer(32), + ConfigFile{ + IdentityUrl: "https://identity.bitwarden.com/", + ApiUrl: "https://identity.bitwarden.com/", + DeviceUUID: deviceUUID.String(), + ConfigKeyHash: "", + EncryptedToken: "", + EncryptedUserSymmetricKey: "", + EncryptedMasterPasswordHash: "", + }, + sync.Mutex{}, + } +} + +func (c *Config) IsLocked() bool { + return c.key == nil +} + +func (c *Config) Unlock(password string) bool { + c.mu.Lock() + defer c.mu.Unlock() + + if !c.IsLocked() { + return true + } + + key := argon2.Key([]byte(password), []byte(c.ConfigFile.DeviceUUID), KDFIterations, KDFMemory, KDFThreads, 32) + debug.FreeOSMemory() + keyHash := sha3.Sum256(key) + configKeyHash := hex.EncodeToString(keyHash[:]) + if cryptoSubtle.ConstantTimeCompare([]byte(configKeyHash), []byte(c.ConfigFile.ConfigKeyHash)) != 1 { + return false + } + + c.key = memguard.NewBufferFromBytes(key) + return true +} + +func (c *Config) Lock() { + c.mu.Lock() + defer c.mu.Unlock() + + if c.IsLocked() { + return + } + c.key.Destroy() + c.key = nil +} + +func (c *Config) Purge() { + c.mu.Lock() + defer c.mu.Unlock() + + c.ConfigFile.EncryptedMasterPasswordHash = "" + c.ConfigFile.EncryptedToken = "" + c.ConfigFile.EncryptedUserSymmetricKey = "" + c.ConfigFile.ConfigKeyHash = "" + c.key = memguard.NewBuffer(32) +} + +func (c *Config) HasPin() bool { + return c.ConfigFile.ConfigKeyHash != "" +} + +func (c *Config) UpdatePin(password string, write bool) { + c.mu.Lock() + + newKey := argon2.Key([]byte(password), []byte(c.ConfigFile.DeviceUUID), KDFIterations, KDFMemory, KDFThreads, 32) + keyHash := sha3.Sum256(newKey) + configKeyHash := hex.EncodeToString(keyHash[:]) + debug.FreeOSMemory() + + c.ConfigFile.ConfigKeyHash = configKeyHash + + plaintextToken, err1 := c.decryptString(c.ConfigFile.EncryptedToken) + plaintextUserSymmetricKey, err3 := c.decryptString(c.ConfigFile.EncryptedUserSymmetricKey) + plaintextEncryptedMasterPasswordHash, err4 := c.decryptString(c.ConfigFile.EncryptedMasterPasswordHash) + + c.key = memguard.NewBufferFromBytes(newKey) + + if err1 == nil { + c.ConfigFile.EncryptedToken, err1 = c.encryptString(plaintextToken) + } + if err3 == nil { + c.ConfigFile.EncryptedUserSymmetricKey, err3 = c.encryptString(plaintextUserSymmetricKey) + } + if err4 == nil { + c.ConfigFile.EncryptedMasterPasswordHash, err4 = c.encryptString(plaintextEncryptedMasterPasswordHash) + } + + if write { + c.WriteConfig() + } + c.mu.Unlock() +} + +func (c *Config) GetToken() (LoginToken, error) { + if c.IsLocked() { + return LoginToken{}, errors.New("config is locked") + } + tokenJson, err := c.decryptString(c.ConfigFile.EncryptedToken) + if err != nil { + return LoginToken{}, err + } + + var token LoginToken + err = json.Unmarshal([]byte(tokenJson), &token) + if err != nil { + return LoginToken{}, err + } + return token, nil +} + +func (c *Config) SetToken(token LoginToken) error { + if c.IsLocked() { + return errors.New("config is locked") + } + + tokenJson, err := json.Marshal(token) + encryptedToken, err := c.encryptString(string(tokenJson)) + if err != nil { + return err + } + // c.mu.Lock() + c.ConfigFile.EncryptedToken = encryptedToken + // c.mu.Unlock() + c.WriteConfig() + return nil +} + +func (c *Config) GetUserSymmetricKey() ([]byte, error) { + if c.IsLocked() { + return []byte{}, errors.New("config is locked") + } + decrypted, err := c.decryptString(c.ConfigFile.EncryptedUserSymmetricKey) + if err != nil { + return []byte{}, err + } + return []byte(decrypted), nil +} + +func (c *Config) SetUserSymmetricKey(key []byte) error { + if c.IsLocked() { + return errors.New("config is locked") + } + encryptedKey, err := c.encryptString(string(key)) + if err != nil { + return err + } + // c.mu.Lock() + c.ConfigFile.EncryptedUserSymmetricKey = encryptedKey + // c.mu.Unlock() + c.WriteConfig() + return nil +} + +func (c *Config) GetMasterPasswordHash() ([]byte, error) { + if c.IsLocked() { + return []byte{}, errors.New("config is locked") + } + decrypted, err := c.decryptString(c.ConfigFile.EncryptedMasterPasswordHash) + if err != nil { + return []byte{}, err + } + return []byte(decrypted), nil +} + +func (c *Config) SetMasterPasswordHash(hash []byte) error { + + if c.IsLocked() { + return errors.New("config is locked") + } + encryptedHash, err := c.encryptString(string(hash)) + if err != nil { + c.mu.Unlock() + return err + } + + // c.mu.Lock() + c.ConfigFile.EncryptedMasterPasswordHash = encryptedHash + // c.mu.Unlock() + + c.WriteConfig() + return nil +} + +func (c *Config) encryptString(data string) (string, error) { + if c.IsLocked() { + return "", errors.New("config is locked") + } + ca, err := subtle.NewChaCha20Poly1305(c.key.Bytes()) + if err != nil { + return "", err + } + result, err := ca.Encrypt([]byte(data), []byte{}) + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(result), nil +} + +func (c *Config) decryptString(data string) (string, error) { + if c.IsLocked() { + return "", errors.New("config is locked") + } + + decoded, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return "", err + } + + ca, err := subtle.NewChaCha20Poly1305(c.key.Bytes()) + if err != nil { + return "", err + } + result, err := ca.Decrypt(decoded, []byte{}) + if err != nil { + return "", err + } + return string(result), nil +} + +func (config *Config) WriteConfig() error { + config.mu.Lock() + defer config.mu.Unlock() + + jsonBytes, err := json.Marshal(config.ConfigFile) + if err != nil { + return err + } + + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + // write to disk + os.Remove(home + ConfigPath) + file, err := os.OpenFile(home+ConfigPath, os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return err + } + defer file.Close() + + _, err = file.Write(jsonBytes) + if err != nil { + return err + } + return nil +} + +func ReadConfig() (Config, error) { + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + file, err := os.Open(home + ConfigPath) + if err != nil { + return Config{ConfigFile: ConfigFile{}}, err + } + defer file.Close() + + decoder := json.NewDecoder(file) + config := ConfigFile{} + err = decoder.Decode(&config) + if err != nil { + return Config{ConfigFile: ConfigFile{}}, err + } + if config.ConfigKeyHash == "" { + return Config{ConfigFile: config, key: memguard.NewBuffer(32)}, nil + } + return Config{ConfigFile: config}, nil +} + +func (cfg *Config) TryUnlock(vault *vault.Vault) error { + pin, err := systemauth.GetPassword("Unlock Goldwarden", "Enter the vault PIN") + if err != nil { + return err + } + cfg.Unlock(pin) + + userKey, err := cfg.GetUserSymmetricKey() + if err == nil { + key, err := crypto.SymmetricEncryptionKeyFromBytes(userKey) + if err != nil { + return err + } + vault.Keyring.AccountKey = &key + } else { + cfg.Lock() + return err + } + + return nil +} diff --git a/agent/sockets/callingcontext.go b/agent/sockets/callingcontext.go new file mode 100644 index 0000000..3afa345 --- /dev/null +++ b/agent/sockets/callingcontext.go @@ -0,0 +1,51 @@ +package sockets + +import ( + "net" + "os/user" + + gops "github.com/mitchellh/go-ps" + "inet.af/peercred" +) + +type CallingContext struct { + UserName string + ProcessName string + ParentProcessName string + GrandParentProcessName string +} + +func GetCallingContext(connection net.Conn) CallingContext { + creds, err := peercred.Get(connection) + if err != nil { + panic(err) + } + pid, _ := creds.PID() + uid, _ := creds.UserID() + process, err := gops.FindProcess(pid) + ppid := process.PPid() + if err != nil { + panic(err) + } + parentProcess, err := gops.FindProcess(ppid) + if err != nil { + panic(err) + } + + parentParentProcess, err := gops.FindProcess(parentProcess.PPid()) + if err != nil { + panic(err) + } + + username, err := user.LookupId(uid) + if err != nil { + panic(err) + } + + return CallingContext{ + UserName: username.Username, + ProcessName: process.Executable(), + ParentProcessName: parentProcess.Executable(), + GrandParentProcessName: parentParentProcess.Executable(), + } +} diff --git a/agent/ssh/keys.go b/agent/ssh/keys.go new file mode 100644 index 0000000..f1f1c33 --- /dev/null +++ b/agent/ssh/keys.go @@ -0,0 +1,71 @@ +package ssh + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "io" + + "github.com/mikesmitty/edkey" + "github.com/quexten/goldwarden/agent/bitwarden/crypto" + "github.com/quexten/goldwarden/agent/bitwarden/models" + "golang.org/x/crypto/ssh" +) + +func NewSSHKeyCipher(name string, keyring *crypto.Keyring) (models.Cipher, string) { + + var reader io.Reader = rand.Reader + pub, priv, err := ed25519.GenerateKey(reader) + + if err != nil { + panic(err) + } + privateKey, err := x509.MarshalPKCS8PrivateKey(priv) + privBlock := pem.Block{ + Type: "OPENSSH PRIVATE KEY", + Bytes: edkey.MarshalED25519PrivateKey(privateKey), + } + + 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) + + cipher := models.Cipher{ + Type: models.CipherNote, + Name: encryptedName, + Notes: &encryptedPublicKeyValue, + ID: nil, + Favorite: false, + OrganizationID: nil, + SecureNote: &models.SecureNoteCipher{ + Type: 0, + }, + Fields: []models.Field{ + { + Type: 0, + Name: encryptedCustomTypeKey, + Value: encryptedCustomTypeValue, + }, + { + Type: 0, + Name: encryptedPublicKeyKey, + Value: encryptedPublicKeyValue, + }, + { + Type: 1, + Name: encryptedPrivateKeyKey, + Value: encryptedPrivateKeyValue, + }, + }, + } + + return cipher, string(ssh.MarshalAuthorizedKey(publicKey)) +} diff --git a/agent/ssh/ssh.go b/agent/ssh/ssh.go new file mode 100644 index 0000000..91cadcc --- /dev/null +++ b/agent/ssh/ssh.go @@ -0,0 +1,178 @@ +package ssh + +import ( + "bytes" + "crypto/rand" + "errors" + "fmt" + "net" + "os" + + "github.com/LlamaNite/llamalog" + "github.com/quexten/goldwarden/agent/sockets" + "github.com/quexten/goldwarden/agent/systemauth" + "github.com/quexten/goldwarden/agent/vault" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +var log = llamalog.NewLogger("Goldwarden", "SSH") + +type vaultAgent struct { + vault *vault.Vault + unlockRequestAction func() bool + context sockets.CallingContext +} + +func (vaultAgent) Add(key agent.AddedKey) error { + return nil +} + +func (vaultAgent vaultAgent) List() ([]*agent.Key, error) { + if vaultAgent.vault.Keyring.IsLocked() { + if !vaultAgent.unlockRequestAction() { + return nil, errors.New("vault is locked") + } + } + + vaultSSHKeys := (*vaultAgent.vault).GetSSHKeys() + var sshKeys []*agent.Key + for _, vaultSSHKey := range vaultSSHKeys { + signer, err := ssh.ParsePrivateKey([]byte(vaultSSHKey.Key)) + if err != nil { + continue + } + pub := signer.PublicKey() + sshKeys = append(sshKeys, &agent.Key{ + Format: pub.Type(), + Blob: pub.Marshal(), + Comment: vaultSSHKey.Name}) + } + + return sshKeys, nil +} + +func (vaultAgent) Lock(passphrase []byte) error { + return nil +} + +func (vaultAgent) Remove(key ssh.PublicKey) error { + return nil +} + +func (vaultAgent) RemoveAll() error { + return nil +} + +func Eq(a, b ssh.PublicKey) bool { + return 0 == bytes.Compare(a.Marshal(), b.Marshal()) +} + +func (vaultAgent vaultAgent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { + log.Info("Sign Request for key: %s", ssh.FingerprintSHA256(key)) + if vaultAgent.vault.Keyring.IsLocked() { + if !vaultAgent.unlockRequestAction() { + return nil, errors.New("vault is locked") + } + } + + var signer ssh.Signer + var sshKey *vault.SSHKey + + vaultSSHKeys := (*vaultAgent.vault).GetSSHKeys() + for _, vaultSSHKey := range vaultSSHKeys { + sg, err := ssh.ParsePrivateKey([]byte(vaultSSHKey.Key)) + if err != nil { + return nil, err + } + if Eq(sg.PublicKey(), key) { + signer = sg + sshKey = &vaultSSHKey + break + } + } + + message := fmt.Sprintf("%s on %s>%s>%s is requesting signage with key %s", vaultAgent.context.UserName, vaultAgent.context.GrandParentProcessName, vaultAgent.context.ParentProcessName, vaultAgent.context.ProcessName, sshKey.Name) + + if approved, err := systemauth.GetApproval("SSH Key Signing Request", message); err != nil || !approved { + log.Info("Sign Request for key: %s denied", sshKey.Name) + return nil, errors.New("Approval not given") + } + + if !systemauth.CheckBiometrics(systemauth.SSHKey) { + log.Info("Sign Request for key: %s denied", key.Marshal()) + return nil, errors.New("Biometrics not checked") + } + + var rand = rand.Reader + log.Info("Sign Request for key: %s %s accepted", ssh.FingerprintSHA256(key), sshKey.Name) + return signer.Sign(rand, data) +} + +func (vaultAgent) Signers() ([]ssh.Signer, error) { + + return []ssh.Signer{}, nil +} + +func (vaultAgent) Unlock(passphrase []byte) error { + return nil +} + +type SSHAgentServer struct { + vault *vault.Vault + unlockRequestAction func() bool +} + +func (v *SSHAgentServer) SetUnlockRequestAction(action func() bool) { + v.unlockRequestAction = action +} + +func NewVaultAgent(vault *vault.Vault) SSHAgentServer { + return SSHAgentServer{ + vault: vault, + unlockRequestAction: func() bool { + log.Info("Unlock Request, but no action defined") + return false + }, + } +} + +func (v SSHAgentServer) Serve() { + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + + path := home + "/.goldwarden-ssh-agent.sock" + + if _, err := os.Stat(path); err == nil { + if err := os.Remove(path); err != nil { + log.Error("Could not remove old socket file: %s", err) + return + } + } + listener, err := net.Listen("unix", path) + if err != nil { + panic(err) + } + + log.Info("SSH Agent listening on %s", path) + + for { + var conn, err = listener.Accept() + if err != nil { + panic(err) + } + + callingContext := sockets.GetCallingContext(conn) + + log.Info("SSH Agent connection from %s>%s>%s \nby user %s", callingContext.GrandParentProcessName, callingContext.ParentProcessName, callingContext.ProcessName, callingContext.UserName) + log.Info("SSH Agent connection accepted") + + go agent.ServeAgent(vaultAgent{ + vault: v.vault, + unlockRequestAction: v.unlockRequestAction, + context: callingContext, + }, conn) + } +} diff --git a/agent/systemauth/biometrics.go b/agent/systemauth/biometrics.go new file mode 100644 index 0000000..47e2c08 --- /dev/null +++ b/agent/systemauth/biometrics.go @@ -0,0 +1,43 @@ +package systemauth + +import ( + "github.com/LlamaNite/llamalog" + "github.com/amenzhinsky/go-polkit" +) + +var log = llamalog.NewLogger("Goldwarden", "Systemauth") + +type Approval string + +const ( + AccessCredential Approval = "com.quexten.goldwarden.accesscredential" + ChangePin Approval = "com.quexten.goldwarden.changepin" + SSHKey Approval = "com.quexten.goldwarden.usesshkey" +) + +func (a Approval) String() string { + return string(a) +} + +func CheckBiometrics(approvalType Approval) bool { + log.Info("Checking biometrics for %s", approvalType.String()) + + authority, err := polkit.NewAuthority() + if err != nil { + return false + } + + result, err := authority.CheckAuthorization( + approvalType.String(), + nil, + polkit.CheckAuthorizationAllowUserInteraction, "", + ) + + if err != nil { + return false + } + + log.Info("Biometrics result: %t", result.IsAuthorized) + + return result.IsAuthorized +} diff --git a/agent/systemauth/pinentry.go b/agent/systemauth/pinentry.go new file mode 100644 index 0000000..6f8c761 --- /dev/null +++ b/agent/systemauth/pinentry.go @@ -0,0 +1,64 @@ +package systemauth + +import ( + "errors" + + "github.com/twpayne/go-pinentry" +) + +func GetPassword(title string, description string) (string, error) { + client, err := pinentry.NewClient( + pinentry.WithBinaryNameFromGnuPGAgentConf(), + pinentry.WithGPGTTY(), + pinentry.WithTitle(title), + pinentry.WithDesc(description), + pinentry.WithPrompt(title), + ) + log.Info("Asking for pin |%s|%s|", title, description) + + if err != nil { + return "", err + } + defer client.Close() + + switch pin, fromCache, err := client.GetPIN(); { + case pinentry.IsCancelled(err): + log.Info("Cancelled") + return "", errors.New("Cancelled") + case err != nil: + return "", err + case fromCache: + log.Info("Got pin from cache") + return pin, nil + default: + log.Info("Got pin from user") + return pin, nil + } +} + +func GetApproval(title string, description string) (bool, error) { + client, err := pinentry.NewClient( + pinentry.WithBinaryNameFromGnuPGAgentConf(), + pinentry.WithGPGTTY(), + pinentry.WithTitle(title), + pinentry.WithDesc(description), + pinentry.WithPrompt(title), + ) + log.Info("Asking for approval |%s|%s|", title, description) + + if err != nil { + return false, err + } + defer client.Close() + + switch _, err := client.Confirm("Confirm"); { + case pinentry.IsCancelled(err): + log.Info("Cancelled") + return false, errors.New("Cancelled") + case err != nil: + return false, err + default: + log.Info("Got approval from user") + return true, nil + } +} diff --git a/agent/vault/vault.go b/agent/vault/vault.go new file mode 100644 index 0000000..4244cce --- /dev/null +++ b/agent/vault/vault.go @@ -0,0 +1,391 @@ +package vault + +import ( + "errors" + "strings" + "sync" + + "github.com/quexten/goldwarden/agent/bitwarden/crypto" + "github.com/quexten/goldwarden/agent/bitwarden/models" + "github.com/rs/zerolog/log" + "golang.org/x/exp/slices" +) + +type Vault struct { + Keyring *crypto.Keyring + logins map[string]models.Cipher + secureNotes map[string]models.Cipher + sshKeyNoteIDs []string + envCredentials map[string]string + mu sync.Mutex +} + +func NewVault(keyring *crypto.Keyring) *Vault { + return &Vault{ + Keyring: keyring, + logins: make(map[string]models.Cipher), + secureNotes: make(map[string]models.Cipher), + sshKeyNoteIDs: make([]string, 0), + envCredentials: make(map[string]string), + } +} + +func (vault *Vault) lockMutex() { + vault.mu.Lock() +} + +func (vault *Vault) unlockMutex() { + vault.mu.Unlock() +} + +func (vault *Vault) Clear() { + vault.lockMutex() + vault.logins = make(map[string]models.Cipher) + vault.secureNotes = make(map[string]models.Cipher) + vault.sshKeyNoteIDs = make([]string, 0) + vault.envCredentials = make(map[string]string) + vault.unlockMutex() +} + +func (vault *Vault) AddOrUpdateLogin(cipher models.Cipher) { + vault.lockMutex() + vault.logins[cipher.ID.String()] = cipher + vault.unlockMutex() +} + +func (vault *Vault) DeleteCipher(uuid string) { + vault.lockMutex() + delete(vault.logins, uuid) + delete(vault.envCredentials, uuid) + + newSecureNotes := make(map[string]models.Cipher) + for _, noteID := range vault.sshKeyNoteIDs { + if noteID != uuid { + newSecureNotes[noteID] = vault.secureNotes[noteID] + } + } + vault.secureNotes = newSecureNotes + vault.unlockMutex() +} + +func (vault *Vault) AddOrUpdateSecureNote(cipher models.Cipher) { + vault.lockMutex() + vault.secureNotes[cipher.ID.String()] = cipher + + if vault.isSSHKey(cipher) { + if !slices.Contains(vault.sshKeyNoteIDs, cipher.ID.String()) { + vault.sshKeyNoteIDs = append(vault.sshKeyNoteIDs, cipher.ID.String()) + } + } else if executableName, isEnv := vault.isEnv(cipher); isEnv { + vault.envCredentials[executableName] = cipher.ID.String() + } + + vault.unlockMutex() +} + +func (vault *Vault) isEnv(cipher models.Cipher) (string, bool) { + if cipher.Type != models.CipherNote { + return "", false + } + + if !cipher.DeletedDate.IsZero() { + return "", false + } + + key, err := cipher.GetKeyForCipher(*vault.Keyring) + if err != nil { + log.Warn().Err(err).Msg("Failed to get key for cipher " + cipher.ID.String()) + return "", false + } + + isEnv := false + executableName := "" + + for _, field := range cipher.Fields { + fieldName, err := crypto.DecryptWith(field.Name, key) + if err != nil { + continue + } + fieldValue, err := crypto.DecryptWith(field.Value, key) + if err != nil { + continue + } + + if string(fieldName) == "custom-type" && string(fieldValue) == "env" { + isEnv = true + } else if string(fieldName) == "executable" { + executableName = string(fieldValue) + } + } + + return executableName, isEnv +} + +func (vault *Vault) isSSHKey(cipher models.Cipher) bool { + if cipher.Type != models.CipherNote { + return false + } + + if !cipher.DeletedDate.IsZero() { + return false + } + + key, err := cipher.GetKeyForCipher(*vault.Keyring) + if err != nil { + log.Warn().Err(err).Msg("Failed to get key for cipher " + cipher.ID.String()) + return false + } + + for _, field := range cipher.Fields { + 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) + continue + } + fieldValue, err := crypto.DecryptWith(field.Value, key) + if err != nil { + continue + } + + if string(fieldName) == "custom-type" && string(fieldValue) == "ssh-key" { + return true + } + } + + return false +} + +type SSHKey struct { + Name string + Key string + PublicKey string +} + +func (vault *Vault) GetSSHKeys() []SSHKey { + vault.lockMutex() + defer vault.unlockMutex() + + var sshKeys []SSHKey + for _, id := range vault.sshKeyNoteIDs { + privateKey := "" + publicKey := "" + + key, err := vault.secureNotes[id].GetKeyForCipher(*vault.Keyring) + if err != nil { + continue + } + + for _, field := range vault.secureNotes[id].Fields { + fieldName, err := crypto.DecryptWith(field.Name, key) + if err != nil { + continue + } + if string(fieldName) == "private-key" { + pk, err := crypto.DecryptWith(field.Value, key) + if err != nil { + continue + } else { + privateKey = string(pk) + } + } + if string(fieldName) == "public-key" { + pk, err := crypto.DecryptWith(field.Value, key) + if err != nil { + continue + } else { + publicKey = string(pk) + } + } + } + + privateKey = strings.Replace(privateKey, "-----BEGIN OPENSSH PRIVATE KEY-----", "", 1) + privateKey = strings.Replace(privateKey, "-----END OPENSSH PRIVATE KEY-----", "", 1) + + pkParts := strings.Join(strings.Split(privateKey, " "), "\n") + privateKeyString := "-----BEGIN OPENSSH PRIVATE KEY-----" + pkParts + "-----END OPENSSH PRIVATE KEY-----" + + decryptedTitle, err := crypto.DecryptWith(vault.secureNotes[id].Name, key) + if err != nil { + continue + } + + sshKeys = append(sshKeys, SSHKey{ + Name: string(decryptedTitle), + Key: string(privateKeyString), + PublicKey: string(publicKey), + }) + } + return sshKeys +} + +func (vault *Vault) GetEnvCredentialForExecutable(executableName string) (map[string]string, bool) { + vault.lockMutex() + defer vault.unlockMutex() + + env := make(map[string]string) + + 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) + return make(map[string]string), false + } + + for _, field := range vault.secureNotes[id].Fields { + fieldName, err := crypto.DecryptWith(field.Name, key) + if err != nil { + continue + } + fieldValue, err := crypto.DecryptWith(field.Value, key) + if err != nil { + continue + } + + if string(fieldName) == "custom-type" || string(fieldName) == "executable" { + continue + } + + env[string(fieldName)] = string(fieldValue) + } + return env, true + } + return make(map[string]string), false +} + +func (vault *Vault) GetLogins() []models.Cipher { + vault.lockMutex() + defer vault.unlockMutex() + + var logins []models.Cipher + for _, cipher := range vault.logins { + if cipher.Type != models.CipherLogin { + continue + } + if !cipher.DeletedDate.IsZero() { + continue + } + logins = append(logins, cipher) + } + return logins +} + +func (vault *Vault) GetNotes() []models.Cipher { + vault.lockMutex() + defer vault.unlockMutex() + + var notes []models.Cipher + for _, cipher := range vault.secureNotes { + if cipher.Type != models.CipherNote { + continue + } + if !cipher.DeletedDate.IsZero() { + continue + } + notes = append(notes, cipher) + } + return notes +} + +func (vault *Vault) GetLoginByFilter(uuid string, orgId string, name string, username string) (models.Cipher, error) { + vault.lockMutex() + defer vault.unlockMutex() + + for _, cipher := range vault.logins { + if uuid != "" && cipher.ID.String() != uuid { + continue + } + if orgId != "" && cipher.OrganizationID.String() != orgId { + continue + } + + key, err := cipher.GetKeyForCipher(*vault.Keyring) + if err != nil { + log.Warn().Err(err).Msg("Failed to get key for cipher " + cipher.ID.String()) + continue + } + if name != "" && !cipher.Name.IsNull() { + decryptedName, err := crypto.DecryptWith(cipher.Name, key) + if err != nil { + log.Warn().Err(err).Msg("Failed to decrypt name for cipher " + cipher.ID.String()) + continue + } + if name != "" && string(decryptedName) != name { + continue + } + } + + if username != "" && !cipher.Login.Username.IsNull() { + 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()) + continue + } + if username != "" && string(decryptedUsername) != username { + continue + } + } + + return cipher, nil + } + + return models.Cipher{}, errors.New("Cipher not found") +} + +func (vault *Vault) GetNoteByFilter(uuid string, orgId string, name string) (models.Cipher, error) { + vault.lockMutex() + defer vault.unlockMutex() + + for _, cipher := range vault.secureNotes { + if uuid != "" && cipher.ID.String() != uuid { + continue + } + if orgId != "" && cipher.OrganizationID.String() != orgId { + continue + } + + key, err := cipher.GetKeyForCipher(*vault.Keyring) + if err != nil { + log.Warn().Err(err).Msg("Failed to get key for cipher " + cipher.ID.String()) + 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()) + continue + } + if name != "" && string(decryptedName) != name { + continue + } + return cipher, nil + } + + return models.Cipher{}, errors.New("cipher not found") +} + +func (vault *Vault) GetLogin(uuid string) (models.Cipher, error) { + vault.lockMutex() + defer vault.unlockMutex() + + for _, cipher := range vault.logins { + if cipher.ID.String() == uuid { + return cipher, nil + } + } + + return models.Cipher{}, errors.New("cipher not found") +} + +func (vault *Vault) GetSecureNote(uuid string) (models.Cipher, error) { + vault.lockMutex() + defer vault.unlockMutex() + + for _, cipher := range vault.secureNotes { + if cipher.ID.String() == uuid { + return cipher, nil + } + } + + return models.Cipher{}, errors.New("cipher not found") +} diff --git a/autofill/autofill.go b/autofill/autofill.go new file mode 100644 index 0000000..def7e77 --- /dev/null +++ b/autofill/autofill.go @@ -0,0 +1,80 @@ +//go:build autofill +package autofill + +import ( + "errors" + + "github.com/atotto/clipboard" + "github.com/quexten/goldwarden/autofill/uinput" + "github.com/quexten/goldwarden/client" + "github.com/quexten/goldwarden/ipc" +) + +func GetLoginByUUID(uuid string) (ipc.DecryptedLoginCipher, error) { + resp, err := client.SendToAgent(ipc.GetLoginRequest{ + UUID: uuid, + }) + if err != nil { + return ipc.DecryptedLoginCipher{}, err + } + + switch resp.(type) { + case ipc.GetLoginResponse: + castedResponse := (resp.(ipc.GetLoginResponse)) + return castedResponse.Result, nil + case ipc.ActionResponse: + castedResponse := (resp.(ipc.ActionResponse)) + return ipc.DecryptedLoginCipher{}, errors.New("Error: " + castedResponse.Message) + default: + return ipc.DecryptedLoginCipher{}, errors.New("Wrong response type") + } +} + +func ListLogins() ([]ipc.DecryptedLoginCipher, error) { + resp, err := client.SendToAgent(ipc.ListLoginsRequest{}) + if err != nil { + return []ipc.DecryptedLoginCipher{}, err + } + + switch resp.(type) { + case ipc.GetLoginsResponse: + castedResponse := (resp.(ipc.GetLoginsResponse)) + return castedResponse.Result, nil + case ipc.ActionResponse: + castedResponse := (resp.(ipc.ActionResponse)) + return []ipc.DecryptedLoginCipher{}, errors.New("Error: " + castedResponse.Message) + default: + return []ipc.DecryptedLoginCipher{}, errors.New("Wrong response type") + } +} + +func Run(layout string) { + logins, err := ListLogins() + 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) + if err != nil { + panic(err) + } + // todo implement alternative auto type + clipboard.WriteAll(string(login.Username)) + uinput.Paste(layout) + uinput.TypeString(string(uinput.KeyTab), layout) + clipboard.WriteAll(login.Password) + uinput.Paste(layout) + clipboard.WriteAll("") + c <- true + }) +} diff --git a/autofill/gioAutofillDialog.go b/autofill/gioAutofillDialog.go new file mode 100644 index 0000000..cab210a --- /dev/null +++ b/autofill/gioAutofillDialog.go @@ -0,0 +1,239 @@ +package autofill + +import ( + "fmt" + "image" + "image/color" + "log" + "os" + "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" +) + +type AutofillEntry struct { + Username string + Name string + UUID string +} + +var autofillEntries = []AutofillEntry{} +var onAutofill func(string, chan bool) +var selectedEntry = 0 + +func GetFilteredAutofillEntries(entries []AutofillEntry, filter string) []AutofillEntry { + var filteredEntries []AutofillEntry + for _, entry := range autofillEntries { + if strings.Contains(strings.ToLower(entry.Username), strings.ToLower(filter)) || strings.Contains(strings.ToLower(entry.Name), strings.ToLower(filter)) { + filteredEntries = append(filteredEntries, entry) + } + } + + return filteredEntries +} + +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/uinput/dvorak.go b/autofill/uinput/dvorak.go new file mode 100644 index 0000000..e544241 --- /dev/null +++ b/autofill/uinput/dvorak.go @@ -0,0 +1,259 @@ +package uinput + +import ( + "errors" + "fmt" + + "github.com/bendahl/uinput" +) + +type Dvorak struct { +} + +func (d Dvorak) TypeKey(key Key, keyboard uinput.Keyboard) error { + switch key { + case KeyA: + keyboard.KeyPress(uinput.KeyA) + break + case KeyAUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyA) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyB: + keyboard.KeyPress(uinput.KeyN) + break + case KeyBUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyN) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyC: + keyboard.KeyPress(uinput.KeyI) + break + case KeyCUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyI) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyD: + keyboard.KeyPress(uinput.KeyH) + break + case KeyDUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyH) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyE: + keyboard.KeyPress(uinput.KeyD) + break + case KeyEUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyD) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyF: + keyboard.KeyPress(uinput.KeyY) + break + case KeyFUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyY) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyG: + keyboard.KeyPress(uinput.KeyU) + break + case KeyGUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyU) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyH: + keyboard.KeyPress(uinput.KeyJ) + break + case KeyHUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyJ) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyI: + keyboard.KeyPress(uinput.KeyG) + break + case KeyIUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyG) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyJ: + keyboard.KeyPress(uinput.KeyC) + break + case KeyJUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyC) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyK: + keyboard.KeyPress(uinput.KeyV) + break + case KeyKUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyV) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyL: + keyboard.KeyPress(uinput.KeyP) + break + case KeyLUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyP) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyM: + keyboard.KeyPress(uinput.KeyM) + break + case KeyMUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyM) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyN: + keyboard.KeyPress(uinput.KeyL) + break + case KeyNUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyL) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyO: + keyboard.KeyPress(uinput.KeyS) + break + case KeyOUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyS) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyP: + keyboard.KeyPress(uinput.KeyR) + break + case KeyPUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyR) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyQ: + keyboard.KeyPress(uinput.KeyX) + break + case KeyQUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyX) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyR: + keyboard.KeyPress(uinput.KeyO) + break + case KeyRUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyO) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyS: + keyboard.KeyPress(uinput.KeySemicolon) + break + case KeySUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeySemicolon) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyT: + keyboard.KeyPress(uinput.KeyK) + break + case KeyTUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyK) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyU: + keyboard.KeyPress(uinput.KeyF) + break + case KeyUUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyF) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyV: + keyboard.KeyPress(uinput.KeyDot) + break + case KeyVUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyDot) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyW: + keyboard.KeyPress(uinput.KeyComma) + break + case KeyWUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyComma) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyX: + keyboard.KeyPress(uinput.KeyB) + break + case KeyXUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyB) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyY: + keyboard.KeyPress(uinput.KeyT) + break + case KeyYUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyT) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyZ: + keyboard.KeyPress(uinput.KeySlash) + break + case KeyZUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.ButtonBumperLeft) + keyboard.KeyUp(uinput.KeyLeftshift) + case Key1: + keyboard.KeyPress(uinput.Key1) + break + case Key2: + keyboard.KeyPress(uinput.Key2) + break + case Key3: + keyboard.KeyPress(uinput.Key3) + break + case Key4: + keyboard.KeyPress(uinput.Key4) + break + case Key5: + keyboard.KeyPress(uinput.Key5) + break + case Key6: + keyboard.KeyPress(uinput.Key6) + break + case Key7: + keyboard.KeyPress(uinput.Key7) + break + case Key8: + keyboard.KeyPress(uinput.Key8) + break + case Key9: + keyboard.KeyPress(uinput.Key9) + break + case Key0: + keyboard.KeyPress(uinput.Key0) + break + case KeyHyphen: + keyboard.KeyPress(uinput.KeyApostrophe) + break + case KeyTab: + keyboard.KeyPress(uinput.KeyTab) + break + case KeyExclamationMark: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.Key1) + keyboard.KeyUp(uinput.KeyLeftshift) + break + case KeyAtSign: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.Key2) + keyboard.KeyUp(uinput.KeyLeftshift) + break + + case KeySpace: + 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 nil +} + +func init() { + DefaultLayoutRegistry.Register("dvorak", Dvorak{}) +} diff --git a/autofill/uinput/qwerty.go b/autofill/uinput/qwerty.go new file mode 100644 index 0000000..0e37894 --- /dev/null +++ b/autofill/uinput/qwerty.go @@ -0,0 +1,253 @@ +package uinput + +import ( + "errors" + "fmt" + + "github.com/bendahl/uinput" +) + +type Qwerty struct { +} + +func (d Qwerty) TypeKey(key Key, keyboard uinput.Keyboard) error { + switch key { + case KeyA: + keyboard.KeyPress(uinput.KeyA) + break + case KeyAUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyA) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyB: + keyboard.KeyPress(uinput.KeyB) + break + case KeyBUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyB) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyC: + keyboard.KeyPress(uinput.KeyC) + break + case KeyCUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyC) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyD: + keyboard.KeyPress(uinput.KeyD) + break + case KeyDUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyD) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyE: + keyboard.KeyPress(uinput.KeyE) + break + case KeyEUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyE) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyF: + keyboard.KeyPress(uinput.KeyF) + break + case KeyFUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyF) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyG: + keyboard.KeyPress(uinput.KeyG) + break + case KeyGUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyG) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyH: + keyboard.KeyPress(uinput.KeyH) + break + case KeyHUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyH) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyI: + keyboard.KeyPress(uinput.KeyI) + break + case KeyIUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyI) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyJ: + keyboard.KeyPress(uinput.KeyJ) + break + case KeyJUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyJ) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyK: + keyboard.KeyPress(uinput.KeyK) + break + case KeyKUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyK) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyL: + keyboard.KeyPress(uinput.KeyL) + break + case KeyLUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyL) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyM: + keyboard.KeyPress(uinput.KeyM) + break + case KeyMUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyM) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyN: + keyboard.KeyPress(uinput.KeyN) + break + case KeyNUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyN) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyO: + keyboard.KeyPress(uinput.KeyO) + break + case KeyOUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyO) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyP: + keyboard.KeyPress(uinput.KeyP) + break + case KeyPUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyP) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyQ: + keyboard.KeyPress(uinput.KeyQ) + break + case KeyQUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyQ) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyR: + keyboard.KeyPress(uinput.KeyR) + break + case KeyRUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyR) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyS: + keyboard.KeyPress(uinput.KeyS) + break + case KeySUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyS) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyT: + keyboard.KeyPress(uinput.KeyT) + break + case KeyTUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyT) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyU: + keyboard.KeyPress(uinput.KeyU) + break + case KeyUUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyU) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyV: + keyboard.KeyPress(uinput.KeyV) + break + case KeyVUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyV) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyW: + keyboard.KeyPress(uinput.KeyW) + break + case KeyWUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyW) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyX: + keyboard.KeyPress(uinput.KeyX) + break + case KeyXUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyX) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyY: + keyboard.KeyPress(uinput.KeyY) + break + case KeyYUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyY) + keyboard.KeyUp(uinput.KeyLeftshift) + case KeyZ: + keyboard.KeyPress(uinput.KeyZ) + break + case KeyZUpper: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.KeyZ) + keyboard.KeyUp(uinput.KeyLeftshift) + case Key1: + keyboard.KeyPress(uinput.Key1) + break + case Key2: + keyboard.KeyPress(uinput.Key2) + break + case Key3: + keyboard.KeyPress(uinput.Key3) + break + case Key4: + keyboard.KeyPress(uinput.Key4) + break + case Key5: + keyboard.KeyPress(uinput.Key5) + break + case Key6: + keyboard.KeyPress(uinput.Key6) + break + case Key7: + keyboard.KeyPress(uinput.Key7) + break + case Key8: + keyboard.KeyPress(uinput.Key8) + break + case Key9: + keyboard.KeyPress(uinput.Key9) + break + case Key0: + keyboard.KeyPress(uinput.Key0) + break + case KeyHyphen: + keyboard.KeyPress(uinput.KeyMinus) + break + case KeyTab: + keyboard.KeyPress(uinput.KeyTab) + break + case KeyExclamationMark: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.Key1) + keyboard.KeyUp(uinput.KeyLeftshift) + break + case KeyAtSign: + keyboard.KeyDown(uinput.KeyLeftshift) + keyboard.KeyPress(uinput.Key2) + keyboard.KeyUp(uinput.KeyLeftshift) + break + default: + fmt.Println("Unknown key: ", key) + fmt.Println("Please add it to the QWERTY layout") + return errors.New("Unknown key") + } + return nil +} + +func init() { + DefaultLayoutRegistry.Register("qwerty", Qwerty{}) +} diff --git a/autofill/uinput/uinput.go b/autofill/uinput/uinput.go new file mode 100644 index 0000000..a8b3e6c --- /dev/null +++ b/autofill/uinput/uinput.go @@ -0,0 +1,170 @@ +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 = "-" + KeyAtSign Key = "@" + + KeySpace Key = " " + KeyExclamationMark Key = "!" + KeyDollar Key = "$" + KeyEqual Key = "=" + KeySemicolon Key = ";" + KeyColon Key = ":" + KeyComma Key = "," + KeyPeriod Key = "." + KeySlash Key = "/" + KeyBackslash Key = "\\" + KeyPound Key = "#" + KeyPercent Key = "%" + KeyCaret Key = "^" + KeyAmpersand Key = "&" + KeyAsterisk Key = "*" + KeyPlus Key = "+" + KeyEquals Key = "=" + KeyUnderscore 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) + } + } + + 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 +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..da86506 --- /dev/null +++ b/client/client.go @@ -0,0 +1,56 @@ +package client + +import ( + "io" + "log" + "net" + "os" + + "github.com/quexten/goldwarden/ipc" +) + +const READ_BUFFER = 1 * 1024 * 1024 // 1MB + +func reader(r io.Reader) interface{} { + buf := make([]byte, READ_BUFFER) + for { + n, err := r.Read(buf[:]) + if err != nil { + return nil + } + message, err := ipc.UnmarshalJSON(buf[0:n]) + if err != nil { + panic(err) + } + return message + } +} + +func SendToAgent(request interface{}) (interface{}, error) { + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + // home := "/home/quexten" + c, err := net.Dial("unix", home+"/.goldwarden.sock") + if err != nil { + return nil, err + } + defer c.Close() + + message, err := ipc.IPCMessageFromPayload(request) + if err != nil { + panic(err) + } + messageJson, err := message.MarshallToJson() + if err != nil { + panic(err) + } + + _, err = c.Write(messageJson) + if err != nil { + log.Fatal("write error:", err) + } + result := reader(c) + return result.(ipc.IPCMessage).ParsedPayload(), nil +} diff --git a/cmd/autofill.go b/cmd/autofill.go new file mode 100644 index 0000000..0694bef --- /dev/null +++ b/cmd/autofill.go @@ -0,0 +1,23 @@ +//go:build autofill + +package cmd + +import ( + "github.com/quexten/goldwarden/autofill" + "github.com/spf13/cobra" +) + +var autofillCmd = &cobra.Command{ + Use: "autofill", + Short: "Autofill credentials", + Long: `Autofill credentials`, + Run: func(cmd *cobra.Command, args []string) { + layout := cmd.Flag("layout").Value.String() + autofill.Run(layout) + }, +} + +func init() { + rootCmd.AddCommand(autofillCmd) + autofillCmd.PersistentFlags().String("layout", "qwerty", "") +} diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..295919e --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "github.com/quexten/goldwarden/client" + "github.com/quexten/goldwarden/ipc" + "github.com/spf13/cobra" +) + +var setApiUrlCmd = &cobra.Command{ + Use: "set-api-url", + Short: "Set the api url", + Long: `Set the api url.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + return + } + + url := args[0] + request := ipc.SetApiURLRequest{} + request.Value = url + + result, err := client.SendToAgent(request) + if err != nil { + println("Error: " + err.Error()) + println("Is the daemon running?") + return + } + + switch result.(type) { + case ipc.ActionResponse: + if result.(ipc.ActionResponse).Success { + println("Done") + } else { + println("Setting api url failed: " + result.(ipc.ActionResponse).Message) + } + default: + println("Wrong IPC response type") + } + + }, +} + +var setIdentityURLCmd = &cobra.Command{ + Use: "set-identity-url", + Short: "Set the identity url", + Long: `Set the identity url.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + return + } + + url := args[0] + request := ipc.SetIdentityURLRequest{} + request.Value = url + + result, err := client.SendToAgent(request) + if err != nil { + println("Error: " + err.Error()) + println("Is the daemon running?") + return + } + + switch result.(type) { + case ipc.ActionResponse: + if result.(ipc.ActionResponse).Success { + println("Done") + } else { + println("Setting identity url failed: " + result.(ipc.ActionResponse).Message) + } + default: + println("Wrong IPC response type") + } + + }, +} + +var configCmd = &cobra.Command{ + Use: "config", + Short: "Manage the configuration", + Long: `Manage the configuration.`, +} + +func init() { + rootCmd.AddCommand(configCmd) + configCmd.AddCommand(setApiUrlCmd) + configCmd.AddCommand(setIdentityURLCmd) +} diff --git a/cmd/daemonize.go b/cmd/daemonize.go new file mode 100644 index 0000000..05cd890 --- /dev/null +++ b/cmd/daemonize.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "os" + "os/signal" + + "github.com/awnumar/memguard" + "github.com/quexten/goldwarden/agent" + "github.com/spf13/cobra" +) + +var daemonizeCmd = &cobra.Command{ + Use: "daemonize", + Short: "Starts the agent as a daemon", + Long: `Starts the agent as a daemon. The agent will run in the background and will + run in the background until it is stopped.`, + Run: func(cmd *cobra.Command, args []string) { + go func() { + signalChannel := make(chan os.Signal, 1) + signal.Notify(signalChannel, os.Interrupt) + <-signalChannel + memguard.SafeExit(0) + }() + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + err = agent.StartUnixAgent(home + "/.goldwarden.sock") + if err != nil { + panic(err) + } + }, +} + +func init() { + rootCmd.AddCommand(daemonizeCmd) +} diff --git a/cmd/login.go b/cmd/login.go new file mode 100644 index 0000000..1bc7fed --- /dev/null +++ b/cmd/login.go @@ -0,0 +1,46 @@ +/* +Copyright © 2023 NAME HERE +*/ +package cmd + +import ( + "github.com/quexten/goldwarden/client" + "github.com/quexten/goldwarden/ipc" + "github.com/spf13/cobra" +) + +var loginCmd = &cobra.Command{ + Use: "login", + Short: "Starts the login process for Bitwarden", + Long: `Starts the login process for Bitwarden. + You will be prompted to enter your password, and confirm your second factor if you have one.`, + Run: func(cmd *cobra.Command, args []string) { + request := ipc.DoLoginRequest{} + email, _ := cmd.Flags().GetString("email") + request.Email = email + + result, err := client.SendToAgent(request) + if err != nil { + println("Error: " + err.Error()) + println("Is the daemon running?") + return + } + + switch result.(type) { + case ipc.ActionResponse: + if result.(ipc.ActionResponse).Success { + println("Logged in") + } else { + println("Login failed: " + result.(ipc.ActionResponse).Message) + } + default: + println("Wrong IPC response type for login") + } + }, +} + +func init() { + vaultCmd.AddCommand(loginCmd) + loginCmd.PersistentFlags().String("email", "", "") + loginCmd.MarkFlagRequired("email") +} diff --git a/cmd/pin.go b/cmd/pin.go new file mode 100644 index 0000000..e0f006f --- /dev/null +++ b/cmd/pin.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "github.com/quexten/goldwarden/client" + "github.com/quexten/goldwarden/ipc" + "github.com/spf13/cobra" +) + +var pinCmd = &cobra.Command{ + Use: "pin", + Short: "Manage the vault pin", + Long: `Manage the vault pin. The pin is used to unlock the vault.`, +} + +var setPinCmd = &cobra.Command{ + Use: "set", + Short: "Set a new pin", + Long: `Set a new pin. The pin is used to unlock the vault.`, + Run: func(cmd *cobra.Command, args []string) { + result, err := client.SendToAgent(ipc.UpdateVaultPINRequest{}) + if err != nil { + println("Error: " + err.Error()) + println("Is the daemon running?") + return + } + + switch result.(type) { + case ipc.ActionResponse: + if result.(ipc.ActionResponse).Success { + println("Pin updated") + } else { + println("Pin updating failed: " + result.(ipc.ActionResponse).Message) + } + default: + println("Wrong response type") + } + }, +} + +var pinStatusCmd = &cobra.Command{ + Use: "status", + Short: "Check if a pin is set", + Long: `Check if a pin is set. The pin is used to unlock the vault.`, + Run: func(cmd *cobra.Command, args []string) { + result, err := client.SendToAgent(ipc.GetVaultPINRequest{}) + if err != nil { + println("Error: " + err.Error()) + println("Is the daemon running?") + return + } + + switch result.(type) { + case ipc.ActionResponse: + println("Pin status: " + result.(ipc.ActionResponse).Message) + default: + println("Wrong response type") + } + }, +} + +func init() { + vaultCmd.AddCommand(pinCmd) + pinCmd.AddCommand(setPinCmd) + pinCmd.AddCommand(pinStatusCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..2c39673 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "goldwarden", + Short: "OS level integration for Bitwarden", + Long: `Goldwarden is a daemon that runs in the background and provides + OS level integration for Bitwarden, such as SSH agent integration, + biometric unlock, and more.`, +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..f206c6d --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,64 @@ +/* +Copyright © 2023 NAME HERE +*/ +package cmd + +import ( + "os" + "os/exec" + + "github.com/quexten/goldwarden/client" + "github.com/quexten/goldwarden/ipc" + "github.com/spf13/cobra" +) + +// runCmd represents the run command +var runCmd = &cobra.Command{ + Use: "run", + Short: "Runs a command with environment variables from your vault", + Long: `Runs a command with environment variables from your vault. + The variables are stored as a secure note. Consult the documentation for more information.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + println("Error: No command specified") + return + } + + executable := args[0] + executableArgs := args[1:] + + env := []string{} + + result, err := client.SendToAgent(ipc.GetCLICredentialsRequest{ + ApplicationName: executable, + }) + if err != nil { + println("Error: " + err.Error()) + println("Is the daemon running?") + return + } + + switch result.(type) { + case ipc.GetCLICredentialsResponse: + response := result.(ipc.GetCLICredentialsResponse) + for key, value := range response.Env { + env = append(env, key+"="+value) + } + case ipc.ActionResponse: + println("Error: " + result.(ipc.ActionResponse).Message) + return + } + + command := exec.Command(executable, executableArgs...) + command.Env = append(command.Env, os.Environ()...) + command.Env = append(command.Env, env...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + command.Stdin = os.Stdin + command.Run() + }, +} + +func init() { + rootCmd.AddCommand(runCmd) +} diff --git a/cmd/ssh.go b/cmd/ssh.go new file mode 100644 index 0000000..b47f94a --- /dev/null +++ b/cmd/ssh.go @@ -0,0 +1,84 @@ +/* +Copyright © 2023 NAME HERE +*/ +package cmd + +import ( + "fmt" + + "github.com/quexten/goldwarden/client" + "github.com/quexten/goldwarden/ipc" + "github.com/spf13/cobra" +) + +var sshCmd = &cobra.Command{ + Use: "ssh", + Short: "Commands for managing SSH keys", + Long: `Commands for managing SSH keys.`, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +// runCmd represents the run command +var sshAddCmd = &cobra.Command{ + Use: "add", + Short: "Runs a command with environment variables from your vault", + Long: `Runs a command with environment variables from your vault. + The variables are stored as a secure note. Consult the documentation for more information.`, + Run: func(cmd *cobra.Command, args []string) { + name, _ := cmd.Flags().GetString("name") + + result, err := client.SendToAgent(ipc.CreateSSHKeyRequest{ + Name: name, + }) + if err != nil { + println("Error: " + err.Error()) + println("Is the daemon running?") + return + } + + switch result.(type) { + case ipc.CreateSSHKeyResponse: + response := result.(ipc.CreateSSHKeyResponse) + fmt.Println(response.Digest) + case ipc.ActionResponse: + println("Error: " + result.(ipc.ActionResponse).Message) + return + } + }, +} + +var listSSHCmd = &cobra.Command{ + Use: "list", + Short: "Lists all SSH keys in your vault", + Long: `Lists all SSH keys in your vault.`, + Run: func(cmd *cobra.Command, args []string) { + result, err := client.SendToAgent(ipc.GetSSHKeysRequest{}) + if err != nil { + println("Error: " + err.Error()) + println("Is the daemon running?") + return + } + + switch result.(type) { + case ipc.GetSSHKeysResponse: + response := result.(ipc.GetSSHKeysResponse) + for _, key := range response.Keys { + fmt.Println(key) + } + break + case ipc.ActionResponse: + println("Error: " + result.(ipc.ActionResponse).Message) + return + } + }, +} + +func init() { + rootCmd.AddCommand(sshCmd) + sshCmd.AddCommand(sshAddCmd) + sshAddCmd.PersistentFlags().String("name", "", "") + sshAddCmd.MarkFlagRequired("name") + sshCmd.AddCommand(listSSHCmd) +} diff --git a/cmd/vault.go b/cmd/vault.go new file mode 100644 index 0000000..9a734a4 --- /dev/null +++ b/cmd/vault.go @@ -0,0 +1,101 @@ +package cmd + +import ( + "github.com/quexten/goldwarden/client" + "github.com/quexten/goldwarden/ipc" + "github.com/spf13/cobra" +) + +var vaultCmd = &cobra.Command{ + Use: "vault", + Short: "Manage the vault", + Long: `Manage the vault.`, +} + +var unlockCmd = &cobra.Command{ + Use: "unlock", + Short: "Unlocks the vault", + Long: `Unlocks the vault. You will be prompted for your pin. The pin is empty by default.`, + Run: func(cmd *cobra.Command, args []string) { + request := ipc.UnlockVaultRequest{} + + result, err := client.SendToAgent(request) + if err != nil { + println("Error: " + err.Error()) + println("Is the daemon running?") + return + } + + switch result.(type) { + case ipc.ActionResponse: + if result.(ipc.ActionResponse).Success { + println("Unlocked") + } else { + println("Not unlocked: " + result.(ipc.ActionResponse).Message) + } + default: + println("Wrong response type") + } + }, +} + +var lockCmd = &cobra.Command{ + Use: "lock", + Short: "Locks the vault", + Long: `Locks the vault.`, + Run: func(cmd *cobra.Command, args []string) { + request := ipc.LockVaultRequest{} + + result, err := client.SendToAgent(request) + if err != nil { + println("Error: " + err.Error()) + println("Is the daemon running?") + return + } + + switch result.(type) { + case ipc.ActionResponse: + if result.(ipc.ActionResponse).Success { + println("Locked") + } else { + println("Not locked: " + result.(ipc.ActionResponse).Message) + } + default: + println("Wrong response type") + } + }, +} + +var purgeCmd = &cobra.Command{ + Use: "purge", + Short: "Wipes the vault", + Long: `Wipes the vault and encryption keys from ram and config. Does not delete any entries on the server side.`, + Run: func(cmd *cobra.Command, args []string) { + request := ipc.WipeVaultRequest{} + + result, err := client.SendToAgent(request) + if err != nil { + println("Error: " + err.Error()) + println("Is the daemon running?") + return + } + + switch result.(type) { + case ipc.ActionResponse: + if result.(ipc.ActionResponse).Success { + println("Purged") + } else { + println("Not purged: " + result.(ipc.ActionResponse).Message) + } + default: + println("Wrong response type") + } + }, +} + +func init() { + rootCmd.AddCommand(vaultCmd) + vaultCmd.AddCommand(unlockCmd) + vaultCmd.AddCommand(lockCmd) + vaultCmd.AddCommand(purgeCmd) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b7a9f92 --- /dev/null +++ b/go.mod @@ -0,0 +1,48 @@ +module github.com/quexten/goldwarden + +go 1.20 + +require ( + gioui.org v0.1.0 + github.com/LlamaNite/llamalog v0.2.1 + github.com/amenzhinsky/go-polkit v0.0.0-20210519083301-ee6a51849123 + 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/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a + github.com/mitchellh/go-ps v1.0.0 + 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.11.0 + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 + golang.org/x/sys v0.10.0 +) + +require ( + gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect + gioui.org/shader v1.0.6 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433 // 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.11.0 // indirect +) + +require ( + github.com/awnumar/memcall v0.1.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + 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/pkg/errors v0.9.1 // indirect + github.com/rs/zerolog v1.29.1 + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.8.1 // 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 new file mode 100644 index 0000000..ebdf37b --- /dev/null +++ b/go.sum @@ -0,0 +1,135 @@ +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= +gioui.org v0.1.0 h1:fEDY5A4+epOdzjCBYSUC4BzvjWqsjfqf5D6mskbthOs= +gioui.org v0.1.0/go.mod h1:a3hz8FyrPMkt899D9YrxMGtyRzpPrJpz1Lzbssn81vI= +gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= +gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc= +gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= +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/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/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/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= +github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433 h1:Pdyvqsfi1QYgFfZa4R8otBOtgO+CGyBDMEG8cM3jwvE= +github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433/go.mod h1:KmrpWuSMFcO2yjmyhGpnBGQHSKAoEgMTSSzvLDzCuEA= +github.com/go-text/typesetting-utils v0.0.0-20230412163830-89e4bcfa3ecc h1:9Kf84pnrmmjdRzZIkomfjowmGUhHs20jkrWYw/I6CYc= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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/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/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= +github.com/keys-pub/go-libfido2 v1.5.3/go.mod h1:P0V19qHwJNY0htZwZDe9Ilvs/nokGhdFX7faKFyZ6+U= +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/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= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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/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= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/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/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= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +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.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +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/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-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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.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.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +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.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.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= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +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= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg= +inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU= diff --git a/ipc/ipc.go b/ipc/ipc.go new file mode 100644 index 0000000..c91cce5 --- /dev/null +++ b/ipc/ipc.go @@ -0,0 +1,456 @@ +package ipc + +import ( + "encoding/json" +) + +type IPCMessageType int64 + +const ( + IPCMessageTypeErrorMessage IPCMessageType = 0 + + IPCMessageTypeDoLoginRequest IPCMessageType = 1 + + IPCMessageTypeUpdateVaultPINRequest IPCMessageType = 4 + IPCMessageTypeUnlockVaultRequest IPCMessageType = 5 + IPCMessageTypeLockVaultRequest IPCMessageType = 6 + IPCMessageTypeWipeVaultRequest IPCMessageType = 7 + + IPCMessageTypeGetCLICredentialsRequest IPCMessageType = 11 + IPCMessageTypeGetCLICredentialsResponse IPCMessageType = 12 + + IPCMessageTypeCreateSSHKeyRequest IPCMessageType = 14 + IPCMessageTypeCreateSSHKeyResponse IPCMessageType = 15 + + IPCMessageTypeGetSSHKeysRequest IPCMessageType = 16 + IPCMessageTypeGetSSHKeysResponse IPCMessageType = 17 + + IPCMessageGetLoginRequest IPCMessageType = 18 + IPCMessageGetLoginResponse IPCMessageType = 19 + + IPCMessageAddLoginRequest IPCMessageType = 20 + IPCMessageAddLoginResponse IPCMessageType = 21 + + IPCMessageGetNoteRequest IPCMessageType = 26 + IPCMessageGetNoteResponse IPCMessageType = 27 + IPCMessageGetNotesResponse IPCMessageType = 32 + IPCMessageGetLoginsResponse IPCMessageType = 33 + + IPCMessageAddNoteRequest IPCMessageType = 28 + IPCMessageAddNoteResponse IPCMessageType = 29 + + IPCMessageListLoginsRequest IPCMessageType = 22 + + IPCMessageTypeActionResponse IPCMessageType = 13 + + IPCMessageTypeGetVaultPINStatusRequest IPCMessageType = 2 + + IPCMessageTypeSetAPIUrlRequest IPCMessageType = 30 + IPCMessageTypeSetIdentityURLRequest IPCMessageType = 31 +) + +type IPCMessage struct { + Type IPCMessageType `json:"type"` + Payload []byte `json:"payload"` +} + +func (m IPCMessage) MarshallToJson() ([]byte, error) { + return json.Marshal(m) +} + +func UnmarshalJSON(data []byte) (IPCMessage, error) { + var m IPCMessage + err := json.Unmarshal(data, &m) + return m, err +} + +func (m IPCMessage) ParsedPayload() interface{} { + switch m.Type { + case IPCMessageTypeDoLoginRequest: + var req DoLoginRequest + err := json.Unmarshal(m.Payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req + case IPCMessageTypeActionResponse: + var res ActionResponse + err := json.Unmarshal(m.Payload, &res) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return res + case IPCMessageTypeErrorMessage: + return nil + case IPCMessageTypeGetCLICredentialsRequest: + var req GetCLICredentialsRequest + err := json.Unmarshal(m.Payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req + case IPCMessageTypeGetCLICredentialsResponse: + var res GetCLICredentialsResponse + err := json.Unmarshal(m.Payload, &res) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return res + case IPCMessageTypeCreateSSHKeyRequest: + var req CreateSSHKeyRequest + err := json.Unmarshal(m.Payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req + case IPCMessageTypeCreateSSHKeyResponse: + var res CreateSSHKeyResponse + err := json.Unmarshal(m.Payload, &res) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return res + case IPCMessageTypeGetSSHKeysRequest: + var req GetSSHKeysRequest + err := json.Unmarshal(m.Payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req + case IPCMessageTypeGetSSHKeysResponse: + var res GetSSHKeysResponse + err := json.Unmarshal(m.Payload, &res) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return res + case IPCMessageGetLoginRequest: + var req GetLoginRequest + err := json.Unmarshal(m.Payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req + case IPCMessageGetLoginResponse: + var res GetLoginResponse + err := json.Unmarshal(m.Payload, &res) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return res + case IPCMessageAddLoginRequest: + var req AddLoginRequest + err := json.Unmarshal(m.Payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req + case IPCMessageAddLoginResponse: + var res AddLoginResponse + err := json.Unmarshal(m.Payload, &res) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return res + case IPCMessageTypeWipeVaultRequest: + var req WipeVaultRequest + err := json.Unmarshal(m.Payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req + case IPCMessageTypeLockVaultRequest: + var req LockVaultRequest + err := json.Unmarshal(m.Payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req + case IPCMessageTypeGetVaultPINStatusRequest: + var req GetVaultPINRequest + err := json.Unmarshal(m.Payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req + case IPCMessageTypeSetAPIUrlRequest: + var req SetApiURLRequest + err := json.Unmarshal(m.Payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req + case IPCMessageTypeSetIdentityURLRequest: + var req SetIdentityURLRequest + err := json.Unmarshal(m.Payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req + case IPCMessageGetLoginsResponse: + var res GetLoginsResponse + err := json.Unmarshal(m.Payload, &res) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return res + default: + return nil + } +} + +func IPCMessageFromPayload(payload interface{}) (IPCMessage, error) { + jsonBytes, err := json.Marshal(payload) + if err != nil { + return IPCMessage{}, err + } + + switch payload.(type) { + case UnlockVaultRequest: + return IPCMessage{ + Type: IPCMessageTypeUnlockVaultRequest, + Payload: jsonBytes, + }, nil + case UpdateVaultPINRequest: + return IPCMessage{ + Type: IPCMessageTypeUpdateVaultPINRequest, + Payload: jsonBytes, + }, nil + case DoLoginRequest: + return IPCMessage{ + Type: IPCMessageTypeDoLoginRequest, + Payload: jsonBytes, + }, nil + case ActionResponse: + return IPCMessage{ + Type: IPCMessageTypeActionResponse, + Payload: jsonBytes, + }, nil + case GetCLICredentialsRequest: + return IPCMessage{ + Type: IPCMessageTypeGetCLICredentialsRequest, + Payload: jsonBytes, + }, nil + case GetCLICredentialsResponse: + return IPCMessage{ + Type: IPCMessageTypeGetCLICredentialsResponse, + Payload: jsonBytes, + }, nil + case CreateSSHKeyRequest: + return IPCMessage{ + Type: IPCMessageTypeCreateSSHKeyRequest, + Payload: jsonBytes, + }, nil + case CreateSSHKeyResponse: + return IPCMessage{ + Type: IPCMessageTypeCreateSSHKeyResponse, + Payload: jsonBytes, + }, nil + case GetSSHKeysRequest: + return IPCMessage{ + Type: IPCMessageTypeGetSSHKeysRequest, + Payload: jsonBytes, + }, nil + case GetSSHKeysResponse: + return IPCMessage{ + Type: IPCMessageTypeGetSSHKeysResponse, + Payload: jsonBytes, + }, nil + case WipeVaultRequest: + return IPCMessage{ + Type: IPCMessageTypeWipeVaultRequest, + Payload: jsonBytes, + }, nil + case LockVaultRequest: + return IPCMessage{ + Type: IPCMessageTypeLockVaultRequest, + Payload: jsonBytes, + }, nil + case GetVaultPINRequest: + return IPCMessage{ + Type: IPCMessageTypeGetVaultPINStatusRequest, + Payload: jsonBytes, + }, nil + case SetApiURLRequest: + return IPCMessage{ + Type: IPCMessageTypeSetAPIUrlRequest, + Payload: jsonBytes, + }, nil + case SetIdentityURLRequest: + return IPCMessage{ + Type: IPCMessageTypeSetIdentityURLRequest, + Payload: jsonBytes, + }, nil + case GetLoginRequest: + return IPCMessage{ + Type: IPCMessageGetLoginRequest, + Payload: jsonBytes, + }, nil + case GetLoginResponse: + return IPCMessage{ + Type: IPCMessageGetLoginResponse, + Payload: jsonBytes, + }, nil + case AddLoginRequest: + return IPCMessage{ + Type: IPCMessageAddLoginRequest, + Payload: jsonBytes, + }, nil + case AddLoginResponse: + return IPCMessage{ + Type: IPCMessageAddLoginResponse, + Payload: jsonBytes, + }, nil + case GetNotesRequest: + return IPCMessage{ + Type: IPCMessageGetNoteRequest, + Payload: jsonBytes, + }, nil + case GetNotesResponse: + return IPCMessage{ + Type: IPCMessageGetNotesResponse, + Payload: jsonBytes, + }, nil + case GetNoteResponse: + return IPCMessage{ + Type: IPCMessageGetNoteResponse, + Payload: jsonBytes, + }, nil + case GetLoginsResponse: + return IPCMessage{ + Type: IPCMessageGetLoginsResponse, + Payload: jsonBytes, + }, nil + case ListLoginsRequest: + return IPCMessage{ + Type: IPCMessageListLoginsRequest, + Payload: jsonBytes, + }, nil + default: + payloadBytes, err := json.Marshal(payload) + if err != nil { + return IPCMessage{}, err + } + + return IPCMessage{ + Type: IPCMessageTypeErrorMessage, + Payload: payloadBytes, + }, nil + } +} + +type DoLoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type LockVaultRequest struct { +} + +type UnlockVaultRequest struct { +} + +type UpdateVaultPINRequest struct { +} + +type ActionResponse struct { + Success bool + Message string +} + +type GetCLICredentialsRequest struct { + ApplicationName string +} + +type GetCLICredentialsResponse struct { + Env map[string]string +} + +type CreateSSHKeyRequest struct { + Name string +} + +type CreateSSHKeyResponse struct { + Digest string +} + +type GetSSHKeysRequest struct { +} + +type GetSSHKeysResponse struct { + Keys []string +} + +type GetLoginRequest struct { + Name string + Username string + UUID string + OrgId string + + GetList bool +} + +type GetLoginResponse struct { + Found bool + Result DecryptedLoginCipher +} + +type GetLoginsResponse struct { + Found bool + Result []DecryptedLoginCipher +} + +type DecryptedLoginCipher struct { + Name string + Username string + Password string + UUID string + OrgaizationID string + Notes string +} + +type GetNotesRequest struct { + Name string +} + +type GetNoteResponse struct { + Found bool + Result DecryptedNoteCipher +} + +type GetNotesResponse struct { + Found bool + Result []DecryptedNoteCipher +} + +type DecryptedNoteCipher struct { + Name string + Contents string +} + +type AddLoginRequest struct { + Name string + UUID string +} + +type AddLoginResponse struct { + Name string + UUID string +} + +type WipeVaultRequest struct { +} + +type GetVaultPINRequest struct { +} + +type SetApiURLRequest struct { + Value string +} + +type SetIdentityURLRequest struct { + Value string +} + +type ListLoginsRequest struct { +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..69e029f --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/quexten/goldwarden/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/resources/com.quexten.goldwarden.policy b/resources/com.quexten.goldwarden.policy new file mode 100644 index 0000000..641db10 --- /dev/null +++ b/resources/com.quexten.goldwarden.policy @@ -0,0 +1,43 @@ + + + + + + Allow Credential Access + Authenticate to allow access to a single credential + + auth_self + auth_self + auth_self + + + + Approve Pin Change + Authenticate to change your Goldwarden PIN. + + auth_self + auth_self + auth_self + + + + Use Bitwarden SSH Key + Authenticate to use an SSH Key from your vault + + auth_self + auth_self + auth_self + + + + Modify Bitwarden Vault + Authenticate to allow modification of your Bitvarden vault in Goldwarden + + auth_self + auth_self + auth_self + + +