Implement initial pinentry

This commit is contained in:
Bernd Schoolmann 2024-02-09 00:24:28 +01:00
parent 8b08d5841a
commit ac0e84a46f
No known key found for this signature in database
15 changed files with 462 additions and 254 deletions

View File

@ -8,7 +8,7 @@ import (
"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(
pinentry.WithBinaryNameFromGnuPGAgentConf(),
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 {
return true, nil
}

View File

@ -10,7 +10,7 @@ import (
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(""), "")
result, err := pinentryInstance.Get(keybase1.SecretEntryArg{
Prompt: title,
@ -28,7 +28,7 @@ func GetPassword(title string, description string) (string, error) {
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(""), "")
result, err := pinentryInstance.Get(keybase1.SecretEntryArg{
Prompt: title,

View File

@ -1,6 +1,7 @@
package pinentry
import (
"errors"
"os"
"github.com/quexten/goldwarden/logging"
@ -9,8 +10,52 @@ import (
var log = logging.GetLogger("Goldwarden", "Pinentry")
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() {
if os.Getenv("GOLDWARDEN_SYSTEM_AUTH_DISABLED") == "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 approval, nil
return true, errors.New("Not implemented")
}

View File

@ -4,12 +4,12 @@ package pinentry
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")
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")
return true, errors.New("Not implemented")
}

View File

@ -17,6 +17,7 @@ import (
"github.com/quexten/goldwarden/agent/sockets"
"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/ipc/messages"
"github.com/quexten/goldwarden/logging"
@ -62,6 +63,7 @@ func serveAgentSession(c net.Conn, vault *vault.Vault, cfg *config.Config) {
continue
}
// todo refactor to other file
if msg.Type == messages.MessageTypeForEmptyPayload(messages.SessionAuthRequest{}) {
log.Info("Received session auth request")
req := messages.ParsePayload(msg).(messages.SessionAuthRequest)
@ -93,6 +95,125 @@ func serveAgentSession(c net.Conn, vault *vault.Vault, cfg *config.Config) {
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,
}
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}
}
}
}
continue
}
var responseBytes []byte
if action, actionFound := actions.AgentActionsRegistry.Get(msg.Type); actionFound {
callingContext := sockets.GetCallingContext(c)

View File

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

View File

@ -1,5 +0,0 @@
package client
type Client interface {
SendToAgent(request interface{}) (interface{}, error)
}

View File

@ -3,7 +3,6 @@ package client
import (
"encoding/json"
"io"
"log"
"net"
"github.com/quexten/goldwarden/agent/config"
@ -16,13 +15,17 @@ type UnixSocketClient struct {
runtimeConfig *config.RuntimeConfig
}
type UnixSocketConnection struct {
conn net.Conn
}
func NewUnixSocketClient(runtimeConfig *config.RuntimeConfig) UnixSocketClient {
return UnixSocketClient{
runtimeConfig: runtimeConfig,
}
}
func reader(r io.Reader) interface{} {
func Reader(r io.Reader) interface{} {
buf := make([]byte, READ_BUFFER)
for {
n, err := r.Read(buf[:])
@ -40,25 +43,55 @@ func reader(r io.Reader) interface{} {
}
func (client UnixSocketClient) SendToAgent(request interface{}) (interface{}, error) {
c, err := net.Dial("unix", client.runtimeConfig.GoldwardenSocketPath)
c, err := client.Connect()
if err != nil {
return nil, err
}
defer c.Close()
message, err := messages.IPCMessageFromPayload(request)
if err != nil {
panic(err)
}
messageJson, err := json.Marshal(message)
if err != nil {
panic(err)
}
_, err = c.Write(messageJson)
if err != nil {
log.Fatal("write error:", err)
}
result := reader(c)
return messages.ParsePayload(result.(messages.IPCMessage)), nil
return c.SendCommand(request)
}
func (client UnixSocketClient) Connect() (UnixSocketConnection, error) {
c, err := net.Dial("unix", client.runtimeConfig.GoldwardenSocketPath)
if err != nil {
return UnixSocketConnection{}, err
}
return UnixSocketConnection{conn: c}, nil
}
func (conn UnixSocketConnection) SendCommand(request interface{}) (interface{}, error) {
err := conn.WriteMessage(request)
if err != nil {
return nil, err
}
return conn.ReadMessage(), nil
}
func (conn UnixSocketConnection) ReadMessage() interface{} {
result := Reader(conn.conn)
// fmt.Println("ReadMessag")
// fmt.Println(result)
payload := messages.ParsePayload(result.(messages.IPCMessage))
// fmt.Println(payload)
return payload
}
func (conn UnixSocketConnection) WriteMessage(message interface{}) error {
// fmt.Println("WriteMessage")
messagePacket, err := messages.IPCMessageFromPayload(message)
// fmt.Println(messagePacket)
if err != nil {
panic(err)
}
messageJson, err := json.Marshal(messagePacket)
if err != nil {
panic(err)
}
// fmt.Println(messageJson)
_, err = conn.conn.Write(messageJson)
return err
}
func (conn UnixSocketConnection) Close() {
conn.conn.Close()
}

View File

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

View File

@ -89,7 +89,7 @@ var listLoginsCmd = &cobra.Command{
},
}
func ListLogins(client client.Client) ([]messages.DecryptedLoginCipher, error) {
func ListLogins(client client.UnixSocketClient) ([]messages.DecryptedLoginCipher, error) {
resp, err := client.SendToAgent(messages.ListLoginsRequest{})
if err != nil {
return []messages.DecryptedLoginCipher{}, err

View File

@ -4,14 +4,13 @@ import (
"fmt"
"os"
"github.com/quexten/goldwarden/agent"
"github.com/quexten/goldwarden/agent/config"
"github.com/quexten/goldwarden/client"
"github.com/quexten/goldwarden/ipc/messages"
"github.com/spf13/cobra"
)
var commandClient client.Client
var commandClient client.UnixSocketClient
var runtimeConfig config.RuntimeConfig
var rootCmd = &cobra.Command{
@ -25,13 +24,7 @@ var rootCmd = &cobra.Command{
func Execute(cfg config.RuntimeConfig) {
runtimeConfig = cfg
goldwardenSingleProcess := os.Getenv("GOLDWARDEN_SINGLE_PROCESS")
if goldwardenSingleProcess == "true" {
recv, send := agent.StartVirtualAgent(runtimeConfig)
commandClient = client.NewVirtualClient(send, recv)
} else {
commandClient = client.NewUnixSocketClient(&cfg)
}
err := rootCmd.Execute()
if err != nil {

View File

@ -34,7 +34,37 @@ var pinentry = &cobra.Command{
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"})
}
}
},
}

View File

@ -10,6 +10,29 @@ 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
@ -28,4 +51,58 @@ func init() {
}
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{})
}

65
ui/src/ui/pinentry.py Normal file
View File

@ -0,0 +1,65 @@
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
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())
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)

View 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")
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")
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)