|
package main |
|
|
|
import ( |
|
"bufio" |
|
"bytes" |
|
"crypto/md5" |
|
"encoding/hex" |
|
"errors" |
|
"fmt" |
|
"io" |
|
"io/ioutil" |
|
"log" |
|
"net" |
|
"net/url" |
|
"os" |
|
"os/exec" |
|
"strings" |
|
|
|
"golang.org/x/crypto/openpgp" |
|
) |
|
|
|
var l = log.New(os.Stderr, "", 0) |
|
|
|
func main() { |
|
if err := run(); err != nil { |
|
log.Fatal(err) |
|
} |
|
} |
|
|
|
func run() error { |
|
ring, err := loadSecring() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
combos := []struct { |
|
Name string |
|
Enc func(string, openpgp.EntityList) (string, error) |
|
Dec func(string) (string, error) |
|
}{ |
|
{Name: "enc=gpg dec=gpg ", Enc: encryptGPG, Dec: decryptGPG}, |
|
{Name: "enc=gpg dec=openpgp ", Enc: encryptGPG, Dec: decryptOpenPGP}, |
|
{Name: "enc=openpgp dec=gpg ", Enc: encryptOpenPGP, Dec: decryptGPG}, |
|
{Name: "enc=openpgp dec=openpgp", Enc: encryptOpenPGP, Dec: decryptOpenPGP}, |
|
} |
|
|
|
for _, c := range combos { |
|
for _, pt := range []string{"hello world", "\n", "\t", "\r", "hello\t\r\nworld\r\n"} { |
|
enc, err := c.Enc(pt, ring) |
|
if err != nil { |
|
return err |
|
} |
|
dec, err := c.Dec(enc) |
|
if err != nil { |
|
return err |
|
} |
|
status := "OK " |
|
if pt != dec { |
|
status = "ERR" |
|
} |
|
l.Printf("%s %s input=%q output=%q", c.Name, status, pt, dec) |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
func decryptGPG(in string) (string, error) { |
|
c := exec.Command("gpg", "--no-tty", "-q") |
|
stderr := &bytes.Buffer{} |
|
stdout := &bytes.Buffer{} |
|
c.Stdin = strings.NewReader(in) |
|
c.Stdout = stdout |
|
c.Stderr = io.MultiWriter(stderr, os.Stdout) |
|
if err := c.Run(); err != nil { |
|
return "", fmt.Errorf("%s: %s", err, stderr.String()) |
|
} |
|
return stdout.String(), nil |
|
} |
|
|
|
func md5sum(in string) string { |
|
cs := md5.New() |
|
_, _ = io.WriteString(cs, in) |
|
return hex.EncodeToString(cs.Sum(nil)) |
|
} |
|
|
|
func encryptGPG(in string, list openpgp.EntityList) (string, error) { |
|
args := []string{"-e"} |
|
for _, r := range list { |
|
if r.PrimaryKey == nil { |
|
continue |
|
} |
|
args = append(args, "-r", r.PrimaryKey.KeyIdShortString()) |
|
} |
|
stderr := &bytes.Buffer{} |
|
stdout := &bytes.Buffer{} |
|
c := exec.Command("gpg", args...) |
|
c.Stdin = strings.NewReader(in) |
|
c.Stdout = stdout |
|
c.Stderr = io.MultiWriter(stderr, os.Stderr) |
|
if err := c.Run(); err != nil { |
|
return "", err |
|
} |
|
return stdout.String(), nil |
|
} |
|
|
|
func encryptOpenPGP(input string, list openpgp.EntityList) (string, error) { |
|
buf := &bytes.Buffer{} |
|
wc, err := openpgp.Encrypt(buf, list, nil, &openpgp.FileHints{IsBinary: true}, nil) |
|
if err != nil { |
|
return "", err |
|
} |
|
_, err = io.WriteString(wc, input) |
|
if err != nil { |
|
return "", err |
|
|
|
} |
|
if err := wc.Close(); err != nil { |
|
return "", err |
|
} |
|
return buf.String(), nil |
|
} |
|
|
|
func loadSecring() (openpgp.EntityList, error) { |
|
f, err := os.Open(os.ExpandEnv("$HOME/.gnupg/secring.gpg")) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer func() { |
|
if err := f.Close(); err != nil { |
|
l.Printf("err=%q", err) |
|
} |
|
}() |
|
return openpgp.ReadKeyRing(f) |
|
} |
|
|
|
func decryptOpenPGP(in string) (string, error) { |
|
con, err := NewGpgAgentConn() |
|
if err != nil { |
|
return "", err |
|
} |
|
defer con.Close() |
|
ring, err := loadSecring() |
|
if err != nil { |
|
return "", err |
|
} |
|
m, err := openpgp.ReadMessage(strings.NewReader(in), ring, Prompt(con), nil) |
|
if err != nil { |
|
return "", err |
|
} |
|
b, err := ioutil.ReadAll(m.UnverifiedBody) |
|
if err != nil && err != io.EOF { |
|
return "", err |
|
} |
|
return string(b), nil |
|
} |
|
|
|
// GPG Agent |
|
func Prompt(con *Conn) func(keys []openpgp.Key, symmetric bool) ([]byte, error) { |
|
return func(keys []openpgp.Key, symmetric bool) ([]byte, error) { |
|
for _, k := range keys { |
|
ids := []string{} |
|
for i := range k.Entity.Identities { |
|
ids = append(ids, i) |
|
} |
|
prompt := "passphrase for gpg key " + k.PrivateKey.KeyIdShortString() |
|
cacheKey := k.PrivateKey.KeyIdString() |
|
preq := &PassphraseRequest{CacheKey: cacheKey, Prompt: prompt, Desc: strings.Join(ids, ", ")} |
|
pp, err := con.GetPassphrase(preq) |
|
if err != nil { |
|
continue |
|
} |
|
if err := k.PrivateKey.Decrypt([]byte(pp)); err == nil { |
|
return nil, nil |
|
} else { |
|
con.RemoveFromCache(cacheKey) |
|
} |
|
} |
|
return nil, fmt.Errorf("unable to decrypt message. tried %d keys", len(keys)) |
|
} |
|
} |
|
|
|
// Conn is a connection to the GPG agent. |
|
type Conn struct { |
|
c io.ReadWriteCloser |
|
br *bufio.Reader |
|
} |
|
|
|
var ( |
|
ErrNoAgent = errors.New("GPG_AGENT_INFO not set in environment") |
|
ErrNoData = errors.New("GPG_ERR_NO_DATA cache miss") |
|
ErrCancel = errors.New("gpgagent: Cancel") |
|
) |
|
|
|
// NewGpgAgentConn connects to the GPG Agent as described in the |
|
// GPG_AGENT_INFO environment variable. |
|
func NewGpgAgentConn() (*Conn, error) { |
|
sp := strings.SplitN(os.Getenv("GPG_AGENT_INFO"), ":", 3) |
|
if len(sp) == 0 || len(sp[0]) == 0 { |
|
return nil, ErrNoAgent |
|
} |
|
addr := &net.UnixAddr{Net: "unix", Name: sp[0]} |
|
uc, err := net.DialUnix("unix", nil, addr) |
|
if err != nil { |
|
return nil, err |
|
} |
|
br := bufio.NewReader(uc) |
|
lineb, err := br.ReadSlice('\n') |
|
if err != nil { |
|
return nil, err |
|
} |
|
line := string(lineb) |
|
if !strings.HasPrefix(line, "OK") { |
|
return nil, fmt.Errorf("gpgagent: didn't get OK; got %q", line) |
|
} |
|
return &Conn{uc, br}, nil |
|
} |
|
|
|
func (c *Conn) Close() error { |
|
c.br = nil |
|
return c.c.Close() |
|
} |
|
|
|
// PassphraseRequest is a request to get a passphrase from the GPG |
|
// Agent. |
|
type PassphraseRequest struct { |
|
CacheKey, Error, Prompt, Desc string |
|
|
|
// If the option --no-ask is used and the passphrase is not in |
|
// the cache the user will not be asked to enter a passphrase |
|
// but the error code GPG_ERR_NO_DATA is returned. (ErrNoData) |
|
NoAsk bool |
|
} |
|
|
|
func (c *Conn) RemoveFromCache(cacheKey string) error { |
|
_, err := fmt.Fprintf(c.c, "CLEAR_PASSPHRASE %s\n", url.QueryEscape(cacheKey)) |
|
if err != nil { |
|
return err |
|
} |
|
lineb, err := c.br.ReadSlice('\n') |
|
if err != nil { |
|
return err |
|
} |
|
line := string(lineb) |
|
if !strings.HasPrefix(line, "OK") { |
|
return fmt.Errorf("gpgagent: CLEAR_PASSPHRASE returned %q", line) |
|
} |
|
return nil |
|
} |
|
|
|
func (c *Conn) GetPassphrase(pr *PassphraseRequest) (passphrase string, outerr error) { |
|
defer func() { |
|
if e, ok := recover().(string); ok { |
|
passphrase = "" |
|
outerr = errors.New(e) |
|
} |
|
}() |
|
set := func(cmd string, val string) { |
|
if val == "" { |
|
return |
|
} |
|
_, err := fmt.Fprintf(c.c, "%s %s\n", cmd, val) |
|
if err != nil { |
|
panic("gpgagent: failed to send " + cmd) |
|
} |
|
line, _, err := c.br.ReadLine() |
|
if err != nil { |
|
panic("gpgagent: failed to read " + cmd) |
|
} |
|
if !strings.HasPrefix(string(line), "OK") { |
|
panic("gpgagent: response to " + cmd + " was " + string(line)) |
|
} |
|
} |
|
if d := os.Getenv("DISPLAY"); d != "" { |
|
set("OPTION", "display="+d) |
|
} |
|
tty, err := os.Readlink("/proc/self/fd/0") |
|
if err == nil { |
|
set("OPTION", "ttyname="+tty) |
|
} |
|
set("OPTION", "ttytype="+os.Getenv("TERM")) |
|
opts := "" |
|
if pr.NoAsk { |
|
opts += "--no-ask " |
|
} |
|
|
|
encOrX := func(s string) string { |
|
if s == "" { |
|
return "X" |
|
} |
|
return url.QueryEscape(s) |
|
} |
|
|
|
_, err = fmt.Fprintf(c.c, "GET_PASSPHRASE %s%s %s %s %s\n", |
|
opts, |
|
url.QueryEscape(pr.CacheKey), |
|
encOrX(pr.Error), |
|
encOrX(pr.Prompt), |
|
encOrX(pr.Desc)) |
|
if err != nil { |
|
return "", err |
|
} |
|
lineb, err := c.br.ReadSlice('\n') |
|
if err != nil { |
|
return "", err |
|
} |
|
line := string(lineb) |
|
if strings.HasPrefix(line, "OK ") { |
|
decb, err := hex.DecodeString(line[3 : len(line)-1]) |
|
if err != nil { |
|
return "", err |
|
} |
|
return string(decb), nil |
|
} |
|
fields := strings.Split(line, " ") |
|
if len(fields) >= 2 && fields[0] == "ERR" { |
|
switch fields[1] { |
|
case "67108922": |
|
return "", ErrNoData |
|
case "83886179": |
|
return "", ErrCancel |
|
} |
|
} |
|
return "", errors.New(line) |
|
} |
Can you update the output to match the current program?
The program shows two decodings for each input but the output only shows one.
Thanks.