package passgo 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 ask openpgp.PromptFunction ) 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) { if ask == nil { ask = AskPass(prompts...) } 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 }