@webex/plugin-encryption 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.eslintrc.js +6 -0
  2. package/LICENSE +2 -0
  3. package/README.md +88 -0
  4. package/babel.config.js +13 -0
  5. package/browsers.js +97 -0
  6. package/coverage/clover.xml +176 -0
  7. package/coverage/coverage-final.json +3 -0
  8. package/coverage/jest-html-reporters-attach/jest-report/index.js +58 -0
  9. package/coverage/jest-html-reporters-attach/jest-report/result.js +1 -0
  10. package/coverage/jest-report.html +1 -0
  11. package/coverage/junit/coverage-junit.xml +27 -0
  12. package/coverage/lcov-report/base.css +224 -0
  13. package/coverage/lcov-report/block-navigation.js +87 -0
  14. package/coverage/lcov-report/constants.ts.html +100 -0
  15. package/coverage/lcov-report/favicon.png +0 -0
  16. package/coverage/lcov-report/index.html +131 -0
  17. package/coverage/lcov-report/index.ts.html +562 -0
  18. package/coverage/lcov-report/prettify.css +1 -0
  19. package/coverage/lcov-report/prettify.js +2 -0
  20. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  21. package/coverage/lcov-report/sorter.js +196 -0
  22. package/coverage/lcov.info +215 -0
  23. package/developer-quickstart.md +104 -0
  24. package/dist/config.js +10 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/constants.js +6 -0
  27. package/dist/constants.js.map +1 -0
  28. package/dist/cypher/constants.js +12 -0
  29. package/dist/cypher/constants.js.map +1 -0
  30. package/dist/cypher/cypher.types.js +6 -0
  31. package/dist/cypher/cypher.types.js.map +1 -0
  32. package/dist/cypher/index.js +141 -0
  33. package/dist/cypher/index.js.map +1 -0
  34. package/dist/cypher/types.js +6 -0
  35. package/dist/cypher/types.js.map +1 -0
  36. package/dist/encryption/constants.js +12 -0
  37. package/dist/encryption/constants.js.map +1 -0
  38. package/dist/encryption/encryption.types.js +6 -0
  39. package/dist/encryption/encryption.types.js.map +1 -0
  40. package/dist/encryption/index.js +84 -0
  41. package/dist/encryption/index.js.map +1 -0
  42. package/dist/index.js +17 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/types/config.d.ts +4 -0
  45. package/dist/types/constants.d.ts +1 -0
  46. package/dist/types/cypher/constants.d.ts +5 -0
  47. package/dist/types/cypher/cypher.types.d.ts +25 -0
  48. package/dist/types/cypher/index.d.ts +53 -0
  49. package/dist/types/cypher/types.d.ts +25 -0
  50. package/dist/types/encryption/constants.d.ts +5 -0
  51. package/dist/types/encryption/encryption.types.d.ts +25 -0
  52. package/dist/types/encryption/index.d.ts +27 -0
  53. package/dist/types/index.d.ts +4 -0
  54. package/dist/types/types.d.ts +40 -0
  55. package/dist/types.js +6 -0
  56. package/dist/types.js.map +1 -0
  57. package/jest.config.js +44 -0
  58. package/junit.xml +27 -0
  59. package/package.json +57 -0
  60. package/process +1 -0
  61. package/src/config.ts +3 -0
  62. package/src/cypher/constants.ts +5 -0
  63. package/src/cypher/index.ts +159 -0
  64. package/src/cypher/types.ts +28 -0
  65. package/src/index.ts +13 -0
  66. package/src/types.ts +45 -0
  67. package/test/unit/spec/cypher/index.ts +146 -0
  68. package/tsconfig.json +18 -0
@@ -0,0 +1,159 @@
1
+ import {WebexPlugin} from '@webex/webex-core';
2
+
3
+ import {FileDownloadOptions, IEncryption} from './types';
4
+ import {CYPHER} from './constants';
5
+ import {WebexSDK} from '../types';
6
+
7
+ /**
8
+ * @description Encryption APIs for KMS
9
+ * @class
10
+ */
11
+ class Cypher extends WebexPlugin implements IEncryption {
12
+ readonly namespace = CYPHER;
13
+ private readonly $webex: WebexSDK;
14
+ registered = false;
15
+
16
+ /**
17
+ * Constructs an instance of the class.
18
+ *
19
+ * @param {...any[]} args - The arguments to pass to the superclass constructor.
20
+ *
21
+ * @remarks
22
+ * This constructor calls the superclass constructor with the provided arguments.
23
+ * It also assigns the `webex` property to the `$webex` property, ignoring TypeScript errors.
24
+ */
25
+ constructor(...args: any[]) {
26
+ super(...args);
27
+ this.$webex = (this as WebexPlugin).webex as WebexSDK;
28
+ }
29
+
30
+ /**
31
+ * Registers the device to WDM. This is required for metrics and other services.
32
+ * @returns {Promise<void>}
33
+ */
34
+ async register() {
35
+ if (this.registered) {
36
+ this.$webex.logger.info('Cypher: webex.internal.device.register already done');
37
+
38
+ return Promise.resolve();
39
+ }
40
+
41
+ return this.$webex.internal.device
42
+ .register()
43
+ .then(() => {
44
+ this.$webex.logger.info('Cypher: webex.internal.device.register successful');
45
+ this.registered = true;
46
+ })
47
+ .catch((error) => {
48
+ this.$webex.logger.error(`Error occurred during device.register() ${error}`);
49
+
50
+ throw error;
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Deregisters the device.
56
+ * @returns {Promise<void>}
57
+ */
58
+ async deregister() {
59
+ if (!this.registered) {
60
+ this.$webex.logger.info('Cypher: webex.internal.device.deregister already done');
61
+
62
+ return Promise.resolve();
63
+ }
64
+
65
+ return this.$webex.internal.device
66
+ .unregister()
67
+ .then(() => {
68
+ this.$webex.logger.info('Cypher: webex.internal.device.deregister successful');
69
+ this.registered = false;
70
+ })
71
+ .catch((error) => {
72
+ this.$webex.logger.error(`Error occurred during device.deregister() ${error}`);
73
+
74
+ throw error;
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Downloads and decrypts a file from the given URI.
80
+ *
81
+ * @param {string} fileUri - The URI of the file to be decrypted.
82
+ * @param {FileDownloadOptions} options - The options for file download.
83
+ * @returns {Promise<ArrayBuffer>} A promise that resolves to the decrypted ArrayBuffer.
84
+ * @throws {Error} If the file download or decryption fails.
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const attachmentURL = 'https:/myfileurl.xyz/zzz/fileid?keyUri=somekeyuri&JWE=somejwe';
89
+ * const options: FileDownloadOptions = {
90
+ * useFileService: false,
91
+ * jwe: somejwe, // Provide the JWE here if not already present in the attachmentURL
92
+ * keyUri: someKeyUri, // Provide the keyURI here if not already present in the attachmentURL
93
+ * };
94
+ * const decryptedBuf = await webex.cypher.downloadAndDecryptFile(attachmentURL, options);
95
+ * const file = new File([decryptedBuf], "myFileName.jpeg", {type: 'image/jpeg'});
96
+ * ```
97
+ */
98
+ public async downloadAndDecryptFile(
99
+ fileUri: string,
100
+ {useFileService = false, ...options}: FileDownloadOptions = {}
101
+ ): Promise<ArrayBuffer> {
102
+ // Sample fileUri: https://someserver.com/3cc29537-7f39-4cce-b204-a529960997fcab9375d8-4fd4-4788-bb12-18426674e95b.jpg?keyUri=kms://kms.wbx2.com/keys/79be16-a4dd-4195-93ef-a72604509aca&JWE=eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIn0..EHYI5SmnFisaOJgv.6Ie3Zui7LFtAr4eWMSnxXAzfi8Cgixcy2b9jy9cImDWvBjjvJQfwik7qvZewCaq-u8lhtTbjEzsJLeVtOKhW_9RoZt3U0RQ-cKaSh2RaK3N_mvuH7_BsoXCMf5zxaqP1HD-3jXUtVSnqFYvEGdGRWxTCWK-PK9BoIUjX6v5t22CUNYbBQBuHizLWvrGAM0UkSvFNRX5n07Xd3WVJ7OnIhYi0JvOb50lbZIrBn27AQL-_CIKoOQxQLkW9zmVACsVpHxLZx9wIo9XYsBYADRTZaw_l_uTiosAd2P1QAGHgLr_Q_qf1wGUn3eGhmptpPx-YCSJikvs2DttwWOg_-vg4jI3EiIXlc0gGsDnleeNRguCLtrks0PMz5hOp5_w9Z5EW05Cx2UAztnp1hE9_nCvPP9wTzdHsG3flkK82HMbPpeVStWvWmAlzp24vTw2KzYrCemdS2AkrShWsNt6_G7_G8nB4RhUnZ11MKaduE5jYpCXNcTx84RBYwA.vqYHCx8uIn-IMQAsPvrw
103
+ // KeyUri: An attachment level unique ID generated by Webex KMS post encryption of an attachment. In order to
104
+ // decrypt an attachment, KeyUri is a required parameter.
105
+ // JWE: JWE can be decrypted using KMS key to obtain the SCR key.
106
+ // Decrypting an attachment requires the SCR key parameter.
107
+ // We need to download the encrypted file from the given URI and then decrypt it using the SCR key.
108
+ // The decrypted file is then returned.
109
+
110
+ // step 1: parse the fileUri to get the keyUri and JWE
111
+ // step 2: if keyUri and JWE are not present in the fileUri, use the options
112
+ // step 3: use the keyUri to decrypt the JWE to get the SCR
113
+ // step 4: download the file from the fileUri and decrypt it using the SCR
114
+
115
+ let keyUri: string | undefined;
116
+ let JWE: string | undefined;
117
+ try {
118
+ const url = new URL(fileUri);
119
+ keyUri = url.searchParams.get('keyUri') ?? undefined;
120
+ JWE = url.searchParams.get('JWE') ?? undefined;
121
+ } catch (error) {
122
+ this.$webex.logger.error(`Cypher: Invalid fileUri: ${(error as Error).message}`);
123
+ throw new Error(
124
+ `Failed to decrypt the JWE: ${(error as Error).message}\nStack: ${(error as Error).stack}`
125
+ );
126
+ }
127
+
128
+ // Check if the keyUri and JWE are present, else take it from options
129
+ if (!keyUri || !JWE) {
130
+ keyUri = options.keyUri;
131
+ JWE = options.jwe;
132
+ }
133
+
134
+ // Check if the keyUri and JWE are present, else throw an error
135
+ if (!keyUri || !JWE) {
136
+ throw new Error(
137
+ 'KeyUri and JWE are required to decrypt the file. Either provide them in the fileUri or in the options.'
138
+ );
139
+ }
140
+
141
+ try {
142
+ // Decrypt the JWE to get the SCR
143
+ const scr = await this.$webex.internal.encryption.decryptScr(keyUri, JWE);
144
+
145
+ // Start the download and decryption process, returning a promise
146
+ return this.$webex.internal.encryption.download(fileUri, scr, {useFileService});
147
+ } catch (error) {
148
+ const enhancedError = new Error(
149
+ `Failed to decrypt or download the file: ${(error as Error).message}\nStack: ${
150
+ (error as Error).stack
151
+ }`
152
+ );
153
+ enhancedError.cause = error;
154
+ throw enhancedError;
155
+ }
156
+ }
157
+ }
158
+
159
+ export default Cypher;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Options for downloading a file with encryption.
3
+ */
4
+ interface FileDownloadOptions {
5
+ /**
6
+ * Indicates whether to use the file service for downloading.
7
+ * If true, the webex files service will be used.
8
+ * If false or undefined, the file will be downloaded directly from the URL.
9
+ */
10
+ useFileService?: boolean;
11
+
12
+ /**
13
+ * The JSON Web Encryption (JWE) string used for decrypting the file.
14
+ * This is a required parameter if the url does not contain the JWE.
15
+ */
16
+ jwe?: string;
17
+
18
+ /**
19
+ * The URI of the key used for decrypting the file.
20
+ * This is a required parameter if the url does not contain the keyUri.
21
+ */
22
+ keyUri?: string;
23
+ }
24
+ interface IEncryption {
25
+ downloadAndDecryptFile(fileUri: string, options: FileDownloadOptions): Promise<ArrayBuffer>;
26
+ }
27
+
28
+ export type {IEncryption, FileDownloadOptions};
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ /* eslint-env browser */
2
+ import {registerPlugin} from '@webex/webex-core';
3
+
4
+ import Cypher from './cypher';
5
+ import config from './config';
6
+ import {FileDownloadOptions, IEncryption} from './cypher/types';
7
+
8
+ registerPlugin('cypher', Cypher, {
9
+ config,
10
+ });
11
+
12
+ export default Cypher;
13
+ export type {FileDownloadOptions, IEncryption};
package/src/types.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Logging related types
3
+ */
4
+ export type Logger = {
5
+ log: (payload: string) => void;
6
+ error: (payload: string) => void;
7
+ warn: (payload: string) => void;
8
+ info: (payload: string) => void;
9
+ trace: (payload: string) => void;
10
+ debug: (payload: string) => void;
11
+ };
12
+
13
+ interface IWebexInternal {
14
+ mercury: {
15
+ connect: () => Promise<void>;
16
+ disconnect: () => Promise<void>;
17
+ };
18
+ device: {
19
+ register: () => Promise<void>;
20
+ unregister: () => Promise<void>;
21
+ };
22
+ encryption: {
23
+ decryptScr: (keyUri: string, jwe: string) => Promise<string>;
24
+ download: (
25
+ fileUri: string,
26
+ scr: string,
27
+ options: {useFileService: boolean}
28
+ ) => Promise<ArrayBuffer>;
29
+ };
30
+ }
31
+
32
+ export interface WebexSDK {
33
+ version: string;
34
+ canAuthorize: boolean;
35
+ credentials: {
36
+ getUserToken: () => Promise<string>;
37
+ getOrgId: () => string;
38
+ };
39
+ ready: boolean;
40
+ once: (event: string, callBack: () => void) => void;
41
+ // internal plugins
42
+ internal: IWebexInternal;
43
+ // public plugins
44
+ logger: Logger;
45
+ }
@@ -0,0 +1,146 @@
1
+ import MockWebex from '@webex/test-helper-mock-webex';
2
+ import Cypher from '../../../../src/cypher';
3
+
4
+ import Encryption from '@webex/internal-plugin-encryption';
5
+
6
+ describe('Cypher', () => {
7
+ let cypher: Cypher;
8
+ let webex: any;
9
+
10
+ beforeEach(() => {
11
+ webex = new MockWebex({
12
+ children: {
13
+ cypher: Cypher,
14
+ encryption: Encryption
15
+ },
16
+ logger: {
17
+ log: jest.fn(),
18
+ debug: jest.fn(),
19
+ error: jest.fn(),
20
+ info: jest.fn(),
21
+ },
22
+ credentials: {
23
+ getOrgId: jest.fn(() => 'mockOrgId'),
24
+ },
25
+ once: jest.fn((event, callback) => callback()),
26
+ });
27
+
28
+ cypher = new Cypher({parent: webex});
29
+ webex.cypher = cypher;
30
+
31
+ webex.internal.encryption.decryptScr = jest.fn();
32
+ webex.internal.encryption.download = jest.fn();
33
+ webex.internal.device.register = jest.fn(() => Promise.resolve());
34
+ webex.internal.device.unregister = jest.fn(() => Promise.resolve());
35
+ });
36
+
37
+ afterEach(() => {
38
+ jest.clearAllMocks();
39
+ });
40
+
41
+ describe('downloadAndDecryptFile', () => {
42
+ const fileUri =
43
+ 'https://example.com/encrypted-file?JWE=eyJhbGci&keyUri=kms://example.com/keys/1234';
44
+ const options = {useFileService: false};
45
+
46
+ it('should throw an error if keyUri and JWE are not present in fileUri or options', async () => {
47
+ await expect(
48
+ cypher.downloadAndDecryptFile('https://example.com/encrypted-file', {})
49
+ ).rejects.toThrow(
50
+ 'KeyUri and JWE are required to decrypt the file. Either provide them in the fileUri or in the options.'
51
+ );
52
+ });
53
+
54
+ it('should use keyUri and JWE from options if not present in fileUri', async () => {
55
+ const customOptions = {...options ,keyUri: 'kms://example.com/keys/1234', jwe: 'eyJhbGci'};
56
+ webex.internal.encryption.decryptScr.mockResolvedValue('scr');
57
+ webex.internal.encryption.download.mockResolvedValue(new ArrayBuffer(8));
58
+
59
+ const result = await cypher.downloadAndDecryptFile('https://example.com/encrypted-file', customOptions);
60
+
61
+ expect(webex.internal.encryption.decryptScr).toHaveBeenCalledWith('kms://example.com/keys/1234', 'eyJhbGci');
62
+ expect(webex.internal.encryption.download).toHaveBeenCalledWith('https://example.com/encrypted-file', 'scr', {useFileService: false});
63
+ expect(result).toBeInstanceOf(ArrayBuffer);
64
+ });
65
+
66
+ it('should decrypt and download the file successfully', async () => {
67
+ webex.internal.encryption.decryptScr.mockResolvedValue('scr');
68
+ webex.internal.encryption.download.mockResolvedValue(new ArrayBuffer(8));
69
+
70
+ const result = await cypher.downloadAndDecryptFile(fileUri, options);
71
+
72
+ expect(webex.internal.encryption.decryptScr).toHaveBeenCalledWith('kms://example.com/keys/1234', 'eyJhbGci');
73
+ expect(webex.internal.encryption.download).toHaveBeenCalledWith(fileUri, 'scr', {useFileService: false});
74
+ expect(result).toBeInstanceOf(ArrayBuffer);
75
+ });
76
+
77
+ it('should throw an error if decryption fails', async () => {
78
+ webex.internal.encryption.decryptScr.mockRejectedValue(new Error('Decryption failed'));
79
+
80
+ await expect(cypher.downloadAndDecryptFile(fileUri, options)).rejects.toThrow('Decryption failed');
81
+ });
82
+
83
+ it('should throw an error if download fails', async () => {
84
+ webex.internal.encryption.decryptScr.mockResolvedValue('scr');
85
+ webex.internal.encryption.download.mockRejectedValue(new Error('Download failed'));
86
+
87
+ await expect(cypher.downloadAndDecryptFile(fileUri, options)).rejects.toThrow('Download failed');
88
+ });
89
+ });
90
+
91
+ describe('register', () => {
92
+ it('should register the device and connect to Mercury', async () => {
93
+ await cypher.register();
94
+
95
+ expect(webex.internal.device.register).toHaveBeenCalled();
96
+ });
97
+
98
+ it('should log already registered message if device is already registered', async () => {
99
+ cypher.registered = true;
100
+
101
+ await cypher.register();
102
+ expect(webex.logger.info).toHaveBeenCalledWith('Cypher: webex.internal.device.register already done');
103
+ });
104
+
105
+ it('should log an error if device registration fails', async () => {
106
+ webex.internal.device.register.mockRejectedValue(new Error('Device registration failed'));
107
+
108
+ try {
109
+ await cypher.register();
110
+ } catch (error) {
111
+ expect(error).toEqual(new Error('Device registration failed'));
112
+ }
113
+
114
+ expect(webex.logger.error).toHaveBeenCalledWith('Error occurred during device.register() Error: Device registration failed');
115
+ });
116
+ });
117
+
118
+ describe('deregister', () => {
119
+ it('should deregister the device from WDM', async () => {
120
+ cypher.registered = true;
121
+
122
+ await cypher.deregister();
123
+
124
+ expect(webex.internal.device.unregister).toHaveBeenCalled();
125
+ });
126
+
127
+ it('should log an error if device deregistration fails', async () => {
128
+ cypher.registered = true;
129
+ webex.internal.device.unregister.mockRejectedValue(new Error('Device deregistration failed'));
130
+
131
+ try {
132
+ await cypher.deregister();
133
+ } catch (error) {
134
+ expect(error).toEqual(new Error('Device deregistration failed'));
135
+ }
136
+
137
+ expect(webex.logger.error).toHaveBeenCalledWith('Error occurred during device.deregister() Error: Device deregistration failed');
138
+ });
139
+
140
+ it('should not deregister if device is not registered', async () => {
141
+ await cypher.deregister();
142
+
143
+ expect(webex.logger.info).toHaveBeenCalledWith('Cypher: webex.internal.device.deregister already done');
144
+ });
145
+ });
146
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "include": ["src"],
4
+ "typedocOptions": {
5
+ "entryPoints": [
6
+ "./src/index.ts"
7
+ ],
8
+ "sort": [
9
+ "source-order"
10
+ ],
11
+ "name": "Encryption (@webex/plugin-encryption)",
12
+ "disableSources": "true",
13
+ "entryPointStrategy": "expand",
14
+ "excludeExternals": "true",
15
+ "excludePrivate": "true",
16
+ "hideGenerator": "true",
17
+ },
18
+ }