From 3494db7432fd09f6b2df4b58809fc3f1c4040728 Mon Sep 17 00:00:00 2001 From: Bharath Balan <62698609+bhabalan@users.noreply.github.com> Date: Wed, 27 Nov 2024 01:05:28 +0530 Subject: [PATCH] feat(plugin-encryption): make POC for CC file decryption --- docs/samples/plugin-encryption/app.js | 113 ++++++ docs/samples/plugin-encryption/index.html | 70 ++++ docs/samples/plugin-encryption/style.css | 358 ++++++++++++++++++ package.json | 6 +- .../src/encryption.js | 4 +- .../@webex/plugin-encryption/.eslintrc.js | 6 + packages/@webex/plugin-encryption/LICENSE | 2 + packages/@webex/plugin-encryption/README.md | 71 ++++ .../@webex/plugin-encryption/babel.config.js | 3 + packages/@webex/plugin-encryption/browsers.js | 108 ++++++ .../@webex/plugin-encryption/jest.config.js | 3 + .../@webex/plugin-encryption/package.json | 75 ++++ .../plugin-encryption/poc-requirements.md | 29 ++ packages/@webex/plugin-encryption/process | 1 + .../@webex/plugin-encryption/src/config.ts | 3 + .../@webex/plugin-encryption/src/constants.ts | 4 + .../src/encryption/constants.ts | 5 + .../src/encryption/encryption.types.ts | 5 + .../plugin-encryption/src/encryption/index.ts | 83 ++++ .../@webex/plugin-encryption/src/index.ts | 12 + .../@webex/plugin-encryption/tsconfig.json | 6 + packages/webex/package.json | 5 +- packages/webex/src/encryption.js | 45 +++ packages/webex/src/webex.js | 1 + tooling/commands/test.js | 2 +- webpack.config.js | 7 + yarn.lock | 27 ++ 27 files changed, 1048 insertions(+), 6 deletions(-) create mode 100644 docs/samples/plugin-encryption/app.js create mode 100644 docs/samples/plugin-encryption/index.html create mode 100644 docs/samples/plugin-encryption/style.css create mode 100644 packages/@webex/plugin-encryption/.eslintrc.js create mode 100644 packages/@webex/plugin-encryption/LICENSE create mode 100644 packages/@webex/plugin-encryption/README.md create mode 100644 packages/@webex/plugin-encryption/babel.config.js create mode 100644 packages/@webex/plugin-encryption/browsers.js create mode 100644 packages/@webex/plugin-encryption/jest.config.js create mode 100644 packages/@webex/plugin-encryption/package.json create mode 100644 packages/@webex/plugin-encryption/poc-requirements.md create mode 100644 packages/@webex/plugin-encryption/process create mode 100644 packages/@webex/plugin-encryption/src/config.ts create mode 100644 packages/@webex/plugin-encryption/src/constants.ts create mode 100644 packages/@webex/plugin-encryption/src/encryption/constants.ts create mode 100644 packages/@webex/plugin-encryption/src/encryption/encryption.types.ts create mode 100644 packages/@webex/plugin-encryption/src/encryption/index.ts create mode 100644 packages/@webex/plugin-encryption/src/index.ts create mode 100644 packages/@webex/plugin-encryption/tsconfig.json create mode 100644 packages/webex/src/encryption.js 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

+
+ + + +
+

Authentication

+
+

Note: Get an access token from our developer portal: https://developer.webex.com/docs/api/getting-started.

+

Note: Webex JS SDK must be initialized using a valid token.

+ + +
+
+ Credentials +
+

Initializes webex object and registers Webex JS SDK as a device.

+ ( Click to change the environment ) + +
+ + +
Not initialized
+
+
+
+
+ + +
+

Encryption Actions

+
+
+ Decrypt files +
+ File URL: + +
+
+
+ +
+
+
+ + + + + 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 b8425801341..5073f4fb5de 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