Skip to content

Commit 38a0922

Browse files
authored
Add pkcs7 padding (#406)
1 parent 5d9c5fe commit 38a0922

File tree

3 files changed

+357
-0
lines changed

3 files changed

+357
-0
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
using System;
2+
using Algorithms.Crypto.Paddings;
3+
using FluentAssertions;
4+
using NUnit.Framework;
5+
6+
namespace Algorithms.Tests.Crypto.Padding;
7+
8+
public class PKCS7PaddingTests
9+
{
10+
private const int defaultBlockSize = 16;
11+
12+
[Test]
13+
public void Constructor_WhenBlockSizeIsLessThanOne_ShouldThrowArgumentOutOfRangeException()
14+
{
15+
const int blockSize = 0;
16+
17+
Action act = () => new Pkcs7Padding(blockSize);
18+
19+
act.Should().Throw<ArgumentOutOfRangeException>()
20+
.WithMessage("Invalid block size: 0 (Parameter 'blockSize')");
21+
}
22+
23+
[Test]
24+
public void Constructor_WhenBlockSizeIsMoreThan255_ShouldThrowArgumentOutOfRangeException()
25+
{
26+
const int blockSize = 256;
27+
28+
Action act = () => new Pkcs7Padding(blockSize);
29+
30+
act.Should().Throw<ArgumentOutOfRangeException>()
31+
.WithMessage("Invalid block size: 256 (Parameter 'blockSize')");
32+
}
33+
34+
[Test]
35+
public void Constructor_WhenBlockSizeIsWithinValidRange_ShouldNotThrowAFit()
36+
{
37+
const int blockSize = 128;
38+
39+
Action act = () => new Pkcs7Padding(blockSize);
40+
41+
act.Should().NotThrow();
42+
}
43+
44+
[Test]
45+
public void AddPadding_WhenNotEnoughSpaceInInputArrayForPadding_ShouldThrowArgumentException()
46+
{
47+
var padding = new Pkcs7Padding(defaultBlockSize);
48+
const int inputOffset = 1;
49+
50+
var size16Input = new byte[16];
51+
52+
Action act = () => padding.AddPadding(size16Input, inputOffset);
53+
54+
act.Should().Throw<ArgumentException>()
55+
.WithMessage("Not enough space in input array for padding");
56+
}
57+
58+
[Test]
59+
public void AddPadding_WhenInputArrayHasEnoughSpace_ShouldReturnCorrectPaddingSize()
60+
{
61+
var padding = new Pkcs7Padding(defaultBlockSize);
62+
const int inputOffset = defaultBlockSize;
63+
64+
var size32Input = new byte[32];
65+
66+
var result = padding.AddPadding(size32Input, inputOffset);
67+
68+
result.Should().Be(defaultBlockSize);
69+
}
70+
71+
[Test]
72+
public void AddPadding_WhenAppliedToAnInputArray_ShouldAddCorrectPKCS7Padding()
73+
{
74+
var padding = new Pkcs7Padding(defaultBlockSize);
75+
const int inputOffset = defaultBlockSize;
76+
77+
var size32Input = new byte[32];
78+
79+
padding.AddPadding(size32Input, inputOffset);
80+
81+
for (var i = 0; i < defaultBlockSize - 1; i++)
82+
{
83+
size32Input[inputOffset + i].Should().Be(defaultBlockSize);
84+
}
85+
}
86+
87+
[Test]
88+
public void RemovePadding_WhenAppliedToAValidInputArray_ShouldRemovePKCS7PaddingCorrectly()
89+
{
90+
var paddingSize = 5;
91+
var size32Input = new byte[32];
92+
for (var i = 0; i < paddingSize; i++)
93+
{
94+
size32Input[size32Input.Length - 1 - i] = (byte)paddingSize;
95+
}
96+
97+
var padding = new Pkcs7Padding(defaultBlockSize);
98+
99+
var output = padding.RemovePadding(size32Input);
100+
101+
output.Length.Should().Be(size32Input.Length - paddingSize);
102+
}
103+
104+
[Test]
105+
public void RemovePadding_WhenInputLengthNotMultipleOfBlockSize_ShouldThrowArgumentException()
106+
{
107+
var input = new byte[defaultBlockSize + 1]; // Length is not a multiple of blockSize
108+
var padding = new Pkcs7Padding(defaultBlockSize);
109+
110+
Action act = () => padding.RemovePadding(input);
111+
112+
act.Should().Throw<ArgumentException>()
113+
.WithMessage("Input length must be a multiple of block size");
114+
}
115+
116+
[Test]
117+
public void RemovePadding_WhenInvalidPaddingLength_ShouldThrowArgumentException()
118+
{
119+
var size32Input = new byte[32];
120+
121+
size32Input[^1] = (byte)(defaultBlockSize + 1); // Set invalid padding length
122+
var padding = new Pkcs7Padding(defaultBlockSize);
123+
124+
Action act = () => padding.RemovePadding(size32Input);
125+
126+
act.Should().Throw<ArgumentException>().WithMessage("Invalid padding length");
127+
}
128+
129+
[Test]
130+
public void RemovePadding_WhenInvalidPadding_ShouldThrowArgumentException()
131+
{
132+
var size32Input = new byte[32];
133+
134+
size32Input[^1] = (byte)(defaultBlockSize); // Set valid padding length
135+
size32Input[^2] = (byte)(defaultBlockSize - 1); // Set invalid padding byte
136+
var padding = new Pkcs7Padding(defaultBlockSize);
137+
138+
Action act = () => padding.RemovePadding(size32Input);
139+
140+
act.Should().Throw<ArgumentException>()
141+
.WithMessage("Invalid padding");
142+
}
143+
144+
[Test]
145+
public void GetPaddingCount_WhenInputArrayIsValid_ShouldReturnCorrectPaddingCount()
146+
{
147+
var paddingSize = 5;
148+
var size32Input = new byte[32];
149+
150+
for (var i = 0; i < paddingSize; i++)
151+
{
152+
size32Input[size32Input.Length - 1 - i] = (byte)paddingSize; // Add padding bytes at the end of the array
153+
}
154+
155+
var padding = new Pkcs7Padding(defaultBlockSize);
156+
157+
var output = padding.GetPaddingCount(size32Input);
158+
159+
output.Should().Be(paddingSize);
160+
}
161+
162+
[Test]
163+
public void GetPaddingCount_WhenInvalidPadding_ShouldThrowArgumentException()
164+
{
165+
var size32Input = new byte[32];
166+
167+
size32Input[^1] = defaultBlockSize;
168+
size32Input[^2] = defaultBlockSize - 1;
169+
170+
var padding = new Pkcs7Padding(defaultBlockSize);
171+
172+
Action act = () => padding.GetPaddingCount(size32Input);
173+
174+
act.Should().Throw<ArgumentException>()
175+
.WithMessage("Padding block is corrupted");
176+
}
177+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
using System;
2+
3+
namespace Algorithms.Crypto.Paddings;
4+
5+
/// <summary>
6+
/// <para>
7+
/// This class implements the PKCS7 padding scheme, which is a standard way of padding data to fit a certain block size.
8+
/// </para>
9+
/// <para>
10+
/// PKCS7 padding adds N bytes of value N to the end of the data, where N is the number of bytes needed to reach the block size.
11+
/// For example, if the block size is 16 bytes, and the data is 11 bytes long, then 5 bytes of value 5 will be added to the
12+
/// end of the data. This way, the padded data will be 16 bytes long and can be encrypted or decrypted by a block cipher algorithm.
13+
/// </para>
14+
/// <para>
15+
/// The padding can be easily removed after decryption by looking at the last byte and subtracting that many bytes from the
16+
/// end of the data.
17+
/// </para>
18+
/// <para>
19+
/// This class supports any block size from 1 to 255 bytes, and can be used with any encryption algorithm that requires
20+
/// padding, such as AES.
21+
/// </para>
22+
/// </summary>
23+
public class Pkcs7Padding
24+
{
25+
private readonly int blockSize;
26+
27+
public Pkcs7Padding(int blockSize)
28+
{
29+
if (blockSize is < 1 or > 255)
30+
{
31+
throw new ArgumentOutOfRangeException(nameof(blockSize), $"Invalid block size: {blockSize}");
32+
}
33+
34+
this.blockSize = blockSize;
35+
}
36+
37+
/// <summary>
38+
/// Adds padding to the end of a byte array according to the PKCS#7 standard.
39+
/// </summary>
40+
/// <param name="input">The byte array to be padded.</param>
41+
/// <param name="inputOffset">The offset from which to start padding.</param>
42+
/// <returns>The padding value that was added to each byte.</returns>
43+
/// <exception cref="ArgumentException">
44+
/// If the input array does not have enough space to add <c>blockSize</c> bytes as padding.
45+
/// </exception>
46+
/// <remarks>
47+
/// The padding value is equal to the number of of bytes that are added to the array.
48+
/// For example, if the input array has a length of 16 and the input offset is 10,
49+
/// then 6 bytes with the value 6 will be added to the end of the array.
50+
/// </remarks>
51+
public int AddPadding(byte[] input, int inputOffset)
52+
{
53+
// Calculate how many bytes need to be added to reach the next multiple of block size.
54+
var code = (byte)((blockSize - (input.Length % blockSize)) % blockSize);
55+
56+
// If no padding is needed, add a full block of padding.
57+
if (code == 0)
58+
{
59+
code = (byte)blockSize;
60+
}
61+
62+
if (inputOffset + code > input.Length)
63+
{
64+
throw new ArgumentException("Not enough space in input array for padding");
65+
}
66+
67+
// Add the padding
68+
for (var i = 0; i < code; i++)
69+
{
70+
input[inputOffset + i] = code;
71+
}
72+
73+
return code;
74+
}
75+
76+
/// <summary>
77+
/// Removes the PKCS7 padding from the given input data.
78+
/// </summary>
79+
/// <param name="input">The input data with PKCS7 padding. Must not be null and must have a vaild length and padding.</param>
80+
/// <returns>The input data without the padding as a new byte array.</returns>
81+
/// <exception cref="ArgumentException">
82+
/// Thrown if the input data is null, has an invalid length, or has an invalid padding.
83+
/// </exception>
84+
public byte[] RemovePadding(byte[] input)
85+
{
86+
// Check if input length is a multiple of blockSize
87+
if (input.Length % blockSize != 0)
88+
{
89+
throw new ArgumentException("Input length must be a multiple of block size");
90+
}
91+
92+
// Get the padding length from the last byte of input
93+
var paddingLength = input[^1];
94+
95+
// Check if padding length is valid
96+
if (paddingLength < 1 || paddingLength > blockSize)
97+
{
98+
throw new ArgumentException("Invalid padding length");
99+
}
100+
101+
// Check if all padding bytes have the correct value
102+
for (var i = 0; i < paddingLength; i++)
103+
{
104+
if (input[input.Length - 1 - i] != paddingLength)
105+
{
106+
throw new ArgumentException("Invalid padding");
107+
}
108+
}
109+
110+
// Create a new array with the size of input minus the padding length
111+
var output = new byte[input.Length - paddingLength];
112+
113+
// Copy the data without the padding into the output array
114+
Array.Copy(input, output, output.Length);
115+
116+
return output;
117+
}
118+
119+
/// <summary>
120+
/// Gets the number of padding bytes in the given input data according to the PKCS7 padding scheme.
121+
/// </summary>
122+
/// <param name="input">The input data with PKCS7 padding. Must not be null and must have a valid padding.</param>
123+
/// <returns>The number of padding bytes in the input data.</returns>
124+
/// <exception cref="ArgumentException">
125+
/// Thrown if the input data is null or has an invalid padding.
126+
/// </exception>
127+
/// <remarks>
128+
/// This method uses bitwise operations to avoid branching.
129+
/// </remarks>
130+
public int GetPaddingCount(byte[] input)
131+
{
132+
if (input == null)
133+
{
134+
throw new ArgumentNullException(nameof(input), "Input cannot be null");
135+
}
136+
137+
// Get the last byte of the input data as the padding value.
138+
var lastByte = input[^1];
139+
var paddingCount = lastByte & 0xFF;
140+
141+
// Calculate the index where the padding starts
142+
var paddingStartIndex = input.Length - paddingCount;
143+
var paddingCheckFailed = 0;
144+
145+
// Check if the padding start index is negative or greater than the input length.
146+
// This is done by using bitwise operations to avoid branching.
147+
// If the padding start index is negative, then its most significant bit will be 1.
148+
// If the padding count is greater than the block size, then its most significant bit will be 1.
149+
// By ORing these two cases, we can get a non-zero value rif either of them is true.
150+
// By shifting this value right by 31 bits, we can get either 0 or -1 as the result.
151+
paddingCheckFailed = (paddingStartIndex | (paddingCount - 1)) >> 31;
152+
153+
for (var i = 0; i < input.Length; i++)
154+
{
155+
// Check if each byte matches the padding value.
156+
// This is done by using bitwise operations to avoid branching.
157+
// If a byte does not match the padding value, then XORing them will give a non-zero value.
158+
// If a byte is before the padding start index, then we want to ignore it.
159+
// This is done by using bitwise operations to create a mask that is either all zeros or all ones.
160+
// If i is less than the padding start index, then subtracting them will give a negative value.
161+
// By shifting this value right by 31 bits, we can get either -1 or 0 as the mask.
162+
// By negating this mask, we can get either 0 or -1 as the mask.
163+
// By ANDing this mask with the XOR result, we can get either 0 or the XOR result as the final result.
164+
// By ORing this final result with the previous padding check result, we can accumulate any non-zero values.
165+
paddingCheckFailed |= (input[i] ^ lastByte) & ~((i - paddingStartIndex) >> 31);
166+
}
167+
168+
// Check if the padding check failed.
169+
if (paddingCheckFailed != 0)
170+
{
171+
throw new ArgumentException("Padding block is corrupted");
172+
}
173+
174+
// Return the number of padding bytes.
175+
return paddingCount;
176+
}
177+
}

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ find more than one implementation for the same objective but using different alg
1919
## List of Algorithms
2020

2121
* [Algorithms](./Algorithms)
22+
* [Crypto](./Algorithms/Crypto/)
23+
* [Paddings](./Algorithms/Crypto/Paddings/)
24+
* [PKC7 Padding](./Algorithms/Crypto/Paddings/PKCS7Padding.cs)
2225
* [Data Compression](./Algorithms/DataCompression)
2326
* [Burrows-Wheeler transform](./Algorithms/DataCompression/BurrowsWheelerTransform.cs)
2427
* [Huffman Compressor](./Algorithms/DataCompression/HuffmanCompressor.cs)

0 commit comments

Comments
 (0)