Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/@webex/internal-plugin-encryption/src/encryption.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Convert non-Buffer inputs before encrypting binary data

encryptBinaryData() documents support for Buffer|ArrayBuffer|Blob|File, but it passes data straight into ensureBuffer(), which only accepts Buffer-like objects (constructor.isBuffer). In browser paths where callers naturally provide Blob, File, or ArrayBuffer, this method will reject with `buffer` must be a buffer and never encrypt, so the new feature does not work for the documented binary input types.

Useful? React with 👍 / 👎.

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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
});
Loading