Skip to main content

jwt_simple/
jwe_token.rs

1//! JWE token building and parsing.
2
3use ct_codecs::{Base64UrlSafeNoPadding, Decoder, Encoder};
4use serde::{de::DeserializeOwned, Serialize};
5
6use crate::algorithms::jwe::content::{ContentEncryption, CEK};
7use crate::claims::*;
8use crate::common::{VerificationOptions, DEFAULT_MAX_TOKEN_LENGTH};
9use crate::error::*;
10use crate::jwe_header::JWEHeader;
11
12pub const MAX_JWE_HEADER_LENGTH: usize = 8192;
13
14/// Largest wrapped key a token may carry: an RSA-OAEP key for a 16384-bit modulus.
15/// AES-KW wrapped keys are 40 bytes, and ECDH-ES direct key agreement carries none.
16pub const MAX_JWE_ENCRYPTED_KEY_LENGTH: usize = 2048;
17
18/// Options for JWE encryption.
19#[derive(Clone, Debug, Default)]
20pub struct EncryptionOptions {
21    /// Content encryption algorithm (default: A256GCM)
22    pub content_encryption: ContentEncryption,
23    /// Content type header
24    pub content_type: Option<String>,
25    /// Key ID
26    pub key_id: Option<String>,
27}
28
29/// Options for JWE decryption.
30#[derive(Clone, Debug)]
31pub struct DecryptionOptions {
32    /// Maximum token length to accept.
33    /// Defaults to `DEFAULT_MAX_TOKEN_LENGTH`, as signed tokens do; `None` accepts any size.
34    pub max_token_length: Option<usize>,
35    /// Maximum header length to accept
36    pub max_header_length: Option<usize>,
37    /// Required key ID
38    pub required_key_id: Option<String>,
39    /// Options for validating claims after decryption
40    pub claim_options: Option<VerificationOptions>,
41}
42
43impl Default for DecryptionOptions {
44    fn default() -> Self {
45        Self {
46            max_token_length: Some(DEFAULT_MAX_TOKEN_LENGTH),
47            max_header_length: None,
48            required_key_id: None,
49            claim_options: None,
50        }
51    }
52}
53
54/// JWE token metadata extracted from the header (before decryption).
55#[derive(Debug, Clone)]
56pub struct JWETokenMetadata {
57    header: JWEHeader,
58}
59
60impl JWETokenMetadata {
61    /// The key management algorithm.
62    pub fn algorithm(&self) -> &str {
63        &self.header.algorithm
64    }
65
66    /// The content encryption algorithm.
67    pub fn encryption(&self) -> &str {
68        &self.header.encryption
69    }
70
71    /// The key ID (if present).
72    pub fn key_id(&self) -> Option<&str> {
73        self.header.key_id.as_deref()
74    }
75
76    /// The content type (if present).
77    pub fn content_type(&self) -> Option<&str> {
78        self.header.content_type.as_deref()
79    }
80
81    /// Get the full header.
82    pub fn header(&self) -> &JWEHeader {
83        &self.header
84    }
85}
86
87/// Utilities for working with JWE tokens.
88pub struct JWEToken;
89
90impl JWEToken {
91    /// Build a JWE token.
92    ///
93    /// This function is called by key management implementations to create
94    /// the final JWE compact serialization.
95    ///
96    /// # Arguments
97    /// * `header` - The JWE header
98    /// * `encrypted_key` - The encrypted CEK (or empty for direct key agreement)
99    /// * `iv` - The initialization vector
100    /// * `ciphertext` - The encrypted content
101    /// * `tag` - The authentication tag
102    pub fn build(
103        header: &JWEHeader,
104        encrypted_key: &[u8],
105        iv: &[u8],
106        ciphertext: &[u8],
107        tag: &[u8],
108    ) -> Result<String, Error> {
109        let header_json = serde_json::to_string(header)?;
110        let header_b64 = Base64UrlSafeNoPadding::encode_to_string(&header_json)?;
111        let encrypted_key_b64 = Base64UrlSafeNoPadding::encode_to_string(encrypted_key)?;
112        let iv_b64 = Base64UrlSafeNoPadding::encode_to_string(iv)?;
113        let ciphertext_b64 = Base64UrlSafeNoPadding::encode_to_string(ciphertext)?;
114        let tag_b64 = Base64UrlSafeNoPadding::encode_to_string(tag)?;
115
116        Ok(format!(
117            "{}.{}.{}.{}.{}",
118            header_b64, encrypted_key_b64, iv_b64, ciphertext_b64, tag_b64
119        ))
120    }
121
122    /// Build a JWE token from claims.
123    ///
124    /// This is a helper that serializes claims to JSON before encryption.
125    pub fn build_from_claims<KeyWrapFn, CustomClaims: Serialize>(
126        header: &JWEHeader,
127        claims: &JWTClaims<CustomClaims>,
128        content_encryption: ContentEncryption,
129        key_wrap_fn: KeyWrapFn,
130    ) -> Result<String, Error>
131    where
132        KeyWrapFn: FnOnce(&[u8]) -> Result<Vec<u8>, Error>,
133    {
134        // Serialize claims to JSON
135        let claims_json = serde_json::to_string(claims)?;
136        let plaintext = claims_json.as_bytes();
137
138        // Generate CEK and IV
139        let cek = CEK::new(content_encryption.generate_cek());
140        let iv = content_encryption.generate_iv();
141
142        // Wrap the CEK
143        let encrypted_key = key_wrap_fn(cek.as_bytes())?;
144
145        // Build the AAD (ASCII bytes of the base64url-encoded header)
146        let header_json = serde_json::to_string(header)?;
147        let header_b64 = Base64UrlSafeNoPadding::encode_to_string(&header_json)?;
148        let aad = header_b64.as_bytes();
149
150        // Encrypt the plaintext
151        let (ciphertext, tag) = content_encryption.encrypt(cek.as_bytes(), &iv, aad, plaintext)?;
152        drop(cek); // Zeroize CEK immediately after use
153
154        // Build the final token
155        let encrypted_key_b64 = Base64UrlSafeNoPadding::encode_to_string(&encrypted_key)?;
156        let iv_b64 = Base64UrlSafeNoPadding::encode_to_string(&iv)?;
157        let ciphertext_b64 = Base64UrlSafeNoPadding::encode_to_string(&ciphertext)?;
158        let tag_b64 = Base64UrlSafeNoPadding::encode_to_string(&tag)?;
159
160        Ok(format!(
161            "{}.{}.{}.{}.{}",
162            header_b64, encrypted_key_b64, iv_b64, ciphertext_b64, tag_b64
163        ))
164    }
165
166    /// Parse and decrypt a JWE token.
167    ///
168    /// This function is called by key management implementations to decrypt
169    /// a JWE token and return the claims.
170    pub fn decrypt<KeyUnwrapFn, CustomClaims: DeserializeOwned>(
171        expected_alg: &str,
172        token: &str,
173        options: Option<DecryptionOptions>,
174        key_unwrap_fn: KeyUnwrapFn,
175    ) -> Result<JWTClaims<CustomClaims>, Error>
176    where
177        KeyUnwrapFn: FnOnce(&JWEHeader, &[u8]) -> Result<Vec<u8>, Error>,
178    {
179        let options = options.unwrap_or_default();
180
181        if let Some(max_len) = options.max_token_length {
182            ensure!(token.len() <= max_len, JWTError::TokenTooLong);
183        }
184
185        let mut parts = token.split('.');
186        let header_b64 = parts.next().ok_or(JWTError::InvalidJWEFormat)?;
187        let encrypted_key_b64 = parts.next().ok_or(JWTError::InvalidJWEFormat)?;
188        let iv_b64 = parts.next().ok_or(JWTError::InvalidJWEFormat)?;
189        let ciphertext_b64 = parts.next().ok_or(JWTError::InvalidJWEFormat)?;
190        let tag_b64 = parts.next().ok_or(JWTError::InvalidJWEFormat)?;
191        ensure!(parts.next().is_none(), JWTError::InvalidJWEFormat);
192
193        let max_header_len = options.max_header_length.unwrap_or(MAX_JWE_HEADER_LENGTH);
194        ensure!(header_b64.len() <= max_header_len, JWTError::HeaderTooLarge);
195
196        let header_bytes = Base64UrlSafeNoPadding::decode_to_vec(header_b64, None)?;
197        let header: JWEHeader = serde_json::from_slice(&header_bytes)?;
198
199        // RFC 7516 requires rejecting unrecognized critical extensions, and we support none.
200        if let Some(ref crit) = header.critical {
201            if !crit.is_empty() {
202                bail!(JWTError::UnknownCriticalExtension);
203            }
204        }
205
206        ensure!(
207            header.algorithm == expected_alg,
208            JWTError::AlgorithmMismatch
209        );
210
211        if let Some(required_key_id) = &options.required_key_id {
212            if let Some(key_id) = &header.key_id {
213                ensure!(key_id == required_key_id, JWTError::KeyIdentifierMismatch);
214            } else {
215                bail!(JWTError::MissingJWTKeyIdentifier);
216            }
217        }
218
219        let content_encryption = ContentEncryption::from_alg_name(&header.encryption)?;
220
221        // Every segment but the ciphertext has a size these algorithms fix, so an inflated one
222        // costs nothing to reject while still encoded.
223        ensure!(
224            encrypted_key_b64.len()
225                <= Base64UrlSafeNoPadding::encoded_len(MAX_JWE_ENCRYPTED_KEY_LENGTH)?,
226            JWTError::InvalidJWEFormat
227        );
228        ensure!(
229            iv_b64.len() == Base64UrlSafeNoPadding::encoded_len(content_encryption.iv_size())?,
230            JWTError::InvalidIV
231        );
232        ensure!(
233            tag_b64.len() == Base64UrlSafeNoPadding::encoded_len(content_encryption.tag_size())?,
234            JWTError::InvalidJWEAuthTag
235        );
236
237        // Nothing decodes the ciphertext until a CEK exists.
238        let encrypted_key = Base64UrlSafeNoPadding::decode_to_vec(encrypted_key_b64, None)?;
239        let cek = CEK::new(key_unwrap_fn(&header, &encrypted_key)?);
240
241        let iv = Base64UrlSafeNoPadding::decode_to_vec(iv_b64, None)?;
242        let tag = Base64UrlSafeNoPadding::decode_to_vec(tag_b64, None)?;
243        let ciphertext = Base64UrlSafeNoPadding::decode_to_vec(ciphertext_b64, None)?;
244
245        // The AAD is the ASCII bytes of the base64url-encoded header
246        let aad = header_b64.as_bytes();
247
248        let plaintext = content_encryption.decrypt(cek.as_bytes(), &iv, aad, &ciphertext, &tag)?;
249        drop(cek); // Zeroize CEK immediately after use
250
251        let claims: JWTClaims<CustomClaims> = serde_json::from_slice(&plaintext)?;
252
253        claims.validate(&options.claim_options.unwrap_or_default())?;
254
255        Ok(claims)
256    }
257
258    /// Decode JWE token metadata without decrypting.
259    ///
260    /// This allows inspection of the header to determine which key to use
261    /// for decryption.
262    pub fn decode_metadata(token: &str) -> Result<JWETokenMetadata, Error> {
263        let mut parts = token.split('.');
264        let header_b64 = parts.next().ok_or(JWTError::InvalidJWEFormat)?;
265
266        ensure!(
267            header_b64.len() <= MAX_JWE_HEADER_LENGTH,
268            JWTError::HeaderTooLarge
269        );
270
271        let header_bytes = Base64UrlSafeNoPadding::decode_to_vec(header_b64, None)?;
272        let header: JWEHeader = serde_json::from_slice(&header_bytes)?;
273
274        Ok(JWETokenMetadata { header })
275    }
276}
277
278#[test]
279fn decrypt_enforces_size_limits_before_doing_work() {
280    use crate::{prelude::*, JWTError};
281
282    const ENCRYPTED_KEY: usize = 1;
283    const IV: usize = 2;
284    const CIPHERTEXT: usize = 3;
285    const TAG: usize = 4;
286
287    let key = A256KWKey::generate();
288    let token = key
289        .encrypt(Claims::create(Duration::from_hours(1)))
290        .unwrap();
291    let segments: Vec<&str> = token.split('.').collect();
292
293    let error_for = |patches: &[(usize, &str)]| {
294        let mut segments = segments.clone();
295        for &(index, segment) in patches {
296            segments[index] = segment;
297        }
298        key.decrypt_token::<NoCustomClaims>(&segments.join("."), None)
299            .unwrap_err()
300            .downcast::<JWTError>()
301            .unwrap()
302    };
303
304    let oversized_key =
305        "A".repeat(Base64UrlSafeNoPadding::encoded_len(MAX_JWE_ENCRYPTED_KEY_LENGTH).unwrap() + 1);
306    assert!(matches!(
307        error_for(&[(ENCRYPTED_KEY, &oversized_key)]),
308        JWTError::InvalidJWEFormat
309    ));
310    assert!(matches!(
311        error_for(&[(IV, &format!("{}AAAA", segments[IV]))]),
312        JWTError::InvalidIV
313    ));
314    assert!(matches!(
315        error_for(&[(TAG, &format!("{}AAAA", segments[TAG]))]),
316        JWTError::InvalidJWEAuthTag
317    ));
318
319    // The ciphertext here is not even valid base64, so failing at key unwrap is what proves
320    // nothing looked at it.
321    let bogus_key = Base64UrlSafeNoPadding::encode_to_string([0u8; 40]).unwrap();
322    assert!(matches!(
323        error_for(&[(ENCRYPTED_KEY, &bogus_key), (CIPHERTEXT, &"!".repeat(1024))]),
324        JWTError::KeyUnwrapFailed
325    ));
326
327    // The whole token gets the same default ceiling as a signed one, and lifting it keeps large
328    // tokens usable.
329    let issuer = "i".repeat(1_100_000);
330    let large = key
331        .encrypt(Claims::create(Duration::from_hours(1)).with_issuer(&issuer))
332        .unwrap();
333    assert!(large.len() > DEFAULT_MAX_TOKEN_LENGTH);
334    assert!(matches!(
335        key.decrypt_token::<NoCustomClaims>(&large, None)
336            .unwrap_err()
337            .downcast::<JWTError>()
338            .unwrap(),
339        JWTError::TokenTooLong
340    ));
341
342    let options = DecryptionOptions {
343        max_token_length: None,
344        ..Default::default()
345    };
346    let claims = key
347        .decrypt_token::<NoCustomClaims>(&large, Some(options))
348        .unwrap();
349    assert_eq!(claims.issuer.unwrap(), issuer);
350}