diff --git a/lib/utils/pkcs7-padding.js b/lib/utils/pkcs7-padding.js new file mode 100644 index 000000000..a2e52a41c --- /dev/null +++ b/lib/utils/pkcs7-padding.js @@ -0,0 +1,42 @@ +// @flow + +import invariant from 'invariant'; + +/** + * PKCS#7 padding function for `Uint8Array` data. + * + * @param {Uint8Array} data - The data to be padded. + * @param {number} blockSizeBytes - The block size in bytes. + * @returns {Uint8Array} The padded data as a new Uint8Array. + */ +function pkcs7pad(data: Uint8Array, blockSizeBytes: number): Uint8Array { + invariant(blockSizeBytes > 0, 'block size must be positive'); + invariant(blockSizeBytes < 256, 'block size must be less than 256'); + const padding = blockSizeBytes - (data.length % blockSizeBytes); + const padded = new Uint8Array(data.length + padding); + padded.set(data); + padded.fill(padding, data.length); + return padded; +} + +/** + * PKCS#7 unpadding function for `Uint8Array` data. + * + * @param {Uint8Array} data - The padded data to be unpadded. + * @returns {Uint8Array} The unpadded data as a new Uint8Array. + * @throws {Error} If the padding is invalid. + */ +function pkcs7unpad(data: Uint8Array): Uint8Array { + const padding = data[data.length - 1]; + invariant(padding > 0, 'padding must be positive'); + invariant(data.length >= padding, 'data length must be at least padding'); + invariant( + data.subarray(data.length - padding).every(x => x === padding), + 'invalid padding', + ); + + const unpadded = data.subarray(0, data.length - padding); + return unpadded; +} + +export { pkcs7pad, pkcs7unpad }; diff --git a/lib/utils/pkcs7-padding.test.js b/lib/utils/pkcs7-padding.test.js new file mode 100644 index 000000000..562cdc43b --- /dev/null +++ b/lib/utils/pkcs7-padding.test.js @@ -0,0 +1,89 @@ +// @flow + +import { pkcs7pad, pkcs7unpad } from './pkcs7-padding.js'; + +describe('PKCS#7 Padding', () => { + it('should pad data to a multiple of blockSize bytes', () => { + const blockSize = 16; + const data = generateRandomData(100); + const expectedPadding = 16 - (data.length % blockSize); + + const padded = pkcs7pad(data, blockSize); + expect(padded.length % 16).toBe(0); + expect(padded[padded.length - 1]).toBe(expectedPadding); + }); + + it('pkcs7pad should add a full block if input is multiple of blockSize bytes', () => { + const blockSize = 16; + const data = generateRandomData(16); + const expectedPadding = 16; + + const padded = pkcs7pad(data, blockSize); + expect(padded.length % 16).toBe(0); + expect(padded[padded.length - 1]).toBe(expectedPadding); + }); + + it('pkcs7pad should fail if blockSize is out of 1-255 range', () => { + const data = generateRandomData(16); + expect(() => pkcs7pad(data, 0)).toThrow(); + expect(() => pkcs7pad(data, 256)).toThrow(); + }); + + it('pkcs7unpad should unpad data', () => { + // blockSize = 16 + const inputData = generateRandomArray(10); + const padded = new Uint8Array([...inputData, ...[6, 6, 6, 6, 6, 6]]); + + const unpadded = pkcs7unpad(padded); + expect(unpadded.length).toBe(10); + expect(unpadded).toStrictEqual(new Uint8Array(inputData)); + }); + + it('pkcs7unpad should throw if the padding length is 0', () => { + // blockSize = 16 + const padded = new Uint8Array([ + ...generateRandomArray(10), + ...[0, 0, 0, 0, 0, 0], + ]); + expect(() => pkcs7unpad(padded)).toThrow(); + }); + + it('pkcs7unpad should throw if the padding length is > blockSize', () => { + // blockSize = 16 + const padded = new Uint8Array([ + ...generateRandomArray(10), + ...[17, 17, 17, 17, 17, 17], + ]); + expect(() => pkcs7unpad(padded)).toThrow(); + }); + + it('pkcs7unpad should throw if the padding is invalid', () => { + // blockSize = 16 + const padded = new Uint8Array([ + ...generateRandomArray(10), + ...[6, 1, 2, 3, 4, 6], + ]); + expect(() => pkcs7unpad(padded)).toThrow(); + }); + + it('pkcs7pad and pkcs7unpad should be inverses', () => { + const blockSize = 16; + const data = generateRandomData(100); + const padded = pkcs7pad(data, blockSize); + const unpadded = pkcs7unpad(padded); + expect(unpadded.length).toBe(data.length); + expect(unpadded).toEqual(data); + }); +}); + +function generateRandomData(length: number): Uint8Array { + const data = new Uint8Array(length); + for (let i = 0; i < length; i++) { + data[i] = Math.floor(Math.random() * 256); + } + return data; +} + +function generateRandomArray(length: number): Array { + return new Array(length).map(() => Math.floor(Math.random() * 256)); +}