diff --git a/internal/services/imapservice/connector.go b/internal/services/imapservice/connector.go index 03ca7dbe..475bbfa6 100644 --- a/internal/services/imapservice/connector.go +++ b/internal/services/imapservice/connector.go @@ -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) } diff --git a/internal/services/imapservice/sync_build.go b/internal/services/imapservice/sync_build.go index 1af8e407..fe7348cb 100644 --- a/internal/services/imapservice/sync_build.go +++ b/internal/services/imapservice/sync_build.go @@ -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 { diff --git a/internal/services/imapservice/sync_message_builder.go b/internal/services/imapservice/sync_message_builder.go index 45adbb61..a0a006e2 100644 --- a/internal/services/imapservice/sync_message_builder.go +++ b/internal/services/imapservice/sync_message_builder.go @@ -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 } diff --git a/pkg/message/build.go b/pkg/message/build.go index baf916ee..e79f287c 100644 --- a/pkg/message/build.go +++ b/pkg/message/build.go @@ -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 } diff --git a/pkg/message/build_custom.go b/pkg/message/build_custom.go index 175991da..dbbb7fc4 100644 --- a/pkg/message/build_custom.go +++ b/pkg/message/build_custom.go @@ -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 { diff --git a/pkg/message/build_test.go b/pkg/message/build_test.go index 58c2ee9b..63807012 100644 --- a/pkg/message/build_test.go +++ b/pkg/message/build_test.go @@ -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", "body", 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", "body", 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", "body", 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", "body", 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", "body", 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", "body", 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", "body", 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(``)) @@ -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(``)) @@ -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{""} - res, err := BuildRFC822(kr, msg, nil, JobOptions{}) + res, err := DecryptAndBuildRFC822(kr, msg, nil, JobOptions{}) require.NoError(t, err) section(t, res).expectHeader(`References`, is(``)) - 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(` `)) @@ -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", "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). @@ -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{}) diff --git a/pkg/message/decrypt.go b/pkg/message/decrypt.go new file mode 100644 index 00000000..ee6a7dd2 --- /dev/null +++ b/pkg/message/decrypt.go @@ -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 . + +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 +} diff --git a/pkg/message/decrypt_and_build.go b/pkg/message/decrypt_and_build.go new file mode 100644 index 00000000..9c35678c --- /dev/null +++ b/pkg/message/decrypt_and_build.go @@ -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 . + +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) +}