From e65ed17b416091e26cb61c2d2ae33ce6185e7737 Mon Sep 17 00:00:00 2001 From: wussler Date: Mon, 3 Jun 2019 17:00:01 +0200 Subject: [PATCH] Refactor api (#6) * Refactor library, remove duplicates * Rebuild structure to use Messages and Signature models * Use PGPSplitMessage * Remove signature model * Various fixes * Add helpers with tests * Fixes, add some docs, add tests * Add attachment helpers * Add helpers Symmetric encryption * Edit docs + examples * Rename kr to keyRing * Various fixes for documentation * Edit JSON handling functions, add decrypt keyring via token * Add proposal changes doc * Fix CI * Drop *Message functions, join CleartextMessage and BinaryMessage * Change canonicalization and trimming only to text signatures * Add cleartextsignature, detach signature from message model, move helpers * Documentation, remove optional parameters * Move verification to separate model * Don't return message in VerifyDetached * Update table of contents in readme * Appease golint * Run go fmt * Rename Encrypt/DecryptMessageWithPassword to ..WithToken These functions shouldn't be used with user-provided passwords, as they don't do any key-stretching. * Change key generation usernames --- ProposalChanges.md | 460 +++++++++++++++++++++++++ README.md | 185 +++++++--- armor/armor.go | 39 ++- constants/armor.go | 1 + constants/cipher.go | 7 + crypto/attachment.go | 78 ++--- crypto/attachment_test.go | 30 +- crypto/key.go | 310 ++--------------- crypto/key_test.go | 85 ++--- crypto/keyring.go | 463 ++++++++----------------- crypto/keyring_message.go | 255 ++++++++++++++ crypto/keyring_test.go | 109 ++++-- crypto/message.go | 505 +++++++++++++++------------- crypto/message_test.go | 97 ++++-- crypto/mime.go | 80 ++--- crypto/mime_test.go | 22 +- crypto/session.go | 161 +-------- crypto/session_test.go | 36 +- crypto/sign_detached.go | 128 ------- crypto/signature_collector.go | 9 +- crypto/signature_test.go | 89 +++-- crypto/symmetrickey.go | 224 ++++++++++++ crypto/testdata/keyring_jsonKeys | 20 ++ crypto/testdata/keyring_newJSONKeys | 12 + crypto/testdata/keyring_userKey | 62 ++++ crypto/testdata/message_expired | 18 + crypto/time.go | 8 +- helper/base_test.go | 22 ++ helper/cleartext.go | 82 +++++ helper/cleartext_test.go | 45 +++ helper/helper.go | 258 ++++++++++++++ helper/helper_test.go | 131 ++++++++ models/models.go | 18 - subtle/subtle_test.go | 2 +- 34 files changed, 2573 insertions(+), 1478 deletions(-) create mode 100644 ProposalChanges.md create mode 100644 crypto/keyring_message.go delete mode 100644 crypto/sign_detached.go create mode 100644 crypto/symmetrickey.go create mode 100644 crypto/testdata/keyring_jsonKeys create mode 100644 crypto/testdata/keyring_newJSONKeys create mode 100644 crypto/testdata/keyring_userKey create mode 100644 crypto/testdata/message_expired create mode 100644 helper/base_test.go create mode 100644 helper/cleartext.go create mode 100644 helper/cleartext_test.go create mode 100644 helper/helper.go create mode 100644 helper/helper_test.go diff --git a/ProposalChanges.md b/ProposalChanges.md new file mode 100644 index 0000000..8dd86a8 --- /dev/null +++ b/ProposalChanges.md @@ -0,0 +1,460 @@ +# Model changes +## Modified +### EncryptedSplit +``` +models.EncryptedSplit struct { + DataPacket []byte + KeyPacket []byte + Algo string +} +``` +is now +``` +crypto.PGPSplitMessage struct { + DataPacket []byte + KeyPacket []byte +} +``` + +### DecryptSignedVerify +``` +models.DecryptSignedVerify struct { + //clear text + Plaintext string + //bitmask verify status : 0 + Verify int + //error message if verify failed + Message string +} +``` +is now +``` +// PlainMessage stores an unencrypted text message. +crypto.PlainMessage struct { + // The content of the message + Text string + // If the decoded message was correctly signed. See constants.SIGNATURE* for all values. + Verified int +} +``` + +### pmKeyObject +``` +type pmKeyObject struct { + ID string + Version int + Flags int + Fingerprint string + PublicKey string `json:",omitempty"` + PrivateKey string + Primary int +} +``` +is now +``` +type pgpKeyObject struct { + ID string + Version int + Flags int + PrivateKey string + Primary int + Token string `json:",omitempty"` + Signature string `json:",omitempty"` +} +``` + +## Dropped +### Signature +``` +type Signature struct { + md *openpgp.MessageDetails +} +``` +### SignedString +``` +// SignedString wraps string with Signature +type SignedString struct { + String string + Signed *Signature +} +``` +## New +### PGPMessage +``` +// PGPMessage stores a PGP-encrypted message. +type PGPMessage struct { + // The content of the message + Data []byte +} +``` +### PGPSignature +``` +// PGPSignature stores a PGP-encoded detached signature. +type PGPSignature struct { + // The content of the message + Data []byte +} +``` + + +# API changes +## armor.go +### ReadClearSignedMessage +Added signature info to returned info. +``` +ReadClearSignedMessage(signedMessage string) (string, error): +``` + +## attachment.go +### AttachmentProcessor +No change. + +### EncryptAttachment +Change encryption parameters to messages: either contextual signature with helper or using messages. +``` +(pm *PmCrypto) EncryptAttachment(plainData []byte, fileName string, publicKey *KeyRing) (*models.EncryptedSplit, error): +* (helper) EncryptSignAttachment(publicKey, privateKey, passphrase, fileName string, plainData []byte) (keyPacket, dataPacket, signature []byte, err error) +* (keyRing *KeyRing) EncryptAttachment(message *PlainMessage, fileName string) (*PGPSplitMessage, error) +``` + +### EncryptAttachmentLowMemory +Renamed. +``` +(pm *PmCrypto) EncryptAttachmentLowMemory(estimatedSize int, fileName string, publicKey *KeyRing) (*AttachmentProcessor, error): +* (keyRing *KeyRing) NewLowMemoryAttachmentProcessor(estimatedSize int, fileName string) (*AttachmentProcessor, error) +``` + +### SplitArmor +Renamed, changed model. +``` +SplitArmor(encrypted string) (*models.EncryptedSplit, error): +* NewPGPSplitMessageFromArmored(encrypted string) (*PGPSplitMessage, error) +``` + +### DecryptAttachment +Same as `EncryptAttachment`. +``` +(pm *PmCrypto) DecryptAttachment(keyPacket []byte, dataPacket []byte, kr *KeyRing, passphrase string) ([]byte, error): +* (helper) DecryptVerifyAttachment(publicKey, privateKey, passphrase string, keyPacket, dataPacket []byte, armoredSignature string) (plainData []byte, err error) +* (keyRing *KeyRing) DecryptAttachment(message *PGPSplitMessage) (*PlainMessage, error) +``` + +## key.go +`SymmetricKey` model and functions have been moved to symmetrickey.go + +### DecryptAttKey +Renamed, change to `[]byte` as it's a binary keypacket. +``` +DecryptAttKey(kr *KeyRing, keyPacket string) (key *SymmetricKey, err error): +* (keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SymmetricKey, error) +``` + +### SeparateKeyAndData +This function has been split in two, as it **did not** only separate the data, but when provided a KeyRing decrypt the session key too. +``` +SeparateKeyAndData(kr *KeyRing, r io.Reader, estimatedLength int, garbageCollector int) (outSplit *models.EncryptedSplit, err error): +* (for separating key and data) (msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) (outSplit *PGPSplitMessage, err error) +* (for decrypting SessionKey) (keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SymmetricKey, error) +``` + +### encodedLength +Dropped as already present in `SeparateKeyAndData` and unused. + +### SetKey +Renamed, change to `[]byte` as it's a binary keypacket. +``` +SetKey(kr *KeyRing, symKey *SymmetricKey) (packets string, err error): +* (keyRing *KeyRing) EncryptSessionKey(sessionSplit *SymmetricKey) ([]byte, error) +``` + +### IsKeyExpiredBin +Renamed. +``` +(pm *PmCrypto) IsKeyExpiredBin(publicKey []byte) (bool, error): +* (pgp *GopenPGP) IsKeyExpired(publicKey []byte) (bool, error) +``` + +### IsKeyExpired +Renamed. +``` +(pm *PmCrypto) IsKeyExpired(publicKey string) (bool, error): +* (pgp *GopenPGP) IsArmoredKeyExpired(publicKey string) (bool, error) +``` + +### GenerateRSAKeyWithPrimes +`userName` and `domain` joined in `email`. +Added `name` parameter. +To emulate the old behaviour `name = email = userName + "@" + domain`. +``` +(pm *PmCrypto) GenerateRSAKeyWithPrimes(userName, domain, passphrase, keyType string, bits int, prime1, prime2, prime3, prime4 []byte) (string, error): +* (pgp *GopenPGP) GenerateRSAKeyWithPrimes(name, email, passphrase, keyType string, bits int, prime1, prime2, prime3, prime4 []byte) (string, error): +``` + +### GenerateKey +`userName` and `domain` joined in `email`. +Added `name` parameter. +To emulate the old behaviour `name = email = userName + "@" + domain`. +``` +(pm *PmCrypto) GenerateKey(userName, domain, passphrase, keyType string, bits int) (string, error) : +* (pgp *GopenPGP) GenerateKey(name, email, passphrase, keyType string, bits int) (string, error): +``` + +### UpdatePrivateKeyPassphrase +No change. + +### CheckKey +Renamed. +``` +(pm *PmCrypto) CheckKey(pubKey string) (string, error): +* (pgp *GopenPGP) PrintFingerprints(pubKey string) (string, error) +``` + +## keyring.go +### Signature.KeyRing +Dropped with signature. + +### Signature.IsBy +Dropped with signature. + +### GetEntities +No change. + +### GetSigningEntity +KeyRings must be already unlocked when provided to encrypt/decrypt/sign/verify functions. +``` +(kr *KeyRing) GetSigningEntity(passphrase string) *openpgp.Entity: +* (keyRing *KeyRing) GetSigningEntity() (*openpgp.Entity, error) +``` + +### Encrypt, EncryptArmored, EncryptString +This function has been divided in different sub-functions and wrappers have been provided for the key unlock and message models. +``` +(kr *KeyRing) Encrypt(w io.Writer, sign *KeyRing, filename string, canonicalizeText bool) (io.WriteCloser, error): +* (if binary data) (keyRing *KeyRing) Encrypt(message *PlainMessage, privateKey *KeyRing) (*PGPMessage, error) +* (if plain text, wrapped) (helper) EncryptMessageArmored(publicKey, plaintext string) (ciphertext string, err error) +* (if plain text, wrapped, signed) (helper) EncryptSignMessageArmored(publicKey, privateKey, passphrase, plaintext string) (ciphertext string, err error) +``` +### EncryptCore +Made an internal function. + +### EncryptSymmetric +Dropped, now the procedure is split in two parts. +``` +(kr *KeyRing) EncryptSymmetric(textToEncrypt string, canonicalizeText bool) (outSplit *models.EncryptedSplit, err error): +* (for encrypting) (keyRing *KeyRing) Encrypt* +* (for splitting) (msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) (outSplit *PGPSplitMessage, err error) +* (alternative) (keyRing *KeyRing) EncryptAttachment(message *PlainMessage, fileName string) (*PGPSplitMessage, error) +``` + +### DecryptString, Decrypt, DecryptArmored +Same as Encrypt* +``` +(kr *KeyRing) DecryptString(encrypted string) (SignedString, error): +* (if binary data) func (keyRing *KeyRing) Decrypt(message *PGPMessage, verifyKey *KeyRing, verifyTime int64) (*PlainMessage, *Verification, error) +* (if plain text, wrapped) (helper) DecryptMessageArmored(privateKey, passphrase, ciphertext string) (plaintext string, err error) +* (if plain text, wrapped, verified) (helper) DecryptVerifyMessageArmored(publicKey, privateKey, passphrase, ciphertext string) (plaintext string, err error) +``` + +### DecryptStringIfNeeded +Replaced with `IsPGPMessage` + `Decrypt*`. +``` +(kr *KeyRing) DecryptStringIfNeeded(data string) (decrypted string, err error): +* (pgp *GopenPGP) IsPGPMessage(data string) bool +``` + +### SignString, DetachedSign +Replaced by signing methods. +``` +(kr *KeyRing) SignString(message string, canonicalizeText bool) (signed string, err error): +(kr *KeyRing) DetachedSign(w io.Writer, toSign io.Reader, canonicalizeText bool, armored bool): +* (keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, error) +``` + +### VerifyString +Same as signing. +``` +(kr *KeyRing) VerifyString(message, signature string, sign *KeyRing) (err error): +* (to verify) (keyRing *KeyRing) VerifyDetached(message *PlainMessage, signature *PGPSignature, verifyTime int64) (*Verification, error) +``` + +### Unlock +No change. Added: +``` +(keyRing *KeyRing) UnlockWithPassphrase(passphrase string) error +``` + +### WriteArmoredPublicKey +No change. + +### ArmoredPublicKeyString +Renamed. +``` +(kr *KeyRing) ArmoredPublicKeyString() (s string, err error): +* (keyRing *KeyRing) GetArmoredPublicKey() (s string, err error) +``` + +### BuildKeyRing +No change. + +### BuildKeyRingNoError +No change. + +### BuildKeyRingArmored +No change. + +### UnmarshalJSON +Renamed. +``` +(kr *KeyRing) UnmarshalJSON(b []byte) (err error): +* (keyRing *KeyRing) ReadFromJSON(jsonData []byte) (err error) +``` + +### Identities +No change + +### KeyIds +No change. + +### ReadArmoredKeyRing +No change. + +### ReadKeyRing +No change. + +### FilterExpiredKeys +No change. + +## message.go +Many functions are duplicates of keyring.go + +### EncryptMessage +See Encrypt* +``` +(pm *PmCrypto) EncryptMessage(plaintext string, publicKey *KeyRing, privateKey *KeyRing, passphrase string, trim bool) (string, error): +* (if binary data) (keyRing *KeyRing) Encrypt(message *PlainMessage, privateKey *KeyRing) (*PGPMessage, error) +* (if plain text, wrapped) (helper) EncryptMessageArmored(publicKey, plaintext string) (ciphertext string, err error) +* (if plain text, wrapped, signed) (helper) EncryptSignMessageArmored(publicKey, privateKey, passphrase, plaintext string) (ciphertext string, err error) +``` + +### DecryptMessage, DecryptMessageVerify, DecryptMessageStringKey +See Decrypt* +``` +(pm *PmCrypto) DecryptMessage(encryptedText string, privateKey *KeyRing, passphrase string) (string, error): +(pm *PmCrypto) DecryptMessageStringKey(encryptedText string, privateKey string, passphrase string) (string, error): +(pm *PmCrypto) DecryptMessageVerify(encryptedText string, verifierKey *KeyRing, privateKeyRing *KeyRing, passphrase string, verifyTime int64) (*models.DecryptSignedVerify, error) : +* (if binary data) (keyRing *KeyRing) Decrypt(message *PGPMessage, verifyKey *KeyRing, verifyTime int64) (*PlainMessage, *Verification, error) +* (if plain text, wrapped) (helper) DecryptMessageArmored(privateKey, passphrase, ciphertext string) (plaintext string, err error) +* (if plain text, wrapped, verified) (helper) DecryptVerifyMessageArmored(publicKey, privateKey, passphrase, ciphertext string) (plaintext string, err error) +``` + +### EncryptMessageWithPassword +The function has been renamed and moved to `SymmetricKey` to allow more encryption modes. Previously AES-128 (! not 256 as stated) was used. +``` +(pm *PmCrypto) EncryptMessageWithPassword(plaintext string, password string) (string, error): +* (if binary data) (symmetricKey *SymmetricKey) Encrypt(message *PlainMessage) (*PGPMessage, error) +* (if plain text, wrapped) (helper) EncryptMessageWithToken(token, plaintext string) (ciphertext string, err error) +* (if plain text, wrapped) (helper) EncryptMessageWithTokenAlgo(token, plaintext, algo string) (ciphertext string, err error) +``` + +### DecryptMessageWithPassword +See `EncryptMessageWithPassword`. +``` +(pm *PmCrypto) DecryptMessageWithPassword(encrypted string, password string) (string, error): +* (if binary data) (symmetricKey *SymmetricKey) Decrypt(message *PGPMessage) (*PlainMessage, error) +* (if plain text, wrapped, for all ciphers) (helper) DecryptMessageWithToken(token, ciphertext string) (plaintext string, err error) +``` + +## mime.go + +### DecryptMIMEMessage +Moved to `KeyRing`. +``` +(pm *PmCrypto) DecryptMIMEMessage(encryptedText string, verifierKey *KeyRing, privateKeyRing *KeyRing, passphrase string, callbacks MIMECallbacks, verifyTime int64): +* (keyRing *KeyRing) DecryptMIMEMessage(message *PGPMessage, verifyKey *KeyRing, callbacks MIMECallbacks, verifyTime int64) +``` + +## session.go +### RandomToken +No change. + +### RandomTokenWith +Renamed. +``` +(pm *PmCrypto) RandomTokenWith(size int) ([]byte, error): +* (pgp *GopenPGP) RandomTokenSize(size int) ([]byte, error) +``` + +### GetSessionFromKeyPacket +Dropped, use now `DecryptSessionKey`. +``` +(pm *PmCrypto) GetSessionFromKeyPacket(keyPackage []byte, privateKey *KeyRing, passphrase string) (*SymmetricKey, error): +* (keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SymmetricKey, error) +``` + +### KeyPacketWithPublicKey, KeyPacketWithPublicKeyBin +Dropped, use now `EncryptSessionKey`. +``` +(pm *PmCrypto) KeyPacketWithPublicKey(sessionSplit *SymmetricKey, publicKey string) ([]byte, error): +(pm *PmCrypto) KeyPacketWithPublicKeyBin(sessionSplit *SymmetricKey, publicKey []byte) ([]byte, error): +* (keyRing *KeyRing) EncryptSessionKey(sessionSplit *SymmetricKey) ([]byte, error) +``` + +### GetSessionFromSymmetricPacket +Renamed, moved to `SymmetricKey`. +``` +(pm *PmCrypto) GetSessionFromSymmetricPacket(keyPackage []byte, password string) (*SymmetricKey, error): +* NewSymmetricKeyFromKeyPacket(keyPacket []byte, password string) (*SymmetricKey, error) +``` + +### SymmetricKeyPacketWithPassword +Renamed, moved to `SymmetricKey`. +``` +(pm *PmCrypto) SymmetricKeyPacketWithPassword(sessionSplit *SymmetricKey, password string) ([]byte, error): +* (symmetricKey *SymmetricKey) EncryptToKeyPacket(password string) ([]byte, error) +``` + +## sign_detached.go + +### SignTextDetached +Moved to `KeyRing`, changed to `Sign`. +``` +(pm *PmCrypto) SignTextDetached(plaintext string, privateKey *KeyRing, passphrase string, trim bool) (string, error): +* (if just signature) (keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, error) +* (if PGP SIGNED MESSAGE) (helper) SignCleartextMessage(keyRing *crypto.KeyRing, text string) (string, error) +* (if PGP SIGNED MESSAGE) (helper) SignCleartextMessageArmored(privateKey, passphrase, text string) (string, error) +``` + +### SignBinDetached +Moved to `KeyRing`. +``` +(pm *PmCrypto) SignBinDetached(plainData []byte, privateKey *KeyRing, passphrase string) (string, error): +* (keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, error) +``` + +### VerifyTextSignDetachedBinKey, VerifyBinSignDetachedBinKey +Moved to `KeyRing`, changed to Verify. +See signature_test.go for use examples. +``` +(pm *PmCrypto) VerifyTextSignDetachedBinKey(signature string, plaintext string, publicKey *KeyRing, verifyTime int64) (bool, error): +(pm *PmCrypto) VerifyBinSignDetachedBinKey(signature string, plainData []byte, publicKey *KeyRing, verifyTime int64) (bool, error): +* (to verify) (keyRing *KeyRing) VerifyDetached(message *PlainMessage, signature *PGPSignature, verifyTime int64) (*Verification, error) +* (if PGP SIGNED MESSAGE) (helper) VerifyCleartextMessage(keyRing *crypto.KeyRing, armored string, verifyTime int64) (string, error) +* (if PGP SIGNED MESSAGE) (helper) VerifyCleartextMessageArmored(publicKey, armored string, verifyTime int64) (string, error) +``` + +## signature_collector.go +No change. + +## time.go +### UpdateTime +No change. + +### GetTimeUnix +Renamed. +``` +(pm *PmCrypto) GetTimeUnix() int64: +(pm *PmCrypto) GetUnixTime() int64 +``` + +### GetTime +No change. diff --git a/README.md b/README.md index 41b72fe..fafa2d5 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,15 @@ crypto library](https://github.com/ProtonMail/crypto). - [Documentation](#documentation) - [Using with Go Mobile](#using-with-go-mobile) - [Other notes](#other-notes) +- [Full documentation](#full-documentation) - [Examples](#examples) - [Set up](#set-up) - - [Encrypt and decrypt](#encrypt-and-decrypt) - - [Encrypt / Decrypt with password](#encrypt--decrypt-with-password) - - [Encrypt / Decrypt with PGP keys](#encrypt--decrypt-with-pgp-keys) + - [Encrypt / Decrypt with password](#encrypt--decrypt-with-password) + - [Encrypt / Decrypt with PGP keys](#encrypt--decrypt-with-pgp-keys) - [Generate key](#generate-key) - - [Sign plain text messages](#sign-plain-text-messages) + - [Detached signatures for plain text messages](#detached-signatures-for-plain-text-messages) - [Detached signatures for binary data](#detached-signatures-for-binary-data) + - [Cleartext signed messages](#cleartext-signed-messages) @@ -70,6 +71,9 @@ If you wish to use build.sh, you may need to modify the paths in it. Interfacing between Go and Swift: https://medium.com/@matryer/tutorial-calling-go-code-from-swift-on-ios-and-vice-versa-with-gomobile-7925620c17a4. +## Full documentation +The full documentation for this API is available here: https://godoc.org/gopkg.in/ProtonMail/gopenpgp.v0/crypto + ## Examples ### Set up @@ -78,27 +82,53 @@ https://medium.com/@matryer/tutorial-calling-go-code-from-swift-on-ios-and-vice- import "github.com/ProtonMail/gopenpgp/crypto" ``` -### Encrypt and decrypt - -Encryption and decryption will use the AES256 algorithm by default. - -#### Encrypt / Decrypt with password +### Encrypt / Decrypt with password ```go -var pgp = crypto.GopenPGP{} +import "github.com/ProtonMail/gopenpgp/helper" const password = "my secret password" // Encrypt data with password -armor, err := pgp.EncryptMessageWithPassword("my message", password) +armor, err := helper.EncryptMessageWithToken(password, "my message") // Decrypt data with password -message, err := pgp.DecryptMessageWithPassword(armor, password) +message, err := helper.DecryptMessageWithToken(password, armor) ``` -#### Encrypt / Decrypt with PGP keys +To use more encryption algorithms: +```go +import "github.com/ProtonMail/gopenpgp/constants" +import "github.com/ProtonMail/gopenpgp/helper" + +// Encrypt data with password +armor, err := helper.EncryptMessageWithTokenAlgo(password, "my message", constants.ThreeDES) + +// Decrypt data with password +message, err := helper.DecryptMessageWithToken(password, armor) +``` + +To encrypt binary data, reuse the key multiple times, or use more advanced modes: +```go +import "github.com/ProtonMail/gopenpgp/constants" + +var key = crypto.NewSymmetricKey("my secret password", constants.AES256) +var message = crypto.NewPlainMessage(data) + +// Encrypt data with password +encrypted, err := key.Encrypt(message) + +// Decrypt data with password +decrypted, err := key.Decrypt(password, encrypted) + +//Original message in decrypted.GetBinary() +``` + +### Encrypt / Decrypt with PGP keys ```go +import "github.com/ProtonMail/gopenpgp/helper" + // put keys in backtick (``) to avoid errors caused by spaces or tabs const pubkey = `-----BEGIN PGP PUBLIC KEY BLOCK----- ... @@ -110,29 +140,52 @@ const privkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- const passphrase = `the passphrase of the private key` // what the privKey is encrypted with -publicKeyRing, err := crypto.ReadArmoredKeyRing(strings.NewReader(pubkey)) - -privateKeyRing, err := crypto.ReadArmoredKeyRing(strings.NewReader(privkey)) -privateKeyRing.Unlock([]byte(passphrase)) // if private key is locked with passphrase - -// encrypt message using public key, can be optionally signed using private key -armor, err := publicKeyRing.EncryptMessage("plain text", privateKeyRing) +// encrypt message using public key +armor, err := helper.EncryptMessageArmored(pubkey, "plain text") // decrypt armored encrypted message using the private key -signedText, err := privateKeyRing.DecryptMessage(armor) -plainText = signedText.String - -// verify signature (optional) -signed = signedText.Signed.IsBy(publicKeyRing) +decrypted, err := helper.DecryptMessageArmored(privkey, passphrase, armor) ``` +With signatures: +```go +// Keys initialization as before (omitted) + +// encrypt message using public key, sign with the private key +armor, err := helper.EncryptSignMessageArmored(pubkey, privkey, passphrase, "plain text") + +// decrypt armored encrypted message using the private key, verify with the public key +// err != nil if verification fails +decrypted, err := helper.DecryptVerifyMessageArmored(pubkey, privkey, passphrase, armor) +``` + +With binary data or advanced modes: +```go +// Keys initialization as before (omitted) +var binMessage = NewPlainMessage(data) + +publicKeyRing, err := pgp.BuildKeyRingArmored(publicKey) +privateKeyRing, err := pgp.BuildKeyRingArmored(privateKey) +err = privateKeyRing.UnlockWithPassphrase(passphrase) +pgpMessage, err := publicKeyRing.Encrypt(binMessage, privateKeyRing) + +// Armored message in pgpMessage.GetArmored() +// pgpMessage can be obtained from NewPGPMessageFromArmored(ciphertext) + +message, verification, err := privateKeyRing.Decrypt(pgpMessage, publicKeyRing, pgp.GetUnixTime()) + +// Original data in message.GetString() +if verification.IsValid() { + // verification success +} +``` ### Generate key Keys are generated with the `GenerateKey` function, that returns the armored key as a string and a potential error. The library supports RSA with different key lengths or Curve25519 keys. ```go -var pgp = crypto.GopenPGP{} +var pgp = crypto.GetGopenPGP() const ( localPart = "name.surname" @@ -149,7 +202,7 @@ rsaKey, err := pgp.GenerateKey(localPart, domain, passphrase, "rsa", rsaBits) ecKey, err := pgp.GenerateKey(localPart, domain, passphrase, "x25519", ecBits) ``` -### Sign plain text messages +### Detached signatures for plain text messages To sign plain text data either an unlocked private keyring or a passphrase must be provided. The output is an armored signature. @@ -158,20 +211,25 @@ The output is an armored signature. const privkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- ... -----END PGP PRIVATE KEY BLOCK-----` // encrypted private key -passphrase = "LongSecret" +const passphrase = "LongSecret" const trimNewlines = false -signingKeyRing, err := crypto.ReadArmoredKeyRing(strings.NewReader(privkey)) +var message = NewPlaintextMessage("Verified message") -signature, err := signingKeyRing.SignTextDetached(plaintext, passphrase, trimNewlines) -// passphrase is optional if the key is already unlocked +signingKeyRing, err := pgp.BuildKeyRingArmored(privkey) +signingKeyRing.UnlockWithPassphrase(passphrase) // if private key is locked with passphrase + +pgpSignature, err := signingKeyRing.SignDetached(message, trimNewlines) + +// The armored signature is in pgpSignature.GetArmored() +// The signed text is in message.GetString() ``` To verify a signature either private or public keyring can be provided. -The newlines in the text are never trimmed in the verification process. -The function outputs a bool, if the verification fails `verified` will be false, and the error will be not `nil`. ```go +var pgp = crypto.GetGopenPGP() + const pubkey = `-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----` @@ -180,36 +238,43 @@ const signature = `-----BEGIN PGP SIGNATURE----- ... -----END PGP SIGNATURE-----` -const verifyTime = 0 -const trimNewlines = false +message := NewPlaintextMessage("Verified message") +pgpSignature, err := NewPGPSignatureFromArmored(signature) +signingKeyRing, err := pgp.BuildKeyRingArmored(pubkey) -signingKeyRing, err := crypto.ReadArmoredKeyRing(strings.NewReader(pubkey)) +verification, err := signingKeyRing.VerifyDetached(message, pgpSignature, pgp.GetUnixTime()) -verified, err := signingKeyRing.VerifyTextDetachedSig(signature, signedPlainText, verifyTime, trimNewlines) +if verification.IsValid() { + // verification success +} ``` ### Detached signatures for binary data -To sign binary data either an unlocked private keyring or a passphrase must be provided. -The output is an armored signature. - ```go +var pgp = crypto.GetGopenPGP() + const privkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- ... -----END PGP PRIVATE KEY BLOCK-----` // encrypted private key -passphrase = "LongSecret" +const passphrase = "LongSecret" -signingKeyRing, err := crypto.ReadArmoredKeyRing(strings.NewReader(privkey)) +var message = NewPlainMessage(data) -signature, err := signingKeyRing.SignBinDetached(data, passphrase) -// passphrase is optional if the key is already unlocked +signingKeyRing, err := pgp.BuildKeyRingArmored(privkey) +signingKeyRing.UnlockWithPassphrase(passphrase) // if private key is locked with passphrase + +pgpSignature, err := signingKeyRing.SignDetached(message) + +// The armored signature is in pgpSignature.GetArmored() +// The signed text is in message.GetBinary() ``` To verify a signature either private or public keyring can be provided. -The newlines in the text are never trimmed in the verification process. -The function outputs a bool, if the verification fails `verified` will be false, and the error will be not `nil`. ```go +var pgp = crypto.GetGopenPGP() + const pubkey = `-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----` @@ -218,9 +283,31 @@ const signature = `-----BEGIN PGP SIGNATURE----- ... -----END PGP SIGNATURE-----` -const verifyTime = 0 +message := NewPlainMessage("Verified message") +pgpSignature, err := NewPGPSignatureFromArmored(signature) +signingKeyRing, err := pgp.BuildKeyRingArmored(pubkey) -signingKeyRing, err := crypto.ReadArmoredKeyRing(strings.NewReader(pubkey)) +verification, err := signingKeyRing.VerifyDetached(message, pgpSignature, pgp.GetUnixTime()) -verified, err := signingKeyRing.VerifyBinDetachedSig(signature, data, verifyTime) +if verification.IsValid() { + // verification success +} +``` + +### Cleartext signed messages +```go +// Keys initialization as before (omitted) + +armored, err := SignCleartextMessageArmored(privateKey, passphrase, plaintext) +``` + +To verify the message it has to be provided unseparated to the library. +If verification fails an error will be returned. +```go +// Keys initialization as before (omitted) + +var pgp = crypto.GetGopenPGP() +var verifyTime = pgp.GetUnixTime() + +verifiedPlainText, err := VerifyCleartextMessageArmored(publicKey, armored, verifyTime) ``` diff --git a/armor/armor.go b/armor/armor.go index 082f48c..778edc1 100644 --- a/armor/armor.go +++ b/armor/armor.go @@ -5,12 +5,14 @@ package armor import ( "bytes" "errors" - "github.com/ProtonMail/gopenpgp/constants" - "github.com/ProtonMail/gopenpgp/internal" - "golang.org/x/crypto/openpgp/armor" - "golang.org/x/crypto/openpgp/clearsign" "io" "io/ioutil" + + "github.com/ProtonMail/gopenpgp/constants" + "github.com/ProtonMail/gopenpgp/internal" + + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/clearsign" ) // ArmorKey armors input as a public key. @@ -50,11 +52,32 @@ func Unarmor(input string) ([]byte, error) { return ioutil.ReadAll(b.Body) } -// ReadClearSignedMessage returns the message body from a clearsigned message. -func ReadClearSignedMessage(signedMessage string) (string, error) { +// ReadClearSignedMessage returns the message body and unarmored signature from a clearsigned message. +func ReadClearSignedMessage(signedMessage string) (string, []byte, error) { modulusBlock, rest := clearsign.Decode([]byte(signedMessage)) if len(rest) != 0 { - return "", errors.New("pmapi: extra data after modulus") + return "", nil, errors.New("pmapi: extra data after modulus") } - return string(modulusBlock.Bytes), nil + + signature, err := ioutil.ReadAll(modulusBlock.ArmoredSignature.Body) + if err != nil { + return "", nil, err + } + + return string(modulusBlock.Bytes), signature, nil +} + +// ArmorClearSignedMessage armors plaintext and signature with the PGP SIGNED MESSAGE armoring +func ArmorClearSignedMessage(plaintext []byte, signature []byte) (string, error) { + armSignature, err := ArmorWithType(signature, constants.PGPSignatureHeader) + if err != nil { + return "", err + } + + str := "-----BEGIN PGP SIGNED MESSAGE-----\r\nHash:SHA512\r\n\r\n" + str += string(plaintext) + str += "\r\n" + str += armSignature + + return str, nil } diff --git a/constants/armor.go b/constants/armor.go index 357c9ee..ac20ea9 100644 --- a/constants/armor.go +++ b/constants/armor.go @@ -6,6 +6,7 @@ const ( ArmorHeaderVersion = "GopenPGP 0.0.1 (" + Version + ")" ArmorHeaderComment = "https://gopenpgp.org" PGPMessageHeader = "PGP MESSAGE" + PGPSignatureHeader = "PGP SIGNATURE" PublicKeyHeader = "PGP PUBLIC KEY BLOCK" PrivateKeyHeader = "PGP PRIVATE KEY BLOCK" ) diff --git a/constants/cipher.go b/constants/cipher.go index 1f1f8fa..a160e2e 100644 --- a/constants/cipher.go +++ b/constants/cipher.go @@ -9,3 +9,10 @@ const ( AES192 = "aes192" AES256 = "aes256" ) + +const ( + SIGNATURE_OK int = 0 + SIGNATURE_NOT_SIGNED int = 1 + SIGNATURE_NO_VERIFIER int = 2 + SIGNATURE_FAILED int = 3 +) diff --git a/crypto/attachment.go b/crypto/attachment.go index 57bc37a..256501a 100644 --- a/crypto/attachment.go +++ b/crypto/attachment.go @@ -2,15 +2,11 @@ package crypto import ( "bytes" - "fmt" "io" "io/ioutil" "runtime" "sync" - armorUtils "github.com/ProtonMail/gopenpgp/armor" - "github.com/ProtonMail/gopenpgp/constants" - "github.com/ProtonMail/gopenpgp/models" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" ) @@ -21,7 +17,7 @@ type AttachmentProcessor struct { w *io.WriteCloser pipe *io.PipeWriter done sync.WaitGroup - split *models.EncryptedSplit + split *PGPSplitMessage garbageCollector int err error } @@ -34,7 +30,7 @@ func (ap *AttachmentProcessor) Process(plainData []byte) { } // Finish closes the attachment and returns the encrypted data -func (ap *AttachmentProcessor) Finish() (*models.EncryptedSplit, error) { +func (ap *AttachmentProcessor) Finish() (*PGPSplitMessage, error) { if ap.err != nil { return nil, ap.err } @@ -47,10 +43,10 @@ func (ap *AttachmentProcessor) Finish() (*models.EncryptedSplit, error) { return ap.split, nil } -// encryptAttachment creates an AttachmentProcessor which can be used to encrypt +// newAttachmentProcessor creates an AttachmentProcessor which can be used to encrypt // a file. It takes an estimatedSize and fileName as hints about the file. -func (pgp *GopenPGP) encryptAttachment( - estimatedSize int, fileName string, publicKey *KeyRing, garbageCollector int, +func (keyRing *KeyRing) newAttachmentProcessor( + estimatedSize int, fileName string, garbageCollector int, ) (*AttachmentProcessor, error) { attachmentProc := &AttachmentProcessor{} // You could also add these one at a time if needed. @@ -70,17 +66,18 @@ func (pgp *GopenPGP) encryptAttachment( go func() { defer attachmentProc.done.Done() - split, splitError := SeparateKeyAndData(nil, reader, estimatedSize, garbageCollector) + ciphertext, _ := ioutil.ReadAll(reader) + message := NewPGPMessage(ciphertext) + split, splitError := message.SeparateKeyAndData(estimatedSize, garbageCollector) if attachmentProc.err != nil { attachmentProc.err = splitError } - split.Algo = constants.AES256 attachmentProc.split = split }() var ew io.WriteCloser var encryptErr error - ew, encryptErr = openpgp.Encrypt(writer, publicKey.entities, nil, hints, config) + ew, encryptErr = openpgp.Encrypt(writer, keyRing.entities, nil, hints, config) if encryptErr != nil { return nil, encryptErr } @@ -90,15 +87,14 @@ func (pgp *GopenPGP) encryptAttachment( return attachmentProc, nil } -// EncryptAttachment encrypts a file. fileName -func (pgp *GopenPGP) EncryptAttachment( - plainData []byte, fileName string, publicKey *KeyRing, -) (*models.EncryptedSplit, error) { - ap, err := pgp.encryptAttachment(len(plainData), fileName, publicKey, -1) +// EncryptAttachment encrypts a file given a PlainMessage and a fileName. +// Returns a PGPSplitMessage containing a session key packet and symmetrically encrypted data +func (keyRing *KeyRing) EncryptAttachment(message *PlainMessage, fileName string) (*PGPSplitMessage, error) { + ap, err := keyRing.newAttachmentProcessor(len(message.GetBinary()), fileName, -1) if err != nil { return nil, err } - ap.Process(plainData) + ap.Process(message.GetBinary()) split, err := ap.Finish() if err != nil { return nil, err @@ -106,47 +102,23 @@ func (pgp *GopenPGP) EncryptAttachment( return split, nil } -// EncryptAttachmentLowMemory creates an AttachmentProcessor which can be used +// NewLowMemoryAttachmentProcessor creates an AttachmentProcessor which can be used // to encrypt a file. It takes an estimatedSize and fileName as hints about the // file. It is optimized for low-memory environments and collects garbage every // megabyte. -func (pgp *GopenPGP) EncryptAttachmentLowMemory( - estimatedSize int, fileName string, publicKey *KeyRing, +func (keyRing *KeyRing) NewLowMemoryAttachmentProcessor( + estimatedSize int, fileName string, ) (*AttachmentProcessor, error) { - return pgp.encryptAttachment(estimatedSize, fileName, publicKey, 1<<20) + return keyRing.newAttachmentProcessor(estimatedSize, fileName, 1<<20) } -// SplitArmor is a helper method which splits an armored message into its -// session key packet and symmetrically encrypted data packet. -func SplitArmor(encrypted string) (*models.EncryptedSplit, error) { - var err error +// DecryptAttachment takes a PGPSplitMessage, containing a session key packet and symmetrically encrypted data +// and returns a decrypted PlainMessage +func (keyRing *KeyRing) DecryptAttachment(message *PGPSplitMessage) (*PlainMessage, error) { + privKeyEntries := keyRing.entities - encryptedRaw, err := armorUtils.Unarmor(encrypted) - if err != nil { - return nil, err - } - - encryptedReader := bytes.NewReader(encryptedRaw) - - return SeparateKeyAndData(nil, encryptedReader, len(encrypted), -1) -} - -// DecryptAttachment takes a session key packet and symmetrically encrypted data -// packet. privateKeys is a KeyRing that can contain multiple keys. The -// passphrase is used to unlock keys in privateKeys. -func (pgp *GopenPGP) DecryptAttachment( - keyPacket, dataPacket []byte, - kr *KeyRing, passphrase string, -) ([]byte, error) { - privKeyEntries := kr.entities - - if err := kr.Unlock([]byte(passphrase)); err != nil { - err = fmt.Errorf("gopenpgp: cannot decrypt attachment: %v", err) - return nil, err - } - - keyReader := bytes.NewReader(keyPacket) - dataReader := bytes.NewReader(dataPacket) + keyReader := bytes.NewReader(message.GetKeyPacket()) + dataReader := bytes.NewReader(message.GetDataPacket()) encryptedReader := io.MultiReader(keyReader, dataReader) @@ -163,5 +135,5 @@ func (pgp *GopenPGP) DecryptAttachment( return nil, err } - return b, nil + return NewPlainMessage(b), nil } diff --git a/crypto/attachment_test.go b/crypto/attachment_test.go index a8fae68..fb54c34 100644 --- a/crypto/attachment_test.go +++ b/crypto/attachment_test.go @@ -2,7 +2,6 @@ package crypto import ( "encoding/base64" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -13,54 +12,45 @@ import ( func TestAttachmentGetKey(t *testing.T) { testKeyPacketsDecoded, err := base64.StdEncoding.DecodeString(readTestFile("attachment_keypacket", false)) - if err != nil { t.Fatal("Expected no error while decoding base64 KeyPacket, got:", err) } - split, err := SeparateKeyAndData( - testPrivateKeyRing, - strings.NewReader(string(testKeyPacketsDecoded)), - len(testKeyPacketsDecoded), - -1) + symmetricKey, err := testPrivateKeyRing.DecryptSessionKey(testKeyPacketsDecoded) if err != nil { - t.Fatal("Expected no error while decrypting attachment key, got:", err) + t.Fatal("Expected no error while decrypting KeyPacket, got:", err) } - assert.Exactly(t, testSymmetricKey.Key, split.KeyPacket) + assert.Exactly(t, testSymmetricKey, symmetricKey) } func TestAttachmentSetKey(t *testing.T) { - packets, err := testPublicKeyRing.EncryptKey(testSymmetricKey) + keyPackets, err := testPublicKeyRing.EncryptSessionKey(testSymmetricKey) if err != nil { t.Fatal("Expected no error while encrypting attachment key, got:", err) } - keyPackets, err := base64.StdEncoding.DecodeString(packets) - if err != nil { - t.Fatal("Expected no error while decoding base64 KeyPacket, got:", err) - } - - split, err := SeparateKeyAndData(testPrivateKeyRing, strings.NewReader(string(keyPackets)), len(keyPackets), -1) + symmetricKey, err := testPrivateKeyRing.DecryptSessionKey(keyPackets) if err != nil { t.Fatal("Expected no error while decrypting attachment key, got:", err) } - assert.Exactly(t, testSymmetricKey.Key, split.KeyPacket) + assert.Exactly(t, testSymmetricKey, symmetricKey) } func TestAttachnentEncryptDecrypt(t *testing.T) { var testAttachmentCleartext = "cc,\ndille." + var message = NewPlainMessage([]byte(testAttachmentCleartext)) - encSplit, err := pgp.EncryptAttachment([]byte(testAttachmentCleartext), "s.txt", testPrivateKeyRing) + encSplit, err := testPrivateKeyRing.EncryptAttachment(message, "s.txt") if err != nil { t.Fatal("Expected no error while encrypting attachment, got:", err) } - redecData, err := pgp.DecryptAttachment(encSplit.KeyPacket, encSplit.DataPacket, testPrivateKeyRing, "") + redecData, err := testPrivateKeyRing.DecryptAttachment(encSplit) if err != nil { t.Fatal("Expected no error while decrypting attachment, got:", err) } - assert.Exactly(t, testAttachmentCleartext, string(redecData)) + assert.Exactly(t, message, redecData) } diff --git a/crypto/key.go b/crypto/key.go index 438a317..dfa1347 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -3,278 +3,22 @@ package crypto import ( "bytes" "crypto" - "encoding/base64" "encoding/hex" "errors" "fmt" - "io" "math/big" - "runtime" "strings" "time" "github.com/ProtonMail/gopenpgp/armor" "github.com/ProtonMail/gopenpgp/constants" - "github.com/ProtonMail/gopenpgp/models" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" ) -// SymmetricKey stores a decrypted session key. -type SymmetricKey struct { - // The decrypted binary session key. - Key []byte - // The symmetric encryption algorithm used with this key. - Algo string -} - -var symKeyAlgos = map[string]packet.CipherFunction{ - constants.ThreeDES: packet.Cipher3DES, - constants.TripleDES: packet.Cipher3DES, - constants.CAST5: packet.CipherCAST5, - constants.AES128: packet.CipherAES128, - constants.AES192: packet.CipherAES192, - constants.AES256: packet.CipherAES256, -} - -// GetCipherFunc returns the cipher function corresponding to the algorithm used -// with this SymmetricKey. -func (sk *SymmetricKey) GetCipherFunc() packet.CipherFunction { - cf, ok := symKeyAlgos[sk.Algo] - if ok { - return cf - } - - panic("gopenpgp: unsupported cipher function: " + sk.Algo) -} - -// GetBase64Key returns the session key as base64 encoded string. -func (sk *SymmetricKey) GetBase64Key() string { - return base64.StdEncoding.EncodeToString(sk.Key) -} - -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("gopenpgp: unsupported cipher function: %v", ek.CipherFunc)) - } - - return &SymmetricKey{ - Key: ek.Key, //base64.StdEncoding.EncodeToString(ek.Key), - Algo: algo, - } -} - -// DecryptAttKey decrypts a public-key encrypted session key and returns the -// decrypted symmetric session key. -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("gopenpgp: cannot decrypt encrypted key packet: %v", decryptErr) - return - } - - key = newSymmetricKey(ek) - return -} - -// SeparateKeyAndData reads a binary PGP message from r and splits it into its -// session key packet and symmetrically encrypted data packet. -func SeparateKeyAndData( - kr *KeyRing, r io.Reader, - estimatedLength, garbageCollector int, -) (outSplit *models.EncryptedSplit, err error) { - // For info on each, see: https://golang.org/pkg/runtime/#MemStats - packets := packet.NewReader(r) - outSplit = &models.EncryptedSplit{} - gcCounter := 0 - - // Store encrypted key and symmetrically encrypted packet separately - 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 - - if kr != nil { - 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: - // The code below is optimized to not - var b bytes.Buffer - // 2^16 is an estimation of the size difference between input and output, the size difference is most probably - // 16 bytes at a maximum though. - // We need to avoid triggering a grow from the system as this will allocate too much memory causing problems - // in low-memory environments - b.Grow(1<<16 + estimatedLength) - // empty encoded length + start byte - b.Write(make([]byte, 6)) - b.WriteByte(byte(1)) - actualLength := 1 - block := make([]byte, 128) - for { - n, err := p.Contents.Read(block) - if err == io.EOF { - break - } - b.Write(block[:n]) - actualLength += n - gcCounter += n - if gcCounter > garbageCollector && garbageCollector > 0 { - runtime.GC() - gcCounter = 0 - } - } - - // quick encoding - symEncryptedData := b.Bytes() - if actualLength < 192 { - symEncryptedData[4] = byte(210) - symEncryptedData[5] = byte(actualLength) - symEncryptedData = symEncryptedData[4:] - } else if actualLength < 8384 { - actualLength = actualLength - 192 - symEncryptedData[3] = byte(210) - symEncryptedData[4] = 192 + byte(actualLength>>8) - symEncryptedData[5] = byte(actualLength) - symEncryptedData = symEncryptedData[3:] - } else { - symEncryptedData[0] = byte(210) - symEncryptedData[1] = byte(255) - symEncryptedData[2] = byte(actualLength >> 24) - symEncryptedData[3] = byte(actualLength >> 16) - symEncryptedData[4] = byte(actualLength >> 8) - symEncryptedData[5] = byte(actualLength) - } - - outSplit.DataPacket = symEncryptedData - } - } - if decryptErr != nil { - err = fmt.Errorf("gopenpgp: cannot decrypt encrypted key packet: %v", decryptErr) - return nil, err - } - if ek == nil { - err = errors.New("gopenpgp: packets don't include an encrypted key packet") - return nil, err - } - - if kr == nil { - var buf bytes.Buffer - if err := ek.Serialize(&buf); err != nil { - err = fmt.Errorf("gopenpgp: cannot serialize encrypted key: %v", err) - return nil, err - } - outSplit.KeyPacket = buf.Bytes() - } else { - key := newSymmetricKey(ek) - outSplit.KeyPacket = key.Key - outSplit.Algo = key.Algo - } - - return outSplit, nil -} - -// EncryptKey encrypts the provided key. -func (kr *KeyRing) EncryptKey(symKey *SymmetricKey) (packets string, err error) { - b := &bytes.Buffer{} - w := base64.NewEncoder(base64.StdEncoding, b) - - cf := symKey.GetCipherFunc() - - if len(kr.entities) == 0 { - err = fmt.Errorf("gopenpgp: 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("gopenpgp: cannot set key: no public key available") - return "", err - } - - if err = packet.SerializeEncryptedKey(w, pub, cf, symKey.Key, nil); err != nil { - err = fmt.Errorf("gopenpgp: cannot set key: %v", err) - return "", err - } - - if err = w.Close(); err != nil { - err = fmt.Errorf("gopenpgp: cannot set key: %v", err) - return "", err - } - - return b.String(), nil -} - -// IsKeyExpiredBin checks whether the given (unarmored, binary) key is expired. -func (pgp *GopenPGP) IsKeyExpiredBin(publicKey []byte) (bool, error) { +// IsKeyExpired checks whether the given (unarmored, binary) key is expired. +func (pgp *GopenPGP) IsKeyExpired(publicKey []byte) (bool, error) { now := pgp.getNow() pubKeyReader := bytes.NewReader(publicKey) pubKeyEntries, err := openpgp.ReadKeyRing(pubKeyReader) @@ -325,34 +69,26 @@ func (pgp *GopenPGP) IsKeyExpiredBin(publicKey []byte) (bool, error) { return true, errors.New("keys expired") } -const ( - ok = 0 - notSigned = 1 - noVerifier = 2 - failed = 3 -) - -// IsKeyExpired checks whether the given armored key is expired. -func (pgp *GopenPGP) IsKeyExpired(publicKey string) (bool, error) { +// IsArmoredKeyExpired checks whether the given armored key is expired. +func (pgp *GopenPGP) IsArmoredKeyExpired(publicKey string) (bool, error) { rawPubKey, err := armor.Unarmor(publicKey) if err != nil { return false, err } - return pgp.IsKeyExpiredBin(rawPubKey) + return pgp.IsKeyExpired(rawPubKey) } func (pgp *GopenPGP) generateKey( - userName, domain, passphrase, keyType string, + name, email, passphrase, keyType string, bits int, prime1, prime2, prime3, prime4 []byte, ) (string, error) { - if len(userName) <= 0 { - return "", errors.New("invalid user name format") + if len(email) <= 0 { + return "", errors.New("invalid email format") } - var email = userName - if len(domain) > 0 { - email = email + "@" + domain + if len(name) <= 0 { + return "", errors.New("invalid name format") } comments := "" @@ -383,7 +119,7 @@ func (pgp *GopenPGP) generateKey( cfg.RSAPrimes = bigPrimes[:] } - newEntity, err := openpgp.NewEntity(email, comments, email, cfg) + newEntity, err := openpgp.NewEntity(name, comments, email, cfg) if err != nil { return "", err } @@ -417,23 +153,22 @@ func (pgp *GopenPGP) generateKey( // GenerateRSAKeyWithPrimes generates a RSA key using the given primes. func (pgp *GopenPGP) GenerateRSAKeyWithPrimes( - userName, domain, passphrase string, + name, email, passphrase string, bits int, primeone, primetwo, primethree, primefour []byte, ) (string, error) { - return pgp.generateKey(userName, domain, passphrase, "rsa", bits, primeone, primetwo, primethree, primefour) + return pgp.generateKey(name, email, passphrase, "rsa", bits, primeone, primetwo, primethree, primefour) } -// GenerateKey generates a key of the given keyType ("rsa" or "x25519"). If -// keyType is "rsa", bits is the RSA bitsize of the key. If keyType is "x25519", -// bits is unused. -func (pgp *GopenPGP) GenerateKey(userName, domain, passphrase, keyType string, bits int) (string, error) { - return pgp.generateKey(userName, domain, passphrase, keyType, bits, nil, nil, nil, nil) +// GenerateKey generates a key of the given keyType ("rsa" or "x25519"). +// If keyType is "rsa", bits is the RSA bitsize of the key. +// If keyType is "x25519" bits is unused. +func (pgp *GopenPGP) GenerateKey(name, email, passphrase, keyType string, bits int) (string, error) { + return pgp.generateKey(name, email, passphrase, keyType, bits, nil, nil, nil, nil) } -// UpdatePrivateKeyPassphrase decrypts the given armored privateKey with -// oldPassphrase, re-encrypts it with newPassphrase, and returns the new armored -// key. +// UpdatePrivateKeyPassphrase decrypts the given armored privateKey with oldPassphrase, +// re-encrypts it with newPassphrase, and returns the new armored key. func (pgp *GopenPGP) UpdatePrivateKeyPassphrase( privateKey string, oldPassphrase string, newPassphrase string, ) (string, error) { @@ -479,9 +214,8 @@ func (pgp *GopenPGP) UpdatePrivateKeyPassphrase( return armor.ArmorWithType(serialized, constants.PrivateKeyHeader) } -// CheckKey is a debug helper function that prints the key and subkey -// fingerprints. -func (pgp *GopenPGP) CheckKey(pubKey string) (string, error) { +// PrintFingerprints is a debug helper function that prints the key and subkey fingerprints. +func (pgp *GopenPGP) PrintFingerprints(pubKey string) (string, error) { pubKeyReader := strings.NewReader(pubKey) entries, err := openpgp.ReadArmoredKeyRing(pubKeyReader) if err != nil { diff --git a/crypto/key_test.go b/crypto/key_test.go index 3bcafce..59bb44c 100644 --- a/crypto/key_test.go +++ b/crypto/key_test.go @@ -6,12 +6,13 @@ import ( "strings" "testing" - "github.com/ProtonMail/gopenpgp/constants" "github.com/stretchr/testify/assert" + + "golang.org/x/crypto/rsa" ) -const name = "richard.stallman" -const domain = "protonmail.ch" +const name = "Richard M. Stallman" +const domain = "rms@protonmail.ch" var passphrase = "I love GNU" var rsaKey, ecKey, rsaPublicKey, ecPublicKey string @@ -55,7 +56,7 @@ func TestGenerateKeyRings(t *testing.T) { t.Fatal("Cannot read RSA public key:", err) } - err = rsaPrivateKeyRing.Unlock([]byte(passphrase)) + err = rsaPrivateKeyRing.UnlockWithPassphrase(passphrase) if err != nil { t.Fatal("Cannot decrypt RSA key:", err) } @@ -75,40 +76,12 @@ func TestGenerateKeyRings(t *testing.T) { t.Fatal("Cannot read EC public key:", err) } - err = ecPrivateKeyRing.Unlock([]byte(passphrase)) + err = ecPrivateKeyRing.UnlockWithPassphrase(passphrase) if err != nil { t.Fatal("Cannot decrypt EC key:", err) } } -func TestEncryptDecryptKeys(t *testing.T) { - var pass, _ = base64.StdEncoding.DecodeString("H2CAwzpdexjxXucVYMERDiAc/td8aGPrr6ZhfMnZlLI=") - var testSymmetricKey = &SymmetricKey{ - Key: pass, - Algo: constants.AES256, - } - - packet, err := rsaPublicKeyRing.EncryptKey(testSymmetricKey) - if err != nil { - t.Fatal("Cannot encrypt keypacket with RSA keyring", err) - } - rsaTestSymmetricKey, err := DecryptAttKey(rsaPrivateKeyRing, packet) - if err != nil { - t.Fatal("Cannot decrypt keypacket with RSA keyring", err) - } - assert.Exactly(t, testSymmetricKey, rsaTestSymmetricKey) - - packet, err = ecPublicKeyRing.EncryptKey(testSymmetricKey) - if err != nil { - t.Fatal("Cannot encrypt keypacket with EC keyring", err) - } - ecTestSymmetricKey, err := DecryptAttKey(ecPrivateKeyRing, packet) - if err != nil { - t.Fatal("Cannot decrypt keypacket with EC keyring", err) - } - assert.Exactly(t, testSymmetricKey, ecTestSymmetricKey) -} - func TestUpdatePrivateKeysPassphrase(t *testing.T) { newPassphrase := "I like GNU" rsaKey, err = pgp.UpdatePrivateKeyPassphrase(rsaKey, passphrase, newPassphrase) @@ -124,20 +97,20 @@ func TestUpdatePrivateKeysPassphrase(t *testing.T) { passphrase = newPassphrase } -func ExampleCheckKeys() { - _, _ = pgp.CheckKey(readTestFile("keyring_publicKey", false)) +func ExamplePrintFingerprints() { + _, _ = pgp.PrintFingerprints(readTestFile("keyring_publicKey", false)) // Output: // SubKey:37e4bcf09b36e34012d10c0247dc67b5cb8267f6 // PrimaryKey:6e8ba229b0cccaf6962f97953eb6259edf21df24 } -func TestIsKeyExpired(t *testing.T) { - rsaRes, err := pgp.IsKeyExpired(rsaPublicKey) +func TestIsArmoredKeyExpired(t *testing.T) { + rsaRes, err := pgp.IsArmoredKeyExpired(rsaPublicKey) if err != nil { t.Fatal("Error in checking expiration of RSA key:", err) } - ecRes, err := pgp.IsKeyExpired(ecPublicKey) + ecRes, err := pgp.IsArmoredKeyExpired(ecPublicKey) if err != nil { t.Fatal("Error in checking expiration of EC key:", err) } @@ -147,11 +120,43 @@ func TestIsKeyExpired(t *testing.T) { pgp.UpdateTime(1557754627) // 2019-05-13T13:37:07+00:00 - expRes, expErr := pgp.IsKeyExpired(readTestFile("key_expiredKey", false)) - futureRes, futureErr := pgp.IsKeyExpired(readTestFile("key_futureKey", false)) + expRes, expErr := pgp.IsArmoredKeyExpired(readTestFile("key_expiredKey", false)) + futureRes, futureErr := pgp.IsArmoredKeyExpired(readTestFile("key_futureKey", false)) assert.Exactly(t, true, expRes) assert.Exactly(t, true, futureRes) assert.EqualError(t, expErr, "keys expired") assert.EqualError(t, futureErr, "keys expired") } + +func TestGenerateKeyWithPrimes(t *testing.T) { + prime1, _ := base64.StdEncoding.DecodeString( + "/thF8zjjk6fFx/y9NId35NFx8JTA7jvHEl+gI0dp9dIl9trmeZb+ESZ8f7bNXUmTI8j271kyenlrVJiqwqk80Q==") + prime2, _ := base64.StdEncoding.DecodeString( + "0HyyG/TShsw7yObD+DDP9Ze39ye1Redljx+KOZ3iNDmuuwwI1/5y44rD/ezAsE7A188NsotMDTSy5xtfHmu0xQ==") + prime3, _ := base64.StdEncoding.DecodeString( + "3OyJpAdnQXNjPNzI1u3BWDmPrzWw099E0UfJj5oJJILSbsAg/DDrmrdrIZDt7f24d06HCnTErCNWjvFJ3Kdq4w==") + prime4, _ := base64.StdEncoding.DecodeString( + "58UEDXTX29Q9JqvuE3Tn+Qj275CXBnJbA8IVM4d05cPYAZ6H43bPN01pbJqJTJw/cuFxs+8C+HNw3/MGQOExqw==") + + staticRsaKey, err := pgp.GenerateRSAKeyWithPrimes(name, domain, passphrase, 1024, prime1, prime2, prime3, prime4) + if err != nil { + t.Fatal("Cannot generate RSA key:", err) + } + rTest := regexp.MustCompile("(?s)^-----BEGIN PGP PRIVATE KEY BLOCK-----.*-----END PGP PRIVATE KEY BLOCK-----$") + assert.Regexp(t, rTest, staticRsaKey) + + staticRsaKeyRing, err := ReadArmoredKeyRing(strings.NewReader(staticRsaKey)) + if err != nil { + t.Fatal("Cannot read RSA key:", err) + } + + err = staticRsaKeyRing.UnlockWithPassphrase(passphrase) + if err != nil { + t.Fatal("Cannot decrypt RSA key:", err) + } + + pk := staticRsaKeyRing.GetEntities()[0].PrivateKey.PrivateKey.(*rsa.PrivateKey) + assert.Exactly(t, prime1, pk.Primes[1].Bytes()) + assert.Exactly(t, prime2, pk.Primes[0].Bytes()) +} diff --git a/crypto/keyring.go b/crypto/keyring.go index ea312d3..8dcdc93 100644 --- a/crypto/keyring.go +++ b/crypto/keyring.go @@ -8,31 +8,35 @@ import ( "encoding/json" "errors" "io" - "io/ioutil" - "regexp" "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" xrsa "golang.org/x/crypto/rsa" armorUtils "github.com/ProtonMail/gopenpgp/armor" - "github.com/ProtonMail/gopenpgp/constants" - "github.com/ProtonMail/gopenpgp/models" ) +// KeyRing contains multiple private and public keys. +type KeyRing struct { + // PGP entities in this keyring. + entities openpgp.EntityList + + // FirstKeyID as obtained from API to match salt + FirstKeyID string +} + // A keypair contains a private key and a public key. type pgpKeyObject struct { - ID string - Version int - Flags int - Fingerprint string - PublicKey string `json:",omitempty"` - PrivateKey string - Primary int + ID string + Version int + Flags int + PrivateKey string + Primary int + Token *string `json:",omitempty"` + Signature *string `json:",omitempty"` } // PrivateKeyReader @@ -46,87 +50,22 @@ type Identity struct { 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 -} - -// SignedString wraps string with a Signature -type SignedString struct { - String string - Signed *Signature -} - -var errKeyringNotUnlocked = errors.New("gopenpgp: 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 -} - -// KeyRing contains multiple private and public keys. -type KeyRing struct { - // PGP entities in this keyring. - entities openpgp.EntityList - - // FirstKeyID as obtained from API to match salt - FirstKeyID string -} - // GetEntities returns openpgp entities contained in this KeyRing. -func (kr *KeyRing) GetEntities() openpgp.EntityList { - return kr.entities +func (keyRing *KeyRing) GetEntities() openpgp.EntityList { + return keyRing.entities } // GetSigningEntity returns first private unlocked signing entity from keyring. -func (kr *KeyRing) GetSigningEntity(passphrase string) (*openpgp.Entity, error) { +func (keyRing *KeyRing) GetSigningEntity() (*openpgp.Entity, error) { var signEntity *openpgp.Entity - for _, e := range kr.entities { + for _, e := range keyRing.entities { // Entity.PrivateKey must be a signing key if e.PrivateKey != nil { - if e.PrivateKey.Encrypted { - if err := e.PrivateKey.Decrypt([]byte(passphrase)); err != nil { - continue - } + if !e.PrivateKey.Encrypted { + signEntity = e + break } - signEntity = e - break } } if signEntity == nil { @@ -137,184 +76,15 @@ func (kr *KeyRing) GetSigningEntity(passphrase string) (*openpgp.Entity, error) return signEntity, nil } -// Encrypt encrypts data to this keyring's owner. If sign is not nil, it also -// signs data with it. The keyring sign must be unlocked to be able to sign data, -// if not 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 - } - } - - return EncryptCore( - w, - encryptEntities, - signEntity, - filename, - canonicalizeText, - func() time.Time { return GetGopenPGP().GetTime() }) -} - -// EncryptCore is lower-level encryption method used by KeyRing.Encrypt. -func EncryptCore(w io.Writer, encryptEntities []*openpgp.Entity, signEntity *openpgp.Entity, filename string, - canonicalizeText bool, timeGenerator func() time.Time) (io.WriteCloser, error) { - - config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: timeGenerator} - - hints := &openpgp.FileHints{ - IsBinary: !canonicalizeText, - FileName: filename, - } - if canonicalizeText { - return openpgp.EncryptText(w, encryptEntities, signEntity, hints, config) - } - return openpgp.Encrypt(w, encryptEntities, signEntity, hints, config) -} - -// An io.WriteCloser that both encrypts and armors data. -type armorEncryptWriter struct { - aw io.WriteCloser // Armored writer - ew io.WriteCloser // Encrypted writer -} - -// Write encrypted data -func (w *armorEncryptWriter) Write(b []byte) (n int, err error) { - return w.ew.Write(b) -} - -// Close armor and encryption io.WriteClose -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. -// Wrapper of Encrypt. -func (kr *KeyRing) EncryptArmored(w io.Writer, sign *KeyRing) (wc io.WriteCloser, err error) { - aw, err := armorUtils.ArmorWithTypeBuffered(w, constants.PGPMessageHeader) - if err != nil { - return - } - - ew, err := kr.Encrypt(aw, sign, "", false) - if err != nil { - aw.Close() - return - } - - wc = &armorEncryptWriter{aw: aw, ew: ew} - return -} - -// EncryptMessage encrypts and armors a string to the keyring's owner. -// Wrapper of Encrypt. -func (kr *KeyRing) EncryptMessage(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 -} - -// EncryptSymmetric data using generated symmetric key encrypted with this KeyRing. -// Wrapper of Encrypt. -func (kr *KeyRing) EncryptSymmetric(textToEncrypt string, canonicalizeText bool) (outSplit *models.EncryptedSplit, - 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 outSplit, err = SeparateKeyAndData(kr, buffer, len(textToEncrypt), -1); err != nil { - return - } - - return -} - -// DecryptMessage 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) DecryptMessage(encrypted string) (SignedString, error) { - r, signed, err := kr.DecryptArmored(strings.NewReader(encrypted)) - if err != nil && err != pgperrors.ErrSignatureExpired { - return SignedString{String: encrypted, Signed: nil}, err - } - - b, err := ioutil.ReadAll(r) - if err != nil && err != pgperrors.ErrSignatureExpired { - return SignedString{String: encrypted, Signed: nil}, err - } - - s := string(b) - return SignedString{String: s, Signed: signed}, nil -} - -// DecryptMessageIfNeeded data if has armored PGP message format, if not return original data. -// 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) DecryptMessageIfNeeded(data string) (decrypted string, err error) { - if re := regexp.MustCompile("^-----BEGIN " + constants.PGPMessageHeader + "-----(?s:.+)-----END " + - constants.PGPMessageHeader + "-----"); re.MatchString(data) { - - var signed SignedString - signed, err = kr.DecryptMessage(data) - decrypted = signed.String - } else { - decrypted = data - } - return -} - // Unlock tries to unlock 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 { +func (keyRing *KeyRing) Unlock(passphrase []byte) error { // Build a list of keys to decrypt var keys []*packet.PrivateKey - for _, e := range kr.entities { + for _, e := range keyRing.entities { // Entity.PrivateKey must be a signing key if e.PrivateKey != nil { keys = append(keys, e.PrivateKey) @@ -352,48 +122,19 @@ func (kr *KeyRing) Unlock(passphrase []byte) error { 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 != constants.PGPMessageHeader { - err = errors.New("gopenpgp: not an armored PGP message") - return - } - - return kr.Decrypt(block.Body) +// UnlockWithPassphrase is a wrapper for Unlock that uses strings +func (keyRing *KeyRing) UnlockWithPassphrase(passphrase string) error { + return keyRing.Unlock([]byte(passphrase)) } // WriteArmoredPublicKey outputs armored public keys from the keyring to w. -func (kr *KeyRing) WriteArmoredPublicKey(w io.Writer) (err error) { +func (keyRing *KeyRing) WriteArmoredPublicKey(w io.Writer) (err error) { aw, err := armor.Encode(w, openpgp.PublicKeyType, nil) if err != nil { return } - for _, e := range kr.entities { + for _, e := range keyRing.entities { if err = e.Serialize(aw); err != nil { aw.Close() return @@ -405,9 +146,9 @@ func (kr *KeyRing) WriteArmoredPublicKey(w io.Writer) (err error) { } // GetArmoredPublicKey returns the armored public keys from this keyring. -func (kr *KeyRing) GetArmoredPublicKey() (s string, err error) { +func (keyRing *KeyRing) GetArmoredPublicKey() (s string, err error) { b := &bytes.Buffer{} - if err = kr.WriteArmoredPublicKey(b); err != nil { + if err = keyRing.WriteArmoredPublicKey(b); err != nil { return } @@ -416,8 +157,8 @@ func (kr *KeyRing) GetArmoredPublicKey() (s string, err error) { } // WritePublicKey outputs unarmored public keys from the keyring to w. -func (kr *KeyRing) WritePublicKey(w io.Writer) (err error) { - for _, e := range kr.entities { +func (keyRing *KeyRing) WritePublicKey(w io.Writer) (err error) { + for _, e := range keyRing.entities { if err = e.Serialize(w); err != nil { return } @@ -427,9 +168,9 @@ func (kr *KeyRing) WritePublicKey(w io.Writer) (err error) { } // GetPublicKey returns the unarmored public keys from this keyring. -func (kr *KeyRing) GetPublicKey() (b []byte, err error) { +func (keyRing *KeyRing) GetPublicKey() (b []byte, err error) { var outBuf bytes.Buffer - if err = kr.WritePublicKey(&outBuf); err != nil { + if err = keyRing.WritePublicKey(&outBuf); err != nil { return } @@ -438,8 +179,8 @@ func (kr *KeyRing) GetPublicKey() (b []byte, err error) { } // GetFingerprint gets the fingerprint from the keyring. -func (kr *KeyRing) GetFingerprint() (string, error) { - for _, entity := range kr.entities { +func (keyRing *KeyRing) GetFingerprint() (string, error) { + for _, entity := range keyRing.entities { fp := entity.PrimaryKey.Fingerprint return hex.EncodeToString(fp[:]), nil } @@ -447,10 +188,10 @@ func (kr *KeyRing) GetFingerprint() (string, error) { } // CheckPassphrase checks if private key passphrase is correct for every sub key. -func (kr *KeyRing) CheckPassphrase(passphrase string) bool { +func (keyRing *KeyRing) CheckPassphrase(passphrase string) bool { var keys []*packet.PrivateKey - for _, entity := range kr.entities { + for _, entity := range keyRing.entities { keys = append(keys, entity.PrivateKey) } var decryptError error @@ -468,7 +209,7 @@ func (kr *KeyRing) CheckPassphrase(passphrase string) bool { } // readFrom reads unarmored and armored keys from r and adds them to the keyring. -func (kr *KeyRing) readFrom(r io.Reader, armored bool) error { +func (keyRing *KeyRing) readFrom(r io.Reader, armored bool) error { var err error var entities openpgp.EntityList if armored { @@ -515,27 +256,27 @@ func (kr *KeyRing) readFrom(r io.Reader, armored bool) error { return errors.New("gopenpgp: key ring doesn't contain any key") } - kr.entities = append(kr.entities, entities...) + keyRing.entities = append(keyRing.entities, entities...) return nil } // BuildKeyRing reads keyring from binary data -func (pgp *GopenPGP) BuildKeyRing(binKeys []byte) (kr *KeyRing, err error) { - kr = &KeyRing{} +func (pgp *GopenPGP) BuildKeyRing(binKeys []byte) (keyRing *KeyRing, err error) { + keyRing = &KeyRing{} entriesReader := bytes.NewReader(binKeys) - err = kr.readFrom(entriesReader, false) + err = keyRing.readFrom(entriesReader, false) return } // BuildKeyRingNoError does not return error on fail -func (pgp *GopenPGP) BuildKeyRingNoError(binKeys []byte) (kr *KeyRing) { - kr, _ = pgp.BuildKeyRing(binKeys) +func (pgp *GopenPGP) BuildKeyRingNoError(binKeys []byte) (keyRing *KeyRing) { + keyRing, _ = pgp.BuildKeyRing(binKeys) return } // BuildKeyRingArmored reads armored string and returns keyring -func (pgp *GopenPGP) BuildKeyRingArmored(key string) (kr *KeyRing, err error) { +func (pgp *GopenPGP) BuildKeyRingArmored(key string) (keyRing *KeyRing, err error) { keyRaw, err := armorUtils.Unarmor(key) if err != nil { return nil, err @@ -545,36 +286,98 @@ func (pgp *GopenPGP) BuildKeyRingArmored(key string) (kr *KeyRing, err error) { return &KeyRing{entities: keyEntries}, err } -// UnmarshalJSON implements encoding/json.Unmarshaler. -func (kr *KeyRing) UnmarshalJSON(b []byte) (err error) { - kr.entities = nil - - keyObjs := []pgpKeyObject{} - if err = json.Unmarshal(b, &keyObjs); err != nil { - return +// ReadFromJSON reads multiple keys from a json array and fills the keyring +func (keyRing *KeyRing) ReadFromJSON(jsonData []byte) (err error) { + keyObjs, err := unmarshalJSON(jsonData) + if err != nil { + return err } - if len(keyObjs) == 0 { - return + return keyRing.newKeyRingFromPGPKeyObject(keyObjs) +} + +// UnlockJSONKeyRing reads keys from a JSON array, creates a newKeyRing, +// then tries to unlock them with the provided keyRing using the token in the structure. +// If the token is not available it will fall back to just reading the keys, and leave them locked. +func (keyRing *KeyRing) UnlockJSONKeyRing(jsonData []byte) (newKeyRing *KeyRing, err error) { + keyObjs, err := unmarshalJSON(jsonData) + newKeyRing = &KeyRing{} + err = newKeyRing.newKeyRingFromPGPKeyObject(keyObjs) + if err != nil { + return nil, err } + for _, ko := range keyObjs { + if ko.Token == nil || ko.Signature == nil { + continue + } + + message, err := NewPGPMessageFromArmored(*ko.Token) + if err != nil { + return nil, err + } + + signature, err := NewPGPSignatureFromArmored(*ko.Signature) + if err != nil { + return nil, err + } + + token, _, err := keyRing.Decrypt(message, nil, 0) + if err != nil { + return nil, err + } + + ver, err := keyRing.VerifyDetached(token, signature, 0) + if err != nil { + return nil, err + } + + if !ver.IsValid() { + return nil, errors.New("gopenpgp: unable to verify token") + } + + err = newKeyRing.Unlock(token.GetBinary()) + if err != nil { + return nil, errors.New("gopenpgp: wrong token") + } + } + + return newKeyRing, nil +} + +// newKeyRingFromPGPKeyObject fills a KeyRing given an array of pgpKeyObject +func (keyRing *KeyRing) newKeyRingFromPGPKeyObject(keyObjs []pgpKeyObject) error { + keyRing.entities = nil for i, ko := range keyObjs { if i == 0 { - kr.FirstKeyID = ko.ID + keyRing.FirstKeyID = ko.ID } - err = kr.readFrom(ko.PrivateKeyReader(), true) + err := keyRing.readFrom(ko.PrivateKeyReader(), true) if err != nil { return err } } - return nil } +// unmarshalJSON implements encoding/json.Unmarshaler. +func unmarshalJSON(jsonData []byte) ([]pgpKeyObject, error) { + keyObjs := []pgpKeyObject{} + if err := json.Unmarshal(jsonData, &keyObjs); err != nil { + return nil, err + } + + if len(keyObjs) == 0 { + return nil, errors.New("gopenpgp: no key found") + } + + return keyObjs, nil +} + // Identities returns the list of identities associated with this key ring. -func (kr *KeyRing) Identities() []*Identity { +func (keyRing *KeyRing) Identities() []*Identity { var identities []*Identity - for _, e := range kr.entities { + for _, e := range keyRing.entities { for _, id := range e.Identities { identities = append(identities, &Identity{ Name: id.UserId.Name, @@ -586,25 +389,25 @@ func (kr *KeyRing) Identities() []*Identity { } // KeyIds returns array of IDs of keys in this KeyRing. -func (kr *KeyRing) KeyIds() []uint64 { +func (keyRing *KeyRing) KeyIds() []uint64 { var res []uint64 - for _, e := range kr.entities { + for _, e := range keyRing.entities { res = append(res, e.PrimaryKey.KeyId) } return res } // ReadArmoredKeyRing reads an armored data into keyring. -func ReadArmoredKeyRing(r io.Reader) (kr *KeyRing, err error) { - kr = &KeyRing{} - err = kr.readFrom(r, true) +func ReadArmoredKeyRing(r io.Reader) (keyRing *KeyRing, err error) { + keyRing = &KeyRing{} + err = keyRing.readFrom(r, true) return } // ReadKeyRing reads an binary data into keyring. -func ReadKeyRing(r io.Reader) (kr *KeyRing, err error) { - kr = &KeyRing{} - err = kr.readFrom(r, false) +func ReadKeyRing(r io.Reader) (keyRing *KeyRing, err error) { + keyRing = &KeyRing{} + err = keyRing.readFrom(r, false) return } @@ -643,7 +446,7 @@ func FilterExpiredKeys(contactKeys []*KeyRing) (filteredKeys []*KeyRing, err err } if len(filteredKeys) == 0 && hasExpiredEntity { - return filteredKeys, errors.New("all contacts keys are expired") + return filteredKeys, errors.New("gopenpgp: all contacts keys are expired") } return filteredKeys, nil diff --git a/crypto/keyring_message.go b/crypto/keyring_message.go new file mode 100644 index 0000000..a38656e --- /dev/null +++ b/crypto/keyring_message.go @@ -0,0 +1,255 @@ +package crypto + +import ( + "bytes" + "crypto" + "errors" + "io" + "io/ioutil" + "math" + "time" + + "golang.org/x/crypto/openpgp" + pgpErrors "golang.org/x/crypto/openpgp/errors" + "golang.org/x/crypto/openpgp/packet" + + "github.com/ProtonMail/gopenpgp/constants" + "github.com/ProtonMail/gopenpgp/internal" +) + +// Encrypt encrypts a PlainMessage, outputs a PGPMessage. +// If an unlocked private key is also provided it will also sign the message. +// message : The plaintext input as a PlainMessage +// privateKey : (optional) an unlocked private keyring to include signature in the message +func (keyRing *KeyRing) Encrypt(message *PlainMessage, privateKey *KeyRing) (*PGPMessage, error) { + encrypted, err := asymmetricEncrypt(message.GetBinary(), keyRing, privateKey, true) + if err != nil { + return nil, err + } + + return NewPGPMessage(encrypted), nil +} + +// Decrypt decrypts encrypted string using pgp keys, returning a PlainMessage +// message : The encrypted input as a PGPMessage +// verifyKey : Public key for signature verification (optional) +// verifyTime : Time at verification (necessary only if verifyKey is not nil) +func (keyRing *KeyRing) Decrypt( + message *PGPMessage, verifyKey *KeyRing, verifyTime int64, +) (*PlainMessage, *Verification, error) { + decrypted, verifyStatus, err := asymmetricDecrypt(message.NewReader(), keyRing, verifyKey, verifyTime) + if err != nil { + return nil, nil, err + } + + return NewPlainMessage(decrypted), newVerification(verifyStatus), nil +} + +// SignDetached generates and returns a PGPSignature for a given PlainMessage +func (keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, error) { + signEntity, err := keyRing.GetSigningEntity() + if err != nil { + return nil, err + } + + config := &packet.Config{DefaultHash: crypto.SHA512, Time: pgp.getTimeGenerator()} + var outBuf bytes.Buffer + //sign bin + if err := openpgp.DetachSign(&outBuf, signEntity, message.NewReader(), config); err != nil { + return nil, err + } + + return NewPGPSignature(outBuf.Bytes()), nil +} + +// VerifyDetached verifies a PlainMessage with embedded a PGPSignature +// and returns a Verification with the filled Verified field. +func (keyRing *KeyRing) VerifyDetached( + message *PlainMessage, signature *PGPSignature, verifyTime int64, +) (*Verification, error) { + var err error + verifyVal, err := verifySignature( + keyRing.GetEntities(), + message.NewReader(), + signature.GetBinary(), + verifyTime, + ) + return newVerification(verifyVal), err +} + +// ------ INTERNAL FUNCTIONS ------- + +// Core for encryption+signature functions +func asymmetricEncrypt(data []byte, publicKey *KeyRing, privateKey *KeyRing, isBinary bool) ([]byte, error) { + var outBuf bytes.Buffer + var encryptWriter io.WriteCloser + var signEntity *openpgp.Entity + var err error + + if privateKey != nil && len(privateKey.entities) > 0 { + var err error + signEntity, err = privateKey.GetSigningEntity() + if err != nil { + return nil, err + } + } + + config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: pgp.getTimeGenerator()} + + hints := &openpgp.FileHints{ + IsBinary: isBinary, + FileName: "", + } + + if isBinary { + encryptWriter, err = openpgp.Encrypt(&outBuf, publicKey.entities, signEntity, hints, config) + } else { + encryptWriter, err = openpgp.EncryptText(&outBuf, publicKey.entities, signEntity, hints, config) + } + if err != nil { + return nil, err + } + + _, err = encryptWriter.Write(data) + encryptWriter.Close() + + if err != nil { + return nil, err + } + + return outBuf.Bytes(), nil +} + +// Core for decryption+verification functions +func asymmetricDecrypt( + encryptedIO io.Reader, privateKey *KeyRing, verifyKey *KeyRing, verifyTime int64, +) (plaintext []byte, verified int, err error) { + privKeyEntries := privateKey.GetEntities() + var additionalEntries openpgp.EntityList + + if verifyKey != nil { + additionalEntries = verifyKey.GetEntities() + } + + if additionalEntries != nil { + privKeyEntries = append(privKeyEntries, additionalEntries...) + } + + config := &packet.Config{Time: pgp.getTimeGenerator()} + + messageDetails, err := openpgp.ReadMessage(encryptedIO, privKeyEntries, nil, config) + if err != nil { + return nil, constants.SIGNATURE_NOT_SIGNED, err + } + + if verifyKey != nil { + processSignatureExpiration(messageDetails, verifyTime) + } + + body, err := ioutil.ReadAll(messageDetails.UnverifiedBody) + if err != nil { + return nil, constants.SIGNATURE_NOT_SIGNED, err + } + + if verifyKey != nil { + verifyStatus, verifyError := verifyDetailsSignature(messageDetails, verifyKey) + + if verifyStatus == constants.SIGNATURE_FAILED { + return nil, verifyStatus, errors.New(verifyError) + } + + return body, verifyStatus, nil + } + + return body, constants.SIGNATURE_NOT_SIGNED, nil +} + +// processSignatureExpiration handles signature time verification manually, so we can add a margin to the +// creationTime check. +func processSignatureExpiration(md *openpgp.MessageDetails, verifyTime int64) { + if md.SignatureError == pgpErrors.ErrSignatureExpired { + if verifyTime > 0 { + created := md.Signature.CreationTime.Unix() + expires := int64(math.MaxInt64) + if md.Signature.SigLifetimeSecs != nil { + expires = int64(*md.Signature.SigLifetimeSecs) + created + } + if created-internal.CreationTimeOffset <= verifyTime && verifyTime <= expires { + md.SignatureError = nil + } + } else { + // verifyTime = 0: time check disabled, everything is okay + md.SignatureError = nil + } + } +} + +// Verify signature from message details +func verifyDetailsSignature(md *openpgp.MessageDetails, verifierKey *KeyRing) (int, string) { + if md.IsSigned { + if md.SignedBy != nil { + if len(verifierKey.entities) > 0 { + matches := verifierKey.entities.KeysById(md.SignedByKeyId) + if len(matches) > 0 { + if md.SignatureError == nil { + return constants.SIGNATURE_OK, "" + } + return constants.SIGNATURE_FAILED, md.SignatureError.Error() + } + } else { + return constants.SIGNATURE_NO_VERIFIER, "" + } + } else { + return constants.SIGNATURE_NO_VERIFIER, "" + } + } + + return constants.SIGNATURE_NOT_SIGNED, "" +} + +// verifySignature verifies if a signature is valid with the entity list +func verifySignature( + pubKeyEntries openpgp.EntityList, origText io.Reader, signature []byte, verifyTime int64) (int, error) { + config := &packet.Config{} + if verifyTime == 0 { + config.Time = func() time.Time { + return time.Unix(0, 0) + } + } else { + config.Time = func() time.Time { + return time.Unix(verifyTime+internal.CreationTimeOffset, 0) + } + } + signatureReader := bytes.NewReader(signature) + + signer, err := openpgp.CheckDetachedSignature(pubKeyEntries, origText, signatureReader, config) + + if err == pgpErrors.ErrSignatureExpired && signer != nil { + if verifyTime > 0 { // if verifyTime = 0: time check disabled, everything is okay + // Maybe the creation time offset pushed it over the edge + // Retry with the actual verification time + config.Time = func() time.Time { + return time.Unix(verifyTime, 0) + } + + _, err = signatureReader.Seek(0, io.SeekStart) + if err != nil { + return constants.SIGNATURE_FAILED, err + } + + signer, err = openpgp.CheckDetachedSignature(pubKeyEntries, origText, signatureReader, config) + if err != nil { + return constants.SIGNATURE_FAILED, err + } + } + } + + if signer == nil { + return constants.SIGNATURE_FAILED, errors.New("gopenpgp: signer is empty") + } + // if signer.PrimaryKey.KeyId != signed.PrimaryKey.KeyId { + // // t.Errorf("wrong signer got:%x want:%x", signer.PrimaryKey.KeyId, 0) + // return false, errors.New("signer is nil") + // } + return constants.SIGNATURE_OK, nil +} diff --git a/crypto/keyring_test.go b/crypto/keyring_test.go index 21d8f1b..4a9aa2e 100644 --- a/crypto/keyring_test.go +++ b/crypto/keyring_test.go @@ -19,14 +19,17 @@ var testSymmetricKey = &SymmetricKey{ Algo: constants.AES256, } +var testWrongSymmetricKey = &SymmetricKey{ + Key: []byte("WrongPass"), + Algo: constants.AES256, +} + // Corresponding key in testdata/keyring_privateKey const testMailboxPassword = "apple" // Corresponding key in testdata/keyring_privateKeyLegacy // const testMailboxPasswordLegacy = "123" -const testToken = "d79ca194a22810a5363eeddfdef7dfbc327c6229" - var ( testPrivateKeyRing *KeyRing testPublicKeyRing *KeyRing @@ -50,43 +53,12 @@ func init() { panic(err) } - err = testPrivateKeyRing.Unlock([]byte(testMailboxPassword)) + err = testPrivateKeyRing.UnlockWithPassphrase(testMailboxPassword) if err != nil { panic(err) } } -func TestKeyRing_Decrypt(t *testing.T) { - decString, err := testPrivateKeyRing.DecryptMessageIfNeeded(readTestFile("keyring_token", false)) - if err != nil { - t.Fatal("Cannot decrypt token:", err) - } - - assert.Exactly(t, testToken, decString) -} - -func TestKeyRing_Encrypt(t *testing.T) { - encrypted, err := testPublicKeyRing.EncryptMessage(testToken, testPrivateKeyRing) - if err != nil { - t.Fatal("Cannot encrypt token:", err) - } - - // We can't just check if encrypted == testEncryptedToken - // Decrypt instead - ss, err := testPrivateKeyRing.DecryptMessage(encrypted) - if err != nil { - t.Fatal("Cannot decrypt token:", err) - } - - assert.Exactly(t, testToken, ss.String) - - signatureKeyRing := ss.Signed.KeyRing() - assert.Exactly(t, testPrivateKeyRing, signatureKeyRing) - - isby := ss.Signed.IsBy(testPublicKeyRing) - assert.Exactly(t, true, isby) -} - func TestKeyRing_ArmoredPublicKeyString(t *testing.T) { s, err := testPrivateKeyRing.GetArmoredPublicKey() if err != nil { @@ -134,10 +106,9 @@ func TestIdentities(t *testing.T) { assert.Exactly(t, identities[0], testIdentity) } - func TestFilterExpiredKeys(t *testing.T) { expiredKey, _ := ReadArmoredKeyRing(strings.NewReader(readTestFile("key_expiredKey", false))) - keys := []*KeyRing {testPrivateKeyRing, expiredKey} + keys := []*KeyRing{testPrivateKeyRing, expiredKey} unexpired, err := FilterExpiredKeys(keys) if err != nil { @@ -147,3 +118,69 @@ func TestFilterExpiredKeys(t *testing.T) { assert.Len(t, unexpired, 1) assert.Exactly(t, unexpired[0], testPrivateKeyRing) } + +func TestGetPublicKey(t *testing.T) { + publicKey, err := testPrivateKeyRing.GetPublicKey() + if err != nil { + t.Fatal("Expected no error while obtaining public key, got:", err) + } + + publicKeyRing, err := pgp.BuildKeyRing(publicKey) + if err != nil { + t.Fatal("Expected no error while creating public key ring, got:", err) + } + + privateFingerprint, err := testPrivateKeyRing.GetFingerprint() + if err != nil { + t.Fatal("Expected no error while extracting private fingerprint, got:", err) + } + + publicFingerprint, err := publicKeyRing.GetFingerprint() + if err != nil { + t.Fatal("Expected no error while extracting public fingerprint, got:", err) + } + + assert.Exactly(t, privateFingerprint, publicFingerprint) +} + +func TestKeyIds(t *testing.T) { + keyIDs := testPrivateKeyRing.KeyIds() + var assertKeyIDs = []uint64{4518840640391470884} + assert.Exactly(t, assertKeyIDs, keyIDs) +} + +func TestReadFromJson(t *testing.T) { + decodedKeyRing := &KeyRing{} + err = decodedKeyRing.ReadFromJSON([]byte(readTestFile("keyring_jsonKeys", false))) + if err != nil { + t.Fatal("Expected no error while reading JSON, got:", err) + } + + fingerprint, err := decodedKeyRing.GetFingerprint() + if err != nil { + t.Fatal("Expected no error while extracting fingerprint, got:", err) + } + + assert.Exactly(t, "91eacacca6837890efa7000470e569d5c182bef6", fingerprint) +} + +func TestUnlockJson(t *testing.T) { + userKeyRing, err := ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_userKey", false))) + if err != nil { + t.Fatal("Expected no error while creating keyring, got:", err) + } + + err = userKeyRing.UnlockWithPassphrase("testpassphrase") + if err != nil { + t.Fatal("Expected no error while creating keyring, got:", err) + } + + addressKeyRing, err := userKeyRing.UnlockJSONKeyRing([]byte(readTestFile("keyring_newJSONKeys", false))) + if err != nil { + t.Fatal("Expected no error while reading and decrypting JSON, got:", err) + } + + for _, e := range addressKeyRing.entities { + assert.Exactly(t, false, e.PrivateKey.Encrypted) + } +} diff --git a/crypto/message.go b/crypto/message.go index 0283993..6e10472 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -2,271 +2,326 @@ package crypto import ( "bytes" + "encoding/base64" "errors" "fmt" "io" "io/ioutil" - "math" - "time" + "regexp" + "runtime" - "golang.org/x/crypto/openpgp" - "golang.org/x/crypto/openpgp/armor" - pgpErrors "golang.org/x/crypto/openpgp/errors" - "golang.org/x/crypto/openpgp/packet" - - armorUtils "github.com/ProtonMail/gopenpgp/armor" + "github.com/ProtonMail/gopenpgp/armor" "github.com/ProtonMail/gopenpgp/constants" "github.com/ProtonMail/gopenpgp/internal" - "github.com/ProtonMail/gopenpgp/models" + + "golang.org/x/crypto/openpgp/packet" ) -// DecryptMessageStringKey decrypts encrypted message use private key (string) -// encryptedText : string armored encrypted -// privateKey : armored private use to decrypt message -// passphrase : match with private key to decrypt message -func (pgp *GopenPGP) DecryptMessageStringKey( - encryptedText, privateKey, passphrase string, -) (string, error) { - privKeyRaw, err := armorUtils.Unarmor(privateKey) - if err != nil { - return "", err - } - privKeyReader := bytes.NewReader(privKeyRaw) - privKeyEntries, err := openpgp.ReadKeyRing(privKeyReader) - if err != nil { - return "", err - } +// ---- MODELS ----- - return pgp.DecryptMessage(encryptedText, &KeyRing{entities: privKeyEntries}, passphrase) +// PlainMessage stores an unencrypted message. +type PlainMessage struct { + // The content of the message + Data []byte + // if the content is text or binary + TextType bool } -// DecryptMessage decrypts encrypted string using keyring -// encryptedText : string armored encrypted -// privateKey : keyring with private key to decrypt message, could be multiple keys -// passphrase : match with private key to decrypt message -func (pgp *GopenPGP) DecryptMessage(encryptedText string, privateKey *KeyRing, passphrase string) (string, error) { - md, err := decryptCore(encryptedText, nil, privateKey, passphrase, pgp.getTimeGenerator()) - if err != nil { - return "", err - } - - decrypted := md.UnverifiedBody - b, err := ioutil.ReadAll(decrypted) - if err != nil { - return "", err - } - - return string(b), nil +// Verification for a PlainMessage +type Verification struct { + // If the decoded message was correctly signed. See constants.SIGNATURE* for all values. + Verified int } -func decryptCore( - encryptedText string, additionalEntries openpgp.EntityList, - privKey *KeyRing, passphrase string, - timeFunc func() time.Time, -) (*openpgp.MessageDetails, error) { - rawPwd := []byte(passphrase) - if err := privKey.Unlock(rawPwd); err != nil { - err = fmt.Errorf("gopenpgp: cannot decrypt passphrase: %v", err) - return nil, err - } - - privKeyEntries := privKey.entities - - if additionalEntries != nil { - privKeyEntries = append(privKeyEntries, additionalEntries...) - } - - encryptedio, err := internal.Unarmor(encryptedText) - if err != nil { - return nil, err - } - - config := &packet.Config{Time: timeFunc} - - md, err := openpgp.ReadMessage(encryptedio.Body, privKeyEntries, nil, config) - return md, err +// PGPMessage stores a PGP-encrypted message. +type PGPMessage struct { + // The content of the message + Data []byte } -// DecryptMessageVerify decrypts message and verify the signature -// encryptedText: string armored encrypted -// verifierKey []byte: unarmored verifier keys -// privateKeyRing []byte: unarmored private key to decrypt. could be multiple -// passphrase: match with private key to decrypt message -func (pgp *GopenPGP) DecryptMessageVerify( - encryptedText string, verifierKey, privateKeyRing *KeyRing, - passphrase string, verifyTime int64, -) (*models.DecryptSignedVerify, error) { - out := &models.DecryptSignedVerify{} - out.Verify = failed +// PGPSignature stores a PGP-encoded detached signature. +type PGPSignature struct { + // The content of the signature + Data []byte +} - var verifierEntries openpgp.EntityList - if len(verifierKey.entities) == 0 { - out.Verify = noVerifier +// PGPSplitMessage contains a separate session key packet and symmetrically +// encrypted data packet. +type PGPSplitMessage struct { + DataPacket []byte + KeyPacket []byte +} + +// ---- GENERATORS ----- + +// NewPlainMessage generates a new binary PlainMessage ready for encryption, +// signature, or verification from the unencrypted binary data. +func NewPlainMessage(data []byte) *PlainMessage { + return &PlainMessage{ + Data: data, + TextType: false, } +} - md, err := decryptCore( - encryptedText, - verifierEntries, - privateKeyRing, - passphrase, - func() time.Time { return time.Unix(0, 0) }) // TODO: I doubt this time is correct +// NewPlainMessageFromString generates a new text PlainMessage, +// ready for encryption, signature, or verification from an unencrypted string. +func NewPlainMessageFromString(text string) *PlainMessage { + return &PlainMessage{ + Data: []byte(text), + TextType: true, + } +} +// newVerification returns a new instance of *Verification with the specified value +func newVerification(value int) *Verification { + return &Verification{ + Verified: value, + } +} + +// NewPGPMessage generates a new PGPMessage from the unarmored binary data. +func NewPGPMessage(data []byte) *PGPMessage { + return &PGPMessage{ + Data: data, + } +} + +// NewPGPMessageFromArmored generates a new PGPMessage from an armored string ready for decryption. +func NewPGPMessageFromArmored(armored string) (*PGPMessage, error) { + encryptedIO, err := internal.Unarmor(armored) if err != nil { return nil, err } - decrypted := md.UnverifiedBody - b, err := ioutil.ReadAll(decrypted) + message, err := ioutil.ReadAll(encryptedIO.Body) if err != nil { return nil, err } - processSignatureExpiration(md, verifyTime) + return &PGPMessage{ + Data: message, + }, nil +} - out.Plaintext = string(b) - if md.IsSigned { - if md.SignedBy != nil { - if len(verifierKey.entities) > 0 { - matches := verifierKey.entities.KeysById(md.SignedByKeyId) - if len(matches) > 0 { - if md.SignatureError == nil { - out.Verify = ok - } else { - out.Message = md.SignatureError.Error() - out.Verify = failed - } +// NewPGPSplitMessage generates a new PGPSplitMessage from the binary unarmored keypacket, +// datapacket, and encryption algorithm. +func NewPGPSplitMessage(keyPacket []byte, dataPacket []byte) *PGPSplitMessage { + return &PGPSplitMessage{ + KeyPacket: keyPacket, + DataPacket: dataPacket, + } +} + +// NewPGPSplitMessageFromArmored generates a new PGPSplitMessage by splitting an armored message into its +// session key packet and symmetrically encrypted data packet. +func NewPGPSplitMessageFromArmored(encrypted string) (*PGPSplitMessage, error) { + message, err := NewPGPMessageFromArmored(encrypted) + if err != nil { + return nil, err + } + + return message.SeparateKeyAndData(len(encrypted), -1) +} + +// NewPGPSignature generates a new PGPSignature from the unarmored binary data. +func NewPGPSignature(data []byte) *PGPSignature { + return &PGPSignature{ + Data: data, + } +} + +// NewPGPSignatureFromArmored generates a new PGPSignature from the armored string ready for verification. +func NewPGPSignatureFromArmored(armored string) (*PGPSignature, error) { + encryptedIO, err := internal.Unarmor(armored) + if err != nil { + return nil, err + } + + signature, err := ioutil.ReadAll(encryptedIO.Body) + if err != nil { + return nil, err + } + + return &PGPSignature{ + Data: signature, + }, nil +} + +// ---- MODEL METHODS ----- + +// GetBinary returns the binary content of the message as a []byte +func (msg *PlainMessage) GetBinary() []byte { + return msg.Data +} + +// GetString returns the content of the message as a string +func (msg *PlainMessage) GetString() string { + return string(msg.Data) +} + +// GetBase64 returns the base-64 encoded binary content of the message as a string +func (msg *PlainMessage) GetBase64() string { + return base64.StdEncoding.EncodeToString(msg.Data) +} + +// GetVerification returns the verification status of a verification, +// to use after the KeyRing.Decrypt* or KeyRing.Verify* functions. +// The int value returned is to compare to constants.SIGNATURE*. +func (ver *Verification) GetVerification() int { + return ver.Verified +} + +// IsValid returns true if the message is signed and the signature is valid. +// To use after the KeyRing.Decrypt* or KeyRing.Verify* functions. +func (ver *Verification) IsValid() bool { + return ver.Verified == constants.SIGNATURE_OK +} + +// NewReader returns a New io.Reader for the bianry data of the message +func (msg *PlainMessage) NewReader() io.Reader { + return bytes.NewReader(msg.GetBinary()) +} + +// IsText returns whether the message is a text message +func (msg *PlainMessage) IsText() bool { + return msg.TextType +} + +// IsBinary returns whether the message is a binary message +func (msg *PlainMessage) IsBinary() bool { + return !msg.TextType +} + +// GetBinary returns the unarmored binary content of the message as a []byte +func (msg *PGPMessage) GetBinary() []byte { + return msg.Data +} + +// NewReader returns a New io.Reader for the unarmored bianry data of the message +func (msg *PGPMessage) NewReader() io.Reader { + return bytes.NewReader(msg.GetBinary()) +} + +// GetArmored returns the armored message as a string +func (msg *PGPMessage) GetArmored() (string, error) { + return armor.ArmorWithType(msg.Data, constants.PGPMessageHeader) +} + +// GetDataPacket returns the unarmored binary datapacket as a []byte +func (msg *PGPSplitMessage) GetDataPacket() []byte { + return msg.DataPacket +} + +// GetKeyPacket returns the unarmored binary keypacket as a []byte +func (msg *PGPSplitMessage) GetKeyPacket() []byte { + return msg.KeyPacket +} + +// SeparateKeyAndData returns the first keypacket and the (hopefully unique) dataPacket (not verified) +func (msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) (outSplit *PGPSplitMessage, err error) { + // For info on each, see: https://golang.org/pkg/runtime/#MemStats + packets := packet.NewReader(bytes.NewReader(msg.Data)) + outSplit = &PGPSplitMessage{} + gcCounter := 0 + + // Store encrypted key and symmetrically encrypted packet separately + var encryptedKey *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: + if encryptedKey != nil && encryptedKey.Key != nil { + break + } + encryptedKey = p + + case *packet.SymmetricallyEncrypted: + // FIXME: add support for multiple keypackets + var b bytes.Buffer + // 2^16 is an estimation of the size difference between input and output, the size difference is most probably + // 16 bytes at a maximum though. + // We need to avoid triggering a grow from the system as this will allocate too much memory causing problems + // in low-memory environments + b.Grow(1<<16 + estimatedLength) + // empty encoded length + start byte + b.Write(make([]byte, 6)) + b.WriteByte(byte(1)) + actualLength := 1 + block := make([]byte, 128) + for { + n, err := p.Contents.Read(block) + if err == io.EOF { + break } + b.Write(block[:n]) + actualLength += n + gcCounter += n + if gcCounter > garbageCollector && garbageCollector > 0 { + runtime.GC() + gcCounter = 0 + } + } + + // quick encoding + symEncryptedData := b.Bytes() + if actualLength < 192 { + symEncryptedData[4] = byte(210) + symEncryptedData[5] = byte(actualLength) + symEncryptedData = symEncryptedData[4:] + } else if actualLength < 8384 { + actualLength = actualLength - 192 + symEncryptedData[3] = byte(210) + symEncryptedData[4] = 192 + byte(actualLength>>8) + symEncryptedData[5] = byte(actualLength) + symEncryptedData = symEncryptedData[3:] } else { - out.Verify = noVerifier + symEncryptedData[0] = byte(210) + symEncryptedData[1] = byte(255) + symEncryptedData[2] = byte(actualLength >> 24) + symEncryptedData[3] = byte(actualLength >> 16) + symEncryptedData[4] = byte(actualLength >> 8) + symEncryptedData[5] = byte(actualLength) } - } else { - out.Verify = noVerifier - } - } else { - out.Verify = notSigned - } - return out, nil -} -// processSignatureExpiration handles signature time verification manually, so we can add a margin to the -// creationTime check. -func processSignatureExpiration(md *openpgp.MessageDetails, verifyTime int64) { - if md.SignatureError == pgpErrors.ErrSignatureExpired { - if verifyTime > 0 { - created := md.Signature.CreationTime.Unix() - expires := int64(math.MaxInt64) - if md.Signature.SigLifetimeSecs != nil { - expires = int64(*md.Signature.SigLifetimeSecs) + created - } - if created-internal.CreationTimeOffset <= verifyTime && verifyTime <= expires { - md.SignatureError = nil - } - } else { - // verifyTime = 0: time check disabled, everything is okay - md.SignatureError = nil + outSplit.DataPacket = symEncryptedData } } + if decryptErr != nil { + return nil, fmt.Errorf("gopenpgp: cannot decrypt encrypted key packet: %v", decryptErr) + } + if encryptedKey == nil { + return nil, errors.New("gopenpgp: packets don't include an encrypted key packet") + } + + var buf bytes.Buffer + if err := encryptedKey.Serialize(&buf); err != nil { + return nil, fmt.Errorf("gopenpgp: cannot serialize encrypted key: %v", err) + } + outSplit.KeyPacket = buf.Bytes() + + return outSplit, nil } -// EncryptMessageWithPassword encrypts a plain text to pgp message with a password -// plainText string: clear text -// output string: armored pgp message -func (pgp *GopenPGP) EncryptMessageWithPassword(plainText string, password string) (string, error) { - var outBuf bytes.Buffer - w, err := armor.Encode(&outBuf, constants.PGPMessageHeader, internal.ArmorHeaders) - if err != nil { - return "", err - } - - config := &packet.Config{Time: pgp.getTimeGenerator()} - plaintext, err := openpgp.SymmetricallyEncrypt(w, []byte(password), nil, config) - if err != nil { - return "", err - } - message := []byte(plainText) - _, err = plaintext.Write(message) - if err != nil { - return "", err - } - err = plaintext.Close() - if err != nil { - return "", err - } - w.Close() - - return outBuf.String(), nil +// GetBinary returns the unarmored binary content of the signature as a []byte +func (msg *PGPSignature) GetBinary() []byte { + return msg.Data } -// EncryptMessage encrypts message with unarmored public key, if pass private key and passphrase will also sign -// the message -// publicKey : bytes unarmored public key -// plainText : the input -// privateKey : optional required when you want to sign -// passphrase : optional required when you pass the private key and this passphrase should decrypt the private key -// trim : bool true if need to trim new lines -func (pgp *GopenPGP) EncryptMessage( - plainText string, publicKey, privateKey *KeyRing, - passphrase string, trim bool, -) (string, error) { - if trim { - plainText = internal.TrimNewlines(plainText) - } - var outBuf bytes.Buffer - w, err := armor.Encode(&outBuf, constants.PGPMessageHeader, internal.ArmorHeaders) - if err != nil { - return "", err - } - - var signEntity *openpgp.Entity - - if len(passphrase) > 0 && len(privateKey.entities) > 0 { - var err error - signEntity, err = privateKey.GetSigningEntity(passphrase) - if err != nil { - return "", err - } - } - - ew, err := EncryptCore(w, publicKey.entities, signEntity, "", false, pgp.getTimeGenerator()) - if err != nil { - return "", err - } - - _, err = ew.Write([]byte(plainText)) - ew.Close() - w.Close() - return outBuf.String(), err +// GetArmored returns the armored signature as a string +func (msg *PGPSignature) GetArmored() (string, error) { + return armor.ArmorWithType(msg.Data, constants.PGPSignatureHeader) } -// DecryptMessageWithPassword decrypts a pgp message with a password -// encrypted string : armored pgp message -// output string : clear text -func (pgp *GopenPGP) DecryptMessageWithPassword(encrypted string, password string) (string, error) { - encryptedio, err := internal.Unarmor(encrypted) - if err != nil { - return "", err - } +// ---- UTILS ----- - firstTimeCalled := true - var prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) { - if firstTimeCalled { - firstTimeCalled = false - return []byte(password), nil - } - return nil, errors.New("password incorrect") - } - - config := &packet.Config{Time: pgp.getTimeGenerator()} - md, err := openpgp.ReadMessage(encryptedio.Body, nil, prompt, config) - if err != nil { - return "", err - } - - messageBuf := bytes.NewBuffer(nil) - _, err = io.Copy(messageBuf, md.UnverifiedBody) - if err != nil { - return "", err - } - - return messageBuf.String(), nil +// IsPGPMessage checks if data if has armored PGP message format. +func (pgp *GopenPGP) IsPGPMessage(data string) bool { + re := regexp.MustCompile("^-----BEGIN " + constants.PGPMessageHeader + "-----(?s:.+)-----END " + + constants.PGPMessageHeader + "-----") + return re.MatchString(data) } diff --git a/crypto/message_test.go b/crypto/message_test.go index e7758a6..ccc6e47 100644 --- a/crypto/message_test.go +++ b/crypto/message_test.go @@ -1,49 +1,104 @@ package crypto import ( - "github.com/stretchr/testify/assert" + "encoding/base64" "strings" "testing" + + "github.com/ProtonMail/gopenpgp/constants" + "github.com/stretchr/testify/assert" ) -func TestMessageEncryptionWithPassword(t *testing.T) { - var pgp = GopenPGP{} - - const password = "my secret password" +func TestTextMessageEncryptionWithSymmetricKey(t *testing.T) { + var message = NewPlainMessageFromString("The secret code is... 1, 2, 3, 4, 5") // Encrypt data with password - armor, err := pgp.EncryptMessageWithPassword("my message", password) + encrypted, err := testSymmetricKey.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } // Decrypt data with wrong password - _, err = pgp.DecryptMessageWithPassword(armor, "wrong password") + _, err = testWrongSymmetricKey.Decrypt(encrypted) assert.NotNil(t, err) + // Decrypt data with the good password - text, err := pgp.DecryptMessageWithPassword(armor, password) + decrypted, err := testSymmetricKey.Decrypt(encrypted) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, "my message", text) + assert.Exactly(t, message.GetString(), decrypted.GetString()) } -func TestMessageEncryption(t *testing.T) { - var pgp = GopenPGP{} - var ( - message = "plain text" - ) +func TestBinaryMessageEncryptionWithSymmetricKey(t *testing.T) { + binData, _ := base64.StdEncoding.DecodeString("ExXmnSiQ2QCey20YLH6qlLhkY3xnIBC1AwlIXwK/HvY=") + var message = NewPlainMessage(binData) - testPrivateKeyRing, err = ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_privateKey", false))) - _ = testPrivateKeyRing.Unlock([]byte(testMailboxPassword)) - testPublicKeyRing, _ = ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_publicKey", false))) - - armor, err := pgp.EncryptMessage(message, testPublicKeyRing, testPrivateKeyRing, testMailboxPassword, false) + // Encrypt data with password + encrypted, err := testSymmetricKey.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - plainText, err := pgp.DecryptMessage(armor, testPrivateKeyRing, testMailboxPassword) + // Decrypt data with wrong password + _, err = testWrongSymmetricKey.Decrypt(encrypted) + assert.NotNil(t, err) + + // Decrypt data with the good password + decrypted, err := testSymmetricKey.Decrypt(encrypted) if err != nil { t.Fatal("Expected no error when decrypting, got:", err) } - assert.Exactly(t, message, plainText) + assert.Exactly(t, message, decrypted) +} + +func TestTextMessageEncryption(t *testing.T) { + var message = NewPlainMessageFromString("plain text") + + testPublicKeyRing, _ = ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_publicKey", false))) + testPrivateKeyRing, err = ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_privateKey", false))) + + // Password defined in keyring_test + err = testPrivateKeyRing.UnlockWithPassphrase(testMailboxPassword) + if err != nil { + t.Fatal("Expected no error unlocking privateKey, got:", err) + } + + ciphertext, err := testPublicKeyRing.Encrypt(message, testPrivateKeyRing) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + + decrypted, ver, err := testPrivateKeyRing.Decrypt(ciphertext, testPublicKeyRing, pgp.GetUnixTime()) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + assert.Exactly(t, message.GetString(), decrypted.GetString()) + assert.Exactly(t, constants.SIGNATURE_OK, ver.GetVerification()) + assert.Exactly(t, true, ver.IsValid()) +} + +func TestBinaryMessageEncryption(t *testing.T) { + binData, _ := base64.StdEncoding.DecodeString("ExXmnSiQ2QCey20YLH6qlLhkY3xnIBC1AwlIXwK/HvY=") + var message = NewPlainMessage(binData) + + testPublicKeyRing, _ = ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_publicKey", false))) + testPrivateKeyRing, err = ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_privateKey", false))) + + // Password defined in keyring_test + err = testPrivateKeyRing.UnlockWithPassphrase(testMailboxPassword) + if err != nil { + t.Fatal("Expected no error unlocking privateKey, got:", err) + } + + ciphertext, err := testPublicKeyRing.Encrypt(message, testPrivateKeyRing) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + + decrypted, ver, err := testPrivateKeyRing.Decrypt(ciphertext, testPublicKeyRing, pgp.GetUnixTime()) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + assert.Exactly(t, message.GetBinary(), decrypted.GetBinary()) + assert.Exactly(t, constants.SIGNATURE_OK, ver.GetVerification()) + assert.Exactly(t, true, ver.IsValid()) } diff --git a/crypto/mime.go b/crypto/mime.go index 479ad51..a93b3b2 100644 --- a/crypto/mime.go +++ b/crypto/mime.go @@ -8,11 +8,52 @@ import ( "strings" gomime "github.com/ProtonMail/go-mime" + "github.com/ProtonMail/gopenpgp/constants" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" ) +// MIMECallbacks defines callback methods to process a MIME message. +type MIMECallbacks interface { + OnBody(body string, mimetype string) + OnAttachment(headers string, data []byte) + // Encrypted headers can be in an attachment and thus be placed at the end of the mime structure. + OnEncryptedHeaders(headers string) + OnVerified(verified int) + OnError(err error) +} + +// DecryptMIMEMessage decrypts a MIME message. +func (keyRing *KeyRing) DecryptMIMEMessage( + message *PGPMessage, verifyKey *KeyRing, callbacks MIMECallbacks, verifyTime int64, +) { + decryptedMessage, verification, err := keyRing.Decrypt(message, verifyKey, verifyTime) + if err != nil { + callbacks.OnError(err) + return + } + + body, verified, attachments, attachmentHeaders, err := pgp.parseMIME(decryptedMessage.GetString(), verifyKey) + if err != nil { + callbacks.OnError(err) + return + } + bodyContent, bodyMimeType := body.GetBody() + callbacks.OnBody(bodyContent, bodyMimeType) + for i := 0; i < len(attachments); i++ { + callbacks.OnAttachment(attachmentHeaders[i], []byte(attachments[i])) + } + callbacks.OnEncryptedHeaders("") + if verification.GetVerification() != constants.SIGNATURE_NOT_SIGNED { + callbacks.OnVerified(verification.GetVerification()) + } else { + callbacks.OnVerified(verified) + } +} + +// ----- INTERNAL FUNCTIONS ----- + func (pgp GopenPGP) parseMIME( mimeBody string, verifierKey *KeyRing, ) (*gomime.BodyCollector, int, []string, []string, error) { @@ -49,42 +90,3 @@ func (pgp GopenPGP) parseMIME( return body, verified, atts, attHeaders, err } - -// MIMECallbacks defines callback methods to process a MIME message. -type MIMECallbacks interface { - OnBody(body string, mimetype string) - OnAttachment(headers string, data []byte) - // Encrypted headers can be in an attachment and thus be placed at the end of the mime structure. - OnEncryptedHeaders(headers string) - OnVerified(verified int) - OnError(err error) -} - -// DecryptMIMEMessage decrypts a MIME message. -func (pgp *GopenPGP) DecryptMIMEMessage( - encryptedText string, verifierKey, privateKeyRing *KeyRing, - passphrase string, callbacks MIMECallbacks, verifyTime int64, -) { - decsignverify, err := pgp.DecryptMessageVerify(encryptedText, verifierKey, privateKeyRing, passphrase, verifyTime) - if err != nil { - callbacks.OnError(err) - return - } - - body, verified, attachments, attachmentHeaders, err := pgp.parseMIME(decsignverify.Plaintext, verifierKey) - if err != nil { - callbacks.OnError(err) - return - } - bodyContent, bodyMimeType := body.GetBody() - callbacks.OnBody(bodyContent, bodyMimeType) - for i := 0; i < len(attachments); i++ { - callbacks.OnAttachment(attachmentHeaders[i], []byte(attachments[i])) - } - callbacks.OnEncryptedHeaders("") - if decsignverify.Verify == notSigned { - callbacks.OnVerified(verified) - } else { - callbacks.OnVerified(decsignverify.Verify) - } -} diff --git a/crypto/mime_test.go b/crypto/mime_test.go index bf993df..26022c5 100644 --- a/crypto/mime_test.go +++ b/crypto/mime_test.go @@ -1,8 +1,8 @@ package crypto import ( - "github.com/stretchr/testify/assert" "github.com/ProtonMail/gopenpgp/internal" + "github.com/stretchr/testify/assert" "io/ioutil" "testing" ) @@ -48,18 +48,26 @@ func TestDecrypt(t *testing.T) { block, err = internal.Unarmor(readTestFile("mime_privateKey", false)) if err != nil { - t.Fatal("Cannot unarmor private key: ", err) + t.Fatal("Cannot unarmor private key:", err) } privateKeyUnarmored, _ := ioutil.ReadAll(block.Body) + privateKeyRing, _ := pgp.BuildKeyRing(privateKeyUnarmored) + err = privateKeyRing.UnlockWithPassphrase(privateKeyPassword) + if err != nil { + t.Fatal("Cannot unlock private key:", err) + } - pgp.DecryptMIMEMessage( - readTestFile("mime_pgpMessage", false), + message, err := NewPGPMessageFromArmored(readTestFile("mime_pgpMessage", false)) + if err != nil { + t.Fatal("Cannot decode armored message:", err) + } + + privateKeyRing.DecryptMIMEMessage( + message, pgp.BuildKeyRingNoError(publicKeyUnarmored), - pgp.BuildKeyRingNoError(privateKeyUnarmored), - privateKeyPassword, &callbacks, - pgp.GetTimeUnix()) + pgp.GetUnixTime()) } func TestParse(t *testing.T) { diff --git a/crypto/session.go b/crypto/session.go index 9979904..41aaaaa 100644 --- a/crypto/session.go +++ b/crypto/session.go @@ -6,26 +6,18 @@ import ( "fmt" "io" - "github.com/ProtonMail/gopenpgp/armor" - "github.com/ProtonMail/gopenpgp/constants" - "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" ) -// RandomToken generates a random token with the key size of the default cipher. +// RandomToken generated a random token of the same size of the keysize of the default cipher. func (pgp *GopenPGP) RandomToken() ([]byte, error) { config := &packet.Config{DefaultCipher: packet.CipherAES256} - keySize := config.DefaultCipher.KeySize() - symKey := make([]byte, keySize) - if _, err := io.ReadFull(config.Random(), symKey); err != nil { - return nil, err - } - return symKey, nil + return pgp.RandomTokenSize(config.DefaultCipher.KeySize()) } -// RandomTokenWith generates a random token with the given key size. -func (pgp *GopenPGP) RandomTokenWith(size int) ([]byte, error) { +// RandomTokenSize generates a random token with the specified key size +func (pgp *GopenPGP) RandomTokenSize(size int) ([]byte, error) { config := &packet.Config{DefaultCipher: packet.CipherAES256} symKey := make([]byte, size) if _, err := io.ReadFull(config.Random(), symKey); err != nil { @@ -34,12 +26,8 @@ func (pgp *GopenPGP) RandomTokenWith(size int) ([]byte, error) { return symKey, nil } -// GetSessionFromKeyPacket returns the decrypted session key from a binary -// public-key encrypted session key packet. -func (pgp *GopenPGP) GetSessionFromKeyPacket( - keyPacket []byte, privateKey *KeyRing, passphrase string, -) (*SymmetricKey, - error) { +// DecryptSessionKey returns the decrypted session key from a binary encrypted session key packet. +func (keyRing *KeyRing) DecryptSessionKey(keyPacket []byte) (*SymmetricKey, error) { keyReader := bytes.NewReader(keyPacket) packets := packet.NewReader(keyReader) @@ -50,15 +38,11 @@ func (pgp *GopenPGP) GetSessionFromKeyPacket( } ek := p.(*packet.EncryptedKey) - - rawPwd := []byte(passphrase) var decryptErr error - for _, key := range privateKey.entities.DecryptionKeys() { + for _, key := range keyRing.entities.DecryptionKeys() { priv := key.PrivateKey if priv.Encrypted { - if err := priv.Decrypt(rawPwd); err != nil { - continue - } + continue } if decryptErr = ek.Decrypt(priv, nil); decryptErr == nil { @@ -70,38 +54,22 @@ func (pgp *GopenPGP) GetSessionFromKeyPacket( return nil, decryptErr } - return getSessionSplit(ek) -} - -// KeyPacketWithPublicKey encrypts the session key with the armored publicKey -// and returns a binary public-key encrypted session key packet. -func (pgp *GopenPGP) KeyPacketWithPublicKey(sessionSplit *SymmetricKey, publicKey string) ([]byte, error) { - pubkeyRaw, err := armor.Unarmor(publicKey) - if err != nil { - return nil, err + if ek == nil { + return nil, errors.New("gopenpgp: unable to decrypt session key") } - return pgp.KeyPacketWithPublicKeyBin(sessionSplit, pubkeyRaw) + + return newSymmetricKeyFromEncrypted(ek) } -// KeyPacketWithPublicKeyBin encrypts the session key with the unarmored +// EncryptSessionKey encrypts the session key with the unarmored // publicKey and returns a binary public-key encrypted session key packet. -func (pgp *GopenPGP) KeyPacketWithPublicKeyBin(sessionSplit *SymmetricKey, publicKey []byte) ([]byte, error) { - publicKeyReader := bytes.NewReader(publicKey) - pubKeyEntries, err := openpgp.ReadKeyRing(publicKeyReader) - if err != nil { - return nil, err - } - +func (keyRing *KeyRing) EncryptSessionKey(sessionSplit *SymmetricKey) ([]byte, error) { outbuf := &bytes.Buffer{} cf := sessionSplit.GetCipherFunc() - if len(pubKeyEntries) == 0 { - return nil, errors.New("cannot set key: key ring is empty") - } - var pub *packet.PublicKey - for _, e := range pubKeyEntries { + for _, e := range keyRing.GetEntities() { for _, subKey := range e.Subkeys { if !subKey.Sig.FlagsValid || subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications { pub = subKey.PublicKey @@ -125,106 +93,9 @@ func (pgp *GopenPGP) KeyPacketWithPublicKeyBin(sessionSplit *SymmetricKey, publi return nil, errors.New("cannot set key: no public key available") } - if err = packet.SerializeEncryptedKey(outbuf, pub, cf, sessionSplit.Key, nil); err != nil { + if err := packet.SerializeEncryptedKey(outbuf, pub, cf, sessionSplit.Key, nil); err != nil { err = fmt.Errorf("gopenpgp: cannot set key: %v", err) return nil, err } return outbuf.Bytes(), nil } - -// GetSessionFromSymmetricPacket decrypts the binary symmetrically encrypted -// session key packet and returns the session key. -func (pgp *GopenPGP) GetSessionFromSymmetricPacket(keyPacket []byte, password string) (*SymmetricKey, error) { - keyReader := bytes.NewReader(keyPacket) - packets := packet.NewReader(keyReader) - - var symKeys []*packet.SymmetricKeyEncrypted - for { - - var p packet.Packet - var err error - if p, err = packets.Next(); err != nil { - break - } - - switch p := p.(type) { - case *packet.SymmetricKeyEncrypted: - symKeys = append(symKeys, p) - } - } - - pwdRaw := []byte(password) - // Try the symmetric passphrase first - if len(symKeys) != 0 && pwdRaw != nil { - for _, s := range symKeys { - key, cipherFunc, err := s.Decrypt(pwdRaw) - if err == nil { - return &SymmetricKey{ - Key: key, - Algo: getAlgo(cipherFunc), - }, nil - } - - } - } - - return nil, errors.New("password incorrect") -} - -// SymmetricKeyPacketWithPassword encrypts the session key with the password and -// returns a binary symmetrically encrypted session key packet. -func (pgp *GopenPGP) SymmetricKeyPacketWithPassword(sessionSplit *SymmetricKey, password string) ([]byte, error) { - outbuf := &bytes.Buffer{} - - cf := sessionSplit.GetCipherFunc() - - if len(password) <= 0 { - return nil, errors.New("password can't be empty") - } - - pwdRaw := []byte(password) - - config := &packet.Config{ - DefaultCipher: cf, - } - - err := packet.SerializeSymmetricKeyEncryptedReuseKey(outbuf, sessionSplit.Key, pwdRaw, config) - if err != nil { - return nil, err - } - return outbuf.Bytes(), nil -} - -func getSessionSplit(ek *packet.EncryptedKey) (*SymmetricKey, error) { - if ek == nil { - return nil, errors.New("can't decrypt key packet") - } - algo := constants.AES256 - for k, v := range symKeyAlgos { - if v == ek.CipherFunc { - algo = k - break - } - } - - if ek.Key == nil { - return nil, errors.New("can't decrypt key packet key is nil") - } - - return &SymmetricKey{ - Key: ek.Key, - Algo: algo, - }, nil -} - -func getAlgo(cipher packet.CipherFunction) string { - algo := constants.AES256 - for k, v := range symKeyAlgos { - if v == cipher { - algo = k - break - } - } - - return algo -} diff --git a/crypto/session_test.go b/crypto/session_test.go index 74b152a..f4e48cd 100644 --- a/crypto/session_test.go +++ b/crypto/session_test.go @@ -4,46 +4,44 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" "github.com/ProtonMail/gopenpgp/constants" + "github.com/stretchr/testify/assert" ) var testRandomToken []byte + func TestRandomToken(t *testing.T) { var err error testRandomToken, err = pgp.RandomToken() + if err != nil { + t.Fatal("Expected no error while generating default length random token, got:", err) + } + + token40, err := pgp.RandomTokenSize(40) if err != nil { t.Fatal("Expected no error while generating random token, got:", err) } assert.Len(t, testRandomToken, 32) -} - -func TestRandomTokenWith(t *testing.T) { - token, err := pgp.RandomTokenWith(40) - if err != nil { - t.Fatal("Expected no error while generating random token, got:", err) - } - - assert.Len(t, token, 40) + assert.Len(t, token40, 40) } func TestAsymmetricKeyPacket(t *testing.T) { symmetricKey := &SymmetricKey{ - Key: testRandomToken, + Key: testRandomToken, Algo: constants.AES256, } privateKeyRing, _ := ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_privateKey", false))) - publicKey, _ := testPrivateKeyRing.GetArmoredPublicKey() + _ = privateKeyRing.UnlockWithPassphrase(testMailboxPassword) - keyPacket, err := pgp.KeyPacketWithPublicKey(symmetricKey, publicKey) + keyPacket, err := privateKeyRing.EncryptSessionKey(symmetricKey) if err != nil { t.Fatal("Expected no error while generating key packet, got:", err) } // Password defined in keyring_test - outputSymmetricKey, err := pgp.GetSessionFromKeyPacket(keyPacket, privateKeyRing, testMailboxPassword) + outputSymmetricKey, err := privateKeyRing.DecryptSessionKey(keyPacket) if err != nil { t.Fatal("Expected no error while decrypting key packet, got:", err) } @@ -53,21 +51,21 @@ func TestAsymmetricKeyPacket(t *testing.T) { func TestSymmetricKeyPacket(t *testing.T) { symmetricKey := &SymmetricKey{ - Key: testRandomToken, + Key: testRandomToken, Algo: constants.AES256, } password := "I like encryption" - keyPacket, err := pgp.SymmetricKeyPacketWithPassword(symmetricKey, password) + keyPacket, err := symmetricKey.EncryptToKeyPacket(password) if err != nil { t.Fatal("Expected no error while generating key packet, got:", err) } - _, err = pgp.GetSessionFromSymmetricPacket(keyPacket, "Wrong password") - assert.EqualError(t, err, "password incorrect") + _, err = NewSymmetricKeyFromKeyPacket(keyPacket, "Wrong password") + assert.EqualError(t, err, "gopenpgp: password incorrect") - outputSymmetricKey, err := pgp.GetSessionFromSymmetricPacket(keyPacket, password) + outputSymmetricKey, err := NewSymmetricKeyFromKeyPacket(keyPacket, password) if err != nil { t.Fatal("Expected no error while decrypting key packet, got:", err) } diff --git a/crypto/sign_detached.go b/crypto/sign_detached.go deleted file mode 100644 index 062cd54..0000000 --- a/crypto/sign_detached.go +++ /dev/null @@ -1,128 +0,0 @@ -package crypto - -import ( - "bytes" - "errors" - "io" - "strings" - "time" - - "github.com/ProtonMail/gopenpgp/internal" - - "golang.org/x/crypto/openpgp" - errorsPGP "golang.org/x/crypto/openpgp/errors" - "golang.org/x/crypto/openpgp/packet" -) - -// SignTextDetached creates an armored detached signature of a given string. -func (kr *KeyRing) SignTextDetached(plainText string, passphrase string, trimNewlines bool) (string, error) { - signEntity, err := kr.GetSigningEntity(passphrase) - if err != nil { - return "", err - } - - config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: pgp.getTimeGenerator()} - - if trimNewlines { - plainText = internal.TrimNewlines(plainText) - } - - att := strings.NewReader(plainText) - - var outBuf bytes.Buffer - //SignText - if err := openpgp.ArmoredDetachSignText(&outBuf, signEntity, att, config); err != nil { - return "", err - } - - return outBuf.String(), nil -} - -// SignBinDetached creates an armored detached signature of binary data. -func (kr *KeyRing) SignBinDetached(plainData []byte, passphrase string) (string, error) { - //sign with 0x00 - signEntity, err := kr.GetSigningEntity(passphrase) - if err != nil { - return "", err - } - - config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: pgp.getTimeGenerator()} - - att := bytes.NewReader(plainData) - - var outBuf bytes.Buffer - //sign bin - if err := openpgp.ArmoredDetachSign(&outBuf, signEntity, att, config); err != nil { - return "", err - } - - return outBuf.String(), nil -} - -// VerifyTextDetachedSig verifies an armored detached signature given the plaintext as a string. -func (kr *KeyRing) VerifyTextDetachedSig( - signature string, plainText string, verifyTime int64, trimNewlines bool, -) (bool, error) { - if trimNewlines { - plainText = internal.TrimNewlines(plainText) - } - origText := bytes.NewReader(bytes.NewBufferString(plainText).Bytes()) - - return verifySignature(kr.GetEntities(), origText, signature, verifyTime) -} - -// VerifyBinDetachedSig verifies an armored detached signature given the plaintext as binary data. -func (kr *KeyRing) VerifyBinDetachedSig(signature string, plainData []byte, verifyTime int64) (bool, error) { - origText := bytes.NewReader(plainData) - - return verifySignature(kr.GetEntities(), origText, signature, verifyTime) -} - -// Internal -func verifySignature( - pubKeyEntries openpgp.EntityList, origText *bytes.Reader, - signature string, verifyTime int64, -) (bool, error) { - config := &packet.Config{} - if verifyTime == 0 { - config.Time = func() time.Time { - return time.Unix(0, 0) - } - } else { - config.Time = func() time.Time { - return time.Unix(verifyTime+internal.CreationTimeOffset, 0) - } - } - signatureReader := strings.NewReader(signature) - - signer, err := openpgp.CheckArmoredDetachedSignature(pubKeyEntries, origText, signatureReader, config) - - if err == errorsPGP.ErrSignatureExpired && signer != nil { - if verifyTime > 0 { // if verifyTime = 0: time check disabled, everything is okay - // Maybe the creation time offset pushed it over the edge - // Retry with the actual verification time - config.Time = func() time.Time { - return time.Unix(verifyTime, 0) - } - - _, err = signatureReader.Seek(0, io.SeekStart) - if err != nil { - return false, err - } - - signer, err = openpgp.CheckArmoredDetachedSignature(pubKeyEntries, origText, signatureReader, config) - if err != nil { - return false, err - } - } - } - - if signer == nil { - return false, errors.New("gopenpgp: signer is empty") - } - // if signer.PrimaryKey.KeyId != signed.PrimaryKey.KeyId { - // // t.Errorf("wrong signer got:%x want:%x", signer.PrimaryKey.KeyId, 0) - // return false, errors.New("signer is nil") - // } - return true, nil -} diff --git a/crypto/signature_collector.go b/crypto/signature_collector.go index 31127eb..37282a1 100644 --- a/crypto/signature_collector.go +++ b/crypto/signature_collector.go @@ -8,6 +8,7 @@ import ( "net/textproto" gomime "github.com/ProtonMail/go-mime" + "github.com/ProtonMail/gopenpgp/constants" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" @@ -51,7 +52,7 @@ func (sc *SignatureCollector) Accept( } } if len(multiparts) != 2 { - sc.verified = notSigned + sc.verified = constants.SIGNATURE_NOT_SIGNED // Invalid multipart/signed format just pass along _, err = ioutil.ReadAll(rawBody) if err != nil { @@ -96,12 +97,12 @@ func (sc *SignatureCollector) Accept( _, err = openpgp.CheckArmoredDetachedSignature(sc.keyring, rawBody, bytes.NewReader(buffer), sc.config) if err != nil { - sc.verified = failed + sc.verified = constants.SIGNATURE_FAILED } else { - sc.verified = ok + sc.verified = constants.SIGNATURE_OK } } else { - sc.verified = noVerifier + sc.verified = constants.SIGNATURE_NO_VERIFIER } return nil } diff --git a/crypto/signature_test.go b/crypto/signature_test.go index a88f5ac..8eb3ea3 100644 --- a/crypto/signature_test.go +++ b/crypto/signature_test.go @@ -5,87 +5,86 @@ import ( "strings" "testing" + "github.com/ProtonMail/gopenpgp/constants" "github.com/stretchr/testify/assert" ) -const signedPlainText = "Signed message" +const signedPlainText = "Signed message\n" const testTime = 1557754627 // 2019-05-13T13:37:07+00:00 var signingKeyRing *KeyRing -var signature, signatureBin string +var textSignature, binSignature *PGPSignature +var message *PlainMessage +var signatureTest = regexp.MustCompile("(?s)^-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$") +var signedMessageTest = regexp.MustCompile( + "(?s)^-----BEGIN PGP SIGNED MESSAGE-----.*-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$") func TestSignTextDetached(t *testing.T) { - signingKeyRing, err := ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_privateKey", false))) + var err error + + signingKeyRing, err = ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_privateKey", false))) if err != nil { t.Fatal("Cannot read private key:", err) } - signature, err = signingKeyRing.SignTextDetached(signedPlainText, "", true) - assert.EqualError(t, err, "gopenpgp: cannot sign message, unable to unlock signer key") - // Password defined in keyring_test - signature, err = signingKeyRing.SignTextDetached(signedPlainText, testMailboxPassword, true) - if err != nil { - t.Fatal("Cannot generate signature with encrypted key:", err) - } - - // Reset keyring to locked state - signingKeyRing, _ = ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_privateKey", false))) - // Password defined in keyring_test - err = signingKeyRing.Unlock([]byte(testMailboxPassword)) + err = signingKeyRing.UnlockWithPassphrase(testMailboxPassword) if err != nil { t.Fatal("Cannot decrypt private key:", err) } - signatureDec, err := signingKeyRing.SignTextDetached(signedPlainText, "", true) + message = NewPlainMessageFromString(signedPlainText) + textSignature, err = signingKeyRing.SignDetached(message) if err != nil { - t.Fatal("Cannot generate signature with decrypted key:", err) + t.Fatal("Cannot generate signature:", err) } - rTest := regexp.MustCompile("(?s)^-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$") - assert.Regexp(t, rTest, signature) - assert.Exactly(t, signatureDec, signature) + armoredSignature, err := textSignature.GetArmored() + if err != nil { + t.Fatal("Cannot armor signature:", err) + } + + assert.Regexp(t, signatureTest, armoredSignature) +} + +func TestVerifyTextDetachedSig(t *testing.T) { + signedMessage, err := signingKeyRing.VerifyDetached(message, textSignature, testTime) + if err != nil { + t.Fatal("Cannot verify plaintext signature:", err) + } + + assert.Exactly(t, constants.SIGNATURE_OK, signedMessage.GetVerification()) +} + +func TestVerifyTextDetachedSigWrong(t *testing.T) { + fakeMessage := NewPlainMessageFromString("wrong text") + signedMessage, err := signingKeyRing.VerifyDetached(fakeMessage, textSignature, testTime) + + assert.EqualError(t, err, "gopenpgp: signer is empty") + assert.Exactly(t, constants.SIGNATURE_FAILED, signedMessage.GetVerification()) } func TestSignBinDetached(t *testing.T) { var err error - // Reset keyring to locked state - signingKeyRing, _ = ReadArmoredKeyRing(strings.NewReader(readTestFile("keyring_privateKey", false))) - signatureBin, err = signingKeyRing.SignBinDetached([]byte(signedPlainText), "") - assert.EqualError(t, err, "gopenpgp: cannot sign message, unable to unlock signer key") - - // Password defined in keyring_test - signatureBin, err = signingKeyRing.SignBinDetached([]byte(signedPlainText), testMailboxPassword) + binSignature, err = signingKeyRing.SignDetached(NewPlainMessage([]byte(signedPlainText))) if err != nil { - t.Fatal("Cannot generate signature with encrypted key:", err) + t.Fatal("Cannot generate signature:", err) } - rTest := regexp.MustCompile("(?s)^-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$") - assert.Regexp(t, rTest, signatureBin) -} - -func TestVerifyTextDetachedSig(t *testing.T) { - verified, err := signingKeyRing.VerifyTextDetachedSig(signature, signedPlainText, testTime, true) + armoredSignature, err := binSignature.GetArmored() if err != nil { - t.Fatal("Cannot verify plaintext signature:", err) + t.Fatal("Cannot armor signature:", err) } - assert.Exactly(t, true, verified) -} - -func TestVerifyTextDetachedSigWrong(t *testing.T) { - verified, err := signingKeyRing.VerifyTextDetachedSig(signature, "wrong text", testTime, true) - - assert.EqualError(t, err, "gopenpgp: signer is empty") - assert.Exactly(t, false, verified) + assert.Regexp(t, signatureTest, armoredSignature) } func TestVerifyBinDetachedSig(t *testing.T) { - verified, err := signingKeyRing.VerifyBinDetachedSig(signatureBin, []byte(signedPlainText), testTime) + signedMessage, err := signingKeyRing.VerifyDetached(message, binSignature, testTime) if err != nil { t.Fatal("Cannot verify binary signature:", err) } - assert.Exactly(t, true, verified) + assert.Exactly(t, constants.SIGNATURE_OK, signedMessage.GetVerification()) } diff --git a/crypto/symmetrickey.go b/crypto/symmetrickey.go new file mode 100644 index 0000000..2526443 --- /dev/null +++ b/crypto/symmetrickey.go @@ -0,0 +1,224 @@ +package crypto + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + + "github.com/ProtonMail/gopenpgp/constants" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" +) + +// SymmetricKey stores a decrypted session key. +type SymmetricKey struct { + // The decrypted binary session key. + Key []byte + // The symmetric encryption algorithm used with this key. + Algo string +} + +var symKeyAlgos = map[string]packet.CipherFunction{ + constants.ThreeDES: packet.Cipher3DES, + constants.TripleDES: packet.Cipher3DES, + constants.CAST5: packet.CipherCAST5, + constants.AES128: packet.CipherAES128, + constants.AES192: packet.CipherAES192, + constants.AES256: packet.CipherAES256, +} + +// GetCipherFunc returns the cipher function corresponding to the algorithm used +// with this SymmetricKey. +func (symmetricKey *SymmetricKey) GetCipherFunc() packet.CipherFunction { + cf, ok := symKeyAlgos[symmetricKey.Algo] + if ok { + return cf + } + + panic("gopenpgp: unsupported cipher function: " + symmetricKey.Algo) +} + +// GetBase64Key returns the session key as base64 encoded string. +func (symmetricKey *SymmetricKey) GetBase64Key() string { + return base64.StdEncoding.EncodeToString(symmetricKey.Key) +} + +func NewSymmetricKeyFromToken(passphrase, algo string) *SymmetricKey { + return NewSymmetricKey([]byte(passphrase), algo) +} + +func NewSymmetricKey(key []byte, algo string) *SymmetricKey { + return &SymmetricKey{ + Key: key, + Algo: algo, + } +} + +func newSymmetricKeyFromEncrypted(ek *packet.EncryptedKey) (*SymmetricKey, error) { + var algo string + for k, v := range symKeyAlgos { + if v == ek.CipherFunc { + algo = k + break + } + } + if algo == "" { + return nil, fmt.Errorf("gopenpgp: unsupported cipher function: %v", ek.CipherFunc) + } + + return NewSymmetricKey(ek.Key, algo), nil +} + +// Encrypt encrypts a PlainMessage to PGPMessage with a SymmetricKey +// message : The plain data as a PlainMessage +// output : The encrypted data as PGPMessage +func (symmetricKey *SymmetricKey) Encrypt(message *PlainMessage) (*PGPMessage, error) { + encrypted, err := symmetricEncrypt(message.GetBinary(), symmetricKey) + if err != nil { + return nil, err + } + + return NewPGPMessage(encrypted), nil +} + +// Decrypt decrypts password protected pgp binary messages +// encrypted: PGPMessage +// output: PlainMessage +func (symmetricKey *SymmetricKey) Decrypt(message *PGPMessage) (*PlainMessage, error) { + decrypted, err := symmetricDecrypt(message.NewReader(), symmetricKey) + if err != nil { + return nil, err + } + + binMessage := NewPlainMessage(decrypted) + return binMessage, nil +} + +// NewSymmetricKeyFromKeyPacket decrypts the binary symmetrically encrypted +// session key packet and returns the session key. +func NewSymmetricKeyFromKeyPacket(keyPacket []byte, password string) (*SymmetricKey, error) { + keyReader := bytes.NewReader(keyPacket) + packets := packet.NewReader(keyReader) + + var symKeys []*packet.SymmetricKeyEncrypted + for { + + var p packet.Packet + var err error + if p, err = packets.Next(); err != nil { + break + } + + switch p := p.(type) { + case *packet.SymmetricKeyEncrypted: + symKeys = append(symKeys, p) + } + } + + pwdRaw := []byte(password) + // Try the symmetric passphrase first + if len(symKeys) != 0 && pwdRaw != nil { + for _, s := range symKeys { + key, cipherFunc, err := s.Decrypt(pwdRaw) + if err == nil { + return &SymmetricKey{ + Key: key, + Algo: getAlgo(cipherFunc), + }, nil + } + + } + } + + return nil, errors.New("gopenpgp: password incorrect") +} + +// EncryptToKeyPacket encrypts the session key with the password and +// returns a binary symmetrically encrypted session key packet. +func (symmetricKey *SymmetricKey) EncryptToKeyPacket(password string) ([]byte, error) { + outbuf := &bytes.Buffer{} + + cf := symmetricKey.GetCipherFunc() + + if len(password) <= 0 { + return nil, errors.New("gopenpgp: password can't be empty") + } + + pwdRaw := []byte(password) + + config := &packet.Config{ + DefaultCipher: cf, + } + + err := packet.SerializeSymmetricKeyEncryptedReuseKey(outbuf, symmetricKey.Key, pwdRaw, config) + if err != nil { + return nil, err + } + return outbuf.Bytes(), nil +} + +// ----- INTERNAL FUNCTIONS ------ + +func symmetricEncrypt(message []byte, sk *SymmetricKey) ([]byte, error) { + var outBuf bytes.Buffer + + config := &packet.Config{ + Time: pgp.getTimeGenerator(), + DefaultCipher: sk.GetCipherFunc(), + } + + encryptWriter, err := openpgp.SymmetricallyEncrypt(&outBuf, sk.Key, nil, config) + if err != nil { + return nil, err + } + _, err = encryptWriter.Write(message) + encryptWriter.Close() + + if err != nil { + return nil, err + } + + return outBuf.Bytes(), nil +} + +func symmetricDecrypt(encryptedIO io.Reader, sk *SymmetricKey) ([]byte, error) { + firstTimeCalled := true + var prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) { + if firstTimeCalled { + firstTimeCalled = false + return sk.Key, nil + } + return nil, errors.New("gopenpgp: wrong password in symmetric decryption") + } + + config := &packet.Config{ + Time: pgp.getTimeGenerator(), + } + md, err := openpgp.ReadMessage(encryptedIO, nil, prompt, config) + if err != nil { + return nil, err + } + + messageBuf := bytes.NewBuffer(nil) + _, err = io.Copy(messageBuf, md.UnverifiedBody) + if err != nil { + return nil, err + } + + return messageBuf.Bytes(), nil +} + +func getAlgo(cipher packet.CipherFunction) string { + algo := constants.AES256 + for k, v := range symKeyAlgos { + if v == cipher { + algo = k + break + } + } + + return algo +} diff --git a/crypto/testdata/keyring_jsonKeys b/crypto/testdata/keyring_jsonKeys new file mode 100644 index 0000000..50bc12d --- /dev/null +++ b/crypto/testdata/keyring_jsonKeys @@ -0,0 +1,20 @@ +[ + { + "ID": "qpRBAbx3s0gw5fdpS4VHPmxFZG42NPWGZW2g0fDhwHeO7duCQgOdnxcSDUxTwfLjfpeIHuPzUQPKeEN8q4TsRA==", + "Primary": 1, + "Flags": 3, + "Version": 3, + "Activation": null, + "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\n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\nSmpbVZj1NSvPN4xFOnZaytD99Yt/8ZFonY9Rjg==\n=5ACd\n-----END PGP PRIVATE KEY BLOCK-----\n", + "Token": null + }, + { + "ID": "88xeHAObMEZp8b15R3643qJFbRoAXPm-FNuHxWO_VBVEqm294wuKCbIdoytQwHq2uwTxAXB-c4Jh0R47F34qig==", + "Primary": 0, + "Flags": 3, + "Version": 3, + "Activation": null, + "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nxYYEXMcTAxYJKwYBBAHaRw8BAQdAjYhUufSrYtCsWhJMjCaEkdJb1Zuauh8P\nS0WipcZO2dD+CQMIxbf+ePNBNFVgXUTPHWmy2PpBS2FHQWgONx0UQLRi+JyT\nMJmxS/CToS54Eo0sWEjaZROZr8f5smCgBZqZsRWv4CA35qBG/RxJMkVH2lKQ\nuM0zZWxlY3Ryb25AcHJvdG9ubWFpbC5ibHVlIDxlbGVjdHJvbkBwcm90b25t\nYWlsLmJsdWU+wncEEBYKAB8FAlzaAdIGCwkHCAMCBBUICgIDFgIBAhkBAhsD\nAh4BAAoJEP7+7R3IrxGwxh0BAKbR76lG0OH83tI5quvyNf7yt+ck/uV8MrPZ\ntcBLHwO3AQCd5CHU3Rx7Huw0QsVkJYr7t3JEYfSaTK2wTxUFKudSDceLBFzH\nEwMSCisGAQQBl1UBBQEBB0Dijq86qjP8AEv98lTBqw69HAAxo0S4Eqh1P4qU\n1irhLwMBCAf+CQMIXvD746WZtFtglCNKwMqcHMFJ0sadSDo6ntYdiQwSM42E\n0jdfdM+JUIfDw9cOXflCcdW8yUJSRaBL1BXwtCcr686pkPZ/79qxuqYY6+Nq\nzMJhBBgWCAAJBQJc2gHSAhsMAAoJEP7+7R3IrxGwKzUA/j/00OybZkE3oTDO\n2fLjBZtlKa7T1n4+vZb+T8dvl2wnAP9Ln3wTzY9oXN/n/WgSi8Q5iT2to7zx\n25aU/PlFqHQmBQ==\n=3PyZ\n-----END PGP PRIVATE KEY BLOCK-----\n", + "Token": null + } +] diff --git a/crypto/testdata/keyring_newJSONKeys b/crypto/testdata/keyring_newJSONKeys new file mode 100644 index 0000000..21ad185 --- /dev/null +++ b/crypto/testdata/keyring_newJSONKeys @@ -0,0 +1,12 @@ +[ + { + "ID": "hKRtZeTDhvzfAaycb5BOVx6Y3hc3gs4QvET8H_YZBTwAQBPp3h6FI4nnkJePCYuM9CG0zf7TQzOJeB2rPi0YmQ==", + "Primary": 1, + "Flags": 3, + "Version": 3, + "Activation": null, + "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nxcMGBFzGzj4BCADh9uuhG3mDhlbFMgQpvP6AReyJYa6KSV9a6AJsNI0/DkVz\nFenIodav/r4RBgSoblqxCa/QH6galbX0eB8Vtxx4xTXCcHHZzTM9oAb4s8r6\ndGchNsSkoLspVwmc2fYfDRCTcapmeza7Jym8gD9wFUSWSwpC49jDeT6VIcOd\n+qNXgIJX1V36BOAB53lKenMYnxfzzAyJDeL/sQ/SWE9h/X+wGdkVmk7gk3pa\nQ9URoxAf7MO808hHyQYcjTV4tOHD/9k+AZC6sqmr2KAefYCxCMoCDj1wQp3J\nN6MfK2Zvc1r5KFQM2YzUXrMNrycAjo7gGeZnsvz758Q+8ouSP7iXGRl/ABEB\nAAH+CQMIQt3QQIJZYjRgTVgIQNSEkhMn4GJ5lQQPBXD0/YyK3xP0bm/bsX8S\nz9dAm1nkP0l8Q9z3lR6OcAX2O/KsqJdQGmJEZAaadz2je+EEWo1FDYaL76E6\n6z+AuteP8UtP61jBt3pTfIlhW8o7o2SlM1lvytpvc5FQplZ/iUuwfsIltCiD\n9VDkprNOVrRsjoh8BV4TrrIvU8qga58Aeg7SjOmm+3oMZ7yPTYirr11Tx/m3\nj1uzdEDfiPk4LmvlzSsWwKZuy6fSul+n92+9qN81wmdts/I2ucuKvOINZim4\nlk/p2AOsPjWpAgkefpTVLZnu2IH+VAyaXt1Fl84badXx4N921nPs7ova1Ud5\n2RddBc7b/01DtOyBSWDoNskLGpsc2mqz9kdkwwQKNjChzZc9nmY9M+AIfgT3\n+2DSQIuoJYPX69DKi/bZDwRzoHmiwiHT6Us7qxd6kD1dzCIHTptxwZQp4Tow\nnN6lmtK4S6O2B47+ROn8s0N+EH9GR8F6mvOTayNLH5yicpR3M4Of0ClvFa0G\n+JUKBXIQXUvF03G3nTPU17nLibC81UmbK3zobfbrLfuU2gU+sY+OfE1E/+GO\nSFpZcrkoRqRr39CfLkLk+GjU7RCLNddb5LxgaurVZo5h0Y7Rr8VvOQMWjjl/\nvTAG7gU/HtXi24TijNC0fP6j9w43K4b6t1SZYn5us7RRlFlWGKMSPf5Q9j6/\nryo3xULUQv0lTCQPtfQQ5UWE5ZpQ4Kjt/k5+/YtfuOcMbrDU4qa+H+rrisBu\nko7f4Wn0iYjRwRIuWh1NfUM3rIbNhq7/wonasEFOeFdPwprzMaawx3rL0Pq5\n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\nV0GB3IX8qBHjAfPmqN9JPxBIn3/6ag==\n=U1yD\n-----END PGP PRIVATE KEY BLOCK-----\n", + "Token": "-----BEGIN PGP MESSAGE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwcBMAxW/uQhm1PKxAQf8D4BAoXqnYgoTr7jEiabP9NkeuD6Z88fZDgUzXte5\n6aYMmCtPr0Vef1eDl2x0S0q/YJgR+A5Icmjxk+jG9nMSnRPlAozJfTCXu/Oy\nfkI4CcClGQv7U1EzfknBVNEKmuPO8XlEkGZ5lW/TzWk8ZtQqJsVuqZlZ5qQE\n/p8FNYEZ5RHPhhKlotA+T6XKh23z9mcN/JNEsSNcx+guERbivtTwnd5JpxQJ\nMNWzUYh+K0W7LgxoAo2jevDd4CZ/0sAAViIl+QrShodseV9agbldGUUoIMzn\nHUuIV6VYi7X82eKQXDInrtPc9IHekbDDFoncLnsGGrEqD/8O/qOHHOPwNwU+\n+cHATAOUTDv/ccWq1QEIALl8SNOQBmCuanAceUTSwCkM1fC3Ddqa8ZmnMMyG\nnDWDI7XkFU31CO10lN20/kAWjdN2073B/NT+17cp8fCqbQ1pAFJHpabdqmbI\ntm3pCC5M6otTN+MhjgFYcBuxo0rq9qtuEzz5j4Ub9MIIJTurUHMEPMI462Dc\ndK/d8BvDkU7q67Lkp65vpe9e/pv0lMMrQjdohnTHNgbZhbI/Z5LU0ApD//Ye\ncSC26BRUMJITiGKb9pKGAi0/ig0jJfzykgEarzOsY/v0W7016AMka+NsHuNc\n7Qfyg2LApF5s9Z6aK+Uy851haUk+p+abjBdcWACziAmimVGjlRYm49ra4UJ7\nc6LSeAElM584e4Z3VnqajAJWbmWt2atgBQmcwEBBsdNAtzIagNydMbBBIELA\n5wGUbMXfQxrMOo/Mdac/5EMTHT0/kVQNIBHtpX5SknWvbc5DjIHoH3+BbL87\neaxhKW94hMubKNKT3dbm4PKHtsMiS3TGkFZ3GjvA0Cy/Kw==\n=b1N8\n-----END PGP MESSAGE-----\n", + "Signature": "-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwsBcBAEBCAAGBQJcxtSkAAoJEGfweL9m9sQefz8IAJhpEOvG7d+PVgq3bEL+\nXzY8yTUKB1ZXEMbcR/uHUWSfu0F8zy4CNtSG5HUVjNxx1xzzJVJcxWB/ljO+\n/bJSFOFexTyNh8i/xU+CiBfm5RhAFTYF9xFwfD3LKp5gaalJAhWhArk1/Wuh\nWTDpFpk39uzBRKNwcnSgiJYPxOjAZxj+w/hhHPwmco5cUwMiMR5MNrfzKf+x\nwX3Cfs9fsiiCDzohBzbK0FFsMnJ8aXNVsDBjEA1KrB7sdyaf8FnaM0RsFRDb\njxtIgfcFBbyNJh4414Unt4AYTIrIWhK4OOXI3AfsJy8p6KRBKQUcUkKcDxab\nPTXOPZsZ+UgQ5MevyVnP1zfCwFwEAQEIAAYFAlzG1KQACgkQm1DMMBzxetx/\nPwf9F04uHtix0zDpP1IvG4VYlor4rjYTdfXqxxiFXHO6MZXJoigS1E71E8r4\nsqZ6PoQ5/xCj2A01KRhuF1Bon3mEZEwaIUuBqTV91sLsVWfoxgyPAYpr6gK/\n1W9JhVNNrVRMGox42LQUjyiq0ESrfWmqC8SuMfZMoUoBZycHicA50RbyOUnT\nLO57ArL3JIVmYtyosaXM3idzmNHmaXSkcGt4cvTVysJZQrneaxmikfm5CH2O\n1z2goLBNnzsbRionoV6gCukZOiM/d14yiyeYtsFJ7u/vodkI5y0M0sF8VnN/\njL9keP4ZpHiJ4MBd6tyIH0pLueDRlurFL3fcHsEzD5SrOA==\n=wGCk\n-----END PGP SIGNATURE-----\n" + } +] diff --git a/crypto/testdata/keyring_userKey b/crypto/testdata/keyring_userKey new file mode 100644 index 0000000..abee140 --- /dev/null +++ b/crypto/testdata/keyring_userKey @@ -0,0 +1,62 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: OpenPGP.js v4.4.5 +Comment: testpassphrase + +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 +uaSC3IcBmBsj1fNb4eYXElILjQ== +=fMOl +-----END PGP PRIVATE KEY BLOCK----- diff --git a/crypto/testdata/message_expired b/crypto/testdata/message_expired new file mode 100644 index 0000000..d1f7b35 --- /dev/null +++ b/crypto/testdata/message_expired @@ -0,0 +1,18 @@ +-----BEGIN PGP MESSAGE----- +Comment: GPGTools - https://gpgtools.org + +owEBWwKk/ZANAwAKAeyAexA3gWZ0AawUYgloZWxsby50eHRaX2WpaGVsbG+JAjME +AAEKAB0WIQTxcIn7ZRrhwd51IZbsgHsQN4FmdAUCWl9lqQAKCRDsgHsQN4FmdCln +D/44x1bcrOXg+DbRStSrC75wFa+cvPEmaTZyqN6d7qlQCMxOcPlq6lbZ74QWfEq7 +i1ZYHp4AU8jALw0QqBQQE5FvABleQKpVfY22s83Bqy+P0DB9ntpD+t+oZrxGCLmL +MbZJNFnGro48gHt+/OQKLuftiVwE2opHfgogVKNL74FmYA0hMItdzpn4OPNFkP8t +Iq/m0hkXlTAKqBPITVLv1FN16v+Sm1iC317eP/HOTYqVZdJN3svVF8ZBfg29a8p6 +6nl67fZhXgrt0OB6KSNIZEwMTWjFAqi365mtTssqAA0un94+cQ/WvAC5QcMM8g5S +i3G7vny9AsXor+GDU1z7UDWs3wBV4mVRdj7bBIS6PK+6oe012aNpRObcI2bU2BT/ +H/7uHZWfwEmpfvH9RVZgoeETA3vSx7MDrNyDt3gwv2hxOHEd7nnVQ3EKG33173o1 +/5/oEmn2USujKGhHJ2Zo3aWNRuUWZlvBaYw+PwB2R0UiuJbi0KofNYPssNdpw4sg +Qs7Nb2/Ilo1zn5bDh+WDrUrn6zHKAfBytBPpwPFWPZ8W10HUlC5vMZSKH5/UZhj5 +kLlUC1zKjFPpRhO27ImTJuImil4lR2/CFjB1duG3JGJQaYIq8RFJOjvTVY29wl0i +pFy6y1Ofv2lLHB9K7N7dvvee2nvpUMkLEL52oFQ6Jc7sdg== +=Q4tk +-----END PGP MESSAGE----- diff --git a/crypto/time.go b/crypto/time.go index 263eaa6..5ce642d 100644 --- a/crypto/time.go +++ b/crypto/time.go @@ -17,8 +17,8 @@ func (pgp *GopenPGP) UpdateTime(newTime int64) { pgp.latestClientTime = time.Now() } -// GetTimeUnix gets latest cached time -func (pgp *GopenPGP) GetTimeUnix() int64 { +// GetUnixTime gets latest cached time +func (pgp *GopenPGP) GetUnixTime() int64 { return pgp.getNow().Unix() } @@ -27,6 +27,9 @@ func (pgp *GopenPGP) GetTime() time.Time { return pgp.getNow() } +// ----- INTERNAL FUNCTIONS ----- + +// getNow returns current time func (pgp *GopenPGP) getNow() time.Time { if pgp.latestServerTime > 0 && !pgp.latestClientTime.IsZero() { // Until is monotonic, it uses a monotonic clock in this case instead of the wall clock @@ -37,6 +40,7 @@ func (pgp *GopenPGP) getNow() time.Time { return time.Now() } +// getTimeGenerator Returns a time generator function func (pgp *GopenPGP) getTimeGenerator() func() time.Time { return func() time.Time { return pgp.getNow() diff --git a/helper/base_test.go b/helper/base_test.go new file mode 100644 index 0000000..4791fda --- /dev/null +++ b/helper/base_test.go @@ -0,0 +1,22 @@ +package helper + +import ( + "io/ioutil" + "strings" +) + +var err error + +func readTestFile(name string, trimNewlines bool) string { + data, err := ioutil.ReadFile("../crypto/testdata/" + name) + if err != nil { + panic(err) + } + if trimNewlines { + return strings.TrimRight(string(data), "\n") + } + return string(data) +} + +// Corresponding key in ../crypto/testdata/keyring_privateKey +const testMailboxPassword = "apple" diff --git a/helper/cleartext.go b/helper/cleartext.go new file mode 100644 index 0000000..b5cfb72 --- /dev/null +++ b/helper/cleartext.go @@ -0,0 +1,82 @@ +package helper + +import ( + "errors" + "strings" + + "github.com/ProtonMail/gopenpgp/armor" + "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/internal" +) + +// SignCleartextMessageArmored signs text given a private key and its passphrase, canonicalizes and trims the newlines, +// and returns the PGP-compliant special armoring +func SignCleartextMessageArmored(privateKey, passphrase, text string) (string, error) { + signingKeyRing, err := pgp.BuildKeyRingArmored(privateKey) + if err != nil { + return "", err + } + + err = signingKeyRing.UnlockWithPassphrase(passphrase) + if err != nil { + return "", err + } + + return SignCleartextMessage(signingKeyRing, text) +} + +// VerifyCleartextMessageArmored verifies PGP-compliant armored signed plain text given the public key +// and returns the text or err if the verification fails +func VerifyCleartextMessageArmored(publicKey, armored string, verifyTime int64) (string, error) { + verifyKeyRing, err := pgp.BuildKeyRingArmored(publicKey) + if err != nil { + return "", err + } + + return VerifyCleartextMessage(verifyKeyRing, armored, verifyTime) +} + +// SignCleartextMessage signs text given a private keyring, canonicalizes and trims the newlines, +// and returns the PGP-compliant special armoring +func SignCleartextMessage(keyRing *crypto.KeyRing, text string) (string, error) { + text = canonicalizeAndTrim(text) + message := crypto.NewPlainMessageFromString(text) + + signature, err := keyRing.SignDetached(message) + if err != nil { + return "", err + } + + return armor.ArmorClearSignedMessage(message.GetBinary(), signature.GetBinary()) +} + +// VerifyCleartextMessage verifies PGP-compliant armored signed plain text given the public keyring +// and returns the text or err if the verification fails +func VerifyCleartextMessage(keyRing *crypto.KeyRing, armored string, verifyTime int64) (string, error) { + text, signatureData, err := armor.ReadClearSignedMessage(armored) + if err != nil { + return "", err + } + + message := crypto.NewPlainMessageFromString(text) + signature := crypto.NewPGPSignature(signatureData) + ver, err := keyRing.VerifyDetached(message, signature, verifyTime) + if err != nil { + return "", err + } + + if !ver.IsValid() { + return "", errors.New("gopenpgp: unable to verify attachment") + } + + return message.GetString(), nil +} + +// ----- INTERNAL FUNCTIONS ----- + +// canonicalizeAndTrim alters a string canonicalizing and trimming the newlines +func canonicalizeAndTrim(text string) string { + text = internal.TrimNewlines(text) + text = strings.Replace(strings.Replace(text, "\r\n", "\n", -1), "\n", "\r\n", -1) + return text +} diff --git a/helper/cleartext_test.go b/helper/cleartext_test.go new file mode 100644 index 0000000..de04f93 --- /dev/null +++ b/helper/cleartext_test.go @@ -0,0 +1,45 @@ +package helper + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" +) + +const signedPlainText = "Signed message\n" +const testTime = 1557754627 // 2019-05-13T13:37:07+00:00 +var signedMessageTest = regexp.MustCompile( + "(?s)^-----BEGIN PGP SIGNED MESSAGE-----.*-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$") + +func TestSignClearText(t *testing.T) { + // Password defined in base_test + armored, err := SignCleartextMessageArmored( + readTestFile("keyring_privateKey", false), + testMailboxPassword, + signedPlainText, + ) + + if err != nil { + t.Fatal("Cannot armor message:", err) + } + + assert.Regexp(t, signedMessageTest, armored) + + verified, err := VerifyCleartextMessageArmored( + readTestFile("keyring_publicKey", false), + armored, + pgp.GetUnixTime(), + ) + if err != nil { + t.Fatal("Cannot verify message:", err) + } + + assert.Exactly(t, canonicalizeAndTrim(signedPlainText), verified) +} + +func TestMessageCanonicalizeAndTrim(t *testing.T) { + text := "Hi \ntest!\r\n\n" + canon := canonicalizeAndTrim(text) + assert.Exactly(t, "Hi\r\ntest!\r\n\r\n", canon) +} diff --git a/helper/helper.go b/helper/helper.go new file mode 100644 index 0000000..f74f06a --- /dev/null +++ b/helper/helper.go @@ -0,0 +1,258 @@ +package helper + +import ( + "errors" + + "github.com/ProtonMail/gopenpgp/constants" + "github.com/ProtonMail/gopenpgp/crypto" +) + +var pgp = crypto.GetGopenPGP() + +// EncryptMessageWithToken encrypts a string with a passphrase using AES256 +func EncryptMessageWithToken( + passphrase, plaintext string, +) (ciphertext string, err error) { + return EncryptMessageWithTokenAlgo(passphrase, plaintext, constants.AES256) +} + +// EncryptMessageWithTokenAlgo encrypts a string with a random token and an algorithm chosen from constants.* +func EncryptMessageWithTokenAlgo( + token, plaintext, algo string, +) (ciphertext string, err error) { + var pgpMessage *crypto.PGPMessage + + var message = crypto.NewPlainMessageFromString(plaintext) + var key = crypto.NewSymmetricKeyFromToken(token, algo) + + if pgpMessage, err = key.Encrypt(message); err != nil { + return "", err + } + + if ciphertext, err = pgpMessage.GetArmored(); err != nil { + return "", err + } + + return ciphertext, nil +} + +// DecryptMessageWithToken decrypts an armored message with a random token. +// The algorithm is derived from the armoring. +func DecryptMessageWithToken(token, ciphertext string) (plaintext string, err error) { + var message *crypto.PlainMessage + var pgpMessage *crypto.PGPMessage + + var key = crypto.NewSymmetricKeyFromToken(token, "") + + if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil { + return "", err + } + + if message, err = key.Decrypt(pgpMessage); err != nil { + return "", err + } + + return message.GetString(), nil +} + +// EncryptMessageArmored generates an armored PGP message given a plaintext and an armored public key +func EncryptMessageArmored(publicKey, plaintext string) (ciphertext string, err error) { + var publicKeyRing *crypto.KeyRing + var pgpMessage *crypto.PGPMessage + + var message = crypto.NewPlainMessageFromString(plaintext) + + if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil { + return "", err + } + + if pgpMessage, err = publicKeyRing.Encrypt(message, nil); err != nil { + return "", err + } + + if ciphertext, err = pgpMessage.GetArmored(); err != nil { + return "", err + } + + return ciphertext, nil +} + +// EncryptSignMessageArmored generates an armored signed PGP message given a plaintext and an armored public key +// a private key and its passphrase +func EncryptSignMessageArmored( + publicKey, privateKey, passphrase, plaintext string, +) (ciphertext string, err error) { + var publicKeyRing, privateKeyRing *crypto.KeyRing + var pgpMessage *crypto.PGPMessage + + var message = crypto.NewPlainMessageFromString(plaintext) + + if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil { + return "", err + } + + if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil { + return "", err + } + + if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil { + return "", err + } + + if pgpMessage, err = publicKeyRing.Encrypt(message, privateKeyRing); err != nil { + return "", err + } + + if ciphertext, err = pgpMessage.GetArmored(); err != nil { + return "", err + } + + return ciphertext, nil +} + +// DecryptMessageArmored decrypts an armored PGP message given a private key and its passphrase +func DecryptMessageArmored( + privateKey, passphrase, ciphertext string, +) (plaintext string, err error) { + var privateKeyRing *crypto.KeyRing + var pgpMessage *crypto.PGPMessage + var message *crypto.PlainMessage + + if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil { + return "", err + } + + if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil { + return "", err + } + + if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil { + return "", err + } + + if message, _, err = privateKeyRing.Decrypt(pgpMessage, nil, 0); err != nil { + return "", err + } + + return message.GetString(), nil +} + +// DecryptVerifyMessageArmored decrypts an armored PGP message given a private key and its passphrase +// and verifies the embedded signature. +// Returns the plain data or an error on signature verification failure. +func DecryptVerifyMessageArmored( + publicKey, privateKey, passphrase, ciphertext string, +) (plaintext string, err error) { + var publicKeyRing, privateKeyRing *crypto.KeyRing + var pgpMessage *crypto.PGPMessage + var message *crypto.PlainMessage + var verification *crypto.Verification + + if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil { + return "", err + } + + if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil { + return "", err + } + + if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil { + return "", err + } + + if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil { + return "", err + } + + if message, verification, err = privateKeyRing.Decrypt(pgpMessage, publicKeyRing, pgp.GetUnixTime()); err != nil { + return "", err + } + + if !verification.IsValid() { + return "", errors.New("gopenpgp: unable to verify message") + } + + return message.GetString(), nil +} + +// EncryptSignAttachment encrypts an attachment using a detached signature, given a publicKey, a privateKey +// and its passphrase, the filename, and the unencrypted file data. +// Returns keypacket, dataPacket and unarmored (!) signature separate. +func EncryptSignAttachment( + publicKey, privateKey, passphrase, fileName string, + plainData []byte, +) (keyPacket, dataPacket, signature []byte, err error) { + var publicKeyRing, privateKeyRing *crypto.KeyRing + var packets *crypto.PGPSplitMessage + var signatureObj *crypto.PGPSignature + + var binMessage = crypto.NewPlainMessage(plainData) + + if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil { + return nil, nil, nil, err + } + + if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil { + return nil, nil, nil, err + } + + if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil { + return nil, nil, nil, err + } + + if packets, err = publicKeyRing.EncryptAttachment(binMessage, fileName); err != nil { + return nil, nil, nil, err + } + + if signatureObj, err = privateKeyRing.SignDetached(binMessage); err != nil { + return nil, nil, nil, err + } + + return packets.GetKeyPacket(), packets.GetDataPacket(), signatureObj.GetBinary(), nil +} + +// DecryptVerifyAttachment decrypts and verifies an attachment split into the keyPacket, dataPacket +// and an armored (!) signature, given a publicKey, and a privateKey with its passphrase. +// Returns the plain data or an error on signature verification failure. +func DecryptVerifyAttachment( + publicKey, privateKey, passphrase string, + keyPacket, dataPacket []byte, + armoredSignature string, +) (plainData []byte, err error) { + var publicKeyRing, privateKeyRing *crypto.KeyRing + var detachedSignature *crypto.PGPSignature + var message *crypto.PlainMessage + var verification *crypto.Verification + + var packets = crypto.NewPGPSplitMessage(keyPacket, dataPacket) + + if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil { + return nil, err + } + + if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil { + return nil, err + } + + if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil { + return nil, err + } + + if detachedSignature, err = crypto.NewPGPSignatureFromArmored(armoredSignature); err != nil { + return nil, err + } + + if message, err = privateKeyRing.DecryptAttachment(packets); err != nil { + return nil, err + } + + if verification, err = publicKeyRing.VerifyDetached(message, detachedSignature, pgp.GetUnixTime()); err != nil { + return nil, errors.New("gopenpgp: unable to verify attachment") + } + + if !verification.IsValid() { + return nil, errors.New("gopenpgp: unable to verify attachment") + } + + return message.GetBinary(), nil +} diff --git a/helper/helper_test.go b/helper/helper_test.go new file mode 100644 index 0000000..1ca316c --- /dev/null +++ b/helper/helper_test.go @@ -0,0 +1,131 @@ +package helper + +import ( + "testing" + + "github.com/ProtonMail/gopenpgp/crypto" + "github.com/stretchr/testify/assert" +) + +func TestAESEncryption(t *testing.T) { + var plaintext = "Symmetric secret" + var passphrase = "passphrase" + + ciphertext, err := EncryptMessageWithToken(passphrase, plaintext) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + + _, err = DecryptMessageWithToken("Wrong passphrase", ciphertext) + assert.EqualError(t, err, "gopenpgp: wrong password in symmetric decryption") + + decrypted, err := DecryptMessageWithToken(passphrase, ciphertext) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + + assert.Exactly(t, plaintext, decrypted) +} + +func TestArmoredTextMessageEncryption(t *testing.T) { + var plaintext = "Secret message" + + armored, err := EncryptMessageArmored(readTestFile("keyring_publicKey", false), plaintext) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + + assert.Exactly(t, true, pgp.IsPGPMessage(armored)) + + decrypted, err := DecryptMessageArmored( + readTestFile("keyring_privateKey", false), + testMailboxPassword, // Password defined in base_test + armored, + ) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + + assert.Exactly(t, plaintext, decrypted) +} + +func TestArmoredTextMessageEncryptionVerification(t *testing.T) { + var plaintext = "Secret message" + + armored, err := EncryptSignMessageArmored( + readTestFile("keyring_publicKey", false), + readTestFile("keyring_privateKey", false), + testMailboxPassword, // Password defined in base_test + plaintext, + ) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + + assert.Exactly(t, true, pgp.IsPGPMessage(armored)) + + _, err = DecryptVerifyMessageArmored( + readTestFile("mime_publicKey", false), // Wrong public key + readTestFile("keyring_privateKey", false), + testMailboxPassword, // Password defined in base_test + armored, + ) + assert.EqualError(t, err, "gopenpgp: unable to verify message") + + decrypted, err := DecryptVerifyMessageArmored( + readTestFile("keyring_publicKey", false), + readTestFile("keyring_privateKey", false), + testMailboxPassword, // Password defined in base_test + armored, + ) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + + assert.Exactly(t, plaintext, decrypted) +} + +func TestAttachmentEncryptionVerification(t *testing.T) { + var attachment = []byte("Secret file\r\nRoot password:hunter2") + + keyPacket, dataPacket, signature, err := EncryptSignAttachment( + readTestFile("keyring_publicKey", false), + readTestFile("keyring_privateKey", false), + testMailboxPassword, // Password defined in base_test + "password.txt", + attachment, + ) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + + sig := crypto.NewPGPSignature(signature) + armoredSig, err := sig.GetArmored() + if err != nil { + t.Fatal("Expected no error when armoring signature, got:", err) + } + + _, err = DecryptVerifyAttachment( + readTestFile("mime_publicKey", false), // Wrong public key + readTestFile("keyring_privateKey", false), + testMailboxPassword, // Password defined in base_test + keyPacket, + dataPacket, + armoredSig, + ) + assert.EqualError(t, err, "gopenpgp: unable to verify attachment") + + decrypted, err := DecryptVerifyAttachment( + readTestFile("keyring_publicKey", false), + readTestFile("keyring_privateKey", false), + testMailboxPassword, // Password defined in base_test + keyPacket, + dataPacket, + armoredSig, + ) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + + assert.Exactly(t, attachment, decrypted) +} diff --git a/models/models.go b/models/models.go index 721d41c..7aa6018 100644 --- a/models/models.go +++ b/models/models.go @@ -1,26 +1,8 @@ // Package models provides structs containing message data. package models -// EncryptedSplit contains a separate session key packet and symmetrically -// encrypted data packet. -type EncryptedSplit struct { - DataPacket []byte - KeyPacket []byte - Algo string -} - // EncryptedSigned contains an encrypted message and signature. type EncryptedSigned struct { Encrypted string Signature string } - -// DecryptSignedVerify contains a decrypted message and verification result. -type DecryptSignedVerify struct { - //clear text - Plaintext string - //bitmask verify status : 0 - Verify int - //error message if verify failed - Message string -} diff --git a/subtle/subtle_test.go b/subtle/subtle_test.go index 251de8c..ee97c69 100644 --- a/subtle/subtle_test.go +++ b/subtle/subtle_test.go @@ -1,8 +1,8 @@ package subtle import ( - "github.com/stretchr/testify/assert" "encoding/hex" + "github.com/stretchr/testify/assert" "testing" )