diff --git a/protonmail/attachments.go b/protonmail/attachments.go index 4e23eba..509b579 100644 --- a/protonmail/attachments.go +++ b/protonmail/attachments.go @@ -82,19 +82,13 @@ func (att *Attachment) Encrypt(ciphertext io.Writer, signed *openpgp.Entity) (cl return nil, errors.New("cannot encrypt attachment: no attachment key available") } - encryptedData, err := packet.SerializeSymmetricallyEncrypted(ciphertext, att.unencryptedKey.CipherFunc, att.unencryptedKey.Key, config) - if err != nil { - return nil, err + // TODO: sign and store signature in att.Signature + + hints := &openpgp.FileHints{ + IsBinary: true, + FileName: att.Name, } - - // 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 + return symetricallyEncrypt(ciphertext, att.unencryptedKey, nil, hints, config) } func (att *Attachment) Read(ciphertext io.Reader, keyring openpgp.KeyRing, prompt openpgp.PromptFunction) (*openpgp.MessageDetails, error) { diff --git a/protonmail/crypto.go b/protonmail/crypto.go index bc8fe0c..7c76510 100644 --- a/protonmail/crypto.go +++ b/protonmail/crypto.go @@ -2,6 +2,8 @@ package protonmail import ( "io" + "crypto" + "hash" "time" "golang.org/x/crypto/openpgp" @@ -103,3 +105,103 @@ func generateUnencryptedKey(cipher packet.CipherFunction, config *packet.Config) Key: symKey, }, nil } + +func symetricallyEncrypt(ciphertext io.Writer, symKey *packet.EncryptedKey, signer *packet.PrivateKey, hints *openpgp.FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) { + // From https://github.com/golang/crypto/blob/master/openpgp/write.go#L172 + + encryptedData, err := packet.SerializeSymmetricallyEncrypted(ciphertext, symKey.CipherFunc, symKey.Key, config) + if err != nil { + return nil, err + } + + hash := crypto.SHA256 + + if signer != nil { + ops := &packet.OnePassSignature{ + SigType: packet.SigTypeBinary, + Hash: hash, + PubKeyAlgo: signer.PubKeyAlgo, + KeyId: signer.KeyId, + IsLast: true, + } + if err := ops.Serialize(encryptedData); err != nil { + return nil, err + } + } + + if hints == nil { + hints = &openpgp.FileHints{} + } + + w := encryptedData + if signer != nil { + // If we need to write a signature packet after the literal + // data then we need to stop literalData from closing + // encryptedData. + w = noOpCloser{encryptedData} + } + var epochSeconds uint32 + if !hints.ModTime.IsZero() { + epochSeconds = uint32(hints.ModTime.Unix()) + } + literalData, err := packet.SerializeLiteral(w, hints.IsBinary, hints.FileName, epochSeconds) + if err != nil { + return nil, err + } + + if signer != nil { + return signatureWriter{encryptedData, literalData, hash, hash.New(), signer, config}, nil + } + return literalData, nil +} + +// signatureWriter hashes the contents of a message while passing it along to +// literalData. When closed, it closes literalData, writes a signature packet +// to encryptedData and then also closes encryptedData. +type signatureWriter struct { + encryptedData io.WriteCloser + literalData io.WriteCloser + hashType crypto.Hash + h hash.Hash + signer *packet.PrivateKey + config *packet.Config +} + +func (s signatureWriter) Write(data []byte) (int, error) { + s.h.Write(data) + return s.literalData.Write(data) +} + +func (s signatureWriter) Close() error { + sig := &packet.Signature{ + SigType: packet.SigTypeBinary, + PubKeyAlgo: s.signer.PubKeyAlgo, + Hash: s.hashType, + CreationTime: s.config.Now(), + IssuerKeyId: &s.signer.KeyId, + } + + if err := sig.Sign(s.h, s.signer, s.config); err != nil { + return err + } + if err := s.literalData.Close(); err != nil { + return err + } + if err := sig.Serialize(s.encryptedData); err != nil { + return err + } + return s.encryptedData.Close() +} + +// noOpCloser is like an ioutil.NopCloser, but for an io.Writer. +type noOpCloser struct { + w io.Writer +} + +func (c noOpCloser) Write(data []byte) (n int, err error) { + return c.w.Write(data) +} + +func (c noOpCloser) Close() error { + return nil +} diff --git a/protonmail/messages.go b/protonmail/messages.go index b3e87ea..a7c3c20 100644 --- a/protonmail/messages.go +++ b/protonmail/messages.go @@ -308,7 +308,7 @@ const ( MessagePackageMIME = 32 ) -// From https://github.com/ProtonMail/Angular/blob/v3/src/app/composer/controllers/composeMessage.js#L656 +// From https://github.com/ProtonMail/WebClient/blob/public/src/app/composer/services/encryptMessage.js type MessagePackage struct { Type MessagePackageType @@ -328,7 +328,7 @@ type MessagePackageSet struct { Type MessagePackageType // OR of each Type Addresses map[string]*MessagePackage MIMEType string - Body string // Body data packet + Body string // Encrypted body data packet // Only if cleartext is sent BodyKey string @@ -336,6 +336,7 @@ type MessagePackageSet struct { bodyKey *packet.EncryptedKey attachmentKeys map[string]*packet.EncryptedKey + signature int } func NewMessagePackageSet(attachmentKeys map[string]*packet.EncryptedKey) *MessagePackageSet { @@ -369,9 +370,7 @@ func (w *outgoingMessageWriter) Close() error { } // Encrypt encrypts the data that will be written to the returned -// io.WriteCloser. -// -// The signed parameter is ignored for now. +// io.WriteCloser, and optionally signs it. func (set *MessagePackageSet) Encrypt(mimeType string, signed *openpgp.Entity) (io.WriteCloser, error) { set.MIMEType = mimeType @@ -383,31 +382,43 @@ func (set *MessagePackageSet) Encrypt(mimeType string, signed *openpgp.Entity) ( } set.bodyKey = key - var encoded bytes.Buffer - ciphertext := base64.NewEncoder(base64.StdEncoding, &encoded) - - encryptedData, err := packet.SerializeSymmetricallyEncrypted(ciphertext, set.bodyKey.CipherFunc, set.bodyKey.Key, config) - if err != nil { - return nil, err + var signer *packet.PrivateKey + if signed != nil { + signKey, ok := signingKey(signed, config.Now()) + if !ok { + return nil, errors.New("no valid signing keys") + } + signer = signKey.PrivateKey + if signer == nil { + return nil, errors.New("no private key in signing key") + } + if signer.Encrypted { + return nil, errors.New("signing key must be decrypted") + } + set.signature = 1 } - // TODO: sign, see https://github.com/golang/crypto/blob/master/openpgp/write.go#L287 + encoded := new(bytes.Buffer) + ciphertext := base64.NewEncoder(base64.StdEncoding, encoded) - literalData, err := packet.SerializeLiteral(encryptedData, false, "", 0) + cleartext, err := symetricallyEncrypt(ciphertext, key, signer, nil, config) if err != nil { return nil, err } return &outgoingMessageWriter{ - cleartext: literalData, + cleartext: cleartext, ciphertext: ciphertext, - encoded: &encoded, + encoded: encoded, set: set, }, nil } func (set *MessagePackageSet) AddCleartext(addr string) error { - set.Addresses[addr] = &MessagePackage{Type: MessagePackageCleartext} + set.Addresses[addr] = &MessagePackage{ + Type: MessagePackageCleartext, + Signature: set.signature, + } set.Type |= MessagePackageCleartext if set.BodyKey == "" || set.AttachmentKeys == nil { @@ -463,6 +474,7 @@ func (set *MessagePackageSet) AddInternal(addr string, pub *openpgp.Entity) erro Type: MessagePackageInternal, BodyKeyPacket: bodyKey, AttachmentKeyPackets: attachmentKeys, + Signature: set.signature, } return nil }