# Decryption of app context with Web Crypto API

**URL:** <https://devforum.zoom.us/t/decryption-of-app-context-with-web-crypto-api/79183>\
**Category:** Zoom Apps\
**Created:** [November 22, 2022, 10:05am UTC](https://devforum.zoom.us/t/decryption-of-app-context-with-web-crypto-api/79183 "2022-11-22T10:05:56Z")\
**Posts on this page:** 9\
**Page:** 1

<div class="post-metadata">

**Author:** ![pbowen](https://avatars.discourse-cdn.com/v4/letter/p/47e85d/32.png) [@pbowen](https://devforum.zoom.us/u/pbowen)\
**Post date:** [November 22, 2022, 10:05am UTC](https://devforum.zoom.us/t/decryption-of-app-context-with-web-crypto-api/79183/1 "2022-11-22T10:05:56Z")

</div>

**Zoom Apps Configuration**  
NextJS Edge Function (middleware) (probably not revelant)

**Description**  
I’ve managed to successfully employ the NodeJS example for decrypting the x-zoom-app-context header, but I’m struggling to port this to Web Crypto API.

**Error?**  
“Unsupported state or unable to authenticate data”

I use the same `unpack` function found [here](https://marketplace.zoom.us/docs/zoom-apps/zoomappcontext/), and have confirmed that between NodeJS Crypto and Web Crypto API the same bytes are passed in for all parameters:

```nohighlight
async function decryptCtxWithHash(cipherText, hash, iv, aad, tag) {
  const key = await subtle.importKey("raw", hash, "aes-gcm", false, [
    "decrypt",
  ]);
  const deciphered = await subtle.decrypt(
    { name: "AES-GCM", iv, additionalData: aad, tagLength: tag.byteLength * 8 },
    key,
    cipherText
  ); //Fails with "Unsupported state or unable to authenticate data"
  const value = deciphered.slice(0, deciphered.byteLength - tag.byteLength);
  return JSON.parse(new TextDecoder().decode(value));
}

```

The key seems to be imported correctly too

```nohighlight
CryptoKey {
  type: 'secret',
  extractable: false,
  algorithm: { name: 'AES-GCM', length: 256 },
  usages: ['decrypt']
}

```

---

<div class="post-metadata">

**Author:** ![MaxM](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/maxm/32/42303_2.png) [@MaxM](https://devforum.zoom.us/u/MaxM)\
**Post date:** [November 22, 2022, 8:41pm UTC](https://devforum.zoom.us/t/decryption-of-app-context-with-web-crypto-api/79183/2 "2022-11-22T20:41:15Z")

</div>

@pbowen Does using a different encoding help to work around this issue?

Here’s an example of stack overflow:

> <https://stackoverflow.com/questions/41266976/unsupported-state-or-unable-to-authenticate-data-with-aes-128-gcm-in-node>

Also you can see another implementation of the unpack function in the [Basic Sample App](https://github.com/zoom/zoomapps-sample-js).

Out of curiosity, what is the use case for checking this header client side? Are you looking to use the getAppContext() function in a serverless context?

---

<div class="post-metadata">

**Author:** ![pbowen](https://avatars.discourse-cdn.com/v4/letter/p/47e85d/32.png) [@pbowen](https://devforum.zoom.us/u/pbowen)\
**Post date:** [November 23, 2022, 9:19am UTC](https://devforum.zoom.us/t/decryption-of-app-context-with-web-crypto-api/79183/3 "2022-11-23T09:19:31Z")

</div>

Hi, Max, I’m unsure which encoding you’re referring to?

Like I said, the unpack function is fine, and I managed to decrypt the header in NodeJS just fine, but [NextJS Edge Functions](https://nextjs.org/learn/foundations/how-nextjs-works/cdns-and-edge) only have Web Crypto API available, which is a preferred position in our stack rather than a NodeJS API.  
The exact same bytes go into that `decryptCtxWithHash` function for NodeJS and NextJS, but the NextJS one fails with that error.

---

<div class="post-metadata">

**Author:** ![pbowen](https://avatars.discourse-cdn.com/v4/letter/p/47e85d/32.png) [@pbowen](https://devforum.zoom.us/u/pbowen)\
**Post date:** [November 24, 2022, 9:24am UTC](https://devforum.zoom.us/t/decryption-of-app-context-with-web-crypto-api/79183/4 "2022-11-24T09:24:14Z")

</div>

(Apologies, I didn’t realise there was a reply function before)  
Do you have a suggestion for a different encoding I could try?

---

<div class="post-metadata">

**Author:** ![MaxM](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/maxm/32/42303_2.png) [@MaxM](https://devforum.zoom.us/u/MaxM)\
**Post date:** [November 28, 2022, 11:24pm UTC](https://devforum.zoom.us/t/decryption-of-app-context-with-web-crypto-api/79183/5 "2022-11-28T23:24:04Z")

</div>

I tried testing this on my end but I suppose I don’t have a good understanding of how this is configured on your end.

Are you using the Edge runtime to run this logic on the backend or are you attempting to decrypt this on the client side?

I’m working to reproduce this using the [Basic Sample App](https://github.com/zoom/zoomapps-sample-js).

---

<div class="post-metadata">

**Author:** ![luisfernandez4u](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/luisfernandez4u/32/20729_2.png) [@luisfernandez4u](https://devforum.zoom.us/u/luisfernandez4u)\
**Post date:** [December 19, 2022, 9:42am UTC](https://devforum.zoom.us/t/decryption-of-app-context-with-web-crypto-api/79183/6 "2022-12-19T09:42:38Z")

</div>

Following this query to resolve my issue.

---

<div class="post-metadata">

**Author:** ![MaxM](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/maxm/32/42303_2.png) [@MaxM](https://devforum.zoom.us/u/MaxM)\
**Post date:** [December 19, 2022, 9:25pm UTC](https://devforum.zoom.us/t/decryption-of-app-context-with-web-crypto-api/79183/7 "2022-12-19T21:25:20Z")

</div>

Can you share your use case for using the web crypto API? Likewise, are you able to provide a code sample that you’re using?

---

<div class="post-metadata">

**Author:** ![pbowen](https://avatars.discourse-cdn.com/v4/letter/p/47e85d/32.png) [@pbowen](https://devforum.zoom.us/u/pbowen)\
**Post date:** [December 30, 2022, 10:42am UTC](https://devforum.zoom.us/t/decryption-of-app-context-with-web-crypto-api/79183/8 "2022-12-30T10:42:41Z")

</div>

My team chose to situate the decryption logic in our NodeJS API rather than our NextJS middleware, to circumvent this issue. Here is the original middleware I tried to use, in full:

```auto
import { NextRequest, NextResponse } from "next/server";

export type ZoomAppContext = {
  uid: string;
  aud: string;
  iss: "marketplace.zoom.us" | string;
  ts: number;
  exp: number;
} & (
  | { typ: "panel" }
  | { typ: "meeting"; mid: string; attendrole: "host" | string }
);

export function ZoomMiddleware(request: NextRequest) {
  //Detect and decrypt Zoom app context
  const zoomAppCtx = request.headers.get("x-zoom-app-context");
  if (zoomAppCtx) {
    const decrypted = decryptAppContext(
      zoomAppCtx,
      process.env.ZOOM_CLIENT_SECRET
    );
    console.log("decrypted", decrypted);
  }
  return NextResponse.next();
}

/**
 * Decode and parse a base64 encoded Zoom App Context
 * @param ctx - Encoded Zoom App Context
 * @return Decoded Zoom App Context object
 */
function unpack(ctx: string) {
  // Decode base64
  let buf = Buffer.from(ctx, "base64");

  // Get iv length (1 byte)
  const ivLength = buf.readUInt8();
  buf = buf.subarray(1);

  // Get iv
  const iv = buf.subarray(0, ivLength);
  buf = buf.subarray(ivLength);

  // Get aad length (2 bytes)
  const aadLength = buf.readUInt16LE();
  buf = buf.subarray(2);

  // Get aad
  const aad = buf.subarray(0, aadLength);
  buf = buf.subarray(aadLength);

  // Get cipher length (4 bytes)
  const cipherLength = buf.readInt32LE();
  buf = buf.subarray(4);

  // Get cipherText
  const cipherText = buf.subarray(0, cipherLength);

  // Get tag
  const tag = buf.subarray(cipherLength);

  return { iv, aad, cipherText, tag };
}

/**
 * Decrypts cipherText from a decoded Zoom App Context object
 * @param cipherText - Data to decrypt
 * @param hash - sha256 hash of the Client Secret
 * @param iv - Initialization Vector for cipherText
 * @param aad - Additional Auth Data for cipher
 * @param tag - cipherText auth tag
 */
async function decryptCtxWithHash(
  cipherText: Buffer,
  hash: ArrayBuffer,
  iv: Buffer,
  aad: Buffer,
  tag: Buffer
) {
  const key = await crypto.subtle.importKey("raw", hash, "aes-gcm", false, [
    "decrypt",
  ]);
  // AES/GCM decryption
  const deciphered = await crypto.subtle.decrypt(
    { name: "AES-GCM", iv, additionalData: aad, tagLength: tag.byteLength * 8, },
    key,
    cipherText
  );
  const value = deciphered.slice(0, deciphered.byteLength - tag.byteLength);
  console.log("value", value);
  return JSON.parse(new TextDecoder().decode(value)) as ZoomAppContext;
}

/**
 * Decodes, parses and decrypts the x-zoom-app-context header
 * @see https://marketplace.zoom.us/docs/beta-docs/zoom-apps/zoomappcontext#decrypting-the-header-value
 * @param header - Encoded Zoom App Context header
 * @param key - Client Secret for the Zoom App
 * @return Decrypted Zoom App Context or Error
 */
async function decryptAppContext(header: string, key = "") {
  // Decode and parse context
  const { iv, aad, cipherText, tag } = unpack(header);

  // Create sha256 hash from Client Secret (key)
  const hash = await crypto.subtle.digest(
    "SHA-256",
    new TextEncoder().encode(key)
  );

  // return decrypted context
  return await decryptCtxWithHash(cipherText, hash, iv, aad, tag);
}

```

I can’t guarantee this is the closest working version I got, but it should be shoe-hornable into a client-side JavaScript environment for testing.

---

<div class="post-metadata">

**Author:** ![jademckinley08](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/jademckinley08/32/45378_2.png) [@jademckinley08](https://devforum.zoom.us/u/jademckinley08)\
**Post date:** [February 11, 2023, 12:36pm UTC](https://devforum.zoom.us/t/decryption-of-app-context-with-web-crypto-api/79183/9 "2023-02-11T12:36:23Z")

</div>

Great stuff, thanks for sharing
