diff --git a/agent/defaultmainloop.go b/agent/defaultmainloop.go new file mode 100644 index 0000000..c90ef4e --- /dev/null +++ b/agent/defaultmainloop.go @@ -0,0 +1,17 @@ +//go:build !windows && !darwin && !linux + +package agent + +import ( + "fmt" + "os" + "os/signal" + "syscall" +) + +func mainloop() { + done := make(chan os.Signal, 1) + signal.Notify(done, syscall.SIGINT, syscall.SIGTERM) + fmt.Println("Blocking, press ctrl+c to continue...") + <-done +} diff --git a/agent/giouimainloop.go b/agent/giouimainloop.go new file mode 100644 index 0000000..ee802bc --- /dev/null +++ b/agent/giouimainloop.go @@ -0,0 +1,9 @@ +//go:build windows || darwin || linux + +package agent + +import "gioui.org/app" + +func mainloop() { + app.Main() +} diff --git a/agent/systemauth/pinentry/gioui-pinentry.go b/agent/systemauth/pinentry/gioui-pinentry.go new file mode 100644 index 0000000..42df955 --- /dev/null +++ b/agent/systemauth/pinentry/gioui-pinentry.go @@ -0,0 +1,23 @@ +//go:build windows || darwin + +package pinentry + +import ( + "github.com/quexten/goldwarden/agent/systemauth/pinentry/giouipinentry" +) + +func GetPassword(title string, description string) (string, error) { + var resultChan = make(chan string) + giouipinentry.GetPin(title, description, func(pin string) { + resultChan <- pin + }) + return <-resultChan, nil +} + +func GetApproval(title string, description string) (bool, error) { + var resultChan = make(chan bool) + giouipinentry.GetApproval(title, description, func(approved bool) { + resultChan <- approved + }) + return <-resultChan, nil +} diff --git a/agent/systemauth/pinentry/giouipinentry/giopinentry.go b/agent/systemauth/pinentry/giouipinentry/giopinentry.go new file mode 100644 index 0000000..83d6c70 --- /dev/null +++ b/agent/systemauth/pinentry/giouipinentry/giopinentry.go @@ -0,0 +1,310 @@ +// go:build windows || darwin +package giouipinentry + +import ( + "fmt" + "image" + "image/color" + "log" + + "gioui.org/app" + "gioui.org/font/gofont" + "gioui.org/io/key" + "gioui.org/io/system" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" +) + +func GetPin(title string, description string, onPinEntered func(string)) { + go func() { + w := app.NewWindow() + w.Option(app.Size(unit.Dp(500), unit.Dp(300))) + w.Option(app.Decorated(false)) + w.Perform(system.ActionCenter) + w.Perform(system.ActionRaise) + if err := runPinentry(w, title, description, onPinEntered); err != nil { + log.Fatal(err) + } + }() +} + +func GetApproval(title string, description string, onApproval func(bool)) { + go func() { + w := app.NewWindow() + w.Option(app.Size(unit.Dp(500), unit.Dp(300))) + w.Option(app.Decorated(false)) + w.Perform(system.ActionCenter) + w.Perform(system.ActionRaise) + if err := runApproval(w, title, description, onApproval); err != nil { + log.Fatal(err) + } + }() +} + +var ( + // #000000 + unselected = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} + // #FFFFFF + unselectedText = color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} + // #000000 + background = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} + // #651FFF + selected = color.NRGBA{R: 0x65, G: 0x1F, B: 0xFF, A: 0xFF} + // #FFFFFF + selectedText = color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} + // #4caf50 + buttonOk = color.NRGBA{R: 0x4C, G: 0xAF, B: 0x50, A: 0xFF} + // #F44336 + buttonCancel = color.NRGBA{R: 0xF4, G: 0x43, B: 0x36, A: 0xFF} +) + +var th = material.NewTheme(gofont.Collection()) + +func runPinentry(w *app.Window, title string, description string, onPinEntered func(string)) error { + var lineEditor = &widget.Editor{ + SingleLine: true, + Submit: true, + } + lineEditor.Focus() + var ops op.Ops + + var btnOk widget.Clickable + var btnCancel widget.Clickable + + for { + e := <-w.Events() + switch e := e.(type) { + case system.DestroyEvent: + return e.Err + case system.FrameEvent: + gtx := layout.NewContext(&ops, e) + + key.InputOp{ + Keys: key.Set(key.NameReturn + "|" + key.NameEscape + "|" + key.NameDownArrow), + Tag: 0, + }.Add(gtx.Ops) + t := lineEditor.Events() + for _, ev := range t { + switch ev.(type) { + case widget.SubmitEvent: + onPinEntered(lineEditor.Text()) + w.Perform(system.ActionClose) + } + } + if btnOk.Clicked() { + onPinEntered(lineEditor.Text()) + w.Perform(system.ActionClose) + } + if btnCancel.Clicked() { + onPinEntered("") + w.Perform(system.ActionClose) + } + + test := gtx.Events(0) + for _, ev := range test { + switch ev := ev.(type) { + case key.Event: + switch ev.Name { + case key.NameReturn: + fmt.Println("uncaught submit") + return nil + case key.NameEscape: + onPinEntered("") + w.Perform(system.ActionClose) + } + } + } + + Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + //title + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + title := material.H6(th, title) + title.Color = unselectedText + return title.Layout(gtx) + }) + }) + }), + // description + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + body := material.Body1(th, description) + body.Color = unselectedText + return body.Layout(gtx) + }) + }) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + searchBox := material.Editor(th, lineEditor, "Pin") + searchBox.Color = selectedText + border := widget.Border{Color: selectedText, CornerRadius: unit.Dp(8), Width: unit.Dp(2)} + return border.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(8)).Layout(gtx, searchBox.Layout) + }) + }) + }) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical, Spacing: layout.SpaceStart}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + acceptBtn := material.Button(th, &btnOk, "Accept") + acceptBtn.Background = buttonOk + return acceptBtn.Layout(gtx) + }), + layout.Rigid( + layout.Spacer{Height: unit.Dp(10)}.Layout, + ), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + cancel := material.Button(th, &btnCancel, "Cancel") + cancel.Background = buttonCancel + return cancel.Layout(gtx) + }), + ) + }) + }), + ) + }) + + e.Frame(gtx.Ops) + } + } + + return nil +} + +func runApproval(w *app.Window, title string, description string, onApproval func(bool)) error { + var ops op.Ops + + var btnOk widget.Clickable + var btnCancel widget.Clickable + + for { + e := <-w.Events() + switch e := e.(type) { + case system.DestroyEvent: + return e.Err + case system.FrameEvent: + gtx := layout.NewContext(&ops, e) + + key.InputOp{ + Keys: key.Set(key.NameReturn + "|" + key.NameEscape + "|" + key.NameDownArrow), + Tag: 0, + }.Add(gtx.Ops) + + if btnOk.Clicked() { + onApproval(true) + w.Perform(system.ActionClose) + } + if btnCancel.Clicked() { + onApproval(false) + w.Perform(system.ActionClose) + } + + test := gtx.Events(0) + for _, ev := range test { + switch ev := ev.(type) { + case key.Event: + switch ev.Name { + case key.NameReturn: + fmt.Println("uncaught submit") + return nil + case key.NameEscape: + onApproval(false) + w.Perform(system.ActionClose) + } + } + } + + Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + //title + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + title := material.H6(th, title) + title.Color = unselectedText + return title.Layout(gtx) + }) + }) + }), + // description + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + body := material.Body1(th, description) + body.Color = unselectedText + return body.Layout(gtx) + }) + }) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical, Spacing: layout.SpaceStart}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + acceptBtn := material.Button(th, &btnOk, "Accept") + acceptBtn.Background = buttonOk + return acceptBtn.Layout(gtx) + }), + layout.Rigid( + layout.Spacer{Height: unit.Dp(10)}.Layout, + ), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + cancel := material.Button(th, &btnCancel, "Cancel") + cancel.Background = buttonCancel + return cancel.Layout(gtx) + }), + ) + }) + }), + ) + }) + + e.Frame(gtx.Ops) + } + } + + return nil +} + +type Background struct { + Color color.NRGBA + CornerRadius unit.Dp +} + +func (b Background) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { + m := op.Record(gtx.Ops) + dims := w(gtx) + size := dims.Size + call := m.Stop() + if r := gtx.Dp(b.CornerRadius); r > 0 { + defer clip.RRect{ + Rect: image.Rect(0, 0, size.X, size.Y), + NE: r, NW: r, SE: r, SW: r, + }.Push(gtx.Ops).Pop() + } + fill{b.Color}.Layout(gtx, size) + call.Add(gtx.Ops) + return dims +} + +type fill struct { + col color.NRGBA +} + +func (f fill) Layout(gtx layout.Context, sz image.Point) layout.Dimensions { + defer clip.Rect(image.Rectangle{Max: sz}).Push(gtx.Ops).Pop() + paint.ColorOp{Color: f.col}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + return layout.Dimensions{Size: sz} +} diff --git a/agent/systemauth/pinentry/go-pinentry.go b/agent/systemauth/pinentry/go-pinentry.go index dea2c0b..8e96bca 100644 --- a/agent/systemauth/pinentry/go-pinentry.go +++ b/agent/systemauth/pinentry/go-pinentry.go @@ -1,4 +1,4 @@ -//go:build linux || freebsd +//go:build freebsd || linux package pinentry diff --git a/agent/systemauth/pinentry/unimplemented.go b/agent/systemauth/pinentry/unimplemented.go index eca160e..3e51a7f 100644 --- a/agent/systemauth/pinentry/unimplemented.go +++ b/agent/systemauth/pinentry/unimplemented.go @@ -1,4 +1,4 @@ -//go:build !linux && !freebsd +//go:build !linux && !freebsd && !darwin && !windows package pinentry diff --git a/agent/unixsocketagent.go b/agent/unixsocketagent.go index a4ff2fc..b4e48e1 100644 --- a/agent/unixsocketagent.go +++ b/agent/unixsocketagent.go @@ -222,13 +222,17 @@ func StartUnixAgent(path string, runtimeConfig config.RuntimeConfig) error { } log.Info("Agent listening on %s...", path) - for { - fd, err := l.Accept() - if err != nil { - println("accept error", err.Error()) - return err - } + go func() { + for { + fd, err := l.Accept() + if err != nil { + println("accept error", err.Error()) + } - go serveAgentSession(fd, ctx, vault, &cfg) - } + go serveAgentSession(fd, ctx, vault, &cfg) + } + }() + + mainloop() + return nil }