From 72b56494bcac2bf2c1d1e7e39feb61746c3dcb37 Mon Sep 17 00:00:00 2001 From: emersion Date: Sun, 3 Dec 2017 11:55:59 +0100 Subject: [PATCH] smtp: upload attachments --- cmd/hydroxide/hydroxide.go | 4 +-- protonmail/attachments.go | 61 ++++++++++++++++++++++++++++++---- protonmail/crypto.go | 14 ++++++++ protonmail/messages.go | 63 ++++++++++++++++------------------- smtp/smtp.go | 68 +++++++++++++++++++++++++++++++++----- 5 files changed, 159 insertions(+), 51 deletions(-) diff --git a/cmd/hydroxide/hydroxide.go b/cmd/hydroxide/hydroxide.go index 86bdf1b..222efac 100644 --- a/cmd/hydroxide/hydroxide.go +++ b/cmd/hydroxide/hydroxide.go @@ -135,8 +135,8 @@ func main() { be := smtpbackend.New(sessions) s := smtp.NewServer(be) - s.Addr = "127.0.0.1:"+port - s.Domain = "localhost" // TODO: make this configurable + s.Addr = "127.0.0.1:" + port + s.Domain = "localhost" // TODO: make this configurable s.AllowInsecureAuth = true // TODO: remove this log.Println("Starting SMTP server at", s.Addr) diff --git a/protonmail/attachments.go b/protonmail/attachments.go index 44459f6..99e8ce1 100644 --- a/protonmail/attachments.go +++ b/protonmail/attachments.go @@ -1,13 +1,18 @@ package protonmail import ( + "bytes" "encoding/base64" + "errors" "fmt" - "net/http" "io" - "mime" "mime/multipart" + "net/http" + "strconv" "strings" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" ) type AttachmentKey struct { @@ -25,7 +30,52 @@ type Attachment struct { ContentID string KeyPackets string // encrypted with the user's key, base64-encoded //Headers map[string]string - Signature string + Signature string +} + +// Encrypt generates an encrypted key for the provided recipients and encrypts +// to w the data that will be written to the returned io.WriteCloser. +// +// signed is ignored for now. +func (att *Attachment) Encrypt(ciphertext io.Writer, to []*openpgp.Entity, signed *openpgp.Entity) (cleartext io.WriteCloser, err error) { + config := &packet.Config{} + + var encodedKeyPackets bytes.Buffer + keyPackets := base64.NewEncoder(base64.StdEncoding, &encodedKeyPackets) + + unencryptedKey, err := generateUnencryptedKey(packet.CipherAES256, config) + if err != nil { + return nil, err + } + + for _, pub := range to { + encKey, ok := encryptionKey(pub, config.Now()) + if !ok { + return nil, errors.New("cannot encrypt an attachment to key id " + strconv.FormatUint(pub.PrimaryKey.KeyId, 16) + " because it has no encryption keys") + } + + err := packet.SerializeEncryptedKey(keyPackets, encKey.PublicKey, unencryptedKey.CipherFunc, unencryptedKey.Key, config) + if err != nil { + return nil, err + } + } + + keyPackets.Close() + att.KeyPackets = encodedKeyPackets.String() + + encryptedData, err := packet.SerializeSymmetricallyEncrypted(ciphertext, unencryptedKey.CipherFunc, unencryptedKey.Key, config) + if err != nil { + return nil, err + } + + // TODO: sign, see https://github.com/golang/crypto/blob/master/openpgp/write.go#L287 + + literalData, err := packet.SerializeLiteral(encryptedData, true, att.Name, 0) + if err != nil { + return nil, err + } + + return literalData, nil } // GetAttachment downloads an attachment's payload. The returned io.ReadCloser @@ -88,7 +138,7 @@ func (c *Client) CreateAttachment(att *Attachment, r io.Reader) (created *Attach } } - if w, err := mw.CreateFormFile("DataPackets", "DataPackets.pgp"); err != nil { + if w, err := mw.CreateFormFile("DataPacket", "DataPacket.pgp"); err != nil { pw.CloseWithError(err) return } else if _, err := io.Copy(w, r); err != nil { @@ -109,8 +159,7 @@ func (c *Client) CreateAttachment(att *Attachment, r io.Reader) (created *Attach return nil, err } - params := map[string]string{"boundary": mw.Boundary()} - req.Header.Set("Content-Type", mime.FormatMediaType("multipart/form-data", params)) + req.Header.Set("Content-Type", mw.FormDataContentType()) var respData struct { resp diff --git a/protonmail/crypto.go b/protonmail/crypto.go index 365ec68..bc8fe0c 100644 --- a/protonmail/crypto.go +++ b/protonmail/crypto.go @@ -1,9 +1,11 @@ package protonmail import ( + "io" "time" "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" ) // primaryIdentity returns the Identity marked as primary or the first identity @@ -89,3 +91,15 @@ func signingKey(e *openpgp.Entity, now time.Time) (openpgp.Key, bool) { return openpgp.Key{}, false } + +func generateUnencryptedKey(cipher packet.CipherFunction, config *packet.Config) (*packet.EncryptedKey, error) { + symKey := make([]byte, cipher.KeySize()) + if _, err := io.ReadFull(config.Random(), symKey); err != nil { + return nil, err + } + + return &packet.EncryptedKey{ + CipherFunc: cipher, + Key: symKey, + }, nil +} diff --git a/protonmail/messages.go b/protonmail/messages.go index e5571d1..0e548e9 100644 --- a/protonmail/messages.go +++ b/protonmail/messages.go @@ -63,7 +63,7 @@ type Message struct { SpamScore int AddressID string Body string - MIMEType string + MIMEType string `json:",omitempty"` CCList []*MessageAddress BCCList []*MessageAddress Header string @@ -214,56 +214,43 @@ const ( type MessagePackage struct { Type MessagePackageType - BodyKeyPacket string + BodyKeyPacket string AttachmentKeyPackets map[string]string - Signature int + Signature int // Only if encrypted for outside PasswordHint string - Auth interface{} // TODO - Token string - EncToken string + Auth interface{} // TODO + Token string + EncToken string } type MessagePackageSet struct { - Type MessagePackageType // OR of each Type + Type MessagePackageType // OR of each Type Addresses map[string]*MessagePackage - MIMEType string - Body string // Body data packet + MIMEType string + Body string // Body data packet // Only if cleartext is sent - BodyKey string + BodyKey string AttachmentKeys map[string]string - bodyKey *packet.EncryptedKey + bodyKey *packet.EncryptedKey attachmentKeys map[string]*packet.EncryptedKey } func NewMessagePackageSet(attachmentKeys map[string]*packet.EncryptedKey) *MessagePackageSet { return &MessagePackageSet{ - Addresses: make(map[string]*MessagePackage), + Addresses: make(map[string]*MessagePackage), attachmentKeys: attachmentKeys, } } -func (set *MessagePackageSet) generateBodyKey(cipher packet.CipherFunction, config *packet.Config) error { - symKey := make([]byte, cipher.KeySize()) - if _, err := io.ReadFull(config.Random(), symKey); err != nil { - return err - } - - set.bodyKey = &packet.EncryptedKey{ - CipherFunc: cipher, - Key: symKey, - } - return nil -} - type outgoingMessageWriter struct { - cleartext io.WriteCloser + cleartext io.WriteCloser ciphertext io.WriteCloser - encoded *bytes.Buffer - set *MessagePackageSet + encoded *bytes.Buffer + set *MessagePackageSet } func (w *outgoingMessageWriter) Write(p []byte) (int, error) { @@ -282,14 +269,20 @@ func (w *outgoingMessageWriter) Close() error { return nil } -func (set *MessagePackageSet) Encrypt(mimeType string) (io.WriteCloser, error) { +// Encrypt encrypts the data that will be written to the returned +// io.WriteCloser. +// +// The signed parameter is ignored for now. +func (set *MessagePackageSet) Encrypt(mimeType string, signed *openpgp.Entity) (io.WriteCloser, error) { set.MIMEType = mimeType config := &packet.Config{} - if err := set.generateBodyKey(packet.CipherAES256, config); err != nil { + key, err := generateUnencryptedKey(packet.CipherAES256, config) + if err != nil { return nil, err } + set.bodyKey = key var encoded bytes.Buffer ciphertext := base64.NewEncoder(base64.StdEncoding, &encoded) @@ -307,10 +300,10 @@ func (set *MessagePackageSet) Encrypt(mimeType string) (io.WriteCloser, error) { } return &outgoingMessageWriter{ - cleartext: literalData, + cleartext: literalData, ciphertext: ciphertext, - encoded: &encoded, - set: set, + encoded: &encoded, + set: set, }, nil } @@ -368,8 +361,8 @@ func (set *MessagePackageSet) AddInternal(addr string, pub *openpgp.Entity) erro set.Type |= MessagePackageInternal set.Addresses[addr] = &MessagePackage{ - Type: MessagePackageInternal, - BodyKeyPacket: bodyKey, + Type: MessagePackageInternal, + BodyKeyPacket: bodyKey, AttachmentKeyPackets: attachmentKeys, } return nil diff --git a/smtp/smtp.go b/smtp/smtp.go index cb88e3c..cafd757 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -101,8 +101,24 @@ func (u *user) Send(from string, to []string, r io.Reader) error { AddressID: fromAddr.ID, } + // Create an empty draft + plaintext, err := msg.Encrypt([]*openpgp.Entity{privateKey}, privateKey) + if err != nil { + return err + } + if err := plaintext.Close(); err != nil { + return err + } + + // TODO: parentID from In-Reply-To + msg, err = u.c.CreateDraftMessage(msg, "") + if err != nil { + return fmt.Errorf("cannot create draft message: %v", err) + } + var body *bytes.Buffer var bodyType string + var attachments []*protonmail.Attachment for { p, err := mr.NextPart() @@ -129,7 +145,42 @@ func (u *user) Send(from string, to []string, r io.Reader) error { return err } case mail.AttachmentHeader: - // TODO + t, _, err := h.ContentType() + if err != nil { + break + } + + filename, err := h.Filename() + if err != nil { + break + } + + att := &protonmail.Attachment{ + MessageID: msg.ID, + Name: filename, + MIMEType: t, + ContentID: h.Get("Content-Id"), + // TODO: Header + } + + var b bytes.Buffer + cleartext, err := att.Encrypt(&b, []*openpgp.Entity{privateKey}, privateKey) + if err != nil { + return fmt.Errorf("cannot encrypt attachment: %v", err) + } + if _, err := io.Copy(cleartext, p.Body); err != nil { + return fmt.Errorf("cannot encrypt attachment: %v", err) + } + if err := cleartext.Close(); err != nil { + return fmt.Errorf("cannot encrypt attachment: %v", err) + } + + att, err = u.c.CreateAttachment(att, &b) + if err != nil { + return fmt.Errorf("cannot upload attachment: %v", err) + } + + attachments = append(attachments, att) } } @@ -139,7 +190,7 @@ func (u *user) Send(from string, to []string, r io.Reader) error { msg.MIMEType = bodyType - plaintext, err := msg.Encrypt([]*openpgp.Entity{privateKey}, privateKey) + plaintext, err = msg.Encrypt([]*openpgp.Entity{privateKey}, privateKey) if err != nil { return err } @@ -150,15 +201,14 @@ func (u *user) Send(from string, to []string, r io.Reader) error { return err } - // TODO: parentID from In-Reply-To - msg, err = u.c.CreateDraftMessage(msg, "") + msg, err = u.c.UpdateDraftMessage(msg) if err != nil { - return fmt.Errorf("cannot create draft message: %v", err) + return fmt.Errorf("cannot update draft message: %v", err) } outgoing := &protonmail.OutgoingMessage{ID: msg.ID} - recipients := make([]*mail.Address, 0, len(toList) + len(ccList) + len(bccList)) + recipients := make([]*mail.Address, 0, len(toList)+len(ccList)+len(bccList)) recipients = append(recipients, toList...) recipients = append(recipients, ccList...) recipients = append(recipients, bccList...) @@ -186,9 +236,10 @@ func (u *user) Send(from string, to []string, r io.Reader) error { } if len(plaintextRecipients) > 0 { + // TODO: attachments plaintextSet := protonmail.NewMessagePackageSet(nil) - plaintext, err := plaintextSet.Encrypt(bodyType) + plaintext, err := plaintextSet.Encrypt(bodyType, privateKey) if err != nil { return err } @@ -210,9 +261,10 @@ func (u *user) Send(from string, to []string, r io.Reader) error { } if len(encryptedRecipients) > 0 { + // TODO: attachments encryptedSet := protonmail.NewMessagePackageSet(nil) - plaintext, err := encryptedSet.Encrypt(bodyType) + plaintext, err := encryptedSet.Encrypt(bodyType, privateKey) if err != nil { return err }