JSON Web Encryption (JWE)
JSON Web Encryption (JWE) is a standard for securely transmitting information as a JSON object. It allows you to encrypt the content of a JSON object, ensuring that only authorized parties can access the sensitive information it contains. Comparing to JWS, JWE provides confidentiality by encrypting the payload, while JWS ensures integrity and authenticity through digital signatures. Full documentation of JWE can be found here: RFC 7516.
JWE is commonly used in scenarios where sensitive information needs to be transmitted securely, such as in authentication tokens, secure API communication, and data exchange between services. Another popular usage of JWE is generating licenses for desktop applications by License Servers.
Roles in JWE
- Sender: The entity that creates and encrypts the JWE.
- Recipient: The entity that receives and decrypts the JWE.
JWE Structure
A JWE consists of five parts, each base64url encoded and separated by periods (.). The general structure is as follows:
Base64Url(Header).Base64Url(ContentEncryptionKey).Base64Url(InitializationVector).Base64Url(Ciphertext).Base64Url(AuthenticationTag)- Header: Contains metadata about the JWE, including the encryption algorithm used.
- Content Encryption Key: (also known as CEK) The symmetric key used to encrypt the payload, encrypted with the recipient's public key.
- Initialization Vector: A random value generated by the sender for the purpose of AES encryption to ensure that identical plaintexts produce different ciphertexts. In case an algorithm that does not require IV is used, this field should be empty.
- Ciphertext: The actual encrypted content (the raw content is also known as plaintext) of the JSON object.
- Authentication Tag: A tag used to verify the integrity and authenticity of the JWE.
Steps to create a JWE
Let's say we want to create a JWE for a simple JSON object containing a message.
The message we would like to encrypt is:
{
"msg": "Hello World"
}
1. Create the Header:
According to RFT 7516, the header for our JWE must include an alg (algorithm) field and an enc (encryption method) field.
In our example, the header would look like this:
{
"alg": "RSA-OAEP",
"enc": "A256GCM"
}
The alg field specifies the algorithm used to encrypt the Content Encryption Key (CEK). In this case, we're using RSA-OAEP, which is an asymmetric encryption algorithm. The enc field specifies the encryption method used to encrypt the payload. A256GCM means we're using AES in Galois/Counter Mode (GCM) with a 256-bit key.
Prepare for payload encryption:
Before we can encrypt the payload, we need to prepare the necessary components required by A256GCM:
- Content Encryption Key (CEK): A random symmetric key used to encrypt the payload.
- Initialization Vector (IV): A random value used to ensure that identical plaintexts produce different ciphertexts.
2. Encrypt the Payload:
Next, we need to encrypt the payload (the message, the plaintext) using the specified encryption method (A256GCM)
In practice CEK is established once and reused for multiple messages while IV is generated fresh for each message.
The procedure looks like this:
payload = {
"msg": "Hello World"
}
AAD = Base64Url(Header) // Additional Authenticated Data (AAD) is the base64url encoded header
CEK = GenerateRandomKey(256) // Generate a random 256-bit Content Encryption Key (CEK)
IV = GenerateRandomIV(96) // Generate a random 96-bit Initialization Vector (IV)
ciphertext, tag = A256GCM_Encrypt(payload, CEK, IV, AAD) // Using AES-256-GCM which produces both ciphertext and authentication tag
The result of this operation is the ciphertext and the authentication tag (tag) which are both essential for the JWE. The tag is used to verify the integrity and authenticity of the ciphertext and is created based on the ciphertext and the associated data (AAD).
3. Encrypt the Content Encryption Key (CEK):
Now, we need to encrypt the CEK using the Key Encryption Key (KEK). In this case (RSA-OAEP) the KEK is the recipient's public key and the specified algorithm.
encryptedCEK = RSA_OAEP_Encrypt(CEK, recipientPublicKey) // Using RSA-OAEP
4. Base64url Encode All Parts:
Next, we need to base64url encode all five parts of the JWE: Header, Encrypted CEK, Initialization Vector, Ciphertext, and Authentication Tag.
Base64Url(Header) = eyJhbGciOiAiUlNBLU9BRVAiLCAiZW5jIjogIkEyNTZHQ00ifQ
Base64Url(EncryptedCEK) = ... // Base64url encoded encrypted CEK
Base64Url(IV) = ... // Base64url encoded IV
Base64Url(Ciphertext) = ... // Base64url encoded ciphertext
Base64Url(Tag) = ... // Base64url encoded authentication tag
5. Construct the JWE:
Finally, we construct the JWE by concatenating all five base64url encoded parts with periods (.) in between.
JWE = Base64Url(Header).Base64Url(EncryptedCEK).Base64Url(IV).Base64Url(Ciphertext).Base64Url(Tag)
JWE = eyJhbGciOiAiUlNBLU9BRVAiLCAiZW5jIjogIkEyNTZHQ00ifQ... // Complete JWE
Verifying and Decrypting a JWE
To verify and decrypt a JWE, the recipient needs to perform the following steps:
- Parse the JWE: Split the JWE into its five parts: Header, Encrypted CEK, Initialization Vector, Ciphertext, and Authentication Tag.
- Base64url Decode: Decode each part from base64url format.
- Decrypt the CEK: Use the recipient's private key to decrypt the Encrypted CEK using the specified algorithm (RSA-OAEP).
- Decrypt the Payload: Use the decrypted CEK and the Initialization Vector to decrypt the Ciphertext using the specified encryption method (A256GCM). Verify the integrity of the ciphertext using the Authentication Tag.
- Access the Payload: If decryption and verification are successful, access the original payload (the plaintext message).
In practice decrypting the payload is done together with verifying its integrity using the Authentication Tag. Example:
decryptedPayload = A256GCM_Decrypt(Ciphertext, CEK, IV, Tag)
Under the hood, this process recomputes Authentication Tag based on the CEK, IV, AAD and the Ciphertext. Then compares that Authentication Tag with the one provided in the JWE.
If the tag verification fails, the decryption process should be aborted, and an error should be raised, indicating that the JWE may have been tampered with or corrupted.
Common Algorithms Used in JWE
JWE supports various algorithms for encrypting the Content Encryption Key (CEK) and for encrypting the payload. Some common algorithms include:
- Key Management Algorithms (for CEK):
- RSA-OAEP - as described above
- ECDH-ES (Elliptic Curve Diffie-Hellman Ephemeral Static) - allows two parties to generate a shared secret over an insecure channel. Sender generates a pair of ephemeral keys (public and private). Uses it's own private key and recipient's public key to derive the shared secret (Z being a function of these keys; Z is used as the Key Encryption Key). Then, the sender encrypts the CEK using this shared secret. The sender's public key is included in the JWE header within "epk" field. Recipient uses that public key and its own private key to derive the same shared secret and decrypt the CEK.
- AES Key Wrap (e.g., A128KW, A256KW) - wraps the CEK using AES in a key wrap mode, providing confidentiality and integrity protection for the CEK. The KEK is derived from the shared secret established during the key agreement process (manual or over a secure channel).
- Direct Encryption (dir) - the CEK is encrypted directly with the content encryption algorithm (e.g., AES-GCM) using the shared secret established during the key agreement process.
- Content Encryption Algorithms (for payload):
- AES in Galois/Counter Mode (GCM) (e.g., A128GCM, A256GCM)
- AES in CBC mode with HMAC-SHA2 (e.g., A128CBC-HS256, A256CBC-HS512)