From 0ac8dda45807dfe8644ea8b17b31ea8da8e8fe91 Mon Sep 17 00:00:00 2001 From: emersion Date: Thu, 11 Jan 2018 00:01:49 +0100 Subject: [PATCH] protonmail: sign message bodies, fixes #22 --- protonmail/crypto.go | 95 ++++++++++++++++++++++++++++++++++++++++++ protonmail/messages.go | 40 ++++++++++++------ 2 files changed, 122 insertions(+), 13 deletions(-) diff --git a/protonmail/crypto.go b/protonmail/crypto.go index bc8fe0c..ed17baf 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,96 @@ func generateUnencryptedKey(cipher packet.CipherFunction, config *packet.Config) Key: symKey, }, nil } + +func symetricallyEncrypt(ciphertext io.Writer, symKey *packet.EncryptedKey, signer *packet.PrivateKey, 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 + } + } + + 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} + } + + literalData, err := packet.SerializeLiteral(w, false, "", 0) + 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 0e548e9..ef708f2 100644 --- a/protonmail/messages.go +++ b/protonmail/messages.go @@ -209,7 +209,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 @@ -229,7 +229,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 @@ -237,6 +237,7 @@ type MessagePackageSet struct { bodyKey *packet.EncryptedKey attachmentKeys map[string]*packet.EncryptedKey + signature int } func NewMessagePackageSet(attachmentKeys map[string]*packet.EncryptedKey) *MessagePackageSet { @@ -284,31 +285,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, 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 { @@ -364,6 +377,7 @@ func (set *MessagePackageSet) AddInternal(addr string, pub *openpgp.Entity) erro Type: MessagePackageInternal, BodyKeyPacket: bodyKey, AttachmentKeyPackets: attachmentKeys, + Signature: set.signature, } return nil }