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 @@ -1,5 +1,19 @@ // @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'; /** @@ -39,4 +53,70 @@ 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 --- a/lib/utils/pkcs7-padding.test.js +++ b/lib/utils/pkcs7-padding.test.js @@ -1,6 +1,11 @@ // @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', () => { @@ -76,6 +81,80 @@ }); }); +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++) {