-module(gose@encrypted_jwt). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/gose/encrypted_jwt.gleam"). -export([key_decryptor/4, password_decryptor/4, encrypt_with_key/4, encrypt_with_password/5, serialize/1, peek_headers/1, decode/2, alg/1, enc/1, kid/1, dangerously_decrypt_and_skip_validation/2, decrypt_and_validate/3]). -export_type([encrypted_jwt/0, decryptor/0, peek_headers/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Encrypted JWT (JWE-based) - [RFC 7519](https://www.rfc-editor.org/rfc/rfc7519.html)\n" "\n" " This module provides encrypted JWT functionality built on top of JWE.\n" " Encrypted JWTs protect the claims payload through encryption rather than\n" " just signing, ensuring confidentiality in addition to integrity.\n" "\n" " Use `peek_headers()` to inspect a token's headers without decrypting.\n" " Use `decrypt_and_validate()` to\n" " decrypt and validate, producing a `EncryptedJwt` whose claims can be\n" " trusted.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " import gleam/dynamic/decode\n" " import gleam/time/duration\n" " import gleam/time/timestamp\n" " import gose/encrypted_jwt\n" " import gose/jwa\n" " import gose/jwk\n" " import gose/jwt\n" "\n" " let key = jwk.generate_enc_key(jwa.AesGcm(jwa.Aes256))\n" " let now = timestamp.system_time()\n" "\n" " // Create claims and encrypt\n" " let claims = jwt.claims()\n" " |> jwt.with_subject(\"user123\")\n" " |> jwt.with_issuer(\"my-app\")\n" " |> jwt.with_expiration(timestamp.add(now, duration.hours(1)))\n" "\n" " let assert Ok(encrypted) = encrypted_jwt.encrypt_with_key(\n" " claims, jwa.JweDirect, jwa.AesGcm(jwa.Aes256), key)\n" " let token = encrypted_jwt.serialize(encrypted)\n" "\n" " // Decrypt and validate using Decryptor (enforces algorithm pinning)\n" " let assert Ok(decryptor) = encrypted_jwt.key_decryptor(\n" " jwa.JweDirect, jwa.AesGcm(jwa.Aes256), [key], jwt.default_validation())\n" " let assert Ok(verified) = encrypted_jwt.decrypt_and_validate(decryptor, token, now)\n" "\n" " // Decode verified claims\n" " let decoder = decode.field(\"sub\", decode.string, decode.success)\n" " let assert Ok(subject) = encrypted_jwt.decode(verified, decoder)\n" " ```\n" ). -opaque encrypted_jwt() :: {encrypted_jwt, gose@jwa:jwe_alg(), gose@jwa:enc(), gleam@option:option(binary()), gose@jwt:claims(), bitstring(), binary()}. -opaque decryptor() :: {key_decryptor, gose@jwa:jwe_alg(), gose@jwa:enc(), list(gose@jwk:jwk()), gose@jwt:jwt_validation_options()} | {password_decryptor, gose@jwa:pbes2_alg(), gose@jwa:enc(), binary(), gose@jwt:jwt_validation_options()}. -type peek_headers() :: {peek_headers, gose@jwa:jwe_alg(), gose@jwa:enc(), gleam@option:option(binary())}. -file("src/gose/encrypted_jwt.gleam", 97). -spec validate_decryption_keys(gose@jwa:jwe_alg(), list(gose@jwk:jwk())) -> {ok, nil} | {error, gose:gose_error()}. validate_decryption_keys(Alg, Keys) -> gose@internal@key_helpers:require_non_empty_keys( Keys, fun() -> gleam@list:try_each( Keys, fun(_capture) -> gose@internal@key_helpers:validate_key_for_jwe_decryption( Alg, _capture ) end ) end ). -file("src/gose/encrypted_jwt.gleam", 124). ?DOC( " Create a key-based decryptor for symmetric (dir, AES-KW, AES-GCM-KW) or\n" " asymmetric (RSA-OAEP, ECDH-ES) algorithms.\n" "\n" " The decryptor pins the expected algorithms. Tokens with different\n" " algorithms will be rejected.\n" "\n" " ## Parameters\n" "\n" " - `alg` - The expected key encryption algorithm.\n" " - `enc` - The expected content encryption algorithm.\n" " - `keys` - One or more keys for decryption.\n" " - `options` - Validation options for JWT claims.\n" "\n" " ## Returns\n" "\n" " `Ok(Decryptor)` configured and ready for use with\n" " `decrypt_and_validate`, or `Error(JoseError(_))` if the key list is\n" " empty, any key's `use` field is set but not `Encrypting`, or any key's\n" " `key_ops` doesn't include `Decrypt` or `UnwrapKey`.\n" ). -spec key_decryptor( gose@jwa:jwe_alg(), gose@jwa:enc(), list(gose@jwk:jwk()), gose@jwt:jwt_validation_options() ) -> {ok, decryptor()} | {error, gose@jwt:jwt_error()}. key_decryptor(Alg, Enc, Keys, Options) -> _pipe = validate_decryption_keys(Alg, Keys), _pipe@1 = gleam@result:replace( _pipe, {key_decryptor, Alg, Enc, Keys, Options} ), gleam@result:map_error(_pipe@1, fun(Field@0) -> {jose_error, Field@0} end). -file("src/gose/encrypted_jwt.gleam", 150). ?DOC( " Create a password-based decryptor for PBES2 algorithms.\n" "\n" " The decryptor pins the expected algorithms. Tokens with different\n" " algorithms will be rejected.\n" "\n" " ## Parameters\n" "\n" " - `alg` - The expected PBES2 algorithm.\n" " - `enc` - The expected content encryption algorithm.\n" " - `password` - The password for key derivation.\n" " - `options` - Validation options for JWT claims.\n" "\n" " ## Returns\n" "\n" " A `Decryptor` configured for use with `decrypt_and_validate`.\n" ). -spec password_decryptor( gose@jwa:pbes2_alg(), gose@jwa:enc(), binary(), gose@jwt:jwt_validation_options() ) -> decryptor(). password_decryptor(Alg, Enc, Password, Options) -> {password_decryptor, Alg, Enc, Password, Options}. -file("src/gose/encrypted_jwt.gleam", 268). -spec claims_to_plaintext(gose@jwt:claims()) -> bitstring(). claims_to_plaintext(Claims) -> _pipe = gose@jwt:claims_to_json_string(Claims), gleam_stdlib:identity(_pipe). -file("src/gose/encrypted_jwt.gleam", 191). -spec do_encrypt_with_key( gose@jwt:claims(), gose@jwa:jwe_alg(), gose@jwa:enc(), gose@jwk:jwk(), gleam@option:option(binary()) ) -> {ok, encrypted_jwt()} | {error, gose:gose_error()}. do_encrypt_with_key(Claims, Alg, Enc, Key, Kid) -> Claims_json = claims_to_plaintext(Claims), gleam@result:'try'( gose@internal@key_helpers:validate_key_for_jwe_encryption(Alg, Key), fun(_) -> _pipe = gose@jwe:encrypt_to_compact( Alg, Enc, Claims_json, Key, Kid, {some, <<"JWT"/utf8>>}, none ), gleam@result:map( _pipe, fun(Pair) -> {Token, Jwe_alg} = Pair, {encrypted_jwt, Jwe_alg, Enc, Kid, Claims, Claims_json, Token} end ) end ). -file("src/gose/encrypted_jwt.gleam", 180). ?DOC( " Encrypt claims using a key-based algorithm.\n" "\n" " Supports all key-based JWE algorithms: direct symmetric (dir), AES Key Wrap,\n" " AES-GCM Key Wrap, RSA-OAEP, and ECDH-ES. PBES2 password-based algorithms\n" " return an error — use `encrypt_with_password` instead.\n" "\n" " Sets `typ: \"JWT\"` in the header. If the encryption key has a `kid`, it is\n" " included in the JWE header.\n" "\n" " ## Parameters\n" "\n" " - `claims` - The JWT claims to encrypt.\n" " - `alg` - The key encryption algorithm.\n" " - `enc` - The content encryption algorithm.\n" " - `key` - The encryption key.\n" "\n" " ## Returns\n" "\n" " `Ok(EncryptedJwt)` with the encrypted JWT ready for\n" " serialization, or `Error(JoseError(_))` if key validation or encryption\n" " fails.\n" ). -spec encrypt_with_key( gose@jwt:claims(), gose@jwa:jwe_alg(), gose@jwa:enc(), gose@jwk:jwk() ) -> {ok, encrypted_jwt()} | {error, gose@jwt:jwt_error()}. encrypt_with_key(Claims, Alg, Enc, Key) -> Kid = gleam@option:from_result(gose@jwk:kid(Key)), _pipe = do_encrypt_with_key(Claims, Alg, Enc, Key, Kid), gleam@result:map_error(_pipe, fun(Field@0) -> {jose_error, Field@0} end). -file("src/gose/encrypted_jwt.gleam", 234). -spec do_encrypt_with_password( gose@jwt:claims(), gose@jwa:pbes2_alg(), gose@jwa:enc(), binary(), gleam@option:option(binary()) ) -> {ok, encrypted_jwt()} | {error, gose:gose_error()}. do_encrypt_with_password(Claims, Alg, Enc, Password, Kid) -> Claims_json = claims_to_plaintext(Claims), Unencrypted = begin _pipe = gose@jwe:new_pbes2(Alg, Enc), gose@jwe:with_typ(_pipe, <<"JWT"/utf8>>) end, Unencrypted@1 = case Kid of {some, K} -> gose@jwe:with_kid(Unencrypted, K); none -> Unencrypted end, gleam@result:'try'( gose@jwe:encrypt_with_password(Unencrypted@1, Password, Claims_json), fun(Encrypted) -> _pipe@1 = gose@jwe:serialize_compact(Encrypted), gleam@result:map( _pipe@1, fun(Token) -> {encrypted_jwt, gose@jwe:alg(Encrypted), Enc, Kid, Claims, Claims_json, Token} end ) end ). -file("src/gose/encrypted_jwt.gleam", 223). ?DOC( " Encrypt claims using PBES2 password-based encryption.\n" "\n" " Sets `typ: \"JWT\"` in the header.\n" "\n" " ## Parameters\n" "\n" " - `claims` - The JWT claims to encrypt.\n" " - `alg` - The PBES2 algorithm.\n" " - `enc` - The content encryption algorithm.\n" " - `password` - The password for key derivation.\n" " - `kid` - Optional key ID to include in the header.\n" "\n" " ## Returns\n" "\n" " `Ok(EncryptedJwt)` with the encrypted JWT ready for\n" " serialization, or `Error(JoseError(_))` if encryption fails.\n" ). -spec encrypt_with_password( gose@jwt:claims(), gose@jwa:pbes2_alg(), gose@jwa:enc(), binary(), gleam@option:option(binary()) ) -> {ok, encrypted_jwt()} | {error, gose@jwt:jwt_error()}. encrypt_with_password(Claims, Alg, Enc, Password, Kid) -> _pipe = do_encrypt_with_password(Claims, Alg, Enc, Password, Kid), gleam@result:map_error(_pipe, fun(Field@0) -> {jose_error, Field@0} end). -file("src/gose/encrypted_jwt.gleam", 283). ?DOC( " Serialize a decrypted encrypted JWT to compact format.\n" "\n" " ## Parameters\n" "\n" " - `jwt` - A `EncryptedJwt` from `encrypt_with_key`,\n" " `encrypt_with_password`, or `decrypt_and_validate`.\n" "\n" " ## Returns\n" "\n" " The JWE compact serialization string.\n" ). -spec serialize(encrypted_jwt()) -> binary(). serialize(Jwt) -> erlang:element(7, Jwt). -file("src/gose/encrypted_jwt.gleam", 314). -spec parse_jwe(binary()) -> {ok, gose@jwe:jwe(gose@jwe:encrypted(), nil, gose@jwe:parsed())} | {error, gose@jwt:jwt_error()}. parse_jwe(Token) -> _pipe = gose@jwe:parse_compact(Token), gleam@result:map_error( _pipe, fun gose@jwt:gose_error_to_malformed_token_error/1 ). -file("src/gose/encrypted_jwt.gleam", 303). ?DOC( " Peek at the header fields from a token without decrypting.\n" "\n" " ## Parameters\n" "\n" " - `token` - The JWE compact serialization string.\n" "\n" " ## Returns\n" "\n" " `Ok(PeekHeaders)` with the algorithm, encryption, and optional key ID\n" " from the header, or `Error(MalformedToken(_))` if the token cannot be\n" " parsed.\n" ). -spec peek_headers(binary()) -> {ok, peek_headers()} | {error, gose@jwt:jwt_error()}. peek_headers(Token) -> _pipe = parse_jwe(Token), gleam@result:map( _pipe, fun(Parsed_jwe) -> {peek_headers, gose@jwe:alg(Parsed_jwe), gose@jwe:enc(Parsed_jwe), gleam@option:from_result(gose@jwe:kid(Parsed_jwe))} end ). -file("src/gose/encrypted_jwt.gleam", 424). ?DOC( " Decode an encrypted JWT's claims using a custom decoder.\n" "\n" " This allows extracting claims directly into your own types using\n" " `gleam/dynamic/decode`. The decoder receives the raw claims JSON.\n" "\n" " ## Parameters\n" "\n" " - `jwt` - A verified (decrypted) encrypted JWT.\n" " - `decoder` - A `gleam/dynamic/decode` decoder for the claims.\n" "\n" " ## Returns\n" "\n" " `Ok(a)` with the decoded claims value, or\n" " `Error(ClaimDecodingFailed(_))` if decoding fails.\n" ). -spec decode(encrypted_jwt(), gleam@dynamic@decode:decoder(TCP)) -> {ok, TCP} | {error, gose@jwt:jwt_error()}. decode(Jwt, Decoder) -> _pipe = gleam@json:parse_bits(erlang:element(6, Jwt), Decoder), gleam@result:replace_error( _pipe, {claim_decoding_failed, <<"failed to decode claims"/utf8>>} ). -file("src/gose/encrypted_jwt.gleam", 441). ?DOC( " Get the key encryption algorithm (`alg`) from a verified encrypted JWT.\n" "\n" " ## Parameters\n" "\n" " - `jwt` - The verified encrypted JWT.\n" "\n" " ## Returns\n" "\n" " The `JweAlg` from the token's header.\n" ). -spec alg(encrypted_jwt()) -> gose@jwa:jwe_alg(). alg(Jwt) -> erlang:element(2, Jwt). -file("src/gose/encrypted_jwt.gleam", 454). ?DOC( " Get the content encryption algorithm (`enc`) from a verified encrypted JWT.\n" "\n" " ## Parameters\n" "\n" " - `jwt` - The verified encrypted JWT.\n" "\n" " ## Returns\n" "\n" " The `Enc` from the token's header.\n" ). -spec enc(encrypted_jwt()) -> gose@jwa:enc(). enc(Jwt) -> erlang:element(3, Jwt). -file("src/gose/encrypted_jwt.gleam", 471). ?DOC( " Get the key ID (kid) from a verified encrypted JWT header.\n" "\n" " **Security Warning:** The `kid` value comes from the token and is untrusted\n" " input. If you use it to look up keys (from a database, filesystem, or key\n" " store), you must sanitize it first to prevent injection attacks.\n" "\n" " ## Parameters\n" "\n" " - `jwt` - The verified encrypted JWT.\n" "\n" " ## Returns\n" "\n" " `Ok(String)` with the key ID, or `Error(Nil)` if no kid was set.\n" ). -spec kid(encrypted_jwt()) -> {ok, binary()} | {error, nil}. kid(Jwt) -> gleam@option:to_result(erlang:element(4, Jwt), nil). -file("src/gose/encrypted_jwt.gleam", 475). -spec decryptor_options(decryptor()) -> gose@jwt:jwt_validation_options(). decryptor_options(Decryptor) -> erlang:element(5, Decryptor). -file("src/gose/encrypted_jwt.gleam", 517). -spec build_jwe_decryptor(decryptor(), list(gose@jwk:jwk())) -> {ok, gose@jwe:decryptor()} | {error, gose@jwt:jwt_error()}. build_jwe_decryptor(Decryptor, Decryption_keys) -> case Decryptor of {key_decryptor, Alg, Enc, _, _} -> _pipe = gose@jwe:key_decryptor(Alg, Enc, Decryption_keys), gleam@result:map_error( _pipe, fun(Field@0) -> {jose_error, Field@0} end ); {password_decryptor, Alg@1, Enc@1, Password, _} -> {ok, gose@jwe:password_decryptor(Alg@1, Enc@1, Password)} end. -file("src/gose/encrypted_jwt.gleam", 530). -spec gose_error_to_decryption_failed(gose:gose_error()) -> gose@jwt:jwt_error(). gose_error_to_decryption_failed(Err) -> {decryption_failed, gose:error_message(Err)}. -file("src/gose/encrypted_jwt.gleam", 534). -spec parse_plaintext_claims(bitstring()) -> {ok, gose@jwt:claims()} | {error, gose@jwt:jwt_error()}. parse_plaintext_claims(Plaintext) -> gose@jwt:parse_claims_bits(Plaintext). -file("src/gose/encrypted_jwt.gleam", 540). -spec require_matching_algorithms( decryptor(), gose@jwa:jwe_alg(), gose@jwa:enc() ) -> {ok, nil} | {error, gose@jwt:jwt_error()}. require_matching_algorithms(Decryptor, Actual_alg, Actual_enc) -> {Expected_alg, Expected_enc} = case Decryptor of {key_decryptor, Alg, Enc, _, _} -> {Alg, Enc}; {password_decryptor, Alg@1, Enc@1, _, _} -> {{jwe_pbes2, Alg@1}, Enc@1} end, case (Expected_alg /= Actual_alg) orelse (Expected_enc /= Actual_enc) of true -> {error, {jwe_algorithm_mismatch, Expected_alg, Expected_enc, Actual_alg, Actual_enc}}; false -> {ok, nil} end. -file("src/gose/encrypted_jwt.gleam", 562). -spec select_decryption_keys( decryptor(), gleam@option:option(binary()), gose@jwt:kid_policy() ) -> {ok, list(gose@jwk:jwk())} | {error, gose@jwt:jwt_error()}. select_decryption_keys(Decryptor, Token_kid, Kid_policy) -> case Decryptor of {password_decryptor, _, _, _, _} -> {ok, []}; {key_decryptor, _, _, Keys, _} -> gose@jwt:select_keys_by_policy(Keys, Token_kid, Kid_policy) end. -file("src/gose/encrypted_jwt.gleam", 479). -spec decrypt_token(decryptor(), binary()) -> {ok, {bitstring(), gose@jwa:jwe_alg(), gose@jwa:enc(), gleam@option:option(binary())}} | {error, gose@jwt:jwt_error()}. decrypt_token(Decryptor, Token) -> gleam@result:'try'( begin _pipe = gose@jwe:parse_compact(Token), gleam@result:map_error( _pipe, fun gose@jwt:gose_error_to_malformed_token_error/1 ) end, fun(Parsed_jwe) -> Actual_alg = gose@jwe:alg(Parsed_jwe), Actual_enc = gose@jwe:enc(Parsed_jwe), Token_kid = gleam@option:from_result(gose@jwe:kid(Parsed_jwe)), gleam@result:'try'( require_matching_algorithms(Decryptor, Actual_alg, Actual_enc), fun(_) -> Options = decryptor_options(Decryptor), gleam@result:'try'( select_decryption_keys( Decryptor, Token_kid, erlang:element(8, Options) ), fun(Decryption_keys) -> gleam@result:'try'( build_jwe_decryptor(Decryptor, Decryption_keys), fun(Jwe_decryptor) -> gleam@result:'try'( begin _pipe@1 = gose@jwe:decrypt( Jwe_decryptor, Parsed_jwe ), gleam@result:map_error( _pipe@1, fun gose_error_to_decryption_failed/1 ) end, fun(Plaintext) -> {ok, {Plaintext, Actual_alg, Actual_enc, Token_kid}} end ) end ) end ) end ) end ). -file("src/gose/encrypted_jwt.gleam", 341). ?DOC( " Decrypt an encrypted JWT, skipping all claim validation.\n" "\n" " **Warning:** This skips expiration, not-before, issuer, and audience checks.\n" " Use only when you have a legitimate reason to bypass validation, such as\n" " inspecting claims before deciding on validation policy.\n" "\n" " Still enforces algorithm pinning for security. **Note:** `kid_policy` only\n" " applies to key-based decryptors, not password-based decryptors.\n" "\n" " ## Parameters\n" "\n" " - `decryptor` - A `Decryptor` created by `key_decryptor` or\n" " `password_decryptor`.\n" " - `token` - The JWE compact serialization string.\n" "\n" " ## Returns\n" "\n" " `Ok(EncryptedJwt)` with the decrypted JWT and accessible\n" " claims, `Error(JweAlgorithmMismatch(_))` if the token's algorithms\n" " don't match, or `Error(DecryptionFailed(_))` if decryption fails.\n" ). -spec dangerously_decrypt_and_skip_validation(decryptor(), binary()) -> {ok, encrypted_jwt()} | {error, gose@jwt:jwt_error()}. dangerously_decrypt_and_skip_validation(Decryptor, Token) -> gleam@result:'try'( decrypt_token(Decryptor, Token), fun(_use0) -> {Plaintext, Actual_alg, Actual_enc, Kid} = _use0, _pipe = parse_plaintext_claims(Plaintext), gleam@result:map( _pipe, fun(Claims) -> {encrypted_jwt, Actual_alg, Actual_enc, Kid, Claims, Plaintext, Token} end ) end ). -file("src/gose/encrypted_jwt.gleam", 388). ?DOC( " Decrypt an encrypted JWT and validate its claims using a Decryptor.\n" "\n" " Checks:\n" " 1. Token's `alg` and `enc` headers match the decryptor's expected algorithms\n" " 2. Decryption succeeds with one of the decryptor's keys\n" " 3. Claims pass validation (exp, nbf, iss, aud per options)\n" "\n" " When multiple keys are configured:\n" " - Keys with matching `kid` are tried first (if token has `kid` header)\n" " - `kid_policy` controls kid header enforcement (see `KidPolicy` type)\n" " - With `NoKidRequirement`, all keys are tried with matching keys prioritized\n" "\n" " ## Parameters\n" "\n" " - `decryptor` - A `Decryptor` created by `key_decryptor` or\n" " `password_decryptor`.\n" " - `token` - The JWE compact serialization string.\n" " - `now` - The current timestamp for time-based claim validation.\n" "\n" " ## Returns\n" "\n" " `Ok(EncryptedJwt)` with the decrypted and validated JWT,\n" " `Error(JweAlgorithmMismatch(_))` if the token's algorithms don't match\n" " the decryptor's expected algorithms, `Error(DecryptionFailed(_))` if\n" " decryption fails, or a claim validation error (`TokenExpired`,\n" " `TokenNotYetValid`, etc.) if claim validation fails.\n" ). -spec decrypt_and_validate( decryptor(), binary(), gleam@time@timestamp:timestamp() ) -> {ok, encrypted_jwt()} | {error, gose@jwt:jwt_error()}. decrypt_and_validate(Decryptor, Token, Now) -> gleam@result:'try'( decrypt_token(Decryptor, Token), fun(_use0) -> {Plaintext, Actual_alg, Actual_enc, Kid} = _use0, gleam@result:'try'( parse_plaintext_claims(Plaintext), fun(Claims) -> Options = decryptor_options(Decryptor), _pipe = gose@jwt:validate_claims(Claims, Now, Options), gleam@result:replace( _pipe, {encrypted_jwt, Actual_alg, Actual_enc, Kid, Claims, Plaintext, Token} ) end ) end ).