Decrypt Zoom Team Chat messages and files that are protected with Customer Managed Keys (CMK) Hybrid / Customer-Side Encryption (CSE).
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.
- How it works
- Prerequisites
- Installation
- Quickstart
- Scripts
- Command reference
- Encrypted payload format
- Exit codes
- Troubleshooting
- Security considerations
- Limitations (non-production)
- Disclaimer
- License
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
- Call the Team Chat API and read the
encryption_infofrom the response. - Send
key_idandencrypt_contextto your KeyConnector archival endpoint (POST /kms/cse/archival/datakey/decrypt). It returns the Base64-encoded, 32-byte plaintext data key. - Run the appropriate script in this repo with that key and the
encrypt_contentvalue to recover the plaintext.
The plaintext data key and all decrypted output stay on your machine. The scripts perform no network calls.
- Python 3.10 or newer (the scripts use
X | Ytype-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.
git clone https://github.com/zoom/cmk-hybrid-decryption-quickstart-python.git
cd cmk-hybrid-decryption-quickstart-python
pip3 install -r requirements.txtThe 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
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.
| 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.
| 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 |
| 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 |
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.pyexpects exactly one segment and rejects any trailing bytes.decrypt_file_content.pyreads segments back-to-back until end of file and concatenates the plaintext.
| 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.
| 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 |
- Decrypt-only, offline. The scripts never contact the network; the plaintext key and decrypted output remain local.
- Key exposure. Passing the key via
--keymakes 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 orstdin. - 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.
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.
--keyis 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 fromstdin, an environment variable, or a secrets manager. - Minimal output-path handling. The file script writes to the exact
--outputpath 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.
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.
Released under the MIT License.