If you see 'No module named Crypto'
The app context is a set of parameters that describes the execution context for the application. These parameters include user identifiers, meeting identifiers, and other contextual information.
You can obtain an app context by using the:
- HomeURL template parameters. The template passes specific parameters into the Home URL query as plain data.
X-Zoom-App-Contextheader that is sent with every HomeURL request when the app is being loaded and contains encrypted data that allows backend validation.
HomeURL template parameters
When configuring the app in the Marketplace, in the Home URL field, enter the parameter name in {} brackets. The Marketplace dynamically replaces the parameter with actual values when the application loads.
Usage example:
https://home.url/?accountId={accountId}&meetingId={meetingUUID}
Each value is URL-encoded. If a value is not available in the specific context, "none" is used as a placeholder.
Supported parameters:
-
accountId- account identifier, matches values available in account and user REST API methodsAccount ID is Personally Identifiable Information (PII), and per the Zoom Apps Security Guidelines, should never be logged or stored in cleartext, and should be encrypted at all times when at rest.
-
runningContext- matchestypfield from X-Zoom-App-Context header -
meetingUUID -
breakoutRoomUUID -
collaborationId -
invitationId -
action- matchesactfield from X-Zoom-App-Context header -
product -
accountNumber- account number, matches values available in Zoom account page, and user REST API methods. Only available for paid users.
X-Zoom-App-Context header
When a user attempts to open your app from the Zoom client, Zoom sends an HTTP request to your app's Home URL and upon successfully completing the request, it renders the content of the Home URL in the Zoom client.
The request sent to your Home URL includes X-Zoom-App-Context header which is an AES-GCM encrypted JSON object with information about the user and the context in which the user opened the app.
This document provides details on how you can decrypt the header values to extract information.
Decrypting the header value
The value of the header contains a base64-encoded string that includes the initialization vector, additional authentication data, the cipher text itself, and an authentication tag which are used as inputs for the decryption process.
The header also includes the length of each input in bytes:
[ivLength: 1 byte][iv][aadLength: 2 bytes][aad][cipherTextLength: 4 bytes][cipherText][tag: 16 bytes]
Thus, to parse the initialization vector (iv), you would read the first byte of the sequence to get its length. Then, you would read the next n bytes in the sequence to get the actual value of iv. To get the aad, you would read the 2 bytes following the iv to get its length, and then the next m bytes in the sequence to get the aad value. The tag at the end of the sequence has a predetermined length of 16 bytes, so its length is not included.
Decryption examples
const crypto = require("crypto");
function unpack(context) {
// Decode base64
let buf = Buffer.from(context, "base64");
// Get iv length (1 byte)
const ivLength = buf.readUInt8();
buf = buf.slice(1);
// Get iv
const iv = buf.slice(0, ivLength);
buf = buf.slice(ivLength);
// Get aad length (2 bytes)
const aadLength = buf.readUInt16LE();
buf = buf.slice(2);
// Get aad
const aad = buf.slice(0, aadLength);
buf = buf.slice(aadLength);
// Get cipher length (4 bytes)
const cipherLength = buf.readInt32LE();
buf = buf.slice(4);
// Get cipherText
const cipherText = buf.slice(0, cipherLength);
// Get tag
const tag = buf.slice(cipherLength);
return {
iv,
aad,
cipherText,
tag,
};
}
function decrypt(context, secret) {
const { iv, aad, cipherText, tag } = unpack(context);
const decipher = crypto
.createDecipheriv(
"aes-256-gcm",
crypto.createHash("sha256").update(secret).digest(),
iv,
)
.setAAD(aad)
.setAuthTag(tag)
.setAutoPadding(false);
const decrypted = decipher.update(cipherText) + decipher.final();
return JSON.parse(decrypted);
}
require "base64"
require 'openssl'
data = '{"typ":"panel","uid":"77A6G6xIS62MkqTlFWJhbg","dev":"qAAqvyeJcTFUDxoW5XzkUfND/nftgjro08GA+niqXwg","ts":1608618226564}'
b64_cipher_context = "DG7HCXYGApQWw9J4nAAAdQAAAKJI45T4UDBcUUrburGWMYVryK6DCYoR1f_xPqlf3-MEDXRT6T3wftRLow-NE3UYqfDORa8tjPzdK8fouUZw0wQDhBT1wF7Whi94JxfgEeorpKb6KErIAZeS-AcnkVBAHs9ZdrrJHg3Svff4irl-ypyYKQIMqNkssqij8Sqb5K3UMaQdOME"
clientSecretKey = "6pTg05u9xBHmFKkhdRieOatMZIihN3m8"
def unpack(b64_cipher_context)
cipher_text = Base64.urlsafe_decode64 b64_cipher_context
# [iv-len:1][iv-bytes][aad-len:2][aad-bytes][cipher-len:4][cipher-bytes][tag-bytes:16]
iv_aad_cipher_text_auth_tag = cipher_text.unpack("C*")
# puts("iv_aad_cipher_text_auth_tag", iv_aad_cipher_text_auth_tag)
# Extract iv
iv_length = iv_aad_cipher_text_auth_tag[0]
#puts("v_length", iv_length)
iv = iv_aad_cipher_text_auth_tag[1..(iv_length + 1 - 1)].pack("C*")
# Extract aad
aad_cipher_text_auth_tag = iv_aad_cipher_text_auth_tag[(iv_length+1)..]
aad_length = aad_cipher_text_auth_tag[0] + (aad_cipher_text_auth_tag[1] << 8)
#puts("aad_length", aad_length)
aad = aad_cipher_text_auth_tag[2..(aad_length + 2 - 1)].pack("C*") if aad_length > 0
# Extract the auth_tag. auth_tag_length = 16.
cipher_text_with_auth_tag = aad_cipher_text_auth_tag[(aad_length + 2)..]
cipher_length = cipher_text_with_auth_tag[0]
cipher_text = cipher_text_with_auth_tag[4..-17].pack("C*")
auth_tag = cipher_text_with_auth_tag.last(16).pack("C*")
return iv, aad, cipher_text, auth_tag
end
def decrypt(context, secret)
iv, aad, cipher_text, auth_tag = unpack(context)
# Initializing with 256. This seems to work with
# 128 byte configuration in Java+
cipher = OpenSSL::Cipher.new ("aes-256-gcm")
sha256 = OpenSSL::Digest::SHA256.new
key = sha256.digest(secret)
# puts('key', key)
# Key and iv should be Hex strings.
cipher.decrypt
cipher.padding = 0
cipher.key = key
cipher.iv = iv
cipher.auth_tag = auth_tag
if (aad and aad.length > 0)
cipher.auth_data = aad
end
cipher.update(cipher_text) + cipher.final
end
plain_text = decrypt(b64_cipher_context, clientSecretKey)
puts("Decrypt Data", plain_text)
puts("Test success", plain_text==data)
import json
import base64
import binascii
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
# If you see 'No module named Crypto'
# Please read
# https://pycryptodome.readthedocs.io/en/latest/src/faq.html#why-do-i-get-the-error-no-module-named-crypto-on-windows
data = '{"typ":"panel","uid":"77A6G6xIS62MkqTlFWJhbg","dev":"qAAqvyeJcTFUDxoW5XzkUfND/nftgjro08GA+niqXwg","ts":1608618226564}'
b64_cipher_context = "DG7HCXYGApQWw9J4nAAAdQAAAKJI45T4UDBcUUrburGWMYVryK6DCYoR1f_xPqlf3-MEDXRT6T3wftRLow-NE3UYqfDORa8tjPzdK8fouUZw0wQDhBT1wF7Whi94JxfgEeorpKb6KErIAZeS-AcnkVBAHs9ZdrrJHg3Svff4irl-ypyYKQIMqNkssqij8Sqb5K3UMaQdOME"
clientSecretKey = "6pTg05u9xBHmFKkhdRieOatMZIihN3m8"
def urlsafe_b64decode(data):
data = str.encode(data)
missing_padding = len(data) % 4
if missing_padding:
data += b'='* (4 - missing_padding)
return base64.urlsafe_b64decode(data)
def unpack(cipher_text):
# [iv-len:1][iv-bytes][aad-len:2][aad-bytes][cipher-len:4][cipher-bytes][tag-bytes:16]
# import pdb; pdb.set_trace()
iv_aad_cipher_text_auth_tag = cipher_text # binascii.hexlify()
# Extract iv
iv_length = iv_aad_cipher_text_auth_tag[0]
iv = iv_aad_cipher_text_auth_tag[1:(iv_length + 1)]
# Extract aad
aad_cipher_text_auth_tag = iv_aad_cipher_text_auth_tag[(iv_length+1):]
aad_length = aad_cipher_text_auth_tag[0] + (aad_cipher_text_auth_tag[1] << 8)
aad = b''
if aad_length > 0:
aad = aad_cipher_text_auth_tag[2:(aad_length + 2)]
# Extract the auth_tag. auth_tag_length = 16.
cipher_text_with_auth_tag = aad_cipher_text_auth_tag[(aad_length + 2):]
cipher_text = cipher_text_with_auth_tag[4:-16]
tag = cipher_text_with_auth_tag[-16:]
return iv, aad, cipher_text, tag
cipher_text = urlsafe_b64decode(b64_cipher_context)
def decrypt(context, secret):
key = hashlib.sha256(secret.encode('utf-8')).digest()
iv, aad, cipher_text, tag = unpack(context);
cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
if len(aad) > 0:
cipher.update(aad)
cipher.update(aad)
data = cipher.decrypt_and_verify(cipher_text, tag)
return data
data_json = decrypt(cipher_text, clientSecretKey)
data_obj = json.loads(data_json)
print(data_obj)
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"fmt"
)
// ZoomContextDecrypter ...
// context - Encrypted Zoom App Contex (x-zoom-zapp-context)
// secretKey - Client Secret Key.
func ZoomContextDecrypter(context string, secretKey []byte) []byte {
ciphertext, err := base64.RawURLEncoding.DecodeString(context)
decoder := bytes.NewReader(ciphertext)
ivLength := make([]byte, 1)
_, _ = decoder.Read(ivLength)
iv := make([]byte, ivLength[0])
_, _ = decoder.Read(iv)
aadLengthBytes := make([]byte, 2)
_, _ = decoder.Read(aadLengthBytes)
addLength := binary.LittleEndian.Uint16(aadLengthBytes)
aad := make([]byte, addLength)
_, _ = decoder.Read(aad)
cipherLengthBytes := make([]byte, 4)
_, _ = decoder.Read(cipherLengthBytes)
cipherLength := binary.LittleEndian.Uint32(cipherLengthBytes)
encrypted := make([]byte, cipherLength+16)
_, _ = decoder.Read(encrypted)
hashed := sha256.Sum256(secretKey)
block, err := aes.NewCipher(hashed[:])
if err != nil {
panic(err.Error())
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
panic(err.Error())
}
plaintext, err := aesgcm.Open(nil, iv, encrypted, aad)
if err != nil {
panic(err.Error())
}
return plaintext
}
func main() {
secretKey := []byte("6pTg05u9xBHmFKkhdRieOatMZIihN3m8")
expectedOutput := "{\"typ\":\"panel\",\"uid\":\"77A6G6xIS62MkqTlFWJhbg\",\"dev\":\"qAAqvyeJcTFUDxoW5XzkUfND/nftgjro08GA+niqXwg\",\"ts\":1608618226564}"
context := "DG7HCXYGApQWw9J4nAAAdQAAAKJI45T4UDBcUUrburGWMYVryK6DCYoR1f_xPqlf3-MEDXRT6T3wftRLow-NE3UYqfDORa8tjPzdK8fouUZw0wQDhBT1wF7Whi94JxfgEeorpKb6KErIAZeS-AcnkVBAHs9ZdrrJHg3Svff4irl-ypyYKQIMqNkssqij8Sqb5K3UMaQdOME"
decryptedContext := ZoomContextDecrypter(context, secretKey)
fmt.Printf("%s\n", decryptedContext)
// Should print true
fmt.Println(expectedOutput == string(decryptedContext))
}
<?php
// See https://wiki.php.net/rfc/openssl_aead for why we are using
// https://packagist.org/packages/Spomky-Labs/php-aes-gcm.
// On Mac:
// > brew install composer
// > composer require "spomky-labs/php-aes-gcm"
require_once(__DIR__."/vendor/autoload.php");
use AESGCM\AESGCM;
class ZoomContextDecrypter {
private $key;
private $iv;
private $aad = "";
private $tag = "";
private $encrypted;
private $output = "";
private function Le2Int($byteArray) {
$value = 0;
for ($i = strlen($byteArray) - 1; $i >= 0; $i--) {
$value |= ord($byteArray[$i]) << $i * 8;
}
return $value;
}
private function scan($context, $clientSecret) {
// [iv-len:1][iv-bytes][aad-len:2][aad-bytes][cipher-len:4][cipher-bytes][tag-bytes:16]
$cipherText = base64_decode( strtr( $context, '-_', '+/') . str_repeat('=', 3 - ( 3 + strlen( $context )) % 4 ));
$readPtr = 0;
$ivLen = $this->Le2Int(substr($cipherText, 0, 1));
$readPtr += 1;
$this->iv = substr($cipherText, $readPtr, $ivLen);
$readPtr += $ivLen;
$aadLengthBytes = substr($cipherText, $readPtr, 2);
$readPtr += 2;
$aadLength = $this->Le2Int($aadLengthBytes);
if ($aadLength > 0) {
$this->aad = substr($cipherText, $readPtr, $aadLength);
$readPtr += $aadLength;
}
$cypherLengthBytes = substr($cipherText, $readPtr, 4);
$readPtr += 4;
$cypherLength = $this->Le2Int($cypherLengthBytes);
$this->encrypted = substr($cipherText, $readPtr, $cypherLength);
$readPtr += $cypherLength;
$this->tag = substr($cipherText, $readPtr);
$this->key = hex2bin(hash('sha256', $clientSecret));
}
public function decrypt($context, $clientSecret){
$this->scan($context, $clientSecret);
$this->output = AESGCM::decrypt($this->key, $this->iv, $this->encrypted, $this->aad, $this->tag);
return $this->output;
}
}
?>
import com.google.gson.JsonParser;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import static org.junit.Assert.assertEquals;
class ZoomAppContext {
public static JSONObject decrpt(String context, String clientSecret) throws IOException, NoSuchPaddingException,
NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException,
IllegalBlockSizeException, ParseException {
JSONObject unpackedContext = unpack(context);
byte[] iv = (byte[]) unpackedContext.get("iv");
byte[] aad = (byte[])unpackedContext.get("aad");
byte[] encrypt = (byte[]) unpackedContext.get("encrypt");
int aadLength = aad.length;
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * Byte.SIZE, iv);
byte[] plainKey = DigestUtils.sha256(clientSecret.getBytes());
SecretKeySpec secretKey = new SecretKeySpec(plainKey, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
if (aadLength > 0) {
cipher.updateAAD(aad);
}
String plainContext = new String(cipher.doFinal(encrypt));
return (JSONObject) new JSONParser().parse(plainContext);
}
public static JSONObject unpack(String context) throws IOException {
//convert context string to byte[]
byte[] contextByte = context.getBytes();
// [iv-len:1][iv-bytes][aad-len:2][aad-bytes][cipher-len:4][cipher-bytes][tag-bytes:16]
ByteArrayInputStream in = new ByteArrayInputStream(Base64.decodeBase64(contextByte));
// read iv
int ivLength = in.read();
byte[] iv = new byte[ivLength];
in.read(iv);
// read aad
int aadLength = in.read() + (in.read() << 8);
byte[] aad = new byte[aadLength];
in.read(aad);
// read cipher and tag
int cipherLength = in.read() + (in.read() << 8) + (in.read() << 16) + (in.read() << 24);
// the tag is always 16 length
byte[] encrypt = new byte[cipherLength + 16];
in.read(encrypt);
JSONObject unpackedContext = new JSONObject();
unpackedContext.put("iv", iv);
unpackedContext.put("aad", aad);
unpackedContext.put("encrypt", encrypt);
return unpackedContext;
}
public static void main(String[] args) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException,
IOException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, ParseException {
String context = "DG7HCXYGApQWw9J4nAAAdQAAAKJI45T4UDBcUUrburGWMYVryK6DCYoR1f_xPqlf3-MEDXRT6T3wftRLow-NE3UYqfDORa8tjPzdK8fouUZw0wQDhBT1wF7Whi94JxfgEeorpKb6KErIAZeS-AcnkVBAHs9ZdrrJHg3Svff4irl-ypyYKQIMqNkssqij8Sqb5K3UMaQdOME";
String clientSecret = "6pTg05u9xBHmFKkhdRieOatMZIihN3m8";
JSONObject decrypted = ZoomAppContext.decrpt(context, clientSecret);
JSONObject actualData = new JSONObject();
actualData.put("typ","panel");
actualData.put("uid","77A6G6xIS62MkqTlFWJhbg");
actualData.put("dev","qAAqvyeJcTFUDxoW5XzkUfND/nftgjro08GA+niqXwg");
actualData.put("ts", 1608618226564L);
assertEquals(JsonParser.parseString(String.valueOf(actualData)), JsonParser.parseString(String.valueOf(decrypted)));
System.out.println("Decrypted Data:" + decrypted);
System.out.println("Test success");
}
}
Validating the decrypted context expiration
After decrypting the Zoom App context, you must validate the app context has not expired before utilizing its values. Check the the expiration timestamp exp and ensure it hasn't expired.
Utilizing the decrypted values
Once you've decrypted the Zoom App context and validated it has not expired, you will find a JSON object similar to this:
{
"act": "action payload supplied in the deeplink",
"exp": "long, the expiration timestamp of this context",
"mid": "string, the Zoom meeting uuid identifies the meeting in which this app is opened, only returned when value of typ is 'meeting'",
"ts": "long, the create timestamp of this context",
"typ": "string, the context type where this app is opened, could be 'panel', 'meeting', 'webinar', 'chat'",
"uid": "string, the Zoom user id who open this app"
}
Values available only in chat context:
{
"aid": "string, the Action Command ID configured in build flow of the shortcut or or the Button Action ID in the Interactive card",
"chid": "string, the chat session id",
"msgid": "string, the message ID of the message from which the app is launched. You can use it to retrieve the files/text/reactions from the Web API",
"of": "string, the feature from where the app is opened or launched |messageShortcut|interactiveCard|composeShortcut|",
"tid": "string, the thread id identifies the thread in a chat",
"trid": "string, triggerId is a unique identifier generated by the Zoom platform for every user interaction with chat app. triggerId is sent in the event payload sent to chat apps.",
"typ": "string, the context type where this app is opened:'chat' for chat app"
}
You can utilize the values to:
- Understand whether the app was opened via the Zoom Apps panel or from a meeting.
- Check the timestamp at which the request was sent by Zoom.
- Build a user session using the
uidproperty containing the user ID of the Zoom user. - Look up the OAuth token associated with the user when needed using the
uid.
Variance in expected values
- Breakout Rooms: The
X-Zoom-App-Contextheader will contain the Breakout Room UUID in themidfield, and the main Meeting UUID in thepidfield.