From 629f00998e918d6f9aef25ece958f74ddcb3e778 Mon Sep 17 00:00:00 2001 From: Mateusz Tosnowiec Date: Sat, 3 Oct 2020 01:47:57 +0200 Subject: [PATCH] RFC5649WrapEngine Code ported from Java BouncyCastle version --- crypto/BouncyCastle.csproj | 2 + crypto/src/crypto/engines/AesWrapPadEngine.cs | 10 + .../src/crypto/engines/Rfc5649WrapEngine.cs | 283 ++++++++++++++++++ crypto/test/UnitTests.csproj | 1 + crypto/test/src/crypto/test/AESWrapPadTest.cs | 244 +++++++++++++++ 5 files changed, 540 insertions(+) create mode 100644 crypto/src/crypto/engines/AesWrapPadEngine.cs create mode 100644 crypto/src/crypto/engines/Rfc5649WrapEngine.cs create mode 100644 crypto/test/src/crypto/test/AESWrapPadTest.cs diff --git a/crypto/BouncyCastle.csproj b/crypto/BouncyCastle.csproj index 3285296bc3..23d72a3a62 100644 --- a/crypto/BouncyCastle.csproj +++ b/crypto/BouncyCastle.csproj @@ -663,6 +663,8 @@ + + diff --git a/crypto/src/crypto/engines/AesWrapPadEngine.cs b/crypto/src/crypto/engines/AesWrapPadEngine.cs new file mode 100644 index 0000000000..d03d48838c --- /dev/null +++ b/crypto/src/crypto/engines/AesWrapPadEngine.cs @@ -0,0 +1,10 @@ +namespace Org.BouncyCastle.Crypto.Engines +{ + public class AesWrapPadEngine : Rfc5649WrapEngine + { + public AesWrapPadEngine() + : base(new AesEngine()) + { + } + } +} diff --git a/crypto/src/crypto/engines/Rfc5649WrapEngine.cs b/crypto/src/crypto/engines/Rfc5649WrapEngine.cs new file mode 100644 index 0000000000..4b2c2fc986 --- /dev/null +++ b/crypto/src/crypto/engines/Rfc5649WrapEngine.cs @@ -0,0 +1,283 @@ +using System; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Utilities; +using Org.BouncyCastle.Utilities; + +namespace Org.BouncyCastle.Crypto.Engines +{ + /// + /// An implementation of the AES Key Wrap with Padding specification as described in RFC 5649. + /// For details on the specification see https://tools.ietf.org/html/rfc5649. + /// + public class Rfc5649WrapEngine : IWrapper + { + private readonly IBlockCipher engine; + private KeyParameter param; + private bool forWrapping; + + // The AIV as defined in the RFC + private static byte[] highOrderIV = { 0xa6, 0x59, 0x59, 0xa6 }; + private byte[] preIV = highOrderIV; + + private byte[] extractedAIV = null; + + public virtual string AlgorithmName => engine.AlgorithmName; + + public Rfc5649WrapEngine(IBlockCipher engine) + { + this.engine = engine; + } + + public void Init(bool forWrapping, ICipherParameters param) + { + this.forWrapping = forWrapping; + + if (param is ParametersWithRandom) + { + param = ((ParametersWithRandom)param).Parameters; + } + + if (param is KeyParameter) + { + this.param = (KeyParameter)param; + this.preIV = highOrderIV; + } + else if (param is ParametersWithIV) + { + this.preIV = ((ParametersWithIV)param).GetIV(); + this.param = (KeyParameter)((ParametersWithIV)param).Parameters; + if (this.preIV.Length != 4) + { + throw new ArgumentException("IV length not equal to 4", nameof(param)); + } + } + } + + /// + /// Pads the plaintext (i.e., the key to be wrapped) as per section 4.1 of RFC 5649. + /// + /// The key being wrapped. + /// The padded key. + private byte[] PadPlaintext(byte[] plaintext) + { + int plaintextLength = plaintext.Length; + int numOfZerosToAppend = (8 - (plaintextLength % 8)) % 8; + byte[] paddedPlaintext = new byte[plaintextLength + numOfZerosToAppend]; + Array.Copy(plaintext, 0, paddedPlaintext, 0, plaintextLength); + if (numOfZerosToAppend != 0) + { + // plaintext (i.e., key to be wrapped) does not have + // a multiple of 8 octet blocks so it must be padded + byte[] zeros = new byte[numOfZerosToAppend]; + Array.Copy(zeros, 0, paddedPlaintext, plaintextLength, numOfZerosToAppend); + } + return paddedPlaintext; + } + + public byte[] Wrap(byte[] input, int inOff, int inLen) + { + if (!forWrapping) + { + throw new InvalidOperationException("not set for wrapping"); + } + byte[] iv = new byte[8]; + + // MLI = size of key to be wrapped + byte[] mli = Pack.UInt32_To_BE((uint)inLen); + // copy in the fixed portion of the AIV + Array.Copy(preIV, 0, iv, 0, preIV.Length); + // copy in the MLI after the AIV + Array.Copy(mli, 0, iv, preIV.Length, mli.Length); + + // get the relevant plaintext to be wrapped + byte[] relevantPlaintext = new byte[inLen]; + Array.Copy(input, inOff, relevantPlaintext, 0, inLen); + byte[] paddedPlaintext = PadPlaintext(relevantPlaintext); + + if (paddedPlaintext.Length == 8) + { + // if the padded plaintext contains exactly 8 octets, + // then prepend iv and encrypt using AES in ECB mode. + + // prepend the IV to the plaintext + byte[] paddedPlainTextWithIV = new byte[paddedPlaintext.Length + iv.Length]; + Array.Copy(iv, 0, paddedPlainTextWithIV, 0, iv.Length); + Array.Copy(paddedPlaintext, 0, paddedPlainTextWithIV, iv.Length, paddedPlaintext.Length); + + engine.Init(true, param); + for (int i = 0; i < paddedPlainTextWithIV.Length; i += engine.GetBlockSize()) + { + engine.ProcessBlock(paddedPlainTextWithIV, i, paddedPlainTextWithIV, i); + } + + return paddedPlainTextWithIV; + } + else + { + // otherwise, apply the RFC 3394 wrap to + // the padded plaintext with the new IV + IWrapper wrapper = new Rfc3394WrapEngine(engine); + ParametersWithIV paramsWithIV = new ParametersWithIV(param, iv); + wrapper.Init(true, paramsWithIV); + return wrapper.Wrap(paddedPlaintext, 0, paddedPlaintext.Length); + } + + } + + public byte[] Unwrap(byte[] input, int inOff, int inLen) + { + if (forWrapping) + { + throw new InvalidOperationException("not set for unwrapping"); + } + + int n = inLen / 8; + + if ((n * 8) != inLen) + { + throw new InvalidCipherTextException("unwrap data must be a multiple of 8 bytes"); + } + + if (n == 1) + { + throw new InvalidCipherTextException("unwrap data must be at least 16 bytes"); + } + + byte[] relevantCiphertext = new byte[inLen]; + Array.Copy(input, inOff, relevantCiphertext, 0, inLen); + byte[] decrypted = new byte[inLen]; + byte[] paddedPlaintext; + + if (n == 2) + { + // When there are exactly two 64-bit blocks of ciphertext, + // they are decrypted as a single block using AES in ECB. + engine.Init(false, param); + for (int i = 0; i < relevantCiphertext.Length; i += engine.GetBlockSize()) + { + engine.ProcessBlock(relevantCiphertext, i, decrypted, i); + } + + // extract the AIV + extractedAIV = new byte[8]; + Array.Copy(decrypted, 0, extractedAIV, 0, extractedAIV.Length); + paddedPlaintext = new byte[decrypted.Length - extractedAIV.Length]; + Array.Copy(decrypted, extractedAIV.Length, paddedPlaintext, 0, paddedPlaintext.Length); + } + else + { + // Otherwise, unwrap as per RFC 3394 but don't check IV the same way + decrypted = Rfc3394UnwrapNoIvCheck(input, inOff, inLen); + paddedPlaintext = decrypted; + } + + // Decompose the extracted AIV to the fixed portion and the MLI + byte[] extractedHighOrderAIV = new byte[4]; + byte[] mliBytes = new byte[4]; + Array.Copy(extractedAIV, 0, extractedHighOrderAIV, 0, extractedHighOrderAIV.Length); + Array.Copy(extractedAIV, extractedHighOrderAIV.Length, mliBytes, 0, mliBytes.Length); + var mli = Pack.BE_To_UInt32(mliBytes, 0); + // Even if a check fails we still continue and check everything + // else in order to avoid certain timing based side-channel attacks. + var isValid = true; + + // Check the fixed portion of the AIV + if (!Arrays.ConstantTimeAreEqual(extractedHighOrderAIV, preIV)) + { + isValid = false; + } + + // Check the MLI against the actual length + int upperBound = paddedPlaintext.Length; + int lowerBound = upperBound - 8; + if (mli <= lowerBound) + { + isValid = false; + } + if (mli > upperBound) + { + isValid = false; + } + + // Check the number of padded zeros + var expectedZeros = upperBound - mli; + if (expectedZeros >= paddedPlaintext.Length) + { + isValid = false; + expectedZeros = paddedPlaintext.Length; + } + + byte[] zeros = new byte[expectedZeros]; + byte[] pad = new byte[expectedZeros]; + Array.Copy(paddedPlaintext, paddedPlaintext.Length - expectedZeros, pad, 0, expectedZeros); + if (!Arrays.ConstantTimeAreEqual(pad, zeros)) + { + isValid = false; + } + + if (!isValid) + { + throw new InvalidCipherTextException("checksum failed"); + } + + // Extract the plaintext from the padded plaintext + byte[] plaintext = new byte[mli]; + Array.Copy(paddedPlaintext, 0, plaintext, 0, plaintext.Length); + + return plaintext; + } + + /// + /// Performs steps 1 and 2 of the unwrap process defined in RFC 3394. + /// This code is duplicated from RFC3394WrapEngine because that class + /// will throw an error during unwrap because the IV won't match up. + /// + /// + /// + /// + /// Unwrapped data. + private byte[] Rfc3394UnwrapNoIvCheck(byte[] input, int inOff, int inLen) + { + byte[] iv = new byte[8]; + byte[] block = new byte[inLen - iv.Length]; + byte[] a = new byte[iv.Length]; + byte[] buf = new byte[8 + iv.Length]; + + Array.Copy(input, inOff, a, 0, iv.Length); + Array.Copy(input, inOff + iv.Length, block, 0, inLen - iv.Length); + + engine.Init(false, param); + + int n = inLen / 8; + n = n - 1; + + for (int j = 5; j >= 0; j--) + { + for (int i = n; i >= 1; i--) + { + Array.Copy(a, 0, buf, 0, iv.Length); + Array.Copy(block, 8 * (i - 1), buf, iv.Length, 8); + + int t = n * j + i; + for (int k = 1; t != 0; k++) + { + byte v = (byte)t; + + buf[iv.Length - k] ^= v; + + t = (int)((uint)t >> 8); + } + + engine.ProcessBlock(buf, 0, buf, 0); + Array.Copy(buf, 0, a, 0, 8); + Array.Copy(buf, 8, block, 8 * (i - 1), 8); + } + } + + // set the extracted AIV + extractedAIV = a; + + return block; + } + } +} diff --git a/crypto/test/UnitTests.csproj b/crypto/test/UnitTests.csproj index 5b39de6adc..ad08abc126 100644 --- a/crypto/test/UnitTests.csproj +++ b/crypto/test/UnitTests.csproj @@ -158,6 +158,7 @@ + diff --git a/crypto/test/src/crypto/test/AESWrapPadTest.cs b/crypto/test/src/crypto/test/AESWrapPadTest.cs new file mode 100644 index 0000000000..4546234fdf --- /dev/null +++ b/crypto/test/src/crypto/test/AESWrapPadTest.cs @@ -0,0 +1,244 @@ +using NUnit.Framework; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Utilities.Encoders; +using Org.BouncyCastle.Utilities.Test; +using System; + +namespace Org.BouncyCastle.Crypto.Tests +{ + [TestFixture] + class AesWrapPadTest : ITest + { + public string Name + { + get + { + return "AESWrapPad"; + } + } + + private ITestResult WrapTest( + int id, + byte[] kek, + byte[] inBytes, + byte[] outBytes) + { + IWrapper wrapper = new AesWrapPadEngine(); + + wrapper.Init(true, new KeyParameter(kek)); + + try + { + byte[] cText = wrapper.Wrap(inBytes, 0, inBytes.Length); + if (!Arrays.AreEqual(cText, outBytes)) + { + return new SimpleTestResult(false, Name + ": failed wrap test " + id + + " expected " + Hex.ToHexString(outBytes) + + " got " + Hex.ToHexString(cText)); + } + } + catch (Exception e) + { + return new SimpleTestResult(false, Name + ": failed wrap test exception " + e); + } + + wrapper.Init(false, new KeyParameter(kek)); + + try + { + byte[] pText = wrapper.Unwrap(outBytes, 0, outBytes.Length); + if (!Arrays.AreEqual(pText, inBytes)) + { + return new SimpleTestResult(false, Name + ": failed unwrap test " + id + + " expected " + Hex.ToHexString(inBytes) + + " got " + Hex.ToHexString(pText)); + } + } + catch (Exception e) + { + return new SimpleTestResult(false, Name + ": failed unwrap test exception.", e); + } + + return new SimpleTestResult(true, Name + ": Okay"); + } + + public ITestResult Perform() + { + // Message length is divided by 8 + + byte[] kek1 = Hex.Decode("000102030405060708090a0b0c0d0e0f"); + byte[] in1 = Hex.Decode("00112233445566778899aabbccddeeff"); + byte[] out1 = Hex.Decode("2cef0c9e30de26016c230cb78bc60d51b1fe083ba0c79cd5"); + ITestResult result = WrapTest(1, kek1, in1, out1); + + if (!result.IsSuccessful()) + { + return result; + } + + byte[] kek2 = Hex.Decode("000102030405060708090a0b0c0d0e0f1011121314151617"); + byte[] in2 = Hex.Decode("00112233445566778899aabbccddeeff"); + byte[] out2 = Hex.Decode("5fd7477fdc165910c8e5dd891a421b10db10362fd293b128"); + result = WrapTest(2, kek2, in2, out2); + if (!result.IsSuccessful()) + { + return result; + } + + byte[] kek3 = Hex.Decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); + byte[] in3 = Hex.Decode("00112233445566778899aabbccddeeff"); + byte[] out3 = Hex.Decode("afc860015ffe2d75bedf43c444fe58f4ad9d89c4ec71e23b"); + result = WrapTest(3, kek3, in3, out3); + if (!result.IsSuccessful()) + { + return result; + } + + byte[] kek4 = Hex.Decode("000102030405060708090a0b0c0d0e0f1011121314151617"); + byte[] in4 = Hex.Decode("00112233445566778899aabbccddeeff0001020304050607"); + byte[] out4 = Hex.Decode("39c3bf03c71e0d49bd968f26397b3855e5e89eaafd256edbc2f1d03f3266f3f4"); + result = WrapTest(4, kek4, in4, out4); + if (!result.IsSuccessful()) + { + return result; + } + + byte[] kek5 = Hex.Decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); + byte[] in5 = Hex.Decode("00112233445566778899aabbccddeeff0001020304050607"); + byte[] out5 = Hex.Decode("b9f05286f13fc80d1f8614a1acac931f293f66d7a3bb3811fb568f7108ec6210"); + result = WrapTest(5, kek5, in5, out5); + if (!result.IsSuccessful()) + { + return result; + } + + byte[] kek6 = Hex.Decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); + byte[] in6 = Hex.Decode("00112233445566778899aabbccddeeff000102030405060708090a0b0c0d0e0f"); + byte[] out6 = Hex.Decode("4a8029243027353b0694cf1bd8fc745bb0ce8a739b19b1960b12426d4c39cfeda926d103ab34e9f6"); + result = WrapTest(6, kek6, in6, out6); + if (!result.IsSuccessful()) + { + return result; + } + + // Message length is NOT divided by 8 (will be padded) + + byte[] kek7 = Hex.Decode("000102030405060708090a0b0c0d0e0f"); + byte[] in7 = Hex.Decode("00112233"); + byte[] out7 = Hex.Decode("9475a703b9ed66e4898e8154e66273a7"); + result = WrapTest(7, kek7, in7, out7); + if (!result.IsSuccessful()) + { + return result; + } + + byte[] kek8 = Hex.Decode("000102030405060708090a0b0c0d0e0f1011121314151617"); + byte[] in8 = Hex.Decode("00112233445566778899"); + byte[] out8 = Hex.Decode("62a641a96427fde579e81d6b9a9ea4fc9585d56736e3b74f"); + result = WrapTest(8, kek8, in8, out8); + if (!result.IsSuccessful()) + { + return result; + } + + byte[] kek9 = Hex.Decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); + byte[] in9 = Hex.Decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f001122334455"); + byte[] out9 = Hex.Decode("3b4d9aa29078180ccfe9c0b8b0775408a071ebf3d58842d8f14b26f55aa4e40e24d138b84023c7c24f2a065f853a59a5"); + result = WrapTest(9, kek9, in9, out9); + if (!result.IsSuccessful()) + { + return result; + } + + // RFC 5649 test vectors + byte[] kek10 = Hex.Decode("5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"); + byte[] in10 = Hex.Decode("c37b7e6492584340bed12207808941155068f738"); + byte[] out10 = Hex.Decode("138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a"); + result = WrapTest(10, kek10, in10, out10); + if (!result.IsSuccessful()) + { + return result; + } + + byte[] kek11 = Hex.Decode("5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"); + byte[] in11 = Hex.Decode("466f7250617369"); + byte[] out11 = Hex.Decode("afbeb0f07dfbf5419200f2ccb50bb24f"); + result = WrapTest(11, kek11, in11, out11); + if (!result.IsSuccessful()) + { + return result; + } + + IWrapper wrapper = new AesWrapPadEngine(); + KeyParameter key = new KeyParameter(new byte[16]); + byte[] buf = new byte[16]; + + try + { + wrapper.Init(true, key); + + wrapper.Unwrap(buf, 0, buf.Length); + + return new SimpleTestResult(false, Name + ": failed unwrap state test."); + } + catch (InvalidOperationException) + { + // expected + } + catch (InvalidCipherTextException e) + { + return new SimpleTestResult(false, Name + ": unexpected exception: " + e, e); + } + + try + { + wrapper.Init(false, key); + + wrapper.Wrap(buf, 0, buf.Length); + + return new SimpleTestResult(false, Name + ": failed unwrap state test."); + } + catch (InvalidOperationException) + { + // expected + } + + // + // short test + // + try + { + wrapper.Init(false, key); + + wrapper.Unwrap(buf, 0, buf.Length / 2); + + return new SimpleTestResult(false, Name + ": failed unwrap short test."); + } + catch (InvalidCipherTextException) + { + // expected + } + + return new SimpleTestResult(true, Name + ": Okay"); + } + + public static void Main() + { + var test = new AesWrapPadTest(); + ITestResult result = test.Perform(); + + Console.WriteLine(result); + } + + [Test] + public void TestFunction() + { + string resultText = Perform().ToString(); + + Assert.AreEqual(Name + ": Okay", resultText); + } + } +}