Zoom Apps Context

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
", 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 AESGCMAESGCM;
  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");
    }
}

Utilizing the decrypted values

Once you’ve decrypted the Zoom App context, you will find a JSON object similar to this:

{
   "typ": "string, the context type where this app is opened, could be 'panel', 'meeting', or 'webinar'",
   "uid": "string, the Zoom user id who open this app",
   "mid": "string, the Zoom meeting uuid identifies the meeting in which this app is opened, only returned when value of typ is 'meeting'",
   "act": "action payload supplied in the deeplink",
   "ts": "long, the create timestamp of this context",
   "exp":"long, the expiration timestamp of this context"
}

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 uid property 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-Context header will contain the Breakout Room UUID in the mid field, and the main Meeting UUID in the pid field.

Need help?

If you're looking for help, try Developer Support or our Developer Forum. Priority support is also available with Premier Developer Support plans.