diff --git a/lib/utils/pkcs7-padding.js b/lib/utils/pkcs7-padding.js index a2e52a41c..0b26a5264 100644 --- a/lib/utils/pkcs7-padding.js +++ b/lib/utils/pkcs7-padding.js @@ -1,42 +1,122 @@ // @flow +/** + * This file contains functions for PKCS#7 padding and unpadding, as well as + * functions for padding on the block basis. This is used to let the padding + * to be larger than maximum PKCS#7 padding block size (255 bytes). + * + * The main idea of this concept is to pad the input using the standard + * PKCS#7 padding, and then pad the result to the neares multiple of + * superblocks (blocks of blocks). The procedure is analogous + * to the standard PKCS#7 padding, but operates on the block basis instead of + * the byte basis. The fill value of the padding is the number of blocks added. + * + * The PKCS#7 padding is described in RFC 5652, section 6.3. + */ + 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 }; +/** + * Pads the PKCS#7-padded input on the block basis. Pads the input to have + * a length that is a multiple of the superblock size. The input must already + * be PKCS#7 padded to the block size. + * + * @param {Uint8Array} data - The PKCS#7 padded input to be block-padded. + * @param {number} blockSizeBytes - The block size in bytes. + * @param {number} superblockSize - The superblock size in blocks. + * @returns {Uint8Array} The block-padded data as a new Uint8Array. + */ +function superblockPad( + data: Uint8Array, + blockSizeBytes: number, + superblockSize: number, +): Uint8Array { + invariant( + data.length % blockSizeBytes === 0, + 'data length must be a multiple of block size', + ); + invariant(superblockSize > 0, 'superblock size must be positive'); + invariant(superblockSize <= 255, 'superblock size must be less than 256'); + invariant(blockSizeBytes > 0, 'block size must be positive'); + invariant(blockSizeBytes <= 255, 'block size must be less than 256'); + + const numBlocks = data.length / blockSizeBytes; + const paddingBlocks = superblockSize - (numBlocks % superblockSize); + const paddingValue = paddingBlocks; + + const outputBuffer = new Uint8Array( + data.length + paddingBlocks * blockSizeBytes, + ); + outputBuffer.set(data); + outputBuffer.fill(paddingValue, data.length); + return outputBuffer; +} + +/** + * Unpads the block-padded input on the block basis. + * + * @param {Uint8Array} data - The block-padded input to be unpaded. + * @param {number} blockSizeBytes - The block size in bytes. + * @returns {Uint8Array} - The unpadded data as a new Uint8Array. + */ +function superblockUnpad(data: Uint8Array, blockSizeBytes: number): Uint8Array { + invariant(blockSizeBytes > 0, 'block size must be positive'); + invariant(blockSizeBytes <= 255, 'block size must be less than 256'); + invariant( + data.length % blockSizeBytes === 0, + 'data length must be a multiple of block size', + ); + + const numBlocks = data.length / blockSizeBytes; + const paddingBlocks = data[data.length - 1]; + invariant(paddingBlocks > 0 && paddingBlocks < numBlocks, 'invalid padding'); + + const unpaddedBlocks = numBlocks - paddingBlocks; + const unpaddedLengthBytes = unpaddedBlocks * blockSizeBytes; + invariant( + data.subarray(unpaddedLengthBytes).every(x => x === paddingBlocks), + 'invalid padding', + ); + + const unpaddedData = data.subarray(0, unpaddedLengthBytes); + return unpaddedData; +} + +export { pkcs7pad, pkcs7unpad, superblockPad, superblockUnpad }; diff --git a/lib/utils/pkcs7-padding.test.js b/lib/utils/pkcs7-padding.test.js index 562cdc43b..69cc3f188 100644 --- a/lib/utils/pkcs7-padding.test.js +++ b/lib/utils/pkcs7-padding.test.js @@ -1,89 +1,168 @@ // @flow -import { pkcs7pad, pkcs7unpad } from './pkcs7-padding.js'; +import { + pkcs7pad, + pkcs7unpad, + superblockPad, + superblockUnpad, +} 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); }); }); +describe('superblock padding', () => { + it('should pad data to a multiple of superblockSize blocks', () => { + const blockSizeBytes = 16; + const superblockSizeBlocks = 4; + const dataLengthBytes = 3 * 16; + const expectedPaddedLength = 4 * 16; + const expectedBlockPadding = 1; + + const data = generateRandomData(dataLengthBytes); + const padded = superblockPad(data, blockSizeBytes, superblockSizeBlocks); + + expect(padded.length % expectedPaddedLength).toBe(0); + expect(padded[padded.length - 1]).toBe(expectedBlockPadding); + }); + + it('pad should add a full superblock if input is a multiple of superblockSize blocks', () => { + const blockSizeBytes = 16; + const superblockSizeBlocks = 4; + const dataLengthBytes = 4 * 16; + const expectedPaddedLength = 8 * 16; + const expectedBlockPadding = 4; + + const data = generateRandomData(dataLengthBytes); + const padded = superblockPad(data, blockSizeBytes, superblockSizeBlocks); + + expect(padded.length % expectedPaddedLength).toBe(0); + expect(padded[padded.length - 1]).toBe(expectedBlockPadding); + }); + + it('superblockUnpad should unpad data', () => { + const blockSizeBytes = 16; + + // 2 blocks of data + 2 blocks of padding = 4 blocks total (1 superblock) + const padded = new Uint8Array([ + ...generateRandomArray(2 * 16), + ...new Array(2 * 16).fill(2), + ]); + + const unpadded = superblockUnpad(padded, blockSizeBytes); + + expect(unpadded.length).toBe(32); + expect(unpadded).toEqual(padded.subarray(0, 32)); + }); + + it('superblockUnpad should throw if the padding length is 0', () => { + const blockSizeBytes = 16; + const padded = new Uint8Array([ + ...generateRandomArray(2 * 16), + ...new Array(2 * 16).fill(0), + ]); + expect(() => superblockUnpad(padded, blockSizeBytes)).toThrow(); + }); + + it('superblockUnpad should throw if the padding length is > num blocks', () => { + const blockSizeBytes = 16; + // 4 blocks total, but filled with 5s + const padded = new Uint8Array([ + ...generateRandomArray(2 * 16), + ...new Array(2 * 16).fill(5), + ]); + expect(() => superblockUnpad(padded, blockSizeBytes)).toThrow(); + }); + + it('superblockUnpad should throw if the padding is invalid', () => { + const blockSizeBytes = 16; + const padded = new Uint8Array([ + ...generateRandomArray(2 * 16), + ...generateRandomArray(2 * 15), + ...[1], + ]); + expect(() => superblockUnpad(padded, blockSizeBytes)).toThrow(); + }); +}); + 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)); }