From eccc1df6195fd03f4f0796f268fb97414dfb87a6 Mon Sep 17 00:00:00 2001 From: "M. Thiercelin" Date: Tue, 24 Jan 2023 17:27:38 +0100 Subject: [PATCH] Add streaming APIs to encrypt with compression --- .github/workflows/go.yml | 2 +- .golangci.yml | 3 +- CHANGELOG.md | 8 +++ crypto/keyring_streaming.go | 105 +++++++++++++++++++++++----- crypto/keyring_streaming_test.go | 52 +++++++++++++- crypto/sessionkey_streaming.go | 46 ++++++++++-- crypto/sessionkey_streaming_test.go | 18 ++++- crypto/signature_test.go | 4 +- 8 files changed, 209 insertions(+), 29 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 675eade..7d2bff8 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -48,4 +48,4 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: - version: v1.46.2 \ No newline at end of file + version: v1.50.1 \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 0db4715..9c9d1a5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -46,4 +46,5 @@ linters: - ireturn # Prevents returning interfaces - forcetypeassert # Forces to assert types in tests - nonamedreturns # Disallows named returns - - exhaustruct # Forces all structs to be named \ No newline at end of file + - exhaustruct # Forces all structs to be named + - nosnakecase # Disallows snake case \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 62c4d73..9dd8763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added +- Streaming API to encrypt with compression: + - `func (keyRing *KeyRing) EncryptStreamWithCompression` + - `func (keyRing *KeyRing) EncryptSplitStreamWithCompression` + - `func (sk *SessionKey) EncryptStreamWithCompression` + ## [2.5.0] 2022-12-16 ### Changed - Update `github.com/ProtonMail/go-crypto` to the latest version diff --git a/crypto/keyring_streaming.go b/crypto/keyring_streaming.go index 215212a..f742a9b 100644 --- a/crypto/keyring_streaming.go +++ b/crypto/keyring_streaming.go @@ -8,6 +8,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/gopenpgp/v2/constants" "github.com/pkg/errors" ) @@ -44,6 +45,47 @@ func (keyRing *KeyRing) EncryptStream( ) (plainMessageWriter WriteCloser, err error) { config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: getTimeGenerator()} + return keyRing.encryptStreamWithConfig( + config, + pgpMessageWriter, + pgpMessageWriter, + plainMessageMetadata, + signKeyRing, + ) +} + +// EncryptStreamWithCompression is used to encrypt data as a Writer. +// The plaintext data is compressed before being encrypted. +// It takes a writer for the encrypted data and returns a WriteCloser for the plaintext data +// If signKeyRing is not nil, it is used to do an embedded signature. +func (keyRing *KeyRing) EncryptStreamWithCompression( + pgpMessageWriter Writer, + plainMessageMetadata *PlainMessageMetadata, + signKeyRing *KeyRing, +) (plainMessageWriter WriteCloser, err error) { + config := &packet.Config{ + DefaultCipher: packet.CipherAES256, + Time: getTimeGenerator(), + DefaultCompressionAlgo: constants.DefaultCompression, + CompressionConfig: &packet.CompressionConfig{Level: constants.DefaultCompressionLevel}, + } + + return keyRing.encryptStreamWithConfig( + config, + pgpMessageWriter, + pgpMessageWriter, + plainMessageMetadata, + signKeyRing, + ) +} + +func (keyRing *KeyRing) encryptStreamWithConfig( + config *packet.Config, + keyPacketWriter Writer, + dataPacketWriter Writer, + plainMessageMetadata *PlainMessageMetadata, + signKeyRing *KeyRing, +) (plainMessageWriter WriteCloser, err error) { if plainMessageMetadata == nil { // Use sensible default metadata plainMessageMetadata = &PlainMessageMetadata{ @@ -59,7 +101,7 @@ func (keyRing *KeyRing) EncryptStream( ModTime: time.Unix(plainMessageMetadata.ModTime, 0), } - plainMessageWriter, err = asymmetricEncryptStream(hints, pgpMessageWriter, pgpMessageWriter, keyRing, signKeyRing, config) + plainMessageWriter, err = asymmetricEncryptStream(hints, keyPacketWriter, dataPacketWriter, keyRing, signKeyRing, config) if err != nil { return nil, err } @@ -109,26 +151,55 @@ func (keyRing *KeyRing) EncryptSplitStream( ) (*EncryptSplitResult, error) { config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: getTimeGenerator()} - if plainMessageMetadata == nil { - // Use sensible default metadata - plainMessageMetadata = &PlainMessageMetadata{ - IsBinary: true, - Filename: "", - ModTime: GetUnixTime(), - } - } - - hints := &openpgp.FileHints{ - FileName: plainMessageMetadata.Filename, - IsBinary: plainMessageMetadata.IsBinary, - ModTime: time.Unix(plainMessageMetadata.ModTime, 0), - } - var keyPacketBuf bytes.Buffer - plainMessageWriter, err := asymmetricEncryptStream(hints, &keyPacketBuf, dataPacketWriter, keyRing, signKeyRing, config) + + plainMessageWriter, err := keyRing.encryptStreamWithConfig( + config, + &keyPacketBuf, + dataPacketWriter, + plainMessageMetadata, + signKeyRing, + ) if err != nil { return nil, err } + + return &EncryptSplitResult{ + keyPacketBuf: &keyPacketBuf, + plainMessageWriter: plainMessageWriter, + }, nil +} + +// EncryptSplitStreamWithCompression is used to encrypt data as a stream. +// It takes a writer for the Symmetrically Encrypted Data Packet +// (https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) +// and returns a writer for the plaintext data and the key packet. +// If signKeyRing is not nil, it is used to do an embedded signature. +func (keyRing *KeyRing) EncryptSplitStreamWithCompression( + dataPacketWriter Writer, + plainMessageMetadata *PlainMessageMetadata, + signKeyRing *KeyRing, +) (*EncryptSplitResult, error) { + config := &packet.Config{ + DefaultCipher: packet.CipherAES256, + Time: getTimeGenerator(), + DefaultCompressionAlgo: constants.DefaultCompression, + CompressionConfig: &packet.CompressionConfig{Level: constants.DefaultCompressionLevel}, + } + + var keyPacketBuf bytes.Buffer + + plainMessageWriter, err := keyRing.encryptStreamWithConfig( + config, + &keyPacketBuf, + dataPacketWriter, + plainMessageMetadata, + signKeyRing, + ) + if err != nil { + return nil, err + } + return &EncryptSplitResult{ keyPacketBuf: &keyPacketBuf, plainMessageWriter: plainMessageWriter, diff --git a/crypto/keyring_streaming_test.go b/crypto/keyring_streaming_test.go index 390edb3..77083f9 100644 --- a/crypto/keyring_streaming_test.go +++ b/crypto/keyring_streaming_test.go @@ -107,10 +107,34 @@ func TestKeyRing_EncryptDecryptStream(t *testing.T) { } func TestKeyRing_EncryptStreamCompatible(t *testing.T) { + enc := func(w io.Writer, meta *PlainMessageMetadata, kr *KeyRing) (io.WriteCloser, error) { + return keyRingTestPublic.EncryptStream( + w, + meta, + kr, + ) + } + testKeyRing_EncryptStreamCompatible(enc, t) +} + +func TestKeyRing_EncryptStreamWithCompressionCompatible(t *testing.T) { + enc := func(w io.Writer, meta *PlainMessageMetadata, kr *KeyRing) (io.WriteCloser, error) { + return keyRingTestPublic.EncryptStreamWithCompression( + w, + meta, + kr, + ) + } + testKeyRing_EncryptStreamCompatible(enc, t) +} + +type keyringEncryptionFunction = func(io.Writer, *PlainMessageMetadata, *KeyRing) (io.WriteCloser, error) + +func testKeyRing_EncryptStreamCompatible(encrypt keyringEncryptionFunction, t *testing.T) { messageBytes := []byte("Hello World!") messageReader := bytes.NewReader(messageBytes) var ciphertextBuf bytes.Buffer - messageWriter, err := keyRingTestPublic.EncryptStream( + messageWriter, err := encrypt( &ciphertextBuf, testMeta, keyRingTestPrivate, @@ -276,10 +300,34 @@ func TestKeyRing_EncryptDecryptSplitStream(t *testing.T) { } func TestKeyRing_EncryptSplitStreamCompatible(t *testing.T) { + enc := func(w io.Writer, meta *PlainMessageMetadata, kr *KeyRing) (*EncryptSplitResult, error) { + return keyRingTestPublic.EncryptSplitStream( + w, + meta, + kr, + ) + } + testKeyRing_EncryptSplitStreamCompatible(enc, t) +} + +func TestKeyRing_EncryptSplitStreamWithCompressionCompatible(t *testing.T) { + enc := func(w io.Writer, meta *PlainMessageMetadata, kr *KeyRing) (*EncryptSplitResult, error) { + return keyRingTestPublic.EncryptSplitStreamWithCompression( + w, + meta, + kr, + ) + } + testKeyRing_EncryptSplitStreamCompatible(enc, t) +} + +type keyringEncryptionSplitFunction = func(io.Writer, *PlainMessageMetadata, *KeyRing) (*EncryptSplitResult, error) + +func testKeyRing_EncryptSplitStreamCompatible(encrypt keyringEncryptionSplitFunction, t *testing.T) { messageBytes := []byte("Hello World!") messageReader := bytes.NewReader(messageBytes) var dataPacketBuf bytes.Buffer - encryptionResult, err := keyRingTestPublic.EncryptSplitStream( + encryptionResult, err := encrypt( &dataPacketBuf, testMeta, keyRingTestPrivate, diff --git a/crypto/sessionkey_streaming.go b/crypto/sessionkey_streaming.go index 02e80ab..8ed3685 100644 --- a/crypto/sessionkey_streaming.go +++ b/crypto/sessionkey_streaming.go @@ -3,6 +3,7 @@ package crypto import ( "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/gopenpgp/v2/constants" "github.com/pkg/errors" ) @@ -29,16 +30,51 @@ func (sk *SessionKey) EncryptStream( dataPacketWriter Writer, plainMessageMetadata *PlainMessageMetadata, signKeyRing *KeyRing, +) (plainMessageWriter WriteCloser, err error) { + config := &packet.Config{ + Time: getTimeGenerator(), + } + return sk.encryptStreamWithConfig( + config, + dataPacketWriter, + plainMessageMetadata, + signKeyRing, + ) +} + +// EncryptStreamWithCompression is used to encrypt data as a Writer. +// The plaintext data is compressed before being encrypted. +// It takes a writer for the encrypted data packet and returns a writer for the plaintext data. +// If signKeyRing is not nil, it is used to do an embedded signature. +func (sk *SessionKey) EncryptStreamWithCompression( + dataPacketWriter Writer, + plainMessageMetadata *PlainMessageMetadata, + signKeyRing *KeyRing, +) (plainMessageWriter WriteCloser, err error) { + config := &packet.Config{ + Time: getTimeGenerator(), + DefaultCompressionAlgo: constants.DefaultCompression, + CompressionConfig: &packet.CompressionConfig{Level: constants.DefaultCompressionLevel}, + } + return sk.encryptStreamWithConfig( + config, + dataPacketWriter, + plainMessageMetadata, + signKeyRing, + ) +} + +func (sk *SessionKey) encryptStreamWithConfig( + config *packet.Config, + dataPacketWriter Writer, + plainMessageMetadata *PlainMessageMetadata, + signKeyRing *KeyRing, ) (plainMessageWriter WriteCloser, err error) { dc, err := sk.GetCipherFunc() if err != nil { return nil, errors.Wrap(err, "gopenpgp: unable to encrypt with session key") } - - config := &packet.Config{ - Time: getTimeGenerator(), - DefaultCipher: dc, - } + config.DefaultCipher = dc var signEntity *openpgp.Entity if signKeyRing != nil { signEntity, err = signKeyRing.getSigningEntity() diff --git a/crypto/sessionkey_streaming_test.go b/crypto/sessionkey_streaming_test.go index f597022..a6f5a62 100644 --- a/crypto/sessionkey_streaming_test.go +++ b/crypto/sessionkey_streaming_test.go @@ -74,10 +74,26 @@ func TestSessionKey_EncryptDecryptStream(t *testing.T) { } func TestSessionKey_EncryptStreamCompatible(t *testing.T) { + enc := func(w io.Writer, meta *PlainMessageMetadata, kr *KeyRing) (io.WriteCloser, error) { + return testSessionKey.EncryptStream(w, meta, kr) + } + testSessionKey_EncryptStreamCompatible(enc, t) +} + +func TestSessionKey_EncryptStreamWithCompressionCompatible(t *testing.T) { + enc := func(w io.Writer, meta *PlainMessageMetadata, kr *KeyRing) (io.WriteCloser, error) { + return testSessionKey.EncryptStreamWithCompression(w, meta, kr) + } + testSessionKey_EncryptStreamCompatible(enc, t) +} + +type sessionKeyEncryptionFunction = func(io.Writer, *PlainMessageMetadata, *KeyRing) (io.WriteCloser, error) + +func testSessionKey_EncryptStreamCompatible(enc sessionKeyEncryptionFunction, t *testing.T) { messageBytes := []byte("Hello World!") messageReader := bytes.NewReader(messageBytes) var dataPacketBuf bytes.Buffer - messageWriter, err := testSessionKey.EncryptStream( + messageWriter, err := enc( &dataPacketBuf, testMeta, keyRingTestPrivate, diff --git a/crypto/signature_test.go b/crypto/signature_test.go index b1f301f..ea7a928 100644 --- a/crypto/signature_test.go +++ b/crypto/signature_test.go @@ -228,8 +228,8 @@ func Test_KeyRing_GetVerifiedSignatureTimestampError(t *testing.T) { if err != nil { t.Errorf("Got an error while generating the signature: %v", err) } - message_corrupted := NewPlainMessageFromString("Ciao world!") - _, err = keyRingTestPublic.GetVerifiedSignatureTimestamp(message_corrupted, signature, 0) + messageCorrupted := NewPlainMessageFromString("Ciao world!") + _, err = keyRingTestPublic.GetVerifiedSignatureTimestamp(messageCorrupted, signature, 0) if err == nil { t.Errorf("Expected an error while parsing the creation time of a wrong signature, got nil") }