aboutsummaryrefslogtreecommitdiffstats
path: root/lib/crypto/gpg/gpgbin
diff options
context:
space:
mode:
Diffstat (limited to 'lib/crypto/gpg/gpgbin')
-rw-r--r--lib/crypto/gpg/gpgbin/decrypt.go34
-rw-r--r--lib/crypto/gpg/gpgbin/encrypt.go35
-rw-r--r--lib/crypto/gpg/gpgbin/gpgbin.go262
-rw-r--r--lib/crypto/gpg/gpgbin/import.go16
-rw-r--r--lib/crypto/gpg/gpgbin/sign.go27
-rw-r--r--lib/crypto/gpg/gpgbin/verify.go41
6 files changed, 415 insertions, 0 deletions
diff --git a/lib/crypto/gpg/gpgbin/decrypt.go b/lib/crypto/gpg/gpgbin/decrypt.go
new file mode 100644
index 00000000..4b8d8f2b
--- /dev/null
+++ b/lib/crypto/gpg/gpgbin/decrypt.go
@@ -0,0 +1,34 @@
+package gpgbin
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+
+ "git.sr.ht/~rjarry/aerc/models"
+)
+
+// Decrypt runs gpg --decrypt on the contents of r. If the packet is signed,
+// the signature is also verified
+func Decrypt(r io.Reader) (*models.MessageDetails, error) {
+ md := new(models.MessageDetails)
+ orig, err := ioutil.ReadAll(r)
+ if err != nil {
+ return md, err
+ }
+ args := []string{"--decrypt"}
+ g := newGpg(bytes.NewReader(orig), args)
+ err = g.cmd.Run()
+ if err != nil {
+ err = parseError(g.stderr.String())
+ switch GPGErrors[err.Error()] {
+ case ERROR_NO_PGP_DATA_FOUND:
+ md.Body = bytes.NewReader(orig)
+ return md, nil
+ default:
+ }
+ }
+ outRdr := bytes.NewReader(g.stdout.Bytes())
+ parse(outRdr, md)
+ return md, nil
+}
diff --git a/lib/crypto/gpg/gpgbin/encrypt.go b/lib/crypto/gpg/gpgbin/encrypt.go
new file mode 100644
index 00000000..4cbac37b
--- /dev/null
+++ b/lib/crypto/gpg/gpgbin/encrypt.go
@@ -0,0 +1,35 @@
+package gpgbin
+
+import (
+ "bytes"
+ "io"
+
+ "git.sr.ht/~rjarry/aerc/models"
+)
+
+// Encrypt runs gpg --encrypt [--sign] -r [recipient]. The default is to have
+// --trust-model always set
+func Encrypt(r io.Reader, to []string, from string) ([]byte, error) {
+ //TODO probably shouldn't have --trust-model always a default
+ args := []string{
+ "--armor",
+ "--trust-model", "always",
+ }
+ if from != "" {
+ args = append(args, "--sign", "--default-key", from)
+ }
+ for _, rcpt := range to {
+ args = append(args, "--recipient", rcpt)
+ }
+ args = append(args, "--encrypt", "-")
+
+ g := newGpg(r, args)
+ g.cmd.Run()
+ outRdr := bytes.NewReader(g.stdout.Bytes())
+ var md models.MessageDetails
+ parse(outRdr, &md)
+ var buf bytes.Buffer
+ io.Copy(&buf, md.Body)
+
+ return buf.Bytes(), nil
+}
diff --git a/lib/crypto/gpg/gpgbin/gpgbin.go b/lib/crypto/gpg/gpgbin/gpgbin.go
new file mode 100644
index 00000000..da046f46
--- /dev/null
+++ b/lib/crypto/gpg/gpgbin/gpgbin.go
@@ -0,0 +1,262 @@
+package gpgbin
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+
+ "git.sr.ht/~rjarry/aerc/models"
+ "github.com/mattn/go-isatty"
+)
+
+// gpg represents a gpg command with buffers attached to stdout and stderr
+type gpg struct {
+ cmd *exec.Cmd
+ stdout bytes.Buffer
+ stderr bytes.Buffer
+}
+
+// newGpg creates a new gpg command with buffers attached
+func newGpg(stdin io.Reader, args []string) *gpg {
+ g := new(gpg)
+ g.cmd = exec.Command("gpg", "--status-fd", "1", "--batch")
+ g.cmd.Args = append(g.cmd.Args, args...)
+ g.cmd.Stdin = stdin
+ g.cmd.Stdout = &g.stdout
+ g.cmd.Stderr = &g.stderr
+
+ return g
+}
+
+// parseError parses errors returned by gpg that don't show up with a [GNUPG:]
+// prefix
+func parseError(s string) error {
+ lines := strings.Split(s, "\n")
+ for _, line := range lines {
+ line = strings.ToLower(line)
+ if GPGErrors[line] > 0 {
+ return errors.New(line)
+ }
+ }
+ return errors.New("unknown gpg error")
+}
+
+// fields returns the field name from --status-fd output. See:
+// https://github.com/gpg/gnupg/blob/master/doc/DETAILS
+func field(s string) string {
+ tokens := strings.SplitN(s, " ", 3)
+ if tokens[0] == "[GNUPG:]" {
+ return tokens[1]
+ }
+ return ""
+}
+
+// getIdentity returns the identity of the given key
+func getIdentity(key uint64) string {
+ fpr := fmt.Sprintf("%X", key)
+ cmd := exec.Command("gpg", "--with-colons", "--batch", "--list-keys", fpr)
+
+ var outbuf strings.Builder
+ cmd.Stdout = &outbuf
+ cmd.Run()
+ out := strings.Split(outbuf.String(), "\n")
+ for _, line := range out {
+ if strings.HasPrefix(line, "uid") {
+ flds := strings.Split(line, ":")
+ return flds[9]
+ }
+ }
+ return ""
+}
+
+// longKeyToUint64 returns a uint64 version of the given key
+func longKeyToUint64(key string) (uint64, error) {
+ fpr := string(key[len(key)-16:])
+ fprUint64, err := strconv.ParseUint(fpr, 16, 64)
+ if err != nil {
+ return 0, err
+ }
+ return fprUint64, nil
+}
+
+// parse parses the output of gpg --status-fd
+func parse(r io.Reader, md *models.MessageDetails) error {
+ var (
+ logOut io.Writer
+ logger *log.Logger
+ )
+ if !isatty.IsTerminal(os.Stdout.Fd()) {
+ logOut = os.Stdout
+ } else {
+ logOut = ioutil.Discard
+ os.Stdout, _ = os.Open(os.DevNull)
+ }
+ logger = log.New(logOut, "", log.LstdFlags)
+ var err error
+ var msgContent []byte
+ var msgCollecting bool
+ newLine := []byte("\r\n")
+ scanner := bufio.NewScanner(r)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if field(line) == "PLAINTEXT_LENGTH" {
+ continue
+ }
+ if strings.HasPrefix(line, "[GNUPG:]") {
+ msgCollecting = false
+ logger.Println(line)
+ }
+ if msgCollecting {
+ msgContent = append(msgContent, scanner.Bytes()...)
+ msgContent = append(msgContent, newLine...)
+ }
+
+ switch field(line) {
+ case "ENC_TO":
+ md.IsEncrypted = true
+ case "DECRYPTION_KEY":
+ md.DecryptedWithKeyId, err = parseDecryptionKey(line)
+ md.DecryptedWith = getIdentity(md.DecryptedWithKeyId)
+ if err != nil {
+ return err
+ }
+ case "DECRYPTION_FAILED":
+ return fmt.Errorf("gpg: decryption failed")
+ case "PLAINTEXT":
+ msgCollecting = true
+ case "NEWSIG":
+ md.IsSigned = true
+ case "GOODSIG":
+ t := strings.SplitN(line, " ", 4)
+ md.SignedByKeyId, err = longKeyToUint64(t[2])
+ if err != nil {
+ return err
+ }
+ md.SignedBy = t[3]
+ case "BADSIG":
+ t := strings.SplitN(line, " ", 4)
+ md.SignedByKeyId, err = longKeyToUint64(t[2])
+ if err != nil {
+ return err
+ }
+ md.SignatureError = "gpg: invalid signature"
+ md.SignedBy = t[3]
+ case "EXPSIG":
+ t := strings.SplitN(line, " ", 4)
+ md.SignedByKeyId, err = longKeyToUint64(t[2])
+ if err != nil {
+ return err
+ }
+ md.SignatureError = "gpg: expired signature"
+ md.SignedBy = t[3]
+ case "EXPKEYSIG":
+ t := strings.SplitN(line, " ", 4)
+ md.SignedByKeyId, err = longKeyToUint64(t[2])
+ if err != nil {
+ return err
+ }
+ md.SignatureError = "gpg: signature made with expired key"
+ md.SignedBy = t[3]
+ case "REVKEYSIG":
+ t := strings.SplitN(line, " ", 4)
+ md.SignedByKeyId, err = longKeyToUint64(t[2])
+ if err != nil {
+ return err
+ }
+ md.SignatureError = "gpg: signature made with revoked key"
+ md.SignedBy = t[3]
+ case "ERRSIG":
+ t := strings.SplitN(line, " ", 9)
+ md.SignedByKeyId, err = longKeyToUint64(t[2])
+ if err != nil {
+ return err
+ }
+ if t[7] == "9" {
+ md.SignatureError = "gpg: missing public key"
+ }
+ if t[7] == "4" {
+ md.SignatureError = "gpg: unsupported algorithm"
+ }
+ md.SignedBy = "(unknown signer)"
+ case "BEGIN_ENCRYPTION":
+ msgCollecting = true
+ case "SIG_CREATED":
+ fields := strings.Split(line, " ")
+ micalg, err := strconv.Atoi(fields[4])
+ if err != nil {
+ return fmt.Errorf("gpg: micalg not found")
+ }
+ md.Micalg = micalgs[micalg]
+ msgCollecting = true
+ case "VALIDSIG":
+ fields := strings.Split(line, " ")
+ micalg, err := strconv.Atoi(fields[9])
+ if err != nil {
+ return fmt.Errorf("gpg: micalg not found")
+ }
+ md.Micalg = micalgs[micalg]
+ case "NODATA":
+ md.SignatureError = "gpg: no signature packet found"
+ }
+ }
+ md.Body = bytes.NewReader(msgContent)
+ return nil
+}
+
+// parseDecryptionKey returns primary key from DECRYPTION_KEY line
+func parseDecryptionKey(l string) (uint64, error) {
+ key := strings.Split(l, " ")[3]
+ fpr := string(key[len(key)-16:])
+ fprUint64, err := longKeyToUint64(fpr)
+ if err != nil {
+ return 0, err
+ }
+ getIdentity(fprUint64)
+ return fprUint64, nil
+}
+
+type GPGError int32
+
+const (
+ ERROR_NO_PGP_DATA_FOUND GPGError = iota + 1
+)
+
+var GPGErrors = map[string]GPGError{
+ "gpg: no valid openpgp data found.": ERROR_NO_PGP_DATA_FOUND,
+}
+
+// micalgs represent hash algorithms for signatures. These are ignored by many
+// email clients, but can be used as an additional verification so are sent.
+// Both gpgmail and pgpmail implementations in aerc check for matching micalgs
+var micalgs = map[int]string{
+ 1: "pgp-md5",
+ 2: "pgp-sha1",
+ 3: "pgp-ripemd160",
+ 8: "pgp-sha256",
+ 9: "pgp-sha384",
+ 10: "pgp-sha512",
+ 11: "pgp-sha224",
+}
+
+func logger(s string) {
+ var (
+ logOut io.Writer
+ logger *log.Logger
+ )
+ if !isatty.IsTerminal(os.Stdout.Fd()) {
+ logOut = os.Stdout
+ } else {
+ logOut = ioutil.Discard
+ os.Stdout, _ = os.Open(os.DevNull)
+ }
+ logger = log.New(logOut, "", log.LstdFlags)
+ logger.Println(s)
+}
diff --git a/lib/crypto/gpg/gpgbin/import.go b/lib/crypto/gpg/gpgbin/import.go
new file mode 100644
index 00000000..49e178b5
--- /dev/null
+++ b/lib/crypto/gpg/gpgbin/import.go
@@ -0,0 +1,16 @@
+package gpgbin
+
+import (
+ "io"
+)
+
+// Import runs gpg --import and thus imports both private and public keys
+func Import(r io.Reader) error {
+ args := []string{"--import"}
+ g := newGpg(r, args)
+ err := g.cmd.Run()
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/lib/crypto/gpg/gpgbin/sign.go b/lib/crypto/gpg/gpgbin/sign.go
new file mode 100644
index 00000000..35ab7e7f
--- /dev/null
+++ b/lib/crypto/gpg/gpgbin/sign.go
@@ -0,0 +1,27 @@
+package gpgbin
+
+import (
+ "bytes"
+ "io"
+
+ "git.sr.ht/~rjarry/aerc/models"
+)
+
+// Sign creates a detached signature based on the contents of r
+func Sign(r io.Reader, from string) ([]byte, string, error) {
+ args := []string{
+ "--armor",
+ "--detach-sign",
+ "--default-key", from,
+ }
+
+ g := newGpg(r, args)
+ g.cmd.Run()
+
+ outRdr := bytes.NewReader(g.stdout.Bytes())
+ var md models.MessageDetails
+ parse(outRdr, &md)
+ var buf bytes.Buffer
+ io.Copy(&buf, md.Body)
+ return buf.Bytes(), md.Micalg, nil
+}
diff --git a/lib/crypto/gpg/gpgbin/verify.go b/lib/crypto/gpg/gpgbin/verify.go
new file mode 100644
index 00000000..be9f26f7
--- /dev/null
+++ b/lib/crypto/gpg/gpgbin/verify.go
@@ -0,0 +1,41 @@
+package gpgbin
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+ "os"
+
+ "git.sr.ht/~rjarry/aerc/models"
+)
+
+// Verify runs gpg --verify. If s is not nil, then gpg interprets the
+// arguments as a detached signature
+func Verify(m io.Reader, s io.Reader) (*models.MessageDetails, error) {
+ args := []string{"--verify"}
+ if s != nil {
+ // Detached sig, save the sig to a tmp file and send msg over stdin
+ sig, err := ioutil.TempFile("", "sig")
+ if err != nil {
+ return nil, err
+ }
+ io.Copy(sig, s)
+ sig.Close()
+ defer os.Remove(sig.Name())
+ args = append(args, sig.Name(), "-")
+ }
+ orig, err := ioutil.ReadAll(m)
+ if err != nil {
+ return nil, err
+ }
+ g := newGpg(bytes.NewReader(orig), args)
+ g.cmd.Run()
+
+ out := bytes.NewReader(g.stdout.Bytes())
+ md := new(models.MessageDetails)
+ parse(out, md)
+
+ md.Body = bytes.NewReader(orig)
+
+ return md, nil
+}