feat: add gpg integration
This commit adds gpg system integration. This is done through two new packages: gpgbin, which handles the system calls and parsing; and gpg which is mostly a copy of emersion/go-pgpmail with modifications to interface with package gpgbin. gpg includes tests for many cases, and by it's nature also tests package gpgbin. I separated these in case an external dependency is ever used for the gpg sys-calls/parsing (IE we mirror how go-pgpmail+openpgp currently are dependencies) Two new config options are introduced: * pgp-provider. If it is not explicitly set to "gpg", aerc will default to it's internal pgp provider * pgp-key-id: (Optionally) specify a key by short or long keyId Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
parent
d09636ee0b
commit
57699b1fa6
|
@ -3,6 +3,12 @@
|
||||||
|
|
||||||
[general]
|
[general]
|
||||||
#
|
#
|
||||||
|
# If set to "gpg", aerc will use system gpg binary and keystore for all crypto
|
||||||
|
# operations. Otherwise, the internal openpgp implemenation will be used.
|
||||||
|
#
|
||||||
|
# Default: internal
|
||||||
|
pgp-provider=internal
|
||||||
|
|
||||||
# By default, the file permissions of accounts.conf must be restrictive and
|
# By default, the file permissions of accounts.conf must be restrictive and
|
||||||
# only allow reading by the file owner (0600). Set this option to true to
|
# only allow reading by the file owner (0600). Set this option to true to
|
||||||
# ignore this permission check. Use this with care as it may expose your
|
# ignore this permission check. Use this with care as it may expose your
|
||||||
|
|
|
@ -104,6 +104,7 @@ type AccountConfig struct {
|
||||||
SignatureCmd string
|
SignatureCmd string
|
||||||
EnableFoldersSort bool `ini:"enable-folders-sort"`
|
EnableFoldersSort bool `ini:"enable-folders-sort"`
|
||||||
FoldersSort []string `ini:"folders-sort" delim:","`
|
FoldersSort []string `ini:"folders-sort" delim:","`
|
||||||
|
PgpKeyId string `ini:"pgp-key-id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BindingConfig struct {
|
type BindingConfig struct {
|
||||||
|
@ -248,6 +249,8 @@ func loadAccountConfig(path string) ([]AccountConfig, error) {
|
||||||
account.Archive = val
|
account.Archive = val
|
||||||
} else if key == "enable-folders-sort" {
|
} else if key == "enable-folders-sort" {
|
||||||
account.EnableFoldersSort, _ = strconv.ParseBool(val)
|
account.EnableFoldersSort, _ = strconv.ParseBool(val)
|
||||||
|
} else if key == "pgp-key-id" {
|
||||||
|
account.PgpKeyId = val
|
||||||
} else if key != "name" {
|
} else if key != "name" {
|
||||||
account.Params[key] = val
|
account.Params[key] = val
|
||||||
}
|
}
|
||||||
|
@ -582,13 +585,14 @@ func validateBorderChars(section *ini.Section, config *UIConfig) error {
|
||||||
|
|
||||||
func validatePgpProvider(section *ini.Section) error {
|
func validatePgpProvider(section *ini.Section) error {
|
||||||
m := map[string]bool{
|
m := map[string]bool{
|
||||||
|
"gpg": true,
|
||||||
"internal": true,
|
"internal": true,
|
||||||
}
|
}
|
||||||
for key, val := range section.KeysHash() {
|
for key, val := range section.KeysHash() {
|
||||||
switch key {
|
switch key {
|
||||||
case "pgp-provider":
|
case "pgp-provider":
|
||||||
if !m[strings.ToLower(val)] {
|
if !m[strings.ToLower(val)] {
|
||||||
return fmt.Errorf("%v must be 'internal'", key)
|
return fmt.Errorf("%v must be either 'gpg' or 'internal'", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,13 @@ These options are configured in the *[general]* section of aerc.conf.
|
||||||
*default-save-path*
|
*default-save-path*
|
||||||
Used as a default path for save operations if no other path is specified.
|
Used as a default path for save operations if no other path is specified.
|
||||||
|
|
||||||
|
*pgp-provider*
|
||||||
|
If set to "gpg", aerc will use system gpg binary and keystore for all
|
||||||
|
crypto operations. Otherwise, the internal openpgp implemenation will be
|
||||||
|
used.
|
||||||
|
|
||||||
|
Default: internal
|
||||||
|
|
||||||
*unsafe-accounts-conf*
|
*unsafe-accounts-conf*
|
||||||
By default, the file permissions of accounts.conf must be restrictive
|
By default, the file permissions of accounts.conf must be restrictive
|
||||||
and only allow reading by the file owner (_0600_). Set this option to
|
and only allow reading by the file owner (_0600_). Set this option to
|
||||||
|
@ -579,6 +586,10 @@ Note that many of these configuration options are written for you, such as
|
||||||
|
|
||||||
Default: none
|
Default: none
|
||||||
|
|
||||||
|
*pgp-key-id*
|
||||||
|
Specify the key id to use when signing a message. Can be either short or
|
||||||
|
long key id. If unset, aerc will look up the key by email
|
||||||
|
|
||||||
*postpone*
|
*postpone*
|
||||||
Specifies the folder to save postponed messages to.
|
Specifies the folder to save postponed messages to.
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"git.sr.ht/~rjarry/aerc/lib/crypto/gpg"
|
||||||
"git.sr.ht/~rjarry/aerc/lib/crypto/pgp"
|
"git.sr.ht/~rjarry/aerc/lib/crypto/pgp"
|
||||||
"git.sr.ht/~rjarry/aerc/models"
|
"git.sr.ht/~rjarry/aerc/models"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp"
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
|
@ -22,6 +23,8 @@ type Provider interface {
|
||||||
|
|
||||||
func New(s string) Provider {
|
func New(s string) Provider {
|
||||||
switch s {
|
switch s {
|
||||||
|
case "gpg":
|
||||||
|
return &gpg.Mail{}
|
||||||
default:
|
default:
|
||||||
return &pgp.Mail{}
|
return &pgp.Mail{}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
package gpg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"git.sr.ht/~rjarry/aerc/lib/crypto/gpg/gpgbin"
|
||||||
|
"git.sr.ht/~rjarry/aerc/models"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mail satisfies the PGPProvider interface in aerc
|
||||||
|
type Mail struct {
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mail) Init(l *log.Logger) error {
|
||||||
|
m.logger = l
|
||||||
|
_, err := exec.LookPath("gpg")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mail) Decrypt(r io.Reader, decryptKeys openpgp.PromptFunction) (*models.MessageDetails, error) {
|
||||||
|
gpgReader, err := Read(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
md := gpgReader.MessageDetails
|
||||||
|
md.SignatureValidity = models.Valid
|
||||||
|
if md.SignatureError != "" {
|
||||||
|
md.SignatureValidity = handleSignatureError(md.SignatureError)
|
||||||
|
}
|
||||||
|
return md, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mail) ImportKeys(r io.Reader) error {
|
||||||
|
return gpgbin.Import(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mail) Encrypt(buf *bytes.Buffer, rcpts []string, signer string, decryptKeys openpgp.PromptFunction, header *mail.Header) (io.WriteCloser, error) {
|
||||||
|
|
||||||
|
return Encrypt(buf, header.Header.Header, rcpts, signer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mail) Sign(buf *bytes.Buffer, signer string, decryptKeys openpgp.PromptFunction, header *mail.Header) (io.WriteCloser, error) {
|
||||||
|
return Sign(buf, header.Header.Header, signer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mail) Close() {}
|
||||||
|
|
||||||
|
func handleSignatureError(e string) models.SignatureValidity {
|
||||||
|
if e == "gpg: missing public key" {
|
||||||
|
return models.UnknownEntity
|
||||||
|
}
|
||||||
|
if e == "gpg: header hash does not match actual sig hash" {
|
||||||
|
return models.MicalgMismatch
|
||||||
|
}
|
||||||
|
return models.UnknownValidity
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
package gpg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.sr.ht/~rjarry/aerc/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHasGpg(t *testing.T) {
|
||||||
|
gpgmail := new(Mail)
|
||||||
|
hasGpg := gpgmail.Init(new(log.Logger))
|
||||||
|
|
||||||
|
if hasGpg != nil {
|
||||||
|
t.Errorf("System does not have GPG")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanUp() {
|
||||||
|
cmd := exec.Command("gpg", "--batch", "--yes", "--delete-secret-and-public-keys", testKeyId)
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Test cleanup failed: you may need to delete the test keys from your GPG keyring")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCRLF(s string) string {
|
||||||
|
return strings.ReplaceAll(s, "\n", "\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func deepEqual(t *testing.T, r *models.MessageDetails, expect *models.MessageDetails) {
|
||||||
|
var resBuf bytes.Buffer
|
||||||
|
if _, err := io.Copy(&resBuf, r.Body); err != nil {
|
||||||
|
t.Fatalf("io.Copy() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expBuf bytes.Buffer
|
||||||
|
if _, err := io.Copy(&expBuf, expect.Body); err != nil {
|
||||||
|
t.Fatalf("io.Copy() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resBuf.String() != expBuf.String() {
|
||||||
|
t.Errorf("MessagesDetails.Body = \n%v\n but want \n%v", resBuf.String(), expBuf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.IsEncrypted != expect.IsEncrypted {
|
||||||
|
t.Errorf("IsEncrypted = \n%v\n but want \n%v", r.IsEncrypted, expect.IsEncrypted)
|
||||||
|
}
|
||||||
|
if r.IsSigned != expect.IsSigned {
|
||||||
|
t.Errorf("IsSigned = \n%v\n but want \n%v", r.IsSigned, expect.IsSigned)
|
||||||
|
}
|
||||||
|
if r.SignedBy != expect.SignedBy {
|
||||||
|
t.Errorf("SignedBy = \n%v\n but want \n%v", r.SignedBy, expect.SignedBy)
|
||||||
|
}
|
||||||
|
if r.SignedByKeyId != expect.SignedByKeyId {
|
||||||
|
t.Errorf("SignedByKeyId = \n%v\n but want \n%v", r.SignedByKeyId, expect.SignedByKeyId)
|
||||||
|
}
|
||||||
|
if r.SignatureError != expect.SignatureError {
|
||||||
|
t.Errorf("SignatureError = \n%v\n but want \n%v", r.SignatureError, expect.SignatureError)
|
||||||
|
}
|
||||||
|
if r.DecryptedWith != expect.DecryptedWith {
|
||||||
|
t.Errorf("DecryptedWith = \n%v\n but want \n%v", r.DecryptedWith, expect.DecryptedWith)
|
||||||
|
}
|
||||||
|
if r.DecryptedWithKeyId != expect.DecryptedWithKeyId {
|
||||||
|
t.Errorf("DecryptedWithKeyId = \n%v\n but want \n%v", r.DecryptedWithKeyId, expect.DecryptedWithKeyId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testKeyId = `B1A8669354153B799F2217BF307215C13DF7A964`
|
||||||
|
|
||||||
|
const testPrivateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
+5anTnUn
|
||||||
|
=gemU
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----
|
||||||
|
`
|
||||||
|
|
||||||
|
const testPublicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQENBF5FJf8BCACvlKhSSsv4P8C3Wbv391SrNUBtFquoMuWKtuCr/Ks6KHuofGLn
|
||||||
|
bM55uBSQp908aITBDPkaOPsQ3OvwgF7SM8bNIDVpO7FHzCEg2Ysp99iPET/+LsbY
|
||||||
|
ugc8oYSuvA5aFFIOMYbAbI+HmbIBuCs+xp0AcU1cemAPzPBDCZs4xl5Y+/ce2yQz
|
||||||
|
ZGK9O/tQQIKoBUOWLo/0byAWyD6Gwn/Le3fVxxK6RPeeizDV6VfzHLxhxBNkMgmd
|
||||||
|
QUkBkvqF154wYxhzsHn72ushbJpspKz1LQN7d5u6QOq3h2sLwcLbT457qbMfZsZs
|
||||||
|
HLhoOibOd+yJ7C6TRbbyC4sQRr+K1CNGcvhJABEBAAG0NEpvaG4gRG9lIChUaGlz
|
||||||
|
IGlzIGEgdGVzdCBrZXkpIDxqb2huLmRvZUBleGFtcGxlLm9yZz6JAU4EEwEIADgW
|
||||||
|
IQSxqGaTVBU7eZ8iF78wchXBPfepZAUCXkUl/wIbAwULCQgHAgYVCgkICwIEFgID
|
||||||
|
AQIeAQIXgAAKCRAwchXBPfepZF4iB/49B7q4AfO3xHEa8LK2H+f7Mnm4dRfS2YPo
|
||||||
|
v2p6TRe1h2DxwpTevNQUhXw2U0nfRIEKBAZqgb7NVktkoh0DWtKatms2yHMAS+ah
|
||||||
|
lQoHb2gRgXa9M9Tq0x5u9sl0NYnx7Wu5uu6Ybw9luPKoAfO91T0vei0p3eMn3fIV
|
||||||
|
0O012ITvmgKJPppQDKFJHGZJMbVDO4TNxP89HgyhB41RO7AZadvu73S00x2K6x+O
|
||||||
|
R4s/++4Y98vScCPm3DUOXeoHXKGqFcNYTxJL9bsE2I0uYgvJSxNoK1dVnmvxp3zz
|
||||||
|
hcxAdzizgMz0ufY6YLMCjy5MDOzPARkmYPXdkJ6jceOIqGLUw1kquQENBF5FJf8B
|
||||||
|
CACpsh5cyHB7eEwQvLzJVsXpTW0Rh/Fe36AwC2Vz13WeE6GFrOvw1qATvtYB1919
|
||||||
|
M4B44YH9J7I5SrFZad86Aw4n5Gi0BwLlGNa/oCMvYzlNHaTXURA271ghJqdZizqV
|
||||||
|
UETj3WNoaYm4mYMfb0dcayDJvVPWP7InzOsdIRU9WXBUSyVMxNMXccr2UvIuhdPg
|
||||||
|
lmVT8NtsWR+q8xBoL2Dp0ojYLVD3MlwKe1pE5mEwasYCkWePLWyGdfDW1MhUDsPH
|
||||||
|
3K1IjpPLWU9FBk8KM4z8WooY9/kyMIyRw39MvOHGfgcFBpiZwlELNZGSFhbRun03
|
||||||
|
PMk2Qd3k+0FGV1IhFAYsr7QRABEBAAGJATYEGAEIACAWIQSxqGaTVBU7eZ8iF78w
|
||||||
|
chXBPfepZAUCXkUl/wIbDAAKCRAwchXBPfepZOtqB/9xsGEgQgm70KYID39H91k4
|
||||||
|
ef/RlpRDY1ndC0MoPfqE03IEXTC/MjtU+ksPKEoZeQsxVaUJ2WBueI5WGJ3Y73pO
|
||||||
|
HAd7N0SyGHT5s6gK1FSx29be1qiPwUu5KR2jpm3RjgpbymnOWe4C6iiYCFQ85IX+
|
||||||
|
LzpE+p9bB02PUrmzOb4MBV6E5mg30UjXIX01+bwZq5XSB4/FaUrQOAxLuRvVRjK0
|
||||||
|
CEcFbPGIlkPSW6s4M9xCC2sQi7caFKVK6Zqf78KbOwAHqfS0x9u2jtTIhsgCjGTI
|
||||||
|
AOQ5lNwpLEMjwLias6e5sM6hcK9Wo+A9Sw23f8lMau5clOZTJeyAUAff+5anTnUn
|
||||||
|
=ZjQT
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
`
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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,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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
// reader.go largerly mimics github.com/emersion/go-gpgmail, with changes made
|
||||||
|
// to interface with the gpg package in aerc
|
||||||
|
|
||||||
|
package gpg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.sr.ht/~rjarry/aerc/lib/crypto/gpg/gpgbin"
|
||||||
|
"git.sr.ht/~rjarry/aerc/models"
|
||||||
|
"github.com/emersion/go-message/textproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Reader struct {
|
||||||
|
Header textproto.Header
|
||||||
|
MessageDetails *models.MessageDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReader(h textproto.Header, body io.Reader) (*Reader, error) {
|
||||||
|
t, params, err := mime.ParseMediaType(h.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(t, "multipart/encrypted") && strings.EqualFold(params["protocol"], "application/pgp-encrypted") {
|
||||||
|
mr := textproto.NewMultipartReader(body, params["boundary"])
|
||||||
|
return newEncryptedReader(h, mr)
|
||||||
|
}
|
||||||
|
if strings.EqualFold(t, "multipart/signed") && strings.EqualFold(params["protocol"], "application/pgp-signature") {
|
||||||
|
micalg := params["micalg"]
|
||||||
|
mr := textproto.NewMultipartReader(body, params["boundary"])
|
||||||
|
return newSignedReader(h, mr, micalg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerBuf bytes.Buffer
|
||||||
|
textproto.WriteHeader(&headerBuf, h)
|
||||||
|
|
||||||
|
return &Reader{
|
||||||
|
Header: h,
|
||||||
|
MessageDetails: &models.MessageDetails{
|
||||||
|
Body: io.MultiReader(&headerBuf, body),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Read(r io.Reader) (*Reader, error) {
|
||||||
|
br := bufio.NewReader(r)
|
||||||
|
|
||||||
|
h, err := textproto.ReadHeader(br)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewReader(h, br)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEncryptedReader(h textproto.Header, mr *textproto.MultipartReader) (*Reader, error) {
|
||||||
|
p, err := mr.NextPart()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gpgmail: failed to read first part in multipart/encrypted message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t, _, err := mime.ParseMediaType(p.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gpgmail: failed to parse Content-Type of first part in multipart/encrypted message: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(t, "application/pgp-encrypted") {
|
||||||
|
return nil, fmt.Errorf("gpgmail: first part in multipart/encrypted message has type %q, not application/pgp-encrypted", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := textproto.ReadHeader(bufio.NewReader(p))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gpgmail: failed to parse application/pgp-encrypted part: %v", err)
|
||||||
|
}
|
||||||
|
if s := metadata.Get("Version"); s != "1" {
|
||||||
|
return nil, fmt.Errorf("gpgmail: unsupported PGP/MIME version: %q", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err = mr.NextPart()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gpgmail: failed to read second part in multipart/encrypted message: %v", err)
|
||||||
|
}
|
||||||
|
t, _, err = mime.ParseMediaType(p.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gpgmail: failed to parse Content-Type of second part in multipart/encrypted message: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(t, "application/octet-stream") {
|
||||||
|
return nil, fmt.Errorf("gpgmail: second part in multipart/encrypted message has type %q, not application/octet-stream", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
md, err := gpgbin.Decrypt(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gpgmail: failed to read PGP message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleartext := bufio.NewReader(md.Body)
|
||||||
|
cleartextHeader, err := textproto.ReadHeader(cleartext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gpgmail: failed to read encrypted header: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t, params, err := mime.ParseMediaType(cleartextHeader.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if md.IsEncrypted && !md.IsSigned && strings.EqualFold(t, "multipart/signed") && strings.EqualFold(params["protocol"], "application/pgp-signature") {
|
||||||
|
// RFC 1847 encapsulation, see RFC 3156 section 6.1
|
||||||
|
micalg := params["micalg"]
|
||||||
|
mr := textproto.NewMultipartReader(cleartext, params["boundary"])
|
||||||
|
mds, err := newSignedReader(cleartextHeader, mr, micalg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gpgmail: failed to read encapsulated multipart/signed message: %v", err)
|
||||||
|
}
|
||||||
|
mds.MessageDetails.IsEncrypted = md.IsEncrypted
|
||||||
|
mds.MessageDetails.DecryptedWith = md.DecryptedWith
|
||||||
|
mds.MessageDetails.DecryptedWithKeyId = md.DecryptedWithKeyId
|
||||||
|
return mds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerBuf bytes.Buffer
|
||||||
|
textproto.WriteHeader(&headerBuf, cleartextHeader)
|
||||||
|
md.Body = io.MultiReader(&headerBuf, cleartext)
|
||||||
|
|
||||||
|
return &Reader{
|
||||||
|
Header: h,
|
||||||
|
MessageDetails: md,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSignedReader(h textproto.Header, mr *textproto.MultipartReader, micalg string) (*Reader, error) {
|
||||||
|
micalg = strings.ToLower(micalg)
|
||||||
|
p, err := mr.NextPart()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gpgmail: failed to read signed part in multipart/signed message: %v", err)
|
||||||
|
}
|
||||||
|
var headerBuf bytes.Buffer
|
||||||
|
textproto.WriteHeader(&headerBuf, p.Header)
|
||||||
|
var msg bytes.Buffer
|
||||||
|
headerRdr := bytes.NewReader(headerBuf.Bytes())
|
||||||
|
fullMsg := io.MultiReader(headerRdr, p)
|
||||||
|
io.Copy(&msg, fullMsg)
|
||||||
|
|
||||||
|
sig, err := mr.NextPart()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gpgmail: failed to read pgp part in multipart/signed message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
md, err := gpgbin.Verify(&msg, sig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gpgmail: failed to read PGP message: %v", err)
|
||||||
|
}
|
||||||
|
if md.Micalg != micalg && md.SignatureError == "" {
|
||||||
|
md.SignatureError = "gpg: header hash does not match actual sig hash"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Reader{
|
||||||
|
Header: h,
|
||||||
|
MessageDetails: md,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,308 @@
|
||||||
|
package gpg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.sr.ht/~rjarry/aerc/lib/crypto/gpg/gpgbin"
|
||||||
|
"git.sr.ht/~rjarry/aerc/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func importSecretKey() {
|
||||||
|
r := strings.NewReader(testPrivateKeyArmored)
|
||||||
|
gpgbin.Import(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func importPublicKey() {
|
||||||
|
r := strings.NewReader(testPublicKeyArmored)
|
||||||
|
gpgbin.Import(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReader_encryptedSignedPGPMIME(t *testing.T) {
|
||||||
|
var expect = models.MessageDetails{
|
||||||
|
IsEncrypted: true,
|
||||||
|
IsSigned: true,
|
||||||
|
SignedBy: "John Doe (This is a test key) <john.doe@example.org>",
|
||||||
|
SignedByKeyId: 3490876580878068068,
|
||||||
|
SignatureError: "",
|
||||||
|
DecryptedWith: "John Doe (This is a test key) <john.doe@example.org>",
|
||||||
|
DecryptedWithKeyId: 3490876580878068068,
|
||||||
|
Body: strings.NewReader(testEncryptedBody),
|
||||||
|
Micalg: "pgp-sha512",
|
||||||
|
}
|
||||||
|
|
||||||
|
importSecretKey()
|
||||||
|
sr := strings.NewReader(testPGPMIMEEncryptedSigned)
|
||||||
|
r, err := Read(sr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("pgpmail.Read() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deepEqual(t, r.MessageDetails, &expect)
|
||||||
|
|
||||||
|
t.Cleanup(CleanUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReader_signedPGPMIME(t *testing.T) {
|
||||||
|
var expect = models.MessageDetails{
|
||||||
|
IsEncrypted: false,
|
||||||
|
IsSigned: true,
|
||||||
|
SignedBy: "John Doe (This is a test key) <john.doe@example.org>",
|
||||||
|
SignedByKeyId: 3490876580878068068,
|
||||||
|
SignatureError: "",
|
||||||
|
DecryptedWith: "",
|
||||||
|
DecryptedWithKeyId: 0,
|
||||||
|
Body: strings.NewReader(testSignedBody),
|
||||||
|
Micalg: "pgp-sha256",
|
||||||
|
}
|
||||||
|
|
||||||
|
importSecretKey()
|
||||||
|
importPublicKey()
|
||||||
|
sr := strings.NewReader(testPGPMIMESigned)
|
||||||
|
r, err := Read(sr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("pgpmail.Read() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deepEqual(t, r.MessageDetails, &expect)
|
||||||
|
|
||||||
|
t.Cleanup(CleanUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReader_encryptedSignedEncapsulatedPGPMIME(t *testing.T) {
|
||||||
|
var expect = models.MessageDetails{
|
||||||
|
IsEncrypted: true,
|
||||||
|
IsSigned: true,
|
||||||
|
SignedBy: "John Doe (This is a test key) <john.doe@example.org>",
|
||||||
|
SignedByKeyId: 3490876580878068068,
|
||||||
|
SignatureError: "",
|
||||||
|
DecryptedWith: "John Doe (This is a test key) <john.doe@example.org>",
|
||||||
|
DecryptedWithKeyId: 3490876580878068068,
|
||||||
|
Body: strings.NewReader(testSignedBody),
|
||||||
|
Micalg: "pgp-sha256",
|
||||||
|
}
|
||||||
|
|
||||||
|
importSecretKey()
|
||||||
|
importPublicKey()
|
||||||
|
sr := strings.NewReader(testPGPMIMEEncryptedSignedEncapsulated)
|
||||||
|
r, err := Read(sr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("pgpmail.Read() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deepEqual(t, r.MessageDetails, &expect)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := io.Copy(&buf, r.MessageDetails.Body); err != nil {
|
||||||
|
t.Fatalf("io.Copy() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestReader_signedPGPMIMEInvalid(t *testing.T) {
|
||||||
|
var expect = models.MessageDetails{
|
||||||
|
IsEncrypted: false,
|
||||||
|
IsSigned: true,
|
||||||
|
SignedBy: "John Doe (This is a test key) <john.doe@example.org>",
|
||||||
|
SignedByKeyId: 3490876580878068068,
|
||||||
|
SignatureError: "gpg: invalid signature",
|
||||||
|
DecryptedWith: "",
|
||||||
|
DecryptedWithKeyId: 0,
|
||||||
|
Body: strings.NewReader(testSignedInvalidBody),
|
||||||
|
Micalg: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
importSecretKey()
|
||||||
|
importPublicKey()
|
||||||
|
sr := strings.NewReader(testPGPMIMESignedInvalid)
|
||||||
|
r, err := Read(sr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("pgpmail.Read() = %v", err)
|
||||||
|
}
|
||||||
|
deepEqual(t, r.MessageDetails, &expect)
|
||||||
|
|
||||||
|
t.Cleanup(CleanUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReader_plaintext(t *testing.T) {
|
||||||
|
sr := strings.NewReader(testPlaintext)
|
||||||
|
r, err := Read(sr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("pgpmail.Read() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := io.Copy(&buf, r.MessageDetails.Body); err != nil {
|
||||||
|
t.Fatalf("io.Copy() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.MessageDetails.IsEncrypted {
|
||||||
|
t.Errorf("MessageDetails.IsEncrypted != false")
|
||||||
|
}
|
||||||
|
if r.MessageDetails.IsSigned {
|
||||||
|
t.Errorf("MessageDetails.IsSigned != false")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s := buf.String(); s != testPlaintext {
|
||||||
|
t.Errorf("MessagesDetails.UnverifiedBody = \n%v\n but want \n%v", s, testPlaintext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testEncryptedBody = toCRLF(`Content-Type: text/plain
|
||||||
|
|
||||||
|
This is an encrypted message!
|
||||||
|
`)
|
||||||
|
|
||||||
|
var testSignedBody = toCRLF(`Content-Type: text/plain
|
||||||
|
|
||||||
|
This is a signed message!
|
||||||
|
`)
|
||||||
|
|
||||||
|
var testSignedInvalidBody = toCRLF(`Content-Type: text/plain
|
||||||
|
|
||||||
|
This is a signed message, but the signature is invalid.
|
||||||
|
`)
|
||||||
|
|
||||||
|
var testPGPMIMEEncryptedSigned = toCRLF(`From: John Doe <john.doe@example.org>
|
||||||
|
To: John Doe <john.doe@example.org>
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: multipart/encrypted; boundary=foo;
|
||||||
|
protocol="application/pgp-encrypted"
|
||||||
|
|
||||||
|
--foo
|
||||||
|
Content-Type: application/pgp-encrypted
|
||||||
|
|
||||||
|
Version: 1
|
||||||
|
|
||||||
|
--foo
|
||||||
|
Content-Type: application/octet-stream
|
||||||
|
|
||||||
|
-----BEGIN PGP MESSAGE-----
|
||||||
|
|
||||||
|
hQEMAxF0jxulHQ8+AQf/SBK2FIIgMA4OkCvlqty/1GmAumWq6J0T+pRLppXHvYFb
|
||||||
|
jbXRzz2h3pE/OoouI6vWzBwb8xU/5f8neen+fvdsF1N6PyLjZcHRB91oPvP8TuHA
|
||||||
|
0vEpiQDbP+0wlQ8BmMnnV06HokWJoKXGmIle0L4QszT/QCbrT80UgKrqXNVHKQtN
|
||||||
|
DUcytFsUCmolZRj074FEpEetjH6QGEX5hAYNBUJziXmOv7vdd4AFgNbbgC5j5ezz
|
||||||
|
h8tCAKUqeUiproYaAMrI0lfqh/t8bacJNkljI2LOxYfdJ/2317Npwly0OqpCM3YT
|
||||||
|
Q4dHuuGM6IuZHtIc9sneIBRhKf8WnWt14hLkHUT80dLA/AHKl0jGYqO34Dxd9JNB
|
||||||
|
EEwQ4j6rxauOEbKLAuYYaEqCzNYBasBrPmpNb4Fx2syWkCoYzwvzv7nj4I8vIBmm
|
||||||
|
FGsAQLX4c18qtZI4XaG4FPUvFQ01Y0rjTxAV3u51lrYjCxFuI5ZEtiT0J/Tv2Unw
|
||||||
|
R6xwtARkEf3W0agegmohEjjkAexKNxGrlulLiPk2j9/dnlAxeGpOuhYuYU2kYbKq
|
||||||
|
x3TkcVYRs1FkmCX0YHNJ2zVWLfDYd2f3UVkXINe7mODGx2A2BxvK9Ig7NMuNmWZE
|
||||||
|
ELiLSIvQk9jlgqWUMwSGPQKaHPrac02EjcBHef2zCoFbTg0TXQeDr5SV7yguX8jB
|
||||||
|
zZnoNs+6+GR1gA6poKzFdiG4NRr0SNgEHazPPkXp3P2KyOINyFJ7SA+HX8iegTqL
|
||||||
|
CTPYPK7UNRmb5s2u5B4e9NiQB9L85W4p7p7uemCSu9bxjs8rkCJpvx9Kb8jzPW17
|
||||||
|
wnEUe10A4JNDBhxiMg+Fm5oM2VxQVy+eDVFOOq7pDYVcSmZc36wO+EwAKph9shby
|
||||||
|
O4sDS4l/8eQTEYUxTavdtQ9O9ZMXvf/L3Rl1uFJXw1lFwPReXwtpA485e031/A==
|
||||||
|
=P0jf
|
||||||
|
-----END PGP MESSAGE-----
|
||||||
|
|
||||||
|
--foo--
|
||||||
|
`)
|
||||||
|
|
||||||
|
var testPGPMIMEEncryptedSignedEncapsulated = toCRLF(`From: John Doe <john.doe@example.org>
|
||||||
|
To: John Doe <john.doe@example.org>
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: multipart/encrypted; boundary=foo;
|
||||||
|
protocol="application/pgp-encrypted"
|
||||||
|
|
||||||
|
--foo
|
||||||
|
Content-Type: application/pgp-encrypted
|
||||||
|
|
||||||
|
Version: 1
|
||||||
|
|
||||||
|
--foo
|
||||||
|
Content-Type: application/octet-stream
|
||||||
|
|
||||||
|
-----BEGIN PGP MESSAGE-----
|
||||||
|
|
||||||
|
hQEMAxF0jxulHQ8+AQf9FCth8p+17rzWL0AtKP+aWndvVUYmaKiUZd+Ya8D9cRnc
|
||||||
|
FAP//JnRvTPhdOyl8x1FQkVxyuKcgpjaClb6/OLgD0lGYLC15p43G4QyU+jtOOQW
|
||||||
|
FFjZj2z8wUuiev8ejNd7DMiOQRSm4d+IIK+Qa2BJ10Y9AuLQtMI8D+joP1D11NeX
|
||||||
|
4FO3SYFEuwH5VWlXGo3bRjg8fKFVG/r/xCwBibqRpfjVnS4EgI04XCsnhqdaCRvE
|
||||||
|
Bw2XEaF62m2MUNbaan410WajzVSbSIqIHw8U7vpR/1nisS+SZmScuCXWFa6W9YgR
|
||||||
|
0nSWi1io2Ratf4F9ORCy0o7QPh7FlpsIUGmp4paF39LpAQ2q0OUnFhkIdLVQscQT
|
||||||
|
JJXLbZwp0CYTAgqwdRWFwY7rEPm2k/Oe4cHKJLEn0hS+X7wch9FAYEMifeqa0FcZ
|
||||||
|
GjxocAlyhmlM0sXIDYP8xx49t4O8JIQU1ep/SX2+rUAKIh2WRdYDy8GrrHba8V8U
|
||||||
|
aBCU9zIMhmOtu7r+FE1djMUhcaSbbvC9zLDMLV8QxogGhxrqaUM8Pj+q1H6myaAr
|
||||||
|
o1xd65b6r2Bph6GUmcMwl28i78u9bKoM0mI+EdUuLwS9EbmjtIwEgxNv4LqK8xw2
|
||||||
|
/tjCe9JSqg+HDaBYnO4QTM29Y+PltRIe6RxpnBcYULTLcSt1UK3YV1KvhqfXMjoZ
|
||||||
|
THsvtxLbmPYFv+g0hiUpuKtyG9NGidKCxrjvNq30KCSUWzNFkh+qv6CPm26sXr5F
|
||||||
|
DTsVpFTM/lomg4Po8sE20BZsk/9IzEh4ERSOu3k0m3mI4QAyJmrOpVGUjd//4cqz
|
||||||
|
Zhhc3tV78BtEYNh0a+78fAHGtdLocLj5IfOCYQWW//EtOY93TnVAtP0puaiNOc8q
|
||||||
|
Vvb5WMamiRJZ9nQXP3paDoqD14B9X6bvNWsDQDkkrWls2sYg7KzqpOM/nlXLBKQd
|
||||||
|
Ok4EJfOpd0hICPwo6tJ6sK2meRcDLxtGJybADE7UHJ4t0SrQBfn/sQhRytQtg2wr
|
||||||
|
U1Thy6RujlrrrdUryo3Mi+xc9Ot1o35JszCjNQGL6BCFsGi9fx5pjWM+lLiJ15aJ
|
||||||
|
jh02mSd/8j7IaJCGgTuyq6uK45EoVqWd1WRSYl4s5tg1g1jckigYYjJdAKNnU/rZ
|
||||||
|
iTk5F8GSyv30EXnqvrs=
|
||||||
|
=Ibxd
|
||||||
|
-----END PGP MESSAGE-----
|
||||||
|
|
||||||
|
--foo--
|
||||||
|
`)
|
||||||
|
|
||||||
|
var testPGPMIMESigned = toCRLF(`From: John Doe <john.doe@example.org>
|
||||||
|
To: John Doe <john.doe@example.org>
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: multipart/signed; boundary=bar; micalg=pgp-sha256;
|
||||||
|
protocol="application/pgp-signature"
|
||||||
|
|
||||||
|
--bar
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
This is a signed message!
|
||||||
|
|
||||||
|
--bar
|
||||||
|
Content-Type: application/pgp-signature
|
||||||
|
|
||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQEzBAABCAAdFiEEsahmk1QVO3mfIhe/MHIVwT33qWQFAl5FRLgACgkQMHIVwT33
|
||||||
|
qWSEQQf/YgRlKlQzSyvm6A52lGIRU3F/z9EGjhCryxj+hSdPlk8O7iZFIjnco4Ea
|
||||||
|
7QIlsOj6D4AlLdhyK6c8IZV7rZoTNE5rc6I5UZjM4Qa0XoyLjao28zR252TtwwWJ
|
||||||
|
e4+wrTQKcVhCyHO6rkvcCpru4qF5CU+Mi8+sf8CNJJyBgw1Pri35rJWMdoTPTqqz
|
||||||
|
kcIGN1JySaI8bbVitJQmnm0FtFTiB7zznv94rMBCiPmPUWd9BSpSBJteJoBLZ+K7
|
||||||
|
Y7ws2Dzp2sBo/RLUM18oXd0N9PLXvFGI3IuF8ey1SPzQH3QbBdJSTmLzRlPjK7A1
|
||||||
|
HVHFb3vTjd71z9j5IGQQ3Awdw30zMg==
|
||||||
|
=gOul
|
||||||
|
-----END PGP SIGNATURE-----
|
||||||
|
|
||||||
|
--bar--
|
||||||
|
`)
|
||||||
|
|
||||||
|
var testPGPMIMESignedInvalid = toCRLF(`From: John Doe <john.doe@example.org>
|
||||||
|
To: John Doe <john.doe@example.org>
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: multipart/signed; boundary=bar; micalg=pgp-sha256;
|
||||||
|
protocol="application/pgp-signature"
|
||||||
|
|
||||||
|
--bar
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
This is a signed message, but the signature is invalid.
|
||||||
|
|
||||||
|
--bar
|
||||||
|
Content-Type: application/pgp-signature
|
||||||
|
|
||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQEzBAABCAAdFiEEsahmk1QVO3mfIhe/MHIVwT33qWQFAl5FRLgACgkQMHIVwT33
|
||||||
|
qWSEQQf/YgRlKlQzSyvm6A52lGIRU3F/z9EGjhCryxj+hSdPlk8O7iZFIjnco4Ea
|
||||||
|
7QIlsOj6D4AlLdhyK6c8IZV7rZoTNE5rc6I5UZjM4Qa0XoyLjao28zR252TtwwWJ
|
||||||
|
e4+wrTQKcVhCyHO6rkvcCpru4qF5CU+Mi8+sf8CNJJyBgw1Pri35rJWMdoTPTqqz
|
||||||
|
kcIGN1JySaI8bbVitJQmnm0FtFTiB7zznv94rMBCiPmPUWd9BSpSBJteJoBLZ+K7
|
||||||
|
Y7ws2Dzp2sBo/RLUM18oXd0N9PLXvFGI3IuF8ey1SPzQH3QbBdJSTmLzRlPjK7A1
|
||||||
|
HVHFb3vTjd71z9j5IGQQ3Awdw30zMg==
|
||||||
|
=gOul
|
||||||
|
-----END PGP SIGNATURE-----
|
||||||
|
|
||||||
|
--bar--
|
||||||
|
`)
|
||||||
|
|
||||||
|
var testPlaintext = toCRLF(`From: John Doe <john.doe@example.org>
|
||||||
|
To: John Doe <john.doe@example.org>
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
This is a plaintext message!
|
||||||
|
`)
|
|
@ -0,0 +1,179 @@
|
||||||
|
// writer.go largerly mimics github.com/emersion/go-pgpmail, with changes made
|
||||||
|
// to interface with the gpg package in aerc
|
||||||
|
|
||||||
|
package gpg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
|
||||||
|
"git.sr.ht/~rjarry/aerc/lib/crypto/gpg/gpgbin"
|
||||||
|
"github.com/emersion/go-message/textproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EncrypterSigner struct {
|
||||||
|
msgBuf bytes.Buffer
|
||||||
|
encryptedWriter io.Writer
|
||||||
|
to []string
|
||||||
|
from string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EncrypterSigner) Write(p []byte) (int, error) {
|
||||||
|
return es.msgBuf.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EncrypterSigner) Close() (err error) {
|
||||||
|
r := bytes.NewReader(es.msgBuf.Bytes())
|
||||||
|
enc, err := gpgbin.Encrypt(r, es.to, es.from)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
es.encryptedWriter.Write(enc)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Signer struct {
|
||||||
|
mw *textproto.MultipartWriter
|
||||||
|
signedMsg bytes.Buffer
|
||||||
|
w io.Writer
|
||||||
|
from string
|
||||||
|
header textproto.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Signer) Write(p []byte) (int, error) {
|
||||||
|
return s.signedMsg.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Signer) Close() (err error) {
|
||||||
|
// TODO should write the whole message up here so we can get the proper micalg from the signature packet
|
||||||
|
|
||||||
|
sig, micalg, err := gpgbin.Sign(bytes.NewReader(s.signedMsg.Bytes()), s.from)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
params := map[string]string{
|
||||||
|
"boundary": s.mw.Boundary(),
|
||||||
|
"protocol": "application/pgp-signature",
|
||||||
|
"micalg": micalg,
|
||||||
|
}
|
||||||
|
s.header.Set("Content-Type", mime.FormatMediaType("multipart/signed", params))
|
||||||
|
|
||||||
|
if err = textproto.WriteHeader(s.w, s.header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
boundary := s.mw.Boundary()
|
||||||
|
fmt.Fprintf(s.w, "--%s\r\n", boundary)
|
||||||
|
s.w.Write(s.signedMsg.Bytes())
|
||||||
|
s.w.Write([]byte("\r\n"))
|
||||||
|
|
||||||
|
var signedHeader textproto.Header
|
||||||
|
signedHeader.Set("Content-Type", "application/pgp-signature")
|
||||||
|
signatureWriter, err := s.mw.CreatePart(signedHeader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = signatureWriter.Write(sig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// for tests
|
||||||
|
var forceBoundary = ""
|
||||||
|
|
||||||
|
type multiCloser []io.Closer
|
||||||
|
|
||||||
|
func (mc multiCloser) Close() error {
|
||||||
|
for _, c := range mc {
|
||||||
|
if err := c.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Encrypt(w io.Writer, h textproto.Header, rcpts []string, from string) (io.WriteCloser, error) {
|
||||||
|
mw := textproto.NewMultipartWriter(w)
|
||||||
|
|
||||||
|
if forceBoundary != "" {
|
||||||
|
mw.SetBoundary(forceBoundary)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := map[string]string{
|
||||||
|
"boundary": mw.Boundary(),
|
||||||
|
"protocol": "application/pgp-encrypted",
|
||||||
|
}
|
||||||
|
h.Set("Content-Type", mime.FormatMediaType("multipart/encrypted", params))
|
||||||
|
|
||||||
|
if err := textproto.WriteHeader(w, h); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var controlHeader textproto.Header
|
||||||
|
controlHeader.Set("Content-Type", "application/pgp-encrypted")
|
||||||
|
controlWriter, err := mw.CreatePart(controlHeader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err = controlWriter.Write([]byte("Version: 1\r\n")); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var encryptedHeader textproto.Header
|
||||||
|
encryptedHeader.Set("Content-Type", "application/octet-stream")
|
||||||
|
encryptedWriter, err := mw.CreatePart(encryptedHeader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
plaintext := &EncrypterSigner{
|
||||||
|
msgBuf: buf,
|
||||||
|
encryptedWriter: encryptedWriter,
|
||||||
|
to: rcpts,
|
||||||
|
from: from,
|
||||||
|
}
|
||||||
|
|
||||||
|
return struct {
|
||||||
|
io.Writer
|
||||||
|
io.Closer
|
||||||
|
}{
|
||||||
|
plaintext,
|
||||||
|
multiCloser{
|
||||||
|
plaintext,
|
||||||
|
mw,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sign(w io.Writer, h textproto.Header, from string) (io.WriteCloser, error) {
|
||||||
|
mw := textproto.NewMultipartWriter(w)
|
||||||
|
|
||||||
|
if forceBoundary != "" {
|
||||||
|
mw.SetBoundary(forceBoundary)
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg bytes.Buffer
|
||||||
|
plaintext := &Signer{
|
||||||
|
mw: mw,
|
||||||
|
signedMsg: msg,
|
||||||
|
w: w,
|
||||||
|
from: from,
|
||||||
|
header: h,
|
||||||
|
}
|
||||||
|
|
||||||
|
return struct {
|
||||||
|
io.Writer
|
||||||
|
io.Closer
|
||||||
|
}{
|
||||||
|
plaintext,
|
||||||
|
multiCloser{
|
||||||
|
plaintext,
|
||||||
|
mw,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
package gpg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.sr.ht/~rjarry/aerc/lib/crypto/gpg/gpgbin"
|
||||||
|
"git.sr.ht/~rjarry/aerc/models"
|
||||||
|
"github.com/emersion/go-message/textproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
forceBoundary = "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncrypt(t *testing.T) {
|
||||||
|
importPublicKey()
|
||||||
|
importSecretKey()
|
||||||
|
var h textproto.Header
|
||||||
|
h.Set("From", "John Doe <john.doe@example.org>")
|
||||||
|
h.Set("To", "John Doe <john.doe@example.org>")
|
||||||
|
|
||||||
|
var encryptedHeader textproto.Header
|
||||||
|
encryptedHeader.Set("Content-Type", "text/plain")
|
||||||
|
|
||||||
|
var encryptedBody = "This is an encrypted message!\r\n"
|
||||||
|
|
||||||
|
to := []string{"john.doe@example.org"}
|
||||||
|
from := "john.doe@example.org"
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
cleartext, err := Encrypt(&buf, h, to, from)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encrypt() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = textproto.WriteHeader(cleartext, encryptedHeader); err != nil {
|
||||||
|
t.Fatalf("textproto.WriteHeader() = %v", err)
|
||||||
|
}
|
||||||
|
if _, err = io.WriteString(cleartext, encryptedBody); err != nil {
|
||||||
|
t.Fatalf("io.WriteString() = %v", err)
|
||||||
|
}
|
||||||
|
if err = cleartext.Close(); err != nil {
|
||||||
|
t.Fatalf("ciphertext.Close() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
md, err := gpgbin.Decrypt(&buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Encrypt error: could not decrypt test encryption")
|
||||||
|
}
|
||||||
|
var body bytes.Buffer
|
||||||
|
io.Copy(&body, md.Body)
|
||||||
|
if s := body.String(); s != wantEncrypted {
|
||||||
|
t.Errorf("Encrypt() = \n%v\n but want \n%v", s, wantEncrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(CleanUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSign(t *testing.T) {
|
||||||
|
importPublicKey()
|
||||||
|
importSecretKey()
|
||||||
|
var h textproto.Header
|
||||||
|
h.Set("From", "John Doe <john.doe@example.org>")
|
||||||
|
h.Set("To", "John Doe <john.doe@example.org>")
|
||||||
|
|
||||||
|
var signedHeader textproto.Header
|
||||||
|
signedHeader.Set("Content-Type", "text/plain")
|
||||||
|
|
||||||
|
var signedBody = "This is a signed message!\r\n"
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
cleartext, err := Sign(&buf, h, "john.doe@example.org")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encrypt() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = textproto.WriteHeader(cleartext, signedHeader); err != nil {
|
||||||
|
t.Fatalf("textproto.WriteHeader() = %v", err)
|
||||||
|
}
|
||||||
|
if _, err = io.WriteString(cleartext, signedBody); err != nil {
|
||||||
|
t.Fatalf("io.WriteString() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cleartext.Close(); err != nil {
|
||||||
|
t.Fatalf("ciphertext.Close() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(buf.String(), "\r\n--foo\r\n")
|
||||||
|
msg := strings.NewReader(parts[1])
|
||||||
|
sig := strings.NewReader(parts[2])
|
||||||
|
md, err := gpgbin.Verify(msg, sig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("gpg.Verify() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deepEqual(t, md, &wantSigned)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wantEncrypted = toCRLF(`Content-Type: text/plain
|
||||||
|
|
||||||
|
This is an encrypted message!
|
||||||
|
`)
|
||||||
|
|
||||||
|
var wantSignedBody = toCRLF(`Content-Type: text/plain
|
||||||
|
|
||||||
|
This is a signed message!
|
||||||
|
`)
|
||||||
|
|
||||||
|
var wantSigned = models.MessageDetails{
|
||||||
|
IsEncrypted: false,
|
||||||
|
IsSigned: true,
|
||||||
|
SignedBy: "John Doe (This is a test key) <john.doe@example.org>",
|
||||||
|
SignedByKeyId: 3490876580878068068,
|
||||||
|
SignatureError: "",
|
||||||
|
DecryptedWith: "",
|
||||||
|
DecryptedWithKeyId: 0,
|
||||||
|
Body: strings.NewReader(wantSignedBody),
|
||||||
|
Micalg: "pgp-sha256",
|
||||||
|
}
|
|
@ -79,6 +79,20 @@ func (m *Mail) getEntityByEmail(email string) (e *openpgp.Entity, err error) {
|
||||||
return nil, fmt.Errorf("entity not found in keyring")
|
return nil, fmt.Errorf("entity not found in keyring")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Mail) getSignerEntityByKeyId(id string) (*openpgp.Entity, error) {
|
||||||
|
id = strings.ToUpper(id)
|
||||||
|
for _, key := range Keyring.DecryptionKeys() {
|
||||||
|
if key.Entity == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kId := key.Entity.PrimaryKey.KeyIdString()
|
||||||
|
if strings.Contains(kId, id) {
|
||||||
|
return key.Entity, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("entity not found in keyring")
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Mail) getSignerEntityByEmail(email string) (e *openpgp.Entity, err error) {
|
func (m *Mail) getSignerEntityByEmail(email string) (e *openpgp.Entity, err error) {
|
||||||
for _, key := range Keyring.DecryptionKeys() {
|
for _, key := range Keyring.DecryptionKeys() {
|
||||||
if key.Entity == nil {
|
if key.Entity == nil {
|
||||||
|
@ -157,12 +171,12 @@ func (m *Mail) ImportKeys(r io.Reader) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mail) Encrypt(buf *bytes.Buffer, rcpts []string, signerEmail string, decryptKeys openpgp.PromptFunction, header *mail.Header) (io.WriteCloser, error) {
|
func (m *Mail) Encrypt(buf *bytes.Buffer, rcpts []string, signer string, decryptKeys openpgp.PromptFunction, header *mail.Header) (io.WriteCloser, error) {
|
||||||
var err error
|
var err error
|
||||||
var to []*openpgp.Entity
|
var to []*openpgp.Entity
|
||||||
var signer *openpgp.Entity
|
var signerEntity *openpgp.Entity
|
||||||
if signerEmail != "" {
|
if signer != "" {
|
||||||
signer, err = m.getSigner(signerEmail, decryptKeys)
|
signerEntity, err = m.getSigner(signer, decryptKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -177,45 +191,50 @@ func (m *Mail) Encrypt(buf *bytes.Buffer, rcpts []string, signerEmail string, de
|
||||||
}
|
}
|
||||||
|
|
||||||
cleartext, err := pgpmail.Encrypt(buf, header.Header.Header,
|
cleartext, err := pgpmail.Encrypt(buf, header.Header.Header,
|
||||||
to, signer, nil)
|
to, signerEntity, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return cleartext, nil
|
return cleartext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mail) Sign(buf *bytes.Buffer, signerEmail string, decryptKeys openpgp.PromptFunction, header *mail.Header) (io.WriteCloser, error) {
|
func (m *Mail) Sign(buf *bytes.Buffer, signer string, decryptKeys openpgp.PromptFunction, header *mail.Header) (io.WriteCloser, error) {
|
||||||
var err error
|
var err error
|
||||||
var signer *openpgp.Entity
|
var signerEntity *openpgp.Entity
|
||||||
if signerEmail != "" {
|
if signer != "" {
|
||||||
signer, err = m.getSigner(signerEmail, decryptKeys)
|
signerEntity, err = m.getSigner(signer, decryptKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cleartext, err := pgpmail.Sign(buf, header.Header.Header, signer, nil)
|
cleartext, err := pgpmail.Sign(buf, header.Header.Header, signerEntity, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return cleartext, nil
|
return cleartext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mail) getSigner(signerEmail string, decryptKeys openpgp.PromptFunction) (signer *openpgp.Entity, err error) {
|
func (m *Mail) getSigner(signer string, decryptKeys openpgp.PromptFunction) (signerEntity *openpgp.Entity, err error) {
|
||||||
|
switch strings.Contains(signer, "@") {
|
||||||
|
case true:
|
||||||
|
signerEntity, err = m.getSignerEntityByEmail(signer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
signer, err = m.getSignerEntityByEmail(signerEmail)
|
case false:
|
||||||
|
signerEntity, err = m.getSignerEntityByKeyId(signer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
key, ok := signer.SigningKey(time.Now())
|
key, ok := signerEntity.SigningKey(time.Now())
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("no signing key found for %s", signerEmail)
|
return nil, fmt.Errorf("no signing key found for %s", signer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !key.PrivateKey.Encrypted {
|
if !key.PrivateKey.Encrypted {
|
||||||
return signer, nil
|
return signerEntity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = decryptKeys([]openpgp.Key{key}, false)
|
_, err = decryptKeys([]openpgp.Key{key}, false)
|
||||||
|
@ -223,7 +242,7 @@ func (m *Mail) getSigner(signerEmail string, decryptKeys openpgp.PromptFunction)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return signer, nil
|
return signerEntity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSignatureError(e string) models.SignatureValidity {
|
func handleSignatureError(e string) models.SignatureValidity {
|
||||||
|
|
|
@ -452,14 +452,16 @@ func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error {
|
||||||
var cleartext io.WriteCloser
|
var cleartext io.WriteCloser
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
var signerEmail string
|
signer := ""
|
||||||
if c.sign {
|
if c.sign {
|
||||||
signerEmail, err = getSenderEmail(c)
|
if c.acctConfig.PgpKeyId != "" {
|
||||||
|
signer = c.acctConfig.PgpKeyId
|
||||||
|
} else {
|
||||||
|
signer, err = getSenderEmail(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
signerEmail = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.encrypt {
|
if c.encrypt {
|
||||||
|
@ -467,12 +469,12 @@ func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cleartext, err = c.aerc.Crypto.Encrypt(&buf, rcpts, signerEmail, c.aerc.DecryptKeys, header)
|
cleartext, err = c.aerc.Crypto.Encrypt(&buf, rcpts, signer, c.aerc.DecryptKeys, header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cleartext, err = c.aerc.Crypto.Sign(&buf, signerEmail, c.aerc.DecryptKeys, header)
|
cleartext, err = c.aerc.Crypto.Sign(&buf, signer, c.aerc.DecryptKeys, header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue