From 09859ca0a9c1c4c889f1d7bc0b28979953d8f272 Mon Sep 17 00:00:00 2001 From: Greg Date: Tue, 10 Sep 2019 08:45:59 -0400 Subject: [PATCH] Compatibility with gpg 2.0 -- call out to gpg commands (on Darwin) if go openpgp is not compatibile with the local gpg installation. --- cmd/passgo-gui/main.go | 198 +++++++++++++++++++++++++++++++++++++---- cmd/passgo-gui/ui.go | 2 +- impl_darwin.go | 90 +++++++++++++++++++ main.go | 105 ++++++++++++++++++++-- 4 files changed, 372 insertions(+), 23 deletions(-) diff --git a/cmd/passgo-gui/main.go b/cmd/passgo-gui/main.go index 545896b..907fa4c 100644 --- a/cmd/passgo-gui/main.go +++ b/cmd/passgo-gui/main.go @@ -49,7 +49,7 @@ func main() { log(Info, " StoreDir = ", store.Dir) err = passgo.GetStore(&store) if err != nil { - log(Fatal, err) + log(Info, err) } if Config.ClearDelay == 0 { Config.ClearDelay = 45 @@ -62,8 +62,10 @@ func main() { reload = make(chan struct{}) updated = make(chan struct{}) + chdir = make(chan struct{}) passch = make(chan []byte) + passgo.Identities() go Updater() log(Info, "Staring event loop") go eventLoop() @@ -78,6 +80,7 @@ var ( store passgo.Store reload chan struct{} updated chan struct{} + chdir chan struct{} passch chan []byte ) @@ -98,7 +101,8 @@ func Updater() { if err != nil { log(Fatal, err) } - watcher.Add(store.Dir) + dir := store.Dir + watcher.Add(dir) for { select { case <-reload: @@ -107,6 +111,13 @@ func Updater() { update() case e := <-watcher.Errors: log(Info, "Watcher error: ", e) + case <-chdir: + err := watcher.Remove(dir) + if err != nil { + log(Info, "Error removing watcher: ", err) + } + watcher.Add(store.Dir) + update() } } } @@ -151,10 +162,10 @@ func eventLoop() { margin := layout.UniformInset(ui.Dp(10)) title := &text.Label{Face: face, Text: "passgo"} - dotsbtn := &Button{ + dotsBtn := &Button{ Face: face, Label: "\xe2\x8b\xae", - Alignment: text.End, + Alignment: text.Middle, Color: black, Background: gray, } @@ -200,6 +211,30 @@ func eventLoop() { } updateBtns() + idBtns := make([]*Button, 0) + + updateIdBtns := func() { + ids, err := passgo.Identities() + if err != nil { + log(Info, err) + return + } + for i,n := range ids { + if i >= len(idBtns) { + idBtns = append(idBtns, &Button{ + Face: face, + Label: n, + Alignment: text.End, + Color: black, + Background: gray, + }) + } else { + idBtns[i].Label = n + } + } + idBtns = idBtns[:len(ids)] + } + confBtn := &Button{ Face: face, Label: "configure", @@ -225,6 +260,14 @@ func eventLoop() { Color: black, Background: gray, } + confirmLabel := &text.Label{Face: face, Text: "Password exists. Overwrite?"} + yesBtn := &Button{ + Face: face, + Label: "yes", + Alignment: text.End, + Color: black, + Background: gray, + } promptLabel := &text.Label{Face: face, Text: "passphrase"} promptEd := &text.Editor{Face: face, SingleLine: true, Submit: true} @@ -235,6 +278,22 @@ func eventLoop() { Color: black, Background: gray, } + plusBtn := &Button{ + Face: face, + Label: "+", + Alignment: text.Middle, + Color: black, + Background: gray, + } + + insertLabel := &text.Label{Face: face, Text: "Insert"} + passnameLabel := &text.Label{Face: face, Text: "password name:"} + passnameEd := &text.Editor{Face: face, SingleLine: true, Submit: true} + passvalLabel := &text.Label{Face: face, Text: "password value:"} + passvalEd := &text.Editor{Face: face, SingleLine: true, Submit: true} + + noidLabel := &text.Label{Face: face, Text: "No GPG ids available. Please create a private key"} + idLabel := &text.Label{Face: face, Text: "Select ID"} anim := &time.Ticker{} animating := false @@ -250,7 +309,7 @@ func eventLoop() { animating = false } - var listPage, confPage, promptPage, page func() + var listPage, idPage, insertPage, confirmPage, confPage, promptPage, page func() prompt := func() []byte { page = promptPage @@ -271,14 +330,19 @@ func eventLoop() { key.HideInputOp{}.Add(ops) } var c2 layout.FlexChild - if len(passBtns) == 0 { + switch { + case store.Empty: c2 = flex.End(confBtn.Layout(c, q, ops, cs)) if confBtn.Clicked() { log(Info, "Configure") w.Invalidate() page = confPage } - } else { + case store.Id == "": + c2 = flex.End(idLabel.Layout(ops, cs)) + w.Invalidate() + page = idPage + default: for lst.Init(c, q, ops, cs, len(passBtns)); lst.More(); lst.Next() { i := lst.Index() btn := passBtns[i] @@ -316,11 +380,6 @@ func eventLoop() { c2 = flex.End(lst.Layout()) } mux.Unlock() - if dotsbtn.Clicked() { - log(Info, "Configure") - w.Invalidate() - page = confPage - } flex.Layout(c1, c2) x := time.Since(overlayStart).Seconds() if x >= fade1b && x < start2 && animating { @@ -372,6 +431,99 @@ func eventLoop() { } } + idPage = func() { + if !animating { + animOn() + } + cs = flex.Rigid() + c2 := flex.End(idLabel.Layout(ops, cs)) + var c3 layout.FlexChild + //if len(idBtns) == 0 { + updateIdBtns() + //} + cs = flex.Rigid() + if len(idBtns) == 0 { // still zero after update + c3 = flex.End(noidLabel.Layout(ops, cs)) + } else { + for lst.Init(c, q, ops, cs, len(idBtns)); lst.More(); lst.Next() { + lst.End(idBtns[lst.Index()].Layout(c, q, ops, lst.Constraints())) + } + c3 = flex.End(lst.Layout()) + for _, btn := range idBtns { + if btn.Clicked() { + log(Info, "ID selected: ", btn.Label) + store.SetID(btn.Label) + w.Invalidate() + animOff() + page = listPage + } + } + } + flex.Layout(c1, c2, c3) + } + + var insName, insValue string + + insertPage = func() { + cs = flex.Rigid() + c2 := flex.End(insertLabel.Layout(ops, cs)) + + c3 := flex.End(passnameLabel.Layout(ops, flex.Rigid())) + c4 := flex.End(passnameEd.Layout(c, q, ops, flex.Rigid())) + c5 := flex.End(passvalLabel.Layout(ops, flex.Rigid())) + c6 := flex.End(passvalEd.Layout(c, q, ops, flex.Rigid())) + cs = flex.Rigid() + al := &layout.Align{Alignment: layout.E} + btnflx := &layout.Flex{Axis: layout.Horizontal} + btnflx.Init(ops, al.Begin(ops, cs)) + bc1 := btnflx.End(backBtn.Layout(c, q, ops, btnflx.Rigid())) + bc2 := btnflx.End(saveBtn.Layout(c, q, ops, btnflx.Rigid())) + c7 := flex.End(al.End(btnflx.Layout(bc1, bc2))) + flex.Layout(c1, c2, c3, c4, c5, c6, c7) + + if backBtn.Clicked() { + w.Invalidate() + page = listPage + } + if saveBtn.Clicked() { + w.Invalidate() + page = listPage + insName = passnameEd.Text() + insValue = passvalEd.Text() + for _, n := range pathnames { + if insName == n { + log(Info,"Password exists") + page = confirmPage + w.Invalidate() + return + } + } + store.Insert(passnameEd.Text(), passvalEd.Text()) + } + } + + confirmPage = func() { + cs = flex.Rigid() + c2 := flex.End(confirmLabel.Layout(ops, cs)) + al := &layout.Align{Alignment: layout.E} + btnflx := &layout.Flex{Axis: layout.Horizontal} + btnflx.Init(ops, al.Begin(ops, flex.Rigid())) + bc1 := btnflx.End(backBtn.Layout(c, q, ops, btnflx.Rigid())) + bc2 := btnflx.End(yesBtn.Layout(c, q, ops, btnflx.Rigid())) + c3 := flex.End(al.End(btnflx.Layout(bc1, bc2))) + flex.Layout(c1, c2, c3) + + if backBtn.Clicked() { + w.Invalidate() + page = insertPage + } + if yesBtn.Clicked() { + w.Invalidate() + page = listPage + store.Insert(insName, insValue) + } + } + confPage = func() { cs = flex.Rigid() c2 := flex.End(storeDirLabel.Layout(ops, cs)) @@ -399,10 +551,12 @@ func eventLoop() { log(Info, "Save") go func() { // do not block UI thread store.Dir = storeDirEd.Text() + store.Id = "" passgo.GetStore(&store) Config.StoreDir = store.Dir + store.Mkdir() saveConf() - reload <- struct{}{} + chdir <- struct{}{} }() w.Invalidate() page = listPage @@ -477,15 +631,29 @@ func eventLoop() { cs = flex.Rigid() titleflex.Init(ops, cs) cs = titleflex.Rigid() - ct2 := titleflex.End(dotsbtn.Layout(c, q, ops, cs)) + ct2 := titleflex.End(plusBtn.Layout(c, q, ops, cs)) + cs = titleflex.Rigid() + ct3 := titleflex.End(dotsBtn.Layout(c, q, ops, cs)) cs = titleflex.Flexible(1.0) cs.Width.Min = cs.Width.Max ct1 := titleflex.End(title.Layout(ops, cs)) - c1 = flex.End(titleflex.Layout(ct1, ct2)) + c1 = flex.End(titleflex.Layout(ct1, ct2, ct3)) page() margin.End(dims) + + if dotsBtn.Clicked() { + log(Info, "Configure") + w.Invalidate() + page = confPage + } + if plusBtn.Clicked() { + log(Info, "Plus") + w.Invalidate() + insName, insValue = "", "" + page = insertPage + } w.Update(ops) } } diff --git a/cmd/passgo-gui/ui.go b/cmd/passgo-gui/ui.go index 6eeeada..50a66f5 100644 --- a/cmd/passgo-gui/ui.go +++ b/cmd/passgo-gui/ui.go @@ -113,7 +113,7 @@ func (b *Button) Layout(c ui.Config, q input.Queue, ops *ui.Ops, cs layout.Const Alignment: b.Alignment, } ins := layout.UniformInset(ui.Dp(4)) - paint.ColorOp{Color: b.Color}.Add(ops) + //paint.ColorOp{Color: b.Color}.Add(ops) dims = ins.End(l.Layout(ops, ins.Begin(c, ops, cs))) pointer.RectAreaOp{image.Rect(0, 0, dims.Size.X, dims.Size.Y)}.Add(ops) b.Click.Add(ops) diff --git a/impl_darwin.go b/impl_darwin.go index ec30741..5459112 100644 --- a/impl_darwin.go +++ b/impl_darwin.go @@ -10,9 +10,75 @@ import ( "os/exec" "os/user" "path" + "strings" "strconv" + "sync" + + "github.com/fsnotify/fsnotify" ) +func (s *Store) gpgDecrypt(name string) (string, error) { + fmt.Println("calling gpg -d") + file := path.Join(s.Dir, strings.Join([]string{name, ".gpg"}, "")) + if _, err := os.Stat(file); os.IsNotExist(err) { + return "", fmt.Errorf("Not in password store.") + } + output, err := exec.Command("gpg", "-d", file).Output() + if err != nil { + return "", fmt.Errorf("Error running gpg: %s", err) + } + return string(output[:len(output)-1]), nil +} + +func (s *Store) gpgEncrypt(pw string) ([]byte, error) { + if s.Id == "" { + return nil, fmt.Errorf("No ID") + } + fmt.Printf("Calling gpg -e -r %s\n", s.Id) + cmd := exec.Command("gpg", "-e", "-r", s.Id) + cmd.Stdin = strings.NewReader(pw + "\n") + output, err := cmd.Output() + if err != nil { + fmt.Printf("Error running GPG: %s\n", err) + return nil, err + } + fmt.Println("success") + return output, nil +} + +var ( + idMux sync.Mutex + cachedIdentities []string +) + +func gpgIdentities() ([]string, error) { + idMux.Lock() + defer idMux.Unlock() + if cachedIdentities != nil { + ret := make([]string,len(cachedIdentities)) + copy(ret, cachedIdentities) + return ret, nil + } + ret := make([]string,0) + fmt.Println("calling gpg") + output, err := exec.Command("gpg", "--list-secret-keys", "--with-colons").Output() + if err != nil { + return nil, fmt.Errorf("Error running gpg: %s", err) + } + scanner := bufio.NewScanner(bytes.NewBuffer(output)) + for scanner.Scan() { + fs := strings.Split(scanner.Text(),":") + if fs[0] == "uid" { + fmt.Printf("%s: %s\n",fs[0], fs[9]) + ret = append(ret, fs[9]) + } + } + if len(ret) > 0 { + cachedIdentities = ret + } + return ret, nil +} + func setAgentInfo() { // get UID of current user usr, err := user.Current() @@ -77,10 +143,33 @@ func setAgentInfo() { // gpg-agent is running, so use GPGPrompt as password prompt ask = GPGPrompt + } func init() { setAgentInfo() + go func() { + watcher, err := fsnotify.NewWatcher() + if err != nil { + fmt.Printf("Cannot create filesystem watcher: %s\n", err) + return + } + u, err := user.Current() + if err != nil { + fmt.Printf("Cannot get current user: %s\n", err) + return + } + watcher.Add(path.Join(u.HomeDir, ".gnupg")) + fmt.Println("Watching ", path.Join(u.HomeDir, ".gnupg")) + for { + select { + case <-watcher.Events: + idMux.Lock() + cachedIdentities = nil + idMux.Unlock() + } + } + }() } //Clip copies a string to the clipboard @@ -90,3 +179,4 @@ func Clip(x string) { cmd.Stdin = b cmd.Run() } + diff --git a/main.go b/main.go index e19d440..dedc938 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "regexp" "sort" "strings" + "time" "github.com/jcmdev0/gpgagent" @@ -23,15 +24,21 @@ import ( var ( basename *regexp.Regexp ask openpgp.PromptFunction + Keyring openpgp.KeyRing + krTime time.Time + useGPG bool + homeDir string ) func init() { + useGPG = true // default unless we can get the Go openpgp code to work basename = regexp.MustCompile(".gpg$") } type Store struct { Dir string - keyring openpgp.KeyRing + Id string + Empty bool } func GetStore(store *Store) error { @@ -42,20 +49,70 @@ func GetStore(store *Store) error { if store.Dir == "" { store.Dir = path.Join(u.HomeDir, ".password-store") } + if _, err := os.Stat(store.Dir); os.IsNotExist(err) { + store.Empty = true + } + id, err := ioutil.ReadFile(path.Join(store.Dir, ".gpg-id")) + if err != nil { + return fmt.Errorf("Can't read .gpg-id: %s", err) + } + if id[len(id)-1] == '\n' { + id = id[:len(id)-1] + } + store.Id = string(id) + return getKeyring() +} - fd, err := os.Open(path.Join(u.HomeDir, ".gnupg/secring.gpg")) +func getKeyring() error { + u, err := user.Current() + homeDir = u.HomeDir + krfile := path.Join(homeDir, ".gnupg/secring.gpg") + if fi, err := os.Stat(krfile); os.IsNotExist(err) { + return nil + } else { + if krTime.Sub(fi.ModTime()) > 0 { // already loaded + return nil + } + } + fmt.Println("Loading secring.gpg") + fd, err := os.Open(krfile) defer fd.Close() if err != nil { - return fmt.Errorf("Can't open keyring file") + return nil } kr, err := openpgp.ReadKeyRing(fd) if err != nil { - return fmt.Errorf("Can't open gnupg keyring.") + //return fmt.Errorf("Can't open gnupg keyring.") + return nil } - store.keyring = kr + useGPG = false + Keyring = kr + krTime = time.Now() return nil } +func (s *Store) Mkdir() error { + err := os.MkdirAll(s.Dir, 0700) + if err != nil { + fmt.Printf("Cannot create store directory.") + return err + } + s.Empty = false + return nil +} + +func (s *Store) SetID(id string) error { + s.Id = id + if _, err := os.Stat(s.Dir); os.IsNotExist(err) { + fmt.Printf("store directory does not exist.\n") + err := s.Mkdir() + if err != nil { + return err + } + } + return ioutil.WriteFile(path.Join(s.Dir, ".gpg-id"), []byte(id + "\n"), 0644) +} + type caseInsensitive []os.FileInfo func (fs caseInsensitive) Len() int { return len(fs) } @@ -192,6 +249,9 @@ func GPGPrompt(keys []openpgp.Key, symmetric bool) ([]byte, error) { } func (s *Store) Decrypt(name string, prompts ...func() []byte) (string, error) { + if useGPG { + return s.gpgDecrypt(name) + } if ask == nil { ask = AskPass(prompts...) } @@ -217,7 +277,7 @@ func (s *Store) Decrypt(name string, prompts ...func() []byte) (string, error) { } reader = fd } - md, err := openpgp.ReadMessage(reader, s.keyring, ask, nil) + md, err := openpgp.ReadMessage(reader, Keyring, ask, nil) if err != nil { fmt.Println("Error reading message") return "", err @@ -226,5 +286,36 @@ func (s *Store) Decrypt(name string, prompts ...func() []byte) (string, error) { if err != nil { return "", err } - return string(ra), nil + if len(ra) > 1 { + return string(ra[:len(ra)-1]), nil + } else { + return "", fmt.Errorf("Password is empty") + } +} + +func Identities() ([]string, error) { + getKeyring() + if useGPG { + return gpgIdentities() + } + ret := make([]string, 0) + for _, k := range Keyring.DecryptionKeys() { + for n, _ := range k.Entity.Identities { + ret = append(ret, n) + } + } + return ret, nil +} + +func (s *Store) Insert(name, value string) error { + var enc []byte + var err error + //if !useGo { // golang openpgp code not implemented yet + fmt.Println("Calling gpgEncrypt") + enc, err = s.gpgEncrypt(value) + if err != nil { + return err + } + //} + return ioutil.WriteFile(path.Join(s.Dir, name + ".gpg"), enc, 0644) }