From a4d89bce326b1e8b6098447f2d3da286f18a7cfc Mon Sep 17 00:00:00 2001 From: wussler Date: Mon, 12 Oct 2020 18:45:57 +0200 Subject: [PATCH] Add filename and time properties to message (#85) * Add filename and time properties to message * Message time defaults to current time --- CHANGELOG.md | 17 +++++++++++++++++ crypto/attachment.go | 35 +++++++++++++++++++++++++++-------- crypto/attachment_test.go | 10 +++++----- crypto/keyring_message.go | 32 +++++++++++++++++--------------- crypto/message.go | 30 +++++++++++++++++++++++++++++- crypto/password.go | 30 +++++++++++++++++------------- crypto/sessionkey.go | 15 +++++++++++++-- helper/mobile.go | 6 +++--- helper/sign_detached.go | 10 ++++++---- 9 files changed, 134 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 111b85d..f142454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,21 @@ EncryptSignArmoredDetachedMobile( ) (wrappedTuple *EncryptSignArmoredDetachedMobileResult, err error) ``` +- `NewPlainMessageFromFile` Function to create new `PlainMessage`s with a filename: +```go +NewPlainMessageFromFile(data []byte, filename string, modTime int) *PlainMessage +``` + +- `GetFilename` to get the filename from a message: +```go +(msg *PlainMessage) GetFilename() string +``` + +- `GetModTime` to get the modification time of a file +```go +(msg *PlainMessage) GetModTime() uint32 +``` + ### Changed - Improved key and message armoring testing - `EncryptSessionKey` now creates encrypted key packets for each valid encryption key in the provided keyring. @@ -80,6 +95,8 @@ EncryptSignArmoredDetachedMobile( - Use aes256 cipher for password-encrypted messages. - The helpers `EncryptSignMessageArmored`, `DecryptVerifyMessageArmored`, `DecryptVerifyAttachment`, and`DecryptBinaryMessageArmored` now accept private keys as public keys and perform automatic casting if the keys are locked. +- The `PlainMessage` struct now contains the fields `filename` (string) and `time` (uint32) +- All the Decrypt* functions return the filename, type, and time specified in the encrypted message ### Fixed - Public key armoring headers diff --git a/crypto/attachment.go b/crypto/attachment.go index 3e51c0a..0c89135 100644 --- a/crypto/attachment.go +++ b/crypto/attachment.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "runtime" "sync" + "time" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" @@ -52,7 +53,7 @@ func (ap *AttachmentProcessor) Finish() (*PGPSplitMessage, error) { // newAttachmentProcessor creates an AttachmentProcessor which can be used to encrypt // a file. It takes an estimatedSize and fileName as hints about the file. func (keyRing *KeyRing) newAttachmentProcessor( - estimatedSize int, fileName string, garbageCollector int, + estimatedSize int, filename string, isBinary bool, modTime uint32, garbageCollector int, ) (*AttachmentProcessor, error) { attachmentProc := &AttachmentProcessor{} // You could also add these one at a time if needed. @@ -60,7 +61,9 @@ func (keyRing *KeyRing) newAttachmentProcessor( attachmentProc.garbageCollector = garbageCollector hints := &openpgp.FileHints{ - FileName: fileName, + FileName: filename, + IsBinary: isBinary, + ModTime: time.Unix(int64(modTime), 0), } config := &packet.Config{ @@ -93,11 +96,22 @@ func (keyRing *KeyRing) newAttachmentProcessor( return attachmentProc, nil } -// EncryptAttachment encrypts a file given a PlainMessage and a fileName. +// EncryptAttachment encrypts a file given a PlainMessage and a filename. +// If given a filename it will override the information in the PlainMessage object. // Returns a PGPSplitMessage containing a session key packet and symmetrically encrypted data. // Specifically designed for attachments rather than text messages. -func (keyRing *KeyRing) EncryptAttachment(message *PlainMessage, fileName string) (*PGPSplitMessage, error) { - ap, err := keyRing.newAttachmentProcessor(len(message.GetBinary()), fileName, -1) +func (keyRing *KeyRing) EncryptAttachment(message *PlainMessage, filename string) (*PGPSplitMessage, error) { + if filename == "" { + filename = message.filename + } + + ap, err := keyRing.newAttachmentProcessor( + len(message.GetBinary()), + filename, + message.IsBinary(), + message.GetTime(), + -1, + ) if err != nil { return nil, err } @@ -114,9 +128,9 @@ func (keyRing *KeyRing) EncryptAttachment(message *PlainMessage, fileName string // file. It is optimized for low-memory environments and collects garbage every // megabyte. func (keyRing *KeyRing) NewLowMemoryAttachmentProcessor( - estimatedSize int, fileName string, + estimatedSize int, filename string, ) (*AttachmentProcessor, error) { - return keyRing.newAttachmentProcessor(estimatedSize, fileName, 1<<20) + return keyRing.newAttachmentProcessor(estimatedSize, filename, true, uint32(GetUnixTime()), 1<<20) } // DecryptAttachment takes a PGPSplitMessage, containing a session key packet and symmetrically encrypted data @@ -143,5 +157,10 @@ func (keyRing *KeyRing) DecryptAttachment(message *PGPSplitMessage) (*PlainMessa return nil, err } - return NewPlainMessage(b), nil + return &PlainMessage{ + Data: b, + TextType: !md.LiteralData.IsBinary, + filename: md.LiteralData.FileName, + time: md.LiteralData.Time, + }, nil } diff --git a/crypto/attachment_test.go b/crypto/attachment_test.go index dd5d279..e6025a0 100644 --- a/crypto/attachment_test.go +++ b/crypto/attachment_test.go @@ -42,9 +42,9 @@ func TestAttachmentSetKey(t *testing.T) { func TestAttachmentEncryptDecrypt(t *testing.T) { var testAttachmentCleartext = "cc,\ndille." - var message = NewPlainMessage([]byte(testAttachmentCleartext)) + var message = NewPlainMessageFromFile([]byte(testAttachmentCleartext), "test.txt", 1602518992) - encSplit, err := keyRingTestPrivate.EncryptAttachment(message, "s.txt") + encSplit, err := keyRingTestPrivate.EncryptAttachment(message, "") if err != nil { t.Fatal("Expected no error while encrypting attachment, got:", err) } @@ -59,9 +59,9 @@ func TestAttachmentEncryptDecrypt(t *testing.T) { func TestAttachmentEncrypt(t *testing.T) { var testAttachmentCleartext = "cc,\ndille." - var message = NewPlainMessage([]byte(testAttachmentCleartext)) + var message = NewPlainMessageFromFile([]byte(testAttachmentCleartext), "test.txt", 1602518992) - encSplit, err := keyRingTestPrivate.EncryptAttachment(message, "s.txt") + encSplit, err := keyRingTestPrivate.EncryptAttachment(message, "") if err != nil { t.Fatal("Expected no error while encrypting attachment, got:", err) } @@ -78,7 +78,7 @@ func TestAttachmentEncrypt(t *testing.T) { func TestAttachmentDecrypt(t *testing.T) { var testAttachmentCleartext = "cc,\ndille." - var message = NewPlainMessage([]byte(testAttachmentCleartext)) + var message = NewPlainMessageFromFile([]byte(testAttachmentCleartext), "test.txt", 1602518992) encrypted, err := keyRingTestPrivate.Encrypt(message, nil) if err != nil { diff --git a/crypto/keyring_message.go b/crypto/keyring_message.go index 81497e9..64e3cdd 100644 --- a/crypto/keyring_message.go +++ b/crypto/keyring_message.go @@ -5,6 +5,7 @@ import ( "crypto" "io" "io/ioutil" + "time" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" @@ -15,7 +16,7 @@ import ( // * 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, message.IsBinary()) + encrypted, err := asymmetricEncrypt(message, keyRing, privateKey) if err != nil { return nil, err } @@ -33,9 +34,7 @@ func (keyRing *KeyRing) Encrypt(message *PlainMessage, privateKey *KeyRing) (*PG func (keyRing *KeyRing) Decrypt( message *PGPMessage, verifyKey *KeyRing, verifyTime int64, ) (*PlainMessage, error) { - decrypted, err := asymmetricDecrypt(message.NewReader(), keyRing, verifyKey, verifyTime) - - return NewPlainMessage(decrypted), err + return asymmetricDecrypt(message.NewReader(), keyRing, verifyKey, verifyTime) } // SignDetached generates and returns a PGPSignature for a given PlainMessage. @@ -69,7 +68,7 @@ func (keyRing *KeyRing) VerifyDetached(message *PlainMessage, signature *PGPSign // ------ INTERNAL FUNCTIONS ------- // Core for encryption+signature functions. -func asymmetricEncrypt(data []byte, publicKey *KeyRing, privateKey *KeyRing, isBinary bool) ([]byte, error) { +func asymmetricEncrypt(plainMessage *PlainMessage, publicKey, privateKey *KeyRing) ([]byte, error) { var outBuf bytes.Buffer var encryptWriter io.WriteCloser var signEntity *openpgp.Entity @@ -86,11 +85,12 @@ func asymmetricEncrypt(data []byte, publicKey *KeyRing, privateKey *KeyRing, isB config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: getTimeGenerator()} hints := &openpgp.FileHints{ - IsBinary: isBinary, - FileName: "", + IsBinary: plainMessage.IsBinary(), + FileName: plainMessage.GetFilename(), + ModTime: time.Unix(int64(plainMessage.GetTime()), 0), } - if isBinary { + if plainMessage.IsBinary() { encryptWriter, err = openpgp.Encrypt(&outBuf, publicKey.entities, signEntity, hints, config) } else { encryptWriter, err = openpgp.EncryptText(&outBuf, publicKey.entities, signEntity, hints, config) @@ -99,7 +99,7 @@ func asymmetricEncrypt(data []byte, publicKey *KeyRing, privateKey *KeyRing, isB return nil, err } - _, err = encryptWriter.Write(data) + _, err = encryptWriter.Write(plainMessage.GetBinary()) if err != nil { return nil, err } @@ -115,7 +115,7 @@ func asymmetricEncrypt(data []byte, publicKey *KeyRing, privateKey *KeyRing, isB // Core for decryption+verification functions. func asymmetricDecrypt( encryptedIO io.Reader, privateKey *KeyRing, verifyKey *KeyRing, verifyTime int64, -) (plaintext []byte, err error) { +) (message *PlainMessage, err error) { privKeyEntries := privateKey.entities var additionalEntries openpgp.EntityList @@ -141,11 +141,13 @@ func asymmetricDecrypt( if verifyKey != nil { processSignatureExpiration(messageDetails, verifyTime) + err = verifyDetailsSignature(messageDetails, verifyKey) } - if verifyKey != nil { - return body, verifyDetailsSignature(messageDetails, verifyKey) - } - - return body, nil + return &PlainMessage{ + Data: body, + TextType: !messageDetails.LiteralData.IsBinary, + filename: messageDetails.LiteralData.FileName, + time: messageDetails.LiteralData.Time, + }, err } diff --git a/crypto/message.go b/crypto/message.go index c43e70a..661f71c 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -24,8 +24,12 @@ import ( type PlainMessage struct { // The content of the message Data []byte - // if the content is text or binary + // If the content is text or binary TextType bool + // The file's latest modification time + time uint32 + // The encrypted message's filename + filename string } // PGPMessage stores a PGP-encrypted message. @@ -62,6 +66,19 @@ func NewPlainMessage(data []byte) *PlainMessage { return &PlainMessage{ Data: clone(data), TextType: false, + time: uint32(GetUnixTime()), + } +} + +// NewPlainMessageFromFile generates a new binary PlainMessage ready for encryption, +// signature, or verification from the unencrypted binary data. +// It assigns a filename and a modification time. +func NewPlainMessageFromFile(data []byte, filename string, time uint32) *PlainMessage { + return &PlainMessage{ + Data: clone(data), + TextType: false, + filename: filename, + time: time, } } @@ -71,6 +88,7 @@ func NewPlainMessageFromString(text string) *PlainMessage { return &PlainMessage{ Data: []byte(text), TextType: true, + time: uint32(GetUnixTime()), } } @@ -201,6 +219,16 @@ func (msg *PlainMessage) IsBinary() bool { return !msg.TextType } +// GetFilename returns the file name of the message as a string. +func (msg *PlainMessage) GetFilename() string { + return msg.filename +} + +// GetTime returns the modification time of a file (if provided in the ciphertext). +func (msg *PlainMessage) GetTime() uint32 { + return msg.time +} + // GetBinary returns the unarmored binary content of the message as a []byte. func (msg *PGPMessage) GetBinary() []byte { return msg.Data diff --git a/crypto/password.go b/crypto/password.go index 9a51971..3b4043d 100644 --- a/crypto/password.go +++ b/crypto/password.go @@ -3,6 +3,7 @@ package crypto import ( "bytes" "io" + "time" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" @@ -16,7 +17,7 @@ import ( // * password: A password that will be derived into an encryption key. // * output : The encrypted data as PGPMessage. func EncryptMessageWithPassword(message *PlainMessage, password []byte) (*PGPMessage, error) { - encrypted, err := passwordEncrypt(message.GetBinary(), password, message.IsBinary()) + encrypted, err := passwordEncrypt(message, password) if err != nil { return nil, err } @@ -29,13 +30,7 @@ func EncryptMessageWithPassword(message *PlainMessage, password []byte) (*PGPMes // * password: A password that will be derived into an encryption key. // * output: The decrypted data as PlainMessage. func DecryptMessageWithPassword(message *PGPMessage, password []byte) (*PlainMessage, error) { - decrypted, err := passwordDecrypt(message.NewReader(), password) - if err != nil { - return nil, err - } - - binMessage := NewPlainMessage(decrypted) - return binMessage, nil + return passwordDecrypt(message.NewReader(), password) } // DecryptSessionKeyWithPassword decrypts the binary symmetrically encrypted @@ -110,7 +105,7 @@ func EncryptSessionKeyWithPassword(sk *SessionKey, password []byte) ([]byte, err // ----- INTERNAL FUNCTIONS ------ -func passwordEncrypt(message []byte, password []byte, isBinary bool) ([]byte, error) { +func passwordEncrypt(message *PlainMessage, password []byte) ([]byte, error) { var outBuf bytes.Buffer config := &packet.Config{ @@ -118,13 +113,17 @@ func passwordEncrypt(message []byte, password []byte, isBinary bool) ([]byte, er Time: getTimeGenerator(), } - hints := &openpgp.FileHints{IsBinary: isBinary} + hints := &openpgp.FileHints{ + IsBinary: message.IsBinary(), + FileName: message.GetFilename(), + ModTime: time.Unix(int64(message.GetTime()), 0), + } encryptWriter, err := openpgp.SymmetricallyEncrypt(&outBuf, password, hints, config) if err != nil { return nil, err } - _, err = encryptWriter.Write(message) + _, err = encryptWriter.Write(message.GetBinary()) if err != nil { return nil, err } @@ -137,7 +136,7 @@ func passwordEncrypt(message []byte, password []byte, isBinary bool) ([]byte, er return outBuf.Bytes(), nil } -func passwordDecrypt(encryptedIO io.Reader, password []byte) ([]byte, error) { +func passwordDecrypt(encryptedIO io.Reader, password []byte) (*PlainMessage, error) { firstTimeCalled := true var prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) { if firstTimeCalled { @@ -163,5 +162,10 @@ func passwordDecrypt(encryptedIO io.Reader, password []byte) ([]byte, error) { return nil, err } - return messageBuf.Bytes(), nil + return &PlainMessage{ + Data: messageBuf.Bytes(), + TextType: !md.LiteralData.IsBinary, + filename: md.LiteralData.FileName, + time: md.LiteralData.Time, + }, nil } diff --git a/crypto/sessionkey.go b/crypto/sessionkey.go index 487896a..36e22fe 100644 --- a/crypto/sessionkey.go +++ b/crypto/sessionkey.go @@ -139,7 +139,13 @@ func (sk *SessionKey) Encrypt(message *PlainMessage) ([]byte, error) { } } - encryptWriter, err = packet.SerializeLiteral(encryptWriter, message.IsBinary(), "", 0) + encryptWriter, err = packet.SerializeLiteral( + encryptWriter, + message.IsBinary(), + message.GetFilename(), + message.GetTime(), + ) + if err != nil { return nil, errors.Wrap(err, "gopenpgp: unable to serialize") } @@ -210,7 +216,12 @@ func (sk *SessionKey) Decrypt(dataPacket []byte) (*PlainMessage, error) { return nil, err } - return NewPlainMessage(messageBuf.Bytes()), nil + return &PlainMessage{ + Data: messageBuf.Bytes(), + TextType: !md.LiteralData.IsBinary, + filename: md.LiteralData.FileName, + time: md.LiteralData.Time, + }, err } func (sk *SessionKey) checkSize() error { diff --git a/helper/mobile.go b/helper/mobile.go index 8350d01..03d63e2 100644 --- a/helper/mobile.go +++ b/helper/mobile.go @@ -60,9 +60,9 @@ func DecryptAttachment(keyPacket []byte, dataPacket []byte, keyRing *crypto.KeyR // Returns a PGPSplitMessage containing a session key packet and symmetrically // encrypted data. Specifically designed for attachments rather than text // messages. -func EncryptAttachment(plainData []byte, fileName string, keyRing *crypto.KeyRing) (*crypto.PGPSplitMessage, error) { - plainMessage := crypto.NewPlainMessage(plainData) - decrypted, err := keyRing.EncryptAttachment(plainMessage, fileName) +func EncryptAttachment(plainData []byte, filename string, keyRing *crypto.KeyRing) (*crypto.PGPSplitMessage, error) { + plainMessage := crypto.NewPlainMessageFromFile(plainData, filename, uint32(crypto.GetUnixTime())) + decrypted, err := keyRing.EncryptAttachment(plainMessage, "") if err != nil { return nil, err } diff --git a/helper/sign_detached.go b/helper/sign_detached.go index 667dc81..a55cd0e 100644 --- a/helper/sign_detached.go +++ b/helper/sign_detached.go @@ -3,20 +3,22 @@ package helper -import "github.com/ProtonMail/gopenpgp/v2/crypto" +import ( + "github.com/ProtonMail/gopenpgp/v2/crypto" +) // 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 string, passphrase []byte, fileName string, plainData []byte, + publicKey, privateKey string, passphrase []byte, filename string, plainData []byte, ) (keyPacket, dataPacket, signature []byte, err error) { var publicKeyObj, privateKeyObj, unlockedKeyObj *crypto.Key var publicKeyRing, privateKeyRing *crypto.KeyRing var packets *crypto.PGPSplitMessage var signatureObj *crypto.PGPSignature - var binMessage = crypto.NewPlainMessage(plainData) + var binMessage = crypto.NewPlainMessageFromFile(plainData, filename, uint32(crypto.GetUnixTime())) if publicKeyObj, err = crypto.NewKeyFromArmored(publicKey); err != nil { return nil, nil, nil, err @@ -45,7 +47,7 @@ func EncryptSignAttachment( return nil, nil, nil, err } - if packets, err = publicKeyRing.EncryptAttachment(binMessage, fileName); err != nil { + if packets, err = publicKeyRing.EncryptAttachment(binMessage, ""); err != nil { return nil, nil, nil, err }