Initial commit

This commit is contained in:
Bernd Schoolmann 2023-07-17 03:23:26 +02:00
commit 30237e79b2
No known key found for this signature in database
51 changed files with 6127 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.vscode
*debug*
goldwarden*

21
LICENSE Normal file
View File

@ -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.

211
Readme.md Normal file
View File

@ -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 <email>
```
Create an ssh key
```
goldwarden ssh add --name <name>
```
Run a command with injected environment variables
```
goldwarden run -- <command>
```
Autofill
```
goldwarden autofill --layout <keyboard-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: <contents of id_ed25519> (hidden field)
public-key: <contents of id_ed25519.pub>
```
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 = <your email>
name = <your name>
signingKey = <your public key>
[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: <your access key>
AWS_SECRET_ACCESS_KEY: <your secret key> (hidden)
RESTIC_PASSWORD: <your restic password> (hidden)
# optional
RESTIC_REPOSITORY: <your restic repository>
...
```
Then, run the command:
```
goldwarden run -- <command>
```
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

77
agent/actions/actions.go Normal file
View File

@ -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)))
}

45
agent/actions/config.go Normal file
View File

@ -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)
}

View File

@ -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))
}

100
agent/actions/login.go Normal file
View File

@ -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))
}

145
agent/actions/logins.go Normal file
View File

@ -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))
}

62
agent/actions/ssh.go Normal file
View File

@ -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)))
}

174
agent/actions/vault.go Normal file
View File

@ -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)
}

211
agent/agent.go Normal file
View File

@ -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)
}
}

159
agent/bitwarden/auth.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}

120
agent/bitwarden/http.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

54
agent/bitwarden/sync.go Normal file
View File

@ -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
}

View File

@ -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()

View File

@ -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
}

351
agent/config/config.go Normal file
View File

@ -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
}

View File

@ -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(),
}
}

71
agent/ssh/keys.go Normal file
View File

@ -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))
}

178
agent/ssh/ssh.go Normal file
View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}
}

391
agent/vault/vault.go Normal file
View File

@ -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")
}

80
autofill/autofill.go Normal file
View File

@ -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
})
}

View File

@ -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}
}

259
autofill/uinput/dvorak.go Normal file
View File

@ -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{})
}

253
autofill/uinput/qwerty.go Normal file
View File

@ -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{})
}

170
autofill/uinput/uinput.go Normal file
View File

@ -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
}

56
client/client.go Normal file
View File

@ -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
}

23
cmd/autofill.go Normal file
View File

@ -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", "")
}

87
cmd/config.go Normal file
View File

@ -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)
}

37
cmd/daemonize.go Normal file
View File

@ -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)
}

46
cmd/login.go Normal file
View File

@ -0,0 +1,46 @@
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
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")
}

65
cmd/pin.go Normal file
View File

@ -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)
}

26
cmd/root.go Normal file
View File

@ -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")
}

64
cmd/run.go Normal file
View File

@ -0,0 +1,64 @@
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
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)
}

84
cmd/ssh.go Normal file
View File

@ -0,0 +1,84 @@
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
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)
}

101
cmd/vault.go Normal file
View File

@ -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)
}

48
go.mod Normal file
View File

@ -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
)

135
go.sum Normal file
View File

@ -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=

456
ipc/ipc.go Normal file
View File

@ -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 {
}

9
main.go Normal file
View File

@ -0,0 +1,9 @@
package main
import (
"github.com/quexten/goldwarden/cmd"
)
func main() {
cmd.Execute()
}

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>
<action id="com.quexten.goldwarden.accesscredential">
<description>Allow Credential Access</description>
<message>Authenticate to allow access to a single credential</message>
<defaults>
<allow_any>auth_self</allow_any>
<allow_inactive>auth_self</allow_inactive>
<allow_active>auth_self</allow_active>
</defaults>
</action>
<action id="com.quexten.goldwarden.changepin">
<description>Approve Pin Change</description>
<message>Authenticate to change your Goldwarden PIN.</message>
<defaults>
<allow_any>auth_self</allow_any>
<allow_inactive>auth_self</allow_inactive>
<allow_active>auth_self</allow_active>
</defaults>
</action>
<action id="com.quexten.goldwarden.usesshkey">
<description>Use Bitwarden SSH Key</description>
<message>Authenticate to use an SSH Key from your vault</message>
<defaults>
<allow_any>auth_self</allow_any>
<allow_inactive>auth_self</allow_inactive>
<allow_active>auth_self</allow_active>
</defaults>
</action>
<action id="com.quexten.goldwarden.modifyvault">
<description>Modify Bitwarden Vault</description>
<message>Authenticate to allow modification of your Bitvarden vault in Goldwarden</message>
<defaults>
<allow_any>auth_self</allow_any>
<allow_inactive>auth_self</allow_inactive>
<allow_active>auth_self</allow_active>
</defaults>
</action>
</policyconfig>