Compatibility with gpg 2.0 -- call out to gpg commands (on Darwin)
if go openpgp is not compatibile with the local gpg installation.
This commit is contained in:
parent
44cab96fff
commit
09859ca0a9
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
107
main.go
107
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,18 +49,68 @@ 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.")
|
||||
}
|
||||
store.keyring = kr
|
||||
//return fmt.Errorf("Can't open gnupg keyring.")
|
||||
return nil
|
||||
}
|
||||
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
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user