diff --git a/docs/samples/plugin-encryption/app.js b/docs/samples/plugin-encryption/app.js
new file mode 100644
index 00000000000..e30f7a71270
--- /dev/null
+++ b/docs/samples/plugin-encryption/app.js
@@ -0,0 +1,113 @@
+/* eslint-env browser */
+
+/* global Webex */
+
+/* eslint-disable no-console */
+/* eslint-disable require-jsdoc */
+
+// Declare some globals that we'll need throughout.
+let webex;
+let enableProd = true;
+let subscribedUserIds = [];
+
+const credentialsFormElm = document.querySelector('#credentials');
+const tokenElm = document.querySelector('#access-token');
+const saveElm = document.querySelector('#access-token-save');
+const authStatusElm = document.querySelector('#access-token-status');
+const encryptedFileUrlInput = document.querySelector('#encrypted-file-url');
+const decryptFileBtn = document.querySelector('#decrypt-my-file-btn');
+const decryptFileResult = document.querySelector('#decrypt-file-result');
+
+// Store and Grab `access-token` from localstorage
+if (localStorage.getItem('date') > new Date().getTime()) {
+ tokenElm.value = localStorage.getItem('access-token');
+} else {
+ localStorage.removeItem('access-token');
+}
+
+tokenElm.addEventListener('change', (event) => {
+ localStorage.setItem('access-token', event.target.value);
+ localStorage.setItem('date', new Date().getTime() + 12 * 60 * 60 * 1000);
+});
+
+function changeEnv() {
+ enableProd = !enableProd;
+ enableProduction.innerHTML = enableProd ? 'In Production' : 'In Integration';
+}
+
+function updateStatus(enabled) {
+ decryptFileBtn.disabled = !enabled;
+}
+
+
+async function initWebex(e) {
+ e.preventDefault();
+ console.log('Authentication#initWebex()');
+
+ tokenElm.disabled = true;
+ saveElm.disabled = true;
+
+ decryptFileBtn.disabled = true;
+ authStatusElm.innerText = 'initializing...';
+
+ const webexConfig = {
+ config: {
+ logger: {
+ level: 'debug', // set the desired log level
+ },
+ meetings: {
+ reconnection: {
+ enabled: true,
+ },
+ enableRtx: true,
+ },
+ encryption: {
+ kmsInitialTimeout: 8000,
+ kmsMaxTimeout: 40000,
+ batcherMaxCalls: 30,
+ caroots: null,
+ },
+ dss: {},
+ },
+ credentials: {
+ access_token: tokenElm.value
+ }
+ };
+
+ if (!enableProd) {
+ webexConfig.config.services = {
+ discovery: {
+ u2c: 'https://u2c-intb.ciscospark.com/u2c/api/v1',
+ hydra: 'https://apialpha.ciscospark.com/v1/',
+ },
+ };
+ }
+
+ webex = window.webex = Webex.init(webexConfig);
+
+ webex.once('ready', () => {
+ console.log('Authentication#initWebex() :: Webex Ready');
+ authStatusElm.innerText = 'Webex is ready. Saved access token!';
+ });
+
+ webex.messages.listen()
+ .then(() => {
+ updateStatus(true);
+ })
+ .catch((err) => {
+ console.error(`error listening to messages: ${err}`);
+ });
+}
+
+credentialsFormElm.addEventListener('submit', initWebex);
+
+
+async function decryptFile() {
+ const fileUrl = encryptedFileUrlInput.value;
+ const file = await webex.cypher.downloadAndDecryptFile(
+ fileUrl
+ );
+
+ window.open(URL.createObjectURL(file));
+ decryptFileResult.innerText = 'success';
+}
diff --git a/docs/samples/plugin-encryption/index.html b/docs/samples/plugin-encryption/index.html
new file mode 100644
index 00000000000..e1e1e916ba6
--- /dev/null
+++ b/docs/samples/plugin-encryption/index.html
@@ -0,0 +1,70 @@
+
+
+
+
+
+ Presence Kitchen Sink
+
+
+
+
+
+
+
+
+
+
+ Webex - encryption
+ Use this kitchen sink to interact with the Webex encryption service
+
+
+
+
+
+
+
+
+ Encryption Actions
+
+
+
+
+
+
+
+
diff --git a/docs/samples/plugin-encryption/style.css b/docs/samples/plugin-encryption/style.css
new file mode 100644
index 00000000000..e8bca7f3eff
--- /dev/null
+++ b/docs/samples/plugin-encryption/style.css
@@ -0,0 +1,358 @@
+body {
+ margin: 0;
+ padding: 0;
+ background-color: #eee;
+ font-family: sans-serif;
+ font-size: 16px;
+}
+
+/* Nice defaults */
+h2 {
+ font-weight: 500;
+ font-size: 2.2rem;
+ margin: 0 0 1rem 0;
+}
+
+section {
+ background-color: #fff;
+ padding: 1rem;
+ margin-bottom: 1.5rem;
+ border-radius: 8px;
+}
+
+select {
+ min-width: 300px;
+ margin-right: 1rem;
+}
+
+label {
+ font-size: 1rem;
+ margin-left: 0.2rem;
+ white-space: nowrap;
+}
+label:first-child {
+ margin-left: 0;
+}
+
+pre {
+ padding: 0.5rem;
+ background-color: #eee;
+ white-space: pre-wrap;
+}
+
+code {
+ display: inline-block;
+ padding: 0.0625rem 0.25rem;
+ font-family: Consolas, Liberation Mono, Courier, monospace;
+ line-height: 1rem;
+ color: crimson;
+ font-size: 0.8rem;
+ background-color: #ededed;
+ border: 0.0625rem solid rgba(0, 0, 0, 0.12);
+ border-radius: 0.25rem;
+ box-sizing: border-box;
+}
+
+fieldset {
+ margin: 0 0.5rem;
+ margin-bottom: 2rem;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+}
+fieldset p {
+ padding: 1rem 0;
+ margin: 0;
+}
+fieldset legend {
+ font-weight: bold;
+ font-size: 1.1rem;
+}
+
+button, input[type="button"] {
+ position: relative;
+ display: inline-block;
+ min-width: 4.5rem;
+ font-family: CiscoSansTT Regular, Helvetica Neue, Helvetica, sans-serif;
+ font-weight: 400;
+ text-align: center;
+ text-decoration: none;
+ cursor: pointer;
+ border: none;
+ font-size: 0.9rem;
+ line-height: 1.5rem;
+ border-radius: 1.125rem;
+ height: 2.25rem;
+ padding: 0.375rem 1.125rem;
+ color: var(--md-button-secondary-text-color, #000);
+ background-color: var(--md-button-secondary-bg-color, #dedede);
+ border-color: transparent;
+ transition: background-color 0.15s ease;
+}
+
+button:hover, input[type="button"]:hover {
+ color: var(--md-button-secondary-text-color, #000);
+ background-color: var(--md-button-secondary-hover-bg-color, #ccc);
+}
+
+button:active, input[type="button"]:active {
+ background-color: var(--md-button-bg-color, rgba(0, 0, 0, 0.3));
+}
+
+button:disabled, input[type="button"]:disabled {
+ color: var(--md-button-disabled-text-color, rgba(0, 0, 0, 0.2));
+ fill: var(--md-button-disabled-text-color, rgba(0, 0, 0, 0.2));
+ background-color: var(--md-button-disabled-bg-color, rgba(0, 0, 0, 0.04));
+ pointer-events: none;
+ cursor: default;
+}
+
+button.btn--green, input[type="button"].btn--green {
+ color: #fff;
+ background-color: #00ab50;
+ border-color: transparent;
+}
+
+button.btn--green:disabled, input[type="button"].btn--green:disabled{
+ opacity: .5;
+}
+
+button.btn--red, input[type="button"].btn--red {
+ color: #fff;
+ color: var(--md-button-cancel-text-color, #fff);
+ background-color: #f7644a;
+ background-color: var(--md-button-cancel-bg-color, #f7644a);
+ border-color: transparent;
+}
+
+.btn--red:disabled,
+.btn--green:disabled {
+ opacity: .5;
+}
+
+button.btn-code, input[type="button"].btn-code {
+ font-family: Consolas, Liberation Mono, Courier, monospace;
+ margin: 0.2rem 0;
+}
+
+input[type=text],
+input[type=password],
+input[type=email] {
+ width: 100%;
+ margin-bottom: 1rem;
+ box-sizing: border-box;
+ border: 0.0625rem solid;
+ border-color: var(--md-input-outline-color, #ccc);
+ background-color: var(--md-input-background-color, #fff);
+ border-radius: 4px;
+ font-family: CiscoSansTT Regular, Helvetica Neue, Helvetica, sans-serif;
+ font-size: 1rem;
+ height: 2.25rem;
+ width: 100%;
+ padding: 0 1rem;
+ transition: box-shadow 0.15s ease-out;
+}
+
+input[type=text]:focus,
+input[type=password]:focus,
+input[type=email]:focus {
+ border-color: transparent;
+ outline: none;
+ box-shadow: 0 0 4px 2px rgba(0, 160, 209, 0.75);
+ transition: box-shadow 0.15s ease-in;
+}
+
+.btn-group {
+ display: flex;
+ margin: 0.5rem 0;
+ margin-bottom: 0;
+}
+.btn-group button {
+ flex: 1;
+ margin-right: 1rem;
+}
+.btn-group button:last-of-type {
+ margin-right: 0;
+}
+
+/* Utilities */
+.flex {
+ display: flex;
+}
+
+.flex--wrap {
+ flex-wrap: wrap;
+}
+
+.flex--center {
+ justify-content: center;
+}
+
+/* Margin utils */
+.u-m {
+ margin: 1rem !important;
+}
+
+.u-mv {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+}
+
+.u-mb {
+ margin-bottom: 1rem !important;
+}
+
+.u-mt {
+ margin-top: 1rem !important;
+}
+
+/* Padding Utils */
+.u-p {
+ padding: 1rem !important;
+}
+
+.u-pv {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+}
+
+.u-pb {
+ padding-bottom: 1rem !important;
+}
+
+.u-pt {
+ padding-top: 1rem !important;
+}
+
+/* Misc Styles */
+.note {
+ margin: 0.8rem 0;
+ background: rgba(232, 232, 232, 0.4);
+ padding: 1rem;
+ border-radius: 6px;
+ color: #555;
+}
+
+.transcription {
+ display: flex;
+ flex-direction: column;
+}
+
+.transcription textarea {
+ width: 100%;
+ min-height: 10rem;
+}
+
+.device-type-label {
+ width: 125px;
+ display: inline-block;
+}
+
+.context-info {
+ width: 200px;
+ display: inline-block;
+}
+
+.meeting-list {
+ list-style-type: none;
+}
+
+/* Container - Docs / Streams Fixed */
+.container {
+ overflow: auto;
+ height: 100vh;
+}
+
+.docs {
+ max-width: 87.5rem;
+ margin-top: 1.5rem;
+}
+
+.hidden {
+ display: none;
+}
+
+th , td {
+ border:1px solid black;
+ padding: 5px;
+ font-size: small;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+.styled-table {
+ border-collapse: collapse;
+ margin: 1.5625 0;
+ font-size: 0.9em;
+ font-family: sans-serif;
+ min-width: 25rem;
+ box-shadow: 0 0 1.25rem rgba(0, 0, 0, 0.15);
+}
+
+.styled-table thead tr {
+ background-color: #009879;
+ color: #ffffff;
+ text-align: left;
+}
+
+.styled-table th,
+.styled-table td {
+ padding: 1.25rem 1.25rem;
+}
+
+.styled-table tbody tr {
+ border-bottom: 0.063rem solid #dddddd;
+}
+
+.styled-table tbody tr:nth-of-type(even) {
+ background-color: #f3f3f3;
+}
+
+.styled-table tbody tr:last-of-type {
+ border-bottom: 0.125rem solid #009879;
+}
+
+h2 {
+ color: #0052bf;
+ font-size: 1.5rem;
+ width: 100%;
+}
+
+legend {
+ font-size: 1.15rem;
+ color: #1a73e8;
+}
+
+.collapsible {
+ cursor: pointer;
+}
+
+.section-content {
+ height: auto;
+ visibility: visible;
+}
+
+.collapsed {
+ height: 0;
+ visibility: hidden;
+}
+
+.text-color {
+ color: #555;
+}
+
+input[type="string"],
+select {
+ padding: 0 0.5rem;
+ height: 1.75rem;
+ border-radius: 4px;
+ box-sizing: border-box;
+ border: 0.0625rem solid;
+ border-color: var(--md-input-outline-color, #ccc);
+ background-color: var(--md-input-background-color, #fff);
+ font-family: CiscoSansTT Regular, Helvetica Neue, Helvetica, sans-serif;
+ font-size: 1rem;
+ transition: box-shadow 0.15s ease-out;
+ color: #555;
+ width: 10rem;
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 775d5e4f22a..44d2304180e 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
},
"scripts": {
"@all": "yarn @workspaces run",
- "@legacy": "yarn @workspaces --from \"{@webex/common,@webex/common-evented,@webex/common-timers,@webex/helper-html,@webex/helper-image,@webex/http-core,@webex/internal-plugin-avatar,@webex/internal-plugin-board,@webex/internal-plugin-calendar,@webex/internal-plugin-conversation,@webex/internal-plugin-device,@webex/internal-plugin-dss,@webex/internal-plugin-ediscovery,@webex/internal-plugin-encryption,@webex/internal-plugin-feature,@webex/internal-plugin-flag,@webex/internal-plugin-llm,@webex/internal-plugin-locus,@webex/internal-plugin-lyra,@webex/internal-plugin-mercury,@webex/internal-plugin-metrics,@webex/internal-plugin-presence,@webex/internal-plugin-scheduler,@webex/internal-plugin-search,@webex/internal-plugin-support,@webex/internal-plugin-team,@webex/internal-plugin-user,@webex/internal-plugin-voicea,@webex/internal-plugin-wdm,@webex/jsdoctrinetest,@webex/media-helpers,@webex/plugin-attachment-actions,@webex/plugin-authorization,@webex/plugin-authorization-browser,@webex/plugin-authorization-browser-first-party,@webex/plugin-authorization-node,@webex/plugin-device-manager,@webex/plugin-logger,@webex/plugin-meetings,@webex/plugin-memberships,@webex/plugin-messages,@webex/plugin-people,@webex/plugin-presence,@webex/plugin-rooms,@webex/plugin-team-memberships,@webex/plugin-teams,@webex/plugin-webhooks,@webex/recipe-private-web-client,@webex/storage-adapter-local-forage,@webex/storage-adapter-local-storage,@webex/storage-adapter-session-storage,@webex/storage-adapter-spec,@webex/test-helper-appid,@webex/test-helper-automation,@webex/test-helper-chai,@webex/test-helper-file,@webex/test-helper-make-local-url,@webex/test-helper-mocha,@webex/test-helper-mock-web-socket,@webex/test-helper-mock-webex,@webex/test-helper-refresh-callback,@webex/test-helper-retry,@webex/test-helper-server,@webex/test-helper-test-users,@webex/test-users,@webex/webex-core,@webex/webex-server,@webex/webrtc,@webex/xunit-with-logs,webex}\" run",
+ "@legacy": "yarn @workspaces --from \"{@webex/common,@webex/common-evented,@webex/common-timers,@webex/helper-html,@webex/helper-image,@webex/http-core,@webex/internal-plugin-avatar,@webex/internal-plugin-board,@webex/internal-plugin-calendar,@webex/internal-plugin-conversation,@webex/internal-plugin-device,@webex/internal-plugin-dss,@webex/internal-plugin-ediscovery,@webex/internal-plugin-encryption,@webex/internal-plugin-feature,@webex/internal-plugin-flag,@webex/internal-plugin-llm,@webex/internal-plugin-locus,@webex/internal-plugin-lyra,@webex/internal-plugin-mercury,@webex/internal-plugin-metrics,@webex/internal-plugin-presence,@webex/internal-plugin-scheduler,@webex/internal-plugin-search,@webex/internal-plugin-support,@webex/internal-plugin-team,@webex/internal-plugin-user,@webex/internal-plugin-voicea,@webex/internal-plugin-wdm,@webex/jsdoctrinetest,@webex/media-helpers,@webex/plugin-attachment-actions,@webex/plugin-authorization,@webex/plugin-authorization-browser,@webex/plugin-authorization-browser-first-party,@webex/plugin-authorization-node,@webex/plugin-device-manager,@webex/plugin-logger,@webex/plugin-meetings,@webex/plugin-encryption,@webex/plugin-memberships,@webex/plugin-messages,@webex/plugin-people,@webex/plugin-presence,@webex/plugin-rooms,@webex/plugin-team-memberships,@webex/plugin-teams,@webex/plugin-webhooks,@webex/recipe-private-web-client,@webex/storage-adapter-local-forage,@webex/storage-adapter-local-storage,@webex/storage-adapter-session-storage,@webex/storage-adapter-spec,@webex/test-helper-appid,@webex/test-helper-automation,@webex/test-helper-chai,@webex/test-helper-file,@webex/test-helper-make-local-url,@webex/test-helper-mocha,@webex/test-helper-mock-web-socket,@webex/test-helper-mock-webex,@webex/test-helper-refresh-callback,@webex/test-helper-retry,@webex/test-helper-server,@webex/test-helper-test-users,@webex/test-users,@webex/webex-core,@webex/webex-server,@webex/webrtc,@webex/xunit-with-logs,webex}\" run",
"@legacy-tools": "yarn @workspaces --topological-dev --from \"{@webex/babel-config-legacy,@webex/env-config-legacy,@webex/eslint-config-legacy,@webex/jest-config-legacy,@webex/legacy-tools}\" run",
"@tools": "yarn @workspaces --topological-dev --from \"{@webex/cli-tools,@webex/package-tools}\" run",
"@workspaces": "yarn workspaces foreach --parallel --verbose",
@@ -37,9 +37,9 @@
"build:tools": "yarn workspaces foreach --from '@webex/*-tools' --topological-dev --parallel --verbose run build:src",
"build:local": "yarn ws:tools && yarn workspace @webex/webex-core run build:src && yarn workspaces foreach --parallel --verbose run build:src && yarn run samples:build",
"build": "yarn workspace @webex/media-helpers run build:src && yarn workspace @webex/calling run build:src && node ./tooling/index.js build && yarn && yarn ws:tools build && yarn run build:tsc",
- "build:tsc": "yarn run tsc -p ./config/tsconfig.typecheck.json --noEmit && yarn workspace @webex/internal-plugin-metrics run build && yarn workspace @webex/plugin-meetings run build",
+ "build:tsc": "yarn run tsc -p ./config/tsconfig.typecheck.json --noEmit && yarn workspace @webex/internal-plugin-metrics run build && yarn workspace @webex/plugin-meetings run build && yarn workspace @webex/plugin-encryption run build",
"build:script": "node ./tooling/index.js build --umd",
- "build:skip-samples": "node ./tooling/index.js build --skip-samples && yarn workspace @webex/internal-plugin-metrics run build && yarn workspace @webex/plugin-meetings run build",
+ "build:skip-samples": "node ./tooling/index.js build --skip-samples && yarn workspace @webex/internal-plugin-metrics run build && yarn workspace @webex/plugin-meetings run build && yarn workspace @webex/plugin-encryption run build",
"build:dev": "NODE_ENV=development node ./tooling/index.js build",
"prebuild:modules": "yarn && yarn @tools build:src && yarn @legacy-tools build:src && yarn workspace @webex/webex-core build:src && yarn @all build:src",
"prebuild:docs": "rimraf ./docs/api",
diff --git a/packages/@webex/internal-plugin-encryption/src/encryption.js b/packages/@webex/internal-plugin-encryption/src/encryption.js
index da080b8b6d9..d2afed01979 100644
--- a/packages/@webex/internal-plugin-encryption/src/encryption.js
+++ b/packages/@webex/internal-plugin-encryption/src/encryption.js
@@ -188,7 +188,9 @@ const Encryption = WebexPlugin.extend({
return Promise.reject(new Error('Cannot encrypt `scr` without first setting `loc`'));
}
- return this.getKey(key, options).then((k) => scr.toJWE(k.jwk));
+ return this.getKey(key, options).then((k) =>
+ SCR.fromJSON(scr).then((encScr) => encScr.toJWE(k.jwk))
+ );
},
/**
diff --git a/packages/@webex/plugin-encryption/.eslintrc.js b/packages/@webex/plugin-encryption/.eslintrc.js
new file mode 100644
index 00000000000..a4fc83c6f44
--- /dev/null
+++ b/packages/@webex/plugin-encryption/.eslintrc.js
@@ -0,0 +1,6 @@
+const config = {
+ root: true,
+ extends: ['@webex/eslint-config-legacy'],
+};
+
+module.exports = config;
diff --git a/packages/@webex/plugin-encryption/LICENSE b/packages/@webex/plugin-encryption/LICENSE
new file mode 100644
index 00000000000..c93cef35444
--- /dev/null
+++ b/packages/@webex/plugin-encryption/LICENSE
@@ -0,0 +1,2 @@
+All contents are licensed under the Cisco EULA
+(https://www.cisco.com/c/en/us/products/end-user-license-agreement.html)
\ No newline at end of file
diff --git a/packages/@webex/plugin-encryption/README.md b/packages/@webex/plugin-encryption/README.md
new file mode 100644
index 00000000000..01a3aefbf3b
--- /dev/null
+++ b/packages/@webex/plugin-encryption/README.md
@@ -0,0 +1,71 @@
+# @webex/plugin-encryption
+
+[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
+
+> Encryption plugin for the Cisco Webex JS SDK.
+
+- [Install](#install)
+- [Usage](#usage)
+- [Development](#development)
+- [Sample Code](#sample-code)
+- [Contribute](#contribute)
+- [Maintainers](#maintainers)
+- [License](#license)
+
+# WARNING: This plugin is currently under active development
+
+## Install
+
+```bash
+npm install --save @webex/plugin-encryption
+```
+
+## Usage
+
+This is a plugin for the Cisco Webex JS SDK . Please see our [developer portal](https://developer.webex.com/) and the [API docs](https://webex.github.io/webex-js-sdk/api/) for full details.
+
+## API Docs and Sample App
+
+API Docs:
+Hosted Sample App:
+See for the sample app code vs the readme
+
+## Sample Code
+
+```typescript
+import { decryptAttachment } from '@webex/plugin-encryption';
+import { Webex } from '@webex/core';
+
+const webex = new Webex({
+ credentials: {
+ access_token
+ }
+});
+
+try {
+ const decryptedFile = await webex.encryption.decryptAttachment(attachmentURL);
+ // Do something with the decrypted file
+} catch (error) {
+ // Handle error
+}
+```
+
+#### Development
+
+To use `webpack-dev-server` to load this package, run `yarn run samples:serve`.
+
+Files placed in the `docs/samples/plugin-encryption` folder will be served statically.
+
+Files in the `src` folder will be compiled, bundled, and served as a static asset at `bundle.js` inside that directory.
+
+## Maintainers
+
+This package is maintained by [Cisco Webex for Developers](https://developer.webex.com/).
+
+## Contribute
+
+Pull requests welcome. Please see [CONTRIBUTING.md](https://github.com/webex/webex-js-sdk/blob/master/CONTRIBUTING.md) for more details.
+
+## License
+
+© 2016-2025 Cisco and/or its affiliates. All Rights Reserved.
diff --git a/packages/@webex/plugin-encryption/babel.config.js b/packages/@webex/plugin-encryption/babel.config.js
new file mode 100644
index 00000000000..71a8b034b1f
--- /dev/null
+++ b/packages/@webex/plugin-encryption/babel.config.js
@@ -0,0 +1,3 @@
+const babelConfigLegacy = require('@webex/babel-config-legacy');
+
+module.exports = babelConfigLegacy;
diff --git a/packages/@webex/plugin-encryption/browsers.js b/packages/@webex/plugin-encryption/browsers.js
new file mode 100644
index 00000000000..0dc08bef04b
--- /dev/null
+++ b/packages/@webex/plugin-encryption/browsers.js
@@ -0,0 +1,108 @@
+/*!
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
+ */
+
+// eslint-disable-next-line strict
+
+/* eslint camelcase: [0] */
+module.exports = function createBrowsers() {
+ if (process.env.SC_TUNNEL_IDENTIFIER || process.env.SAUCE) {
+ return {
+ sl_chrome_latest_macOS_Catalina: {
+ base: 'SauceLabs',
+ platform: 'macOS 10.15',
+ browserName: 'Chrome',
+ version: 'latest',
+ extendedDebugging: true,
+ 'goog:chromeOptions': {
+ args: [
+ '--disable-features=WebRtcHideLocalIpsWithMdns',
+ '--use-fake-device-for-media-stream',
+ '--use-fake-ui-for-media-stream',
+ ],
+ },
+ flags: [
+ '--disable-features=WebRtcHideLocalIpsWithMdns',
+ '--use-fake-device-for-media-stream',
+ '--use-fake-ui-for-media-stream',
+ ],
+ },
+ sl_edge_latest_Win_10: {
+ base: 'SauceLabs',
+ platform: 'Windows 10',
+ browserName: 'MicrosoftEdge',
+ version: 'latest',
+ extendedDebugging: true,
+ 'ms:edgeOptions': {
+ args: [
+ '--disable-features=WebRtcHideLocalIpsWithMdns',
+ '--use-fake-device-for-media-stream',
+ '--use-fake-ui-for-media-stream',
+ ],
+ },
+ },
+ // sl_firefox_latest_linux: {
+ // base: 'SauceLabs',
+ // platform: 'Linux',
+ // browserName: 'firefox',
+ // version: 'latest'
+ // }
+ sl_safari_latest_macOS_Catalina: {
+ base: 'SauceLabs',
+ platform: 'macOS 10.15',
+ browserName: 'Safari',
+ version: 'latest',
+ 'webkit:WebRTC': {
+ DisableInsecureMediaCapture: true,
+ },
+ },
+ sl_firefox_macOS_Catalina: {
+ base: 'SauceLabs',
+ platform: 'macOS 10.15',
+ browserName: 'Firefox',
+ extendedDebugging: true,
+ 'moz:firefoxOptions': {
+ args: ['-start-debugger-server', '9222'],
+ prefs: {
+ 'devtools.chrome.enabled': true,
+ 'devtools.debugger.prompt-connection': false,
+ 'devtools.debugger.remote-enabled': true,
+ 'dom.webnotifications.enabled': false,
+ 'media.webrtc.hw.h264.enabled': true,
+ 'media.getusermedia.screensharing.enabled': true,
+ 'media.navigator.permission.disabled': true,
+ 'media.navigator.streams.fake': true,
+ 'media.peerconnection.video.h264_enabled': true,
+ },
+ },
+ },
+ // sl_firefox_latest_win7: {
+ // base: `SauceLabs`,
+ // platform: `Windows 7`,
+ // browserName: `firefox`,
+ // }
+ };
+ }
+
+ return {
+ firefox_h264: {
+ base: 'FirefoxHeadless',
+ prefs: {
+ 'dom.webnotifications.enabled': false,
+ 'media.webrtc.hw.h264.enabled': true,
+ 'media.getusermedia.screensharing.enabled': true,
+ 'media.navigator.permission.disabled': true,
+ 'media.navigator.streams.fake': true,
+ 'media.peerconnection.video.h264_enabled': true,
+ },
+ },
+ ChromeH264: {
+ base: 'ChromeHeadless',
+ flags: [
+ '--disable-features=WebRtcHideLocalIpsWithMdns',
+ '--use-fake-device-for-media-stream',
+ '--use-fake-ui-for-media-stream',
+ ],
+ },
+ };
+};
diff --git a/packages/@webex/plugin-encryption/jest.config.js b/packages/@webex/plugin-encryption/jest.config.js
new file mode 100644
index 00000000000..0e9d38b401c
--- /dev/null
+++ b/packages/@webex/plugin-encryption/jest.config.js
@@ -0,0 +1,3 @@
+const config = require('@webex/jest-config-legacy');
+
+module.exports = config;
diff --git a/packages/@webex/plugin-encryption/package.json b/packages/@webex/plugin-encryption/package.json
new file mode 100644
index 00000000000..caf6bbe91c6
--- /dev/null
+++ b/packages/@webex/plugin-encryption/package.json
@@ -0,0 +1,75 @@
+{
+ "name": "@webex/plugin-encryption",
+ "description": "",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/webex/webex-js-sdk.git",
+ "directory": "packages/@webex/plugin-encryption"
+ },
+ "license": "Cisco EULA (https://www.cisco.com/c/en/us/products/end-user-license-agreement.html)",
+ "contributors": [
+ "Bharath Balan "
+ ],
+ "main": "dist/index.js",
+ "types": "dist/types/index.d.ts",
+ "scripts": {
+ "build": "yarn run -T tsc --declaration true --declarationDir ./dist/types",
+ "build:src": "webex-legacy-tools build -dest \"./dist\" -src \"./src\" -js -ts -maps && yarn build",
+ "deploy:npm": "yarn npm publish",
+ "test:broken": "yarn test:style && yarn test:unit && yarn test:integration && yarn test:browser",
+ "test:browser": "webex-legacy-tools test --integration --runner karma",
+ "test:style": "eslint ./src/**/*.*",
+ "test:unit": "webex-legacy-tools test --unit --runner mocha"
+ },
+ "browserify": {
+ "transform": [
+ "babelify",
+ "envify"
+ ]
+ },
+ "dependencies": {
+ "@webex/internal-plugin-encryption": "workspace:*",
+ "@webex/webex-core": "workspace:*",
+ "@webex/common": "workspace:*",
+ "node-scr": "^0.3.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.17.10",
+ "@types/jsdom": "^21",
+ "@webex/babel-config-legacy": "workspace:*",
+ "@webex/eslint-config-legacy": "workspace:*",
+ "@webex/jest-config-legacy": "workspace:*",
+ "@webex/legacy-tools": "workspace:*",
+ "chai": "^4.3.4",
+ "chai-as-promised": "^7.1.1",
+ "eslint": "^8.24.0",
+ "jsdom": "19.0.0",
+ "jsdom-global": "3.0.2",
+ "prettier": "^2.7.1",
+ "sinon": "^9.2.4",
+ "typed-emitter": "^2.1.0",
+ "typescript": "^4.7.4"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "//": {
+ "dependencies": {
+ "@webex/common": "workspace:*"
+ },
+ "devDependencies": {
+ "@webex/babel-config-legacy": "workspace:*",
+ "@webex/eslint-config-legacy": "workspace:*",
+ "@webex/jest-config-legacy": "workspace:*",
+ "@webex/legacy-tools": "workspace:*",
+ "@webex/plugin-encryption": "workspace:*",
+ "@webex/core": "workspace:*",
+ "@webex/test-helper-chai": "workspace:*",
+ "@webex/test-helper-mocha": "workspace:*",
+ "@webex/test-helper-mock-webex": "workspace:*",
+ "@webex/test-helper-retry": "workspace:*",
+ "@webex/test-helper-test-users": "workspace:*"
+ }
+ },
+ "devMain": "src/index.js"
+}
diff --git a/packages/@webex/plugin-encryption/poc-requirements.md b/packages/@webex/plugin-encryption/poc-requirements.md
new file mode 100644
index 00000000000..20984d1f1fe
--- /dev/null
+++ b/packages/@webex/plugin-encryption/poc-requirements.md
@@ -0,0 +1,29 @@
+# plugin-encryption
+
+## Requirements
+
+- Org admin needs to be able to download and decrypt media files.
+- Needs an input token, take in an attachment URL and return the decrypted file.
+- Decryption happens on the SDK side.
+- Needs to be in TypeScript.
+
+## Sample Code
+
+```typescript
+ import { decryptAttachment } from '@webex/plugin-encryption';
+ import { Webex } from '@webex/core';
+
+ const webex = new Webex({
+ credentials: {
+ access_token
+ }
+ });
+
+ try {
+ const decryptedFile = await webex.encryption.decryptAttachment(attachmentURL);
+ // Do something with the decrypted file
+ } catch (error) {
+ // Handle error
+ }
+
+```
diff --git a/packages/@webex/plugin-encryption/process b/packages/@webex/plugin-encryption/process
new file mode 100644
index 00000000000..94119b99dbf
--- /dev/null
+++ b/packages/@webex/plugin-encryption/process
@@ -0,0 +1 @@
+module.exports = {browser: true};
diff --git a/packages/@webex/plugin-encryption/src/config.ts b/packages/@webex/plugin-encryption/src/config.ts
new file mode 100644
index 00000000000..18421a1da99
--- /dev/null
+++ b/packages/@webex/plugin-encryption/src/config.ts
@@ -0,0 +1,3 @@
+export default {
+ cypher: {},
+};
diff --git a/packages/@webex/plugin-encryption/src/constants.ts b/packages/@webex/plugin-encryption/src/constants.ts
new file mode 100644
index 00000000000..c088bcde464
--- /dev/null
+++ b/packages/@webex/plugin-encryption/src/constants.ts
@@ -0,0 +1,4 @@
+// @ts-ignore
+export type Enum> = T[keyof T];
+
+// *********** LOWERCASE / CAMELCASE STRINGS ************
diff --git a/packages/@webex/plugin-encryption/src/encryption/constants.ts b/packages/@webex/plugin-encryption/src/encryption/constants.ts
new file mode 100644
index 00000000000..7e360e7b31e
--- /dev/null
+++ b/packages/@webex/plugin-encryption/src/encryption/constants.ts
@@ -0,0 +1,5 @@
+export const ENCRYPTION = 'cypher';
+export const DECRYPTION = 'decryption';
+export const DECRYPTION_FAILED = 'decryptionFailed';
+export const DECRYPTION_IN_PROGRESS = 'decryptionInProgress';
+export const DECRYPTION_SUCCESS = 'decryptionSuccess';
diff --git a/packages/@webex/plugin-encryption/src/encryption/encryption.types.ts b/packages/@webex/plugin-encryption/src/encryption/encryption.types.ts
new file mode 100644
index 00000000000..dbdf879483f
--- /dev/null
+++ b/packages/@webex/plugin-encryption/src/encryption/encryption.types.ts
@@ -0,0 +1,5 @@
+interface IEncryption {
+ downloadAndDecryptFile(fileUri: string): Promise;
+}
+
+export type {IEncryption};
diff --git a/packages/@webex/plugin-encryption/src/encryption/index.ts b/packages/@webex/plugin-encryption/src/encryption/index.ts
new file mode 100644
index 00000000000..616588af273
--- /dev/null
+++ b/packages/@webex/plugin-encryption/src/encryption/index.ts
@@ -0,0 +1,83 @@
+/* eslint-disable require-jsdoc */
+/* eslint-disable no-unused-expressions */
+import {WebexPlugin} from '@webex/webex-core';
+import {proxyEvents} from '@webex/common';
+import {EventEmitter} from 'events';
+
+import {IEncryption} from './encryption.types';
+import {ENCRYPTION} from './constants';
+
+/**
+ * @description Encryption APIs for KMS
+ * @class
+ */
+class Cypher extends WebexPlugin implements IEncryption {
+ namespace = ENCRYPTION;
+
+ constructor(...args: any[]) {
+ super(...args);
+
+ 1 + 2;
+ }
+
+ // eslint-disable-next-line require-jsdoc
+ public async testRun(): Promise {
+ const file = await this.downloadAndDecryptFile(
+ 'https://files-api-a.wbx2.com/v1/spaces/32ee69e8-f614-4b1b-a9f9-e472ab338e15/contents/ff44c732-91da-4d20-8979-aee71a0194e4/versions/2b59d407e4a64e6c8d71ec5eb4886832/bytes?keyUri=kms%3A%2F%2Fcisco.com%2Fkeys%2F3a0be121-c8e6-4ddb-b8a5-912c05b22c40&JWE=eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIn0..JnhlKeq14qXPLsKi.qDnQZFIyB7UCh1VxF53h42pWfQ1jnP6ZcwixnM9zFW9z4xMaKr3a-pLH_OEZNUOHM7A-qsoJHVvnACWNDNXfwPnKAMDERaS-9GsA3_mGqiT-CqgAWXrnkMSe1I3NHd3Q80RZ2zQ6TwxG9XrKUMgDvOWWp3oAc2UPJUDPJY04mAmK4LLXGrSJLENSuyf6IoofrBCtQMJGIw_vkwoA_9_uq0gK9JqSh-tjO-N7RBi51DyGmJko0Yr7Hb_QQgm2tv0eBVvQal0pIRJEPWmFTxu61XBvd2giOuRxDshG_Kmdf8MbngIdiLUcOfhosaNSFcVRzZYs_ZH4W0973QJCFw4-QFMOlL2rRztBQebgTTuxFPC6JouXth150y88BBBq-gbDzM__BAYITIsaR4v0YkPoAckF-9qvM4S2fqJOCKslCzf7u5yxEs9p8eVP1roVofM.dp2VT2zAPY1OUD-r-pOpGQ'
+ );
+ window.open(URL.createObjectURL(file));
+
+ return 'success';
+ }
+
+ /**
+ * Downloads and decrypts a file from the given URI.
+ *
+ * @param {string} fileUri - The URI of the file to be decrypted.
+ * @returns {Promise} A promise that resolves to the decrypted file.
+ * @throws {Error} If the file download or decryption fails.
+ *
+ * @example
+ * ```typescript
+ * const decryptedFile = await downloadFile('https://example.com/encrypted-file');
+ * console.log(decryptedFile);
+ * ```
+ */
+ public async downloadAndDecryptFile(fileUri: string): Promise {
+ // Sample fileUri: https://beta.webex.com/files/api/v1/spaces/4783eac2-e139-4f24-a2d9-0302ee1a8a98/contents/419213bf-ed87-4655-950a-f04afbfea3df/versions/3a75cedf39434bde9d650bbe0ad24221/bytes?keyUri=kms://kms-usint.wbx2.com/keys/141686e6-9ftd-4tcf-8eb8-80718b95847557f96b&JWE=eyAiYWxnIjogImRpciIsICJlbmMiOifyndafRDhTI1NkdDTSIgfQ
+ // KeyUri: An attachment level unique ID generated by Webex KMS post encryption of an attachment. In order to
+ // decrypt an attachment, KeyUri is a required parameter.
+ // JWE: JWE can be decrypted using KMS key to obtain the SCR key.
+ // Decrypting an attachment requires the SCR key parameter.
+ // We need to download the encrypted file from the given URI and then decrypt it using the SCR key.
+ // The decrypted file is then returned.
+
+ // step 1: parse the fileUri to get the keyUri and JWE
+ // step 2: use the keyUri to decrypt the JWE to get the SCR
+ // step 3: download the file from the fileUri and decrypt it using the SCR
+
+ // Parse the fileUri & JWE to get the keyUri using urlSearchParams
+ const url = new URL(fileUri);
+ const keyUri = url.searchParams.get('keyUri');
+ const JWE = url.searchParams.get('JWE');
+
+ // Decrypt the JWE to get the SCR
+ const scr = await this.webex.internal.encryption.decryptScr(keyUri, JWE);
+
+ // Download and decrypt the file using the SCR
+ const shunt = new EventEmitter();
+ let promise = this.webex.internal.encryption.download(fileUri, scr); // TODO: Add a param to bypass hitting webex-files and directly download the file
+
+ promise = promise
+ .on('progress', (...args) => shunt.emit('progress', ...args))
+ .then((decryptedBuf) => {
+ return new File([decryptedBuf], 'README.md', {type: 'text/markdown'});
+ });
+
+ proxyEvents(shunt, promise);
+
+ return promise;
+ }
+}
+
+export default Cypher;
diff --git a/packages/@webex/plugin-encryption/src/index.ts b/packages/@webex/plugin-encryption/src/index.ts
new file mode 100644
index 00000000000..b1cd6c3fe54
--- /dev/null
+++ b/packages/@webex/plugin-encryption/src/index.ts
@@ -0,0 +1,12 @@
+/* eslint-env browser */
+import {registerPlugin} from '@webex/webex-core';
+
+import Cypher from './encryption';
+import config from './config';
+
+registerPlugin('cypher', Cypher, {
+ config,
+ interceptors: {},
+});
+
+export default Cypher;
diff --git a/packages/@webex/plugin-encryption/tsconfig.json b/packages/@webex/plugin-encryption/tsconfig.json
new file mode 100644
index 00000000000..5936b541529
--- /dev/null
+++ b/packages/@webex/plugin-encryption/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../../../tsconfig.json",
+ "include": [
+ "src"
+ ],
+}
\ No newline at end of file
diff --git a/packages/webex/package.json b/packages/webex/package.json
index 4bd3307cc34..04d044940a7 100644
--- a/packages/webex/package.json
+++ b/packages/webex/package.json
@@ -17,6 +17,7 @@
"exports": {
"./calling": "./dist/calling.js",
".": "./dist/index.js",
+ "./encryption": "./dist/encryption.js",
"./meetings": "./dist/meetings.js",
"./package": "./package.json"
},
@@ -78,6 +79,7 @@
"@webex/plugin-attachment-actions": "workspace:*",
"@webex/plugin-authorization": "workspace:*",
"@webex/plugin-device-manager": "workspace:*",
+ "@webex/plugin-encryption": "workspace:*",
"@webex/plugin-logger": "workspace:*",
"@webex/plugin-meetings": "workspace:*",
"@webex/plugin-memberships": "workspace:*",
@@ -89,6 +91,7 @@
"@webex/plugin-webhooks": "workspace:*",
"@webex/storage-adapter-local-storage": "workspace:*",
"@webex/webex-core": "workspace:*",
- "lodash": "^4.17.21"
+ "lodash": "^4.17.21",
+ "node-scr": "^0.3.0"
}
}
diff --git a/packages/webex/src/encryption.js b/packages/webex/src/encryption.js
new file mode 100644
index 00000000000..67f634515d8
--- /dev/null
+++ b/packages/webex/src/encryption.js
@@ -0,0 +1,45 @@
+/*!
+ * Copyright (c) 2015-2024 Cisco Systems, Inc. See the LICENSE file.
+ */
+
+// Note: this file is written using commonjs instead of import/export to
+// simplify consumption by those less familiar with the current state of
+// JavaScript modularization
+
+/* istanbul ignore else */
+if (!global._babelPolyfill) {
+ /* eslint global-require: [0] */
+ require('@babel/polyfill');
+}
+
+require('@webex/plugin-authorization');
+// explicitly load wdm, since we're relying on preDiscoveryServices and the
+// url interceptor
+require('@webex/common');
+require('@webex/internal-plugin-encryption');
+require('@webex/plugin-encryption');
+require('node-scr');
+
+const merge = require('lodash/merge');
+const WebexCore = require('@webex/webex-core').default;
+
+const config = require('./config');
+
+const Webex = WebexCore.extend({
+ webex: true,
+ version: PACKAGE_VERSION,
+});
+
+Webex.init = function init(attrs = {}) {
+ attrs.config = merge(
+ {
+ sdkType: 'encryption',
+ },
+ config,
+ attrs.config
+ ); // eslint-disable-line no-param-reassign
+
+ return new Webex(attrs);
+};
+
+module.exports = Webex;
diff --git a/packages/webex/src/webex.js b/packages/webex/src/webex.js
index 0581b8bf23f..8c0bc812b4d 100644
--- a/packages/webex/src/webex.js
+++ b/packages/webex/src/webex.js
@@ -27,6 +27,7 @@ require('@webex/plugin-rooms');
require('@webex/plugin-teams');
require('@webex/plugin-team-memberships');
require('@webex/plugin-webhooks');
+require('@webex/plugin-encryption');
const merge = require('lodash/merge');
const WebexCore = require('@webex/webex-core').default;
diff --git a/tooling/commands/test.js b/tooling/commands/test.js
index 19fdb7f8754..26a539d0afb 100644
--- a/tooling/commands/test.js
+++ b/tooling/commands/test.js
@@ -168,7 +168,7 @@ module.exports = {
/** Each package is run through testPackage util */
for (const packageName of argv.packages) {
- const onMocha = packageName === '@webex/plugin-meetings' || packageName === 'webex';
+ const onMocha = packageName === '@webex/plugin-encryption' || packageName === '@webex/plugin-meetings' || packageName === 'webex';
await testPackage(argv, packageName, onMocha);
}
diff --git a/webpack.config.js b/webpack.config.js
index 13c283b7805..b0ea371db62 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -39,6 +39,13 @@ module.exports = (env = {NODE_ENV: process.env.NODE_ENV || 'production'}) => ({
type: 'umd',
},
},
+ encryption: {
+ import: `${path.resolve(__dirname)}/packages/webex/src/encryption.js`,
+ library: {
+ name: 'Webex',
+ type: 'umd',
+ },
+ },
calling: {
import: `${path.resolve(__dirname)}/packages/webex/src/calling.js`,
library: {
diff --git a/yarn.lock b/yarn.lock
index 3c8f7a98dbf..c0bdfa3cc65 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8689,6 +8689,31 @@ __metadata:
languageName: unknown
linkType: soft
+"@webex/plugin-encryption@workspace:*, @webex/plugin-encryption@workspace:packages/@webex/plugin-encryption":
+ version: 0.0.0-use.local
+ resolution: "@webex/plugin-encryption@workspace:packages/@webex/plugin-encryption"
+ dependencies:
+ "@babel/core": ^7.17.10
+ "@types/jsdom": ^21
+ "@webex/babel-config-legacy": "workspace:*"
+ "@webex/eslint-config-legacy": "workspace:*"
+ "@webex/internal-plugin-encryption": "workspace:*"
+ "@webex/jest-config-legacy": "workspace:*"
+ "@webex/legacy-tools": "workspace:*"
+ "@webex/webex-core": "workspace:*"
+ chai: ^4.3.4
+ chai-as-promised: ^7.1.1
+ eslint: ^8.24.0
+ jsdom: 19.0.0
+ jsdom-global: 3.0.2
+ node-scr: ^0.3.0
+ prettier: ^2.7.1
+ sinon: ^9.2.4
+ typed-emitter: ^2.1.0
+ typescript: ^4.7.4
+ languageName: unknown
+ linkType: soft
+
"@webex/plugin-logger@workspace:*, @webex/plugin-logger@workspace:packages/@webex/plugin-logger":
version: 0.0.0-use.local
resolution: "@webex/plugin-logger@workspace:packages/@webex/plugin-logger"
@@ -32875,6 +32900,7 @@ __metadata:
"@webex/plugin-attachment-actions": "workspace:*"
"@webex/plugin-authorization": "workspace:*"
"@webex/plugin-device-manager": "workspace:*"
+ "@webex/plugin-encryption": "workspace:*"
"@webex/plugin-logger": "workspace:*"
"@webex/plugin-meetings": "workspace:*"
"@webex/plugin-memberships": "workspace:*"
@@ -32893,6 +32919,7 @@ __metadata:
"@webex/webex-core": "workspace:*"
eslint: ^8.24.0
lodash: ^4.17.21
+ node-scr: ^0.3.0
sinon: ^9.2.4
webex: "workspace:*"
languageName: unknown