diff --git a/lib/utils/pkcs7-padding.js b/lib/utils/pkcs7-padding.js index 0b26a5264..8e1e92486 100644 --- a/lib/utils/pkcs7-padding.js +++ b/lib/utils/pkcs7-padding.js @@ -1,122 +1,180 @@ // @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'; +export type PaddingConfiguration = { + +blockSizeBytes: number, + +superblockSizeBlocks: number, +}; + +/** + * The padding configuration for 10KB superblocks. + * The block size is 250 bytes, and the superblock size is 40 blocks. + */ +const PKCS7_10KB: PaddingConfiguration = { + blockSizeBytes: 250, + superblockSizeBlocks: 40, +}; + +/** + * Pads the input using the extended PKCS#7 padding (superblock padding). + * The input is first padded using the standard PKCS#7 padding, and then + * padded to the nearest multiple of superblocks (blocks of blocks). + * + * @param {Uint8Array} data - The input to be padded. + * @param {PaddingConfiguration} paddingConfiguration - The padding + * configuration. Defaults to multiple of 10KB. See {@link PKCS7_10KB}. + * @returns {Uint8Array} The padded input. + */ +function pad( + data: Uint8Array, + { blockSizeBytes, superblockSizeBlocks }: PaddingConfiguration = PKCS7_10KB, +): Uint8Array { + const pkcs7Padded = pkcs7pad(data, blockSizeBytes); + return superblockPad(pkcs7Padded, blockSizeBytes, superblockSizeBlocks); +} + +/** + * Unpads the input using the extended PKCS#7 padding (superblock padding). + * The input is first unpadded on the block basis, and then unpadded using + * the standard PKCS#7 padding. + * + * @param {Uint8Array} data - The input to be unpadded. + * @param {PaddingConfiguration} paddingConfiguration - The padding + * configuration. Defaults to multiple of 10KB. See {@link PKCS7_10KB}. + * @returns {Uint8Array} The unpadded input. + */ +function unpad( + data: Uint8Array, + { blockSizeBytes }: PaddingConfiguration = PKCS7_10KB, +): Uint8Array { + const blockUnpadded = superblockUnpad(data, blockSizeBytes); + return pkcs7unpad(blockUnpadded); +} + /** * 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; } /** * 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 }; +export { PKCS7_10KB, pad, unpad }; + +// exported for testing purposes only +export const testing = { + pkcs7pad, + pkcs7unpad, + superblockPad, + superblockUnpad, +}; diff --git a/lib/utils/pkcs7-padding.test.js b/lib/utils/pkcs7-padding.test.js index 69cc3f188..90e8e0193 100644 --- a/lib/utils/pkcs7-padding.test.js +++ b/lib/utils/pkcs7-padding.test.js @@ -1,168 +1,226 @@ // @flow -import { - pkcs7pad, - pkcs7unpad, - superblockPad, - superblockUnpad, -} from './pkcs7-padding.js'; +import type { PaddingConfiguration } from './pkcs7-padding'; +import { pad, unpad, testing } from './pkcs7-padding.js'; + +const { pkcs7pad, pkcs7unpad, superblockPad, superblockUnpad } = testing; 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(); }); }); +describe('padding integration tests (pad and unpad)', () => { + it('should pad data to a multiple of superblockSize blocks', () => { + const config: PaddingConfiguration = { + blockSizeBytes: 16, + superblockSizeBlocks: 4, + }; + + // 20 bytes of data - expected 4 blocks total (1 superblock): + // - block 1 (16 bytes) = 16 bytes of data + // - block 2 (16 bytes) = 4 bytes of data (remaining 12 bytes of padding) + // - block 3 (16 bytes) = full padding (filled with 2s) + // - block 4 (16 bytes) = full padding (filled with 2s) + const dataLengthBytes = 20; + const expectedPkcs7Padding = 12; + const expectedBlockPadding = 2; + const expectedPaddedLength = 4 * 16; + + const data = generateRandomData(dataLengthBytes); + const padded = pad(data, config); + + expect(padded.length % expectedPaddedLength).toBe(0); + expect(padded[padded.length - 1]).toBe(expectedBlockPadding); + expect(padded[2 * 16 - 1]).toBe(expectedPkcs7Padding); + }); + + it('pad should add a full superblock if pkcs7-padded input is a multiple of superblockSize blocks', () => { + const config: PaddingConfiguration = { + blockSizeBytes: 16, + superblockSizeBlocks: 4, + }; + + // 5 bytes less so pkcs7 padding is 5 bytes and pads equally to superblock + // size + const pkcs7paddingLength = 5; + const dataLengthBytes = 4 * 16 - pkcs7paddingLength; + + const expectedPaddedLength = 8 * 16; + const expectedBlockPadding = 4; + + const data = generateRandomData(dataLengthBytes); + const padded = pad(data, config); + + expect(padded.length % expectedPaddedLength).toBe(0); + expect(padded[padded.length - 1]).toBe(expectedBlockPadding); + }); + + it('pad and unpad should be inverses', () => { + const config: PaddingConfiguration = { + blockSizeBytes: 16, + superblockSizeBlocks: 4, + }; + + const data = generateRandomData(100); + const padded = pad(data, config); + const unpadded = unpad(padded, config); + 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)); }