import {WebexPlugin} from '@webex/webex-core'; import {FileDownloadOptions, IEncryption} from './types'; import {CYPHER} from './constants'; import {WebexSDK} from '../types'; /** * @description Encryption APIs for KMS * @class */ class Cypher extends WebexPlugin implements IEncryption { readonly namespace = CYPHER; private readonly $webex: WebexSDK; registered = false; /** * Constructs an instance of the class. * * @param {...any[]} args - The arguments to pass to the superclass constructor. * * @remarks * This constructor calls the superclass constructor with the provided arguments. * It also assigns the `webex` property to the `$webex` property, ignoring TypeScript errors. */ constructor(...args: any[]) { super(...args); this.$webex = (this as WebexPlugin).webex as WebexSDK; } /** * Registers the device to WDM. This is required for metrics and other services. * @returns {Promise} */ async register() { if (this.registered) { this.$webex.logger.info('Cypher: webex.internal.device.register already done'); return Promise.resolve(); } return this.$webex.internal.device .register() .then(() => { this.$webex.logger.info('Cypher: webex.internal.device.register successful'); this.registered = true; }) .catch((error) => { this.$webex.logger.error(`Error occurred during device.register() ${error}`); throw error; }); } /** * Deregisters the device. * @returns {Promise} */ async deregister() { if (!this.registered) { this.$webex.logger.info('Cypher: webex.internal.device.deregister already done'); return Promise.resolve(); } return this.$webex.internal.device .unregister() .then(() => { this.$webex.logger.info('Cypher: webex.internal.device.deregister successful'); this.registered = false; }) .catch((error) => { this.$webex.logger.error(`Error occurred during device.deregister() ${error}`); throw error; }); } /** * Downloads and decrypts a file from the given URI. * * @param {string} fileUri - The URI of the file to be decrypted. * @param {FileDownloadOptions} options - The options for file download. * @returns {Promise} A promise that resolves to the decrypted ArrayBuffer. * @throws {Error} If the file download or decryption fails. * * @example * ```typescript * const attachmentURL = 'https:/myfileurl.xyz/zzz/fileid?keyUri=somekeyuri&JWE=somejwe'; * const options: FileDownloadOptions = { * useFileService: false, * jwe: somejwe, // Provide the JWE here if not already present in the attachmentURL * keyUri: someKeyUri, // Provide the keyURI here if not already present in the attachmentURL * }; * const decryptedBuf = await webex.cypher.downloadAndDecryptFile(attachmentURL, options); * const file = new File([decryptedBuf], "myFileName.jpeg", {type: 'image/jpeg'}); * ``` */ public async downloadAndDecryptFile( fileUri: string, {useFileService = false, ...options}: FileDownloadOptions = {} ): Promise { // 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 // 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: if keyUri and JWE are not present in the fileUri, use the options // step 3: use the keyUri to decrypt the JWE to get the SCR // step 4: download the file from the fileUri and decrypt it using the SCR let keyUri: string | undefined; let JWE: string | undefined; try { const url = new URL(fileUri); keyUri = url.searchParams.get('keyUri') ?? undefined; JWE = url.searchParams.get('JWE') ?? undefined; } catch (error) { this.$webex.logger.error(`Cypher: Invalid fileUri: ${(error as Error).message}`); throw new Error( `Failed to decrypt the JWE: ${(error as Error).message}\nStack: ${(error as Error).stack}` ); } // Check if the keyUri and JWE are present, else take it from options if (!keyUri || !JWE) { keyUri = options.keyUri; JWE = options.jwe; } // Check if the keyUri and JWE are present, else throw an error if (!keyUri || !JWE) { throw new Error( 'KeyUri and JWE are required to decrypt the file. Either provide them in the fileUri or in the options.' ); } try { // Decrypt the JWE to get the SCR const scr = await this.$webex.internal.encryption.decryptScr(keyUri, JWE); // Start the download and decryption process, returning a promise return this.$webex.internal.encryption.download(fileUri, scr, {useFileService}); } catch (error) { const enhancedError = new Error( `Failed to decrypt or download the file: ${(error as Error).message}\nStack: ${ (error as Error).stack }` ); enhancedError.cause = error; throw enhancedError; } } } export default Cypher;