jwe.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. import binascii
  2. import json
  3. import zlib
  4. from collections.abc import Mapping
  5. from struct import pack
  6. from . import jwk
  7. from .backends import get_random_bytes
  8. from .constants import ALGORITHMS, ZIPS
  9. from .exceptions import JWEError, JWEParseError
  10. from .utils import base64url_decode, base64url_encode, ensure_binary
  11. def encrypt(plaintext, key, encryption=ALGORITHMS.A256GCM, algorithm=ALGORITHMS.DIR, zip=None, cty=None, kid=None):
  12. """Encrypts plaintext and returns a JWE cmpact serialization string.
  13. Args:
  14. plaintext (bytes): A bytes object to encrypt
  15. key (str or dict): The key(s) to use for encrypting the content. Can be
  16. individual JWK or JWK set.
  17. encryption (str, optional): The content encryption algorithm used to
  18. perform authenticated encryption on the plaintext to produce the
  19. ciphertext and the Authentication Tag. Defaults to A256GCM.
  20. algorithm (str, optional): The cryptographic algorithm used
  21. to encrypt or determine the value of the CEK. Defaults to dir.
  22. zip (str, optional): The compression algorithm) applied to the
  23. plaintext before encryption. Defaults to None.
  24. cty (str, optional): The media type for the secured content.
  25. See http://www.iana.org/assignments/media-types/media-types.xhtml
  26. kid (str, optional): Key ID for the provided key
  27. Returns:
  28. bytes: The string representation of the header, encrypted key,
  29. initialization vector, ciphertext, and authentication tag.
  30. Raises:
  31. JWEError: If there is an error signing the token.
  32. Examples:
  33. >>> from jose import jwe
  34. >>> jwe.encrypt('Hello, World!', 'asecret128bitkey', algorithm='dir', encryption='A128GCM')
  35. 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..McILMB3dYsNJSuhcDzQshA.OfX9H_mcUpHDeRM4IA.CcnTWqaqxNsjT4eCaUABSg'
  36. """
  37. plaintext = ensure_binary(plaintext) # Make sure it's bytes
  38. if algorithm not in ALGORITHMS.SUPPORTED:
  39. raise JWEError("Algorithm %s not supported." % algorithm)
  40. if encryption not in ALGORITHMS.SUPPORTED:
  41. raise JWEError("Algorithm %s not supported." % encryption)
  42. key = jwk.construct(key, algorithm)
  43. encoded_header = _encoded_header(algorithm, encryption, zip, cty, kid)
  44. plaintext = _compress(zip, plaintext)
  45. enc_cek, iv, cipher_text, auth_tag = _encrypt_and_auth(key, algorithm, encryption, zip, plaintext, encoded_header)
  46. jwe_string = _jwe_compact_serialize(encoded_header, enc_cek, iv, cipher_text, auth_tag)
  47. return jwe_string
  48. def decrypt(jwe_str, key):
  49. """Decrypts a JWE compact serialized string and returns the plaintext.
  50. Args:
  51. jwe_str (str): A JWE to be decrypt.
  52. key (str or dict): A key to attempt to decrypt the payload with. Can be
  53. individual JWK or JWK set.
  54. Returns:
  55. bytes: The plaintext bytes, assuming the authentication tag is valid.
  56. Raises:
  57. JWEError: If there is an exception verifying the token.
  58. Examples:
  59. >>> from jose import jwe
  60. >>> jwe.decrypt(jwe_string, 'asecret128bitkey')
  61. 'Hello, World!'
  62. """
  63. header, encoded_header, encrypted_key, iv, cipher_text, auth_tag = _jwe_compact_deserialize(jwe_str)
  64. # Verify that the implementation understands and can process all
  65. # fields that it is required to support, whether required by this
  66. # specification, by the algorithms being used, or by the "crit"
  67. # Header Parameter value, and that the values of those parameters
  68. # are also understood and supported.
  69. try:
  70. # Determine the Key Management Mode employed by the algorithm
  71. # specified by the "alg" (algorithm) Header Parameter.
  72. alg = header["alg"]
  73. enc = header["enc"]
  74. if alg not in ALGORITHMS.SUPPORTED:
  75. raise JWEError("Algorithm %s not supported." % alg)
  76. if enc not in ALGORITHMS.SUPPORTED:
  77. raise JWEError("Algorithm %s not supported." % enc)
  78. except KeyError:
  79. raise JWEParseError("alg and enc headers are required!")
  80. # Verify that the JWE uses a key known to the recipient.
  81. key = jwk.construct(key, alg)
  82. # When Direct Key Agreement or Key Agreement with Key Wrapping are
  83. # employed, use the key agreement algorithm to compute the value
  84. # of the agreed upon key. When Direct Key Agreement is employed,
  85. # let the CEK be the agreed upon key. When Key Agreement with Key
  86. # Wrapping is employed, the agreed upon key will be used to
  87. # decrypt the JWE Encrypted Key.
  88. #
  89. # When Key Wrapping, Key Encryption, or Key Agreement with Key
  90. # Wrapping are employed, decrypt the JWE Encrypted Key to produce
  91. # the CEK. The CEK MUST have a length equal to that required for
  92. # the content encryption algorithm. Note that when there are
  93. # multiple recipients, each recipient will only be able to decrypt
  94. # JWE Encrypted Key values that were encrypted to a key in that
  95. # recipient's possession. It is therefore normal to only be able
  96. # to decrypt one of the per-recipient JWE Encrypted Key values to
  97. # obtain the CEK value. Also, see Section 11.5 for security
  98. # considerations on mitigating timing attacks.
  99. if alg == ALGORITHMS.DIR:
  100. # When Direct Key Agreement or Direct Encryption are employed,
  101. # verify that the JWE Encrypted Key value is an empty octet
  102. # sequence.
  103. # Record whether the CEK could be successfully determined for this
  104. # recipient or not.
  105. cek_valid = encrypted_key == b""
  106. # When Direct Encryption is employed, let the CEK be the shared
  107. # symmetric key.
  108. cek_bytes = _get_key_bytes_from_key(key)
  109. else:
  110. try:
  111. cek_bytes = key.unwrap_key(encrypted_key)
  112. # Record whether the CEK could be successfully determined for this
  113. # recipient or not.
  114. cek_valid = True
  115. except NotImplementedError:
  116. raise JWEError(f"alg {alg} is not implemented")
  117. except Exception:
  118. # Record whether the CEK could be successfully determined for this
  119. # recipient or not.
  120. cek_valid = False
  121. # To mitigate the attacks described in RFC 3218 [RFC3218], the
  122. # recipient MUST NOT distinguish between format, padding, and length
  123. # errors of encrypted keys. It is strongly recommended, in the event
  124. # of receiving an improperly formatted key, that the recipient
  125. # substitute a randomly generated CEK and proceed to the next step, to
  126. # mitigate timing attacks.
  127. cek_bytes = _get_random_cek_bytes_for_enc(enc)
  128. # Compute the Encoded Protected Header value BASE64URL(UTF8(JWE
  129. # Protected Header)). If the JWE Protected Header is not present
  130. # (which can only happen when using the JWE JSON Serialization and
  131. # no "protected" member is present), let this value be the empty
  132. # string.
  133. protected_header = encoded_header
  134. # Let the Additional Authenticated Data encryption parameter be
  135. # ASCII(Encoded Protected Header). However, if a JWE AAD value is
  136. # present (which can only be the case when using the JWE JSON
  137. # Serialization), instead let the Additional Authenticated Data
  138. # encryption parameter be ASCII(Encoded Protected Header || '.' ||
  139. # BASE64URL(JWE AAD)).
  140. aad = protected_header
  141. # Decrypt the JWE Ciphertext using the CEK, the JWE Initialization
  142. # Vector, the Additional Authenticated Data value, and the JWE
  143. # Authentication Tag (which is the Authentication Tag input to the
  144. # calculation) using the specified content encryption algorithm,
  145. # returning the decrypted plaintext and validating the JWE
  146. # Authentication Tag in the manner specified for the algorithm,
  147. # rejecting the input without emitting any decrypted output if the
  148. # JWE Authentication Tag is incorrect.
  149. try:
  150. plain_text = _decrypt_and_auth(cek_bytes, enc, cipher_text, iv, aad, auth_tag)
  151. except NotImplementedError:
  152. raise JWEError(f"enc {enc} is not implemented")
  153. except Exception as e:
  154. raise JWEError(e)
  155. # If a "zip" parameter was included, uncompress the decrypted
  156. # plaintext using the specified compression algorithm.
  157. if plain_text is not None:
  158. plain_text = _decompress(header.get("zip"), plain_text)
  159. return plain_text if cek_valid else None
  160. def get_unverified_header(jwe_str):
  161. """Returns the decoded headers without verification of any kind.
  162. Args:
  163. jwe_str (str): A compact serialized JWE to decode the headers from.
  164. Returns:
  165. dict: The dict representation of the JWE headers.
  166. Raises:
  167. JWEError: If there is an exception decoding the JWE.
  168. """
  169. header = _jwe_compact_deserialize(jwe_str)[0]
  170. return header
  171. def _decrypt_and_auth(cek_bytes, enc, cipher_text, iv, aad, auth_tag):
  172. """
  173. Decrypt and verify the data
  174. Args:
  175. cek_bytes (bytes): cek to derive encryption and possible auth key to
  176. verify the auth tag
  177. cipher_text (bytes): Encrypted data
  178. iv (bytes): Initialization vector (iv) used to encrypt data
  179. aad (bytes): Additional Authenticated Data used to verify the data
  180. auth_tag (bytes): Authentication ntag to verify the data
  181. Returns:
  182. (bytes): Decrypted data
  183. """
  184. # Decrypt the JWE Ciphertext using the CEK, the JWE Initialization
  185. # Vector, the Additional Authenticated Data value, and the JWE
  186. # Authentication Tag (which is the Authentication Tag input to the
  187. # calculation) using the specified content encryption algorithm,
  188. # returning the decrypted plaintext
  189. # and validating the JWE
  190. # Authentication Tag in the manner specified for the algorithm,
  191. if enc in ALGORITHMS.HMAC_AUTH_TAG:
  192. encryption_key, mac_key, key_len = _get_encryption_key_mac_key_and_key_length_from_cek(cek_bytes, enc)
  193. auth_tag_check = _auth_tag(cipher_text, iv, aad, mac_key, key_len)
  194. elif enc in ALGORITHMS.GCM:
  195. encryption_key = jwk.construct(cek_bytes, enc)
  196. auth_tag_check = auth_tag # GCM check auth on decrypt
  197. else:
  198. raise NotImplementedError(f"enc {enc} is not implemented!")
  199. plaintext = encryption_key.decrypt(cipher_text, iv, aad, auth_tag)
  200. if auth_tag != auth_tag_check:
  201. raise JWEError("Invalid JWE Auth Tag")
  202. return plaintext
  203. def _get_encryption_key_mac_key_and_key_length_from_cek(cek_bytes, enc):
  204. derived_key_len = len(cek_bytes) // 2
  205. mac_key_bytes = cek_bytes[0:derived_key_len]
  206. mac_key = _get_hmac_key(enc, mac_key_bytes)
  207. encryption_key_bytes = cek_bytes[-derived_key_len:]
  208. encryption_alg, _ = enc.split("-")
  209. encryption_key = jwk.construct(encryption_key_bytes, encryption_alg)
  210. return encryption_key, mac_key, derived_key_len
  211. def _jwe_compact_deserialize(jwe_bytes):
  212. """
  213. Deserialize and verify the header and segments are appropriate.
  214. Args:
  215. jwe_bytes (bytes): The compact serialized JWE
  216. Returns:
  217. (dict, bytes, bytes, bytes, bytes, bytes)
  218. """
  219. # Base64url decode the encoded representations of the JWE
  220. # Protected Header, the JWE Encrypted Key, the JWE Initialization
  221. # Vector, the JWE Ciphertext, the JWE Authentication Tag, and the
  222. # JWE AAD, following the restriction that no line breaks,
  223. # whitespace, or other additional characters have been used.
  224. jwe_bytes = ensure_binary(jwe_bytes)
  225. try:
  226. header_segment, encrypted_key_segment, iv_segment, cipher_text_segment, auth_tag_segment = jwe_bytes.split(
  227. b".", 4
  228. )
  229. header_data = base64url_decode(header_segment)
  230. except ValueError:
  231. raise JWEParseError("Not enough segments")
  232. except (TypeError, binascii.Error):
  233. raise JWEParseError("Invalid header")
  234. # Verify that the octet sequence resulting from decoding the
  235. # encoded JWE Protected Header is a UTF-8-encoded representation
  236. # of a completely valid JSON object conforming to RFC 7159
  237. # [RFC7159]; let the JWE Protected Header be this JSON object.
  238. #
  239. # If using the JWE Compact Serialization, let the JOSE Header be
  240. # the JWE Protected Header. Otherwise, when using the JWE JSON
  241. # Serialization, let the JOSE Header be the union of the members
  242. # of the JWE Protected Header, the JWE Shared Unprotected Header
  243. # and the corresponding JWE Per-Recipient Unprotected Header, all
  244. # of which must be completely valid JSON objects. During this
  245. # step, verify that the resulting JOSE Header does not contain
  246. # duplicate Header Parameter names. When using the JWE JSON
  247. # Serialization, this restriction includes that the same Header
  248. # Parameter name also MUST NOT occur in distinct JSON object
  249. # values that together comprise the JOSE Header.
  250. try:
  251. header = json.loads(header_data)
  252. except ValueError as e:
  253. raise JWEParseError(f"Invalid header string: {e}")
  254. if not isinstance(header, Mapping):
  255. raise JWEParseError("Invalid header string: must be a json object")
  256. try:
  257. encrypted_key = base64url_decode(encrypted_key_segment)
  258. except (TypeError, binascii.Error):
  259. raise JWEParseError("Invalid encrypted key")
  260. try:
  261. iv = base64url_decode(iv_segment)
  262. except (TypeError, binascii.Error):
  263. raise JWEParseError("Invalid IV")
  264. try:
  265. ciphertext = base64url_decode(cipher_text_segment)
  266. except (TypeError, binascii.Error):
  267. raise JWEParseError("Invalid cyphertext")
  268. try:
  269. auth_tag = base64url_decode(auth_tag_segment)
  270. except (TypeError, binascii.Error):
  271. raise JWEParseError("Invalid auth tag")
  272. return header, header_segment, encrypted_key, iv, ciphertext, auth_tag
  273. def _encoded_header(alg, enc, zip, cty, kid):
  274. """
  275. Generate an appropriate JOSE header based on the values provided
  276. Args:
  277. alg (str): Key wrap/negotiation algorithm
  278. enc (str): Encryption algorithm
  279. zip (str): Compression method
  280. cty (str): Content type of the encrypted data
  281. kid (str): ID for the key used for the operation
  282. Returns:
  283. bytes: JSON object of header based on input
  284. """
  285. header = {"alg": alg, "enc": enc}
  286. if zip:
  287. header["zip"] = zip
  288. if cty:
  289. header["cty"] = cty
  290. if kid:
  291. header["kid"] = kid
  292. json_header = json.dumps(
  293. header,
  294. separators=(",", ":"),
  295. sort_keys=True,
  296. ).encode("utf-8")
  297. return base64url_encode(json_header)
  298. def _big_endian(int_val):
  299. return pack("!Q", int_val)
  300. def _encrypt_and_auth(key, alg, enc, zip, plaintext, aad):
  301. """
  302. Generate a content encryption key (cek) and initialization
  303. vector (iv) based on enc and alg, compress the plaintext based on zip,
  304. encrypt the compressed plaintext using the cek and iv based on enc
  305. Args:
  306. key (Key): The key provided for encryption
  307. alg (str): The algorithm use for key wrap/negotiation
  308. enc (str): The encryption algorithm with which to encrypt the plaintext
  309. zip (str): The compression algorithm with which to compress the plaintext
  310. plaintext (bytes): The data to encrypt
  311. aad (str): Additional authentication data utilized for generating an
  312. auth tag
  313. Returns:
  314. (bytes, bytes, bytes, bytes): A tuple of the following data
  315. (key wrapped cek, iv, cipher text, auth tag)
  316. """
  317. try:
  318. cek_bytes, kw_cek = _get_cek(enc, alg, key)
  319. except NotImplementedError:
  320. raise JWEError(f"alg {alg} is not implemented")
  321. if enc in ALGORITHMS.HMAC_AUTH_TAG:
  322. encryption_key, mac_key, key_len = _get_encryption_key_mac_key_and_key_length_from_cek(cek_bytes, enc)
  323. iv, ciphertext, tag = encryption_key.encrypt(plaintext, aad)
  324. auth_tag = _auth_tag(ciphertext, iv, aad, mac_key, key_len)
  325. elif enc in ALGORITHMS.GCM:
  326. encryption_key = jwk.construct(cek_bytes, enc)
  327. iv, ciphertext, auth_tag = encryption_key.encrypt(plaintext, aad)
  328. else:
  329. raise NotImplementedError(f"enc {enc} is not implemented!")
  330. return kw_cek, iv, ciphertext, auth_tag
  331. def _get_hmac_key(enc, mac_key_bytes):
  332. """
  333. Get an HMACKey for the provided encryption algorithm and key bytes
  334. Args:
  335. enc (str): Encryption algorithm
  336. mac_key_bytes (bytes): vytes for the HMAC key
  337. Returns:
  338. (HMACKey): The key to perform HMAC actions
  339. """
  340. _, hash_alg = enc.split("-")
  341. mac_key = jwk.construct(mac_key_bytes, hash_alg)
  342. return mac_key
  343. def _compress(zip, plaintext):
  344. """
  345. Compress the plaintext based on the algorithm supplied
  346. Args:
  347. zip (str): Compression Algorithm
  348. plaintext (bytes): plaintext to compress
  349. Returns:
  350. (bytes): Compressed plaintext
  351. """
  352. if zip not in ZIPS.SUPPORTED:
  353. raise NotImplementedError("ZIP {} is not supported!")
  354. if zip is None:
  355. compressed = plaintext
  356. elif zip == ZIPS.DEF:
  357. compressed = zlib.compress(plaintext)
  358. else:
  359. raise NotImplementedError("ZIP {} is not implemented!")
  360. return compressed
  361. def _decompress(zip, compressed):
  362. """
  363. Decompress the plaintext based on the algorithm supplied
  364. Args:
  365. zip (str): Compression Algorithm
  366. plaintext (bytes): plaintext to decompress
  367. Returns:
  368. (bytes): Compressed plaintext
  369. """
  370. if zip not in ZIPS.SUPPORTED:
  371. raise NotImplementedError("ZIP {} is not supported!")
  372. if zip is None:
  373. decompressed = compressed
  374. elif zip == ZIPS.DEF:
  375. decompressed = zlib.decompress(compressed)
  376. else:
  377. raise NotImplementedError("ZIP {} is not implemented!")
  378. return decompressed
  379. def _get_cek(enc, alg, key):
  380. """
  381. Get the content encryption key
  382. Args:
  383. enc (str): Encryption algorithm
  384. alg (str): kwy wrap/negotiation algorithm
  385. key (Key): Key provided to encryption method
  386. Return:
  387. (bytes, bytes): Tuple of (cek bytes and wrapped cek)
  388. """
  389. if alg == ALGORITHMS.DIR:
  390. cek, wrapped_cek = _get_direct_key_wrap_cek(key)
  391. else:
  392. cek, wrapped_cek = _get_key_wrap_cek(enc, key)
  393. return cek, wrapped_cek
  394. def _get_direct_key_wrap_cek(key):
  395. """
  396. Get the cek and wrapped cek from the encryption key direct
  397. Args:
  398. key (Key): Key provided to encryption method
  399. Return:
  400. (Key, bytes): Tuple of (cek Key object and wrapped cek)
  401. """
  402. # Get the JWK data to determine how to derive the cek
  403. jwk_data = key.to_dict()
  404. if jwk_data["kty"] == "oct":
  405. # Get the last half of an octal key as the cek
  406. cek_bytes = _get_key_bytes_from_key(key)
  407. wrapped_cek = b""
  408. else:
  409. raise NotImplementedError("JWK type {} not supported!".format(jwk_data["kty"]))
  410. return cek_bytes, wrapped_cek
  411. def _get_key_bytes_from_key(key):
  412. """
  413. Get the raw key bytes from a Key object
  414. Args:
  415. key (Key): Key from which to extract the raw key bytes
  416. Returns:
  417. (bytes) key data
  418. """
  419. jwk_data = key.to_dict()
  420. encoded_key = jwk_data["k"]
  421. cek_bytes = base64url_decode(encoded_key)
  422. return cek_bytes
  423. def _get_key_wrap_cek(enc, key):
  424. """_get_rsa_key_wrap_cek
  425. Get the content encryption key for RSA key wrap
  426. Args:
  427. enc (str): Encryption algorithm
  428. key (Key): Key provided to encryption method
  429. Returns:
  430. (Key, bytes): Tuple of (cek Key object and wrapped cek)
  431. """
  432. cek_bytes = _get_random_cek_bytes_for_enc(enc)
  433. wrapped_cek = key.wrap_key(cek_bytes)
  434. return cek_bytes, wrapped_cek
  435. def _get_random_cek_bytes_for_enc(enc):
  436. """
  437. Get the random cek bytes based on the encryptionn algorithm
  438. Args:
  439. enc (str): Encryption algorithm
  440. Returns:
  441. (bytes) random bytes for cek key
  442. """
  443. if enc == ALGORITHMS.A128GCM:
  444. num_bits = 128
  445. elif enc == ALGORITHMS.A192GCM:
  446. num_bits = 192
  447. elif enc in (ALGORITHMS.A128CBC_HS256, ALGORITHMS.A256GCM):
  448. num_bits = 256
  449. elif enc == ALGORITHMS.A192CBC_HS384:
  450. num_bits = 384
  451. elif enc == ALGORITHMS.A256CBC_HS512:
  452. num_bits = 512
  453. else:
  454. raise NotImplementedError(f"{enc} not supported")
  455. cek_bytes = get_random_bytes(num_bits // 8)
  456. return cek_bytes
  457. def _auth_tag(ciphertext, iv, aad, mac_key, tag_length):
  458. """
  459. Get ann auth tag from the provided data
  460. Args:
  461. ciphertext (bytes): Encrypted value
  462. iv (bytes): Initialization vector
  463. aad (bytes): Additional Authenticated Data
  464. mac_key (bytes): Key to use in generating the MAC
  465. tag_length (int): How log the tag should be
  466. Returns:
  467. (bytes) Auth tag
  468. """
  469. al = _big_endian(len(aad) * 8)
  470. auth_tag_input = aad + iv + ciphertext + al
  471. signature = mac_key.sign(auth_tag_input)
  472. auth_tag = signature[0:tag_length]
  473. return auth_tag
  474. def _jwe_compact_serialize(encoded_header, encrypted_cek, iv, cipher_text, auth_tag):
  475. """
  476. Generate a compact serialized JWE
  477. Args:
  478. encoded_header (bytes): Base64 URL Encoded JWE header JSON
  479. encrypted_cek (bytes): Encrypted content encryption key (cek)
  480. iv (bytes): Initialization vector (IV)
  481. cipher_text (bytes): Cipher text
  482. auth_tag (bytes): JWE Auth Tag
  483. Returns:
  484. (str): JWE compact serialized string
  485. """
  486. cipher_text = ensure_binary(cipher_text)
  487. encoded_encrypted_cek = base64url_encode(encrypted_cek)
  488. encoded_iv = base64url_encode(iv)
  489. encoded_cipher_text = base64url_encode(cipher_text)
  490. encoded_auth_tag = base64url_encode(auth_tag)
  491. return (
  492. encoded_header
  493. + b"."
  494. + encoded_encrypted_cek
  495. + b"."
  496. + encoded_iv
  497. + b"."
  498. + encoded_cipher_text
  499. + b"."
  500. + encoded_auth_tag
  501. )