init: pristine aerc 0.20.0 source
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package gpgbin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"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 := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return md, err
|
||||
}
|
||||
args := []string{"--decrypt"}
|
||||
g := newGpg(bytes.NewReader(orig), args)
|
||||
_ = g.cmd.Run()
|
||||
// Always parse stdout, even if there was an error running command.
|
||||
// We'll find the error in the parsing
|
||||
err = parseStatusFd(bytes.NewReader(g.stderr.Bytes()), md)
|
||||
|
||||
if errors.Is(err, NoValidOpenPgpData) {
|
||||
md.Body = bytes.NewReader(orig)
|
||||
return md, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md.Body = bytes.NewReader(g.stdout.Bytes())
|
||||
return md, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package gpgbin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
)
|
||||
|
||||
// Encrypt runs gpg --encrypt [--sign] -r [recipient]
|
||||
func Encrypt(r io.Reader, to []string, from string) ([]byte, error) {
|
||||
args := []string{
|
||||
"--armor",
|
||||
}
|
||||
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()
|
||||
var md models.MessageDetails
|
||||
err := parseStatusFd(bytes.NewReader(g.stderr.Bytes()), &md)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gpg: failure to encrypt: %w. check public key(s)", err)
|
||||
}
|
||||
|
||||
return g.stdout.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package gpgbin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/lib/pinentry"
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
)
|
||||
|
||||
// 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", "2", "--log-file", "/dev/null", "--batch")
|
||||
g.cmd.Args = append(g.cmd.Args, args...)
|
||||
g.cmd.Stdin = stdin
|
||||
g.cmd.Stdout = &g.stdout
|
||||
g.cmd.Stderr = &g.stderr
|
||||
|
||||
pinentry.SetCmdEnv(g.cmd)
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// 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
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
log.Errorf("gpg: failed to get identity: %v", err)
|
||||
return ""
|
||||
}
|
||||
out := strings.Split(outbuf.String(), "\n")
|
||||
for _, line := range out {
|
||||
if strings.HasPrefix(line, "uid") {
|
||||
flds := strings.Split(line, ":")
|
||||
return flds[9]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getKeyId returns the 16 digit key id, if key exists
|
||||
func getKeyId(s string, private bool) string {
|
||||
cmd := exec.Command("gpg", "--with-colons", "--batch")
|
||||
listArg := "--list-keys"
|
||||
if private {
|
||||
listArg = "--list-secret-keys"
|
||||
}
|
||||
cmd.Args = append(cmd.Args, listArg, s)
|
||||
|
||||
var outbuf strings.Builder
|
||||
cmd.Stdout = &outbuf
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
log.Errorf("gpg: failed to get key ID: %v", err)
|
||||
return ""
|
||||
}
|
||||
out := strings.Split(outbuf.String(), "\n")
|
||||
for _, line := range out {
|
||||
if strings.HasPrefix(line, "fpr") {
|
||||
flds := strings.Split(line, ":")
|
||||
id := flds[9]
|
||||
return id[len(id)-16:]
|
||||
}
|
||||
}
|
||||
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 parseStatusFd(r io.Reader, md *models.MessageDetails) error {
|
||||
var err error
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if field(line) == "PLAINTEXT_LENGTH" {
|
||||
continue
|
||||
}
|
||||
log.Tracef(line)
|
||||
|
||||
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 EncryptionFailed
|
||||
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 "INV_RECP":
|
||||
t := strings.SplitN(line, " ", 4)
|
||||
if t[2] == "10" {
|
||||
return fmt.Errorf("gpg: public key of %s is not trusted", t[3])
|
||||
}
|
||||
case "SIG_CREATED":
|
||||
fields := strings.Split(line, " ")
|
||||
micalg, err := strconv.Atoi(fields[4])
|
||||
if err != nil {
|
||||
return MicalgNotFound
|
||||
}
|
||||
md.Micalg = micalgs[micalg]
|
||||
case "VALIDSIG":
|
||||
fields := strings.Split(line, " ")
|
||||
micalg, err := strconv.Atoi(fields[9])
|
||||
if err != nil {
|
||||
return MicalgNotFound
|
||||
}
|
||||
md.Micalg = micalgs[micalg]
|
||||
case "NODATA":
|
||||
t := strings.SplitN(line, " ", 3)
|
||||
if t[2] == "4" {
|
||||
md.SignatureError = "gpg: no signature packet found"
|
||||
}
|
||||
if t[2] == "1" {
|
||||
return NoValidOpenPgpData
|
||||
}
|
||||
case "FAILURE":
|
||||
return fmt.Errorf("%s", strings.TrimPrefix(line, "[GNUPG:] "))
|
||||
}
|
||||
}
|
||||
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 StatusFdParsingError int32
|
||||
|
||||
const (
|
||||
EncryptionFailed StatusFdParsingError = iota + 1
|
||||
MicalgNotFound
|
||||
NoValidOpenPgpData
|
||||
)
|
||||
|
||||
func (err StatusFdParsingError) Error() string {
|
||||
switch err {
|
||||
case EncryptionFailed:
|
||||
return "gpg: decryption failed"
|
||||
case MicalgNotFound:
|
||||
return "gpg: micalg not found"
|
||||
case NoValidOpenPgpData:
|
||||
return "gpg: no valid OpenPGP data found"
|
||||
default:
|
||||
return "gpg: unknown status fd parsing error"
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package gpgbin
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// Import runs gpg --import-ownertrust and thus imports trusts for keys
|
||||
func ImportOwnertrust(r io.Reader) error {
|
||||
args := []string{"--import-ownertrust"}
|
||||
g := newGpg(r, args)
|
||||
err := g.cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package gpgbin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetPrivateKeyId runs gpg --list-secret-keys s
|
||||
func GetPrivateKeyId(s string) (string, error) {
|
||||
private := true
|
||||
id := getKeyId(s, private)
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("no private key found")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// GetKeyId runs gpg --list-keys s
|
||||
func GetKeyId(s string) (string, error) {
|
||||
private := false
|
||||
id := getKeyId(s, private)
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("no public key found")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// ExportPublicKey exports the public key identified by k in armor format
|
||||
func ExportPublicKey(k string) (io.Reader, error) {
|
||||
cmd := exec.Command("gpg", "--armor",
|
||||
"--export-options", "export-minimal", "--export", k)
|
||||
|
||||
var outbuf bytes.Buffer
|
||||
var stderr strings.Builder
|
||||
cmd.Stdout = &outbuf
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gpg: export failed: %w", err)
|
||||
}
|
||||
if strings.Contains(stderr.String(), "gpg") {
|
||||
return nil, fmt.Errorf("gpg: error exporting key")
|
||||
}
|
||||
return &outbuf, nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package gpgbin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"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()
|
||||
|
||||
var md models.MessageDetails
|
||||
err := parseStatusFd(bytes.NewReader(g.stderr.Bytes()), &md)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to parse messagedetails: %w", err)
|
||||
}
|
||||
|
||||
return g.stdout.Bytes(), md.Micalg, nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package gpgbin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"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 := os.CreateTemp("", "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 := io.ReadAll(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g := newGpg(bytes.NewReader(orig), args)
|
||||
_ = g.cmd.Run()
|
||||
|
||||
md := new(models.MessageDetails)
|
||||
_ = parseStatusFd(bytes.NewReader(g.stderr.Bytes()), md)
|
||||
|
||||
md.Body = bytes.NewReader(orig)
|
||||
|
||||
return md, nil
|
||||
}
|
||||
Reference in New Issue
Block a user