Skip to content

Commit 826222f

Browse files
authored
Increase test coverage for AesCipher (#1232)
* Increase test coverage for AesCipher The tests were generated by a script which is also added for posterity. The script works by running "openssl enc [...]" (via WSL) to generate the expected encrypted values, and also verifies those values against the .NET BCL implementation as an extra validation (it uncovered a difference in CFB mode between the two relating to the feedback size). * Fix OfbCipherMode It was an exact copy of CfbCipherMode
1 parent 508fc87 commit 826222f

File tree

3 files changed

+1655
-3
lines changed

3 files changed

+1655
-3
lines changed

src/Renci.SshNet/Security/Cryptography/Ciphers/Modes/OfbCipherMode.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,13 @@ public override int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputC
5050

5151
_ = Cipher.EncryptBlock(IV, 0, IV.Length, _ivOutput, 0);
5252

53+
Buffer.BlockCopy(_ivOutput, 0, IV, 0, IV.Length);
54+
5355
for (var i = 0; i < _blockSize; i++)
5456
{
5557
outputBuffer[outputOffset + i] = (byte)(_ivOutput[i] ^ inputBuffer[inputOffset + i]);
5658
}
5759

58-
Buffer.BlockCopy(IV, _blockSize, IV, 0, IV.Length - _blockSize);
59-
Buffer.BlockCopy(outputBuffer, outputOffset, IV, IV.Length - _blockSize, _blockSize);
60-
6160
return _blockSize;
6261
}
6362

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Used to generate tests in AesCipherTest.cs
2+
3+
// The script works by running "openssl enc [...]" (via WSL) to generate the
4+
// expected encrypted values, and also verifies those values against the .NET
5+
// BCL implementation as an extra validation before generating the tests.
6+
7+
Dictionary<string, (string, CipherMode?)> modes = new()
8+
{
9+
["ecb"] = ("mode: null", CipherMode.ECB),
10+
["cbc"] = ("new CbcCipherMode((byte[])iv.Clone())", CipherMode.CBC),
11+
["cfb"] = ("new CfbCipherMode((byte[])iv.Clone())", CipherMode.CFB),
12+
["ctr"] = ("new CtrCipherMode((byte[])iv.Clone())", null),
13+
["ofb"] = ("new OfbCipherMode((byte[])iv.Clone())", CipherMode.OFB),
14+
};
15+
16+
Random random = new(123);
17+
18+
using IndentedTextWriter tw = new(Console.Out);
19+
20+
foreach ((string mode, (string modeCode, CipherMode? bclMode)) in modes)
21+
{
22+
foreach (int keySize in new int[] { 128, 192, 256 })
23+
{
24+
foreach (int inputLength in new int[] { 16, 32, 64 })
25+
{
26+
byte[] input = new byte[inputLength];
27+
random.NextBytes(input);
28+
29+
byte[] key = new byte[keySize / 8];
30+
random.NextBytes(key);
31+
32+
byte[] iv = new byte[16];
33+
random.NextBytes(iv);
34+
35+
StringBuilder openSslCmd = new();
36+
37+
openSslCmd.Append($"echo -n -e '{string.Join("", input.Select(b => $"\\x{b:x2}"))}' |");
38+
openSslCmd.Append($" openssl enc -e -aes-{keySize}-{mode}");
39+
openSslCmd.Append($" -K {Convert.ToHexString(key)}");
40+
if (mode != "ecb")
41+
{
42+
openSslCmd.Append($" -iv {Convert.ToHexString(iv)}");
43+
}
44+
openSslCmd.Append(" -nopad");
45+
46+
ProcessStartInfo pi = new("wsl", openSslCmd.ToString())
47+
{
48+
RedirectStandardOutput = true,
49+
RedirectStandardError = true,
50+
};
51+
52+
byte[] expected;
53+
string error;
54+
55+
using (MemoryStream ms = new())
56+
{
57+
var p = Process.Start(pi);
58+
p.StandardOutput.BaseStream.CopyTo(ms);
59+
error = p.StandardError.ReadToEnd();
60+
61+
p.WaitForExit();
62+
63+
expected = ms.ToArray();
64+
}
65+
66+
tw.WriteLine("[TestMethod]");
67+
tw.WriteLine($"public void AES_{mode.ToUpper()}_{keySize}_Length{inputLength}()");
68+
tw.WriteLine("{");
69+
tw.Indent++;
70+
71+
WriteBytes(input);
72+
WriteBytes(key);
73+
if (mode != "ecb")
74+
{
75+
WriteBytes(iv);
76+
}
77+
tw.WriteLine();
78+
79+
if (!string.IsNullOrWhiteSpace(error))
80+
{
81+
tw.WriteLine($"// {openSslCmd}");
82+
tw.WriteLine($"Assert.Fail(@\"{error}\");");
83+
84+
tw.Indent--;
85+
tw.WriteLine("}");
86+
tw.WriteLine();
87+
continue;
88+
}
89+
90+
tw.WriteLine($"// {openSslCmd} | hd"); // pipe to hexdump
91+
WriteBytes(expected);
92+
tw.WriteLine();
93+
tw.WriteLine($"var actual = new AesCipher(key, {modeCode}, padding: null).Encrypt(input);");
94+
tw.WriteLine();
95+
tw.WriteLine($"CollectionAssert.AreEqual(expected, actual);");
96+
97+
if (bclMode is not null and not CipherMode.OFB)
98+
{
99+
// Verify the OpenSSL result is the same as the .NET BCL, just to be sure
100+
Aes bcl = Aes.Create();
101+
bcl.Key = key;
102+
bcl.IV = iv;
103+
bcl.Mode = bclMode.Value;
104+
bcl.Padding = PaddingMode.None;
105+
bcl.FeedbackSize = 128; // .NET is CFB8 by default, OpenSSL is CFB128
106+
byte[] bclBytes = bcl.CreateEncryptor().TransformFinalBlock(input, 0, input.Length);
107+
108+
if (!bclBytes.AsSpan().SequenceEqual(expected))
109+
{
110+
tw.WriteLine();
111+
tw.WriteLine(@"Assert.Inconclusive(@""OpenSSL does not match the .NET BCL");
112+
tw.Indent++;
113+
tw.WriteLine($@"OpenSSL: {Convert.ToHexString(expected)}");
114+
tw.WriteLine($@"BCL: {Convert.ToHexString(bclBytes)}"");");
115+
tw.Indent--;
116+
}
117+
}
118+
119+
tw.WriteLine();
120+
tw.WriteLine($"var decrypted = new AesCipher(key, {modeCode}, padding: null).Decrypt(actual);");
121+
tw.WriteLine();
122+
tw.WriteLine($"CollectionAssert.AreEqual(input, decrypted);");
123+
124+
tw.Indent--;
125+
tw.WriteLine("}");
126+
tw.WriteLine();
127+
}
128+
}
129+
}
130+
131+
void WriteBytes(byte[] bytes, [CallerArgumentExpression(nameof(bytes))] string name = null)
132+
{
133+
tw.WriteLine($"var {name} = new byte[]");
134+
tw.WriteLine("{");
135+
tw.Indent++;
136+
foreach (byte[] chunk in bytes.Chunk(16))
137+
{
138+
tw.WriteLine(string.Join(", ", chunk.Select(b => $"0x{b:x2}")) + ',');
139+
}
140+
tw.Indent--;
141+
tw.WriteLine("};");
142+
}

0 commit comments

Comments
 (0)