diff --git a/Changes b/Changes index dd8c2465..1dea18fc 100644 --- a/Changes +++ b/Changes @@ -4,6 +4,18 @@ Changes v2 has many incompatibilities with v1. To see the full list of differences between v1 and v2, please read the Changes-v2.md file (https://github.com/lestrrat-go/jwx/blob/develop/v2/Changes-v2.md) +v2.0.21 UNRELEASED + * [jwe] Added `jwe.Settings(jwe.WithMaxDecompressBufferSize(int64))` to specify the + maximum size of a decompressed JWE payload. The default value is 10MB. If you + are compressing payloads greater than this and want to decompress it during + a call to `jwe.Decrypt`, you need to explicitly set a value large enough to + hold that data. + + The same option can be passed to `jwe.Decrypt` to control this behavior on + a per-message basis. + * [jwe] Added documentation stating that `jwe.WithMaxBufferSize` option will be + renamed in future versions, i.e. v3 + v2.0.20 20 Feb 2024 [New Features] * [jwe] Added `jwe.Settings(WithMaxBufferSize(int64))` to set the maximum size of diff --git a/jwe/compress.go b/jwe/compress.go index 0beba4a5..bc55841b 100644 --- a/jwe/compress.go +++ b/jwe/compress.go @@ -9,8 +9,34 @@ import ( "github.com/lestrrat-go/jwx/v2/internal/pool" ) -func uncompress(plaintext []byte) ([]byte, error) { - return io.ReadAll(flate.NewReader(bytes.NewReader(plaintext))) +func uncompress(src []byte, maxBufferSize int64) ([]byte, error) { + var dst bytes.Buffer + r := flate.NewReader(bytes.NewReader(src)) + defer r.Close() + var buf [16384]byte + var sofar int64 + for { + n, readErr := r.Read(buf[:]) + sofar += int64(n) + if sofar > maxBufferSize { + return nil, fmt.Errorf(`compressed payload exceeds maximum allowed size`) + } + if readErr != nil { + // if we have a read error, and it's not EOF, then we need to stop + if readErr != io.EOF { + return nil, fmt.Errorf(`failed to read inflated data: %w`, readErr) + } + } + + if _, err := dst.Write(buf[:n]); err != nil { + return nil, fmt.Errorf(`failed to write inflated data: %w`, err) + } + + if readErr != nil { + // if it got here, then readErr == io.EOF, we're done + return dst.Bytes(), nil + } + } } func compress(plaintext []byte) ([]byte, error) { diff --git a/jwe/jwe.go b/jwe/jwe.go index ae1b8e3a..44909a26 100644 --- a/jwe/jwe.go +++ b/jwe/jwe.go @@ -28,6 +28,7 @@ import ( var muSettings sync.RWMutex var maxPBES2Count = 10000 +var maxDecompressBufferSize int64 = 10 * 1024 * 1024 // 10MB func Settings(options ...GlobalOption) { muSettings.Lock() @@ -37,6 +38,8 @@ func Settings(options ...GlobalOption) { switch option.Ident() { case identMaxPBES2Count{}: maxPBES2Count = option.Value().(int) + case identMaxDecompressBufferSize{}: + maxDecompressBufferSize = option.Value().(int64) case identMaxBufferSize{}: aescbc.SetMaxBufferSize(option.Value().(int64)) } @@ -463,28 +466,50 @@ func encrypt(payload, cek []byte, options ...EncryptOption) ([]byte, error) { } type decryptCtx struct { - msg *Message - aad []byte - cek *[]byte - computedAad []byte - keyProviders []KeyProvider - protectedHeaders Headers + msg *Message + aad []byte + cek *[]byte + computedAad []byte + keyProviders []KeyProvider + protectedHeaders Headers + maxDecompressBufferSize int64 } -// Decrypt takes the key encryption algorithm and the corresponding -// key to decrypt the JWE message, and returns the decrypted payload. +// Decrypt takes encrypted payload, and information required to decrypt the +// payload (e.g. the key encryption algorithm and the corresponding +// key to decrypt the JWE message) in its optional arguments. See +// the examples and list of options that return a DecryptOption for possible +// values. Upon successful decryptiond returns the decrypted payload. +// // The JWE message can be either compact or full JSON format. // -// `alg` accepts a `jwa.KeyAlgorithm` for convenience so you can directly pass -// the result of `(jwk.Key).Algorithm()`, but in practice it must be of type +// When using `jwe.WithKeyEncryptionAlgorithm()`, you can pass a `jwa.KeyAlgorithm` +// for convenience: this is mainly to allow you to directly pass the result of `(jwk.Key).Algorithm()`. +// However, do note that while `(jwk.Key).Algorithm()` could very well contain key encryption +// algorithms, it could also contain other types of values, such as _signature algorithms_. +// In order for `jwe.Decrypt` to work properly, the `alg` parameter must be of type // `jwa.KeyEncryptionAlgorithm` or otherwise it will cause an error. // -// `key` must be a private key. It can be either in its raw format (e.g. *rsa.PrivateKey) or a jwk.Key +// When using `jwe.WithKey()`, the value must be a private key. +// It can be either in its raw format (e.g. *rsa.PrivateKey) or a jwk.Key +// +// When the encrypted message is also compressed, the decompressed payload must be +// smaller than the size specified by the `jwe.WithMaxDecompressBufferSize` setting, +// which defaults to 10MB. If the decompressed payload is larger than this size, +// an error is returned. +// +// You can opt to change the MaxDecompressBufferSize setting globally, or on a +// per-call basis by passing the `jwe.WithMaxDecompressBufferSize` option to +// either `jwe.Settings()` or `jwe.Decrypt()`: +// +// jwe.Settings(jwe.WithMaxDecompressBufferSize(10*1024*1024)) // changes value globally +// jwe.Decrypt(..., jwe.WithMaxDecompressBufferSize(250*1024)) // changes just for this call func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) { var keyProviders []KeyProvider var keyUsed interface{} var cek *[]byte var dst *Message + perCallMaxDecompressBufferSize := maxDecompressBufferSize //nolint:forcetypeassert for _, option := range options { switch option.Ident() { @@ -506,6 +531,8 @@ func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) { }) case identCEK{}: cek = option.Value().(*[]byte) + case identMaxDecompressBufferSize{}: + perCallMaxDecompressBufferSize = option.Value().(int64) } } @@ -565,6 +592,7 @@ func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) { dctx.keyProviders = keyProviders dctx.protectedHeaders = h dctx.cek = cek + dctx.maxDecompressBufferSize = perCallMaxDecompressBufferSize var lastError error for _, recipient := range recipients { @@ -741,7 +769,7 @@ func (dctx *decryptCtx) decryptContent(ctx context.Context, alg jwa.KeyEncryptio } if h2.Compression() == jwa.Deflate { - buf, err := uncompress(plaintext) + buf, err := uncompress(plaintext, dctx.maxDecompressBufferSize) if err != nil { return nil, fmt.Errorf(`jwe.Derypt: failed to uncompress payload: %w`, err) } diff --git a/jwe/jwe_test.go b/jwe/jwe_test.go index 5f9a3bcb..d2a22ec9 100644 --- a/jwe/jwe_test.go +++ b/jwe/jwe_test.go @@ -971,3 +971,89 @@ func TestMaxBufferSize(t *testing.T) { _, err = jwe.Encrypt([]byte("Lorem Ipsum"), jwe.WithContentEncryption(jwa.A128CBC_HS256), jwe.WithKey(jwa.RSA_OAEP, key)) require.Error(t, err, `jwe.Encrypt should fail`) } + +func TestMaxDecompressBufferSize(t *testing.T) { + // This payload size is intentionally set to a small value to avoid + // causing problems for regular users and CI/CD systems. If you wish to + // verify that root issue is fixed, you may want to try increasing the + // payload size to a larger value. + const payloadSize = 1 << 16 + + privkey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, `rsa.GenerateKey should succeed`) + + pubkey := &privkey.PublicKey + + wrongPrivkey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, `rsa.GenerateKey should succeed`) + wrongPubkey := &wrongPrivkey.PublicKey + + payload := strings.Repeat("x", payloadSize) + + testcases := []struct { + Name string + GlobalMaxSize int64 + PublicKey *rsa.PublicKey + Error bool + ProcessDecryptOptions func([]jwe.DecryptOption) []jwe.DecryptOption + }{ + // This should work, because we set the MaxSize to be large (==payload size) + { + Name: "same as payload size", + GlobalMaxSize: payloadSize, + PublicKey: pubkey, + }, + // This should fail, because we set the GlobalMaxSize to be smaller than the payload size + { + Name: "smaller than payload size", + GlobalMaxSize: payloadSize - 1, + PublicKey: pubkey, + Error: true, + }, + // This should fail, because the public key does not match the + // private key used to decrypt the payload. In essence this way + // we do NOT trigger the root cause of this issue, but we bail out early + { + Name: "Wrong PublicKey", + GlobalMaxSize: payloadSize, + PublicKey: wrongPubkey, + Error: true, + }, + { + Name: "global=payloadSize-1, per-call=payloadSize", + GlobalMaxSize: payloadSize - 1, + PublicKey: pubkey, + ProcessDecryptOptions: func(options []jwe.DecryptOption) []jwe.DecryptOption { + return append(options, jwe.WithMaxDecompressBufferSize(payloadSize)) + }, + }, + // This should be the last test case to put the value back to default :) + { + Name: "Default 10MB globally", + GlobalMaxSize: 10 * 1024 * 1024, + PublicKey: pubkey, + }, + } + for _, tc := range testcases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + jwe.Settings(jwe.WithMaxDecompressBufferSize(tc.GlobalMaxSize)) + + encrypted, err := jwe.Encrypt([]byte(payload), jwe.WithKey(jwa.RSA_OAEP, tc.PublicKey), jwe.WithContentEncryption("A128CBC-HS256"), jwe.WithCompress(jwa.Deflate)) + + require.NoError(t, err, `jwe.Encrypt should succeed`) + + decryptOptions := []jwe.DecryptOption{jwe.WithKey(jwa.RSA_OAEP, privkey)} + + if fn := tc.ProcessDecryptOptions; fn != nil { + decryptOptions = fn(decryptOptions) + } + _, err = jwe.Decrypt(encrypted, decryptOptions...) + if tc.Error { + require.Error(t, err, `jwe.Decrypt should fail`) + } else { + require.NoError(t, err, `jwe.Decrypt should succeed`) + } + }) + } +} diff --git a/jwe/options.yaml b/jwe/options.yaml index 2de7fdd1..f465ab2e 100644 --- a/jwe/options.yaml +++ b/jwe/options.yaml @@ -4,6 +4,12 @@ interfaces: - name: GlobalOption comment: | GlobalOption describes options that changes global settings for this package + - name: GlobalDecryptOption + comment: | + GlobalDecryptOption describes options that changes global settings and for each call of the `jwe.Decrypt` function + methods: + - globalOption + - decryptOption - name: CompactOption comment: | CompactOption describes options that can be passed to `jwe.Compact` @@ -141,6 +147,18 @@ options: value of 10,000 is used. This option has a global effect. + - ident: MaxDecompressBufferSize + interface: GlobalDecryptOption + argument_type: int64 + comment: | + WithMaxDecompressBufferSize specifies the maximum buffer size for used when + decompressing the payload of a JWE message. If a compressed JWE payload + exceeds this amount when decompressed, jwe.Decrypt will return an error. + The default value is 10MB. + + This option can be used for `jwe.Settings()`, which changes the behavior + globally, or for `jwe.Decrypt()`, which changes the behavior for that + specific call. - ident: MaxBufferSize interface: GlobalOption argument_type: int64 @@ -149,4 +167,7 @@ options: calculations, such as when AES-CBC is performed. The default value is 256MB. If set to an invalid value, the default value is used. - This option has a global effect. \ No newline at end of file + This option has a global effect. + + Due to historical reasons this option has a vague name, but in future versions + it will be appropriately renamed. \ No newline at end of file diff --git a/jwe/options_gen.go b/jwe/options_gen.go index 3b74a8a9..6118fb4b 100644 --- a/jwe/options_gen.go +++ b/jwe/options_gen.go @@ -62,6 +62,21 @@ type encryptOption struct { func (*encryptOption) encryptOption() {} +// GlobalDecryptOption describes options that changes global settings and for each call of the `jwe.Decrypt` function +type GlobalDecryptOption interface { + Option + globalOption() + decryptOption() +} + +type globalDecryptOption struct { + Option +} + +func (*globalDecryptOption) globalOption() {} + +func (*globalDecryptOption) decryptOption() {} + // GlobalOption describes options that changes global settings for this package type GlobalOption interface { Option @@ -130,6 +145,7 @@ type identKey struct{} type identKeyProvider struct{} type identKeyUsed struct{} type identMaxBufferSize struct{} +type identMaxDecompressBufferSize struct{} type identMaxPBES2Count struct{} type identMergeProtectedHeaders struct{} type identMessage struct{} @@ -171,6 +187,10 @@ func (identMaxBufferSize) String() string { return "WithMaxBufferSize" } +func (identMaxDecompressBufferSize) String() string { + return "WithMaxDecompressBufferSize" +} + func (identMaxPBES2Count) String() string { return "WithMaxPBES2Count" } @@ -255,10 +275,25 @@ func WithKeyUsed(v interface{}) DecryptOption { // If set to an invalid value, the default value is used. // // This option has a global effect. +// +// Due to historical reasons this option has a vague name, but in future versions +// it will be appropriately renamed. func WithMaxBufferSize(v int64) GlobalOption { return &globalOption{option.New(identMaxBufferSize{}, v)} } +// WithMaxDecompressBufferSize specifies the maximum buffer size for used when +// decompressing the payload of a JWE message. If a compressed JWE payload +// exceeds this amount when decompressed, jwe.Decrypt will return an error. +// The default value is 10MB. +// +// This option can be used for `jwe.Settings()`, which changes the behavior +// globally, or for `jwe.Decrypt()`, which changes the behavior for that +// specific call. +func WithMaxDecompressBufferSize(v int64) GlobalDecryptOption { + return &globalDecryptOption{option.New(identMaxDecompressBufferSize{}, v)} +} + // WithMaxPBES2Count specifies the maximum number of PBES2 iterations // to use when decrypting a message. If not specified, the default // value of 10,000 is used. diff --git a/jwe/options_gen_test.go b/jwe/options_gen_test.go index b58d2362..a872d45f 100644 --- a/jwe/options_gen_test.go +++ b/jwe/options_gen_test.go @@ -17,6 +17,7 @@ func TestOptionIdent(t *testing.T) { require.Equal(t, "WithKeyProvider", identKeyProvider{}.String()) require.Equal(t, "WithKeyUsed", identKeyUsed{}.String()) require.Equal(t, "WithMaxBufferSize", identMaxBufferSize{}.String()) + require.Equal(t, "WithMaxDecompressBufferSize", identMaxDecompressBufferSize{}.String()) require.Equal(t, "WithMaxPBES2Count", identMaxPBES2Count{}.String()) require.Equal(t, "WithMergeProtectedHeaders", identMergeProtectedHeaders{}.String()) require.Equal(t, "WithMessage", identMessage{}.String())