Merge pull request #100 from quexten/feature/authenticated-connection
Authenticated Session & External Pinentry
This commit is contained in:
commit
52156f8892
55 changed files with 1145 additions and 491 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -3,4 +3,5 @@ goldwarden
|
||||||
__pycache__
|
__pycache__
|
||||||
.flatpak-builder
|
.flatpak-builder
|
||||||
flatpak-pip-generator
|
flatpak-pip-generator
|
||||||
repo
|
repo
|
||||||
|
__debug*
|
||||||
|
|
@ -97,16 +97,16 @@ func handleGetLoginCipher(request messages.IPCMessage, cfg *config.Config, vault
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleListLoginsRequest(request messages.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx *sockets.CallingContext) (response messages.IPCMessage, err error) {
|
func handleListLoginsRequest(request messages.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx *sockets.CallingContext) (response messages.IPCMessage, err error) {
|
||||||
if approved, err := pinentry.GetApproval("Access Vault", fmt.Sprintf("%s on %s>%s>%s is trying access ALL CREDENTIALS", ctx.UserName, ctx.GrandParentProcessName, ctx.ParentProcessName, ctx.ProcessName)); err != nil || !approved {
|
// if approved, err := pinentry.GetApproval("Access Vault", fmt.Sprintf("%s on %s>%s>%s is trying access ALL CREDENTIALS", ctx.UserName, ctx.GrandParentProcessName, ctx.ParentProcessName, ctx.ProcessName)); err != nil || !approved {
|
||||||
response, err = messages.IPCMessageFromPayload(messages.ActionResponse{
|
// response, err = messages.IPCMessageFromPayload(messages.ActionResponse{
|
||||||
Success: false,
|
// Success: false,
|
||||||
Message: "not approved",
|
// Message: "not approved",
|
||||||
})
|
// })
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return messages.IPCMessage{}, err
|
// return messages.IPCMessage{}, err
|
||||||
}
|
// }
|
||||||
return response, nil
|
// return response, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
logins := vault.GetLogins()
|
logins := vault.GetLogins()
|
||||||
decryptedLoginCiphers := make([]messages.DecryptedLoginCipher, 0)
|
decryptedLoginCiphers := make([]messages.DecryptedLoginCipher, 0)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -114,7 +113,7 @@ func makeAuthenticatedHTTPRequest(ctx context.Context, req *http.Request, recv i
|
||||||
return &errStatusCode{res.StatusCode, body}
|
return &errStatusCode{res.StatusCode, body}
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, recv); err != nil {
|
if err := json.Unmarshal(body, recv); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, string(body))
|
fmt.Println(string(body))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ type RuntimeConfig struct {
|
||||||
UseMemguard bool
|
UseMemguard bool
|
||||||
SSHAgentSocketPath string
|
SSHAgentSocketPath string
|
||||||
GoldwardenSocketPath string
|
GoldwardenSocketPath string
|
||||||
|
DaemonAuthToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigFile struct {
|
type ConfigFile struct {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ type CallingContext struct {
|
||||||
ParentProcessPid int
|
ParentProcessPid int
|
||||||
GrandParentProcessPid int
|
GrandParentProcessPid int
|
||||||
Error bool
|
Error bool
|
||||||
|
Authenticated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCallingContext(connection net.Conn) CallingContext {
|
func GetCallingContext(connection net.Conn) CallingContext {
|
||||||
|
|
@ -30,6 +31,7 @@ func GetCallingContext(connection net.Conn) CallingContext {
|
||||||
ParentProcessPid: 0,
|
ParentProcessPid: 0,
|
||||||
GrandParentProcessPid: 0,
|
GrandParentProcessPid: 0,
|
||||||
Error: true,
|
Error: true,
|
||||||
|
Authenticated: false,
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorContext
|
return errorContext
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/twpayne/go-pinentry"
|
"github.com/twpayne/go-pinentry"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetPassword(title string, description string) (string, error) {
|
func getPassword(title string, description string) (string, error) {
|
||||||
client, err := pinentry.NewClient(
|
client, err := pinentry.NewClient(
|
||||||
pinentry.WithBinaryNameFromGnuPGAgentConf(),
|
pinentry.WithBinaryNameFromGnuPGAgentConf(),
|
||||||
pinentry.WithGPGTTY(),
|
pinentry.WithGPGTTY(),
|
||||||
|
|
@ -38,7 +38,7 @@ func GetPassword(title string, description string) (string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetApproval(title string, description string) (bool, error) {
|
func getApproval(title string, description string) (bool, error) {
|
||||||
if systemAuthDisabled {
|
if systemAuthDisabled {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
pinentry "github.com/quexten/goldwarden/agent/systemauth/pinentry/keybase-pinentry"
|
pinentry "github.com/quexten/goldwarden/agent/systemauth/pinentry/keybase-pinentry"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetPassword(title string, description string) (string, error) {
|
func getPassword(title string, description string) (string, error) {
|
||||||
pinentryInstance := pinentry.New("", logger.New(""), "")
|
pinentryInstance := pinentry.New("", logger.New(""), "")
|
||||||
result, err := pinentryInstance.Get(keybase1.SecretEntryArg{
|
result, err := pinentryInstance.Get(keybase1.SecretEntryArg{
|
||||||
Prompt: title,
|
Prompt: title,
|
||||||
|
|
@ -28,7 +28,7 @@ func GetPassword(title string, description string) (string, error) {
|
||||||
return result.Text, nil
|
return result.Text, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetApproval(title string, description string) (bool, error) {
|
func getApproval(title string, description string) (bool, error) {
|
||||||
pinentryInstance := pinentry.New("", logger.New(""), "")
|
pinentryInstance := pinentry.New("", logger.New(""), "")
|
||||||
result, err := pinentryInstance.Get(keybase1.SecretEntryArg{
|
result, err := pinentryInstance.Get(keybase1.SecretEntryArg{
|
||||||
Prompt: title,
|
Prompt: title,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package pinentry
|
package pinentry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/quexten/goldwarden/logging"
|
"github.com/quexten/goldwarden/logging"
|
||||||
|
|
@ -9,8 +10,52 @@ import (
|
||||||
var log = logging.GetLogger("Goldwarden", "Pinentry")
|
var log = logging.GetLogger("Goldwarden", "Pinentry")
|
||||||
var systemAuthDisabled = false
|
var systemAuthDisabled = false
|
||||||
|
|
||||||
|
type Pinentry struct {
|
||||||
|
GetPassword func(title string, description string) (string, error)
|
||||||
|
GetApproval func(title string, description string) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var externalPinentry Pinentry = Pinentry{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if os.Getenv("GOLDWARDEN_SYSTEM_AUTH_DISABLED") == "true" {
|
if os.Getenv("GOLDWARDEN_SYSTEM_AUTH_DISABLED") == "true" {
|
||||||
systemAuthDisabled = true
|
systemAuthDisabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetExternalPinentry(pinentry Pinentry) error {
|
||||||
|
if externalPinentry.GetPassword != nil {
|
||||||
|
return errors.New("External pinentry already set")
|
||||||
|
}
|
||||||
|
|
||||||
|
externalPinentry = pinentry
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPassword(title string, description string) (string, error) {
|
||||||
|
password, err := getPassword(title, description)
|
||||||
|
if err == nil {
|
||||||
|
return password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if externalPinentry.GetPassword != nil {
|
||||||
|
return externalPinentry.GetPassword(title, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// return "", errors.New("Not implemented")
|
||||||
|
return password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetApproval(title string, description string) (bool, error) {
|
||||||
|
approval, err := getApproval(title, description)
|
||||||
|
if err == nil {
|
||||||
|
return approval, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if externalPinentry.GetApproval != nil {
|
||||||
|
return externalPinentry.GetApproval(title, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// return true, errors.New("Not implemented")
|
||||||
|
return approval, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ package pinentry
|
||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
func GetPassword(title string, description string) (string, error) {
|
func getPassword(title string, description string) (string, error) {
|
||||||
log.Info("Asking for password is not implemented on this platform")
|
log.Info("Asking for password is not implemented on this platform")
|
||||||
return "", errors.New("Not implemented")
|
return "", errors.New("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetApproval(title string, description string) (bool, error) {
|
func getApproval(title string, description string) (bool, error) {
|
||||||
log.Info("Asking for approval is not implemented on this platform")
|
log.Info("Asking for approval is not implemented on this platform")
|
||||||
return true, errors.New("Not implemented")
|
return true, errors.New("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,10 @@ func (s *SessionStore) verifySession(ctx sockets.CallingContext, sessionType Ses
|
||||||
|
|
||||||
// with session
|
// with session
|
||||||
func GetPermission(sessionType SessionType, ctx sockets.CallingContext, config *config.Config) (bool, error) {
|
func GetPermission(sessionType SessionType, ctx sockets.CallingContext, config *config.Config) (bool, error) {
|
||||||
|
if ctx.Authenticated {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
log.Info("Checking permission for " + ctx.ProcessName + " with session type " + string(sessionType))
|
log.Info("Checking permission for " + ctx.ProcessName + " with session type " + string(sessionType))
|
||||||
var actionDescription = ""
|
var actionDescription = ""
|
||||||
biometricsApprovalType := biometrics.AccessVault
|
biometricsApprovalType := biometrics.AccessVault
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import (
|
||||||
"github.com/quexten/goldwarden/agent/processsecurity"
|
"github.com/quexten/goldwarden/agent/processsecurity"
|
||||||
"github.com/quexten/goldwarden/agent/sockets"
|
"github.com/quexten/goldwarden/agent/sockets"
|
||||||
"github.com/quexten/goldwarden/agent/ssh"
|
"github.com/quexten/goldwarden/agent/ssh"
|
||||||
|
"github.com/quexten/goldwarden/agent/systemauth"
|
||||||
|
"github.com/quexten/goldwarden/agent/systemauth/pinentry"
|
||||||
"github.com/quexten/goldwarden/agent/vault"
|
"github.com/quexten/goldwarden/agent/vault"
|
||||||
"github.com/quexten/goldwarden/ipc/messages"
|
"github.com/quexten/goldwarden/ipc/messages"
|
||||||
"github.com/quexten/goldwarden/logging"
|
"github.com/quexten/goldwarden/logging"
|
||||||
|
|
@ -44,7 +46,7 @@ func writeError(c net.Conn, errMsg error) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveAgentSession(c net.Conn, ctx context.Context, vault *vault.Vault, cfg *config.Config) {
|
func serveAgentSession(c net.Conn, vault *vault.Vault, cfg *config.Config) {
|
||||||
for {
|
for {
|
||||||
buf := make([]byte, 1024*1024)
|
buf := make([]byte, 1024*1024)
|
||||||
nr, err := c.Read(buf)
|
nr, err := c.Read(buf)
|
||||||
|
|
@ -61,7 +63,204 @@ func serveAgentSession(c net.Conn, ctx context.Context, vault *vault.Vault, cfg
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
responseBytes := []byte{}
|
// todo refactor to other file
|
||||||
|
if msg.Type == messages.MessageTypeForEmptyPayload(messages.SessionAuthRequest{}) {
|
||||||
|
log.Info("Received session auth request")
|
||||||
|
req := messages.ParsePayload(msg).(messages.SessionAuthRequest)
|
||||||
|
fmt.Println("Daemontoken", cfg.ConfigFile.RuntimeConfig.DaemonAuthToken)
|
||||||
|
fmt.Println("Token", req.Token)
|
||||||
|
fmt.Println(len(cfg.ConfigFile.RuntimeConfig.DaemonAuthToken), len(req.Token))
|
||||||
|
payload := messages.SessionAuthResponse{
|
||||||
|
Verified: cfg.ConfigFile.RuntimeConfig.DaemonAuthToken == req.Token,
|
||||||
|
}
|
||||||
|
log.Info("Verified: %t", payload.Verified)
|
||||||
|
callingContext := sockets.GetCallingContext(c)
|
||||||
|
systemauth.CreatePinSession(callingContext)
|
||||||
|
|
||||||
|
responsePayload, err := messages.IPCMessageFromPayload(payload)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
payloadBytes, err := json.Marshal(responsePayload)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.Write(payloadBytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed writing to socket " + err.Error())
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo refactor to other file
|
||||||
|
if msg.Type == messages.MessageTypeForEmptyPayload(messages.PinentryRegistrationRequest{}) {
|
||||||
|
log.Info("Received pinentry registration request")
|
||||||
|
|
||||||
|
getPasswordChan := make(chan struct {
|
||||||
|
title string
|
||||||
|
description string
|
||||||
|
})
|
||||||
|
getPasswordReturnChan := make(chan struct {
|
||||||
|
password string
|
||||||
|
err error
|
||||||
|
})
|
||||||
|
getApprovalChan := make(chan struct {
|
||||||
|
title string
|
||||||
|
description string
|
||||||
|
})
|
||||||
|
getApprovalReturnChan := make(chan struct {
|
||||||
|
approved bool
|
||||||
|
err error
|
||||||
|
})
|
||||||
|
|
||||||
|
pe := pinentry.Pinentry{
|
||||||
|
GetPassword: func(title string, description string) (string, error) {
|
||||||
|
getPasswordChan <- struct {
|
||||||
|
title string
|
||||||
|
description string
|
||||||
|
}{title, description}
|
||||||
|
returnValue := <-getPasswordReturnChan
|
||||||
|
return returnValue.password, returnValue.err
|
||||||
|
},
|
||||||
|
GetApproval: func(title string, description string) (bool, error) {
|
||||||
|
getApprovalChan <- struct {
|
||||||
|
title string
|
||||||
|
description string
|
||||||
|
}{title, description}
|
||||||
|
returnValue := <-getApprovalReturnChan
|
||||||
|
return returnValue.approved, returnValue.err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pinnentrySetError := pinentry.SetExternalPinentry(pe)
|
||||||
|
payload := messages.PinentryRegistrationResponse{
|
||||||
|
Success: pinnentrySetError == nil,
|
||||||
|
}
|
||||||
|
log.Info("Pinentry registration success: %t", payload.Success)
|
||||||
|
|
||||||
|
responsePayload, err := messages.IPCMessageFromPayload(payload)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
payloadBytes, err := json.Marshal(responsePayload)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.Write(payloadBytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed writing to socket " + err.Error())
|
||||||
|
}
|
||||||
|
_, err = c.Write([]byte("\n"))
|
||||||
|
time.Sleep(50 * time.Millisecond) //todo fix properly
|
||||||
|
|
||||||
|
if pinnentrySetError != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
fmt.Println("Waiting for pinentry request")
|
||||||
|
select {
|
||||||
|
case getPasswordRequest := <-getPasswordChan:
|
||||||
|
log.Info("Received getPassword request")
|
||||||
|
payload := messages.PinentryPinRequest{
|
||||||
|
Message: getPasswordRequest.description,
|
||||||
|
}
|
||||||
|
payloadPayload, err := messages.IPCMessageFromPayload(payload)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadBytes, err := json.Marshal(payloadPayload)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.Write(payloadBytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed writing to socket " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 1024*1024)
|
||||||
|
nr, err := c.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := buf[0:nr]
|
||||||
|
|
||||||
|
var msg messages.IPCMessage
|
||||||
|
err = json.Unmarshal(data, &msg)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Type == messages.MessageTypeForEmptyPayload(messages.PinentryPinResponse{}) {
|
||||||
|
getPasswordResponse := messages.ParsePayload(msg).(messages.PinentryPinResponse)
|
||||||
|
getPasswordReturnChan <- struct {
|
||||||
|
password string
|
||||||
|
err error
|
||||||
|
}{getPasswordResponse.Pin, nil}
|
||||||
|
}
|
||||||
|
case getApprovalRequest := <-getApprovalChan:
|
||||||
|
log.Info("Received getApproval request")
|
||||||
|
payload := messages.PinentryApprovalRequest{
|
||||||
|
Message: getApprovalRequest.description,
|
||||||
|
}
|
||||||
|
payloadPayload, err := messages.IPCMessageFromPayload(payload)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadBytes, err := json.Marshal(payloadPayload)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.Write(payloadBytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed writing to socket " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 1024*1024)
|
||||||
|
nr, err := c.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := buf[0:nr]
|
||||||
|
|
||||||
|
var msg messages.IPCMessage
|
||||||
|
err = json.Unmarshal(data, &msg)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Type == messages.MessageTypeForEmptyPayload(messages.PinentryApprovalResponse{}) {
|
||||||
|
getApprovalResponse := messages.ParsePayload(msg).(messages.PinentryApprovalResponse)
|
||||||
|
getApprovalReturnChan <- struct {
|
||||||
|
approved bool
|
||||||
|
err error
|
||||||
|
}{getApprovalResponse.Approved, nil}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseBytes []byte
|
||||||
if action, actionFound := actions.AgentActionsRegistry.Get(msg.Type); actionFound {
|
if action, actionFound := actions.AgentActionsRegistry.Get(msg.Type); actionFound {
|
||||||
callingContext := sockets.GetCallingContext(c)
|
callingContext := sockets.GetCallingContext(c)
|
||||||
payload, err := action(msg, cfg, vault, &callingContext)
|
payload, err := action(msg, cfg, vault, &callingContext)
|
||||||
|
|
@ -291,7 +490,7 @@ func StartUnixAgent(path string, runtimeConfig config.RuntimeConfig) error {
|
||||||
|
|
||||||
l, err := net.Listen("unix", path)
|
l, err := net.Listen("unix", path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
println("listen error", err.Error())
|
fmt.Println("listen error", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer l.Close()
|
defer l.Close()
|
||||||
|
|
@ -301,10 +500,10 @@ func StartUnixAgent(path string, runtimeConfig config.RuntimeConfig) error {
|
||||||
for {
|
for {
|
||||||
fd, err := l.Accept()
|
fd, err := l.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
println("accept error", err.Error())
|
fmt.Println("accept error", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
go serveAgentSession(fd, ctx, vault, &cfg)
|
go serveAgentSession(fd, vault, &cfg)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os/user"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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/processsecurity"
|
|
||||||
"github.com/quexten/goldwarden/agent/sockets"
|
|
||||||
"github.com/quexten/goldwarden/agent/vault"
|
|
||||||
"github.com/quexten/goldwarden/ipc/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
func writeErrorToLog(err error) {
|
|
||||||
log.Error(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveVirtualAgent(recv chan []byte, send chan []byte, ctx context.Context, vault *vault.Vault, cfg *config.Config) {
|
|
||||||
for {
|
|
||||||
data := <-recv
|
|
||||||
|
|
||||||
var msg messages.IPCMessage
|
|
||||||
err := json.Unmarshal(data, &msg)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorToLog(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
responseBytes := []byte{}
|
|
||||||
if action, actionFound := actions.AgentActionsRegistry.Get(msg.Type); actionFound {
|
|
||||||
user, _ := user.Current()
|
|
||||||
process := "goldwarden"
|
|
||||||
parent := "SINGLE_PROC_MODE"
|
|
||||||
grandparent := "SINGLE_PROC_MODE"
|
|
||||||
callingContext := sockets.CallingContext{
|
|
||||||
UserName: user.Name,
|
|
||||||
ProcessName: process,
|
|
||||||
ParentProcessName: parent,
|
|
||||||
GrandParentProcessName: grandparent,
|
|
||||||
}
|
|
||||||
payload, err := action(msg, cfg, vault, &callingContext)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorToLog(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
responseBytes, err = json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorToLog(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
payload := messages.ActionResponse{
|
|
||||||
Success: false,
|
|
||||||
Message: "Action not found",
|
|
||||||
}
|
|
||||||
payloadBytes, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorToLog(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
responseBytes = payloadBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
send <- responseBytes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartVirtualAgent(runtimeConfig config.RuntimeConfig) (chan []byte, chan []byte) {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
var keyring crypto.Keyring
|
|
||||||
if runtimeConfig.UseMemguard {
|
|
||||||
keyring = crypto.NewMemguardKeyring(nil)
|
|
||||||
} else {
|
|
||||||
keyring = crypto.NewMemoryKeyring(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
var vault = vault.NewVault(&keyring)
|
|
||||||
cfg, err := config.ReadConfig(runtimeConfig)
|
|
||||||
if err != nil {
|
|
||||||
var cfg = config.DefaultConfig(runtimeConfig.UseMemguard)
|
|
||||||
cfg.WriteConfig()
|
|
||||||
}
|
|
||||||
cfg.ConfigFile.RuntimeConfig = runtimeConfig
|
|
||||||
if cfg.ConfigFile.RuntimeConfig.ApiURI != "" {
|
|
||||||
cfg.ConfigFile.ApiUrl = cfg.ConfigFile.RuntimeConfig.ApiURI
|
|
||||||
}
|
|
||||||
if cfg.ConfigFile.RuntimeConfig.IdentityURI != "" {
|
|
||||||
cfg.ConfigFile.IdentityUrl = cfg.ConfigFile.RuntimeConfig.IdentityURI
|
|
||||||
}
|
|
||||||
if cfg.ConfigFile.RuntimeConfig.NotificationsURI != "" {
|
|
||||||
cfg.ConfigFile.NotificationsUrl = cfg.ConfigFile.RuntimeConfig.NotificationsURI
|
|
||||||
}
|
|
||||||
if cfg.ConfigFile.RuntimeConfig.DeviceUUID != "" {
|
|
||||||
cfg.ConfigFile.DeviceUUID = cfg.ConfigFile.RuntimeConfig.DeviceUUID
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cfg.IsLocked() && !cfg.ConfigFile.RuntimeConfig.DoNotPersistConfig {
|
|
||||||
log.Warn("Config is not locked. 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)
|
|
||||||
}
|
|
||||||
var protectedUserSymetricKey crypto.SymmetricEncryptionKey
|
|
||||||
if keyring.IsMemguard {
|
|
||||||
protectedUserSymetricKey, err = crypto.MemguardSymmetricEncryptionKeyFromBytes(userSymmetricKey)
|
|
||||||
} else {
|
|
||||||
protectedUserSymetricKey, err = crypto.MemorySymmetricEncryptionKeyFromBytes(userSymmetricKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = bitwarden.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, &protectedUserSymetricKey, true)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
processsecurity.DisableDumpable()
|
|
||||||
err = processsecurity.MonitorLocks(func() {
|
|
||||||
cfg.Lock()
|
|
||||||
vault.Clear()
|
|
||||||
vault.Keyring.Lock()
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not monitor screensaver: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
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.DoFullSync(context.WithValue(ctx, bitwarden.AuthToken{}, token.AccessToken), vault, &cfg, nil, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
recv := make(chan []byte)
|
|
||||||
send := make(chan []byte)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
go serveVirtualAgent(recv, send, ctx, vault, &cfg)
|
|
||||||
}()
|
|
||||||
return recv, send
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
type Client interface {
|
|
||||||
SendToAgent(request interface{}) (interface{}, error)
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@ package client
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/quexten/goldwarden/agent/config"
|
"github.com/quexten/goldwarden/agent/config"
|
||||||
|
|
@ -16,13 +15,17 @@ type UnixSocketClient struct {
|
||||||
runtimeConfig *config.RuntimeConfig
|
runtimeConfig *config.RuntimeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UnixSocketConnection struct {
|
||||||
|
conn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
func NewUnixSocketClient(runtimeConfig *config.RuntimeConfig) UnixSocketClient {
|
func NewUnixSocketClient(runtimeConfig *config.RuntimeConfig) UnixSocketClient {
|
||||||
return UnixSocketClient{
|
return UnixSocketClient{
|
||||||
runtimeConfig: runtimeConfig,
|
runtimeConfig: runtimeConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func reader(r io.Reader) interface{} {
|
func Reader(r io.Reader) interface{} {
|
||||||
buf := make([]byte, READ_BUFFER)
|
buf := make([]byte, READ_BUFFER)
|
||||||
for {
|
for {
|
||||||
n, err := r.Read(buf[:])
|
n, err := r.Read(buf[:])
|
||||||
|
|
@ -40,25 +43,49 @@ func reader(r io.Reader) interface{} {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client UnixSocketClient) SendToAgent(request interface{}) (interface{}, error) {
|
func (client UnixSocketClient) SendToAgent(request interface{}) (interface{}, error) {
|
||||||
c, err := net.Dial("unix", client.runtimeConfig.GoldwardenSocketPath)
|
c, err := client.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
return c.SendCommand(request)
|
||||||
message, err := messages.IPCMessageFromPayload(request)
|
}
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
func (client UnixSocketClient) Connect() (UnixSocketConnection, error) {
|
||||||
}
|
c, err := net.Dial("unix", client.runtimeConfig.GoldwardenSocketPath)
|
||||||
messageJson, err := json.Marshal(message)
|
if err != nil {
|
||||||
if err != nil {
|
return UnixSocketConnection{}, err
|
||||||
panic(err)
|
}
|
||||||
}
|
return UnixSocketConnection{conn: c}, nil
|
||||||
|
}
|
||||||
_, err = c.Write(messageJson)
|
|
||||||
if err != nil {
|
func (conn UnixSocketConnection) SendCommand(request interface{}) (interface{}, error) {
|
||||||
log.Fatal("write error:", err)
|
err := conn.WriteMessage(request)
|
||||||
}
|
if err != nil {
|
||||||
result := reader(c)
|
return nil, err
|
||||||
return messages.ParsePayload(result.(messages.IPCMessage)), nil
|
}
|
||||||
|
return conn.ReadMessage(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn UnixSocketConnection) ReadMessage() interface{} {
|
||||||
|
result := Reader(conn.conn)
|
||||||
|
payload := messages.ParsePayload(result.(messages.IPCMessage))
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn UnixSocketConnection) WriteMessage(message interface{}) error {
|
||||||
|
messagePacket, err := messages.IPCMessageFromPayload(message)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
messageJson, err := json.Marshal(messagePacket)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = conn.conn.Write(messageJson)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn UnixSocketConnection) Close() {
|
||||||
|
conn.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/quexten/goldwarden/ipc/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewVirtualClient(recv chan []byte, send chan []byte) VirtualClient {
|
|
||||||
return VirtualClient{
|
|
||||||
recv,
|
|
||||||
send,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type VirtualClient struct {
|
|
||||||
recv chan []byte
|
|
||||||
send chan []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func virtualReader(recv chan []byte) interface{} {
|
|
||||||
for {
|
|
||||||
var message messages.IPCMessage
|
|
||||||
err := json.Unmarshal(<-recv, &message)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client VirtualClient) SendToAgent(request interface{}) (interface{}, error) {
|
|
||||||
message, err := messages.IPCMessageFromPayload(request)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
messageJson, err := json.Marshal(message)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client.send <- messageJson
|
|
||||||
result := virtualReader(client.recv)
|
|
||||||
return messages.ParsePayload(result.(messages.IPCMessage)), nil
|
|
||||||
}
|
|
||||||
|
|
@ -10,9 +10,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var autofillCmd = &cobra.Command{
|
var autofillCmd = &cobra.Command{
|
||||||
Use: "autotype",
|
Hidden: true,
|
||||||
Short: "Autotype credentials",
|
Use: "autotype",
|
||||||
Long: `Autotype credentials`,
|
Short: "Autotype credentials",
|
||||||
|
Long: `Autotype credentials`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
username, _ := cmd.Flags().GetString("username")
|
username, _ := cmd.Flags().GetString("username")
|
||||||
// get pasword from env
|
// get pasword from env
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -30,12 +31,12 @@ var setApiUrlCmd = &cobra.Command{
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
if result.(messages.ActionResponse).Success {
|
if result.(messages.ActionResponse).Success {
|
||||||
println("Done")
|
fmt.Println("Done")
|
||||||
} else {
|
} else {
|
||||||
println("Setting api url failed: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Setting api url failed: " + result.(messages.ActionResponse).Message)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
println("Wrong IPC response type")
|
fmt.Println("Wrong IPC response type")
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -63,12 +64,12 @@ var setIdentityURLCmd = &cobra.Command{
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
if result.(messages.ActionResponse).Success {
|
if result.(messages.ActionResponse).Success {
|
||||||
println("Done")
|
fmt.Println("Done")
|
||||||
} else {
|
} else {
|
||||||
println("Setting identity url failed: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Setting identity url failed: " + result.(messages.ActionResponse).Message)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
println("Wrong IPC response type")
|
fmt.Println("Wrong IPC response type")
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -96,12 +97,12 @@ var setNotificationsURLCmd = &cobra.Command{
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
if result.(messages.ActionResponse).Success {
|
if result.(messages.ActionResponse).Success {
|
||||||
println("Done")
|
fmt.Println("Done")
|
||||||
} else {
|
} else {
|
||||||
println("Setting notifications url failed: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Setting notifications url failed: " + result.(messages.ActionResponse).Message)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
println("Wrong IPC response type")
|
fmt.Println("Wrong IPC response type")
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -129,12 +130,12 @@ var setVaultURLCmd = &cobra.Command{
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
if result.(messages.ActionResponse).Success {
|
if result.(messages.ActionResponse).Success {
|
||||||
println("Done")
|
fmt.Println("Done")
|
||||||
} else {
|
} else {
|
||||||
println("Setting vault url failed: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Setting vault url failed: " + result.(messages.ActionResponse).Message)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
println("Wrong IPC response type")
|
fmt.Println("Wrong IPC response type")
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -162,12 +163,12 @@ var setURLsAutomaticallyCmd = &cobra.Command{
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
if result.(messages.ActionResponse).Success {
|
if result.(messages.ActionResponse).Success {
|
||||||
println("Done")
|
fmt.Println("Done")
|
||||||
} else {
|
} else {
|
||||||
println("Setting urls automatically failed: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Setting urls automatically failed: " + result.(messages.ActionResponse).Message)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
println("Wrong IPC response type")
|
fmt.Println("Wrong IPC response type")
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -188,14 +189,15 @@ var getEnvironmentCmd = &cobra.Command{
|
||||||
|
|
||||||
switch result := result.(type) {
|
switch result := result.(type) {
|
||||||
case messages.GetConfigEnvironmentResponse:
|
case messages.GetConfigEnvironmentResponse:
|
||||||
fmt.Println("{")
|
response := map[string]string{}
|
||||||
fmt.Println(" \"api\": \"" + result.ApiURL + "\",")
|
response["api"] = result.ApiURL
|
||||||
fmt.Println(" \"identity\": \"" + result.IdentityURL + "\",")
|
response["identity"] = result.IdentityURL
|
||||||
fmt.Println(" \"notifications\": \"" + result.NotificationsURL + "\",")
|
response["notifications"] = result.NotificationsURL
|
||||||
fmt.Println(" \"vault\": \"" + result.VaultURL + "\"")
|
response["vault"] = result.VaultURL
|
||||||
fmt.Println("}")
|
responseJSON, _ := json.Marshal(response)
|
||||||
|
fmt.Println(string(responseJSON))
|
||||||
default:
|
default:
|
||||||
println("Wrong IPC response type")
|
fmt.Println("Wrong IPC response type")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -226,12 +228,12 @@ var setApiClientIDCmd = &cobra.Command{
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
if result.(messages.ActionResponse).Success {
|
if result.(messages.ActionResponse).Success {
|
||||||
println("Done")
|
fmt.Println("Done")
|
||||||
} else {
|
} else {
|
||||||
println("Setting api client id failed: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Setting api client id failed: " + result.(messages.ActionResponse).Message)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
println("Wrong IPC response type")
|
fmt.Println("Wrong IPC response type")
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -263,12 +265,12 @@ var setApiSecretCmd = &cobra.Command{
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
if result.(messages.ActionResponse).Success {
|
if result.(messages.ActionResponse).Success {
|
||||||
println("Done")
|
fmt.Println("Done")
|
||||||
} else {
|
} else {
|
||||||
println("Setting api secret failed: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Setting api secret failed: " + result.(messages.ActionResponse).Message)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
println("Wrong IPC response type")
|
fmt.Println("Wrong IPC response type")
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -289,13 +291,14 @@ var getRuntimeConfigCmd = &cobra.Command{
|
||||||
|
|
||||||
switch result := result.(type) {
|
switch result := result.(type) {
|
||||||
case messages.GetRuntimeConfigResponse:
|
case messages.GetRuntimeConfigResponse:
|
||||||
fmt.Println("{")
|
response := map[string]interface{}{}
|
||||||
fmt.Println(" \"useMemguard\": " + fmt.Sprintf("%t", result.UseMemguard) + ",")
|
response["useMemguard"] = result.UseMemguard
|
||||||
fmt.Println(" \"SSHAgentSocketPath\": \"" + result.SSHAgentSocketPath + "\",")
|
response["SSHAgentSocketPath"] = result.SSHAgentSocketPath
|
||||||
fmt.Println(" \"goldwardenSocketPath\": \"" + result.GoldwardenSocketPath + "\"")
|
response["goldwardenSocketPath"] = result.GoldwardenSocketPath
|
||||||
fmt.Println("}")
|
responseJSON, _ := json.Marshal(response)
|
||||||
|
fmt.Println(string(responseJSON))
|
||||||
default:
|
default:
|
||||||
println("Wrong IPC response type")
|
fmt.Println("Wrong IPC response type")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,11 @@ var daemonizeCmd = &cobra.Command{
|
||||||
sshDisabled := runtimeConfig.DisableSSHAgent
|
sshDisabled := runtimeConfig.DisableSSHAgent
|
||||||
|
|
||||||
if websocketDisabled {
|
if websocketDisabled {
|
||||||
println("Websocket disabled")
|
fmt.Println("Websocket disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
if sshDisabled {
|
if sshDisabled {
|
||||||
println("SSH agent disabled")
|
fmt.Println("SSH agent disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
|
|
|
||||||
10
cmd/login.go
10
cmd/login.go
|
|
@ -4,6 +4,8 @@ Copyright © 2023 NAME HERE <EMAIL ADDRESS>
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/quexten/goldwarden/ipc/messages"
|
"github.com/quexten/goldwarden/ipc/messages"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -17,7 +19,7 @@ var loginCmd = &cobra.Command{
|
||||||
request := messages.DoLoginRequest{}
|
request := messages.DoLoginRequest{}
|
||||||
email, _ := cmd.Flags().GetString("email")
|
email, _ := cmd.Flags().GetString("email")
|
||||||
if email == "" {
|
if email == "" {
|
||||||
println("Error: No email specified")
|
fmt.Println("Error: No email specified")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,12 +36,12 @@ var loginCmd = &cobra.Command{
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
if result.(messages.ActionResponse).Success {
|
if result.(messages.ActionResponse).Success {
|
||||||
println("Logged in")
|
fmt.Println("Logged in")
|
||||||
} else {
|
} else {
|
||||||
println("Login failed: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Login failed: " + result.(messages.ActionResponse).Message)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
println("Wrong IPC response type for login")
|
fmt.Println("Wrong IPC response type for login")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ var getLoginCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
println("Error: " + resp.(messages.ActionResponse).Message)
|
fmt.Println("Error: " + resp.(messages.ActionResponse).Message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -72,8 +72,8 @@ var listLoginsCmd = &cobra.Command{
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("[")
|
var toPrintLogins []map[string]string
|
||||||
for index, login := range logins {
|
for _, login := range logins {
|
||||||
data := map[string]string{
|
data := map[string]string{
|
||||||
"name": stringsx.Clean(login.Name),
|
"name": stringsx.Clean(login.Name),
|
||||||
"uuid": stringsx.Clean(login.UUID),
|
"uuid": stringsx.Clean(login.UUID),
|
||||||
|
|
@ -82,23 +82,14 @@ var listLoginsCmd = &cobra.Command{
|
||||||
"totp": stringsx.Clean(login.TOTPSeed),
|
"totp": stringsx.Clean(login.TOTPSeed),
|
||||||
"uri": stringsx.Clean(login.URI),
|
"uri": stringsx.Clean(login.URI),
|
||||||
}
|
}
|
||||||
jsonString, err := json.Marshal(data)
|
toPrintLogins = append(toPrintLogins, data)
|
||||||
if err != nil {
|
|
||||||
handleSendToAgentError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Print(string(jsonString))
|
|
||||||
if index != len(logins)-1 {
|
|
||||||
fmt.Println(",")
|
|
||||||
} else {
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fmt.Println("]")
|
toPrintJSON, _ := json.Marshal(toPrintLogins)
|
||||||
|
fmt.Println(string(toPrintJSON))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListLogins(client client.Client) ([]messages.DecryptedLoginCipher, error) {
|
func ListLogins(client client.UnixSocketClient) ([]messages.DecryptedLoginCipher, error) {
|
||||||
resp, err := client.SendToAgent(messages.ListLoginsRequest{})
|
resp, err := client.SendToAgent(messages.ListLoginsRequest{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []messages.DecryptedLoginCipher{}, err
|
return []messages.DecryptedLoginCipher{}, err
|
||||||
|
|
|
||||||
12
cmd/pin.go
12
cmd/pin.go
|
|
@ -1,6 +1,8 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/quexten/goldwarden/ipc/messages"
|
"github.com/quexten/goldwarden/ipc/messages"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -25,12 +27,12 @@ var setPinCmd = &cobra.Command{
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
if result.(messages.ActionResponse).Success {
|
if result.(messages.ActionResponse).Success {
|
||||||
println("Pin updated")
|
fmt.Println("Pin updated")
|
||||||
} else {
|
} else {
|
||||||
println("Pin updating failed: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Pin updating failed: " + result.(messages.ActionResponse).Message)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
println("Wrong response type")
|
fmt.Println("Wrong response type")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -48,9 +50,9 @@ var pinStatusCmd = &cobra.Command{
|
||||||
|
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
println("Pin status: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Pin status: " + result.(messages.ActionResponse).Message)
|
||||||
default:
|
default:
|
||||||
println("Wrong response type")
|
fmt.Println("Wrong response type")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
cmd/root.go
16
cmd/root.go
|
|
@ -1,16 +1,16 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/quexten/goldwarden/agent"
|
|
||||||
"github.com/quexten/goldwarden/agent/config"
|
"github.com/quexten/goldwarden/agent/config"
|
||||||
"github.com/quexten/goldwarden/client"
|
"github.com/quexten/goldwarden/client"
|
||||||
"github.com/quexten/goldwarden/ipc/messages"
|
"github.com/quexten/goldwarden/ipc/messages"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var commandClient client.Client
|
var commandClient client.UnixSocketClient
|
||||||
var runtimeConfig config.RuntimeConfig
|
var runtimeConfig config.RuntimeConfig
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
|
|
@ -24,13 +24,7 @@ var rootCmd = &cobra.Command{
|
||||||
func Execute(cfg config.RuntimeConfig) {
|
func Execute(cfg config.RuntimeConfig) {
|
||||||
runtimeConfig = cfg
|
runtimeConfig = cfg
|
||||||
|
|
||||||
goldwardenSingleProcess := os.Getenv("GOLDWARDEN_SINGLE_PROCESS")
|
commandClient = client.NewUnixSocketClient(&cfg)
|
||||||
if goldwardenSingleProcess == "true" {
|
|
||||||
recv, send := agent.StartVirtualAgent(runtimeConfig)
|
|
||||||
commandClient = client.NewVirtualClient(send, recv)
|
|
||||||
} else {
|
|
||||||
commandClient = client.NewUnixSocketClient(&cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := rootCmd.Execute()
|
err := rootCmd.Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -62,8 +56,8 @@ func loginIfRequired() error {
|
||||||
|
|
||||||
func handleSendToAgentError(err error) {
|
func handleSendToAgentError(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
println("Error: " + err.Error())
|
fmt.Println("Error: " + err.Error())
|
||||||
println("Is the daemon running?")
|
fmt.Println("Is the daemon running?")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ Copyright © 2023 NAME HERE <EMAIL ADDRESS>
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
|
|
@ -19,7 +20,7 @@ var runCmd = &cobra.Command{
|
||||||
The variables are stored as a secure note. Consult the documentation for more information.`,
|
The variables are stored as a secure note. Consult the documentation for more information.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
println("Error: No command specified")
|
fmt.Println("Error: No command specified")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,7 +44,7 @@ var runCmd = &cobra.Command{
|
||||||
env = append(env, key+"="+value)
|
env = append(env, key+"="+value)
|
||||||
}
|
}
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
println("Error: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Error: " + result.(messages.ActionResponse).Message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ var sendCreateCmd = &cobra.Command{
|
||||||
fmt.Println("Send created: " + result.(messages.CreateSendResponse).URL)
|
fmt.Println("Send created: " + result.(messages.CreateSendResponse).URL)
|
||||||
break
|
break
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
println("Error: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Error: " + result.(messages.ActionResponse).Message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
90
cmd/session.go
Normal file
90
cmd/session.go
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/quexten/goldwarden/ipc/messages"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sessionCmd represents the run command
|
||||||
|
var sessionCmd = &cobra.Command{
|
||||||
|
Use: "session",
|
||||||
|
Hidden: true,
|
||||||
|
Short: "Starts a new session",
|
||||||
|
Long: `Starts a new session.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
for {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
text, _ := reader.ReadString('\n')
|
||||||
|
text = strings.TrimSuffix(text, "\n")
|
||||||
|
args := strings.Split(text, " ")
|
||||||
|
rootCmd.SetArgs(args)
|
||||||
|
rootCmd.Execute()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var pinentry = &cobra.Command{
|
||||||
|
Use: "pinentry",
|
||||||
|
Hidden: true,
|
||||||
|
Short: "Registers as a pinentry program",
|
||||||
|
Long: `Registers as a pinentry program.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
conn, err := commandClient.Connect()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
_, err = conn.SendCommand(messages.PinentryRegistrationRequest{})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
response := conn.ReadMessage()
|
||||||
|
switch response.(type) {
|
||||||
|
case messages.PinentryPinRequest:
|
||||||
|
fmt.Println("pin-request" + "," + response.(messages.PinentryPinRequest).Message)
|
||||||
|
case messages.PinentryApprovalRequest:
|
||||||
|
fmt.Println("approval-request" + "," + response.(messages.PinentryApprovalRequest).Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// read line
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
text, _ := reader.ReadString('\n')
|
||||||
|
text = strings.TrimSuffix(text, "\n")
|
||||||
|
|
||||||
|
switch response.(type) {
|
||||||
|
case messages.PinentryPinRequest:
|
||||||
|
err = conn.WriteMessage(messages.PinentryPinResponse{Pin: text})
|
||||||
|
case messages.PinentryApprovalRequest:
|
||||||
|
err = conn.WriteMessage(messages.PinentryApprovalResponse{Approved: text == "true"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var authenticateSession = &cobra.Command{
|
||||||
|
Use: "authenticate-session",
|
||||||
|
Hidden: true,
|
||||||
|
Short: "Authenticates a session",
|
||||||
|
Long: `Authenticates a session.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
token := args[0]
|
||||||
|
response, err := commandClient.SendToAgent(messages.SessionAuthRequest{Token: token})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(response.(messages.SessionAuthResponse).Verified)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(sessionCmd)
|
||||||
|
rootCmd.AddCommand(pinentry)
|
||||||
|
rootCmd.AddCommand(authenticateSession)
|
||||||
|
}
|
||||||
|
|
@ -47,7 +47,7 @@ var sshAddCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
println("Error: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Error: " + result.(messages.ActionResponse).Message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -74,7 +74,7 @@ var listSSHCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
println("Error: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Error: " + result.(messages.ActionResponse).Message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
40
cmd/vault.go
40
cmd/vault.go
|
|
@ -1,6 +1,7 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -30,12 +31,12 @@ var unlockCmd = &cobra.Command{
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
if result.(messages.ActionResponse).Success {
|
if result.(messages.ActionResponse).Success {
|
||||||
println("Unlocked")
|
fmt.Println("Unlocked")
|
||||||
} else {
|
} else {
|
||||||
println("Not unlocked: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Not unlocked: " + result.(messages.ActionResponse).Message)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
println("Wrong response type")
|
fmt.Println("Wrong response type")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -56,12 +57,12 @@ var lockCmd = &cobra.Command{
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
if result.(messages.ActionResponse).Success {
|
if result.(messages.ActionResponse).Success {
|
||||||
println("Locked")
|
fmt.Println("Locked")
|
||||||
} else {
|
} else {
|
||||||
println("Not locked: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Not locked: " + result.(messages.ActionResponse).Message)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
println("Wrong response type")
|
fmt.Println("Wrong response type")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -82,12 +83,12 @@ var purgeCmd = &cobra.Command{
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.ActionResponse:
|
case messages.ActionResponse:
|
||||||
if result.(messages.ActionResponse).Success {
|
if result.(messages.ActionResponse).Success {
|
||||||
println("Purged")
|
fmt.Println("Purged")
|
||||||
} else {
|
} else {
|
||||||
println("Not purged: " + result.(messages.ActionResponse).Message)
|
fmt.Println("Not purged: " + result.(messages.ActionResponse).Message)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
println("Wrong response type")
|
fmt.Println("Wrong response type")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -107,18 +108,19 @@ var statusCmd = &cobra.Command{
|
||||||
|
|
||||||
switch result.(type) {
|
switch result.(type) {
|
||||||
case messages.VaultStatusResponse:
|
case messages.VaultStatusResponse:
|
||||||
|
response := map[string]interface{}{}
|
||||||
status := result.(messages.VaultStatusResponse)
|
status := result.(messages.VaultStatusResponse)
|
||||||
fmt.Println("{")
|
response["locked"] = status.Locked
|
||||||
fmt.Println(" \"locked\":", status.Locked, ",")
|
response["loginEntries"] = status.NumberOfLogins
|
||||||
fmt.Println(" \"loginEntries\":", status.NumberOfLogins, ",")
|
response["noteEntries"] = status.NumberOfNotes
|
||||||
fmt.Println(" \"noteEntries\":", status.NumberOfNotes, ",")
|
response["lastSynced"] = time.Unix(status.LastSynced, 0).String()
|
||||||
fmt.Println(" \"lastSynced\": \"" + time.Unix(status.LastSynced, 0).String() + "\",")
|
response["websocketConnected"] = status.WebsockedConnected
|
||||||
fmt.Println(" \"websocketConnected\":", status.WebsockedConnected, ",")
|
response["pinSet"] = status.PinSet
|
||||||
fmt.Println(" \"pinSet\":", status.PinSet, ",")
|
response["loggedIn"] = status.LoggedIn
|
||||||
fmt.Println(" \"loggedIn\":", status.LoggedIn)
|
responseJSON, _ := json.Marshal(response)
|
||||||
fmt.Println("}")
|
fmt.Println(string(responseJSON))
|
||||||
default:
|
default:
|
||||||
println("Wrong response type")
|
fmt.Println("Wrong response type")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ id: com.quexten.Goldwarden
|
||||||
runtime: org.gnome.Platform
|
runtime: org.gnome.Platform
|
||||||
runtime-version: '45'
|
runtime-version: '45'
|
||||||
sdk: org.gnome.Sdk
|
sdk: org.gnome.Sdk
|
||||||
command: main.py
|
command: goldwarden_ui_main.py
|
||||||
finish-args:
|
finish-args:
|
||||||
# Allow network access for sync
|
# Allow network access for sync
|
||||||
- --share=network
|
- --share=network
|
||||||
|
|
@ -28,7 +28,7 @@ finish-args:
|
||||||
# biometric / user password auth
|
# biometric / user password auth
|
||||||
- --system-talk-name=org.freedesktop.PolicyKit1
|
- --system-talk-name=org.freedesktop.PolicyKit1
|
||||||
modules:
|
modules:
|
||||||
- ./ui/python3-requirements.json
|
- ./gui/python3-requirements.json
|
||||||
- name: wl-clipboard
|
- name: wl-clipboard
|
||||||
buildsystem: meson
|
buildsystem: meson
|
||||||
config-opts:
|
config-opts:
|
||||||
|
|
@ -40,18 +40,18 @@ modules:
|
||||||
- name: goldwarden-python-ui
|
- name: goldwarden-python-ui
|
||||||
buildsystem: simple
|
buildsystem: simple
|
||||||
build-commands:
|
build-commands:
|
||||||
- cp -R ./ui/* /app/bin
|
- cp -R ./gui/* /app/bin
|
||||||
- chmod +x /app/bin/main.py
|
- chmod +x /app/bin/goldwarden_ui_main.py
|
||||||
- install -D ./ui/com.quexten.Goldwarden.desktop /app/share/applications/com.quexten.Goldwarden.desktop
|
- install -D ./gui/com.quexten.Goldwarden.desktop /app/share/applications/com.quexten.Goldwarden.desktop
|
||||||
- install -D ./ui/goldwarden.svg /app/share/icons/hicolor/scalable/apps/com.quexten.Goldwarden.svg
|
- install -D ./gui/goldwarden.svg /app/share/icons/hicolor/scalable/apps/com.quexten.Goldwarden.svg
|
||||||
- install -Dm644 ./ui/com.quexten.Goldwarden.metainfo.xml -t /app/share/metainfo/
|
- install -Dm644 ./gui/com.quexten.Goldwarden.metainfo.xml -t /app/share/metainfo/
|
||||||
sources:
|
sources:
|
||||||
- type: dir
|
- type: dir
|
||||||
path: ./
|
path: ./
|
||||||
- name: goldwarden-core-daemon
|
- name: goldwarden-core-daemon
|
||||||
buildsystem: simple
|
buildsystem: simple
|
||||||
build-commands:
|
build-commands:
|
||||||
- install -D goldwarden /app/bin/goldwarden
|
- install -D goldwarden /app/bin/src/goldwarden
|
||||||
sources:
|
sources:
|
||||||
- type: file
|
- type: file
|
||||||
path: ./goldwarden
|
path: ./goldwarden
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
Name=Goldwarden
|
Name=Goldwarden
|
||||||
Comment=A Bitwarden compatible desktop password manager
|
Comment=A Bitwarden compatible desktop password manager
|
||||||
Keywords=password;ssh;auto-type;keys;
|
Keywords=password;ssh;auto-type;keys;
|
||||||
Exec=main.py
|
Exec=goldwarden_ui_main.py
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Icon=com.quexten.Goldwarden
|
Icon=com.quexten.Goldwarden
|
||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
4
gui/goldwarden_ui_main.py
Normal file
4
gui/goldwarden_ui_main.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import src.linux.main as linux_main
|
||||||
|
|
||||||
|
linux_main.main()
|
||||||
14
gui/python3-requirements.json
Normal file
14
gui/python3-requirements.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "python3-tendo",
|
||||||
|
"buildsystem": "simple",
|
||||||
|
"build-commands": [
|
||||||
|
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"tendo==0.3.0\" --no-build-isolation"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://files.pythonhosted.org/packages/ce/3f/761077d55732b0b1a673b15d4fdaa947a7c1eb5c9a23b7142df557019823/tendo-0.3.0-py3-none-any.whl",
|
||||||
|
"sha256": "026b70b355ea4c9da7c2123fa2d5c280c8983c1b34e329ff49260e2e78b93be7"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1
gui/requirements.txt
Normal file
1
gui/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
tendo==0.3.0
|
||||||
69
gui/src/gui/pinentry.py
Normal file
69
gui/src/gui/pinentry.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '4.0')
|
||||||
|
gi.require_version('Adw', '1')
|
||||||
|
import gc
|
||||||
|
import time
|
||||||
|
from gi.repository import Gtk, Adw, GLib, Notify, Gdk
|
||||||
|
from threading import Thread
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
message = sys.stdin.readline()
|
||||||
|
|
||||||
|
class MyApp(Adw.Application):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.connect('activate', self.on_activate)
|
||||||
|
|
||||||
|
def on_activate(self, app):
|
||||||
|
self.pinentry_window = MainWindow(application=app)
|
||||||
|
self.pinentry_window.present()
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
class MainWindow(Gtk.ApplicationWindow):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.stack = Gtk.Stack()
|
||||||
|
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
|
||||||
|
self.set_child(self.stack)
|
||||||
|
|
||||||
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||||
|
self.stack.add_child(box)
|
||||||
|
|
||||||
|
label = Gtk.Label(label=message)
|
||||||
|
box.append(label)
|
||||||
|
|
||||||
|
# Create the password entry
|
||||||
|
self.password_entry = Gtk.Entry()
|
||||||
|
self.password_entry.set_placeholder_text("Enter your password")
|
||||||
|
self.password_entry.set_visibility(False) # Hide the password
|
||||||
|
box.append(self.password_entry)
|
||||||
|
|
||||||
|
# Create a button box for cancel and approve buttons
|
||||||
|
button_box = Gtk.Box(spacing=6)
|
||||||
|
box.append(button_box)
|
||||||
|
|
||||||
|
# Cancel button
|
||||||
|
cancel_button = Gtk.Button(label="Cancel")
|
||||||
|
cancel_button.set_hexpand(True) # Make the button expand horizontally
|
||||||
|
def on_cancel_button_clicked(button):
|
||||||
|
print("", flush=True)
|
||||||
|
os._exit(0)
|
||||||
|
cancel_button.connect("clicked", on_cancel_button_clicked)
|
||||||
|
button_box.append(cancel_button)
|
||||||
|
|
||||||
|
# Approve button
|
||||||
|
approve_button = Gtk.Button(label="Approve")
|
||||||
|
approve_button.set_hexpand(True) # Make the button expand horizontally
|
||||||
|
def on_approve_button_clicked(button):
|
||||||
|
print(self.password_entry.get_text(), flush=True)
|
||||||
|
os._exit(0)
|
||||||
|
approve_button.connect("clicked", on_approve_button_clicked)
|
||||||
|
button_box.append(approve_button)
|
||||||
|
|
||||||
|
self.set_default_size(700, 200)
|
||||||
|
self.set_title("Goldwarden Pinentry")
|
||||||
|
|
||||||
|
app = MyApp(application_id="com.quexten.Goldwarden.pinentry")
|
||||||
|
app.run(sys.argv)
|
||||||
63
gui/src/gui/pinentry_approval.py
Normal file
63
gui/src/gui/pinentry_approval.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '4.0')
|
||||||
|
gi.require_version('Adw', '1')
|
||||||
|
import gc
|
||||||
|
import time
|
||||||
|
from gi.repository import Gtk, Adw, GLib, Notify, Gdk
|
||||||
|
from threading import Thread
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
message = sys.stdin.readline()
|
||||||
|
|
||||||
|
class MyApp(Adw.Application):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.connect('activate', self.on_activate)
|
||||||
|
|
||||||
|
def on_activate(self, app):
|
||||||
|
self.pinentry_window = MainWindow(application=app)
|
||||||
|
self.pinentry_window.present()
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
class MainWindow(Gtk.ApplicationWindow):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.stack = Gtk.Stack()
|
||||||
|
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
|
||||||
|
self.set_child(self.stack)
|
||||||
|
|
||||||
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||||
|
self.stack.add_child(box)
|
||||||
|
|
||||||
|
label = Gtk.Label(label=message)
|
||||||
|
box.append(label)
|
||||||
|
|
||||||
|
# Create a button box for cancel and approve buttons
|
||||||
|
button_box = Gtk.Box(spacing=6)
|
||||||
|
box.append(button_box)
|
||||||
|
|
||||||
|
# Cancel button
|
||||||
|
cancel_button = Gtk.Button(label="Cancel")
|
||||||
|
cancel_button.set_hexpand(True) # Make the button expand horizontally
|
||||||
|
def on_cancel_button_clicked(button):
|
||||||
|
print("false", flush=True)
|
||||||
|
os._exit(0)
|
||||||
|
cancel_button.connect("clicked", on_cancel_button_clicked)
|
||||||
|
button_box.append(cancel_button)
|
||||||
|
|
||||||
|
# Approve button
|
||||||
|
approve_button = Gtk.Button(label="Approve")
|
||||||
|
approve_button.set_hexpand(True) # Make the button expand horizontally
|
||||||
|
def on_approve_button_clicked(button):
|
||||||
|
print("true", flush=True)
|
||||||
|
os._exit(0)
|
||||||
|
approve_button.connect("clicked", on_approve_button_clicked)
|
||||||
|
button_box.append(approve_button)
|
||||||
|
|
||||||
|
self.set_default_size(700, 200)
|
||||||
|
self.set_title("Goldwarden Approval")
|
||||||
|
|
||||||
|
app = MyApp(application_id="com.quexten.Goldwarden.pinentry")
|
||||||
|
app.run(sys.argv)
|
||||||
|
|
@ -4,28 +4,36 @@ gi.require_version('Adw', '1')
|
||||||
import gc
|
import gc
|
||||||
import time
|
import time
|
||||||
from gi.repository import Gtk, Adw, GLib, Notify, Gdk
|
from gi.repository import Gtk, Adw, GLib, Notify, Gdk
|
||||||
import goldwarden
|
from ..services import goldwarden
|
||||||
import clipboard
|
from ..linux import clipboard
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import totp
|
from ..services import totp
|
||||||
Notify.init("Goldwarden")
|
Notify.init("Goldwarden")
|
||||||
|
|
||||||
|
token = sys.stdin.readline()
|
||||||
|
goldwarden.create_authenticated_connection(token)
|
||||||
|
|
||||||
class MyApp(Adw.Application):
|
class MyApp(Adw.Application):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.connect('activate', self.on_activate)
|
self.connect('activate', self.on_activate)
|
||||||
|
|
||||||
def on_activate(self, app):
|
def update_logins(self):
|
||||||
self.autofill_window = MainWindow(application=app)
|
|
||||||
self.autofill_window.logins = []
|
|
||||||
self.autofill_window.present()
|
|
||||||
logins = goldwarden.get_vault_logins()
|
logins = goldwarden.get_vault_logins()
|
||||||
if logins == None:
|
if logins == None:
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
return
|
return
|
||||||
app.autofill_window.logins = logins
|
self.app.autofill_window.logins = logins
|
||||||
|
|
||||||
|
def on_activate(self, app):
|
||||||
|
self.autofill_window = MainWindow(application=app)
|
||||||
|
self.autofill_window.logins = []
|
||||||
|
self.autofill_window.present()
|
||||||
|
self.app = app
|
||||||
|
thread = Thread(target=self.update_logins)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
class MainWindow(Gtk.ApplicationWindow):
|
class MainWindow(Gtk.ApplicationWindow):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
@ -85,6 +93,9 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||||
|
|
||||||
keycont = Gtk.EventControllerKey()
|
keycont = Gtk.EventControllerKey()
|
||||||
def handle_keypress(cotroller, keyval, keycode, state, user_data):
|
def handle_keypress(cotroller, keyval, keycode, state, user_data):
|
||||||
|
# if ctrl is pressed
|
||||||
|
if state == 4:
|
||||||
|
print("ctrl")
|
||||||
if keycode == 36:
|
if keycode == 36:
|
||||||
print("enter")
|
print("enter")
|
||||||
self.hide()
|
self.hide()
|
||||||
|
|
@ -130,7 +141,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||||
self.history_list.get_style_context().add_class("boxed-list")
|
self.history_list.get_style_context().add_class("boxed-list")
|
||||||
self.box.append(self.history_list)
|
self.box.append(self.history_list)
|
||||||
self.set_default_size(700, 700)
|
self.set_default_size(700, 700)
|
||||||
self.set_title("Goldwarden")
|
self.set_title("Goldwarden Quick Access")
|
||||||
|
|
||||||
app = MyApp(application_id="com.quexten.Goldwarden.autofill-menu")
|
app = MyApp(application_id="com.quexten.Goldwarden.autofill-menu")
|
||||||
app.run(sys.argv)
|
app.run(sys.argv)
|
||||||
|
|
@ -6,12 +6,16 @@ gi.require_version('Adw', '1')
|
||||||
import gc
|
import gc
|
||||||
|
|
||||||
from gi.repository import Gtk, Adw, GLib, Gdk
|
from gi.repository import Gtk, Adw, GLib, Gdk
|
||||||
import goldwarden
|
from ..services import goldwarden
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import subprocess
|
import subprocess
|
||||||
import components
|
from . import components
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
root_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir))
|
||||||
|
token = sys.stdin.readline()
|
||||||
|
goldwarden.create_authenticated_connection(None)
|
||||||
|
|
||||||
class SettingsWinvdow(Gtk.ApplicationWindow):
|
class SettingsWinvdow(Gtk.ApplicationWindow):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
@ -46,7 +50,11 @@ class SettingsWinvdow(Gtk.ApplicationWindow):
|
||||||
self.autotype_button = Gtk.Button()
|
self.autotype_button = Gtk.Button()
|
||||||
self.autotype_button.set_label("Quick Access")
|
self.autotype_button.set_label("Quick Access")
|
||||||
self.autotype_button.set_margin_top(10)
|
self.autotype_button.set_margin_top(10)
|
||||||
self.autotype_button.connect("clicked", lambda button: subprocess.Popen(["python3", "/app/bin/autofill.py"], start_new_session=True))
|
def quickaccess_button_clicked():
|
||||||
|
p = subprocess.Popen(["python3", "-m", "src.gui.quickaccess"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=root_path, start_new_session=True)
|
||||||
|
p.stdin.write(f"{token}\n".encode())
|
||||||
|
p.stdin.flush()
|
||||||
|
self.autotype_button.connect("clicked", lambda button: quickaccess_button_clicked())
|
||||||
self.autotype_button.get_style_context().add_class("suggested-action")
|
self.autotype_button.get_style_context().add_class("suggested-action")
|
||||||
self.action_preferences_group.add(self.autotype_button)
|
self.action_preferences_group.add(self.autotype_button)
|
||||||
|
|
||||||
|
|
@ -201,6 +209,7 @@ class SettingsWinvdow(Gtk.ApplicationWindow):
|
||||||
|
|
||||||
pin_set = goldwarden.is_pin_enabled()
|
pin_set = goldwarden.is_pin_enabled()
|
||||||
status = goldwarden.get_vault_status()
|
status = goldwarden.get_vault_status()
|
||||||
|
print("status", status)
|
||||||
runtimeCfg = goldwarden.get_runtime_config()
|
runtimeCfg = goldwarden.get_runtime_config()
|
||||||
if runtimeCfg != None:
|
if runtimeCfg != None:
|
||||||
self.ssh_row.set_subtitle("Listening at "+runtimeCfg["SSHAgentSocketPath"])
|
self.ssh_row.set_subtitle("Listening at "+runtimeCfg["SSHAgentSocketPath"])
|
||||||
55
gui/src/linux/main.py
Normal file
55
gui/src/linux/main.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
from tendo import singleton
|
||||||
|
from .monitors import dbus_autofill_monitor
|
||||||
|
from .monitors import dbus_monitor
|
||||||
|
import sys
|
||||||
|
from src.services import goldwarden
|
||||||
|
from src.services import pinentry
|
||||||
|
from threading import Thread
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
root_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir))
|
||||||
|
is_flatpak = os.path.exists("/.flatpak-info")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
token = secrets.token_hex(32)
|
||||||
|
if not os.environ.get("GOLDWARDEN_DAEMON_AUTH_TOKEN") == None:
|
||||||
|
token = os.environ["GOLDWARDEN_DAEMON_AUTH_TOKEN"]
|
||||||
|
|
||||||
|
# check if already running
|
||||||
|
try:
|
||||||
|
me = singleton.SingleInstance()
|
||||||
|
except:
|
||||||
|
import dbus
|
||||||
|
|
||||||
|
bus = dbus.SessionBus()
|
||||||
|
the_object = bus.get_object("com.quexten.Goldwarden.gui", "/com/quexten/Goldwarden/gui")
|
||||||
|
the_interface = dbus.Interface(the_object, "com.quexten.Goldwarden.gui.actions")
|
||||||
|
reply = the_interface.settings()
|
||||||
|
exit()
|
||||||
|
|
||||||
|
goldwarden.run_daemon_background(token)
|
||||||
|
# start daemons
|
||||||
|
dbus_autofill_monitor.run_daemon(token) # todo: remove after migration
|
||||||
|
dbus_monitor.run_daemon(token)
|
||||||
|
pinentry.daemonize()
|
||||||
|
|
||||||
|
if not "--hidden" in sys.argv:
|
||||||
|
p = subprocess.Popen(["python3", "-m", "src.gui.settings"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=root_path, start_new_session=True)
|
||||||
|
p.stdin.write(f"{token}\n".encode())
|
||||||
|
p.stdin.flush()
|
||||||
|
|
||||||
|
if is_flatpak:
|
||||||
|
# to autostart the appes
|
||||||
|
try:
|
||||||
|
subprocess.Popen(["python3", f'{source_path}/background.py'], start_new_session=True)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
while True:
|
||||||
|
time.sleep(60)
|
||||||
|
|
@ -10,7 +10,7 @@ import dbus.service
|
||||||
from dbus.mainloop.glib import DBusGMainLoop
|
from dbus.mainloop.glib import DBusGMainLoop
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
on_autofill = lambda: None
|
daemon_token = None
|
||||||
|
|
||||||
class GoldwardenDBUSService(dbus.service.Object):
|
class GoldwardenDBUSService(dbus.service.Object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -19,11 +19,20 @@ class GoldwardenDBUSService(dbus.service.Object):
|
||||||
|
|
||||||
@dbus.service.method('com.quexten.Goldwarden.Autofill')
|
@dbus.service.method('com.quexten.Goldwarden.Autofill')
|
||||||
def autofill(self):
|
def autofill(self):
|
||||||
on_autofill()
|
p = subprocess.Popen(["python3", "-m", "src.gui.quickaccess"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=root_path, start_new_session=True)
|
||||||
|
p.stdin.write(f"{daemon_token}\n".encode())
|
||||||
|
p.stdin.flush()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def run_daemon():
|
def daemon():
|
||||||
DBusGMainLoop(set_as_default=True)
|
DBusGMainLoop(set_as_default=True)
|
||||||
service = GoldwardenDBUSService()
|
service = GoldwardenDBUSService()
|
||||||
from gi.repository import GLib, GObject as gobject
|
from gi.repository import GLib, GObject as gobject
|
||||||
gobject.MainLoop().run()
|
gobject.MainLoop().run()
|
||||||
|
|
||||||
|
def run_daemon(token):
|
||||||
|
global daemon_token
|
||||||
|
daemon_token = token
|
||||||
|
thread = Thread(target=daemon)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
41
gui/src/linux/monitors/dbus_monitor.py
Normal file
41
gui/src/linux/monitors/dbus_monitor.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
from gi.repository import Gtk
|
||||||
|
import dbus
|
||||||
|
import dbus.service
|
||||||
|
from dbus.mainloop.glib import DBusGMainLoop
|
||||||
|
from threading import Thread
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
root_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir, os.pardir))
|
||||||
|
daemon_token = None
|
||||||
|
|
||||||
|
class GoldwardenDBUSService(dbus.service.Object):
|
||||||
|
def __init__(self):
|
||||||
|
bus_name = dbus.service.BusName('com.quexten.Goldwarden.gui', bus=dbus.SessionBus())
|
||||||
|
dbus.service.Object.__init__(self, bus_name, '/com/quexten/Goldwarden/gui')
|
||||||
|
|
||||||
|
@dbus.service.method('com.quexten.Goldwarden.gui.actions')
|
||||||
|
def quickaccess(self):
|
||||||
|
p = subprocess.Popen(["python3", "-m", "src.gui.quickaccess"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=root_path, start_new_session=True)
|
||||||
|
p.stdin.write(f"{daemon_token}\n".encode())
|
||||||
|
p.stdin.flush()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@dbus.service.method('com.quexten.Goldwarden.gui.actions')
|
||||||
|
def settings(self):
|
||||||
|
p = subprocess.Popen(["python3", "-m", "src.gui.settings"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=root_path, start_new_session=True)
|
||||||
|
p.stdin.write(f"{daemon_token}\n".encode())
|
||||||
|
p.stdin.flush()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def daemon():
|
||||||
|
DBusGMainLoop(set_as_default=True)
|
||||||
|
service = GoldwardenDBUSService()
|
||||||
|
from gi.repository import GLib, GObject as gobject
|
||||||
|
gobject.MainLoop().run()
|
||||||
|
|
||||||
|
def run_daemon(token):
|
||||||
|
global daemon_token
|
||||||
|
daemon_token = token
|
||||||
|
thread = Thread(target=daemon)
|
||||||
|
thread.start()
|
||||||
196
gui/src/services/goldwarden.py
Normal file
196
gui/src/services/goldwarden.py
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
is_flatpak = os.path.exists("/.flatpak-info")
|
||||||
|
log_directory = str(Path.home()) + "/.local/share/goldwarden"
|
||||||
|
if is_flatpak:
|
||||||
|
log_directory = str(Path.home()) + "/.var/app/com.quexten.goldwarden/data/goldwarden"
|
||||||
|
os.makedirs(log_directory, exist_ok=True)
|
||||||
|
|
||||||
|
BINARY_PATHS = [
|
||||||
|
"/app/bin/goldwarden",
|
||||||
|
"/usr/bin/goldwarden",
|
||||||
|
str(Path.home()) + "/go/src/github.com/quexten/goldwarden/goldwarden"
|
||||||
|
]
|
||||||
|
|
||||||
|
BINARY_PATH = ""
|
||||||
|
BINARY_PATH = None
|
||||||
|
for path in BINARY_PATHS:
|
||||||
|
if os.path.exists(path):
|
||||||
|
BINARY_PATH = path
|
||||||
|
break
|
||||||
|
if BINARY_PATH == None:
|
||||||
|
raise Exception("Could not find goldwarden binary")
|
||||||
|
authenticated_connection = None
|
||||||
|
|
||||||
|
def create_authenticated_connection(token):
|
||||||
|
global authenticated_connection
|
||||||
|
authenticated_connection = subprocess.Popen([f"{BINARY_PATH}", "session"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
|
if not token == None:
|
||||||
|
authenticated_connection.stdin.write("authenticate-session " + token + "\n")
|
||||||
|
authenticated_connection.stdin.flush()
|
||||||
|
# read entire message
|
||||||
|
result = authenticated_connection.stdout.readline()
|
||||||
|
if "true" not in result:
|
||||||
|
raise Exception("Failed to authenticate")
|
||||||
|
|
||||||
|
def send_authenticated_command(cmd):
|
||||||
|
if authenticated_connection == None:
|
||||||
|
print("No daemon connection running, please restart the application completely.")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
authenticated_connection.stdin.write(cmd + "\n")
|
||||||
|
authenticated_connection.stdin.flush()
|
||||||
|
result = authenticated_connection.stdout.readline()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def set_api_url(url):
|
||||||
|
send_authenticated_command(f"config set-api-url {url}")
|
||||||
|
|
||||||
|
def set_identity_url(url):
|
||||||
|
send_authenticated_command(f"config set-identity-url {url}")
|
||||||
|
|
||||||
|
def set_notification_url(url):
|
||||||
|
send_authenticated_command(f"config set-notifications-url {url}")
|
||||||
|
|
||||||
|
def set_vault_url(url):
|
||||||
|
send_authenticated_command(f"config set-vault-url {url}")
|
||||||
|
|
||||||
|
def set_url(url):
|
||||||
|
send_authenticated_command(f"config set-url {url}")
|
||||||
|
|
||||||
|
def get_environment():
|
||||||
|
result = send_authenticated_command(f"config get-environment")
|
||||||
|
try:
|
||||||
|
return json.loads(result)
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_client_id(client_id):
|
||||||
|
send_authenticated_command(f"config set-client-id \"{client_id}\"")
|
||||||
|
|
||||||
|
def set_client_secret(client_secret):
|
||||||
|
send_authenticated_command(f"config set-client-secret \"{client_secret}\"")
|
||||||
|
|
||||||
|
def login_with_password(email, password):
|
||||||
|
result = send_authenticated_command(f"vault login --email {email}")
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise Exception("Failed to initialize repository, err", result.stderr)
|
||||||
|
if len(result.stderr.strip()) > 0:
|
||||||
|
print(result.stderr)
|
||||||
|
if "password" in result.stderr:
|
||||||
|
return "badpass"
|
||||||
|
else:
|
||||||
|
if "Logged in" in result.stderr:
|
||||||
|
print("ok")
|
||||||
|
return "ok"
|
||||||
|
return "error"
|
||||||
|
print("ok")
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
def login_passwordless(email):
|
||||||
|
send_authenticated_command(f"vault login --email {email} --passwordless")
|
||||||
|
|
||||||
|
def is_pin_enabled():
|
||||||
|
result = send_authenticated_command("vault pin status")
|
||||||
|
return "enabled" in result
|
||||||
|
|
||||||
|
def enable_pin():
|
||||||
|
send_authenticated_command(f"vault pin set")
|
||||||
|
|
||||||
|
def unlock():
|
||||||
|
send_authenticated_command(f"vault unlock")
|
||||||
|
|
||||||
|
def lock():
|
||||||
|
send_authenticated_command(f"vault lock")
|
||||||
|
|
||||||
|
def purge():
|
||||||
|
send_authenticated_command(f"vault purge")
|
||||||
|
|
||||||
|
def get_vault_status():
|
||||||
|
result = send_authenticated_command(f"vault status")
|
||||||
|
try:
|
||||||
|
return json.loads(result)
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_vault_logins():
|
||||||
|
result = send_authenticated_command(f"logins list")
|
||||||
|
try:
|
||||||
|
return json.loads(result)
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_runtime_config():
|
||||||
|
result = send_authenticated_command(f"config get-runtime-config")
|
||||||
|
try:
|
||||||
|
return json.loads(result)
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def autotype(username, password):
|
||||||
|
env = os.environ.copy()
|
||||||
|
# todo convert to stdin
|
||||||
|
env["PASSWORD"] = password
|
||||||
|
goldwarden_cmd = f"{BINARY_PATH} autotype --username {username}"
|
||||||
|
result = subprocess.run(goldwarden_cmd.split(), capture_output=True, text=True, env=env)
|
||||||
|
print(result.stderr)
|
||||||
|
print(result.stdout)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise Exception("Failed to initialize repository, err", result.stderr)
|
||||||
|
|
||||||
|
def is_daemon_running():
|
||||||
|
result = send_authenticated_command(f"vault status")
|
||||||
|
daemon_not_running = ("daemon running" in result)
|
||||||
|
return not daemon_not_running
|
||||||
|
|
||||||
|
def listen_for_pinentry(on_pinentry, on_pin_approval):
|
||||||
|
print("listening for pinentry", BINARY_PATH)
|
||||||
|
pinentry_process = subprocess.Popen([f"{BINARY_PATH}", "pinentry"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
|
while True:
|
||||||
|
line = pinentry_process.stdout.readline()
|
||||||
|
# starts with pin-request
|
||||||
|
if line.startswith("pin-request"):
|
||||||
|
text = line.split(",")[1].strip()
|
||||||
|
pin = on_pinentry(text)
|
||||||
|
if pin == None:
|
||||||
|
pin = ""
|
||||||
|
pinentry_process.stdin.write(pin + "\n")
|
||||||
|
pinentry_process.stdin.flush()
|
||||||
|
if line.startswith("approval-request"):
|
||||||
|
text = line.split(",")[1].strip()
|
||||||
|
approval = on_pin_approval(text)
|
||||||
|
if approval:
|
||||||
|
pinentry_process.stdin.write("true\n")
|
||||||
|
pinentry_process.stdin.flush()
|
||||||
|
else:
|
||||||
|
pinentry_process.stdin.write("false\n")
|
||||||
|
pinentry_process.stdin.flush()
|
||||||
|
|
||||||
|
def run_daemon(token):
|
||||||
|
#todo replace with stdin
|
||||||
|
daemon_env = os.environ.copy()
|
||||||
|
daemon_env["GOLDWARDEN_DAEMON_AUTH_TOKEN"] = token
|
||||||
|
|
||||||
|
print("starting goldwarden daemon", BINARY_PATH)
|
||||||
|
|
||||||
|
# print while running
|
||||||
|
result = subprocess.Popen([f"{BINARY_PATH}", "daemonize"], stdout=subprocess.PIPE, text=True, env=daemon_env)
|
||||||
|
|
||||||
|
# write log to file until process exits
|
||||||
|
log_file = open(f"{log_directory}/daemon.log", "w")
|
||||||
|
while result.poll() == None:
|
||||||
|
# read stdout and stder
|
||||||
|
stdout = result.stdout.readline()
|
||||||
|
log_file.write(stdout)
|
||||||
|
log_file.flush()
|
||||||
|
log_file.close()
|
||||||
|
print("quitting goldwarden daemon")
|
||||||
|
return result.returncode
|
||||||
|
|
||||||
|
def run_daemon_background(token):
|
||||||
|
thread = Thread(target=lambda: run_daemon(token))
|
||||||
|
thread.start()
|
||||||
34
gui/src/services/pinentry.py
Normal file
34
gui/src/services/pinentry.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
from src.services import goldwarden
|
||||||
|
from threading import Thread
|
||||||
|
import time
|
||||||
|
|
||||||
|
root_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir))
|
||||||
|
|
||||||
|
def get_pin(message):
|
||||||
|
p = subprocess.Popen(["python3", "-m", "src.gui.pinentry"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=root_path, start_new_session=True)
|
||||||
|
p.stdin.write(f"{message}\n".encode())
|
||||||
|
p.stdin.flush()
|
||||||
|
pin = p.stdout.readline().decode().strip()
|
||||||
|
if pin == "":
|
||||||
|
return None
|
||||||
|
return pin
|
||||||
|
|
||||||
|
def get_approval(message):
|
||||||
|
p = subprocess.Popen(["python3", "-m", "src.gui.pinentry_approval"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=root_path, start_new_session=True)
|
||||||
|
p.stdin.write(f"{message}\n".encode())
|
||||||
|
p.stdin.flush()
|
||||||
|
result = p.stdout.readline().decode().strip()
|
||||||
|
if result == "true":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def daemon():
|
||||||
|
goldwarden.listen_for_pinentry(get_pin, get_approval)
|
||||||
|
|
||||||
|
def daemonize():
|
||||||
|
#todo fix this
|
||||||
|
time.sleep(3)
|
||||||
|
thread = Thread(target=daemon)
|
||||||
|
thread.start()
|
||||||
108
ipc/messages/session.go
Normal file
108
ipc/messages/session.go
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
package messages
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type SessionAuthRequest struct {
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionAuthResponse struct {
|
||||||
|
Verified bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type PinentryRegistrationRequest struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type PinentryRegistrationResponse struct {
|
||||||
|
Success bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type PinentryPinRequest struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PinentryPinResponse struct {
|
||||||
|
Pin string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PinentryApprovalRequest struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PinentryApprovalResponse struct {
|
||||||
|
Approved bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registerPayloadParser(func(payload []byte) (interface{}, error) {
|
||||||
|
var req SessionAuthRequest
|
||||||
|
err := json.Unmarshal(payload, &req)
|
||||||
|
if err != nil {
|
||||||
|
panic("Unmarshal: " + err.Error())
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}, SessionAuthRequest{})
|
||||||
|
|
||||||
|
registerPayloadParser(func(payload []byte) (interface{}, error) {
|
||||||
|
var req SessionAuthResponse
|
||||||
|
err := json.Unmarshal(payload, &req)
|
||||||
|
if err != nil {
|
||||||
|
panic("Unmarshal: " + err.Error())
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}, SessionAuthResponse{})
|
||||||
|
|
||||||
|
registerPayloadParser(func(payload []byte) (interface{}, error) {
|
||||||
|
var req PinentryRegistrationRequest
|
||||||
|
err := json.Unmarshal(payload, &req)
|
||||||
|
if err != nil {
|
||||||
|
panic("Unmarshal: " + err.Error())
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}, PinentryRegistrationRequest{})
|
||||||
|
|
||||||
|
registerPayloadParser(func(payload []byte) (interface{}, error) {
|
||||||
|
var req PinentryRegistrationResponse
|
||||||
|
err := json.Unmarshal(payload, &req)
|
||||||
|
if err != nil {
|
||||||
|
panic("Unmarshal: " + err.Error())
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}, PinentryRegistrationResponse{})
|
||||||
|
|
||||||
|
registerPayloadParser(func(payload []byte) (interface{}, error) {
|
||||||
|
var req PinentryPinRequest
|
||||||
|
err := json.Unmarshal(payload, &req)
|
||||||
|
if err != nil {
|
||||||
|
panic("Unmarshal: " + err.Error())
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}, PinentryPinRequest{})
|
||||||
|
|
||||||
|
registerPayloadParser(func(payload []byte) (interface{}, error) {
|
||||||
|
var req PinentryPinResponse
|
||||||
|
err := json.Unmarshal(payload, &req)
|
||||||
|
if err != nil {
|
||||||
|
panic("Unmarshal: " + err.Error())
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}, PinentryPinResponse{})
|
||||||
|
|
||||||
|
registerPayloadParser(func(payload []byte) (interface{}, error) {
|
||||||
|
var req PinentryApprovalRequest
|
||||||
|
err := json.Unmarshal(payload, &req)
|
||||||
|
if err != nil {
|
||||||
|
panic("Unmarshal: " + err.Error())
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}, PinentryApprovalRequest{})
|
||||||
|
|
||||||
|
registerPayloadParser(func(payload []byte) (interface{}, error) {
|
||||||
|
var req PinentryApprovalResponse
|
||||||
|
err := json.Unmarshal(payload, &req)
|
||||||
|
if err != nil {
|
||||||
|
panic("Unmarshal: " + err.Error())
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}, PinentryApprovalResponse{})
|
||||||
|
}
|
||||||
2
main.go
2
main.go
|
|
@ -36,6 +36,7 @@ func main() {
|
||||||
UseMemguard: os.Getenv("GOLDWARDEN_NO_MEMGUARD") != "true",
|
UseMemguard: os.Getenv("GOLDWARDEN_NO_MEMGUARD") != "true",
|
||||||
SSHAgentSocketPath: os.Getenv("GOLDWARDEN_SSH_AUTH_SOCK"),
|
SSHAgentSocketPath: os.Getenv("GOLDWARDEN_SSH_AUTH_SOCK"),
|
||||||
GoldwardenSocketPath: os.Getenv("GOLDWARDEN_SOCKET_PATH"),
|
GoldwardenSocketPath: os.Getenv("GOLDWARDEN_SOCKET_PATH"),
|
||||||
|
DaemonAuthToken: os.Getenv("GOLDWARDEN_DAEMON_AUTH_TOKEN"),
|
||||||
|
|
||||||
ConfigDirectory: configPath,
|
ConfigDirectory: configPath,
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +63,6 @@ func main() {
|
||||||
userHome, _ := os.UserHomeDir()
|
userHome, _ := os.UserHomeDir()
|
||||||
runtimeConfig.ConfigDirectory = userHome + "/.var/app/com.quexten.Goldwarden/config/goldwarden.json"
|
runtimeConfig.ConfigDirectory = userHome + "/.var/app/com.quexten.Goldwarden/config/goldwarden.json"
|
||||||
runtimeConfig.ConfigDirectory = strings.ReplaceAll(runtimeConfig.ConfigDirectory, "~", userHome)
|
runtimeConfig.ConfigDirectory = strings.ReplaceAll(runtimeConfig.ConfigDirectory, "~", userHome)
|
||||||
println("Flatpak Config directory: " + runtimeConfig.ConfigDirectory)
|
|
||||||
runtimeConfig.SSHAgentSocketPath = userHome + "/.var/app/com.quexten.Goldwarden/data/ssh-auth-sock"
|
runtimeConfig.SSHAgentSocketPath = userHome + "/.var/app/com.quexten.Goldwarden/data/ssh-auth-sock"
|
||||||
runtimeConfig.GoldwardenSocketPath = userHome + "/.var/app/com.quexten.Goldwarden/data/goldwarden.sock"
|
runtimeConfig.GoldwardenSocketPath = userHome + "/.var/app/com.quexten.Goldwarden/data/goldwarden.sock"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
53
ui/main.py
53
ui/main.py
|
|
@ -1,53 +0,0 @@
|
||||||
#!/usr/bin/python
|
|
||||||
import time
|
|
||||||
import subprocess
|
|
||||||
from tendo import singleton
|
|
||||||
import monitors.dbus_autofill_monitor
|
|
||||||
import sys
|
|
||||||
import goldwarden
|
|
||||||
from threading import Thread
|
|
||||||
import os
|
|
||||||
|
|
||||||
isflatpak = os.path.exists("/.flatpak-info")
|
|
||||||
pathprefix = "/app/bin/" if isflatpak else "./"
|
|
||||||
|
|
||||||
try:
|
|
||||||
subprocess.Popen(["python3", f'{pathprefix}background.py'], start_new_session=True)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
is_hidden = "--hidden" in sys.argv
|
|
||||||
|
|
||||||
if not is_hidden:
|
|
||||||
try:
|
|
||||||
subprocess.Popen(["python3", f'{pathprefix}settings.py'], start_new_session=True)
|
|
||||||
except:
|
|
||||||
subprocess.Popen(["python3", f'{pathprefix}settings.py'], start_new_session=True)
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
me = singleton.SingleInstance()
|
|
||||||
except:
|
|
||||||
exit()
|
|
||||||
|
|
||||||
def run_daemon():
|
|
||||||
# todo: do a proper check
|
|
||||||
if is_hidden:
|
|
||||||
time.sleep(20)
|
|
||||||
print("IS daemon running", goldwarden.is_daemon_running())
|
|
||||||
if not goldwarden.is_daemon_running():
|
|
||||||
print("running daemon")
|
|
||||||
goldwarden.run_daemon()
|
|
||||||
print("daemon running")
|
|
||||||
|
|
||||||
thread = Thread(target=run_daemon)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
def on_autofill():
|
|
||||||
subprocess.Popen(["python3", f'{pathprefix}autofill.py'], start_new_session=True)
|
|
||||||
|
|
||||||
monitors.dbus_autofill_monitor.on_autofill = lambda: on_autofill()
|
|
||||||
monitors.dbus_autofill_monitor.run_daemon()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
time.sleep(60)
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
{
|
|
||||||
"name": "python3-requirements",
|
|
||||||
"buildsystem": "simple",
|
|
||||||
"build-commands": [],
|
|
||||||
"modules": [
|
|
||||||
{
|
|
||||||
"name": "python3-ChaCha20",
|
|
||||||
"buildsystem": "simple",
|
|
||||||
"build-commands": [
|
|
||||||
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"ChaCha20==1.1.1\" --no-build-isolation"
|
|
||||||
],
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"type": "file",
|
|
||||||
"url": "https://files.pythonhosted.org/packages/7d/93/2c5c7203bcb1ece177a49d14b32f0e0511f7d4eaf8446c15f7d64af082ad/ChaCha20-1.1.1.tar.gz",
|
|
||||||
"sha256": "6d6b1ef373058540369c14d2369ab14ee4ffaeae4dc53d8a18b5b617bd06c6bb"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "python3-chacha20poly1305",
|
|
||||||
"buildsystem": "simple",
|
|
||||||
"build-commands": [
|
|
||||||
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"chacha20poly1305==0.0.3\" --no-build-isolation"
|
|
||||||
],
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"type": "file",
|
|
||||||
"url": "https://files.pythonhosted.org/packages/38/2c/5b4eb73c5cb30ded3082af025e76f529764971c57c02b101e842ff998f63/chacha20poly1305-0.0.3.tar.gz",
|
|
||||||
"sha256": "f2f005c7cf4638ffa4ff06c02c78748068b642916795c6d16c7cc5e355e70edf"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "python3-tendo",
|
|
||||||
"buildsystem": "simple",
|
|
||||||
"build-commands": [
|
|
||||||
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"tendo==0.3.0\" --no-build-isolation"
|
|
||||||
],
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"type": "file",
|
|
||||||
"url": "https://files.pythonhosted.org/packages/ce/3f/761077d55732b0b1a673b15d4fdaa947a7c1eb5c9a23b7142df557019823/tendo-0.3.0-py3-none-any.whl",
|
|
||||||
"sha256": "026b70b355ea4c9da7c2123fa2d5c280c8983c1b34e329ff49260e2e78b93be7"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
ChaCha20==1.1.1
|
|
||||||
chacha20poly1305==0.0.3
|
|
||||||
tendo==0.3.0
|
|
||||||
Loading…
Reference in a new issue