Repository: lestrrat-go/jwx
Pull request #1011 (2023-10-30 04:30:21)

Add jwe.WithCEK

github-actions[bot]: This PR is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/lestrrat-go/jwx/pull/1011?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) Report
Attention: `6 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`0a15b4d`)](https://app.codecov.io/gh/lestrrat-go/jwx/commit/0a15b4d9a180b06c6924c4a03b39ae70d05514f4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) 72.00% compared to head [(`e0e9e1d`)](https://app.codecov.io/gh/lestrrat-go/jwx/pull/1011?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) 72.12%.
> Report is 1 commits behind head on develop/v2.

| [Files](https://app.codecov.io/gh/lestrrat-go/jwx/pull/1011?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) | Patch % | Lines |
|---|---|---|
| [jwe/jwe.go](https://app.codecov.io/gh/lestrrat-go/jwx/pull/1011?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andlL2p3ZS5nbw==) | 70.00% | [4 Missing and 2 partials :warning: ](https://app.codecov.io/gh/lestrrat-go/jwx/pull/1011?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) |

<details><summary>Additional details and impacted files</summary>


```diff
@@              Coverage Diff               @@
##           develop/v2    #1011      +/-   ##
==============================================
+ Coverage       72.00%   72.12%   +0.12%     
==============================================
  Files              93       93              
  Lines           13795    13816      +21     
==============================================
+ Hits             9933     9965      +32     
+ Misses           3044     3032      -12     
- Partials          818      819       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/lestrrat-go/jwx/pull/1011?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go).

lestrrat: Although the original poster has not responded, I think this is not a problematic addition, as long as using the static CEK can't be sneaked in by mistake. so I'll merge it

Patch:
diff --git a/Changes b/Changes
index 26959387..26dabe5c 100644
--- a/Changes
+++ b/Changes
@@ -4,6 +4,13 @@ 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.17 UNRELEASED
+[New Features]
+  * [jwe] (EXPERIMENTAL) `jwe.WithCEK` has been added to extract the content encryption key (CEK) from the Decrypt operation.
+  * [jwe] (EXPERIMENTAL) `jwe.EncryptStatic` has been added to encrypt content using a static CEK.
+    Using static CEKs has serious security implications, and you should not use
+    this unless you completely understand the risks involved.
+
 v2.0.16 31 Oct 2023
 [Security]
   * [jws] ECDSA signature verification requires us to check if the signature
diff --git a/examples/go.sum b/examples/go.sum
index 015a0662..046e430e 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -36,8 +36,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
-golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
-golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
+golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -59,21 +59,21 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
+golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
diff --git a/jwe/decrypt.go b/jwe/decrypt.go
index 387d4a99..8729e4e5 100644
--- a/jwe/decrypt.go
+++ b/jwe/decrypt.go
@@ -28,6 +28,7 @@ type decrypter struct {
 	aad         []byte
 	apu         []byte
 	apv         []byte
+	cek         *[]byte
 	computedAad []byte
 	iv          []byte
 	keyiv       []byte
@@ -120,6 +121,11 @@ func (d *decrypter) Tag(tag []byte) *decrypter {
 	return d
 }
 
+func (d *decrypter) CEK(ptr *[]byte) *decrypter {
+	d.cek = ptr
+	return d
+}
+
 func (d *decrypter) ContentCipher() (content_crypt.Cipher, error) {
 	if d.cipher == nil {
 		switch d.ctalg {
@@ -161,6 +167,9 @@ func (d *decrypter) Decrypt(recipient Recipient, ciphertext []byte, msg *Message
 		return
 	}
 
+	if d.cek != nil {
+		*d.cek = cek
+	}
 	return plaintext, nil
 }
 
diff --git a/jwe/jwe.go b/jwe/jwe.go
index 67b8e97b..4a7cb19e 100644
--- a/jwe/jwe.go
+++ b/jwe/jwe.go
@@ -247,6 +247,29 @@ func (b *recipientBuilder) Build(cek []byte, calg jwa.ContentEncryptionAlgorithm
 // Look for options that return `jwe.EncryptOption` or `jws.EncryptDecryptOption`
 // for a complete list of options that can be passed to this function.
 func Encrypt(payload []byte, options ...EncryptOption) ([]byte, error) {
+	return encrypt(payload, nil, options...)
+}
+
+// Encryptstatic is exactly like Encrypt, except it accepts a static
+// content encryption key (CEK). It is separated out from the main
+// Encrypt function such that the latter does not accidentally use a static
+// CEK.
+//
+// DO NOT attempt to use this function unless you completely understand the
+// security implications to using static CEKs. You have been warned.
+//
+// This function is currently considered EXPERIMENTAL, and is subject to
+// future changes across minor/micro versions.
+func EncryptStatic(payload, cek []byte, options ...EncryptOption) ([]byte, error) {
+	if len(cek) <= 0 {
+		return nil, fmt.Errorf(`jwe.EncryptStatic: empty CEK`)
+	}
+	return encrypt(payload, cek, options...)
+}
+
+// encrypt is separate so it can receive cek from outside.
+// (but we don't want to receive it in the options slice)
+func encrypt(payload, cek []byte, options ...EncryptOption) ([]byte, error) {
 	// default content encryption algorithm
 	calg := jwa.A256GCM
 
@@ -327,12 +350,14 @@ func Encrypt(payload []byte, options ...EncryptOption) ([]byte, error) {
 		return nil, fmt.Errorf(`jwe.Encrypt: failed to create AES encrypter: %w`, err)
 	}
 
-	generator := keygen.NewRandom(contentcrypt.KeySize())
-	bk, err := generator.Generate()
-	if err != nil {
-		return nil, fmt.Errorf(`jwe.Encrypt: failed to generate key: %w`, err)
+	if len(cek) <= 0 {
+		generator := keygen.NewRandom(contentcrypt.KeySize())
+		bk, err := generator.Generate()
+		if err != nil {
+			return nil, fmt.Errorf(`jwe.Encrypt: failed to generate key: %w`, err)
+		}
+		cek = bk.Bytes()
 	}
-	cek := bk.Bytes()
 
 	recipients := make([]Recipient, len(builders))
 	for i, builder := range builders {
@@ -421,6 +446,7 @@ func Encrypt(payload []byte, options ...EncryptOption) ([]byte, error) {
 type decryptCtx struct {
 	msg              *Message
 	aad              []byte
+	cek              *[]byte
 	computedAad      []byte
 	keyProviders     []KeyProvider
 	protectedHeaders Headers
@@ -438,7 +464,7 @@ type decryptCtx struct {
 func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) {
 	var keyProviders []KeyProvider
 	var keyUsed interface{}
-
+	var cek *[]byte
 	var dst *Message
 	//nolint:forcetypeassert
 	for _, option := range options {
@@ -459,6 +485,8 @@ func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) {
 				alg: alg,
 				key: pair.key,
 			})
+		case identCEK{}:
+			cek = option.Value().(*[]byte)
 		}
 	}
 
@@ -517,6 +545,7 @@ func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) {
 	dctx.msg = msg
 	dctx.keyProviders = keyProviders
 	dctx.protectedHeaders = h
+	dctx.cek = cek
 
 	var lastError error
 	for _, recipient := range recipients {
@@ -583,7 +612,8 @@ func (dctx *decryptCtx) decryptContent(ctx context.Context, alg jwa.KeyEncryptio
 		AuthenticatedData(dctx.aad).
 		ComputedAuthenticatedData(dctx.computedAad).
 		InitializationVector(dctx.msg.initializationVector).
-		Tag(dctx.msg.tag)
+		Tag(dctx.msg.tag).
+		CEK(dctx.cek)
 
 	if recipient.Headers().Algorithm() != alg {
 		// algorithms don't match
diff --git a/jwe/options.yaml b/jwe/options.yaml
index 84f89666..623a1866 100644
--- a/jwe/options.yaml
+++ b/jwe/options.yaml
@@ -119,4 +119,13 @@ options:
       `jwk.Key` here unless you are 100% sure that all keys that you
       have provided are instances of `jwk.Key` (remember that the
       jwx API allows users to specify a raw key such as *rsa.PublicKey)
-
+  - ident: CEK
+    interface: DecryptOption
+    argument_type: '*[]byte'
+    comment: |
+      WithCEK allows users to specify a variable to store the CEK used in the
+      message upon successful decryption. The variable must be a pointer to
+      a byte slice, and it will only be populated if the decryption is successful.
+      
+      This option is currently considered EXPERIMENTAL, and is subject to
+      future changes across minor/micro versions.
diff --git a/jwe/options_gen.go b/jwe/options_gen.go
index c22e2a5f..cdb22bef 100644
--- a/jwe/options_gen.go
+++ b/jwe/options_gen.go
@@ -110,6 +110,7 @@ type withKeySetSuboption struct {
 
 func (*withKeySetSuboption) withKeySetSuboption() {}
 
+type identCEK struct{}
 type identCompress struct{}
 type identContentEncryptionAlgorithm struct{}
 type identFS struct{}
@@ -124,6 +125,10 @@ type identProtectedHeaders struct{}
 type identRequireKid struct{}
 type identSerialization struct{}
 
+func (identCEK) String() string {
+	return "WithCEK"
+}
+
 func (identCompress) String() string {
 	return "WithCompress"
 }
@@ -176,6 +181,16 @@ func (identSerialization) String() string {
 	return "WithSerialization"
 }
 
+// WithCEK allows users to specify a variable to store the CEK used in the
+// message upon successful decryption. The variable must be a pointer to
+// a byte slice, and it will only be populated if the decryption is successful.
+//
+// This option is currently considered EXPERIMENTAL, and is subject to
+// future changes across minor/micro versions.
+func WithCEK(v *[]byte) DecryptOption {
+	return &decryptOption{option.New(identCEK{}, v)}
+}
+
 // WithCompress specifies the compression algorithm to use when encrypting
 // a payload using `jwe.Encrypt` (Yes, we know it can only be "" or "DEF",
 // but the way the specification is written it could allow for more options,


Test patch:
diff --git a/jwe/jwe_test.go b/jwe/jwe_test.go
index 4e8f6f80..59796e61 100644
--- a/jwe/jwe_test.go
+++ b/jwe/jwe_test.go
@@ -883,3 +883,31 @@ func TestGH924(t *testing.T) {
 	require.NoError(t, err, `jwe.Decrypt should succeed`)
 	require.Equal(t, payload, decrypted, `decrypt messages match`)
 }
+
+func TestGH1001(t *testing.T) {
+	rawKey, err := jwxtest.GenerateRsaKey()
+	require.NoError(t, err, `jwxtest.GenerateRsaKey should succeed`)
+
+	encrypted, err := jwe.Encrypt([]byte("Lorem Ipsum"), jwe.WithKey(jwa.RSA_OAEP, rawKey.PublicKey))
+	require.NoError(t, err, `jwe.Encrypt should succeed`)
+	var cek []byte
+	decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA_OAEP, rawKey), jwe.WithCEK(&cek))
+	require.NoError(t, err, `jwe.Decrypt should succeed`)
+
+	require.Equal(t, "Lorem Ipsum", string(decrypted), `decrypted message should match`)
+	require.NotNil(t, cek, `cek should not be nil`)
+
+	reEncrypted, err := jwe.EncryptStatic([]byte("Lorem Ipsum"), cek, jwe.WithKey(jwa.RSA_OAEP, rawKey.PublicKey))
+	require.NoError(t, err, `jwe.EncryptStatic should succeed`)
+
+	// sanity. empty CEKs should be rejected
+	_, err = jwe.EncryptStatic([]byte("Lorem Ipsum"), nil, jwe.WithKey(jwa.RSA_OAEP, rawKey.PublicKey))
+	require.Error(t, err, `jwe.Encryptstatic should fail with empty cek`)
+
+	cek = []byte(nil)
+	decrypted, err = jwe.Decrypt(reEncrypted, jwe.WithKey(jwa.RSA_OAEP, rawKey), jwe.WithCEK(&cek))
+	require.NoError(t, err, `jwe.Decrypt should succeed`)
+
+	require.Equal(t, "Lorem Ipsum", string(decrypted), `decrypted message should match`)
+	require.NotNil(t, cek, `cek should not be nil`)
+}
diff --git a/jwe/options_gen_test.go b/jwe/options_gen_test.go
index da3bbbfe..16984e25 100644
--- a/jwe/options_gen_test.go
+++ b/jwe/options_gen_test.go
@@ -9,6 +9,7 @@ import (
 )
 
 func TestOptionIdent(t *testing.T) {
+	require.Equal(t, "WithCEK", identCEK{}.String())
 	require.Equal(t, "WithCompress", identCompress{}.String())
 	require.Equal(t, "WithContentEncryption", identContentEncryptionAlgorithm{}.String())
 	require.Equal(t, "WithFS", identFS{}.String())