From 4349d59c269abaa4d2512081f4482a6462fb1e6f Mon Sep 17 00:00:00 2001 From: Nishant M C Date: Fri, 6 Mar 2026 12:52:58 -0600 Subject: [PATCH] feat(plugin-encryption): enhance encryption internal plugin to support encryption of binary data --- .../src/encryption.js | 23 ++++ .../test/integration/spec/encryption.js | 101 ++++++++++++++++++ .../test/unit/spec/encryption.js | 78 ++++++++++++++ 3 files changed, 202 insertions(+) diff --git a/packages/@webex/internal-plugin-encryption/src/encryption.js b/packages/@webex/internal-plugin-encryption/src/encryption.js index 9bb346c0178..4dc964bef88 100644 --- a/packages/@webex/internal-plugin-encryption/src/encryption.js +++ b/packages/@webex/internal-plugin-encryption/src/encryption.js @@ -240,6 +240,29 @@ const Encryption = WebexPlugin.extend({ ); }, + /** + * Encrypt binary data using the supplied key uri. + * + * @param {string} kmsKeyUri - The uri of a key stored in KMS + * @param {Buffer|ArrayBuffer|Blob|File} data - Binary data to encrypt + * @param {Object} options + * @param {string} options.onBehalfOf - Fetch the KMS key on behalf of another user (using the user's UUID), active user requires the 'spark.kms_orgagent' role + * @returns {string} Encrypted binary data as JWE + */ + encryptBinaryData(kmsKeyUri, data, options) { + return this.getKey(kmsKeyUri, options).then((k) => + ensureBuffer(data).then((buffer) => + jose.JWE.createEncrypt(this.config.joseOptions, { + key: k.jwk, + header: { + alg: 'dir', + }, + reference: null, + }).final(buffer) + ) + ); + }, + /** * Fetch the key associated with the supplied KMS uri. * diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js index 5600328cd88..28727d229f3 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js @@ -390,6 +390,78 @@ describe('Encryption', function () { // browserOnly(it)(`accepts a Blob`); }); + describe('#encryptBinaryData()', () => { + it('encrypts binary data', () => + webex.internal.encryption + .encryptBinaryData(key, FILE) + .then((jwe) => { + assert.isString(jwe); + assert.notEqual(jwe, FILE.toString('base64')); + // JWE format starts with eyJ (base64 encoded header) + assert.match(jwe, /^eyJ/); + })); + + it('encrypts binary data with Buffer input', () => { + const binaryData = Buffer.from('test binary data', 'utf8'); + + return webex.internal.encryption + .encryptBinaryData(key, binaryData) + .then((jwe) => { + assert.isString(jwe); + assert.notEqual(jwe, binaryData.toString()); + assert.match(jwe, /^eyJ/); + }); + }); + + it('encrypts binary data with options parameter', () => { + const binaryData = Buffer.from('test binary data with options', 'utf8'); + + return webex.internal.encryption + .encryptBinaryData(key, binaryData, {}) + .then((jwe) => { + assert.isString(jwe); + assert.match(jwe, /^eyJ/); + }); + }); + + it('encrypts binary data with dynamically generated 3000 character string', () => { + // Generate a 3000-character string dynamically using different character sets + const generateLargeString = (length) => { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'; + let result = ''; + const charsLength = chars.length; + + for (let i = 0; i < length; i++) { + // Use modulo to cycle through different patterns for variety + const index = (i * 7 + Math.floor(i / 100) * 3) % charsLength; + result += chars.charAt(index); + } + + return result; + }; + + const largeString = generateLargeString(3000); + const binaryData = Buffer.from(largeString, 'utf8'); + + // Verify the string is exactly 3000 characters + assert.equal(largeString.length, 3000); + + return webex.internal.encryption + .encryptBinaryData(key, binaryData) + .then((jwe) => { + assert.isString(jwe); + assert.notEqual(jwe, binaryData.toString('base64')); + + return webex.internal.encryption.decryptBinaryData(key, jwe); + }) + .then((decryptedData) => { + assert.isTrue(isBuffer(decryptedData)); + assert.equal(decryptedData.toString('utf8'), largeString); + assert.equal(decryptedData.toString('utf8').length, 3000); + }); + }); + }); + describe('#encryptScr()', () => { it('encrypts an scr', () => webex.internal.encryption @@ -463,6 +535,35 @@ describe('Encryption', function () { }); }); + it('encrypt binary data', () => { + const binaryData = Buffer.from('compliance encrypt binary data', 'utf8'); + + return complianceUser.webex.internal.encryption + .encryptBinaryData(key, binaryData, {onBehalfOf: user.id}) + .then((jwe) => { + assert.isString(jwe); + assert.match(jwe, /^eyJ/); + }); + }); + + it('encrypt and decrypt binary data', () => { + const binaryData = Buffer.from('compliance round-trip binary data', 'utf8'); + + return complianceUser.webex.internal.encryption + .encryptBinaryData(key, binaryData, {onBehalfOf: user.id}) + .then((jwe) => { + assert.isString(jwe); + + return complianceUser.webex.internal.encryption.decryptBinaryData(key, jwe, { + onBehalfOf: user.id, + }); + }) + .then((decryptedData) => { + assert.isTrue(isBuffer(decryptedData)); + assert.equal(decryptedData.toString('utf8'), 'compliance round-trip binary data'); + }); + }); + it('encrypt and decrypt text', () => complianceUser.webex.internal.encryption .encryptText(key, PLAINTEXT, {onBehalfOf: user.id}) diff --git a/packages/@webex/internal-plugin-encryption/test/unit/spec/encryption.js b/packages/@webex/internal-plugin-encryption/test/unit/spec/encryption.js index 452bfa3b69d..8bb18bbb5a8 100644 --- a/packages/@webex/internal-plugin-encryption/test/unit/spec/encryption.js +++ b/packages/@webex/internal-plugin-encryption/test/unit/spec/encryption.js @@ -154,4 +154,82 @@ describe('internal-plugin-encryption', () => { }); }); }); + + describe('encryptBinaryData', () => { + let webex; + + beforeEach(() => { + webex = new MockWebex({ + children: { + encryption: Encryption, + }, + }); + }); + + describe('check encryptBinaryData()', () => { + const testKey = 'https://kms.example.com/keys/test-key-id'; + const testData = Buffer.from('binary data to encrypt'); + const testOptions = {onBehalfOf: 'test-user-uuid'}; + const mockJwk = {kty: 'oct', k: 'test-key-material'}; + const mockKey = {jwk: mockJwk}; + const mockEncryptedJWE = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..encrypted.data.here'; + let getKeyStub; + let joseEncryptStub; + let mockEncryptor; + + beforeEach(() => { + getKeyStub = sinon.stub(webex.internal.encryption, 'getKey').resolves(mockKey); + + // Mock the jose.JWE.createEncrypt chain + mockEncryptor = { + final: sinon.stub().resolves(mockEncryptedJWE) + }; + joseEncryptStub = sinon.stub(require('node-jose').JWE, 'createEncrypt').returns(mockEncryptor); + }); + + it('should call getKey and jose.JWE.createEncrypt with correct parameters', async () => { + await webex.internal.encryption.encryptBinaryData(testKey, testData, testOptions); + + assert.equal(getKeyStub.calledOnce, true); + assert.equal(getKeyStub.args[0][0], testKey); + assert.deepEqual(getKeyStub.args[0][1], testOptions); + + assert.equal(joseEncryptStub.calledOnce, true); + assert.deepEqual(joseEncryptStub.args[0][1], { + key: mockJwk, + header: { + alg: 'dir', + }, + reference: null, + }); + }); + + it('should call final with buffer', async () => { + await webex.internal.encryption.encryptBinaryData(testKey, testData, testOptions); + + assert.equal(mockEncryptor.final.calledOnce, true); + assert.equal(Buffer.isBuffer(mockEncryptor.final.args[0][0]), true); + }); + + it('should return the encrypted JWE string', async () => { + const result = await webex.internal.encryption.encryptBinaryData(testKey, testData, testOptions); + + assert.equal(result, mockEncryptedJWE); + assert.equal(typeof result, 'string'); + }); + + it('should work without options parameter', async () => { + await webex.internal.encryption.encryptBinaryData(testKey, testData); + + assert.equal(getKeyStub.calledOnce, true); + assert.equal(getKeyStub.args[0][0], testKey); + assert.equal(getKeyStub.args[0][1], undefined); + }); + + afterEach(() => { + getKeyStub.restore(); + joseEncryptStub.restore(); + }); + }); + }); });