diff --git a/lib/utils/pkcs7-padding.js b/lib/utils/pkcs7-padding.js --- a/lib/utils/pkcs7-padding.js +++ b/lib/utils/pkcs7-padding.js @@ -16,6 +16,56 @@ 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. + */ +export 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. + */ +export 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. + */ +export function unpad( + data: Uint8Array, + { blockSizeBytes }: PaddingConfiguration = PKCS7_10KB, +): Uint8Array { + const blockUnpadded = superblockUnpad(data, blockSizeBytes); + return pkcs7unpad(blockUnpadded); +} + /** * PKCS#7 padding function for `Uint8Array` data. * @@ -23,7 +73,7 @@ * @param {number} blockSizeBytes - The block size in bytes. * @returns {Uint8Array} The padded data as a new Uint8Array. */ -export function pkcs7pad(data: Uint8Array, blockSizeBytes: number): 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); @@ -40,7 +90,7 @@ * @returns {Uint8Array} The unpadded data as a new Uint8Array. * @throws {Error} If the padding is invalid. */ -export function pkcs7unpad(data: Uint8Array): Uint8Array { +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'); @@ -63,7 +113,7 @@ * @param {number} superblockSize - The superblock size in blocks. * @returns {Uint8Array} The block-padded data as a new Uint8Array. */ -export function superblockPad( +function superblockPad( data: Uint8Array, blockSizeBytes: number, superblockSize: number, @@ -96,10 +146,7 @@ * @param {number} blockSizeBytes - The block size in bytes. * @returns {Uint8Array} - The unpadded data as a new Uint8Array. */ -export function superblockUnpad( - data: Uint8Array, - blockSizeBytes: number, -): 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( @@ -121,3 +168,11 @@ const unpaddedData = data.subarray(0, unpaddedLengthBytes); return unpaddedData; } + +// 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 --- a/lib/utils/pkcs7-padding.test.js +++ b/lib/utils/pkcs7-padding.test.js @@ -1,11 +1,7 @@ // @flow -import { - pkcs7pad, - pkcs7unpad, - superblockPad, - superblockUnpad, -} from './pkcs7-padding.js'; +import type { PaddingConfiguration } from './pkcs7-padding'; +import { pad, unpad, testing } from './pkcs7-padding.js'; describe('PKCS#7 Padding', () => { it('should pad data to a multiple of blockSize bytes', () => { @@ -13,7 +9,7 @@ const data = generateRandomData(100); const expectedPadding = 16 - (data.length % blockSize); - const padded = pkcs7pad(data, blockSize); + const padded = testing.pkcs7pad(data, blockSize); expect(padded.length % 16).toBe(0); expect(padded[padded.length - 1]).toBe(expectedPadding); }); @@ -23,15 +19,15 @@ const data = generateRandomData(16); const expectedPadding = 16; - const padded = pkcs7pad(data, blockSize); + const padded = testing.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(); + expect(() => testing.pkcs7pad(data, 0)).toThrow(); + expect(() => testing.pkcs7pad(data, 256)).toThrow(); }); it('pkcs7unpad should unpad data', () => { @@ -41,7 +37,7 @@ ...[6, 6, 6, 6, 6, 6], ]); - const unpadded = pkcs7unpad(padded); + const unpadded = testing.pkcs7unpad(padded); expect(unpadded.length).toBe(10); }); @@ -51,7 +47,7 @@ ...generateRandomArray(10), ...[0, 0, 0, 0, 0, 0], ]); - expect(() => pkcs7unpad(padded)).toThrow(); + expect(() => testing.pkcs7unpad(padded)).toThrow(); }); it('pkcs7unpad should throw if the padding length is > blockSize', () => { @@ -60,7 +56,7 @@ ...generateRandomArray(10), ...[17, 17, 17, 17, 17, 17], ]); - expect(() => pkcs7unpad(padded)).toThrow(); + expect(() => testing.pkcs7unpad(padded)).toThrow(); }); it('pkcs7unpad should throw if the padding is invalid', () => { @@ -69,14 +65,14 @@ ...generateRandomArray(10), ...[6, 1, 2, 3, 4, 6], ]); - expect(() => pkcs7unpad(padded)).toThrow(); + expect(() => testing.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); + const padded = testing.pkcs7pad(data, blockSize); + const unpadded = testing.pkcs7unpad(padded); expect(unpadded.length).toBe(data.length); expect(unpadded).toEqual(data); }); @@ -91,7 +87,11 @@ const expectedBlockPadding = 1; const data = generateRandomData(dataLengthBytes); - const padded = superblockPad(data, blockSizeBytes, superblockSizeBlocks); + const padded = testing.superblockPad( + data, + blockSizeBytes, + superblockSizeBlocks, + ); expect(padded.length % expectedPaddedLength).toBe(0); expect(padded[padded.length - 1]).toBe(expectedBlockPadding); @@ -105,7 +105,11 @@ const expectedBlockPadding = 4; const data = generateRandomData(dataLengthBytes); - const padded = superblockPad(data, blockSizeBytes, superblockSizeBlocks); + const padded = testing.superblockPad( + data, + blockSizeBytes, + superblockSizeBlocks, + ); expect(padded.length % expectedPaddedLength).toBe(0); expect(padded[padded.length - 1]).toBe(expectedBlockPadding); @@ -120,7 +124,7 @@ ...new Array(2 * 16).fill(2), ]); - const unpadded = superblockUnpad(padded, blockSizeBytes); + const unpadded = testing.superblockUnpad(padded, blockSizeBytes); expect(unpadded.length).toBe(32); expect(unpadded).toEqual(padded.subarray(0, 32)); @@ -132,7 +136,7 @@ ...generateRandomArray(2 * 16), ...new Array(2 * 16).fill(0), ]); - expect(() => superblockUnpad(padded, blockSizeBytes)).toThrow(); + expect(() => testing.superblockUnpad(padded, blockSizeBytes)).toThrow(); }); it('superblockUnpad should throw if the padding length is > num blocks', () => { @@ -142,7 +146,7 @@ ...generateRandomArray(2 * 16), ...new Array(2 * 16).fill(5), ]); - expect(() => superblockUnpad(padded, blockSizeBytes)).toThrow(); + expect(() => testing.superblockUnpad(padded, blockSizeBytes)).toThrow(); }); it('superblockUnpad should throw if the padding is invalid', () => { @@ -152,7 +156,67 @@ ...generateRandomArray(2 * 15), ...[1], ]); - expect(() => superblockUnpad(padded, blockSizeBytes)).toThrow(); + expect(() => testing.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); }); });