// +build darwin linux package main import ( "io/ioutil" "os" "path" "strconv" "strings" "sync" "time" "gioui.org/ui" "gioui.org/ui/app" "gioui.org/ui/key" "gioui.org/ui/layout" "gioui.org/ui/measure" "gioui.org/ui/text" "golang.org/x/image/font/sfnt" "github.com/fsnotify/fsnotify" "gopkg.in/yaml.v2" "git.wow.st/gmp/passgo" "git.wow.st/gmp/rand" ) type conf struct { StoreDir string ClearDelay int } func main() { var fd *os.File confFile := path.Join(confDir, "config.yml") if _, err := os.Stat(confFile); os.IsNotExist(err) { fd, err = os.Create(confFile) if err != nil { log(Fatal, "Cannot create configuration file: ", err) } } confbytes, err := ioutil.ReadFile(confFile) if err != nil { log(Fatal, "Cannot read configuration file: ", err) } if err = yaml.UnmarshalStrict(confbytes, &Config); err != nil { log(Fatal, "Cannot parse configuration file: ", err) } store.Dir = Config.StoreDir log(Info, " StoreDir = ", store.Dir) go func() { err = passgo.GetStore(&store) if err != nil { log(Info, err) } }() if Config.ClearDelay == 0 { Config.ClearDelay = 45 } if fd != nil { // we still have an empty conf file open: Config.StoreDir = store.Dir saveConf(fd) } 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() app.Main() log(Info, "Event loop returned") } var ( confDir string regular *sfnt.Font Config conf l []passgo.Pass mux sync.Mutex store passgo.Store reload chan struct{} updated chan struct{} chdir chan struct{} passch chan []byte ) func Updater() { update := func() { ltmp, err := store.List() if err != nil { log(Info, err) } mux.Lock() l = ltmp mux.Unlock() updated <- struct{}{} } update() watcher, err := fsnotify.NewWatcher() if err != nil { log(Fatal, err) } dir := store.Dir watcher.Add(dir) for { select { case <-reload: update() case <-watcher.Events: 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() } } } func saveConf(fds ...*os.File) { var fd *os.File var err error if len(fds) > 0 && fds[0] != nil { fd = fds[0] } else { fd, err = os.Create(path.Join(confDir, "config.yml")) if err != nil { log(Fatal, "Cannot open config file: ", err) } } defer fd.Close() confbytes, err := yaml.Marshal(Config) if err != nil { log(Fatal, "Cannot save configuration: ", err) } _, err = fd.Write(confbytes) if err != nil { log(Fatal, "Cannot write to configuration: ", err) } } func eventLoop() { w := app.NewWindow( app.WithWidth(ui.Dp(250)), app.WithTitle("passgo")) q := w.Queue() var c ui.Config ops := new(ui.Ops) var dims layout.Dimensions var cs layout.Constraints var margincs layout.Constraints var faces measure.Faces var c1 layout.FlexChild // flex child for title bar face := faces.For(regular, ui.Sp(16)) sysinset := &layout.Inset{} margin := layout.UniformInset(ui.Dp(10)) title := &text.Label{Face: face, Text: "passgo"} dotsBtn := &Button{ Face: face, Label: "\xe2\x8b\xae", Alignment: text.Middle, Color: black, Background: gray, } titleflex := &layout.Flex{Axis: layout.Horizontal} flex := &layout.Flex{Axis: layout.Vertical} lst := &layout.List{Axis: layout.Vertical} passBtns := make([]*Button, 0) pathnames := make([]string, 0) copied := &Overlay{Face: face, Text: "copied to clipboard", Color: black, Background: darkgray, Alignment: text.Middle, } cleared := &Overlay{Face: face, Text: "clipboard cleared", Color: black, Background: darkgray, Alignment: text.Middle, } overlay := copied var overlayStart time.Time 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}, ""), Background: gray, }) pathnames = append(pathnames, x.Pathname) } mux.Unlock() } 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", Alignment: text.Middle, Color: black, Background: gray, } storeDirLabel := &text.Label{Face: face, Text: "Store directory"} storeDirEd := &text.Editor{Face: face, SingleLine: true} storeDirEd.SetText(store.Dir) saveBtn := &Button{ Face: face, Label: "save", Alignment: text.End, Color: black, Background: gray, } backBtn := &Button{ Face: face, Label: "back", Alignment: text.End, 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} okBtn := &Button{ Face: face, Label: "ok", Alignment: text.End, 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 animOn := func() { log(Info, "animOn()") anim = time.NewTicker(time.Second / 120) animating = true w.Invalidate() } animOff := func() { log(Info, "animOff()") anim.Stop() animating = false } var listPage, idPage, insertPage, confirmPage, confPage, promptPage, page func() prompt := func() []byte { page = promptPage promptEd.SetText("") w.Invalidate() return <-passch } listPage = func() { // timing variables used for animation fade1a, fade1b := 1.5, 2.0 start2 := float64(Config.ClearDelay) fade2a, end := start2+1.5, start2+2.0 cs = flex.Flexible(1.0) mux.Lock() if lst.Dragging() { key.HideInputOp{}.Add(ops) } var c2 layout.FlexChild switch { case store.Empty: c2 = flex.End(confBtn.Layout(c, q, ops, cs)) if confBtn.Clicked() { log(Info, "Configure") w.Invalidate() page = confPage } 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] dims = btn.Layout(c, q, ops, lst.Constraints()) lst.End(dims) if btn.Clicked() { log(Info, "Clicked ", btn.Label) // don't block UI thread on decryption attempt go func(name string) { p, err := store.Decrypt(name, prompt) //p, err := store.Decrypt(name) if err == nil { passgo.Clip(p) overlayStart = time.Now() overlay = copied overlay.Color = black overlay.Background = darkgray w.Invalidate() go func() { time.Sleep(time.Millisecond * time.Duration(fade1a*1000)) animOn() }() go func() { time.Sleep(time.Millisecond * time.Duration(Config.ClearDelay*1000)) log(Info, "clearing clipboard") passgo.Clip("") }() } else { log(Info, "Can't decrypt ", name) log(Info, err) } }(pathnames[i]) } } c2 = flex.End(lst.Layout()) } mux.Unlock() flex.Layout(c1, c2) x := time.Since(overlayStart).Seconds() if x >= fade1b && x < start2 && animating { animOff() go func() { time.Sleep(time.Millisecond * time.Duration((start2-x)*1000)) w.Invalidate() time.Sleep(time.Millisecond * time.Duration((fade2a-start2)*1000)) animOn() }() } if (x >= fade1a && x < fade1b) || (x > fade2a && x < end) { if !animating { animOn() } var fade float64 switch { case x < fade1b: fade = (fade1b - x) / (fade1b - fade1a) case x > fade2a: fade = (end - x) / (end - fade2a) } overlay.Color.R = uint8(float64(black.R) * fade) overlay.Color.G = uint8(float64(black.G) * fade) overlay.Color.B = uint8(float64(black.B) * fade) overlay.Color.A = uint8(float64(black.A) * fade) overlay.Background.R = uint8(float64(darkgray.R) * fade) overlay.Background.G = uint8(float64(darkgray.G) * fade) overlay.Background.B = uint8(float64(darkgray.B) * fade) overlay.Background.A = uint8(float64(darkgray.A) * fade) } if x <= fade1b || (x > start2 && x < end) { if x > start2 && overlay == copied { overlay = cleared overlay.Color = black overlay.Background = darkgray } cs = margincs al := layout.Align{Alignment: layout.SE} cs = al.Begin(ops, cs) cs.Width.Min = cs.Width.Max dims = al.End(overlay.Layout(c, ops, cs)) } if x > start2 && x < fade2a { // animOff() } if animating && x > end { animOff() } } 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 genBtn := &SelButton{SelColor: darkgray} genBtn.Button = Button{Face: face, Label: "generate", Background: gray} symBtn := &SelButton{SelColor: gray, Selected: true} numBtn := &SelButton{SelColor: gray, Selected: true} symBtn.Button = Button{Face: face, Label: "@", Background: darkgray} numBtn.Button = Button{Face: face, Label: "#", Background: darkgray} lenEd := &text.Editor{Face: face, SingleLine: true, Alignment: text.End} lenEd.SetText("15") lBtn := &Button{Face: face, Label: "<", Background: gray} rBtn := &Button{Face: face, Label: ">", Background: gray} updatePw := func() { if !genBtn.Selected { passvalEd.SetText("") return } var gen rand.Generator switch { case symBtn.Selected && numBtn.Selected: gen = rand.Char case symBtn.Selected: gen = rand.LettersSymbols case numBtn.Selected: gen = rand.LetterDigits default: gen = rand.Letter } l,_ := strconv.Atoi(lenEd.Text()) pw,_ := rand.Slice(gen,l) passvalEd.SetText(string(pw)) } resetInsertPage := func() { passnameEd.SetText("") passvalEd.SetText("") genBtn.Deselect() } insertPage = func() { c2 := flex.End(insertLabel.Layout(ops, flex.Rigid())) 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())) al := &layout.Align{Alignment: layout.E} btnflx := &layout.Flex{Axis: layout.Horizontal} btnflx.Init(ops, al.Begin(ops, flex.Rigid())) bc1 := btnflx.End(lBtn.Layout(c, q, ops, btnflx.Rigid())) cs := btnflx.Rigid() cs.Width.Min = 60 bc2 := btnflx.End(lenEd.Layout(c, q, ops, cs)) bc3 := btnflx.End(rBtn.Layout(c, q, ops, btnflx.Rigid())) bc4 := btnflx.End(symBtn.Layout(c, q, ops, btnflx.Rigid())) bc5 := btnflx.End(numBtn.Layout(c, q, ops, btnflx.Rigid())) bc6 := btnflx.End(genBtn.Layout(c, q, ops, btnflx.Rigid())) c7 := flex.End(al.End(btnflx.Layout(bc1, bc2, bc3, bc4, bc5, bc6))) btnflx.Init(ops, al.Begin(ops, flex.Rigid())) bc1 = btnflx.End(backBtn.Layout(c, q, ops, btnflx.Rigid())) bc2 = btnflx.End(saveBtn.Layout(c, q, ops, btnflx.Rigid())) c8 := flex.End(al.End(btnflx.Layout(bc1, bc2))) flex.Layout(c1, c2, c3, c4, c5, c6, c7, c8) if lBtn.Clicked() { l,_ := strconv.Atoi(lenEd.Text()) if l > 0 { l -= 1 } lenEd.SetText(strconv.Itoa(l)) updatePw() w.Invalidate() } if rBtn.Clicked() { l,_ := strconv.Atoi(lenEd.Text()) lenEd.SetText(strconv.Itoa(l+1)) updatePw() w.Invalidate() } if genBtn.Clicked() { updatePw() w.Invalidate() } if symBtn.Clicked() { updatePw() w.Invalidate() } if numBtn.Clicked() { updatePw() w.Invalidate() } if backBtn.Clicked() { w.Invalidate() resetInsertPage() 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 resetInsertPage() store.Insert(insName, insValue) } } confPage = func() { cs = flex.Rigid() c2 := flex.End(storeDirLabel.Layout(ops, cs)) cs = flex.Rigid() c3 := flex.End(storeDirEd.Layout(c, q, ops, cs)) cs = flex.Rigid() al := &layout.Align{Alignment: layout.E} cs = al.Begin(ops, cs) btnflx := &layout.Flex{Axis: layout.Horizontal} btnflx.Init(ops, cs) cs = btnflx.Rigid() bc1 := btnflx.End(backBtn.Layout(c, q, ops, cs)) cs = btnflx.Rigid() bc2 := btnflx.End(saveBtn.Layout(c, q, ops, cs)) dims = btnflx.Layout(bc1, bc2) c4 := flex.End(al.End(dims)) flex.Layout(c1, c2, c3, c4) if backBtn.Clicked() { log(Info, "Back") storeDirEd.SetText(store.Dir) w.Invalidate() page = listPage } if saveBtn.Clicked() { 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() chdir <- struct{}{} }() w.Invalidate() page = listPage } } promptPage = func() { submit := false for e, ok := promptEd.Next(c, q); ok; e, ok = promptEd.Next(c, q) { switch e.(type) { case text.SubmitEvent: log(Info, "Submit") submit = true } } cs = flex.Rigid() c2 := flex.End(promptLabel.Layout(ops, cs)) cs = flex.Rigid() c3 := flex.End(promptEd.Layout(c, q, ops, cs)) cs = flex.Rigid() al := &layout.Align{Alignment: layout.E} cs = al.Begin(ops, cs) btnflx := &layout.Flex{Axis: layout.Horizontal} btnflx.Init(ops, cs) cs = btnflx.Rigid() bc1 := btnflx.End(backBtn.Layout(c, q, ops, cs)) cs = btnflx.Rigid() bc2 := btnflx.End(okBtn.Layout(c, q, ops, cs)) dims = btnflx.Layout(bc1, bc2) c4 := flex.End(al.End(dims)) flex.Layout(c1, c2, c3, c4) if submit || okBtn.Clicked() { log(Info, "Ok") go func() { // do not block UI thread passch <- []byte(promptEd.Text()) }() w.Invalidate() page = listPage } if backBtn.Clicked() { log(Info, "Back") go func() { passch <- nil // cancel prompt }() w.Invalidate() page = listPage } } page = listPage for { select { case <-updated: log(Info, "UPDATE") updateBtns() w.Invalidate() case <-anim.C: 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) cs = layout.RigidConstraints(e.Size) sysinset.Top = e.Insets.Top sysinset.Bottom = e.Insets.Bottom sysinset.Left = e.Insets.Left sysinset.Right = e.Insets.Right cs = sysinset.Begin(c, ops, cs) cs = margin.Begin(c, ops, cs) margincs = cs flex.Init(ops, cs) cs = flex.Rigid() titleflex.Init(ops, cs) cs = titleflex.Rigid() 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, ct3)) page() sysinset.End(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) } } } }