chore(GODT-2916): Split Decryption from Message Building

This helps the export tool to deal with problems arising from message
assembly after everything has been successfully encrypted.

The original behavior is still available under `DecryptAndBuildRFC822`.
This commit is contained in:
Leander Beernaert
2023-09-18 14:38:51 +02:00
parent fa794a982b
commit 03c3404044
8 changed files with 240 additions and 167 deletions

View File

@ -159,7 +159,7 @@ func (s *Connector) GetMessageLiteral(ctx context.Context, id imap.MessageID) ([
var literal []byte
err = s.identityState.WithAddrKR(msg.AddressID, func(_, addrKR *crypto.KeyRing) error {
l, buildErr := message.BuildRFC822(addrKR, msg.Message, msg.AttData, defaultMessageJobOpts())
l, buildErr := message.DecryptAndBuildRFC822(addrKR, msg.Message, msg.AttData, defaultMessageJobOpts())
if buildErr != nil {
return buildErr
}
@ -249,7 +249,7 @@ func (s *Connector) CreateMessage(ctx context.Context, _ connector.IMAPStateWrit
if err := s.identityState.WithAddrKR(full.AddressID, func(_, addrKR *crypto.KeyRing) error {
var err error
if literal, err = message.BuildRFC822(addrKR, full.Message, full.AttData, defaultMessageJobOpts()); err != nil {
if literal, err = message.DecryptAndBuildRFC822(addrKR, full.Message, full.AttData, defaultMessageJobOpts()); err != nil {
return err
}
@ -611,7 +611,7 @@ func (s *Connector) importMessage(
return fmt.Errorf("failed to fetch message: %w", err)
}
if literal, err = message.BuildRFC822(addrKR, full.Message, full.AttData, defaultMessageJobOpts()); err != nil {
if literal, err = message.DecryptAndBuildRFC822(addrKR, full.Message, full.AttData, defaultMessageJobOpts()); err != nil {
return fmt.Errorf("failed to build message: %w", err)
}

View File

@ -57,7 +57,7 @@ func buildRFC822(apiLabels map[string]proton.Label, full proton.FullMessage, add
buffer.Grow(full.Size)
if buildErr := message.BuildRFC822Into(addrKR, full.Message, full.AttData, defaultMessageJobOpts(), buffer); buildErr != nil {
if buildErr := message.DecryptAndBuildRFC822Into(addrKR, full.Message, full.AttData, defaultMessageJobOpts(), buffer); buildErr != nil {
update = newMessageCreatedFailedUpdate(apiLabels, full.MessageMetadata, buildErr)
err = buildErr
} else if created, parseErr := newMessageCreatedUpdate(apiLabels, full.MessageMetadata, buffer.Bytes()); parseErr != nil {

View File

@ -46,7 +46,7 @@ func (s SyncMessageBuilder) BuildMessage(
) (syncservice.BuildResult, error) {
buffer.Grow(full.Size)
if err := message.BuildRFC822Into(addrKR, full.Message, full.AttData, defaultMessageJobOpts(), buffer); err != nil {
if err := message.DecryptAndBuildRFC822Into(addrKR, full.Message, full.AttData, defaultMessageJobOpts(), buffer); err != nil {
return syncservice.BuildResult{}, err
}

View File

@ -19,8 +19,6 @@ package message
import (
"bytes"
"encoding/base64"
"io"
"mime"
"net/mail"
"strings"
@ -47,48 +45,36 @@ var (
// InternalIDDomain is used as a placeholder for reference/message ID headers to improve compatibility with various clients.
const InternalIDDomain = `protonmail.internalid`
func BuildRFC822(kr *crypto.KeyRing, msg proton.Message, attData [][]byte, opts JobOptions) ([]byte, error) {
buf := new(bytes.Buffer)
if err := BuildRFC822Into(kr, msg, attData, opts, buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func BuildRFC822Into(kr *crypto.KeyRing, msg proton.Message, attData [][]byte, opts JobOptions, buf *bytes.Buffer) error {
func BuildRFC822Into(kr *crypto.KeyRing, decrypted *DecryptedMessage, opts JobOptions, buf *bytes.Buffer) error {
switch {
case len(msg.Attachments) > 0:
return buildMultipartRFC822(kr, msg, attData, opts, buf)
case len(decrypted.Msg.Attachments) > 0:
return buildMultipartRFC822(decrypted, opts, buf)
case msg.MIMEType == "multipart/mixed":
return buildPGPRFC822(kr, msg, opts, buf)
case decrypted.Msg.MIMEType == "multipart/mixed":
return buildPGPRFC822(kr, decrypted, opts, buf)
default:
return buildSimpleRFC822(kr, msg, opts, buf)
return buildSimpleRFC822(decrypted, opts, buf)
}
}
func buildSimpleRFC822(kr *crypto.KeyRing, msg proton.Message, opts JobOptions, buf *bytes.Buffer) error {
var decrypted bytes.Buffer
decrypted.Grow(len(msg.Body))
if err := msg.DecryptInto(kr, &decrypted); err != nil {
func buildSimpleRFC822(decrypted *DecryptedMessage, opts JobOptions, buf *bytes.Buffer) error {
if decrypted.BodyErr != nil {
if !opts.IgnoreDecryptionErrors {
return errors.Wrap(ErrDecryptionFailed, err.Error())
return decrypted.BodyErr
}
return buildMultipartRFC822(kr, msg, nil, opts, buf)
return buildMultipartRFC822(decrypted, opts, buf)
}
hdr := getTextPartHeader(getMessageHeader(msg, opts), decrypted.Bytes(), msg.MIMEType)
hdr := getTextPartHeader(getMessageHeader(decrypted.Msg, opts), decrypted.Body.Bytes(), decrypted.Msg.MIMEType)
w, err := message.CreateWriter(buf, hdr)
if err != nil {
return err
}
if _, err := w.Write(decrypted.Bytes()); err != nil {
if _, err := w.Write(decrypted.Body.Bytes()); err != nil {
return err
}
@ -96,15 +82,13 @@ func buildSimpleRFC822(kr *crypto.KeyRing, msg proton.Message, opts JobOptions,
}
func buildMultipartRFC822(
kr *crypto.KeyRing,
msg proton.Message,
attData [][]byte,
decrypted *DecryptedMessage,
opts JobOptions,
buf *bytes.Buffer,
) error {
boundary := newBoundary(msg.ID)
boundary := newBoundary(decrypted.Msg.ID)
hdr := getMessageHeader(msg, opts)
hdr := getMessageHeader(decrypted.Msg, opts)
hdr.SetContentType("multipart/mixed", map[string]string{"boundary": boundary.gen()})
@ -115,31 +99,31 @@ func buildMultipartRFC822(
var (
inlineAtts []proton.Attachment
inlineData [][]byte
inlineData []DecryptedAttachment
attachAtts []proton.Attachment
attachData [][]byte
attachData []DecryptedAttachment
)
for index, att := range msg.Attachments {
for index, att := range decrypted.Msg.Attachments {
if att.Disposition == proton.InlineDisposition {
inlineAtts = append(inlineAtts, att)
inlineData = append(inlineData, attData[index])
inlineData = append(inlineData, decrypted.Attachments[index])
} else {
attachAtts = append(attachAtts, att)
attachData = append(attachData, attData[index])
attachData = append(attachData, decrypted.Attachments[index])
}
}
if len(inlineAtts) > 0 {
if err := writeRelatedParts(w, kr, boundary, msg, inlineAtts, inlineData, opts); err != nil {
if err := writeRelatedParts(w, boundary, decrypted, inlineAtts, inlineData, opts); err != nil {
return err
}
} else if err := writeTextPart(w, kr, msg, opts); err != nil {
} else if err := writeTextPart(w, decrypted, opts); err != nil {
return err
}
for i, att := range attachAtts {
if err := writeAttachmentPart(w, kr, att, attachData[i], opts); err != nil {
if err := writeAttachmentPart(w, att, attachData[i], opts); err != nil {
return err
}
}
@ -149,89 +133,53 @@ func buildMultipartRFC822(
func writeTextPart(
w *message.Writer,
kr *crypto.KeyRing,
msg proton.Message,
decrypted *DecryptedMessage,
opts JobOptions,
) error {
var decrypted bytes.Buffer
decrypted.Grow(len(msg.Body))
if err := msg.DecryptInto(kr, &decrypted); err != nil {
if decrypted.BodyErr != nil {
if !opts.IgnoreDecryptionErrors {
return errors.Wrap(ErrDecryptionFailed, err.Error())
return decrypted.BodyErr
}
return writeCustomTextPart(w, msg, err)
return writeCustomTextPart(w, decrypted, decrypted.BodyErr)
}
return writePart(w, getTextPartHeader(message.Header{}, decrypted.Bytes(), msg.MIMEType), decrypted.Bytes())
return writePart(w, getTextPartHeader(message.Header{}, decrypted.Body.Bytes(), decrypted.Msg.MIMEType), decrypted.Body.Bytes())
}
func writeAttachmentPart(
w *message.Writer,
kr *crypto.KeyRing,
att proton.Attachment,
attData []byte,
decryptedAttachment DecryptedAttachment,
opts JobOptions,
) error {
kps, err := base64.StdEncoding.DecodeString(att.KeyPackets)
if err != nil {
return err
}
// Use io.Multi
attachmentReader := io.MultiReader(bytes.NewReader(kps), bytes.NewReader(attData))
stream, err := kr.DecryptStream(attachmentReader, nil, crypto.GetUnixTime())
if err != nil {
if decryptedAttachment.Err != nil {
if !opts.IgnoreDecryptionErrors {
return errors.Wrap(ErrDecryptionFailed, err.Error())
return decryptedAttachment.Err
}
log.
WithField("attID", att.ID).
WithError(err).
Warn("Attachment decryption failed - construct")
WithError(decryptedAttachment.Err).
Warn("Attachment decryption failed")
var pgpMessageBuffer bytes.Buffer
pgpMessageBuffer.Grow(len(kps) + len(attData))
pgpMessageBuffer.Write(kps)
pgpMessageBuffer.Write(attData)
pgpMessageBuffer.Grow(len(decryptedAttachment.Packet) + len(decryptedAttachment.Encrypted))
pgpMessageBuffer.Write(decryptedAttachment.Packet)
pgpMessageBuffer.Write(decryptedAttachment.Encrypted)
return writeCustomAttachmentPart(w, att, &crypto.PGPMessage{Data: pgpMessageBuffer.Bytes()}, err)
return writeCustomAttachmentPart(w, att, &crypto.PGPMessage{Data: pgpMessageBuffer.Bytes()}, decryptedAttachment.Err)
}
var decryptBuffer bytes.Buffer
decryptBuffer.Grow(len(kps) + len(attData))
if _, err := decryptBuffer.ReadFrom(stream); err != nil {
if !opts.IgnoreDecryptionErrors {
return errors.Wrap(ErrDecryptionFailed, err.Error())
}
log.
WithField("attID", att.ID).
WithError(err).
Warn("Attachment decryption failed - stream")
var pgpMessageBuffer bytes.Buffer
pgpMessageBuffer.Grow(len(kps) + len(attData))
pgpMessageBuffer.Write(kps)
pgpMessageBuffer.Write(attData)
return writeCustomAttachmentPart(w, att, &crypto.PGPMessage{Data: pgpMessageBuffer.Bytes()}, err)
}
return writePart(w, getAttachmentPartHeader(att), decryptBuffer.Bytes())
return writePart(w, getAttachmentPartHeader(att), decryptedAttachment.Data.Bytes())
}
func writeRelatedParts(
w *message.Writer,
kr *crypto.KeyRing,
boundary *boundary,
msg proton.Message,
decrypted *DecryptedMessage,
atts []proton.Attachment,
attData [][]byte,
attData []DecryptedAttachment,
opts JobOptions,
) error {
hdr := message.Header{}
@ -239,12 +187,12 @@ func writeRelatedParts(
hdr.SetContentType("multipart/related", map[string]string{"boundary": boundary.gen()})
return createPart(w, hdr, func(rel *message.Writer) error {
if err := writeTextPart(rel, kr, msg, opts); err != nil {
if err := writeTextPart(rel, decrypted, opts); err != nil {
return err
}
for i, att := range atts {
if err := writeAttachmentPart(rel, kr, att, attData[i], opts); err != nil {
if err := writeAttachmentPart(rel, att, attData[i], opts); err != nil {
return err
}
}
@ -253,37 +201,34 @@ func writeRelatedParts(
})
}
func buildPGPRFC822(kr *crypto.KeyRing, msg proton.Message, opts JobOptions, buf *bytes.Buffer) error {
var decrypted bytes.Buffer
decrypted.Grow(len(msg.Body))
if err := msg.DecryptInto(kr, &decrypted); err != nil {
func buildPGPRFC822(kr *crypto.KeyRing, decrypted *DecryptedMessage, opts JobOptions, buf *bytes.Buffer) error {
if decrypted.BodyErr != nil {
if !opts.IgnoreDecryptionErrors {
return errors.Wrap(ErrDecryptionFailed, err.Error())
return decrypted.BodyErr
}
return buildPGPMIMEFallbackRFC822(msg, opts, buf)
return buildPGPMIMEFallbackRFC822(decrypted, opts, buf)
}
hdr := getMessageHeader(msg, opts)
hdr := getMessageHeader(decrypted.Msg, opts)
sigs, err := proton.ExtractSignatures(kr, msg.Body)
sigs, err := proton.ExtractSignatures(kr, decrypted.Msg.Body)
if err != nil {
log.WithError(err).WithField("id", msg.ID).Warn("Extract signature failed")
log.WithError(err).WithField("id", decrypted.Msg.ID).Warn("Extract signature failed")
}
if len(sigs) > 0 {
return writeMultipartSignedRFC822(hdr, decrypted.Bytes(), sigs[0], buf)
return writeMultipartSignedRFC822(hdr, decrypted.Body.Bytes(), sigs[0], buf)
}
return writeMultipartEncryptedRFC822(hdr, decrypted.Bytes(), buf)
return writeMultipartEncryptedRFC822(hdr, decrypted.Body.Bytes(), buf)
}
func buildPGPMIMEFallbackRFC822(msg proton.Message, opts JobOptions, buf *bytes.Buffer) error {
hdr := getMessageHeader(msg, opts)
func buildPGPMIMEFallbackRFC822(decrypted *DecryptedMessage, opts JobOptions, buf *bytes.Buffer) error {
hdr := getMessageHeader(decrypted.Msg, opts)
hdr.SetContentType("multipart/encrypted", map[string]string{
"boundary": newBoundary(msg.ID).gen(),
"boundary": newBoundary(decrypted.Msg.ID).gen(),
"protocol": "application/pgp-encrypted",
})
@ -307,7 +252,7 @@ func buildPGPMIMEFallbackRFC822(msg proton.Message, opts JobOptions, buf *bytes.
dataHdr.SetContentDisposition("inline", map[string]string{"filename": "encrypted.asc"})
dataHdr.Set("Content-Description", "OpenPGP encrypted message")
if err := writePart(w, dataHdr, []byte(msg.Body)); err != nil {
if err := writePart(w, dataHdr, []byte(decrypted.Msg.Body)); err != nil {
return err
}

View File

@ -30,10 +30,10 @@ import (
// writeCustomTextPart writes an armored-PGP text part for a message body that couldn't be decrypted.
func writeCustomTextPart(
w *message.Writer,
msg proton.Message,
decrypted *DecryptedMessage,
decError error,
) error {
enc, err := crypto.NewPGPMessageFromArmored(msg.Body)
enc, err := crypto.NewPGPMessageFromArmored(decrypted.Msg.Body)
if err != nil {
return err
}
@ -48,7 +48,7 @@ func writeCustomTextPart(
var hdr message.Header
hdr.SetContentType(string(msg.MIMEType), nil)
hdr.SetContentType(string(decrypted.Msg.MIMEType), nil)
part, err := w.CreatePart(hdr)
if err != nil {

View File

@ -39,7 +39,7 @@ func TestBuildPlainMessage(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now())
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -56,7 +56,7 @@ func TestBuildPlainMessageWithLongKey(t *testing.T) {
msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now())
msg.ParsedHeaders["ReallyVeryVeryVeryVeryVeryLongLongLongLongLongLongLongKeyThatWillHaveNotSoLongValue"] = []string{"value"}
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -73,7 +73,7 @@ func TestBuildHTMLMessage(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "<html><body>body</body></html>", time.Now())
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -91,7 +91,7 @@ func TestBuildPlainEncryptedMessage(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -116,7 +116,7 @@ func TestBuildPlainEncryptedMessageMissingHeader(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Now())
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -133,7 +133,7 @@ func TestBuildPlainEncryptedMessageInvalidHeader(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Now())
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -158,7 +158,7 @@ func TestBuildPlainSignedEncryptedMessageMissingHeader(t *testing.T) {
msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -195,7 +195,7 @@ func TestBuildPlainSignedEncryptedMessageInvalidHeader(t *testing.T) {
msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -224,7 +224,7 @@ func TestBuildPlainEncryptedLatin2Message(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -246,7 +246,7 @@ func TestBuildHTMLEncryptedMessage(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -280,7 +280,7 @@ func TestBuildPlainSignedMessage(t *testing.T) {
msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -318,7 +318,7 @@ func TestBuildPlainSignedBase64Message(t *testing.T) {
msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -349,7 +349,7 @@ func TestBuildSignedPlainEncryptedMessage(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -386,7 +386,7 @@ func TestBuildSignedHTMLEncryptedMessage(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -425,7 +425,7 @@ func TestBuildSignedPlainEncryptedMessageWithPubKey(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -471,7 +471,7 @@ func TestBuildSignedHTMLEncryptedMessageWithPubKey(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -518,7 +518,7 @@ func TestBuildSignedMultipartAlternativeEncryptedMessageWithPubKey(t *testing.T)
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -575,7 +575,7 @@ func TestBuildSignedEmbeddedMessageRFC822EncryptedMessageWithPubKey(t *testing.T
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -625,7 +625,7 @@ func TestBuildHTMLMessageWithAttachment(t *testing.T) {
msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "<html><body>body</body></html>", time.Now())
att := addTestAttachment(t, kr, &msg, "attachID", "file.png", "image/png", "attachment", "attachment")
res, err := BuildRFC822(kr, msg, [][]byte{att}, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, [][]byte{att}, JobOptions{})
require.NoError(t, err)
section(t, res, 1).
@ -649,7 +649,7 @@ func TestBuildHTMLMessageWithRFC822Attachment(t *testing.T) {
msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "<html><body>body</body></html>", time.Now())
att := addTestAttachment(t, kr, &msg, "attachID", "file.eml", "message/rfc822", "attachment", "... message/rfc822 ...")
res, err := BuildRFC822(kr, msg, [][]byte{att}, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, [][]byte{att}, JobOptions{})
require.NoError(t, err)
section(t, res, 1).
@ -673,7 +673,7 @@ func TestBuildHTMLMessageWithInlineAttachment(t *testing.T) {
msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "<html><body>body</body></html>", time.Now())
inl := addTestAttachment(t, kr, &msg, "inlineID", "file.png", "image/png", "inline", "inline")
res, err := BuildRFC822(kr, msg, [][]byte{inl}, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, [][]byte{inl}, JobOptions{})
require.NoError(t, err)
section(t, res, 1).
@ -703,7 +703,7 @@ func TestBuildHTMLMessageWithComplexAttachments(t *testing.T) {
att0 := addTestAttachment(t, kr, &msg, "attachID0", "attach0.png", "image/png", "attachment", "attach0")
att1 := addTestAttachment(t, kr, &msg, "attachID1", "attach1.png", "image/png", "attachment", "attach1")
res, err := BuildRFC822(kr, msg, [][]byte{
res, err := DecryptAndBuildRFC822(kr, msg, [][]byte{
inl0,
inl1,
att0,
@ -756,7 +756,7 @@ func TestBuildAttachmentWithExoticFilename(t *testing.T) {
msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "<html><body>body</body></html>", time.Now())
att := addTestAttachment(t, kr, &msg, "attachID", `I řeally šhould leařn czech.png`, "image/png", "attachment", "attachment")
res, err := BuildRFC822(kr, msg, [][]byte{att}, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, [][]byte{att}, JobOptions{})
require.NoError(t, err)
// The "name" and "filename" params should actually be RFC2047-encoded because they aren't 7-bit clean.
@ -778,7 +778,7 @@ func TestBuildAttachmentWithLongFilename(t *testing.T) {
msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "<html><body>body</body></html>", time.Now())
att := addTestAttachment(t, kr, &msg, "attachID", veryLongName, "image/png", "attachment", "attachment")
res, err := BuildRFC822(kr, msg, [][]byte{att}, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, [][]byte{att}, JobOptions{})
require.NoError(t, err)
// NOTE: hasMaxLineLength is too high! Long filenames should be linewrapped using multipart filenames.
@ -798,7 +798,7 @@ func TestBuildMessageDate(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
@ -814,7 +814,7 @@ func TestBuildMessageWithInvalidDate(t *testing.T) {
msg := newTestMessage(t, kr, "messageID", "addressID", "text/html", "<html><body>body</body></html>", time.Unix(-1, 0))
// Build the message as usual; the date will be before 1970.
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -822,7 +822,7 @@ func TestBuildMessageWithInvalidDate(t *testing.T) {
expectHeader(`X-Original-Date`, isMissing())
// Build the message with date sanitization enabled; the date will be RFC822's birthdate.
resFix, err := BuildRFC822(kr, msg, nil, JobOptions{SanitizeDate: true})
resFix, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{SanitizeDate: true})
require.NoError(t, err)
section(t, resFix).
@ -849,7 +849,7 @@ func TestBuildMessageWithExistingOriginalDate(t *testing.T) {
})
// Build the message as usual; the date will be before 1970.
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -857,7 +857,7 @@ func TestBuildMessageWithExistingOriginalDate(t *testing.T) {
expectHeader(`X-Original-Date`, is("Sun, 15 Jan 2023 04:23:03 +0100 (W. Europe Standard Time)"))
// Build the message with date sanitization enabled; the date will be RFC822's birthdate.
resFix, err := BuildRFC822(kr, msg, nil, JobOptions{SanitizeDate: true})
resFix, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{SanitizeDate: true})
require.NoError(t, err)
section(t, resFix).
@ -872,7 +872,7 @@ func TestBuildMessageInternalID(t *testing.T) {
kr := utils.MakeKeyRing(t)
msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now())
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).expectHeader(`Message-Id`, is(`<messageID@protonmail.internalid>`))
@ -888,7 +888,7 @@ func TestBuildMessageExternalID(t *testing.T) {
// Set the message's external ID; this should be used preferentially to set the Message-Id header field.
msg.ExternalID = "externalID"
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).expectHeader(`Message-Id`, is(`<externalID>`))
@ -903,7 +903,7 @@ func TestBuild8BitBody(t *testing.T) {
// Set an 8-bit body; the charset should be set to UTF-8.
msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "I řeally šhould leařn czech", time.Now())
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).expectContentTypeParam(`charset`, is(`utf-8`))
@ -919,7 +919,7 @@ func TestBuild8BitSubject(t *testing.T) {
// Set an 8-bit subject; it should be RFC2047-encoded.
msg.Subject = `I řeally šhould leařn czech`
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -940,7 +940,7 @@ func TestBuild8BitSender(t *testing.T) {
Address: `mail@example.com`,
}
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -961,7 +961,7 @@ func TestBuild8BitRecipients(t *testing.T) {
{Name: `leařn czech`, Address: `mail2@example.com`},
}
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).
@ -979,12 +979,12 @@ func TestBuildIncludeMessageIDReference(t *testing.T) {
// Add references.
msg.ParsedHeaders["References"] = []string{"<myreference@domain.com>"}
res, err := BuildRFC822(kr, msg, nil, JobOptions{})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.NoError(t, err)
section(t, res).expectHeader(`References`, is(`<myreference@domain.com>`))
resRef, err := BuildRFC822(kr, msg, nil, JobOptions{AddMessageIDReference: true})
resRef, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{AddMessageIDReference: true})
require.NoError(t, err)
section(t, resRef).expectHeader(`References`, is(`<myreference@domain.com> <messageID@protonmail.internalid>`))
@ -999,10 +999,10 @@ func TestBuildMessageIsDeterministic(t *testing.T) {
inl := addTestAttachment(t, kr, &msg, "inlineID", "file.png", "image/png", "inline", "inline")
att := addTestAttachment(t, kr, &msg, "attachID", "attach.png", "image/png", "attachment", "attachment")
res1, err := BuildRFC822(kr, msg, [][]byte{inl, att}, JobOptions{})
res1, err := DecryptAndBuildRFC822(kr, msg, [][]byte{inl, att}, JobOptions{})
require.NoError(t, err)
res2, err := BuildRFC822(kr, msg, [][]byte{inl, att}, JobOptions{})
res2, err := DecryptAndBuildRFC822(kr, msg, [][]byte{inl, att}, JobOptions{})
require.NoError(t, err)
assert.Equal(t, res1, res2)
@ -1017,7 +1017,7 @@ func TestBuildUndecryptableMessage(t *testing.T) {
// Use a different keyring for encrypting the message; it won't be decryptable.
msg := newTestMessage(t, utils.MakeKeyRing(t), "messageID", "addressID", "text/plain", "body", time.Now())
_, err := BuildRFC822(kr, msg, nil, JobOptions{})
_, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{})
require.ErrorIs(t, err, ErrDecryptionFailed)
}
@ -1031,7 +1031,7 @@ func TestBuildUndecryptableAttachment(t *testing.T) {
// Use a different keyring for encrypting the attachment; it won't be decryptable.
att := addTestAttachment(t, utils.MakeKeyRing(t), &msg, "attachID", "file.png", "image/png", "attachment", "attachment")
_, err := BuildRFC822(kr, msg, [][]byte{att}, JobOptions{})
_, err := DecryptAndBuildRFC822(kr, msg, [][]byte{att}, JobOptions{})
require.ErrorIs(t, err, ErrDecryptionFailed)
}
@ -1046,7 +1046,7 @@ func TestBuildCustomMessagePlain(t *testing.T) {
msg := newTestMessage(t, foreignKR, "messageID", "addressID", "text/plain", "body", time.Now())
// Tell the job to ignore decryption errors; a custom message will be returned instead of an error.
res, err := BuildRFC822(kr, msg, nil, JobOptions{IgnoreDecryptionErrors: true})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{IgnoreDecryptionErrors: true})
require.NoError(t, err)
section(t, res).
@ -1070,7 +1070,7 @@ func TestBuildCustomMessageHTML(t *testing.T) {
msg := newTestMessage(t, foreignKR, "messageID", "addressID", "text/html", "<html><body>body</body></html>", time.Now())
// Tell the job to ignore decryption errors; a custom message will be returned instead of an error.
res, err := BuildRFC822(kr, msg, nil, JobOptions{IgnoreDecryptionErrors: true})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{IgnoreDecryptionErrors: true})
require.NoError(t, err)
section(t, res).
@ -1098,7 +1098,7 @@ func TestBuildCustomMessageEncrypted(t *testing.T) {
msg.Subject = "this is a subject to make sure we preserve subject"
// Tell the job to ignore decryption errors; a custom message will be returned instead of an error.
res, err := BuildRFC822(kr, msg, nil, JobOptions{IgnoreDecryptionErrors: true})
res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{IgnoreDecryptionErrors: true})
require.NoError(t, err)
section(t, res).
@ -1132,7 +1132,7 @@ func TestBuildCustomMessagePlainWithAttachment(t *testing.T) {
att := addTestAttachment(t, foreignKR, &msg, "attachID", "file.png", "image/png", "attachment", "attachment")
// Tell the job to ignore decryption errors; a custom message will be returned instead of an error.
res, err := BuildRFC822(kr, msg, [][]byte{att}, JobOptions{IgnoreDecryptionErrors: true})
res, err := DecryptAndBuildRFC822(kr, msg, [][]byte{att}, JobOptions{IgnoreDecryptionErrors: true})
require.NoError(t, err)
section(t, res).
@ -1165,7 +1165,7 @@ func TestBuildCustomMessageHTMLWithAttachment(t *testing.T) {
att := addTestAttachment(t, foreignKR, &msg, "attachID", "file.png", "image/png", "attachment", "attachment")
// Tell the job to ignore decryption errors; a custom message will be returned instead of an error.
res, err := BuildRFC822(kr, msg, [][]byte{att}, JobOptions{IgnoreDecryptionErrors: true})
res, err := DecryptAndBuildRFC822(kr, msg, [][]byte{att}, JobOptions{IgnoreDecryptionErrors: true})
require.NoError(t, err)
section(t, res).
@ -1200,7 +1200,7 @@ func TestBuildCustomMessageOnlyBodyIsUndecryptable(t *testing.T) {
att := addTestAttachment(t, kr, &msg, "attachID", "file.png", "image/png", "attachment", "attachment")
// Tell the job to ignore decryption errors; a custom message will be returned instead of an error.
res, err := BuildRFC822(kr, msg, [][]byte{att}, JobOptions{IgnoreDecryptionErrors: true})
res, err := DecryptAndBuildRFC822(kr, msg, [][]byte{att}, JobOptions{IgnoreDecryptionErrors: true})
require.NoError(t, err)
section(t, res).
@ -1233,7 +1233,7 @@ func TestBuildCustomMessageOnlyAttachmentIsUndecryptable(t *testing.T) {
att := addTestAttachment(t, foreignKR, &msg, "attachID", "file.png", "image/png", "attachment", "attachment")
// Tell the job to ignore decryption errors; a custom message will be returned instead of an error.
res, err := BuildRFC822(kr, msg, [][]byte{att}, JobOptions{IgnoreDecryptionErrors: true})
res, err := DecryptAndBuildRFC822(kr, msg, [][]byte{att}, JobOptions{IgnoreDecryptionErrors: true})
require.NoError(t, err)
section(t, res).
@ -1271,7 +1271,7 @@ func TestBuildComplexMIMEType(t *testing.T) {
att0 := addTestAttachment(t, kr, &msg, "attachID0", "attach0.png", "image/png", "attachment", "attach0")
att1 := addTestAttachment(t, kr, &msg, "attachID1", "Cat_August_2010-4.jpeg", "image/jpeg; name=Cat_August_2010-4.jpeg; x-unix-mode=0644", "attachment", "attach1")
res, err := BuildRFC822(kr, msg, [][]byte{
res, err := DecryptAndBuildRFC822(kr, msg, [][]byte{
att0,
att1,
}, JobOptions{})

88
pkg/message/decrypt.go Normal file
View File

@ -0,0 +1,88 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bytes"
"encoding/base64"
"io"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/pkg/errors"
)
type DecryptedAttachment struct {
Packet []byte
Encrypted []byte
Data bytes.Buffer
Err error
}
type DecryptedMessage struct {
Msg proton.Message
Body bytes.Buffer
BodyErr error
Attachments []DecryptedAttachment
}
var ErrInvalidAttachmentPacket = errors.New("invalid attachment packet")
func DecryptMessage(kr *crypto.KeyRing, msg proton.Message, attData [][]byte) DecryptedMessage {
result := DecryptedMessage{
Msg: msg,
}
result.Body.Grow(len(msg.Body))
if err := msg.DecryptInto(kr, &result.Body); err != nil {
result.BodyErr = errors.Wrap(ErrDecryptionFailed, err.Error())
}
result.Attachments = make([]DecryptedAttachment, len(msg.Attachments))
for i, attachment := range msg.Attachments {
result.Attachments[i].Encrypted = attData[i]
kps, err := base64.StdEncoding.DecodeString(attachment.KeyPackets)
if err != nil {
result.Attachments[i].Err = errors.Wrap(ErrInvalidAttachmentPacket, err.Error())
continue
}
result.Attachments[i].Packet = kps
// Use io.Multi
attachmentReader := io.MultiReader(bytes.NewReader(kps), bytes.NewReader(attData[i]))
stream, err := kr.DecryptStream(attachmentReader, nil, crypto.GetUnixTime())
if err != nil {
result.Attachments[i].Err = errors.Wrap(ErrDecryptionFailed, err.Error())
continue
}
result.Attachments[i].Data.Grow(len(kps) + len(attData))
if _, err := result.Attachments[i].Data.ReadFrom(stream); err != nil {
result.Attachments[i].Err = errors.Wrap(ErrDecryptionFailed, err.Error())
continue
}
}
return result
}

View File

@ -0,0 +1,40 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bytes"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
)
func DecryptAndBuildRFC822(kr *crypto.KeyRing, msg proton.Message, attData [][]byte, opts JobOptions) ([]byte, error) {
buf := new(bytes.Buffer)
if err := DecryptAndBuildRFC822Into(kr, msg, attData, opts, buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func DecryptAndBuildRFC822Into(kr *crypto.KeyRing, msg proton.Message, attData [][]byte, opts JobOptions, buf *bytes.Buffer) error {
decrypted := DecryptMessage(kr, msg, attData)
return BuildRFC822Into(kr, &decrypted, opts, buf)
}