diff --git a/crypto/attachment.go b/crypto/attachment.go index f5c8019..16cfcb9 100644 --- a/crypto/attachment.go +++ b/crypto/attachment.go @@ -5,12 +5,12 @@ import ( "io" "io/ioutil" + armorUtils "gitlab.com/ProtonMail/go-pm-crypto/armor" + "gitlab.com/ProtonMail/go-pm-crypto/internal" + "gitlab.com/ProtonMail/go-pm-crypto/models" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/armor" "golang.org/x/crypto/openpgp/packet" - armorUtils "proton/pmcrypto/armor" - "proton/pmcrypto/internal" - "proton/pmcrypto/models" ) //EncryptAttachmentBinKey ... diff --git a/crypto/key.go b/crypto/key.go index a418ec2..0588d21 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -8,10 +8,10 @@ import ( "strings" "time" + "gitlab.com/ProtonMail/go-pm-crypto/armor" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" "math/big" - "proton/pmcrypto/armor" ) const ( diff --git a/crypto/message.go b/crypto/message.go index e2bd6fb..5c81453 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -8,14 +8,14 @@ import ( "strings" "time" + armorUtils "gitlab.com/ProtonMail/go-pm-crypto/armor" + "gitlab.com/ProtonMail/go-pm-crypto/internal" + "gitlab.com/ProtonMail/go-pm-crypto/models" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/armor" errors2 "golang.org/x/crypto/openpgp/errors" "golang.org/x/crypto/openpgp/packet" "math" - armorUtils "proton/pmcrypto/armor" - "proton/pmcrypto/internal" - "proton/pmcrypto/models" ) // DecryptMessage decrypt encrypted message use private key (string ) diff --git a/crypto/mime.go b/crypto/mime.go index 3ad5ec7..ad4bbf7 100644 --- a/crypto/mime.go +++ b/crypto/mime.go @@ -1,22 +1,21 @@ package crypto import ( - "proton/pmmime" - "net/mail" - "strings" - "golang.org/x/crypto/openpgp/packet" - "net/textproto" - "io/ioutil" "bytes" + "github.com/ProtonMail/go-pm-mime" + "gitlab.com/ProtonMail/go-pm-crypto/armor" "golang.org/x/crypto/openpgp" - "proton/pmcrypto/armor" + "golang.org/x/crypto/openpgp/packet" + "io/ioutil" + "net/mail" + "net/textproto" + "strings" ) // ======================== Attachments Collector ============== // Collect contents of all attachment parts and return // them as a string - func (pm PmCrypto) parseMIME(mimeBody string, verifierKey []byte) (*pmmime.BodyCollector, int, []string, []string, error) { pubKey := bytes.NewReader(verifierKey) pubKeyEntries, err := openpgp.ReadKeyRing(pubKey) @@ -34,7 +33,10 @@ func (pm PmCrypto) parseMIME(mimeBody string, verifierKey []byte) (*pmmime.BodyC bodyCollector := pmmime.NewBodyCollector(printAccepter) attachmentsCollector := pmmime.NewAttachmentsCollector(bodyCollector) mimeVisitor := pmmime.NewMimeVisitor(attachmentsCollector) - str, err := armor.ArmorKey(verifierKey) + // TODO: build was failing on this unused 'str' variable. This code looks like WIP + //str, err := armor.ArmorKey(verifierKey) + _, err = armor.ArmorKey(verifierKey) + signatureCollector := newSignatureCollector(mimeVisitor, pubKeyEntries, config) err = pmmime.VisitAll(bytes.NewReader(mmBodyData), h, signatureCollector) @@ -80,4 +82,4 @@ func (pm *PmCrypto) DecryptMIMEMessage(encryptedText string, verifierKey []byte, } else { callbacks.OnVerified(decsignverify.Verify) } -} \ No newline at end of file +} diff --git a/crypto/session.go b/crypto/session.go index f1188b5..aece9e7 100644 --- a/crypto/session.go +++ b/crypto/session.go @@ -7,10 +7,10 @@ import ( "io" "strings" + "gitlab.com/ProtonMail/go-pm-crypto/armor" + "gitlab.com/ProtonMail/go-pm-crypto/models" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" - "proton/pmcrypto/armor" - "proton/pmcrypto/models" ) //RandomToken ... diff --git a/crypto/sign_detached.go b/crypto/sign_detached.go index 14772d6..0edd489 100644 --- a/crypto/sign_detached.go +++ b/crypto/sign_detached.go @@ -6,11 +6,11 @@ import ( "strings" "time" + "gitlab.com/ProtonMail/go-pm-crypto/internal" "golang.org/x/crypto/openpgp" - "golang.org/x/crypto/openpgp/packet" errors2 "golang.org/x/crypto/openpgp/errors" + "golang.org/x/crypto/openpgp/packet" "io" - "proton/pmcrypto/internal" ) // SignTextDetached sign detached text type @@ -45,7 +45,7 @@ func (pm *PmCrypto) SignTextDetached(plainText string, privateKey string, passph return "", errors.New("cannot sign message, signer key is not unlocked") } - config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: pm.getTimeGenerator() } + config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: pm.getTimeGenerator()} att := strings.NewReader(plainText) @@ -90,7 +90,7 @@ func (pm *PmCrypto) SignTextDetachedBinKey(plainText string, privateKey []byte, return "", errors.New("cannot sign message, singer key is not unlocked") } - config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: pm.getTimeGenerator() } + config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: pm.getTimeGenerator()} att := strings.NewReader(plainText) @@ -131,7 +131,7 @@ func (pm *PmCrypto) SignBinDetached(plainData []byte, privateKey string, passphr return "", errors.New("cannot sign message, singer key is not unlocked") } - config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: pm.getTimeGenerator() } + config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: pm.getTimeGenerator()} att := bytes.NewReader(plainData) @@ -172,7 +172,7 @@ func (pm *PmCrypto) SignBinDetachedBinKey(plainData []byte, privateKey []byte, p return "", errors.New("cannot sign message, singer key is not unlocked") } - config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: pm.getTimeGenerator() } + config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: pm.getTimeGenerator()} att := bytes.NewReader(plainData) @@ -226,7 +226,7 @@ func verifySignature(pubKeyEntries openpgp.EntityList, origText *bytes.Reader, s } } else { config.Time = func() time.Time { - return time.Unix(verifyTime + internal.CreationTimeOffset, 0) + return time.Unix(verifyTime+internal.CreationTimeOffset, 0) } } signatureReader := strings.NewReader(signature) @@ -261,7 +261,6 @@ func verifySignature(pubKeyEntries openpgp.EntityList, origText *bytes.Reader, s return true, nil } - // VerifyBinSignDetached ... func (pm *PmCrypto) VerifyBinSignDetached(signature string, plainData []byte, publicKey string, verifyTime int64) (bool, error) { diff --git a/crypto/signature_collector.go b/crypto/signature_collector.go index bd1d544..bd4f0ff 100644 --- a/crypto/signature_collector.go +++ b/crypto/signature_collector.go @@ -1,16 +1,14 @@ package crypto import ( - "bufio" "bytes" + "github.com/ProtonMail/go-pm-mime" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" "io" "io/ioutil" "mime" - "mime/multipart" "net/textproto" - "proton/pmmime" ) type SignatureCollector struct { diff --git a/crypto/time.go b/crypto/time.go index a2e980d..57af22b 100644 --- a/crypto/time.go +++ b/crypto/time.go @@ -11,15 +11,20 @@ func (pm *PmCrypto) UpdateTime(newTime int64) { } //GetTime get latest cached time -func (pm *PmCrypto) GetTime() int64 { +func (pm *PmCrypto) GetTimeUnix() int64 { return pm.getNow().Unix() } +//GetTime get latest cached time +func (pm *PmCrypto) GetTime() time.Time { + return pm.getNow() +} + func (pm *PmCrypto) getNow() time.Time { if pm.latestServerTime > 0 && !pm.latestClientTime.IsZero() { // Sub is monotome, it uses a monotime time clock in this case instead of the wall clock extrapolate := int64(pm.latestClientTime.Sub(time.Now()).Seconds()) - return time.Unix(pm.latestServerTime + extrapolate, 0) + return time.Unix(pm.latestServerTime+extrapolate, 0) } return time.Now() diff --git a/pmapi/key.go b/pmapi/key.go new file mode 100644 index 0000000..783f3e6 --- /dev/null +++ b/pmapi/key.go @@ -0,0 +1,235 @@ +package pmapi + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + "io/ioutil" + // "net/http" + // "net/url" + "strings" + + //"gitlab.com/ProtonMail/go-pm-crypto/armor" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" +) + +// A decrypted session key. +type SymmetricKey struct { + // The clear base64-encoded key. + Key string + // The algorithm used by this key. + Algo string +} + +//18 with the 2 highest order bits set to 1 +const SymmetricallyEncryptedTag = 210 + +var symKeyAlgos = map[string]packet.CipherFunction{ + "3des": packet.Cipher3DES, + "tripledes": packet.Cipher3DES, + "cast5": packet.CipherCAST5, + "aes128": packet.CipherAES128, + "aes192": packet.CipherAES192, + "aes256": packet.CipherAES256, +} + +// Get this's cipher function. +func (sk *SymmetricKey) cipherFunc() packet.CipherFunction { + cf, ok := symKeyAlgos[sk.Algo] + if ok { + return cf + } + + panic("pmapi: unsupported cipher function: " + sk.Algo) +} + +func newSymmetricKey(ek *packet.EncryptedKey) *SymmetricKey { + var algo string + for k, v := range symKeyAlgos { + if v == ek.CipherFunc { + algo = k + break + } + } + if algo == "" { + panic(fmt.Sprintf("pmapi: unsupported cipher function: %v", ek.CipherFunc)) + } + + return &SymmetricKey{ + Key: base64.StdEncoding.EncodeToString(ek.Key), + Algo: algo, + } +} + +func DecryptAttKey(kr *KeyRing, keyPacket string) (key *SymmetricKey, err error) { + r := base64.NewDecoder(base64.StdEncoding, strings.NewReader(keyPacket)) + packets := packet.NewReader(r) + + var p packet.Packet + if p, err = packets.Next(); err != nil { + return + } + + ek := p.(*packet.EncryptedKey) + + var decryptErr error + for _, key := range kr.entities.DecryptionKeys() { + priv := key.PrivateKey + if priv.Encrypted { + continue + } + + if decryptErr = ek.Decrypt(priv, nil); decryptErr == nil { + break + } + } + + if decryptErr != nil { + err = fmt.Errorf("pmapi: cannot decrypt encrypted key packet: %v", decryptErr) + return + } + + key = newSymmetricKey(ek) + return +} + +func SeparateKeyAndData(kr *KeyRing, r io.Reader) (key *SymmetricKey, symEncryptedData []byte, err error) { + packets := packet.NewReader(r) + + // Save encrypted key and signature apart + var ek *packet.EncryptedKey + var decryptErr error + for { + var p packet.Packet + if p, err = packets.Next(); err == io.EOF { + err = nil + break + } + switch p := p.(type) { + case *packet.EncryptedKey: + // We got an encrypted key. Try to decrypt it with each available key + if ek != nil && ek.Key != nil { + break + } + ek = p + + for _, key := range kr.entities.DecryptionKeys() { + priv := key.PrivateKey + if priv.Encrypted { + continue + } + + if decryptErr = ek.Decrypt(priv, nil); decryptErr == nil { + break + } + } + case *packet.SymmetricallyEncrypted: + var packetContents []byte + if packetContents, err = ioutil.ReadAll(p.Contents); err != nil { + return + } + + encodedLength := encodedLength(len(packetContents) + 1) + + symEncryptedData = append(symEncryptedData, byte(210)) + symEncryptedData = append(symEncryptedData, encodedLength...) + symEncryptedData = append(symEncryptedData, byte(1)) + symEncryptedData = append(symEncryptedData, packetContents...) + break + } + } + if decryptErr != nil { + err = fmt.Errorf("pmapi: cannot decrypt encrypted key packet: %v", decryptErr) + return + } + if ek == nil { + err = errors.New("pmapi: packets don't include an encrypted key packet") + return + } + if ek.Key == nil { + err = errors.New("pmapi: could not find any key to decrypt key") + return + } + + key = newSymmetricKey(ek) + return +} + +//encode length based on 4.2.2. in the RFC +func encodedLength(length int) (b []byte) { + if length < 192 { + b = append(b, byte(length)) + } else if length < 8384 { + length = length - 192 + b = append(b, 192+byte(length>>8)) + b = append(b, byte(length)) + } else { + b = append(b, byte(255)) + b = append(b, byte(length>>24)) + b = append(b, byte(length>>16)) + b = append(b, byte(length>>8)) + b = append(b, byte(length)) + } + return +} + +// SetKey encrypts the provided key. +func SetKey(kr *KeyRing, symKey *SymmetricKey) (packets string, err error) { + b := &bytes.Buffer{} + w := base64.NewEncoder(base64.StdEncoding, b) + + cf := symKey.cipherFunc() + + k, err := base64.StdEncoding.DecodeString(symKey.Key) + if err != nil { + err = fmt.Errorf("pmapi: cannot set key: %v", err) + return + } + + if len(kr.entities) == 0 { + err = fmt.Errorf("pmapi: cannot set key: key ring is empty") + return + } + + var pub *packet.PublicKey + for _, e := range kr.entities { + for _, subKey := range e.Subkeys { + if !subKey.Sig.FlagsValid || subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications { + pub = subKey.PublicKey + break + } + } + if pub == nil && len(e.Identities) > 0 { + var i *openpgp.Identity + for _, i = range e.Identities { + break + } + if i.SelfSignature.FlagsValid || i.SelfSignature.FlagEncryptStorage || i.SelfSignature.FlagEncryptCommunications { + pub = e.PrimaryKey + } + } + if pub != nil { + break + } + } + if pub == nil { + err = fmt.Errorf("pmapi: cannot set key: no public key available") + return + } + + if err = packet.SerializeEncryptedKey(w, pub, cf, k, nil); err != nil { + err = fmt.Errorf("pmapi: cannot set key: %v", err) + return + } + + if err = w.Close(); err != nil { + err = fmt.Errorf("pmapi: cannot set key: %v", err) + return + } + + packets = b.String() + return +} diff --git a/pmapi/keyring.go b/pmapi/keyring.go new file mode 100644 index 0000000..4980eea --- /dev/null +++ b/pmapi/keyring.go @@ -0,0 +1,559 @@ +package pmapi + +import ( + "bytes" + "crypto/ecdsa" + "crypto/rsa" + "encoding/json" + "errors" + "io" + "io/ioutil" + "strings" + "time" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + pgperrors "golang.org/x/crypto/openpgp/errors" + "golang.org/x/crypto/openpgp/packet" + + "gitlab.com/ProtonMail/go-pm-crypto/crypto" +) + +// Armored type for PGP encrypted messages. +const pgpMessageType = "PGP MESSAGE" + +// A keypair contains a private key and a public key. +type pmKeyObject struct { + ID string + Version int + Flags int + Fingerprint string + PublicKey string `json:",omitempty"` + PrivateKey string + //Activation string // Undocumented + Primary int +} + +func (ko *pmKeyObject) PrivateKeyReader() io.Reader { + return strings.NewReader(ko.PrivateKey) +} + +// Identity contains the name and the email of a key holder. +type Identity struct { + Name string + Email string +} + +// Signature is be used to check a signature. Because the signature is checked +// when the reader is consumed, Signature must only be used after EOF has been +// seen. A signature is only valid if s.Err() returns nil, otherwise the +// sender's identity cannot be trusted. +type Signature struct { + md *openpgp.MessageDetails +} + +var errKeyringNotUnlocked = errors.New("pmapi: cannot sign message, key ring is not unlocked") + +// Err returns a non-nil error if the signature is invalid. +func (s *Signature) Err() error { + return s.md.SignatureError +} + +// KeyRing returns the key ring that was used to produce the signature, if +// available. +func (s *Signature) KeyRing() *KeyRing { + if s.md.SignedBy == nil { + return nil + } + + return &KeyRing{ + entities: openpgp.EntityList{s.md.SignedBy.Entity}, + } +} + +// IsBy returns true if the signature has been created by kr's owner. +func (s *Signature) IsBy(kr *KeyRing) bool { + // Use fingerprint if possible + if s.md.SignedBy != nil { + for _, e := range kr.entities { + if e.PrimaryKey.Fingerprint == s.md.SignedBy.PublicKey.Fingerprint { + return true + } + } + return false + } + + for _, e := range kr.entities { + if e.PrimaryKey.KeyId == s.md.SignedByKeyId { + return true + } + } + return false +} + +// A keyring contains multiple private and public keys. +type KeyRing struct { + // PGP entities in this keyring. + entities openpgp.EntityList + pmCrypto *crypto.PmCrypto +} + +func (kr *KeyRing) GetEntities() openpgp.EntityList { + return kr.entities +} + +// Encrypt encrypts data to this keyring's owner. If sign is not nil, it also +// signs data with it. sign must be unlock to be able to sign data, if it's not +// the case an error will be returned. +func (kr *KeyRing) Encrypt(w io.Writer, sign *KeyRing, filename string, canonicalizeText bool) (io.WriteCloser, error) { + // The API returns keys sorted by descending priority + // Only encrypt to the first one + var encryptEntities []*openpgp.Entity + for _, e := range kr.entities { + encryptEntities = append(encryptEntities, e) + break + } + + var signEntity *openpgp.Entity + if sign != nil { + // To sign a message, the private key must be decrypted + for _, e := range sign.entities { + // Entity.PrivateKey must be a signing key + if e.PrivateKey != nil && !e.PrivateKey.Encrypted { + signEntity = e + break + } + } + + if signEntity == nil { + return nil, errKeyringNotUnlocked + } + } + + config := &packet.Config{DefaultCipher: packet.CipherAES256, + Time: func() time.Time { + return kr.pmCrypto.GetTime() + }, + } + + hints := &openpgp.FileHints{ + IsBinary: !canonicalizeText, + FileName: filename, + } + if canonicalizeText { + return openpgp.EncryptText(w, encryptEntities, signEntity, hints, config) + } else { + return openpgp.Encrypt(w, encryptEntities, signEntity, hints, config) + } +} + +func (kr *KeyRing) EncryptSymmetric(textToEncrypt string, canonicalizeText bool) (key *SymmetricKey, symEncryptedData []byte, err error) { + + var encryptedWriter io.WriteCloser + buffer := &bytes.Buffer{} + + if encryptedWriter, err = kr.Encrypt(buffer, kr, "msg.txt", canonicalizeText); err != nil { + return + } + + if _, err = io.Copy(encryptedWriter, bytes.NewBufferString(textToEncrypt)); err != nil { + return + } + encryptedWriter.Close() + + if key, symEncryptedData, err = SeparateKeyAndData(kr, buffer); err != nil { + return + } + + return +} + +// An io.WriteCloser that both encrypts and armors data. +type armorEncryptWriter struct { + aw io.WriteCloser // Armored writer + ew io.WriteCloser // Encrypted writer +} + +func (w *armorEncryptWriter) Write(b []byte) (n int, err error) { + return w.ew.Write(b) +} + +func (w *armorEncryptWriter) Close() (err error) { + if err = w.ew.Close(); err != nil { + return + } + err = w.aw.Close() + return +} + +// EncryptArmored encrypts and armors data to the keyring's owner. +func (kr *KeyRing) EncryptArmored(w io.Writer, sign *KeyRing) (wc io.WriteCloser, err error) { + aw, err := armor.Encode(w, pgpMessageType, nil) + if err != nil { + return + } + + ew, err := kr.Encrypt(aw, sign, "", false) + if err != nil { + aw.Close() + return + } + + wc = &armorEncryptWriter{aw: aw, ew: ew} + return +} + +// EncryptString encrypts and armors a string to the keyring's owner. +func (kr *KeyRing) EncryptString(s string, sign *KeyRing) (encrypted string, err error) { + var b bytes.Buffer + w, err := kr.EncryptArmored(&b, sign) + if err != nil { + return + } + + if _, err = w.Write([]byte(s)); err != nil { + return + } + if err = w.Close(); err != nil { + return + } + + encrypted = b.String() + return +} + +func (kr *KeyRing) SignString(message string, canonicalizeText bool) (signed string, err error) { + + var sig bytes.Buffer + err = kr.DetachedSign(&sig, strings.NewReader(message), canonicalizeText, true) + + if err != nil { + return "", err + } else { + return sig.String(), nil + } +} + +func (kr *KeyRing) DetachedSign(w io.Writer, toSign io.Reader, canonicalizeText bool, armored bool) (err error) { + + var signEntity *openpgp.Entity + for _, e := range kr.entities { + if e.PrivateKey != nil && !e.PrivateKey.Encrypted { + signEntity = e + break + } + } + + if signEntity == nil { + return errKeyringNotUnlocked + } + + config := &packet.Config{DefaultCipher: packet.CipherAES256, + Time: func() time.Time { + return kr.pmCrypto.GetTime() + }, + } + + if canonicalizeText { + err = openpgp.ArmoredDetachSignText(w, signEntity, toSign, config) + } else { + if armored { + err = openpgp.ArmoredDetachSign(w, signEntity, toSign, config) + } else { + err = openpgp.DetachSign(w, signEntity, toSign, config) + } + } + if err != nil { + return + } + + return +} + +// May return errors.ErrSignatureExpired (defined in golang.org/x/crypto/openpgp/errors) +// In this case signature has been verified successfuly, but it is either expired or +// in the future. +func (kr *KeyRing) VerifyString(message, signature string, sign *KeyRing) (err error) { + + messageReader := strings.NewReader(message) + signatureReader := strings.NewReader(signature) + + err = nil + if sign != nil { + for _, e := range sign.entities { + if e.PrivateKey != nil && !e.PrivateKey.Encrypted { + _, err = openpgp.CheckArmoredDetachedSignature(kr.entities, messageReader, signatureReader, nil) + if err == nil || err == pgperrors.ErrSignatureExpired { + return + } + } + } + } + + if err == nil { + return errKeyringNotUnlocked + } + + return err +} + +// Unlock unlocks as many keys as possible with the following password. Note +// that keyrings can contain keys locked with different passwords, and thus +// err == nil does not mean that all keys have been successfully decrypted. +// If err != nil, the password is wrong for every key, and err is the last error +// encountered. +func (kr *KeyRing) Unlock(passphrase []byte) error { + // Build a list of keys to decrypt + var keys []*packet.PrivateKey + for _, e := range kr.entities { + // Entity.PrivateKey must be a signing key + if e.PrivateKey != nil { + keys = append(keys, e.PrivateKey) + } + + // Entity.Subkeys can be used for encryption + for _, subKey := range e.Subkeys { + if subKey.PrivateKey != nil && (!subKey.Sig.FlagsValid || subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications) { + keys = append(keys, subKey.PrivateKey) + } + } + } + + if len(keys) == 0 { + return errors.New("pmapi: cannot unlock key ring, no private key available") + } + + var err error + var n int + for _, key := range keys { + if !key.Encrypted { + continue // Key already decrypted + } + + if err = key.Decrypt(passphrase); err == nil { + n++ + } + } + + if n == 0 { + return err + } + return nil +} + +// Decrypt decrypts a message sent to the keypair's owner. If the message is not +// signed, signed will be nil. +// If error is errors.ErrSignatureExpired (from golang.org/x/crypto/openpgp/errors), +// contents are still provided if library clients wish to process this message further +func (kr *KeyRing) Decrypt(r io.Reader) (decrypted io.Reader, signed *Signature, err error) { + md, err := openpgp.ReadMessage(r, kr.entities, nil, nil) + if err != nil && err != pgperrors.ErrSignatureExpired { + return + } + + decrypted = md.UnverifiedBody + if md.IsSigned { + signed = &Signature{md} + } + return +} + +// DecryptArmored decrypts an armored message sent to the keypair's owner. +// If error is errors.ErrSignatureExpired (from golang.org/x/crypto/openpgp/errors), +// contents are still provided if library clients wish to process this message further +func (kr *KeyRing) DecryptArmored(r io.Reader) (decrypted io.Reader, signed *Signature, err error) { + block, err := armor.Decode(r) + if err != nil && err != pgperrors.ErrSignatureExpired { + return + } + + if block.Type != pgpMessageType { + err = errors.New("pmapi: not an armored PGP message") + return + } + + return kr.Decrypt(block.Body) +} + +// DecryptString decrypts an armored string sent to the keypair's owner. +// If error is errors.ErrSignatureExpired (from golang.org/x/crypto/openpgp/errors), +// contents are still provided if library clients wish to process this message further +func (kr *KeyRing) DecryptString(encrypted string) (s string, signed *Signature, err error) { + r, signed, err := kr.DecryptArmored(strings.NewReader(encrypted)) + if err != nil && err != pgperrors.ErrSignatureExpired { + return encrypted, nil, err + } + + b, err := ioutil.ReadAll(r) + if err != nil && err != pgperrors.ErrSignatureExpired { + return encrypted, nil, err + } + + s = string(b) + return +} + +// WriteArmoredPublicKey outputs armored public keys from the keyring to w. +func (kr *KeyRing) WriteArmoredPublicKey(w io.Writer) (err error) { + aw, err := armor.Encode(w, openpgp.PublicKeyType, nil) + if err != nil { + return + } + + for _, e := range kr.entities { + if err = e.Serialize(aw); err != nil { + aw.Close() + return + } + } + + err = aw.Close() + return +} + +// ArmoredPublicKeyString returns the armored public keys from this keyring. +func (kr *KeyRing) ArmoredPublicKeyString() (s string, err error) { + b := &bytes.Buffer{} + if err = kr.WriteArmoredPublicKey(b); err != nil { + return + } + + s = b.String() + return +} + +// readFrom reads unarmored and armored keys from r and adds them to the keyring. +func (kr *KeyRing) readFrom(r io.Reader, armored bool) error { + var err error + var entities openpgp.EntityList + if armored { + entities, err = openpgp.ReadArmoredKeyRing(r) + } else { + entities, err = openpgp.ReadKeyRing(r) + } + for _, entity := range entities { + if entity.PrivateKey != nil { + switch entity.PrivateKey.PrivateKey.(type) { + // TODO: type mismatch after crypto lib update, fix this: + case *rsa.PrivateKey: + //entity.PrimaryKey = packet.NewRSAPublicKey(time.Now(), entity.PrivateKey.PrivateKey.(*rsa.PrivateKey).Public().(*rsa.PublicKey)) + case *ecdsa.PrivateKey: + entity.PrimaryKey = packet.NewECDSAPublicKey(time.Now(), entity.PrivateKey.PrivateKey.(*ecdsa.PrivateKey).Public().(*ecdsa.PublicKey)) + } + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey != nil { + switch subkey.PrivateKey.PrivateKey.(type) { + case *rsa.PrivateKey: + //subkey.PublicKey = packet.NewRSAPublicKey(time.Now(), subkey.PrivateKey.PrivateKey.(*rsa.PrivateKey).Public().(*rsa.PublicKey)) + case *ecdsa.PrivateKey: + subkey.PublicKey = packet.NewECDSAPublicKey(time.Now(), subkey.PrivateKey.PrivateKey.(*ecdsa.PrivateKey).Public().(*ecdsa.PublicKey)) + } + } + } + } + if err != nil { + return err + } + + if len(entities) == 0 { + return errors.New("pmapi: key ring doesn't contain any key") + } + + kr.entities = append(kr.entities, entities...) + return nil +} + +// UnmarshalJSON implements encoding/json.Unmarshaler. +func (kr *KeyRing) UnmarshalJSON(b []byte) (err error) { + kr.entities = nil + + keyObjs := []pmKeyObject{} + if err = json.Unmarshal(b, &keyObjs); err != nil { + return + } + + if len(keyObjs) == 0 { + return + } + + for _, ko := range keyObjs { + kr.readFrom(ko.PrivateKeyReader(), true) + } + + return +} + +// Identities returns the list of identities associated with this key ring. +func (kr *KeyRing) Identities() []*Identity { + var identities []*Identity + for _, e := range kr.entities { + for _, id := range e.Identities { + identities = append(identities, &Identity{ + Name: id.UserId.Name, + Email: id.UserId.Email, + }) + } + } + return identities +} + +func (kr *KeyRing) KeyIds() []uint64 { + var res []uint64 + for _, e := range kr.entities { + res = append(res, e.PrimaryKey.KeyId) + } + return res +} + +// ReadArmoredKeyRing reads an armored keyring. +func ReadArmoredKeyRing(r io.Reader) (kr *KeyRing, err error) { + kr = &KeyRing{} + err = kr.readFrom(r, true) + return +} + +// ReadArmoredKeyRing reads an armored keyring. +func ReadKeyRing(r io.Reader) (kr *KeyRing, err error) { + kr = &KeyRing{} + err = kr.readFrom(r, false) + return +} + +func FilterExpiredKeys(contactKeys []*KeyRing) (filteredKeys []*KeyRing, err error) { + now := time.Now() + hasExpiredEntity := false + filteredKeys = make([]*KeyRing, 0, 0) + + for _, contactKeyRing := range contactKeys { + keyRingHasUnexpiredEntity := false + keyRingHasTotallyExpiredEntity := false + for _, entity := range contactKeyRing.GetEntities() { + hasExpired := false + hasUnexpired := false + for _, subkey := range entity.Subkeys { + if subkey.Sig.KeyExpired(now) { + hasExpired = true + } else { + hasUnexpired = true + } + } + if hasExpired && !hasUnexpired { + keyRingHasTotallyExpiredEntity = true + } else if hasUnexpired { + keyRingHasUnexpiredEntity = true + } + } + if keyRingHasUnexpiredEntity { + filteredKeys = append(filteredKeys, contactKeyRing) + } else if keyRingHasTotallyExpiredEntity { + hasExpiredEntity = true + } + } + + if len(filteredKeys) == 0 && hasExpiredEntity { + return filteredKeys, errors.New("all contacts keys are expired") + } + + return +} diff --git a/pmapi/keyring_test.go b/pmapi/keyring_test.go new file mode 100644 index 0000000..870489a --- /dev/null +++ b/pmapi/keyring_test.go @@ -0,0 +1,283 @@ +package pmapi + +import ( + "bytes" + "golang.org/x/crypto/openpgp/armor" + "io/ioutil" + "strings" + "testing" +) + +const testPrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: OpenPGP.js v0.7.1 +Comment: http://openpgpjs.org + +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 +qqGRQm3MxoTdgQUShAwbNwNNQR9cInfMnA== +=2wIY +-----END PGP PRIVATE KEY BLOCK----- +` + +const testPrivateKeyLegacy = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: OpenPGP.js v0.9.0 +Comment: http://openpgpjs.org + +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 +PPekG4ttm6z3/BqqVplanIRSXlsqyp6J8A== +=Pyb1 +-----END PGP PRIVATE KEY BLOCK----- +` + +const testPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: OpenPGP.js v0.7.1 +Comment: http://openpgpjs.org + +xsBNBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefE +WSHlCjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39 +vPiLJXUqZs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKi +MeVa+GLEHhgZ2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5 +c8CmpqJuASIJNrSXM/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrb +DEVRA2/BCJonw7aASiNCrSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEB +AAHNBlVzZXJJRMLAcgQQAQgAJgUCVEltzwYLCQgHAwIJED62JZ7fId8kBBUI +AgoDFgIBAhsDAh4BAAD0nQf9EtH9TC0JqSs8q194Zo244jjlJFM3EzxOSULq +0zbywlLORfyoo/O8jU/HIuGz+LT98JDtnltTqfjWgu6pS3ZL2/L4AGUKEoB7 +OI6oIdRwzMc61sqI+Qpbzxo7rzufH4CiXZc6cxORUgL550xSCcqnq0q1mds7 +h5roKDzxMW6WLiEsc1dN8IQKzC7Ec5wA7U4oNGsJ3TyI8jkIs0IhXrRCd26K +0TW8Xp6GCsfblWXosR13y89WVNgC+xrrJKTZEisc0tRlneIgjcwEUvwfIg2n +9cDUFA/5BsfzTW5IurxqDEziIVP0L44PXjtJrBQaGMPlEbtP5i2oi3OADVX2 +XbvsRc7ATQRUSW3PAQgAkPnu5fps5zhOB/e618v/iF3KiogxUeRhA68TbvA+ +xnFfTxCx2Vo14aOL0CnaJ8gO5yRSqfomL2O1kMq07N1MGbqucbmc+aSfoElc ++Gd5xBE/w3RcEhKcAaYTi35vG22zlZup4x3ElioyIarOssFEkQgNNyDf5AXZ +jdHLA6qVxeqAb/Ff74+y9HUmLPSsRU9NwFzvK3Jv8C/ubHVLzTYdFgYkc4W1 +Uug9Ou08K+/4NEMrwnPFBbZdJAuUjQz2zW2ZiEKiBggiorH2o5N3mYUnWEmU +vqL3EOS8TbWo8UBIW3DDm2JiZR8VrEgvBtc9mVDUj/x+5pR07Fy1D6DjRmAc +9wARAQABwsBfBBgBCAATBQJUSW3SCRA+tiWe3yHfJAIbDAAA/iwH/ik9RKZM +B9Ir0x5mGpKPuqhugwrc3d04m1sOdXJm2NtD4ddzSEvzHwaPNvEvUl5v7FVM +zf6+6mYGWHyNP4+e7RtwYLlRpud6smuGyDSsotUYyumiqP6680ZIeWVQ+a1T +ThNs878mAJy1FhvQFdTmA8XIC616hDFpamQKPlpoO1a0wZnQhrPwT77HDYEE +a+hqY4Jr/a7ui40S+7xYRHKL/7ZAS4/grWllhU3dbNrwSzrOKwrA/U0/9t73 +8Ap6JL71YymDeaL4sutcoaahda1pTrMWePtrCltz6uySwbZs7GXoEzjX3EAH ++6qhkUJtzMaE3YEFEoQMGzcDTUEfXCJ3zJw= +=yT9U +-----END PGP PUBLIC KEY BLOCK----- +` + +const testMailboxPassword = "apple" +const testMailboxPasswordLegacy = "123" + +const testToken = "d79ca194a22810a5363eeddfdef7dfbc327c6229" + +const testEncryptedToken = `-----BEGIN PGP MESSAGE----- +Version: OpenPGP.js v1.2.0 +Comment: http://openpgpjs.org + +wcBMA0fcZ7XLgmf2AQf/RxDfA7g85KzH4371D/jx6deJIXPOWAqgTlGQMsTt +yg4ny3phSC2An/bUXNEBm8UMXqqtS7O+S8n1GjkDrCOkxyC+HugOFQwtybzI +eRX0X0qqvR6ry940SNGjPfJJ4Z0FYSLJtT8YxqO38t38WAYV1j9mBBVPMPJF +r7cQXxEcQAd6NZWF1Cf5Ajuum/zFjbA10Ksbi1tC4fsdtHcS94h1GCfsdNQi +xxbAuoyNYX2wsc6WX8IcmDNn564ZoHfvf2tX4Csf+2czByyOPtfyCn1aee51 +I40/I+65w8NfYEfzu7pbUcdo041Xg3lOhDNcuX/zANNw6zEWbE+12G5KVvwC +NNJgARWnwnOKtov2d73wGqNawn21SzA+zEd2mAPv1LPPIupW+0xOUSp5muov +aLEjcIuZeu+vyhXGZxIgoY4Bw8XCO9uWKZuzmqp+AOIP+kSi5aWnOaDFIOq0 +B3KtZ33bMZeX +=mig5 +-----END PGP MESSAGE----- +` + +var ( + testPrivateKeyRing *KeyRing + testPublicKeyRing *KeyRing +) + +var testIdentity = &Identity{ + Name: "UserID", + Email: "", +} + +func init() { + var err error + if testPrivateKeyRing, err = ReadArmoredKeyRing(strings.NewReader(testPrivateKey)); err != nil { + panic(err) + } + + if testPublicKeyRing, err = ReadArmoredKeyRing(strings.NewReader(testPublicKey)); err != nil { + panic(err) + } + + if err := testPrivateKeyRing.Unlock([]byte(testMailboxPassword)); err != nil { + panic(err) + } +} + +func TestKeyRing_Decrypt(t *testing.T) { + s, _, err := testPrivateKeyRing.DecryptString(testEncryptedToken) + if err != nil { + t.Fatal("Cannot decrypt token:", err) + } + + if s != testToken { + t.Fatalf("Invalid decrypted token: want %v but got %v", testToken, s) + } +} + +func TestKeyRing_Encrypt(t *testing.T) { + encrypted, err := testPublicKeyRing.EncryptString(testToken, nil) + if err != nil { + t.Fatal("Cannot encrypt token:", err) + } + + // We can't just check if encrypted == testEncryptedToken + // Decrypt instead + s, _, err := testPrivateKeyRing.DecryptString(encrypted) + if err != nil { + t.Fatal("Cannot decrypt token:", err) + } + + if s != testToken { + t.Fatalf("Invalid decrypted token: want %v but got %v", testToken, s) + } +} + +func TestKeyRing_ArmoredPublicKeyString(t *testing.T) { + s, err := testPrivateKeyRing.ArmoredPublicKeyString() + if err != nil { + t.Fatal("Expected no error while getting armored public key, got:", err) + } + + // Decode armored keys + block, err := armor.Decode(strings.NewReader(s)) + if err != nil { + t.Fatal("Expected no error while decoding armored public key, got:", err) + } + expected, err := armor.Decode(strings.NewReader(testPublicKey)) + if err != nil { + t.Fatal("Expected no error while decoding expected armored public key, got:", err) + } + + if expected.Type != block.Type { + t.Fatalf("Invalid public key block type: expected %v, got %v", expected.Type, block.Type) + } + + b, err := ioutil.ReadAll(block.Body) + if err != nil { + t.Fatal("Expected no error while reading armored public key body, got:", err) + } + eb, err := ioutil.ReadAll(expected.Body) + if err != nil { + t.Fatal("Expected no error while reading expected armored public key body, got:", err) + } + + if bytes.Compare(eb, b) != 0 { + t.Fatal("Invalid public key body: expected %v, got %v", eb, b) + } +}