commit e8a4ad031b20cc30b44f6c0bb317af4ddfea3a2e Author: Greg Date: Wed Sep 4 22:19:39 2019 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1dce362 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cmd/gpass/gpass +cmd/gpass-gui/gpass-gui diff --git a/cmd/gpass-gui/log.go b/cmd/gpass-gui/log.go new file mode 100644 index 0000000..83d8756 --- /dev/null +++ b/cmd/gpass-gui/log.go @@ -0,0 +1,28 @@ +package main + +import ( + golog "log" + "os" +) + +type logLevelT int +const ( + Fatal logLevelT = 1 << iota + Error + Warn + Info + Debug + DebugGfx +) + +var loglevel = Fatal | Error | Warn | Info + +func log(level logLevelT, msg ...interface{}) { + if level & loglevel != 0 { + golog.Print(msg...) + } + if level & Fatal != 0 { + os.Exit(-1) + } +} + diff --git a/cmd/gpass-gui/main.go b/cmd/gpass-gui/main.go new file mode 100644 index 0000000..16c049d --- /dev/null +++ b/cmd/gpass-gui/main.go @@ -0,0 +1,243 @@ +// +build darwin linux + +package main + +import ( + //"fmt" + "path" + "strings" + "image" + "image/color" + "sync" + + "gioui.org/ui" + "gioui.org/ui/app" + "gioui.org/ui/input" + "gioui.org/ui/key" + "gioui.org/ui/layout" + "gioui.org/ui/text" + "gioui.org/ui/measure" + "gioui.org/ui/f32" + "gioui.org/ui/paint" + "gioui.org/ui/gesture" + "gioui.org/ui/pointer" + + "github.com/fsnotify/fsnotify" + "golang.org/x/image/font/gofont/goregular" + "golang.org/x/image/font/sfnt" + + + "git.wow.st/gmp/pass" +) + +func main() { + var err error + store,err = pass.GetStore() + if err != nil { + log(Fatal, err) + } + updated = make(chan struct{}) + go Updater() + log(Info,"Staring event loop") + go eventLoop() + app.Main() + log(Info,"Event loop returned") +} + +var ( + l []pass.Pass + mux sync.Mutex + store *pass.Store + updated chan struct{} +) + +func Updater() { + update := func() { + ltmp,err := store.List() + if err != nil { + log(Fatal, err) + } + mux.Lock() + l = ltmp + mux.Unlock() + updated <- struct{}{} + } + update() + + watcher, err := fsnotify.NewWatcher() + if err != nil { + log(Fatal, err) + } + watcher.Add(store.Dir) + for { + select { + case <-watcher.Events: + update() + case e := <-watcher.Errors: + log(Info, "Watcher error: ",e) + } + } +} + +type Button struct { + Face text.Face + Label string + Click gesture.Click + Color color.RGBA + clicked bool +} + +func layoutRRect(col color.RGBA, c ui.Config, ops *ui.Ops, cs layout.Constraints) layout.Dimensions { + r := float32(c.Px(ui.Dp(4))) + sz := image.Point{X: cs.Width.Min, Y: cs.Height.Min} + w, h := float32(sz.X), float32(sz.Y) + rrect(ops, w, h, r, r, r, r) + paint.ColorOp{Color: col}.Add(ops) + paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X: w, Y: h}}}.Add(ops) + return layout.Dimensions{Size: sz} +} + +// https://pomax.github.io/bezierinfo/#circles_cubic. +func rrect(ops *ui.Ops, width, height, se, sw, nw, ne float32) { + w, h := float32(width), float32(height) + const c = 0.55228475 // 4*(sqrt(2)-1)/3 + var b paint.PathBuilder + b.Init(ops) + b.Move(f32.Point{X: w, Y: h - se}) + b.Cube(f32.Point{X: 0, Y: se * c}, f32.Point{X: -se + se*c, Y: se}, f32.Point{X: -se, Y: se}) // SE + b.Line(f32.Point{X: sw - w + se, Y: 0}) + b.Cube(f32.Point{X: -sw * c, Y: 0}, f32.Point{X: -sw, Y: -sw + sw*c}, f32.Point{X: -sw, Y: -sw}) // SW + b.Line(f32.Point{X: 0, Y: nw - h + sw}) + b.Cube(f32.Point{X: 0, Y: -nw * c}, f32.Point{X: nw - nw*c, Y: -nw}, f32.Point{X: nw, Y: -nw}) // NW + b.Line(f32.Point{X: w - ne - nw, Y: 0}) + b.Cube(f32.Point{X: ne * c, Y: 0}, f32.Point{X: ne, Y: ne - ne*c}, f32.Point{X: ne, Y: ne}) // NE + b.End() +} + + +func (b *Button) Layout(c ui.Config, ops *ui.Ops, q input.Queue, cs layout.Constraints) layout.Dimensions { + b.clicked = false + for ev, ok := b.Click.Next(q); ok; ev, ok = b.Click.Next(q) { + if ev.Type == gesture.TypeClick { + b.clicked = true + } + } + ins := layout.UniformInset(ui.Dp(1)) + cs = ins.Begin(c, ops, cs) + var dims layout.Dimensions + st := layout.Stack{} + st.Init(ops, cs) + { + cs = st.Rigid() + l := text.Label{ + Face: b.Face, + Text: b.Label, + } + ins := layout.UniformInset(ui.Dp(4)) + 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) + } + c2 := st.End(dims) + c1 := st.End(layoutRRect(b.Color, c, ops, st.Expand())) + dims = st.Layout(c1, c2) + return ins.End(dims) +} + +func (b *Button) Clicked() bool { + return b.clicked +} + +func eventLoop() { + w := app.NewWindow(app.WithWidth(ui.Dp(250))) + q := w.Queue() + _ = q + ops := new(ui.Ops) + var faces measure.Faces + regular, err := sfnt.Parse(goregular.TTF) + if err != nil { + log(Fatal, "Cannot parse font.") + } + face := faces.For(regular, ui.Sp(16)) + + margin := layout.UniformInset(ui.Dp(10)) + passInput := &text.Editor{Face: face, SingleLine: true} + passInput.SetText("passphrase") + passSubmit := &Button{Face: face, Label: "submit"} + lst := &layout.List{Axis: layout.Vertical} + passBtns := make([]*Button,0) + _ = passInput + _ = passSubmit + pathnames := make([]string,0) + + updateBtns := func() { + passBtns = passBtns[:0] + pathnames = pathnames[:0] + mux.Lock() + for _,x := range l { + _, n := path.Split(x.Pathname) + s := strings.Repeat(" /",x.Level) + z := "" + if x.Dir { z = "/" } + passBtns = append(passBtns,&Button{ + Face:face, + Label:strings.Join([]string{s,n,z},""), + Color: color.RGBA{A: 0xff, R: 0xf0, G: 0xf0, B: 0xf0}, + }) + pathnames = append(pathnames, x.Pathname) + } + mux.Unlock() + } + updateBtns() + + + for { + select { + case <-updated: + log(Info,"UPDATE") + updateBtns() + w.Invalidate() + case e := <-w.Events(): + switch e := e.(type) { + case app.DestroyEvent: + return + case app.UpdateEvent: + c := &e.Config + ops.Reset() + faces.Reset(c) + var dims layout.Dimensions + cs := layout.RigidConstraints(e.Size) + cs = margin.Begin(c, ops, cs) + + mux.Lock() + if lst.Dragging() { + key.HideInputOp{}.Add(ops) + } + for lst.Init(c, q, ops, cs, len(passBtns)); lst.More(); lst.Next() { + btn := passBtns[lst.Index()] + dims = btn.Layout(c, ops, q, lst.Constraints()) + lst.End(dims) + if btn.Clicked() { + // don't block UI thread on decryption attempt + log(Info,"Clicked ", btn.Label) + go func(name string) { + //p,err := store.Decrypt(name, prompt) + p,err := store.Decrypt(name) + if err == nil { + pass.Clip(p) + } else { + log(Info,"Can't decrypt ", name) + log(Info,err) + } + }(pathnames[lst.Index()]) + } + } + mux.Unlock() + dims = lst.Layout() + dims = margin.End(dims) + w.Update(ops) + } + } + } +} + diff --git a/cmd/gpass/main.go b/cmd/gpass/main.go new file mode 100644 index 0000000..7f26711 --- /dev/null +++ b/cmd/gpass/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "fmt" + "os" + "path" + "strings" + + "gitlab.wow.st/gmp/pass" +) + +func usage() { + fmt.Println("gpass [-c|--clip] [name]") + os.Exit(-1) +} + +type options struct { + clip bool +} + +func parse(args []string) ([]string,options) { + ret := make([]string,0) + opts := options{} + for _,a := range args { + switch { + case a == "-c",a == "--clip": + opts.clip = true + case a[0] == '-': + usage() + default: + ret = append(ret,a) + } + } + return ret,opts +} + +func main() { + store,err := pass.GetStore() + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + + args := os.Args[1:] + args,opts := parse(args) + switch len(args) { + case 0: + l,err := store.List() + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + for _, x := range l { + _, n := path.Split(x.Pathname) + s := strings.Repeat(" /",x.Level) + z := "" + if x.Dir { z = "/" } + fmt.Println(strings.Join([]string{s,n,z},"")) + } + case 1: + p,err := store.Decrypt(args[0]) + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + if opts.clip { + pass.Clip(p) + } else { + fmt.Print(p) + } + default: + usage() + } + pass.AskPass2() +} + diff --git a/impl_darwin.go b/impl_darwin.go new file mode 100644 index 0000000..81e8308 --- /dev/null +++ b/impl_darwin.go @@ -0,0 +1,85 @@ +//+build !android !linux + +package pass + +import ( + "fmt" + "bufio" + "bytes" + "os" + "os/exec" + "os/user" + "path" + "strconv" +) + +func setAgentInfo() { + // get UID + usr, err := user.Current() + if err != nil { + fmt.Printf("Error: cannot get user ID: %s\n", err) + return + } + uid := usr.Uid + + // get GPG Agent PID + output, err := exec.Command("pgrep", "-U", uid, "gpg-agent").Output() + if err != nil { + fmt.Printf("Error: %s\n", err) + return + } else { + fmt.Printf("gpg-agent process number is %s\n", output) + } + pid, err := strconv.Atoi(string(output[:len(output)-1])) + if err != nil { + fmt.Printf("Integer conversion failed: %s\n", err) + return + } + + // find agent socket file + cmd := exec.Command("lsof", "-w", "-Fn", "-u", uid, "-baUcgpg-agent") + stdout, err := cmd.StdoutPipe() + if err != nil { + fmt.Printf("Error: connect stdout to lsof: %s\n", err) + return + } + scanner := bufio.NewScanner(stdout) + err = cmd.Start() + if err != nil { + fmt.Printf("Error: cannot run lsof: %s\n", err) + } + + filename := "" + for scanner.Scan() { + x := scanner.Text() + if x[0] != 'n' { + continue + } + x = x[1:] + if path.Base(x) == "S.gpg-agent" { + fmt.Println(x) + filename = x + } + } + cmd.Wait() + + if filename == "" { + fmt.Printf("Error: gpg-agent socket file not found\n") + return + } + s := fmt.Sprintf("%s:%d:1", filename, pid) + fmt.Printf("GPG_AGENT_INFO = %s\n", s) + os.Setenv("GPG_AGENT_INFO",s) +} + +func init() { + setAgentInfo() +} + +func Clip(x string) { + b := bytes.NewBuffer([]byte(x)) + cmd := exec.Command("pbcopy") + cmd.Stdin = b + cmd.Run() +} + diff --git a/main.go b/main.go new file mode 100644 index 0000000..83b7b69 --- /dev/null +++ b/main.go @@ -0,0 +1,217 @@ +package pass + +import ( + "bufio" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "os" + "os/user" + "path" + "regexp" + "sort" + "strings" + + "github.com/jcmdev0/gpgagent" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/packet" +) + +var ( + basename *regexp.Regexp +) + +func init() { + basename = regexp.MustCompile(".gpg$") +} + +type Store struct { + Dir string + keyring openpgp.KeyRing +} + +func GetStore() (*Store, error) { + ret := &Store{} + u, err := user.Current() + if err != nil { + return ret, fmt.Errorf("Can't get current user.") + } + ret.Dir = path.Join(u.HomeDir,".password-store") + + fd, err := os.Open(path.Join(u.HomeDir,".gnupg/secring.gpg")) + defer fd.Close() + if err != nil { + return ret, fmt.Errorf("Can't open keyring file") + } + kr, err := openpgp.ReadKeyRing(fd) + if err != nil { + return ret, fmt.Errorf("Can't open gnupg keyring.") + } + ret.keyring = kr + return ret, nil +} + +type caseInsensitive []os.FileInfo + +func (fs caseInsensitive) Len() int { return len(fs) } +func (fs caseInsensitive) Swap(i, j int) { fs[i], fs[j] = fs[j], fs[i] } +func (fs caseInsensitive) Less(i, j int) bool { + return strings.ToLower(fs[i].Name()) < strings.ToLower(fs[j].Name()) +} + +type Pass struct { + Pathname string + Level int + Dir bool +} + +func (s *Store) List() ([]Pass,error) { + return s.list(0,"") +} + +func (s *Store) list(level int, p string) ([]Pass,error) { + ret := make([]Pass,0) + dir := path.Join(s.Dir,p) + fd, err := os.Open(dir) + defer fd.Close() + if err != nil { + return nil, fmt.Errorf("Cannot open password store") + } + files, err := fd.Readdir(0) + if err != nil { + return nil, fmt.Errorf("Cannot read password store") + } + sort.Sort(caseInsensitive(files)) + for _, x := range files { + n := basename.ReplaceAllLiteralString(x.Name(),"") + entry := Pass{Pathname: path.Join(p,n),Level: level} + + if n[0] == '.' { + continue + } + if x.IsDir() { + entry.Dir = true + ret = append(ret,entry) + l,err := s.list(level+1,path.Join(p,x.Name())) + if err != nil { + return nil,err + } + ret = append(ret,l...) + } else { + ret = append(ret,entry) + } + } + return ret,nil +} + +func AskPass(prompts ...func() []byte) openpgp.PromptFunction { + var prompt func() []byte + if len(prompts) > 0 { + prompt = prompts[0] + } else { + prompt = func() []byte { + fmt.Print("AskPass(): enter passphrase: ") + reader := bufio.NewReader(os.Stdin) + text, _ := reader.ReadString('\n') + return []byte(text[:len(text)-1]) + } + } + + var err error + var passphrase []byte + dec := func(p *packet.PrivateKey) { + for i := 0; i<3; i++ { + if err = p.Decrypt(passphrase); err == nil { + break + } + passphrase = prompt() + } + } + + var ret openpgp.PromptFunction + ret = func(keys []openpgp.Key, symmetric bool) ([]byte, error) { + if !symmetric { + for _,k := range keys { + if p := k.PrivateKey; p != nil && p.Encrypted { + dec(p) + } + for _, s := range k.Entity.Subkeys { + if p := s.PrivateKey; p != nil && p.Encrypted { + dec(p) + } + } + } + } + return passphrase, err + } + return ret +} + +//https://github.com/jcmdev0/gpgagent/blob/master/example/example.go +func GPGPrompt(keys []openpgp.Key, symmetric bool) ([]byte, error) { + conn, err := gpgagent.NewGpgAgentConn() + if err != nil { + return nil, err + } + defer conn.Close() + + for _, key := range keys { + cacheId := strings.ToUpper(hex.EncodeToString(key.PublicKey.Fingerprint[:])) + request := gpgagent.PassphraseRequest{CacheKey: cacheId} + passphrase, err := conn.GetPassphrase(&request) + if err != nil { + return nil, err + } + err = key.PrivateKey.Decrypt([]byte(passphrase)) + if err != nil { + return nil, err + } + return []byte(passphrase), nil + } + return nil, fmt.Errorf("Unable to find key") +} + +func (s *Store) Decrypt(name string, prompts ...func() []byte) (string,error) { + var ask openpgp.PromptFunction + if len(prompts) > 0 { + ask = AskPass(prompts...) + } else { + ask = GPGPrompt + } + 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.") + } + fd, err := os.Open(file) + defer fd.Close() + if err != nil { + return "",err + } + var reader io.Reader + unarmor, err := armor.Decode(fd) + if err == nil { + reader = unarmor.Body + } else { + fd.Close() + fd, err = os.Open(file) + defer fd.Close() + if err != nil { + return "",err + } + reader = fd + } + md, err := openpgp.ReadMessage(reader,s.keyring,ask,nil) + if err != nil { + fmt.Println("Error reading message") + return "",err + } + ra, err := ioutil.ReadAll(md.UnverifiedBody) + if err != nil { + return "",err + } + return string(ra), nil +} +