Repository: lestrrat-go/jwx
Pull request #584 (2022-02-28 08:25:39)

rework jwe.Encrypt as jwe.Encrypt([]byte, options...)
This implements part of #582 


Patch:
diff --git a/internal/cmd/genoptions/gen.sh b/internal/cmd/genoptions/gen.sh
new file mode 100755
index 00000000..732bfca4
--- /dev/null
+++ b/internal/cmd/genoptions/gen.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# Script to perform code generation. This exists to overcome
+# the fact that go:generate doesn't really allow you to change directories
+
+# This script is expected to be executed from the root directory of jwx
+
+set -e
+
+pushd internal/cmd/genoptions
+go build -o genoptions main.go
+popd
+
+./internal/cmd/genoptions/genoptions -objects=jwe/options.yaml
+# ./internal/cmd/genoptions/genoptions -objects=jws/options.yaml
+
+rm internal/cmd/genoptions/genoptions
diff --git a/internal/cmd/genoptions/go.mod b/internal/cmd/genoptions/go.mod
new file mode 100644
index 00000000..9e735a39
--- /dev/null
+++ b/internal/cmd/genoptions/go.mod
@@ -0,0 +1,22 @@
+module github.com/lestrrat-go/jwx/jwe/internal/cmd/genoptions
+
+go 1.17
+
+require (
+	github.com/goccy/go-yaml v1.9.5
+	github.com/lestrrat-go/codegen v1.0.3
+	github.com/lestrrat-go/jwx v1.2.19
+	github.com/lestrrat-go/option v1.0.0
+	github.com/lestrrat-go/xstrings v0.0.0-20210804220435-4dd8b234342b
+)
+
+require (
+	github.com/fatih/color v1.10.0 // indirect
+	github.com/mattn/go-colorable v0.1.8 // indirect
+	github.com/mattn/go-isatty v0.0.12 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	golang.org/x/mod v0.3.0 // indirect
+	golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect
+	golang.org/x/tools v0.0.0-20200918232735-d647fc253266 // indirect
+	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
+)
diff --git a/internal/cmd/genoptions/go.sum b/internal/cmd/genoptions/go.sum
new file mode 100644
index 00000000..78552fb9
--- /dev/null
+++ b/internal/cmd/genoptions/go.sum
@@ -0,0 +1,79 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
+github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
+github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
+github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/goccy/go-json v0.9.4/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-yaml v1.9.5 h1:Eh/+3uk9kLxG4koCX6lRMAPS1OaMSAi+FJcya0INdB0=
+github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
+github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
+github.com/lestrrat-go/codegen v1.0.3 h1:HviEmojOS51tH5L762bRMAZPj8DTIkwk24Il8M+54sE=
+github.com/lestrrat-go/codegen v1.0.3/go.mod h1:q3mmYUQW1bg3Z74ap7RgaVv1LmrBtlAKQOElxZTKlRM=
+github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
+github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
+github.com/lestrrat-go/jwx v1.2.19 h1:qxxLmAXNwZpTTvjc4PH21nT7I4wPK6lVv3lVNcZPnUk=
+github.com/lestrrat-go/jwx v1.2.19/go.mod h1:bWTBO7IHHVMtNunM8so9MT8wD+euEY1PzGEyCnuI2qM=
+github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
+github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
+github.com/lestrrat-go/xstrings v0.0.0-20210804220435-4dd8b234342b h1:3laG8JWIeDGb7lf00nMRznLdCHy0aZPd/CGz7Okn1SY=
+github.com/lestrrat-go/xstrings v0.0.0-20210804220435-4dd8b234342b/go.mod h1:mPFmD3Wuy0ddyPFvllLq4sUpGfE40T3VE8kWWS8fxGA=
+github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
+github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201217014255-9d1352758620 h1:3wPMTskHO3+O6jqTEXyFcsnuxMQOqYSaHsDxcbUXpqA=
+golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+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.0.0-20200918232735-d647fc253266 h1:k7tVuG0g1JwmD3Jh8oAl1vQ1C3jb4Hi/dUl1wWDBJpQ=
+golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/cmd/genoptions/main.go b/internal/cmd/genoptions/main.go
new file mode 100644
index 00000000..a387d07f
--- /dev/null
+++ b/internal/cmd/genoptions/main.go
@@ -0,0 +1,233 @@
+package main
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"os"
+	"sort"
+	"strings"
+
+	"github.com/goccy/go-yaml"
+	"github.com/lestrrat-go/codegen"
+	"github.com/lestrrat-go/xstrings"
+)
+
+var objectsFile = flag.String(`objects`, `objects.yaml`, `specify file containing object definitions`)
+
+func main() {
+	flag.Parse()
+
+	if err := _main(); err != nil {
+		fmt.Fprintf(os.Stderr, "%s\n", err)
+		os.Exit(1)
+	}
+}
+
+func writeComment(o *codegen.Output, comment string) bool {
+	comment = strings.TrimSpace(comment)
+	if comment == "" {
+		return false
+	}
+	for i, line := range strings.Split(comment, "\n") {
+		if i == 0 {
+			o.LL(`// %s`, line)
+		} else {
+			o.L(`// %s`, line)
+		}
+	}
+	return true
+}
+
+func _main() error {
+	var objects struct {
+		Output     string
+		Interfaces []*struct {
+			Name         string
+			Comment      string
+			ConcreteType string
+			Methods      []string
+		} `yaml:"interfaces"`
+		Options []*struct {
+			Ident         string
+			OptionName    string `yaml:"option_name"` // usually "With" + $Ident
+			SkipOption    bool   `yaml:"skip_option"`
+			ConcreteType  string
+			Interface     string
+			Comment       string
+			ArgumentType  string `yaml:"argument_type"`
+			ConstantValue string `yaml:"constant_value"`
+		} `yaml:"options"`
+	}
+
+	{
+		buf, err := os.ReadFile(*objectsFile)
+		if err != nil {
+			return err
+		}
+		if err := yaml.Unmarshal(buf, &objects); err != nil {
+			return err
+		}
+	}
+
+	for _, iface := range objects.Interfaces {
+		if iface.ConcreteType == "" {
+			iface.ConcreteType = xstrings.LcFirst(iface.Name)
+		}
+		if len(iface.Methods) == 0 {
+			iface.Methods = append(iface.Methods, iface.ConcreteType)
+		}
+	}
+
+	/*
+		var options = []*struct {
+			Ident        string
+			OptionName   string // usually "With" + $Ident
+			SkipOption   bool
+			ConcreteType string
+			Interface    string
+			Arguments    []OptionArgument
+		}{
+			{
+				Ident:     `Compress`,
+				Interface: `EncryptOption`,
+				Arguments: []OptionArgument{
+					{
+						Name: `alg`,
+						Type: `jwa.CompressionAlgorithm`,
+					},
+				},
+			},
+			{
+				Ident:     `ContentEncryption`,
+				Interface: `EncryptOption`,
+				Arguments: []OptionArgument{
+					{
+						Name: `alg`,
+						Type: `jwa.ContentEncryptionAlgorithm`,
+					},
+				},
+			},
+			{
+				Ident:     `Message`,
+				Interface: `DecryptOption`,
+				Arguments: []OptionArgument{
+					{
+						Name: `m`,
+						Type: `*Message`,
+					},
+				},
+			},
+			{
+				Ident:      `Serialization`,
+				Interface:  `EncryptOption`,
+				SkipOption: true,
+			},
+		}
+	*/
+
+	for _, option := range objects.Options {
+		if option.OptionName == "" {
+			option.OptionName = `With` + option.Ident
+		}
+		if option.ConcreteType == "" {
+			option.ConcreteType = xstrings.LcFirst(option.Interface)
+		}
+	}
+
+	sort.Slice(objects.Interfaces, func(i, j int) bool {
+		return objects.Interfaces[i].Name < objects.Interfaces[j].Name
+	})
+	sort.Slice(objects.Options, func(i, j int) bool {
+		return objects.Options[i].Ident < objects.Options[j].Ident
+	})
+
+	var buf bytes.Buffer
+
+	o := codegen.NewOutput(&buf)
+
+	o.L("// This file is auto-generated by internal/cmd/genoptions/main.go. DO NOT EDIT")
+
+	o.LL(`package jwe`)
+
+	o.LL(`type Option = option.Interface`)
+
+	for _, iface := range objects.Interfaces {
+		if writeComment(o, iface.Comment) {
+			o.L(`type %s interface {`, iface.Name)
+		} else {
+			o.LL(`type %s interface {`, iface.Name)
+		}
+		o.L(`Option`)
+		for _, method := range iface.Methods {
+			o.L(`%s()`, method)
+		}
+		o.L(`}`)
+
+		o.LL(`type %s struct {`, iface.ConcreteType)
+		o.L(`Option`)
+		o.L(`}`)
+
+		for _, method := range iface.Methods {
+			o.LL(`func (*%s) %s() {}`, iface.ConcreteType, method)
+		}
+	}
+
+	o.L(``)
+
+	{
+		seen := make(map[string]struct{})
+		for _, option := range objects.Options {
+			_, ok := seen[option.Ident]
+			if !ok {
+				o.L(`type ident%s struct{}`, option.Ident)
+				seen[option.Ident] = struct{}{}
+			}
+		}
+	}
+
+	{
+		seen := make(map[string]struct{})
+		for _, option := range objects.Options {
+			_, ok := seen[option.Ident]
+			if !ok {
+				o.LL(`func (ident%s) String() string {`, option.Ident)
+				o.L(`return %q`, option.OptionName)
+				o.L(`}`)
+				seen[option.Ident] = struct{}{}
+			}
+		}
+	}
+
+	for _, option := range objects.Options {
+		if option.SkipOption {
+			continue
+		}
+
+		if writeComment(o, option.Comment) {
+			o.L(`func %s(`, option.OptionName)
+		} else {
+			o.LL(`func %s(`, option.OptionName)
+		}
+		if argType := option.ArgumentType; argType != "" {
+			o.R(`v %s`, argType)
+		}
+		o.R(`) %s {`, option.Interface)
+
+		value := `v`
+		if cv := option.ConstantValue; cv != "" {
+			value = cv
+		}
+
+		o.L(`return &%s{option.New(ident%s{}, %s)}`, option.ConcreteType, option.Ident, value)
+		o.L(`}`)
+	}
+
+	if err := o.WriteFile(objects.Output, codegen.WithFormatCode(true)); err != nil {
+		if cfe, ok := err.(codegen.CodeFormatError); ok {
+			fmt.Fprint(os.Stderr, cfe.Source())
+		}
+		return fmt.Errorf(`failed to write to headers_gen.go: %w`, err)
+	}
+	return nil
+}
diff --git a/jwe/compress.go b/jwe/compress.go
index e3836a69..d5690b23 100644
--- a/jwe/compress.go
+++ b/jwe/compress.go
@@ -6,7 +6,6 @@ import (
 	"io/ioutil"
 
 	"github.com/lestrrat-go/jwx/internal/pool"
-	"github.com/lestrrat-go/jwx/jwa"
 	"github.com/pkg/errors"
 )
 
@@ -14,11 +13,7 @@ func uncompress(plaintext []byte) ([]byte, error) {
 	return ioutil.ReadAll(flate.NewReader(bytes.NewReader(plaintext)))
 }
 
-func compress(plaintext []byte, alg jwa.CompressionAlgorithm) ([]byte, error) {
-	if alg == jwa.NoCompress {
-		return plaintext, nil
-	}
-
+func compress(plaintext []byte) ([]byte, error) {
 	buf := pool.GetBytesBuffer()
 	defer pool.ReleaseBytesBuffer(buf)
 
diff --git a/jwe/encrypt.go b/jwe/encrypt.go
deleted file mode 100644
index 0b1b40bf..00000000
--- a/jwe/encrypt.go
+++ /dev/null
@@ -1,144 +0,0 @@
-package jwe
-
-import (
-	"context"
-	"sync"
-
-	"github.com/lestrrat-go/jwx/internal/base64"
-	"github.com/lestrrat-go/jwx/jwa"
-	"github.com/pkg/errors"
-)
-
-var encryptCtxPool = sync.Pool{
-	New: func() interface{} {
-		return &encryptCtx{}
-	},
-}
-
-func getEncryptCtx() *encryptCtx {
-	return encryptCtxPool.Get().(*encryptCtx)
-}
-
-func releaseEncryptCtx(ctx *encryptCtx) {
-	ctx.protected = nil
-	ctx.contentEncrypter = nil
-	ctx.generator = nil
-	ctx.keyEncrypters = nil
-	ctx.compress = jwa.NoCompress
-	encryptCtxPool.Put(ctx)
-}
-
-// Encrypt takes the plaintext and encrypts into a JWE message.
-func (e encryptCtx) Encrypt(plaintext []byte) (*Message, error) {
-	bk, err := e.generator.Generate()
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to generate key")
-	}
-	cek := bk.Bytes()
-
-	if e.protected == nil {
-		// shouldn't happen, but...
-		e.protected = NewHeaders()
-	}
-
-	if err := e.protected.Set(ContentEncryptionKey, e.contentEncrypter.Algorithm()); err != nil {
-		return nil, errors.Wrap(err, `failed to set "enc" in protected header`)
-	}
-
-	compression := e.compress
-	if compression != jwa.NoCompress {
-		if err := e.protected.Set(CompressionKey, compression); err != nil {
-			return nil, errors.Wrap(err, `failed to set "zip" in protected header`)
-		}
-	}
-
-	// In JWE, multiple recipients may exist -- they receive an
-	// encrypted version of the CEK, using their key encryption
-	// algorithm of choice.
-	recipients := make([]Recipient, len(e.keyEncrypters))
-	for i, enc := range e.keyEncrypters {
-		r := NewRecipient()
-		if err := r.Headers().Set(AlgorithmKey, enc.Algorithm()); err != nil {
-			return nil, errors.Wrap(err, "failed to set header")
-		}
-		if v := enc.KeyID(); v != "" {
-			if err := r.Headers().Set(KeyIDKey, v); err != nil {
-				return nil, errors.Wrap(err, "failed to set header")
-			}
-		}
-
-		enckey, err := enc.Encrypt(cek)
-		if err != nil {
-			return nil, errors.Wrap(err, `failed to encrypt key`)
-		}
-		if enc.Algorithm() == jwa.ECDH_ES || enc.Algorithm() == jwa.DIRECT {
-			if len(e.keyEncrypters) > 1 {
-				return nil, errors.Errorf("unable to support multiple recipients for ECDH-ES")
-			}
-			cek = enckey.Bytes()
-		} else {
-			if err := r.SetEncryptedKey(enckey.Bytes()); err != nil {
-				return nil, errors.Wrap(err, "failed to set encrypted key")
-			}
-		}
-		if hp, ok := enckey.(populater); ok {
-			if err := hp.Populate(r.Headers()); err != nil {
-				return nil, errors.Wrap(err, "failed to populate")
-			}
-		}
-		recipients[i] = r
-	}
-
-	// If there's only one recipient, you want to include that in the
-	// protected header
-	if len(recipients) == 1 {
-		h, err := e.protected.Merge(context.TODO(), recipients[0].Headers())
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to merge protected headers")
-		}
-		e.protected = h
-	}
-
-	aad, err := e.protected.Encode()
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to base64 encode protected headers")
-	}
-
-	plaintext, err = compress(plaintext, compression)
-	if err != nil {
-		return nil, errors.Wrap(err, `failed to compress payload before encryption`)
-	}
-
-	// ...on the other hand, there's only one content cipher.
-	iv, ciphertext, tag, err := e.contentEncrypter.Encrypt(cek, plaintext, aad)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to encrypt payload")
-	}
-
-	msg := NewMessage()
-
-	decodedAad, err := base64.Decode(aad)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to decode base64")
-	}
-	if err := msg.Set(AuthenticatedDataKey, decodedAad); err != nil {
-		return nil, errors.Wrapf(err, `failed to set %s`, AuthenticatedDataKey)
-	}
-	if err := msg.Set(CipherTextKey, ciphertext); err != nil {
-		return nil, errors.Wrapf(err, `failed to set %s`, CipherTextKey)
-	}
-	if err := msg.Set(InitializationVectorKey, iv); err != nil {
-		return nil, errors.Wrapf(err, `failed to set %s`, InitializationVectorKey)
-	}
-	if err := msg.Set(ProtectedHeadersKey, e.protected); err != nil {
-		return nil, errors.Wrapf(err, `failed to set %s`, ProtectedHeadersKey)
-	}
-	if err := msg.Set(RecipientsKey, recipients); err != nil {
-		return nil, errors.Wrapf(err, `failed to set %s`, RecipientsKey)
-	}
-	if err := msg.Set(TagKey, tag); err != nil {
-		return nil, errors.Wrapf(err, `failed to set %s`, TagKey)
-	}
-
-	return msg, nil
-}
diff --git a/jwe/interface.go b/jwe/interface.go
index c2312088..1e22cdcd 100644
--- a/jwe/interface.go
+++ b/jwe/interface.go
@@ -3,8 +3,6 @@ package jwe
 import (
 	"github.com/lestrrat-go/iter/mapiter"
 	"github.com/lestrrat-go/jwx/internal/iter"
-	"github.com/lestrrat-go/jwx/jwa"
-	"github.com/lestrrat-go/jwx/jwe/internal/keyenc"
 	"github.com/lestrrat-go/jwx/jwe/internal/keygen"
 )
 
@@ -17,7 +15,25 @@ type Recipient interface {
 }
 
 type stdRecipient struct {
-	headers      Headers
+	// Comments on each field are taken from https://datatracker.ietf.org/doc/html/rfc7516
+	//
+	// header
+	//    The "header" member MUST be present and contain the value JWE Per-
+	//    Recipient Unprotected Header when the JWE Per-Recipient
+	//    Unprotected Header value is non-empty; otherwise, it MUST be
+	//    absent.  This value is represented as an unencoded JSON object,
+	//    rather than as a string.  These Header Parameter values are not
+	//    integrity protected.
+	//
+	// At least one of the "header", "protected", and "unprotected" members
+	// MUST be present so that "alg" and "enc" Header Parameter values are
+	// conveyed for each recipient computation.
+	headers Headers
+
+	// encrypted_key
+	//    The "encrypted_key" member MUST be present and contain the value
+	//    BASE64URL(JWE Encrypted Key) when the JWE Encrypted Key value is
+	//    non-empty; otherwise, it MUST be absent.
 	encryptedKey []byte
 }
 
@@ -42,13 +58,77 @@ type stdRecipient struct {
 // if we tried to re-calculate it from a parsed message.
 //nolint:govet
 type Message struct {
-	authenticatedData    []byte
-	cipherText           []byte
+	// Comments on each field are taken from https://datatracker.ietf.org/doc/html/rfc7516
+	//
+	// protected
+	//    The "protected" member MUST be present and contain the value
+	//    BASE64URL(UTF8(JWE Protected Header)) when the JWE Protected
+	//    Header value is non-empty; otherwise, it MUST be absent.  These
+	//    Header Parameter values are integrity protected.
+	protectedHeaders Headers
+
+	// unprotected
+	//    The "unprotected" member MUST be present and contain the value JWE
+	//    Shared Unprotected Header when the JWE Shared Unprotected Header
+	//    value is non-empty; otherwise, it MUST be absent.  This value is
+	//    represented as an unencoded JSON object, rather than as a string.
+	//    These Header Parameter values are not integrity protected.
+	unprotectedHeaders Headers
+
+	// iv
+	//    The "iv" member MUST be present and contain the value
+	//    BASE64URL(JWE Initialization Vector) when the JWE Initialization
+	//    Vector value is non-empty; otherwise, it MUST be absent.
 	initializationVector []byte
-	tag                  []byte
-	recipients           []Recipient
-	protectedHeaders     Headers
-	unprotectedHeaders   Headers
+
+	// aad
+	//    The "aad" member MUST be present and contain the value
+	//    BASE64URL(JWE AAD)) when the JWE AAD value is non-empty;
+	//    otherwise, it MUST be absent.  A JWE AAD value can be included to
+	//    supply a base64url-encoded value to be integrity protected but not
+	//    encrypted.
+	authenticatedData []byte
+
+	// ciphertext
+	//    The "ciphertext" member MUST be present and contain the value
+	//    BASE64URL(JWE Ciphertext).
+	cipherText []byte
+
+	// tag
+	//    The "tag" member MUST be present and contain the value
+	//    BASE64URL(JWE Authentication Tag) when the JWE Authentication Tag
+	//    value is non-empty; otherwise, it MUST be absent.
+	tag []byte
+
+	// recipients
+	//    The "recipients" member value MUST be an array of JSON objects.
+	//    Each object contains information specific to a single recipient.
+	//    This member MUST be present with exactly one array element per
+	//    recipient, even if some or all of the array element values are the
+	//    empty JSON object "{}" (which can happen when all Header Parameter
+	//    values are shared between all recipients and when no encrypted key
+	//    is used, such as when doing Direct Encryption).
+	//
+	// Some Header Parameters, including the "alg" parameter, can be shared
+	// among all recipient computations.  Header Parameters in the JWE
+	// Protected Header and JWE Shared Unprotected Header values are shared
+	// among all recipients.
+	//
+	// The Header Parameter values used when creating or validating per-
+	// recipient ciphertext and Authentication Tag values are the union of
+	// the three sets of Header Parameter values that may be present: (1)
+	// the JWE Protected Header represented in the "protected" member, (2)
+	// the JWE Shared Unprotected Header represented in the "unprotected"
+	// member, and (3) the JWE Per-Recipient Unprotected Header represented
+	// in the "header" member of the recipient's array element.  The union
+	// of these sets of Header Parameters comprises the JOSE Header.  The
+	// Header Parameter names in the three locations MUST be disjoint.
+	recipients []Recipient
+
+	// TODO: Additional members can be present in both the JSON objects defined
+	// above; if not understood by implementations encountering them, they
+	// MUST be ignored.
+	// privateParams map[string]interface{}
 
 	// These two fields below are not available for the public consumers of this object.
 	// rawProtectedHeaders stores the original protected header buffer
@@ -59,22 +139,6 @@ type Message struct {
 	storeProtectedHeaders bool
 }
 
-// contentEncrypter encrypts the content using the content using the
-// encrypted key
-type contentEncrypter interface {
-	Algorithm() jwa.ContentEncryptionAlgorithm
-	Encrypt([]byte, []byte, []byte) ([]byte, []byte, []byte, error)
-}
-
-//nolint:govet
-type encryptCtx struct {
-	keyEncrypters    []keyenc.Encrypter
-	protected        Headers
-	contentEncrypter contentEncrypter
-	generator        keygen.Generator
-	compress         jwa.CompressionAlgorithm
-}
-
 // populater is an interface for things that may modify the
 // JWE header. e.g. ByteWithECPrivateKey
 type populater interface {
diff --git a/jwe/internal/cipher/cipher.go b/jwe/internal/cipher/cipher.go
index 586feff8..19391f1b 100644
--- a/jwe/internal/cipher/cipher.go
+++ b/jwe/internal/cipher/cipher.go
@@ -83,7 +83,7 @@ func NewAES(alg jwa.ContentEncryptionAlgorithm) (*AesContentCipher, error) {
 	}, nil
 }
 
-func (c AesContentCipher) Encrypt(cek, plaintext, aad []byte) (iv, ciphertext, tag []byte, err error) {
+func (c AesContentCipher) Encrypt(cek, plaintext, aad []byte) (iv, ciphertxt, tag []byte, err error) {
 	var aead cipher.AEAD
 	aead, err = c.fetch.Fetch(cek)
 	if err != nil {
@@ -124,8 +124,8 @@ func (c AesContentCipher) Encrypt(cek, plaintext, aad []byte) (iv, ciphertext, t
 	}
 
 	tag = combined[tagoffset:]
-	ciphertext = make([]byte, tagoffset)
-	copy(ciphertext, combined[:tagoffset])
+	ciphertxt = make([]byte, tagoffset)
+	copy(ciphertxt, combined[:tagoffset])
 
 	return
 }
diff --git a/jwe/jwe.go b/jwe/jwe.go
index 2e184b1e..88c6b814 100644
--- a/jwe/jwe.go
+++ b/jwe/jwe.go
@@ -5,8 +5,10 @@ package jwe
 
 import (
 	"bytes"
+	"context"
 	"crypto/ecdsa"
 	"crypto/rsa"
+	"fmt"
 	"io"
 	"io/ioutil"
 
@@ -23,90 +25,86 @@ import (
 	"github.com/pkg/errors"
 )
 
-var registry = json.NewRegistry()
+const (
+	fmtInvalid = iota
+	fmtCompact
+	fmtJSON
+	fmtJSONPretty
+	fmtMax
+)
 
-// Encrypt takes the plaintext payload and encrypts it in JWE compact format.
-// `key` should be a public key, and it may be a raw key (e.g. rsa.PublicKey) or a jwk.Key
-//
-// `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
-// `jwa.KeyEncryptionAlgorithm` or otherwise it will cause an error.
-//
-// Encrypt currently does not support multi-recipient messages.
-func Encrypt(payload []byte, alg jwa.KeyAlgorithm, key interface{}, contentalg jwa.ContentEncryptionAlgorithm, compressalg jwa.CompressionAlgorithm, options ...EncryptOption) ([]byte, error) {
-	keyalg, ok := alg.(jwa.KeyEncryptionAlgorithm)
-	if !ok {
-		return nil, errors.Errorf(`expected alg to be jwa.KeyEncryptionAlgorithm, but got %T`, alg)
-	}
+var _ = fmtInvalid
+var _ = fmtMax
 
-	var protected Headers
-	for _, option := range options {
-		//nolint:forcetypeassert
-		switch option.Ident() {
-		case identProtectedHeader{}:
-			protected = option.Value().(Headers)
-		}
-	}
-	if protected == nil {
-		protected = NewHeaders()
-	}
+var registry = json.NewRegistry()
 
-	contentcrypt, err := content_crypt.NewGeneric(contentalg)
-	if err != nil {
-		return nil, errors.Wrap(err, `failed to create AES encrypter`)
-	}
+type recipientBuilder struct {
+	alg     jwa.KeyEncryptionAlgorithm
+	key     interface{}
+	headers Headers
+}
+
+func (b *recipientBuilder) Build(cek []byte, calg jwa.ContentEncryptionAlgorithm, cc *content_crypt.Generic) (Recipient, []byte, error) {
+	// we need the raw key
+	rawKey := b.key
 
 	var keyID string
-	if jwkKey, ok := key.(jwk.Key); ok {
+	if jwkKey, ok := b.key.(jwk.Key); ok {
+		// Meanwhile, grab the kid as well
 		keyID = jwkKey.KeyID()
 
 		var raw interface{}
 		if err := jwkKey.Raw(&raw); err != nil {
-			return nil, errors.Wrapf(err, `failed to retrieve raw key out of %T`, key)
+			return nil, nil, fmt.Errorf(`failed to retrieve raw key out of %T: %w`, b.key, err)
 		}
 
-		key = raw
+		rawKey = raw
 	}
 
+	// First, create a key encryptor
 	var enc keyenc.Encrypter
-	switch keyalg {
+	switch b.alg {
 	case jwa.RSA1_5:
 		var pubkey rsa.PublicKey
-		if err := keyconv.RSAPublicKey(&pubkey, key); err != nil {
-			return nil, errors.Wrapf(err, "failed to generate public key from key (%T)", key)
+		if err := keyconv.RSAPublicKey(&pubkey, rawKey); err != nil {
+			return nil, nil, fmt.Errorf(`failed to generate public key from key (%T): %w`, rawKey, err)
 		}
 
-		enc, err = keyenc.NewRSAPKCSEncrypt(keyalg, &pubkey)
+		v, err := keyenc.NewRSAPKCSEncrypt(b.alg, &pubkey)
 		if err != nil {
-			return nil, errors.Wrap(err, "failed to create RSA PKCS encrypter")
+			return nil, nil, fmt.Errorf(`failed to create RSA PKCS encrypter: %w`, err)
 		}
+		enc = v
 	case jwa.RSA_OAEP, jwa.RSA_OAEP_256:
 		var pubkey rsa.PublicKey
-		if err := keyconv.RSAPublicKey(&pubkey, key); err != nil {
-			return nil, errors.Wrapf(err, "failed to generate public key from key (%T)", key)
+		if err := keyconv.RSAPublicKey(&pubkey, rawKey); err != nil {
+			return nil, nil, fmt.Errorf(`failed to generate public key from key (%T): %w`, rawKey, err)
 		}
 
-		enc, err = keyenc.NewRSAOAEPEncrypt(keyalg, &pubkey)
+		v, err := keyenc.NewRSAOAEPEncrypt(b.alg, &pubkey)
 		if err != nil {
-			return nil, errors.Wrap(err, "failed to create RSA OAEP encrypter")
+			return nil, nil, fmt.Errorf(`failed to create RSA OAEP encrypter: %w`, err)
 		}
+		enc = v
 	case jwa.A128KW, jwa.A192KW, jwa.A256KW,
 		jwa.A128GCMKW, jwa.A192GCMKW, jwa.A256GCMKW,
 		jwa.PBES2_HS256_A128KW, jwa.PBES2_HS384_A192KW, jwa.PBES2_HS512_A256KW:
-		sharedkey, ok := key.([]byte)
+		sharedkey, ok := rawKey.([]byte)
 		if !ok {
-			return nil, errors.New("invalid key: []byte required")
+			return nil, nil, fmt.Errorf(`invalid key: []byte required (%T)`, rawKey)
 		}
-		switch keyalg {
+
+		var err error
+		switch b.alg {
 		case jwa.A128KW, jwa.A192KW, jwa.A256KW:
-			enc, err = keyenc.NewAES(keyalg, sharedkey)
+			enc, err = keyenc.NewAES(b.alg, sharedkey)
 		case jwa.PBES2_HS256_A128KW, jwa.PBES2_HS384_A192KW, jwa.PBES2_HS512_A256KW:
-			enc, err = keyenc.NewPBES2Encrypt(keyalg, sharedkey)
+			enc, err = keyenc.NewPBES2Encrypt(b.alg, sharedkey)
 		default:
-			enc, err = keyenc.NewAESGCMEncrypt(keyalg, sharedkey)
+			enc, err = keyenc.NewAESGCMEncrypt(b.alg, sharedkey)
 		}
 		if err != nil {
-			return nil, errors.Wrap(err, "failed to create key wrap encrypter")
+			return nil, nil, fmt.Errorf(`failed to create key wrap encrypter: %w`, err)
 		}
 		// NOTE: there was formerly a restriction, introduced
 		// in PR #26, which disallowed certain key/content
@@ -114,12 +112,12 @@ func Encrypt(payload []byte, alg jwa.KeyAlgorithm, key interface{}, contentalg j
 		// interop with the jose tool demonstrates it.
 	case jwa.ECDH_ES, jwa.ECDH_ES_A128KW, jwa.ECDH_ES_A192KW, jwa.ECDH_ES_A256KW:
 		var keysize int
-		switch keyalg {
+		switch b.alg {
 		case jwa.ECDH_ES:
 			// https://tools.ietf.org/html/rfc7518#page-15
 			// In Direct Key Agreement mode, the output of the Concat KDF MUST be a
 			// key of the same length as that used by the "enc" algorithm.
-			keysize = contentcrypt.KeySize()
+			keysize = cc.KeySize()
 		case jwa.ECDH_ES_A128KW:
 			keysize = 16
 		case jwa.ECDH_ES_A192KW:
@@ -128,48 +126,227 @@ func Encrypt(payload []byte, alg jwa.KeyAlgorithm, key interface{}, contentalg j
 			keysize = 32
 		}
 
-		switch key := key.(type) {
+		switch key := rawKey.(type) {
 		case x25519.PublicKey:
-			enc, err = keyenc.NewECDHESEncrypt(keyalg, contentalg, keysize, key)
+			v, err := keyenc.NewECDHESEncrypt(b.alg, calg, keysize, rawKey)
+			if err != nil {
+				return nil, nil, fmt.Errorf(`failed to create ECDHS key wrap encrypter: %w`, err)
+			}
+			enc = v
 		default:
 			var pubkey ecdsa.PublicKey
-			if err := keyconv.ECDSAPublicKey(&pubkey, key); err != nil {
-				return nil, errors.Wrapf(err, "failed to generate public key from key (%T)", key)
+			if err := keyconv.ECDSAPublicKey(&pubkey, rawKey); err != nil {
+				return nil, nil, fmt.Errorf(`failed to generate public key from key (%T): %w`, key, err)
 			}
-			enc, err = keyenc.NewECDHESEncrypt(keyalg, contentalg, keysize, &pubkey)
-		}
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to create ECDHS key wrap encrypter")
+			v, err := keyenc.NewECDHESEncrypt(b.alg, calg, keysize, &pubkey)
+			if err != nil {
+				return nil, nil, fmt.Errorf(`failed to create ECDHS key wrap encrypter: %w`, err)
+			}
+			enc = v
 		}
 	case jwa.DIRECT:
-		sharedkey, ok := key.([]byte)
+		sharedkey, ok := rawKey.([]byte)
 		if !ok {
-			return nil, errors.New("invalid key: []byte required")
+			return nil, nil, fmt.Errorf("invalid key: []byte required")
 		}
-		enc, _ = keyenc.NewNoop(keyalg, sharedkey)
+		enc, _ = keyenc.NewNoop(b.alg, sharedkey)
 	default:
-		return nil, errors.Errorf(`invalid key encryption algorithm (%s)`, keyalg)
+		return nil, nil, fmt.Errorf(`invalid key encryption algorithm (%s)`, b.alg)
 	}
 
 	if keyID != "" {
 		enc.SetKeyID(keyID)
 	}
 
-	keysize := contentcrypt.KeySize()
-	encctx := getEncryptCtx()
-	defer releaseEncryptCtx(encctx)
+	r := NewRecipient()
+
+	if err := r.Headers().Set(AlgorithmKey, b.alg); err != nil {
+		return nil, nil, fmt.Errorf(`failed to set header: %w`, err)
+	}
+	if v := enc.KeyID(); v != "" {
+		if err := r.Headers().Set(KeyIDKey, v); err != nil {
+			return nil, nil, fmt.Errorf(`failed to set header: %w`, err)
+		}
+	}
+
+	var rawCEK []byte
+	enckey, err := enc.Encrypt(cek)
+	if err != nil {
+		return nil, nil, fmt.Errorf(`failed to encrypt key: %w`, err)
+	}
+	if enc.Algorithm() == jwa.ECDH_ES || enc.Algorithm() == jwa.DIRECT {
+		rawCEK = enckey.Bytes()
+	} else {
+		if err := r.SetEncryptedKey(enckey.Bytes()); err != nil {
+			return nil, nil, fmt.Errorf(`failed to set encrypted key: %w`, err)
+		}
+	}
+
+	if hp, ok := enckey.(populater); ok {
+		if err := hp.Populate(r.Headers()); err != nil {
+			return nil, nil, fmt.Errorf(`failed to populate: %w`, err)
+		}
+	}
+
+	return r, rawCEK, nil
+}
+
+func Encrypt(payload []byte, options ...EncryptOption) ([]byte, error) {
+	// default content encryption algorithm
+	calg := jwa.A256GCM
+
+	// default compression is "none"
+	compression := jwa.NoCompress
+
+	format := fmtCompact
+
+	// builds each "recipient" with encrypted_key and headers
+	var builders []*recipientBuilder
+
+	var protected Headers
+	var useRawCEK bool
+	for _, option := range options {
+		//nolint:forcetypeassert
+		switch option.Ident() {
+		case identKey{}:
+			data := option.Value().(*withKey)
+			v, ok := data.alg.(jwa.KeyEncryptionAlgorithm)
+			if !ok {
+				return nil, fmt.Errorf(`jwe.Encrypt: expected alg to be jwa.KeyEncryptionAlgorithm, but got %T`, data.alg)
+			}
+
+			switch v {
+			case jwa.DIRECT, jwa.ECDH_ES:
+				useRawCEK = true
+			}
+
+			builders = append(builders, &recipientBuilder{
+				alg:     v,
+				key:     data.key,
+				headers: data.headers,
+			})
+		case identContentEncryptionAlgorithm{}:
+			calg = option.Value().(jwa.ContentEncryptionAlgorithm)
+		case identCompress{}:
+			compression = option.Value().(jwa.CompressionAlgorithm)
+		case identProtectedHeaders{}:
+			protected = option.Value().(Headers)
+		case identSerialization{}:
+			format = option.Value().(int)
+		}
+	}
+
+	// We need to have at least one builder
+	if len(builders) == 0 {
+		return nil, fmt.Errorf(`jwe.Encrypt: missing key encryption builders: use jwe.WithKey() to specify one`)
+	}
+
+	if useRawCEK {
+		if len(builders) != 1 {
+			return nil, fmt.Errorf(`jwe.Encrypt: multiple recipients for ECDH-ES/DIRECT mode supported`)
+		}
+	}
+
+	// There is exactly one content encrypter.
+	contentcrypt, err := content_crypt.NewGeneric(calg)
+	if err != nil {
+		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)
+	}
+	cek := bk.Bytes()
+
+	recipients := make([]Recipient, len(builders))
+	for i, builder := range builders {
+		// some builders require hint from the contentcrypt object
+		r, rawCEK, err := builder.Build(cek, calg, contentcrypt)
+		if err != nil {
+			return nil, fmt.Errorf(`jwe.Encrypt: failed to create recipient #%d: %w`, i, err)
+		}
+		recipients[i] = r
+
+		// Kinda feels weird, but if useRawCEK == true, we asserted earlier
+		// that len(builders) == 1, so this is OK
+		if useRawCEK {
+			cek = rawCEK
+		}
+	}
+
+	if protected == nil {
+		protected = NewHeaders()
+	}
+
+	if err := protected.Set(ContentEncryptionKey, calg); err != nil {
+		return nil, fmt.Errorf(`jwe.Encrypt: failed to set "enc" in protected header: %w`, err)
+	}
+
+	if compression != jwa.NoCompress {
+		payload, err = compress(payload)
+		if err != nil {
+			return nil, fmt.Errorf(`jwe.Encrypt: failed to compress payload before encryption: %w`, err)
+		}
+		if err := protected.Set(CompressionKey, compression); err != nil {
+			return nil, fmt.Errorf(`jwe.Encrypt: failed to set "zip" in protected header: %w`, err)
+		}
+	}
+
+	// If there's only one recipient, you want to include that in the
+	// protected header
+	if len(recipients) == 1 {
+		h, err := protected.Merge(context.TODO(), recipients[0].Headers())
+		if err != nil {
+			return nil, fmt.Errorf(`jwe.Encrypt: failed to merge protected headers: %w`, err)
+		}
+		protected = h
+	}
+
+	aad, err := protected.Encode()
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to base64 encode protected headers")
+	}
 
-	encctx.protected = protected
-	encctx.contentEncrypter = contentcrypt
-	encctx.generator = keygen.NewRandom(keysize)
-	encctx.keyEncrypters = []keyenc.Encrypter{enc}
-	encctx.compress = compressalg
-	msg, err := encctx.Encrypt(payload)
+	iv, ciphertext, tag, err := contentcrypt.Encrypt(cek, payload, aad)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to encrypt payload")
 	}
 
-	return Compact(msg)
+	msg := NewMessage()
+
+	decodedAad, err := base64.Decode(aad)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to decode base64")
+	}
+	if err := msg.Set(AuthenticatedDataKey, decodedAad); err != nil {
+		return nil, errors.Wrapf(err, `failed to set %s`, AuthenticatedDataKey)
+	}
+	if err := msg.Set(CipherTextKey, ciphertext); err != nil {
+		return nil, errors.Wrapf(err, `failed to set %s`, CipherTextKey)
+	}
+	if err := msg.Set(InitializationVectorKey, iv); err != nil {
+		return nil, errors.Wrapf(err, `failed to set %s`, InitializationVectorKey)
+	}
+	if err := msg.Set(ProtectedHeadersKey, protected); err != nil {
+		return nil, errors.Wrapf(err, `failed to set %s`, ProtectedHeadersKey)
+	}
+	if err := msg.Set(RecipientsKey, recipients); err != nil {
+		return nil, errors.Wrapf(err, `failed to set %s`, RecipientsKey)
+	}
+	if err := msg.Set(TagKey, tag); err != nil {
+		return nil, errors.Wrapf(err, `failed to set %s`, TagKey)
+	}
+
+	switch format {
+	case fmtCompact:
+		return Compact(msg)
+	case fmtJSON:
+		return JSON(msg)
+	default:
+		return nil, fmt.Errorf(`jwe.Encrypt: invalid serialization`)
+	}
 }
 
 // DecryptCtx is used internally when jwe.Decrypt is called, and is
diff --git a/jwe/options.go b/jwe/options.go
index 617e0e47..7621364b 100644
--- a/jwe/options.go
+++ b/jwe/options.go
@@ -3,26 +3,10 @@ package jwe
 import (
 	"context"
 
+	"github.com/lestrrat-go/jwx/jwa"
 	"github.com/lestrrat-go/option"
 )
 
-type Option = option.Interface
-type identMessage struct{}
-type identPostParser struct{}
-type identPrettyFormat struct{}
-type identProtectedHeader struct{}
-
-type DecryptOption interface {
-	Option
-	decryptOption()
-}
-
-type decryptOption struct {
-	Option
-}
-
-func (*decryptOption) decryptOption() {}
-
 type SerializerOption interface {
 	Option
 	serializerOption()
@@ -34,17 +18,6 @@ type serializerOption struct {
 
 func (*serializerOption) serializerOption() {}
 
-type EncryptOption interface {
-	Option
-	encryptOption()
-}
-
-type encryptOption struct {
-	Option
-}
-
-func (*encryptOption) encryptOption() {}
-
 // WithPrettyFormat specifies if the `jwe.JSON` serialization tool
 // should generate pretty-formatted output
 func WithPrettyFormat(b bool) SerializerOption {
@@ -55,19 +28,7 @@ func WithPrettyFormat(b bool) SerializerOption {
 // "enc" and "zip" will be overwritten when encryption is performed.
 func WithProtectedHeaders(h Headers) EncryptOption {
 	cloned, _ := h.Clone(context.Background())
-	return &encryptOption{option.New(identProtectedHeader{}, cloned)}
-}
-
-// WithMessage provides a message object to be populated by `jwe.Decrpt`
-// Using this option allows you to decrypt AND obtain the `jwe.Message`
-// in one go.
-//
-// Note that you should NOT be using the message object for anything other
-// than inspecting its contents. Particularly, do not expect the message
-// reliable when you call `Decrypt` on it. `(jwe.Message).Decrypt` is
-// slated to be deprecated in the next major version.
-func WithMessage(m *Message) DecryptOption {
-	return &decryptOption{option.New(identMessage{}, m)}
+	return &encryptOption{option.New(identProtectedHeaders{}, cloned)}
 }
 
 // WithPostParser specifies the handler to be called immediately
@@ -85,3 +46,90 @@ func WithMessage(m *Message) DecryptOption {
 func WithPostParser(p PostParser) DecryptOption {
 	return &decryptOption{option.New(identPostParser{}, p)}
 }
+
+type DecryptEncryptOption interface {
+	DecryptOption
+	EncryptOption
+}
+
+type decryptEncryptOption struct {
+	Option
+}
+
+func (*decryptEncryptOption) decryptOption() {}
+func (*decryptEncryptOption) encryptOption() {}
+
+type withKey struct {
+	alg     jwa.KeyAlgorithm
+	key     interface{}
+	headers Headers
+}
+
+type WithKeySuboption interface {
+	Option
+	withKeySuboption()
+}
+
+type withKeySuboption struct {
+	Option
+}
+
+func (*withKeySuboption) withKeySuboption() {}
+
+func WithRecipientHeaders(hdr Headers) WithKeySuboption {
+	return &withKeySuboption{option.New(identRecipientHeaders{}, hdr)}
+}
+
+func WithKey(alg jwa.KeyAlgorithm, key interface{}, options ...WithKeySuboption) DecryptEncryptOption {
+	var hdr Headers
+	for _, option := range options {
+		//nolint:forcetypeassert
+		switch option.Ident() {
+		case identRecipientHeaders{}:
+			hdr = option.Value().(Headers)
+		}
+	}
+
+	return &decryptEncryptOption{option.New(identKey{}, &withKey{
+		alg:     alg,
+		key:     key,
+		headers: hdr,
+	})}
+}
+
+type JSONSuboption interface {
+	Option
+	withJSONSuboption()
+}
+
+type jsonSuboption struct {
+	Option
+}
+
+func (*jsonSuboption) withJSONSuboption() {}
+
+func WithPretty(v bool) JSONSuboption {
+	return &jsonSuboption{option.New(identPretty{}, v)}
+}
+
+// WithJSON specifies that the result of `jwe.Encrypt()` is serialized in
+// JSON format.
+//
+// If you pass multiple keys to `jwe.Encrypt()`, it will fail unless
+// you also pass this option.
+func WithJSON(options ...JSONSuboption) EncryptOption {
+	var pretty bool
+	for _, option := range options {
+		//nolint:forcetypeassert
+		switch option.Ident() {
+		case identPretty{}:
+			pretty = option.Value().(bool)
+		}
+	}
+
+	format := fmtJSON
+	if pretty {
+		format = fmtJSONPretty
+	}
+	return &encryptOption{option.New(identSerialization{}, format)}
+}
diff --git a/jwe/options.yaml b/jwe/options.yaml
new file mode 100644
index 00000000..c96115ac
--- /dev/null
+++ b/jwe/options.yaml
@@ -0,0 +1,60 @@
+output: jwe/options_gen.go
+interfaces:
+  - name: DecryptOption
+    comment: |
+      DecryptOption describes options that can be passed to `jwe.Decrypt`
+  - name: EncryptOption
+    comment: |
+      EncryptOption describes options that can be passed to `jwe.Encrypt`
+options:
+  - ident: Key
+    skip_option: true
+  - ident: Pretty
+    skip_option: true
+  - ident: ProtectedHeaders
+    skip_option: true
+  # These should be deprecated?
+  - ident: PostParser
+    skip_option: true
+  - ident: PrettyFormat
+    skip_option: true
+  - ident: RecipientHeaders
+    skip_option: true
+  - ident: Serialization
+    option_name: WithCompact
+    interface: EncryptOption
+    constant_value: fmtCompact
+    comment: |
+      WithCompact specifies that the result of `jwe.Encrypt()` is serialized in
+      compact format.
+      
+      By default `jwe.Encrypt()` will opt to use compact format, so you usually
+      do not need to specify this option other than to be explicit about it
+  - ident: Compress
+    interface: EncryptOption
+    argument_type: jwa.CompressionAlgorithm
+    comment: |
+      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,
+      and therefore this option takes an argument)
+  - ident: ContentEncryptionAlgorithm
+    interface: EncryptOption
+    option_name: WithContentEncryption
+    argument_type: jwa.ContentEncryptionAlgorithm
+    comment: |
+      WithContentEncryptionAlgorithm specifies the algorithm to encrypt the
+      JWE message content with. If not provided, `jwa.A256GCM` is used.
+  - ident: Message
+    interface: DecryptOption
+    argument_type: '*Message'
+    comment: |
+      WithMessage provides a message object to be populated by `jwe.Decrpt`
+      Using this option allows you to decrypt AND obtain the `jwe.Message`
+      in one go.
+      
+      Note that you should NOT be using the message object for anything other
+      than inspecting its contents. Particularly, do not expect the message
+      reliable when you call `Decrypt` on it. `(jwe.Message).Decrypt` is
+      slated to be deprecated in the next major version.
+
diff --git a/jwe/options_gen.go b/jwe/options_gen.go
new file mode 100644
index 00000000..4c9cfdab
--- /dev/null
+++ b/jwe/options_gen.go
@@ -0,0 +1,120 @@
+// This file is auto-generated by internal/cmd/genoptions/main.go. DO NOT EDIT
+
+package jwe
+
+import (
+	"github.com/lestrrat-go/jwx/jwa"
+	"github.com/lestrrat-go/option"
+)
+
+type Option = option.Interface
+
+// DecryptOption describes options that can be passed to `jwe.Decrypt`
+type DecryptOption interface {
+	Option
+	decryptOption()
+}
+
+type decryptOption struct {
+	Option
+}
+
+func (*decryptOption) decryptOption() {}
+
+// EncryptOption describes options that can be passed to `jwe.Encrypt`
+type EncryptOption interface {
+	Option
+	encryptOption()
+}
+
+type encryptOption struct {
+	Option
+}
+
+func (*encryptOption) encryptOption() {}
+
+type identCompress struct{}
+type identContentEncryptionAlgorithm struct{}
+type identKey struct{}
+type identMessage struct{}
+type identPostParser struct{}
+type identPretty struct{}
+type identPrettyFormat struct{}
+type identProtectedHeaders struct{}
+type identRecipientHeaders struct{}
+type identSerialization struct{}
+
+func (identCompress) String() string {
+	return "WithCompress"
+}
+
+func (identContentEncryptionAlgorithm) String() string {
+	return "WithContentEncryption"
+}
+
+func (identKey) String() string {
+	return "WithKey"
+}
+
+func (identMessage) String() string {
+	return "WithMessage"
+}
+
+func (identPostParser) String() string {
+	return "WithPostParser"
+}
+
+func (identPretty) String() string {
+	return "WithPretty"
+}
+
+func (identPrettyFormat) String() string {
+	return "WithPrettyFormat"
+}
+
+func (identProtectedHeaders) String() string {
+	return "WithProtectedHeaders"
+}
+
+func (identRecipientHeaders) String() string {
+	return "WithRecipientHeaders"
+}
+
+func (identSerialization) String() string {
+	return "WithCompact"
+}
+
+// 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,
+// and therefore this option takes an argument)
+func WithCompress(v jwa.CompressionAlgorithm) EncryptOption {
+	return &encryptOption{option.New(identCompress{}, v)}
+}
+
+// WithContentEncryptionAlgorithm specifies the algorithm to encrypt the
+// JWE message content with. If not provided, `jwa.A256GCM` is used.
+func WithContentEncryption(v jwa.ContentEncryptionAlgorithm) EncryptOption {
+	return &encryptOption{option.New(identContentEncryptionAlgorithm{}, v)}
+}
+
+// WithMessage provides a message object to be populated by `jwe.Decrpt`
+// Using this option allows you to decrypt AND obtain the `jwe.Message`
+// in one go.
+//
+// Note that you should NOT be using the message object for anything other
+// than inspecting its contents. Particularly, do not expect the message
+// reliable when you call `Decrypt` on it. `(jwe.Message).Decrypt` is
+// slated to be deprecated in the next major version.
+func WithMessage(v *Message) DecryptOption {
+	return &decryptOption{option.New(identMessage{}, v)}
+}
+
+// WithCompact specifies that the result of `jwe.Encrypt()` is serialized in
+// compact format.
+//
+// By default `jwe.Encrypt()` will opt to use compact format, so you usually
+// do not need to specify this option other than to be explicit about it
+func WithCompact() EncryptOption {
+	return &encryptOption{option.New(identSerialization{}, fmtCompact)}
+}
diff --git a/jws/jws.go b/jws/jws.go
index 7369b256..1dbbe6dd 100644
--- a/jws/jws.go
+++ b/jws/jws.go
@@ -135,6 +135,7 @@ const (
 	fmtInvalid = iota
 	fmtCompact
 	fmtJSON
+	fmtJSONPretty
 	fmtMax
 )
 
@@ -221,6 +222,8 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
 	switch format {
 	case fmtJSON:
 		return json.Marshal(result)
+	case fmtJSONPretty:
+		return json.MarshalIndent(result, "", "  ")
 	case fmtCompact:
 		// Take the only signature object, and convert it into a Compact
 		// serialization format
diff --git a/jws/option.go b/jws/option.go
index 0b719e41..12a4a4e2 100644
--- a/jws/option.go
+++ b/jws/option.go
@@ -18,6 +18,7 @@ type identKey struct{}
 type identKeyUsed struct{}
 type identKeyProvider struct{}
 type identSerialization struct{}
+type identPretty struct{}
 
 // WithKey options
 type identSignProtected struct{}
@@ -95,13 +96,41 @@ func WithCompact() SignOption {
 	return &signOption{option.New(identSerialization{}, fmtCompact)}
 }
 
+type JSONSuboption interface {
+	Option
+	withJSONSuboption()
+}
+
+type jsonSuboption struct {
+	Option
+}
+
+func (*jsonSuboption) withJSONSuboption() {}
+
+func WithPretty(v bool) JSONSuboption {
+	return &jsonSuboption{option.New(identPretty{}, v)}
+}
+
 // WithJSON specifies that the result of `jws.Sign()` is serialized in
 // JSON format.
 //
 // If you pass multiple keys to `jws.Sign()`, it will fail unless
 // you also pass this option.
-func WithJSON() SignOption {
-	return &signOption{option.New(identSerialization{}, fmtJSON)}
+func WithJSON(options ...JSONSuboption) SignOption {
+	var pretty bool
+	for _, option := range options {
+		//nolint:forcetypeassert
+		switch option.Ident() {
+		case identPretty{}:
+			pretty = option.Value().(bool)
+		}
+	}
+
+	format := fmtJSON
+	if pretty {
+		format = fmtJSONPretty
+	}
+	return &signOption{option.New(identSerialization{}, format)}
 }
 
 // WithKeySuboption describes option types that can be passed to the `jws.WithKey()`
diff --git a/jwt/serialize.go b/jwt/serialize.go
index f81d5178..bf782e02 100644
--- a/jwt/serialize.go
+++ b/jwt/serialize.go
@@ -198,7 +198,7 @@ func (s *jweSerializer) Serialize(ctx SerializeCtx, v interface{}) (interface{},
 	if err := setTypeOrCty(ctx, hdrs); err != nil {
 		return nil, err // this is already wrapped
 	}
-	return jwe.Encrypt(payload, s.keyalg, s.key, s.contentalg, s.compressalg, jwe.WithProtectedHeaders(hdrs))
+	return jwe.Encrypt(payload, jwe.WithKey(s.keyalg, s.key), jwe.WithContentEncryption(s.contentalg), jwe.WithCompress(s.compressalg), jwe.WithProtectedHeaders(hdrs))
 }
 
 func (s *Serializer) Encrypt(keyalg jwa.KeyEncryptionAlgorithm, key interface{}, contentalg jwa.ContentEncryptionAlgorithm, compressalg jwa.CompressionAlgorithm, options ...EncryptOption) *Serializer {
diff --git a/jwx.go b/jwx.go
index 17668d1e..961450f7 100644
--- a/jwx.go
+++ b/jwx.go
@@ -1,4 +1,5 @@
 //go:generate ./gen.sh
+//go:generate ./internal/cmd/genoptions/gen.sh
 //go:generate stringer -type=FormatKind
 //go:generate mv formatkind_string.go formatkind_string_gen.go
 


Test patch:
diff --git a/examples/jwe_example_test.go b/examples/jwe_example_test.go
index 327fad21..4284e846 100644
--- a/examples/jwe_example_test.go
+++ b/examples/jwe_example_test.go
@@ -21,7 +21,7 @@ func ExampleJWE_Encrypt() {
 
 	payload := []byte("Lorem Ipsum")
 
-	encrypted, err := jwe.Encrypt(payload, jwa.RSA1_5, &privkey.PublicKey, jwa.A128CBC_HS256, jwa.NoCompress)
+	encrypted, err := jwe.Encrypt(payload, jwe.WithKey(jwa.RSA1_5, &privkey.PublicKey), jwe.WithContentEncryption(jwa.A128CBC_HS256))
 	if err != nil {
 		log.Printf("failed to encrypt payload: %s", err)
 		return
@@ -38,7 +38,7 @@ func exampleGenPayload() (*rsa.PrivateKey, []byte, error) {
 
 	payload := []byte("Lorem Ipsum")
 
-	encrypted, err := jwe.Encrypt(payload, jwa.RSA1_5, &privkey.PublicKey, jwa.A128CBC_HS256, jwa.NoCompress)
+	encrypted, err := jwe.Encrypt(payload, jwe.WithKey(jwa.RSA1_5, &privkey.PublicKey), jwe.WithContentEncryption(jwa.A128CBC_HS256))
 	if err != nil {
 		return nil, nil, err
 	}
@@ -86,10 +86,7 @@ func ExampleJWE_ComplexDecrypt() {
 	protected.Set(`jwx-hints`, `foobar`) // in real life this would a more meaningful value
 	encrypted, err := jwe.Encrypt(
 		[]byte(payload),
-		jwa.RSA_OAEP,
-		privkey.PublicKey,
-		jwa.A256GCM,
-		jwa.NoCompress,
+		jwe.WithKey(jwa.RSA_OAEP, privkey.PublicKey),
 		jwe.WithProtectedHeaders(protected),
 	)
 	if err != nil {
diff --git a/internal/jwxtest/jwxtest.go b/internal/jwxtest/jwxtest.go
index 03a08835..65263f06 100644
--- a/internal/jwxtest/jwxtest.go
+++ b/internal/jwxtest/jwxtest.go
@@ -308,7 +308,7 @@ func EncryptJweFile(ctx context.Context, payload []byte, keyalg jwa.KeyEncryptio
 		keyif = rawkey
 	}
 
-	buf, err := jwe.Encrypt(payload, keyalg, keyif, contentalg, compressalg)
+	buf, err := jwe.Encrypt(payload, jwe.WithKey(keyalg, keyif), jwe.WithContentEncryption(contentalg), jwe.WithCompress(compressalg))
 	if err != nil {
 		return "", nil, errors.Wrap(err, `failed to encrypt payload`)
 	}
diff --git a/jwe/jwe_test.go b/jwe/jwe_test.go
index 8bf67489..374c0d1c 100644
--- a/jwe/jwe_test.go
+++ b/jwe/jwe_test.go
@@ -174,11 +174,6 @@ func TestParse_RSAES_OAEP_AES_GCM(t *testing.T) {
 		return
 	}
 
-	{
-		buf, _ := json.MarshalIndent(msg, "", "  ")
-		t.Logf("%s", buf)
-	}
-
 	if !assert.Equal(t, 1, len(msg.Recipients()), "message recipients header length is 1") {
 		return
 	}
@@ -192,24 +187,24 @@ func TestParse_RSAES_OAEP_AES_GCM(t *testing.T) {
 		return
 	}
 
-	serializers := []struct {
+	templates := []*struct {
 		Name     string
-		Func     func(*jwe.Message) ([]byte, error)
+		Options  []jwe.EncryptOption
 		Expected string
 	}{
 		{
 			Name:     "Compact",
-			Func:     func(m *jwe.Message) ([]byte, error) { return jwe.Compact(m) },
+			Options:  []jwe.EncryptOption{jwe.WithCompact()},
 			Expected: serialized,
 		},
 		{
 			Name:     "JSON",
-			Func:     func(m *jwe.Message) ([]byte, error) { return jwe.JSON(m) },
+			Options:  []jwe.EncryptOption{jwe.WithJSON()},
 			Expected: `{"ciphertext":"5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A","iv":"48V1_ALb6US04U3b","protected":"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ","header":{"alg":"RSA-OAEP"},"encrypted_key":"OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg","tag":"XFBoMYUZodetZdvTiFvSkQ"}`,
 		},
 		{
-			Name: "JSON (Pretty)",
-			Func: func(m *jwe.Message) ([]byte, error) { return jwe.JSON(m, jwe.WithPrettyFormat(true)) },
+			Name:    "JSON (Pretty)",
+			Options: []jwe.EncryptOption{jwe.WithJSON(jwe.WithPretty(true))},
 			Expected: `{
   "ciphertext": "5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A",
   "iv": "48V1_ALb6US04U3b",
@@ -223,41 +218,54 @@ func TestParse_RSAES_OAEP_AES_GCM(t *testing.T) {
 		},
 	}
 
-	for _, serializer := range serializers {
-		serializer := serializer
-		for _, compression := range []jwa.CompressionAlgorithm{jwa.NoCompress, jwa.Deflate} {
-			compression := compression
+	ntmpl := len(templates)
+	testcases := make([]struct {
+		Name     string
+		Options  []jwe.EncryptOption
+		Expected string
+	}, ntmpl*2)
+
+	for i, tmpl := range templates {
+		options := make([]jwe.EncryptOption, len(tmpl.Options))
+		copy(options, tmpl.Options)
+
+		for j, compression := range []jwa.CompressionAlgorithm{jwa.NoCompress, jwa.Deflate} {
 			compName := compression.String()
 			if compName == "" {
 				compName = "none"
 			}
-			t.Run(serializer.Name+" (compression="+compName+")", func(t *testing.T) {
-				jsonbuf, err := serializer.Func(msg)
-				if !assert.NoError(t, err, "serialize succeeded") {
-					return
-				}
+			tc := testcases[i+j]
+			tc.Name = tmpl.Name + " (compression=" + compName + ")"
+			tc.Expected = tmpl.Expected
+			tc.Options = append(options, jwe.WithCompress(compression))
+		}
+	}
 
-				if !assert.Equal(t, serializer.Expected, string(jsonbuf), "serialize result matches") {
-					jsonbuf, _ = jwe.JSON(msg, jwe.WithPrettyFormat(true))
-					t.Logf("%s", jsonbuf)
-					return
-				}
+	for _, tc := range testcases {
+		tc := tc
+		t.Run(tc.Name, func(t *testing.T) {
+			options := tc.Options
+			options = append(options, jwe.WithKey(jwa.RSA_OAEP, rawkey.PublicKey))
 
-				encrypted, err := jwe.Encrypt(plaintext, jwa.RSA_OAEP, rawkey.PublicKey, jwa.A256GCM, compression)
-				if !assert.NoError(t, err, "jwe.Encrypt should succeed") {
-					return
-				}
+			for i, option := range options {
+				t.Logf("%d: %s", i, option)
+			}
 
-				plaintext, err = jwe.Decrypt(encrypted, jwa.RSA_OAEP, rawkey)
-				if !assert.NoError(t, err, "jwe.Decrypt should succeed") {
-					return
-				}
+			encrypted, err := jwe.Encrypt(plaintext, options...)
+			if !assert.NoError(t, err, "jwe.Encrypt should succeed") {
+				return
+			}
+			t.Logf("%s", encrypted)
 
-				if !assert.Equal(t, payload, string(plaintext), "jwe.Decrypt should produce the same plaintext") {
-					return
-				}
-			})
-		}
+			plaintext, err = jwe.Decrypt(encrypted, jwa.RSA_OAEP, rawkey)
+			if !assert.NoError(t, err, "jwe.Decrypt should succeed") {
+				return
+			}
+
+			if !assert.Equal(t, payload, string(plaintext), "jwe.Decrypt should produce the same plaintext") {
+				return
+			}
+		})
 	}
 
 	// Test direct marshaling and unmarshaling
@@ -295,7 +303,7 @@ func TestRoundtrip_RSAES_OAEP_AES_GCM(t *testing.T) {
 	}
 
 	for i := 0; i < max; i++ {
-		encrypted, err := jwe.Encrypt(plaintext, jwa.RSA_OAEP, &rsaPrivKey.PublicKey, jwa.A256GCM, jwa.NoCompress)
+		encrypted, err := jwe.Encrypt(plaintext, jwe.WithKey(jwa.RSA_OAEP, &rsaPrivKey.PublicKey))
 		if !assert.NoError(t, err, "Encrypt should succeed") {
 			return
 		}
@@ -323,7 +331,7 @@ func TestRoundtrip_RSA1_5_A128CBC_HS256(t *testing.T) {
 	}
 
 	for i := 0; i < max; i++ {
-		encrypted, err := jwe.Encrypt(plaintext, jwa.RSA1_5, &rsaPrivKey.PublicKey, jwa.A128CBC_HS256, jwa.NoCompress)
+		encrypted, err := jwe.Encrypt(plaintext, jwe.WithKey(jwa.RSA1_5, &rsaPrivKey.PublicKey), jwe.WithContentEncryption(jwa.A128CBC_HS256))
 		if !assert.NoError(t, err, "Encrypt is successful") {
 			return
 		}
@@ -356,7 +364,7 @@ func TestEncode_A128KW_A128CBC_HS256(t *testing.T) {
 	}
 
 	for i := 0; i < max; i++ {
-		encrypted, err := jwe.Encrypt(plaintext, jwa.A128KW, sharedkey, jwa.A128CBC_HS256, jwa.NoCompress)
+		encrypted, err := jwe.Encrypt(plaintext, jwe.WithKey(jwa.A128KW, sharedkey), jwe.WithContentEncryption(jwa.A128CBC_HS256))
 		if !assert.NoError(t, err, "Encrypt is successful") {
 			return
 		}
@@ -386,27 +394,16 @@ func testEncodeECDHWithKey(t *testing.T, privkey interface{}, pubkey interface{}
 	for _, alg := range algorithms {
 		alg := alg
 		t.Run(alg.String(), func(t *testing.T) {
-			encrypted, err := jwe.Encrypt(plaintext, alg, pubkey, jwa.A256GCM, jwa.NoCompress)
+			encrypted, err := jwe.Encrypt(plaintext, jwe.WithKey(alg, pubkey))
 			if !assert.NoError(t, err, "Encrypt succeeds") {
 				return
 			}
 
-			t.Logf("encrypted = %s", encrypted)
-
-			msg, err := jwe.Parse(encrypted)
+			_, err = jwe.Parse(encrypted)
 			if !assert.NoError(t, err, `jwe.Parse should succeed`) {
 				return
 			}
 
-			{
-				buf, _ := json.MarshalIndent(msg, "", "  ")
-				t.Logf("%s", buf)
-			}
-			{
-				buf, _ := json.MarshalIndent(msg.ProtectedHeaders(), "", "  ")
-				t.Logf("%s", buf)
-			}
-
 			decrypted, err := jwe.Decrypt(encrypted, alg, privkey)
 			if !assert.NoError(t, err, "Decrypt succeeds") {
 				return
@@ -494,11 +491,6 @@ func Test_GHIssue207(t *testing.T) {
 				return
 			}
 
-			{
-				buf, _ := json.MarshalIndent(msg, "", "  ")
-				t.Logf("%s", buf)
-			}
-
 			decrypted, err := msg.Decrypt(((msg.Recipients())[0]).Headers().Algorithm(), &key)
 			if !assert.NoError(t, err, `jwe.Decrypt should succeed`) {
 				return
@@ -530,16 +522,20 @@ func TestEncode_Direct(t *testing.T) {
 		tc := tc
 		t.Run(tc.Algorithm.String(), func(t *testing.T) {
 			key := make([]byte, tc.KeySize)
-			_, err := rand.Read(key)
-			if !assert.NoError(t, err, "Key generation succeeds") {
-				return
+			/*
+				_, err := rand.Read(key)
+				if !assert.NoError(t, err, "Key generation succeeds") {
+					return
+				}*/
+			for n := 0; n < len(key); {
+				w := copy(key[n:], []byte(`12345678`))
+				n += w
 			}
 
-			encrypted, err := jwe.Encrypt(plaintext, jwa.DIRECT, key, tc.Algorithm, jwa.NoCompress)
-			if !assert.NoError(t, err, "Encrypt succeeds") {
+			encrypted, err := jwe.Encrypt(plaintext, jwe.WithKey(jwa.DIRECT, key), jwe.WithContentEncryption(tc.Algorithm))
+			if !assert.NoError(t, err, `jwe.Encrypt should succeed`) {
 				return
 			}
-
 			decrypted, err := jwe.Decrypt(encrypted, jwa.DIRECT, key)
 			if !assert.NoError(t, err, `jwe.Decrypt should succeed`) {
 				return
@@ -706,7 +702,7 @@ func TestCustomField(t *testing.T) {
 	protected := jwe.NewHeaders()
 	protected.Set(`x-birthday`, string(bdaybytes))
 
-	encrypted, err := jwe.Encrypt(plaintext, jwa.RSA_OAEP, pubkey, jwa.A256GCM, jwa.NoCompress, jwe.WithProtectedHeaders(protected))
+	encrypted, err := jwe.Encrypt(plaintext, jwe.WithKey(jwa.RSA_OAEP, pubkey), jwe.WithProtectedHeaders(protected))
 	if !assert.NoError(t, err, `jwe.Encrypt should succeed`) {
 		return
 	}
@@ -767,7 +763,7 @@ func TestGH554(t *testing.T) {
 		return
 	}
 
-	encrypted, err := jwe.Encrypt([]byte(plaintext), jwa.ECDH_ES, pubkey, jwa.A256GCM, jwa.NoCompress)
+	encrypted, err := jwe.Encrypt([]byte(plaintext), jwe.WithKey(jwa.ECDH_ES, pubkey))
 	if !assert.NoError(t, err, `jwk.Encrypt() should succeed`) {
 		return
 	}