2
0

AesCipherTest.Gen.cs.txt 6.0 KB

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