Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CMK Hybrid Decryption Quickstart (Python)

Decrypt Zoom Team Chat messages and files that are protected with Customer Managed Keys (CMK) Hybrid / Customer-Side Encryption (CSE).

Python License: MIT Crypto: AES-GCM-256

When CMK Hybrid encryption is enabled, content returned by the Zoom Team Chat archival APIs is encrypted with AES-GCM-256. The plaintext data key is released only by your KeyConnector — Zoom never has access to it. These quickstart scripts perform the final, local decryption step so you can recover the original message text or file contents on your own machine.


Table of contents


How it works

The Zoom Team Chat archival API (for example, reportChatMessages) returns each encrypted item alongside an encryption_info block:

{
  "encryption_info": {
    "key_id": "RVhBTVBMRV9LRVlfSUQ=",
    "encrypt_context": "RVhBTVBMRV9BQ0NPVU5UIyMjQ1NFOlRlYW1DaGF0OkV4dGVybmFsIyMjRVhBTVBMRV9DSEFOTkVMX0lEQGNvbmZlcmVuY2UueG1wcC5leGFtcGxlLmNvbQ==",
    "encrypt_content": "DAABAgMEBQYHCAkKCwAAGgAAAGWA1xC2OLTex7FR5B6gvCSybWLqHP8c6lT9u0p0NTHapKQopJ6YTctZaQ==",
    "encryption_algorithm": "AES-GCM-256"
  }
}

Decryption is a two-step, client-side process:

       You  (run this quickstart)
                    |
                    |  (1) GET reportChatMessages
                    v
   +--------------------------------+
   |       Zoom Team Chat API       |
   +--------------------------------+
                    |
                    |  (2) encryption_info
                    |     { key_id, encrypt_context,
                    |       encrypt_content, algorithm }
                    v
       You  (run this quickstart)
                    |
                    |  (3) key_id + encrypt_context
                    v
   +--------------------------------+
   |          KeyConnector          |
   |       (customer-managed)       |
   +--------------------------------+
                    |
                    |  (4) plaintext data key
                    v
       You  (run this quickstart)
                    |
                    |  (5) --key <data key>  --data <encrypt_content>
                    v
   +--------------------------------+
   | decrypt_text.py   (text value) |
   | decrypt_file_content.py (file) |
   +--------------------------------+
                    |
                    |  (6) decrypted plaintext / bytes
                    v
        Decrypted message or file
  1. Call the Team Chat API and read the encryption_info from the response.
  2. Send key_id and encrypt_context to your KeyConnector archival endpoint (POST /kms/cse/archival/datakey/decrypt). It returns the Base64-encoded, 32-byte plaintext data key.
  3. Run the appropriate script in this repo with that key and the encrypt_content value to recover the plaintext.

The plaintext data key and all decrypted output stay on your machine. The scripts perform no network calls.

Prerequisites

  • Python 3.10 or newer (the scripts use X | Y type-union syntax).
  • Access to your organization's KeyConnector archival API to obtain the plaintext data key.
  • The encrypted content (encryption_info.encrypt_content) from a Team Chat API response.

Installation

git clone https://github.com/zoom/cmk-hybrid-decryption-quickstart-python.git
cd cmk-hybrid-decryption-quickstart-python
pip3 install -r requirements.txt

The only runtime dependency is cryptography.

Tip: use a virtual environment to keep dependencies isolated:

python3 -m venv .venv && source .venv/bin/activate
pip3 install -r requirements.txt

Quickstart

Assuming you have already retrieved the plaintext data key from your KeyConnector:

# Decrypt a single encrypted chat message value
python3 decrypt_text.py \
    --key 'BASE64_PLAINTEXT_DATA_KEY' \
    --data 'BASE64_ENCRYPT_CONTENT'
# Decrypt an encrypted chat file
python3 decrypt_file_content.py \
    --key 'BASE64_PLAINTEXT_DATA_KEY' \
    --input './encrypted_file.bin' \
    --output './decrypted_file'

Add --debug to either command to print structural details (segment lengths, IV, AAD, tag) to stderr.

Scripts

Script Use for Input Output
decrypt_text.py A single encrypted text value (e.g. a chat message's encrypt_content) Base64 key + Base64 payload Decrypted UTF-8 text on stdout
decrypt_file_content.py An encrypted file of one or more segments (e.g. a downloaded chat file) Base64 key + input file path Decrypted bytes written to an output file

Both scripts share the same segment payload format and the same AES-GCM-256 primitive.

Command reference

decrypt_text.py

Argument Required Description
--key Base64-encoded 32-byte AES-256 key (the plaintext data key from the KeyConnector)
--data Base64-encoded encrypted payload (encryption_info.encrypt_content)
--debug Print payload parsing details to stderr

decrypt_file_content.py

Argument Required Description
--key Base64-encoded 32-byte AES-256 key (the plaintext data key from the KeyConnector)
--input Path to the encrypted input file
--output Path where the decrypted file is written
--debug Print per-segment details to stderr

Encrypted payload format

Each encrypted segment is a length-prefixed binary structure. Multi-byte length fields are little-endian.

Offset  Size (bytes)   Field        Description
------  ------------   ----------   -------------------------------------------
0       1              iv_len       Length of the IV (expected 12)
1       iv_len         iv           Initialization vector / GCM nonce
+       2 (LE uint16)  aad_len      Length of the AAD (0 if none)
+       aad_len        aad          Additional Authenticated Data (optional)
+       4 (LE uint32)  cipher_len   Length of the cipher text
+       cipher_len     cipher       Encrypted bytes
+       16             tag          GCM authentication tag
  • decrypt_text.py expects exactly one segment and rejects any trailing bytes.
  • decrypt_file_content.py reads segments back-to-back until end of file and concatenates the plaintext.

Exit codes

Code Meaning
0 Success
2 Handled error — invalid Base64, wrong key length, malformed/truncated payload, authentication-tag mismatch, or non-UTF-8 text output
3 Unexpected error

The scripts use authenticated decryption: plaintext is emitted only after the GCM authentication tag verifies. A tag mismatch (exit 2) means the key is wrong, the content was corrupted or modified, or the payload is not in the expected format.

Troubleshooting

Symptom Likely cause Fix
Plain key must decode to 32 bytes... --key is not a Base64-encoded 256-bit key Use the raw plaintext data key returned by the KeyConnector, Base64-encoded
authentication tag mismatch Wrong key, or encrypt_content was altered/truncated Re-fetch both the data key and encrypt_content; ensure they belong to the same item
... is not valid Base64 Whitespace or shell mangling in the argument Wrap the value in single quotes; avoid line breaks
plaintext is not valid UTF-8 text (text script) The content is binary, not text Use decrypt_file_content.py and write to a file instead
ModuleNotFoundError: cryptography Dependency not installed pip3 install -r requirements.txt

Security considerations

  • Decrypt-only, offline. The scripts never contact the network; the plaintext key and decrypted output remain local.
  • Key exposure. Passing the key via --key makes it visible in process listings (ps) and shell history. Run only on trusted machines and clear your shell history afterward. For automation, consider adapting the scripts to read the key from an environment variable or stdin.
  • Sensitive output. Decrypted content may contain private communications. Store output files securely and delete them when no longer needed.
  • Integrity. AES-GCM verification guarantees the ciphertext (and any AAD) was not modified; the scripts never return unverified or partial plaintext.

Limitations (non-production)

This is quickstart code optimized for clarity. Before using it at scale or in an automated pipeline, be aware of the following and adapt as needed:

  • Whole-segment in-memory decryption. Each segment's ciphertext is read fully into memory before decryption. There is no maximum-size guard, so a very large input could exhaust memory. For large or untrusted files, add an explicit size cap and/or process in bounded chunks.
  • Key passed on the command line. --key is convenient for a quickstart but is visible in process listings and shell history (see Security considerations). For automation, adapt the scripts to read the key from stdin, an environment variable, or a secrets manager.
  • Minimal output-path handling. The file script writes to the exact --output path you provide and will overwrite an existing file. It does not normalize or sandbox the path. Supply trusted paths, or add path validation before adopting it in an automated context.

Disclaimer

This is sample/quickstart code provided as-is to illustrate the CMK Hybrid decryption flow. Review and adapt it to your own security and operational requirements before using it with production data.

License

Released under the MIT License.

About

Python scripts for decrypting Customer Managed Keys (CMK) Hybrid encrypted chat messages and files using the Zoom Team Chat API.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages