package passgo import ( "bufio" "encoding/hex" "fmt" "io" "io/ioutil" "os" "os/user" "path" "regexp" "sort" "strings" "time" "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 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 Id string Empty bool } func GetStore(store *Store) error { u, err := user.Current() if err != nil { return fmt.Errorf("Can't get current user.") } 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() } 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 nil } kr, err := openpgp.ReadKeyRing(fd) if err != nil { //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 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 passphrase []byte dec := func(p *packet.PrivateKey) error { for i := 0; i < 3; i++ { if err := p.Decrypt(passphrase); err == nil { passphrase = nil return err } passphrase = prompt() if passphrase == nil { passphrase = nil return fmt.Errorf("Passphrase entry aborted") } } passphrase = nil return fmt.Errorf("Passphrase entry failed") } 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 { if err := dec(p); err != nil { return nil, err } } for _, s := range k.Entity.Subkeys { if p := s.PrivateKey; p != nil && p.Encrypted { if err := dec(p); err != nil { return nil, err } } } } } return passphrase, nil } 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} for i := 0; i < 3; i++ { var passphrase string passphrase, err = conn.GetPassphrase(&request) if err != nil { continue } err = key.PrivateKey.Decrypt([]byte(passphrase)) if err != nil { conn.RemoveFromCache(cacheId) continue } return []byte(passphrase), nil } return nil, err } return nil, fmt.Errorf("Unable to find key") } func (s *Store) Decrypt(name string, prompts ...func() []byte) (string, error) { if useGPG { return s.gpgDecrypt(name) } 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, Keyring, ask, nil) if err != nil { fmt.Println("Error reading message") return "", err } ra, err := ioutil.ReadAll(md.UnverifiedBody) if err != nil { return "", err } 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) }