package passgo import ( "bufio" "fmt" "io" "io/ioutil" "log" "os" "os/user" "path" "regexp" "sort" "strings" "time" "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 useNative bool homeDir string ) func init() { useNative = 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) } // extract only the email address portion of the ID (required // for Android) if id[len(id)-1] == '\n' { id = id[:len(id)-1] } for i := len(id) - 1; i > 0; i-- { if id[i] == '>' { for j := i-1; j > 0; j-- { if id[j] == '<' { id = id[j+1:i] } } break } } store.Id = string(id) log.Printf("ID = %s", store.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 } useNative = 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 { log.Printf("password store directory = %s", dir) log.Printf("error is %s", err) 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 } func (s *Store) Decrypt(name string, prompts ...func() []byte) (string, error) { if useNative { return s.nativeDecrypt(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) { log.Printf("Identities()") getKeyring() if useNative { return nativeIdentities() } 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 useNative { // golang openpgp code not implemented yet fmt.Println("Calling nativeEncrypt") enc, err = s.nativeEncrypt(value) if err != nil { return err } //} return ioutil.WriteFile(path.Join(s.Dir, name+".gpg"), enc, 0644) }