smtp: upload attachments

This commit is contained in:
emersion 2017-12-03 11:55:59 +01:00
parent 0e1b866880
commit 72b56494bc
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
5 changed files with 159 additions and 51 deletions

View File

@ -1,13 +1,18 @@
package protonmail package protonmail
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"net/http"
"io" "io"
"mime"
"mime/multipart" "mime/multipart"
"net/http"
"strconv"
"strings" "strings"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/packet"
) )
type AttachmentKey struct { type AttachmentKey struct {
@ -28,6 +33,51 @@ type Attachment struct {
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 // GetAttachment downloads an attachment's payload. The returned io.ReadCloser
// may be encrypted. // may be encrypted.
func (c *Client) GetAttachment(id string) (io.ReadCloser, error) { func (c *Client) GetAttachment(id string) (io.ReadCloser, error) {
@ -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) pw.CloseWithError(err)
return return
} else if _, err := io.Copy(w, r); err != nil { } 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 return nil, err
} }
params := map[string]string{"boundary": mw.Boundary()} req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Content-Type", mime.FormatMediaType("multipart/form-data", params))
var respData struct { var respData struct {
resp resp

View File

@ -1,9 +1,11 @@
package protonmail package protonmail
import ( import (
"io"
"time" "time"
"golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/packet"
) )
// primaryIdentity returns the Identity marked as primary or the first identity // 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 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
}

View File

@ -63,7 +63,7 @@ type Message struct {
SpamScore int SpamScore int
AddressID string AddressID string
Body string Body string
MIMEType string MIMEType string `json:",omitempty"`
CCList []*MessageAddress CCList []*MessageAddress
BCCList []*MessageAddress BCCList []*MessageAddress
Header string Header string
@ -246,19 +246,6 @@ func NewMessagePackageSet(attachmentKeys map[string]*packet.EncryptedKey) *Messa
} }
} }
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 { type outgoingMessageWriter struct {
cleartext io.WriteCloser cleartext io.WriteCloser
ciphertext io.WriteCloser ciphertext io.WriteCloser
@ -282,14 +269,20 @@ func (w *outgoingMessageWriter) Close() error {
return nil 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 set.MIMEType = mimeType
config := &packet.Config{} config := &packet.Config{}
if err := set.generateBodyKey(packet.CipherAES256, config); err != nil { key, err := generateUnencryptedKey(packet.CipherAES256, config)
if err != nil {
return nil, err return nil, err
} }
set.bodyKey = key
var encoded bytes.Buffer var encoded bytes.Buffer
ciphertext := base64.NewEncoder(base64.StdEncoding, &encoded) ciphertext := base64.NewEncoder(base64.StdEncoding, &encoded)

View File

@ -101,8 +101,24 @@ func (u *user) Send(from string, to []string, r io.Reader) error {
AddressID: fromAddr.ID, 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 body *bytes.Buffer
var bodyType string var bodyType string
var attachments []*protonmail.Attachment
for { for {
p, err := mr.NextPart() p, err := mr.NextPart()
@ -129,7 +145,42 @@ func (u *user) Send(from string, to []string, r io.Reader) error {
return err return err
} }
case mail.AttachmentHeader: 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 msg.MIMEType = bodyType
plaintext, err := msg.Encrypt([]*openpgp.Entity{privateKey}, privateKey) plaintext, err = msg.Encrypt([]*openpgp.Entity{privateKey}, privateKey)
if err != nil { if err != nil {
return err return err
} }
@ -150,10 +201,9 @@ func (u *user) Send(from string, to []string, r io.Reader) error {
return err return err
} }
// TODO: parentID from In-Reply-To msg, err = u.c.UpdateDraftMessage(msg)
msg, err = u.c.CreateDraftMessage(msg, "")
if err != nil { 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} outgoing := &protonmail.OutgoingMessage{ID: msg.ID}
@ -186,9 +236,10 @@ func (u *user) Send(from string, to []string, r io.Reader) error {
} }
if len(plaintextRecipients) > 0 { if len(plaintextRecipients) > 0 {
// TODO: attachments
plaintextSet := protonmail.NewMessagePackageSet(nil) plaintextSet := protonmail.NewMessagePackageSet(nil)
plaintext, err := plaintextSet.Encrypt(bodyType) plaintext, err := plaintextSet.Encrypt(bodyType, privateKey)
if err != nil { if err != nil {
return err return err
} }
@ -210,9 +261,10 @@ func (u *user) Send(from string, to []string, r io.Reader) error {
} }
if len(encryptedRecipients) > 0 { if len(encryptedRecipients) > 0 {
// TODO: attachments
encryptedSet := protonmail.NewMessagePackageSet(nil) encryptedSet := protonmail.NewMessagePackageSet(nil)
plaintext, err := encryptedSet.Encrypt(bodyType) plaintext, err := encryptedSet.Encrypt(bodyType, privateKey)
if err != nil { if err != nil {
return err return err
} }