Update message send API from WebClient code

This commit is contained in:
emersion 2017-09-23 19:20:47 +02:00
parent 129b258ed7
commit 8066bcbad4
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
2 changed files with 262 additions and 17 deletions

91
protonmail/crypto.go Normal file
View File

@ -0,0 +1,91 @@
package protonmail
import (
"time"
"golang.org/x/crypto/openpgp"
)
// primaryIdentity returns the Identity marked as primary or the first identity
// if none are so marked.
func primaryIdentity(e *openpgp.Entity) *openpgp.Identity {
var firstIdentity *openpgp.Identity
for _, ident := range e.Identities {
if firstIdentity == nil {
firstIdentity = ident
}
if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
return ident
}
}
return firstIdentity
}
// encryptionKey returns the best candidate Key for encrypting a message to the
// given Entity.
func encryptionKey(e *openpgp.Entity, now time.Time) (openpgp.Key, bool) {
candidateSubkey := -1
// Iterate the keys to find the newest key
var maxTime time.Time
for i, subkey := range e.Subkeys {
if subkey.Sig.FlagsValid &&
subkey.Sig.FlagEncryptCommunications &&
subkey.PublicKey.PubKeyAlgo.CanEncrypt() &&
!subkey.Sig.KeyExpired(now) &&
(maxTime.IsZero() || subkey.Sig.CreationTime.After(maxTime)) {
candidateSubkey = i
maxTime = subkey.Sig.CreationTime
}
}
if candidateSubkey != -1 {
subkey := e.Subkeys[candidateSubkey]
return openpgp.Key{e, subkey.PublicKey, subkey.PrivateKey, subkey.Sig}, true
}
// If we don't have any candidate subkeys for encryption and
// the primary key doesn't have any usage metadata then we
// assume that the primary key is ok. Or, if the primary key is
// marked as ok to encrypt to, then we can obviously use it.
i := primaryIdentity(e)
if !i.SelfSignature.FlagsValid || i.SelfSignature.FlagEncryptCommunications &&
e.PrimaryKey.PubKeyAlgo.CanEncrypt() &&
!i.SelfSignature.KeyExpired(now) {
return openpgp.Key{e, e.PrimaryKey, e.PrivateKey, i.SelfSignature}, true
}
// This Entity appears to be signing only.
return openpgp.Key{}, false
}
// signingKey return the best candidate Key for signing a message with this
// Entity.
func signingKey(e *openpgp.Entity, now time.Time) (openpgp.Key, bool) {
candidateSubkey := -1
for i, subkey := range e.Subkeys {
if subkey.Sig.FlagsValid &&
subkey.Sig.FlagSign &&
subkey.PublicKey.PubKeyAlgo.CanSign() &&
!subkey.Sig.KeyExpired(now) {
candidateSubkey = i
break
}
}
if candidateSubkey != -1 {
subkey := e.Subkeys[candidateSubkey]
return openpgp.Key{e, subkey.PublicKey, subkey.PrivateKey, subkey.Sig}, true
}
// If we have no candidate subkey then we assume that it's ok to sign
// with the primary key.
i := primaryIdentity(e)
if !i.SelfSignature.FlagsValid || i.SelfSignature.FlagSign &&
!i.SelfSignature.KeyExpired(now) {
return openpgp.Key{e, e.PrimaryKey, e.PrivateKey, i.SelfSignature}, true
}
return openpgp.Key{}, false
}

View File

@ -2,12 +2,16 @@ package protonmail
import ( import (
"bytes" "bytes"
"encoding/base64"
"errors"
"io" "io"
"net/http" "net/http"
"strconv"
"strings" "strings"
"golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor" "golang.org/x/crypto/openpgp/armor"
"golang.org/x/crypto/openpgp/packet"
) )
type MessageType int type MessageType int
@ -199,37 +203,187 @@ type MessagePackageType int
const ( const (
MessagePackageInternal MessagePackageType = 1 MessagePackageInternal MessagePackageType = 1
MessagePackageEncryptedOutside = 2 MessagePackageEncryptedOutside = 2
MessagePackageClear = 4 MessagePackageCleartext = 4
MessagePackageInlinePGP = 8 MessagePackageInlinePGP = 8
MessagePackagePGPMIME = 16 MessagePackagePGPMIME = 16
MessagePackageMIME = 32 MessagePackageMIME = 32
) )
type MessagePackage struct { // From https://github.com/ProtonMail/Angular/blob/v3/src/app/composer/controllers/composeMessage.js#L656
Address string
Type MessagePackageType
Body string
KeyPackets []*MessageKeyPacket
Token string type MessagePackage struct {
EncToken string Type MessagePackageType
PasswordHint string `json:",omitempty"`
BodyKeyPacket string
AttachmentKeyPackets map[string]string
Signature int
// Only if encrypted for outside
PasswordHint string
Auth interface{} // TODO
Token string
EncToken string
} }
type MessageOutgoing struct { type MessagePackageSet struct {
Type MessagePackageType // OR of each Type
Addresses map[string]*MessagePackage
MIMEType string
Body string // Body data packet
// Only if cleartext is sent
BodyKey string
AttachmentKeys map[string]string
bodyKey *packet.EncryptedKey
attachmentKeys map[string]*packet.EncryptedKey
}
func NewMessagePackageSet(attachmentKeys map[string]*packet.EncryptedKey) *MessagePackageSet {
return &MessagePackageSet{
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
ciphertext io.WriteCloser
armored *bytes.Buffer
set *MessagePackageSet
}
func (w *outgoingMessageWriter) Write(p []byte) (int, error) {
return w.cleartext.Write(p)
}
func (w *outgoingMessageWriter) Close() error {
if err := w.cleartext.Close(); err != nil {
return err
}
if err := w.ciphertext.Close(); err != nil {
return err
}
w.set.Body = w.armored.String()
w.armored = nil
return nil
}
func (set *MessagePackageSet) Encrypt() (io.WriteCloser, error) {
config := &packet.Config{}
if err := set.generateBodyKey(packet.CipherAES256, config); err != nil {
return nil, err
}
var armored bytes.Buffer
ciphertext, err := armor.Encode(&armored, "PGP MESSAGE", nil)
if err != nil {
return nil, err
}
encryptedData, err := packet.SerializeSymmetricallyEncrypted(ciphertext, set.bodyKey.CipherFunc, set.bodyKey.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, false, "", 0)
if err != nil {
return nil, err
}
return &outgoingMessageWriter{
cleartext: literalData,
ciphertext: ciphertext,
armored: &armored,
set: set,
}, nil
}
func (set *MessagePackageSet) AddCleartext(addr string) error {
set.Addresses[addr] = &MessagePackage{Type: MessagePackageCleartext}
if set.BodyKey == "" || set.AttachmentKeys == nil {
set.BodyKey = base64.StdEncoding.EncodeToString(set.bodyKey.Key)
set.AttachmentKeys = make(map[string]string, len(set.attachmentKeys))
for att, key := range set.attachmentKeys {
set.AttachmentKeys[att] = base64.StdEncoding.EncodeToString(key.Key)
}
}
return nil
}
func serializeEncryptedKey(symKey *packet.EncryptedKey, pub *packet.PublicKey, config *packet.Config) (string, error) {
var armored bytes.Buffer
ciphertext, err := armor.Encode(&armored, "PGP MESSAGE", nil)
if err != nil {
return "", err
}
err = packet.SerializeEncryptedKey(ciphertext, pub, symKey.CipherFunc, symKey.Key, config)
if err != nil {
return "", err
}
return armored.String(), nil
}
func (set *MessagePackageSet) AddInternal(addr string, pub *openpgp.Entity) error {
config := &packet.Config{}
encKey, ok := encryptionKey(pub, config.Now())
if !ok {
return errors.New("cannot encrypt a message to key id " + strconv.FormatUint(pub.PrimaryKey.KeyId, 16) + " because it has no encryption keys")
}
bodyKey, err := serializeEncryptedKey(set.bodyKey, encKey.PublicKey, config)
if err != nil {
return err
}
attachmentKeys := make(map[string]string, len(set.attachmentKeys))
for att, key := range set.attachmentKeys {
attKey, err := serializeEncryptedKey(key, encKey.PublicKey, config)
if err != nil {
return err
}
attachmentKeys[att] = attKey
}
set.Addresses[addr] = &MessagePackage{
Type: MessagePackageInternal,
BodyKeyPacket: bodyKey,
AttachmentKeyPackets: attachmentKeys,
}
return nil
}
type OutgoingMessage struct {
ID string ID string
// Only if there's a recipient without a public key
ClearBody string
AttachmentKeys []*AttachmentKey
// Only if message expires // Only if message expires
ExpirationTime int // duration in seconds ExpirationTime int // Duration in seconds
Packages []*MessagePackage Packages []*MessagePackageSet
} }
func (c *Client) SendMessage(msg *MessageOutgoing) (sent, parent *Message, err error) { func (c *Client) SendMessage(msg *OutgoingMessage) (sent, parent *Message, err error) {
req, err := c.newJSONRequest(http.MethodPut, "/messages/"+msg.ID, msg) req, err := c.newJSONRequest(http.MethodPut, "/messages/"+msg.ID, msg)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err