diff --git a/lib/utils/pkcs7-padding.js b/lib/utils/pkcs7-padding.js index 8e1e92486..413f35425 100644 --- a/lib/utils/pkcs7-padding.js +++ b/lib/utils/pkcs7-padding.js @@ -1,180 +1,201 @@ // @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, }; +/** + * Calculates length of the data padded with given padding configuration. + * + * @param {number} inputLength length of the unpadded input (in bytes) + * @param paddingConfiguration padding configuration + * @returns length of the padded input (in bytes) + */ +function calculatePaddedLength( + inputLength: number, + { blockSizeBytes, superblockSizeBlocks }: PaddingConfiguration = PKCS7_10KB, +): number { + const pkcs7padding = blockSizeBytes - (inputLength % blockSizeBytes); + const pkcs7PaddedLen = inputLength + pkcs7padding; + + const numBlocks = pkcs7PaddedLen / blockSizeBytes; + const paddingBlocks = + superblockSizeBlocks - (numBlocks % superblockSizeBlocks); + + return pkcs7PaddedLen + paddingBlocks * blockSizeBytes; +} + /** * 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 { PKCS7_10KB, pad, unpad }; +export { PKCS7_10KB, pad, unpad, calculatePaddedLength }; // 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 90e8e0193..c7ffa2a06 100644 --- a/lib/utils/pkcs7-padding.test.js +++ b/lib/utils/pkcs7-padding.test.js @@ -1,226 +1,247 @@ // @flow import type { PaddingConfiguration } from './pkcs7-padding'; -import { pad, unpad, testing } from './pkcs7-padding.js'; +import { pad, unpad, testing, calculatePaddedLength } 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); }); }); +describe('calculatePaddingLength', () => { + it('should calculate the correct padding length', () => { + // we're using a 16 byte block size and 4 block superblocks (64 bytes total) + // let's say our data is 83 bytes long (1 full superblock + 19 bytes) + // we need to pad it to be equal to 2 superblocks (2 * 64 = 128 bytes) + // expected padding length is 128-83 = 45 bytes + const blockSizeBytes = 16; + const superblockSizeBlocks = 4; + + const inputLength = 83; + const expectedPaddedLength = 128; + + const calculatedLength = calculatePaddedLength(inputLength, { + blockSizeBytes, + superblockSizeBlocks, + }); + + expect(calculatedLength).toBe(expectedPaddedLength); + }); +}); + 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)); }