2019-09-05 06:41:39 -04:00
|
|
|
package passgo
|
2019-09-04 22:19:39 -04:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"os/user"
|
|
|
|
"path"
|
|
|
|
"regexp"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
2019-09-10 08:45:59 -04:00
|
|
|
"time"
|
2019-09-04 22:19:39 -04:00
|
|
|
|
|
|
|
"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
|
2019-09-05 06:41:39 -04:00
|
|
|
ask openpgp.PromptFunction
|
2019-09-10 08:45:59 -04:00
|
|
|
Keyring openpgp.KeyRing
|
|
|
|
krTime time.Time
|
|
|
|
useGPG bool
|
|
|
|
homeDir string
|
2019-09-04 22:19:39 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
2019-09-10 08:45:59 -04:00
|
|
|
useGPG = true // default unless we can get the Go openpgp code to work
|
2019-09-04 22:19:39 -04:00
|
|
|
basename = regexp.MustCompile(".gpg$")
|
|
|
|
}
|
|
|
|
|
|
|
|
type Store struct {
|
2019-09-05 06:41:39 -04:00
|
|
|
Dir string
|
2019-09-10 08:45:59 -04:00
|
|
|
Id string
|
|
|
|
Empty bool
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
|
2019-09-06 10:45:18 -04:00
|
|
|
func GetStore(store *Store) error {
|
2019-09-05 06:41:39 -04:00
|
|
|
u, err := user.Current()
|
|
|
|
if err != nil {
|
2019-09-06 10:45:18 -04:00
|
|
|
return fmt.Errorf("Can't get current user.")
|
|
|
|
}
|
|
|
|
if store.Dir == "" {
|
|
|
|
store.Dir = path.Join(u.HomeDir, ".password-store")
|
2019-09-05 06:41:39 -04:00
|
|
|
}
|
2019-09-10 08:45:59 -04:00
|
|
|
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()
|
|
|
|
}
|
2019-09-04 22:19:39 -04:00
|
|
|
|
2019-09-10 08:45:59 -04:00
|
|
|
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)
|
2019-09-04 22:19:39 -04:00
|
|
|
defer fd.Close()
|
|
|
|
if err != nil {
|
2019-09-10 08:45:59 -04:00
|
|
|
return nil
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
kr, err := openpgp.ReadKeyRing(fd)
|
|
|
|
if err != nil {
|
2019-09-10 08:45:59 -04:00
|
|
|
//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
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
2019-09-10 08:45:59 -04:00
|
|
|
s.Empty = false
|
2019-09-06 10:45:18 -04:00
|
|
|
return nil
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
|
2019-09-10 08:45:59 -04:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2019-09-04 22:19:39 -04:00
|
|
|
type caseInsensitive []os.FileInfo
|
|
|
|
|
2019-09-05 06:41:39 -04:00
|
|
|
func (fs caseInsensitive) Len() int { return len(fs) }
|
2019-09-04 22:19:39 -04:00
|
|
|
func (fs caseInsensitive) Swap(i, j int) { fs[i], fs[j] = fs[j], fs[i] }
|
|
|
|
func (fs caseInsensitive) Less(i, j int) bool {
|
2019-09-05 06:41:39 -04:00
|
|
|
return strings.ToLower(fs[i].Name()) < strings.ToLower(fs[j].Name())
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
type Pass struct {
|
|
|
|
Pathname string
|
2019-09-05 06:41:39 -04:00
|
|
|
Level int
|
|
|
|
Dir bool
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
|
2019-09-05 06:41:39 -04:00
|
|
|
func (s *Store) List() ([]Pass, error) {
|
|
|
|
return s.list(0, "")
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
|
2019-09-05 06:41:39 -04:00
|
|
|
func (s *Store) list(level int, p string) ([]Pass, error) {
|
|
|
|
ret := make([]Pass, 0)
|
|
|
|
dir := path.Join(s.Dir, p)
|
2019-09-04 22:19:39 -04:00
|
|
|
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 {
|
2019-09-05 06:41:39 -04:00
|
|
|
n := basename.ReplaceAllLiteralString(x.Name(), "")
|
|
|
|
entry := Pass{Pathname: path.Join(p, n), Level: level}
|
2019-09-04 22:19:39 -04:00
|
|
|
|
|
|
|
if n[0] == '.' {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if x.IsDir() {
|
|
|
|
entry.Dir = true
|
2019-09-05 06:41:39 -04:00
|
|
|
ret = append(ret, entry)
|
|
|
|
l, err := s.list(level+1, path.Join(p, x.Name()))
|
2019-09-04 22:19:39 -04:00
|
|
|
if err != nil {
|
2019-09-05 06:41:39 -04:00
|
|
|
return nil, err
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
2019-09-05 06:41:39 -04:00
|
|
|
ret = append(ret, l...)
|
2019-09-04 22:19:39 -04:00
|
|
|
} else {
|
2019-09-05 06:41:39 -04:00
|
|
|
ret = append(ret, entry)
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
}
|
2019-09-05 06:41:39 -04:00
|
|
|
return ret, nil
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2019-09-06 11:43:14 -04:00
|
|
|
dec := func(p *packet.PrivateKey) error {
|
2019-09-05 06:41:39 -04:00
|
|
|
for i := 0; i < 3; i++ {
|
2019-09-06 11:43:14 -04:00
|
|
|
if err := p.Decrypt(passphrase); err == nil {
|
2019-09-06 13:36:06 -04:00
|
|
|
passphrase = nil
|
2019-09-06 11:43:14 -04:00
|
|
|
return err
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
passphrase = prompt()
|
2019-09-06 11:43:14 -04:00
|
|
|
if passphrase == nil {
|
2019-09-06 13:36:06 -04:00
|
|
|
passphrase = nil
|
2019-09-06 11:43:14 -04:00
|
|
|
return fmt.Errorf("Passphrase entry aborted")
|
|
|
|
}
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
2019-09-06 13:36:06 -04:00
|
|
|
passphrase = nil
|
2019-09-06 11:43:14 -04:00
|
|
|
return fmt.Errorf("Passphrase entry failed")
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
var ret openpgp.PromptFunction
|
|
|
|
ret = func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
|
|
|
|
if !symmetric {
|
2019-09-05 06:41:39 -04:00
|
|
|
for _, k := range keys {
|
2019-09-04 22:19:39 -04:00
|
|
|
if p := k.PrivateKey; p != nil && p.Encrypted {
|
2019-09-06 11:43:14 -04:00
|
|
|
if err := dec(p); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
for _, s := range k.Entity.Subkeys {
|
|
|
|
if p := s.PrivateKey; p != nil && p.Encrypted {
|
2019-09-06 11:43:14 -04:00
|
|
|
if err := dec(p); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-09-06 11:43:14 -04:00
|
|
|
return passphrase, nil
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
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}
|
2019-09-06 13:36:06 -04:00
|
|
|
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
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
2019-09-06 13:36:06 -04:00
|
|
|
return nil, err
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("Unable to find key")
|
|
|
|
}
|
|
|
|
|
2019-09-05 06:41:39 -04:00
|
|
|
func (s *Store) Decrypt(name string, prompts ...func() []byte) (string, error) {
|
2019-09-10 08:45:59 -04:00
|
|
|
if useGPG {
|
|
|
|
return s.gpgDecrypt(name)
|
|
|
|
}
|
2019-09-04 22:43:29 -04:00
|
|
|
if ask == nil {
|
2019-09-04 22:19:39 -04:00
|
|
|
ask = AskPass(prompts...)
|
|
|
|
}
|
2019-09-05 06:41:39 -04:00
|
|
|
file := path.Join(s.Dir, strings.Join([]string{name, ".gpg"}, ""))
|
2019-09-04 22:19:39 -04:00
|
|
|
if _, err := os.Stat(file); os.IsNotExist(err) {
|
2019-09-05 06:41:39 -04:00
|
|
|
return "", fmt.Errorf("Not in password store.")
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
fd, err := os.Open(file)
|
|
|
|
defer fd.Close()
|
|
|
|
if err != nil {
|
2019-09-05 06:41:39 -04:00
|
|
|
return "", err
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
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 {
|
2019-09-05 06:41:39 -04:00
|
|
|
return "", err
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
reader = fd
|
|
|
|
}
|
2019-09-10 08:45:59 -04:00
|
|
|
md, err := openpgp.ReadMessage(reader, Keyring, ask, nil)
|
2019-09-04 22:19:39 -04:00
|
|
|
if err != nil {
|
|
|
|
fmt.Println("Error reading message")
|
2019-09-05 06:41:39 -04:00
|
|
|
return "", err
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
|
|
|
ra, err := ioutil.ReadAll(md.UnverifiedBody)
|
|
|
|
if err != nil {
|
2019-09-05 06:41:39 -04:00
|
|
|
return "", err
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|
2019-09-10 08:45:59 -04:00
|
|
|
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)
|
2019-09-04 22:19:39 -04:00
|
|
|
}
|