You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
258 lines
6.0 KiB
258 lines
6.0 KiB
// Copyright (c) 2022 Proton Technologies AG |
|
// |
|
// This file is part of ProtonMail Bridge. |
|
// |
|
// ProtonMail 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. |
|
// |
|
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. |
|
|
|
package message |
|
|
|
import ( |
|
"bytes" |
|
"encoding/base64" |
|
"io" |
|
"io/ioutil" |
|
"mime" |
|
"mime/quotedprintable" |
|
"strings" |
|
|
|
"github.com/ProtonMail/gopenpgp/v2/crypto" |
|
"github.com/emersion/go-message/textproto" |
|
pmmime "github.com/ljanyst/peroxide/pkg/mime" |
|
"github.com/pkg/errors" |
|
) |
|
|
|
func EncryptRFC822(kr *crypto.KeyRing, r io.Reader) ([]byte, error) { |
|
b, err := ioutil.ReadAll(r) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
header, body, err := readHeaderBody(b) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
buf := new(bytes.Buffer) |
|
|
|
result, err := writeEncryptedPart(kr, header, bytes.NewReader(body)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
if err := textproto.WriteHeader(buf, *header); err != nil { |
|
return nil, err |
|
} |
|
|
|
if _, err := result.WriteTo(buf); err != nil { |
|
return nil, err |
|
} |
|
|
|
return buf.Bytes(), nil |
|
} |
|
|
|
func writeEncryptedPart(kr *crypto.KeyRing, header *textproto.Header, r io.Reader) (io.WriterTo, error) { |
|
decoder, err := getTransferDecoder(r, header.Get("Content-Transfer-Encoding")) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
encoded := new(bytes.Buffer) |
|
|
|
contentType, contentParams, err := parseContentType(header.Get("Content-Type")) |
|
// Ignoring invalid media parameter makes it work for invalid tutanota RFC2047-encoded attachment filenames since we often only really need the content type and not the optional media parameters. |
|
if err != nil && !errors.Is(err, mime.ErrInvalidMediaParameter) { |
|
return nil, err |
|
} |
|
|
|
switch { |
|
case contentType == "", strings.HasPrefix(contentType, "text/"), strings.HasPrefix(contentType, "message/"): |
|
header.Del("Content-Transfer-Encoding") |
|
|
|
if charset, ok := contentParams["charset"]; ok { |
|
if reader, err := pmmime.CharsetReader(charset, decoder); err == nil { |
|
decoder = reader |
|
|
|
// We can decode the charset to utf-8 so let's set that as the content type charset parameter. |
|
contentParams["charset"] = "utf-8" |
|
|
|
header.Set("Content-Type", mime.FormatMediaType(contentType, contentParams)) |
|
} |
|
} |
|
|
|
if err := encode(&writeCloser{encoded}, func(w io.Writer) error { |
|
return writeEncryptedTextPart(w, decoder, kr) |
|
}); err != nil { |
|
return nil, err |
|
} |
|
|
|
case contentType == "multipart/encrypted": |
|
if _, err := encoded.ReadFrom(decoder); err != nil { |
|
return nil, err |
|
} |
|
|
|
case strings.HasPrefix(contentType, "multipart/"): |
|
if err := encode(&writeCloser{encoded}, func(w io.Writer) error { |
|
return writeEncryptedMultiPart(kr, w, header, decoder) |
|
}); err != nil { |
|
return nil, err |
|
} |
|
|
|
default: |
|
header.Set("Content-Transfer-Encoding", "base64") |
|
|
|
if err := encode(base64.NewEncoder(base64.StdEncoding, encoded), func(w io.Writer) error { |
|
return writeEncryptedAttachmentPart(w, decoder, kr) |
|
}); err != nil { |
|
return nil, err |
|
} |
|
} |
|
|
|
return encoded, nil |
|
} |
|
|
|
func writeEncryptedTextPart(w io.Writer, r io.Reader, kr *crypto.KeyRing) error { |
|
dec, err := ioutil.ReadAll(r) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
var arm string |
|
|
|
if msg, err := crypto.NewPGPMessageFromArmored(string(dec)); err != nil { |
|
enc, err := kr.Encrypt(crypto.NewPlainMessage(dec), kr) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if arm, err = enc.GetArmored(); err != nil { |
|
return err |
|
} |
|
} else if arm, err = msg.GetArmored(); err != nil { |
|
return err |
|
} |
|
|
|
if _, err := io.WriteString(w, arm); err != nil { |
|
return err |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func writeEncryptedAttachmentPart(w io.Writer, r io.Reader, kr *crypto.KeyRing) error { |
|
dec, err := ioutil.ReadAll(r) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
enc, err := kr.Encrypt(crypto.NewPlainMessage(dec), kr) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if _, err := w.Write(enc.GetBinary()); err != nil { |
|
return err |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func writeEncryptedMultiPart(kr *crypto.KeyRing, w io.Writer, header *textproto.Header, r io.Reader) error { |
|
_, contentParams, err := parseContentType(header.Get("Content-Type")) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
scanner, err := newPartScanner(r, contentParams["boundary"]) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
parts, err := scanner.scanAll() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
writer := newPartWriter(w, contentParams["boundary"]) |
|
|
|
for _, part := range parts { |
|
header, body, err := readHeaderBody(part.b) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
result, err := writeEncryptedPart(kr, header, bytes.NewReader(body)) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if err := writer.createPart(func(w io.Writer) error { |
|
if err := textproto.WriteHeader(w, *header); err != nil { |
|
return err |
|
} |
|
|
|
if _, err := result.WriteTo(w); err != nil { |
|
return err |
|
} |
|
|
|
return nil |
|
}); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
return writer.done() |
|
} |
|
|
|
func getTransferDecoder(r io.Reader, encoding string) (io.Reader, error) { |
|
switch strings.ToLower(encoding) { |
|
case "base64": |
|
cleaner, err := NewBase64Sanitizer(r) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return base64.NewDecoder(base64.StdEncoding, cleaner), nil |
|
|
|
case "quoted-printable": |
|
cleaner, err := NewQuotedPrintableSanitizer(r) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return quotedprintable.NewReader(cleaner), nil |
|
|
|
default: |
|
return r, nil |
|
} |
|
} |
|
|
|
func encode(wc io.WriteCloser, fn func(io.Writer) error) error { |
|
if err := fn(wc); err != nil { |
|
return err |
|
} |
|
|
|
return wc.Close() |
|
} |
|
|
|
type writeCloser struct { |
|
io.Writer |
|
} |
|
|
|
func (writeCloser) Close() error { return nil } |
|
|
|
func parseContentType(val string) (string, map[string]string, error) { |
|
if val == "" { |
|
val = "text/plain" |
|
} |
|
|
|
return pmmime.ParseMediaType(val) |
|
}
|
|
|