From 1fed749fc7329f495203bf320409410b00878087 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Wed, 26 Nov 2025 16:58:00 -0800 Subject: [PATCH 1/3] DGS-22964 Ensure all deps are strongly named --- ...Confluent.SchemaRegistry.Encryption.csproj | 13 +- .../Cryptor.cs | 2 +- .../KmsClients.cs | 2 +- .../LocalKmsClient.cs | 2 +- .../Vendored/HkdfStandard/Hkdf.cs | 130 ++++++++++ .../Vendored/HkdfStandard/LICENSE | 21 ++ .../Vendored/Miscreant/Aead.cs | 115 +++++++++ .../Vendored/Miscreant/AesCmac.cs | 142 +++++++++++ .../Vendored/Miscreant/AesCtr.cs | 165 ++++++++++++ .../Vendored/Miscreant/AesPmac.cs | 186 ++++++++++++++ .../Vendored/Miscreant/AesSiv.cs | 234 ++++++++++++++++++ .../Vendored/Miscreant/Constants.cs | 12 + .../Vendored/Miscreant/IMac.cs | 24 ++ .../Vendored/Miscreant/LICENSE.txt | 25 ++ .../Vendored/Miscreant/NonceEncoder.cs | 56 +++++ .../Vendored/Miscreant/StreamDecryptor.cs | 89 +++++++ .../Vendored/Miscreant/StreamEncryptor.cs | 95 +++++++ .../Vendored/Miscreant/Subtle.cs | 30 +++ .../Vendored/Miscreant/Utils.cs | 87 +++++++ .../Vendored/README.md | 85 +++++++ .../Confluent.SchemaRegistry.Rules.csproj | 2 +- 21 files changed, 1510 insertions(+), 7 deletions(-) create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/HkdfStandard/Hkdf.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/HkdfStandard/LICENSE create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Aead.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesCmac.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesCtr.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesPmac.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesSiv.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Constants.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/IMac.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/LICENSE.txt create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/NonceEncoder.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/StreamDecryptor.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/StreamEncryptor.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Subtle.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Utils.cs create mode 100644 src/Confluent.SchemaRegistry.Encryption/Vendored/README.md diff --git a/src/Confluent.SchemaRegistry.Encryption/Confluent.SchemaRegistry.Encryption.csproj b/src/Confluent.SchemaRegistry.Encryption/Confluent.SchemaRegistry.Encryption.csproj index 7e86df044..c907d7cc3 100644 --- a/src/Confluent.SchemaRegistry.Encryption/Confluent.SchemaRegistry.Encryption.csproj +++ b/src/Confluent.SchemaRegistry.Encryption/Confluent.SchemaRegistry.Encryption.csproj @@ -33,8 +33,8 @@ - - + + @@ -45,6 +45,13 @@ - + + + + + + + + diff --git a/src/Confluent.SchemaRegistry.Encryption/Cryptor.cs b/src/Confluent.SchemaRegistry.Encryption/Cryptor.cs index 747d64845..198a89347 100644 --- a/src/Confluent.SchemaRegistry.Encryption/Cryptor.cs +++ b/src/Confluent.SchemaRegistry.Encryption/Cryptor.cs @@ -3,7 +3,7 @@ using System.Security.Cryptography; using Google.Crypto.Tink; using Google.Protobuf; -using Miscreant; +using Confluent.SchemaRegistry.Encryption.Vendored.Miscreant; namespace Confluent.SchemaRegistry.Encryption { diff --git a/src/Confluent.SchemaRegistry.Encryption/KmsClients.cs b/src/Confluent.SchemaRegistry.Encryption/KmsClients.cs index d3c0b159c..39b0dc852 100644 --- a/src/Confluent.SchemaRegistry.Encryption/KmsClients.cs +++ b/src/Confluent.SchemaRegistry.Encryption/KmsClients.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Security.Cryptography; -using Miscreant; +using Confluent.SchemaRegistry.Encryption.Vendored.Miscreant; namespace Confluent.SchemaRegistry.Encryption { diff --git a/src/Confluent.SchemaRegistry.Encryption/LocalKmsClient.cs b/src/Confluent.SchemaRegistry.Encryption/LocalKmsClient.cs index 5b05dac89..c1dac4eae 100644 --- a/src/Confluent.SchemaRegistry.Encryption/LocalKmsClient.cs +++ b/src/Confluent.SchemaRegistry.Encryption/LocalKmsClient.cs @@ -1,5 +1,5 @@ using System; -using HkdfStandard; +using Confluent.SchemaRegistry.Encryption.Vendored.HkdfStandard; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/HkdfStandard/Hkdf.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/HkdfStandard/Hkdf.cs new file mode 100644 index 000000000..afae69a03 --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/HkdfStandard/Hkdf.cs @@ -0,0 +1,130 @@ +using System; +using System.Security.Cryptography; + +namespace Confluent.SchemaRegistry.Encryption.Vendored.HkdfStandard +{ + /// + /// Simplified HKDF implementation for use in the Confluent Schema Registry Encryption library. + /// This is a subset of the original HKDF.Standard library, containing only the methods needed. + /// Original source: https://github.com/andreimilto/HKDF.Standard + /// + public static class Hkdf + { + /// + /// Derives a key from the provided input key material using HKDF. + /// + /// The hash algorithm to use. + /// The input key material. + /// The desired length of the output key in bytes. + /// Optional salt value (defaults to hash output length of zeros if null). + /// Optional context information (defaults to empty if null). + /// The derived key. + public static byte[] DeriveKey(HashAlgorithmName hashAlgorithmName, byte[] ikm, int outputLength, byte[] salt = null, byte[] info = null) + { + if (ikm == null) + throw new ArgumentNullException(nameof(ikm)); + if (outputLength <= 0) + throw new ArgumentOutOfRangeException(nameof(outputLength)); + + // Extract + byte[] prk = Extract(hashAlgorithmName, ikm, salt); + + // Expand + return Expand(hashAlgorithmName, prk, outputLength, info); + } + + /// + /// HKDF Extract step - extracts a pseudorandom key from input key material. + /// + private static byte[] Extract(HashAlgorithmName hashAlgorithmName, byte[] ikm, byte[] salt) + { + int hashLength = GetHashLength(hashAlgorithmName); + + // Use a salt of HashLen zeros if not provided + if (salt == null || salt.Length == 0) + { + salt = new byte[hashLength]; + } + + using (var hmac = CreateHMAC(hashAlgorithmName, salt)) + { + return hmac.ComputeHash(ikm); + } + } + + /// + /// HKDF Expand step - expands the pseudorandom key to desired length. + /// + private static byte[] Expand(HashAlgorithmName hashAlgorithmName, byte[] prk, int outputLength, byte[] info) + { + int hashLength = GetHashLength(hashAlgorithmName); + + if (prk.Length < hashLength) + throw new ArgumentException("PRK must be at least HashLen bytes.", nameof(prk)); + + int n = (outputLength + hashLength - 1) / hashLength; // Ceiling division + if (n > 255) + throw new ArgumentOutOfRangeException(nameof(outputLength), "Output length too large."); + + if (info == null) + info = new byte[0]; + + byte[] okm = new byte[outputLength]; + byte[] t = new byte[0]; + int offset = 0; + + using (var hmac = CreateHMAC(hashAlgorithmName, prk)) + { + for (byte i = 1; i <= n; i++) + { + byte[] input = new byte[t.Length + info.Length + 1]; + Buffer.BlockCopy(t, 0, input, 0, t.Length); + Buffer.BlockCopy(info, 0, input, t.Length, info.Length); + input[input.Length - 1] = i; + + t = hmac.ComputeHash(input); + + int bytesToCopy = Math.Min(t.Length, outputLength - offset); + Buffer.BlockCopy(t, 0, okm, offset, bytesToCopy); + offset += bytesToCopy; + } + } + + return okm; + } + + /// + /// Gets the hash length in bytes for the specified algorithm. + /// + private static int GetHashLength(HashAlgorithmName hashAlgorithmName) + { + if (hashAlgorithmName == HashAlgorithmName.SHA1) + return 20; + if (hashAlgorithmName == HashAlgorithmName.SHA256) + return 32; + if (hashAlgorithmName == HashAlgorithmName.SHA384) + return 48; + if (hashAlgorithmName == HashAlgorithmName.SHA512) + return 64; + + throw new ArgumentOutOfRangeException(nameof(hashAlgorithmName), "Unsupported hash algorithm."); + } + + /// + /// Creates an HMAC instance for the specified algorithm with the given key. + /// + private static HMAC CreateHMAC(HashAlgorithmName hashAlgorithmName, byte[] key) + { + if (hashAlgorithmName == HashAlgorithmName.SHA1) + return new HMACSHA1(key); + if (hashAlgorithmName == HashAlgorithmName.SHA256) + return new HMACSHA256(key); + if (hashAlgorithmName == HashAlgorithmName.SHA384) + return new HMACSHA384(key); + if (hashAlgorithmName == HashAlgorithmName.SHA512) + return new HMACSHA512(key); + + throw new ArgumentOutOfRangeException(nameof(hashAlgorithmName), "Unsupported hash algorithm."); + } + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/HkdfStandard/LICENSE b/src/Confluent.SchemaRegistry.Encryption/Vendored/HkdfStandard/LICENSE new file mode 100644 index 000000000..c89db1535 --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/HkdfStandard/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Andrei Milto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Aead.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Aead.cs new file mode 100644 index 000000000..a48f5a68e --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Aead.cs @@ -0,0 +1,115 @@ +using System; +using System.Security.Cryptography; + +namespace Confluent.SchemaRegistry.Encryption.Vendored.Miscreant +{ + /// + /// The Aead class provides authenticated encryption with associated + /// data. This class provides a high-level interface to Miscreant's + /// misuse-resistant encryption. + /// + public sealed class Aead : IDisposable + { + private readonly AesSiv siv; + private bool disposed; + + private Aead(AesSiv siv) + { + this.siv = siv; + } + + /// + /// Generates a random nonce. + /// + /// Nonce size in bytes. + /// Generated nonce. + public static byte[] GenerateNonce(int size) + { + if (size < Constants.BlockSize) + { + throw new CryptographicException("Nonce size is too small."); + } + + return Utils.GetRandomBytes(size); + } + + /// + /// Generates a random 32-byte encryption key. + /// + /// Generated key. + public static byte[] GenerateKey256() + { + return Utils.GetRandomBytes(Constants.AesSiv256KeySize); + } + + /// + /// Generates a random 64-byte encryption key. + /// + /// Generated key. + public static byte[] GenerateKey512() + { + return Utils.GetRandomBytes(Constants.AesSiv512KeySize); + } + + /// + /// Initializes a new AEAD instance using the AES-CMAC-SIV algorithm. + /// + /// The secret key for AES-CMAC-SIV encryption. + /// An AEAD instance. + public static Aead CreateAesCmacSiv(byte[] key) + { + return new Aead(AesSiv.CreateAesCmacSiv(key)); + } + + /// + /// Initializes a new AEAD instance using the AES-PMAC-SIV algorithm. + /// + /// The secret key for AES-PMAC-SIV encryption. + /// An AEAD instance. + public static Aead CreateAesPmacSiv(byte[] key) + { + return new Aead(AesSiv.CreateAesPmacSiv(key)); + } + + /// + /// Seal encrypts and authenticates plaintext, authenticates + /// the associated data, and returns the result. + /// + /// The plaintext to encrypt. + /// The nonce for encryption. + /// Associated data to authenticate. + /// Concatenation of the authentication tag and the encrypted data. + public byte[] Seal(byte[] plaintext, byte[] nonce = null, byte[] data = null) + { + return siv.Seal(plaintext, data, nonce); + } + + /// + /// Open decrypts ciphertext, authenticates the decrypted plaintext + /// and the associated data and, if successful, returns the result. + /// In case of failed decryption, this method throws + /// . + /// + /// The ciphertext to decrypt. + /// The nonce for encryption. + /// Associated data to authenticate. + /// The decrypted plaintext. + /// Thrown when the ciphertext is invalid. + public byte[] Open(byte[] ciphertext, byte[] nonce = null, byte[] data = null) + { + return siv.Open(ciphertext, data, nonce); + } + + /// + /// Disposes this object. + /// + public void Dispose() + { + if (!disposed) + { + siv.Dispose(); + disposed = true; + } + } + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesCmac.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesCmac.cs new file mode 100644 index 000000000..1527b46a1 --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesCmac.cs @@ -0,0 +1,142 @@ +using System; +using System.Linq; +using System.Security.Cryptography; + +namespace Confluent.SchemaRegistry.Encryption.Vendored.Miscreant +{ + /// + /// CMAC message authentication code, defined in NIST Special Publication + /// SP 800-38B. + /// + public sealed class AesCmac : IMac + { + private const int BlockSize = Constants.BlockSize; + private const int BufferSize = 4096; + private static readonly byte[] Zero = new byte[BlockSize]; + + private readonly Aes aes; + private readonly ICryptoTransform encryptor; + private readonly byte[] buffer = new byte[BufferSize]; + private readonly byte[] K1 = new byte[BlockSize]; + private readonly byte[] K2 = new byte[BlockSize]; + private int position; + private bool disposed; + + /// + /// Initializes a new instance of the class with the specified key. + /// + /// The secret key for authentication. + public AesCmac(byte[] key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + using (var aes = Utils.CreateAes(CipherMode.ECB)) + using (var encryptor = aes.CreateEncryptor(key, null)) + { + encryptor.TransformBlock(Zero, 0, BlockSize, K1, 0); + Utils.Multiply(K1); + + Array.Copy(K1, K2, BlockSize); + Utils.Multiply(K2); + } + + aes = Utils.CreateAes(CipherMode.CBC); + encryptor = aes.CreateEncryptor(key, Zero); + } + + internal static IMac Create(byte[] key) + { + return new AesCmac(key); + } + + /// + /// Adds more data to the running hash. + /// + /// The input to hash. + /// The offset into the input byte array from which to begin using data. + /// The number of bytes in the input byte array to use as data. + public void HashCore(byte[] input, int index, int size) + { + if (disposed) + { + throw new ObjectDisposedException(nameof(AesCmac)); + } + + var seg = new ArraySegment(input, index, size); + var left = BlockSize - position; + + if (position > 0 && seg.Count > left) + { + Array.Copy(seg.Array, seg.Offset, buffer, position, left); + encryptor.TransformBlock(buffer, 0, BlockSize, buffer, 0); + seg = seg.Slice(left); + position = 0; + } + + while (seg.Count > BlockSize) + { + // Encrypting single block in .NET is extremely slow, so we want + // to encrypt as much of the input as possible in a single call to + // TransformBlock. TransformBlock expects valid output buffer, so + // we pre-allocate 4KB buffer for this purpose. + + int count = Math.Min(BufferSize, (seg.Count - 1) / BlockSize * BlockSize); + encryptor.TransformBlock(seg.Array, seg.Offset, count, buffer, 0); + seg = seg.Slice(count); + } + + if (seg.Count > 0) + { + Array.Copy(seg.Array, seg.Offset, buffer, position, seg.Count); + position += seg.Count; + } + } + + /// + /// Returns the current hash and resets the hash state. + /// + /// The value of the computed hash. + public byte[] HashFinal() + { + if (disposed) + { + throw new ObjectDisposedException(nameof(AesCmac)); + } + + if (position == BlockSize) + { + Utils.Xor(K1, buffer, BlockSize); + } + else + { + Utils.Pad(buffer, position); + Utils.Xor(K2, buffer, BlockSize); + } + + position = 0; + + return encryptor.TransformFinalBlock(buffer, 0, BlockSize); + } + + /// + /// Disposes this object. + /// + public void Dispose() + { + if (!disposed) + { + aes.Dispose(); + encryptor.Dispose(); + + Array.Clear(buffer, 0, BufferSize); + Array.Clear(K1, 0, BlockSize); + Array.Clear(K2, 0, BlockSize); + + disposed = true; + } + } + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesCtr.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesCtr.cs new file mode 100644 index 000000000..2c14ba139 --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesCtr.cs @@ -0,0 +1,165 @@ +using System; +using System.Security.Cryptography; + +namespace Confluent.SchemaRegistry.Encryption.Vendored.Miscreant +{ + /// + /// Counter (CTR) mode, defined in NIST Special Publication + /// SP 800-38A. + /// + internal sealed class AesCtr : IDisposable + { + private const int BlockSize = Constants.BlockSize; + private const int KeyStreamBufferSize = 4096; + + private readonly Aes aes; + private readonly ICryptoTransform encryptor; + private byte[] counter; + private ArraySegment keyStream; + private bool disposed; + + /// + /// Initializes a new instance of the class with the specified key and initialization vector. + /// + /// The secret key for encryption. + /// The initialization vector for encryption. + public AesCtr(byte[] key, byte[] iv) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (iv == null) + { + throw new ArgumentNullException(nameof(iv)); + } + + if (iv.Length != BlockSize) + { + throw new CryptographicException("Specified initialization vector (IV) does not match the block size for this algorithm."); + } + + aes = Aes.Create(); + aes.Mode = CipherMode.ECB; + + encryptor = aes.CreateEncryptor(key, null); + counter = (byte[])iv.Clone(); + + var buffer = new byte[KeyStreamBufferSize]; + keyStream = new ArraySegment(buffer, 0, 0); + } + + /// + /// Initializes a new instance of the class with the + /// specified key. For internal use only. The initialization vector will + /// be set later by the object. + /// + /// The secret key for encryption. + internal AesCtr(byte[] key) + { + aes = Aes.Create(); + aes.Mode = CipherMode.ECB; + + encryptor = aes.CreateEncryptor(key, null); + + var buffer = new byte[KeyStreamBufferSize]; + keyStream = new ArraySegment(buffer, 0, 0); + } + + /// + /// Encrypt/decrypt the input by xoring it with the CTR keystream. + /// + /// The input to encrypt. + /// The offset into the input byte array from which to begin using data. + /// The number of bytes in the input byte array to use as data. + /// The output to which to write the encrypted data. + /// The offset into the output byte array from which to begin writing data. + public void Encrypt(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) + { + if (disposed) + { + throw new ObjectDisposedException(nameof(AesCtr)); + } + + var inputSeg = new ArraySegment(input, inputOffset, inputCount); + var outputSeg = new ArraySegment(output, outputOffset, inputCount); + + while (inputSeg.Count > 0) + { + if (keyStream.Count == 0) + { + GenerateKeyStream(inputSeg.Count); + } + + int count = Math.Min(inputSeg.Count, keyStream.Count); + int keyStreamPosition = keyStream.Offset; + int inputPosition = inputSeg.Offset; + int outputPosition = outputSeg.Offset; + + for (int i = 0; i < count; ++i) + { + byte c = (byte)(keyStream.Array[keyStreamPosition + i] ^ input[inputPosition + i]); + output[outputPosition + i] = c; + } + + keyStream = keyStream.Slice(count); + inputSeg = inputSeg.Slice(count); + outputSeg = outputSeg.Slice(count); + } + } + + /// + /// Reset the initialization vector. For internal use only. This + /// method is needed in order to avoid creating heavyweight + /// object every time we call + /// or methods. + /// + /// The initialization vector for encryption. + internal void Reset(byte[] iv) + { + counter = iv; + keyStream = new ArraySegment(keyStream.Array, 0, 0); + } + + private void GenerateKeyStream(int inputCount) + { + int size = Math.Min(KeyStreamBufferSize, Utils.Ceil(inputCount, BlockSize) * BlockSize); + byte[] array = keyStream.Array; + + for (int i = 0; i < size; i += BlockSize) + { + Array.Copy(counter, 0, array, i, BlockSize); + IncrementCounter(); + } + + encryptor.TransformBlock(array, 0, size, array, 0); + keyStream = new ArraySegment(array, 0, size); + } + + private void IncrementCounter() + { + for (int i = BlockSize - 1; i >= 0; --i) + { + if (++counter[i] != 0) + { + break; + } + } + } + + public void Dispose() + { + if (!disposed) + { + aes.Dispose(); + encryptor.Dispose(); + + Array.Clear(counter, 0, BlockSize); + Array.Clear(keyStream.Array, 0, KeyStreamBufferSize); + + disposed = true; + } + } + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesPmac.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesPmac.cs new file mode 100644 index 000000000..8e1ab7df7 --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesPmac.cs @@ -0,0 +1,186 @@ +using System; +using System.Linq; +using System.Security.Cryptography; + +namespace Confluent.SchemaRegistry.Encryption.Vendored.Miscreant +{ + /// + /// PMAC message authentication code, defined in the paper + /// + /// A Block-Cipher Mode of Operation for Parallelizable Message Authentication + /// . + /// + public sealed class AesPmac : IMac + { + private const int BlockSize = Constants.BlockSize; + private const int BufferSize = 4096; + + private readonly Aes aes; + private readonly ICryptoTransform encryptor; + private readonly byte[][] l = new byte[31][]; + private readonly byte[] inv; + private readonly byte[] buffer = new byte[BufferSize]; + private readonly byte[] offset = new byte[BlockSize]; + private readonly byte[] sum = new byte[BlockSize]; + private uint counter; + private int position; + private bool disposed; + + /// + /// Initializes a new instance of the class with the specified key. + /// + /// The secret key for authentication. + public AesPmac(byte[] key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + aes = Utils.CreateAes(CipherMode.ECB); + encryptor = aes.CreateEncryptor(key, null); + + byte[] temp = new byte[BlockSize]; + encryptor.TransformBlock(temp, 0, BlockSize, temp, 0); + + for (int i = 0; i < l.Length; ++i) + { + l[i] = (byte[])temp.Clone(); + Utils.Multiply(temp); + } + + inv = (byte[])l[0].Clone(); + int lastBit = inv[BlockSize - 1] & 1; + + for (int i = BlockSize - 1; i > 0; --i) + { + int carry = Subtle.ConstantTimeSelect(inv[i - 1] & 1, 0x80, 0); + inv[i] = (byte)((inv[i] >> 1) | carry); + } + + inv[0] >>= 1; + inv[0] ^= (byte)Subtle.ConstantTimeSelect(lastBit, 0x80, 0); + inv[BlockSize - 1] ^= (byte)Subtle.ConstantTimeSelect(lastBit, Constants.R >> 1, 0); + } + + internal static IMac Create(byte[] key) + { + return new AesPmac(key); + } + + /// + /// Adds more data to the running hash. + /// + /// The input to hash. + /// The offset into the input byte array from which to begin using data. + /// The number of bytes in the input byte array to use as data. + public void HashCore(byte[] input, int index, int size) + { + if (disposed) + { + throw new ObjectDisposedException(nameof(AesCmac)); + } + + var seg = new ArraySegment(input, index, size); + var left = BlockSize - position; + + if (position > 0 && seg.Count > left) + { + Array.Copy(seg.Array, seg.Offset, buffer, position, left); + ProcessBuffer(BlockSize); + seg = seg.Slice(left); + position = 0; + } + + while (seg.Count > BlockSize) + { + // Encrypting single block in .NET is extremely slow, so we want + // to encrypt as much of the input as possible in a single call to + // TransformBlock. TransformBlock expects valid output buffer, so + // we pre-allocate 4KB buffer for this purpose. + + int count = Math.Min(BufferSize, (seg.Count - 1) / BlockSize * BlockSize); + Array.Copy(seg.Array, seg.Offset, buffer, position, count); + ProcessBuffer(count); + seg = seg.Slice(count); + } + + if (seg.Count > 0) + { + Array.Copy(seg.Array, seg.Offset, buffer, position, seg.Count); + position += seg.Count; + } + } + + /// + /// Returns the current hash and resets the hash state. + /// + /// The value of the computed hash. + public byte[] HashFinal() + { + if (disposed) + { + throw new ObjectDisposedException(nameof(AesCmac)); + } + + if (position == BlockSize) + { + Utils.Xor(buffer, sum, BlockSize); + Utils.Xor(inv, sum, BlockSize); + } + else + { + Utils.Pad(buffer, position); + Utils.Xor(buffer, sum, BlockSize); + } + + byte[] result = encryptor.TransformFinalBlock(sum, 0, BlockSize); + + Array.Clear(offset, 0, BlockSize); + Array.Clear(sum, 0, BlockSize); + + counter = 0; + position = 0; + + return result; + } + + private void ProcessBuffer(int size) + { + for (int i = 0; i < size; i += BlockSize) + { + int trailingZeros = Utils.TrailingZeros(counter + 1); + + Utils.Xor(l[trailingZeros], offset, BlockSize); + Utils.Xor(offset, 0, buffer, i, BlockSize); + + ++counter; + } + + encryptor.TransformBlock(buffer, 0, size, buffer, 0); + + for (int i = 0; i < size; i += BlockSize) + { + Utils.Xor(buffer, i, sum, 0, BlockSize); + } + } + + /// + /// Disposes this object. + /// + public void Dispose() + { + if (!disposed) + { + aes.Dispose(); + encryptor.Dispose(); + + Array.Clear(buffer, 0, BufferSize); + Array.Clear(offset, 0, BlockSize); + Array.Clear(sum, 0, BlockSize); + + disposed = true; + } + } + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesSiv.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesSiv.cs new file mode 100644 index 000000000..1fae239b4 --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesSiv.cs @@ -0,0 +1,234 @@ +using System; +using System.Security.Cryptography; + +namespace Confluent.SchemaRegistry.Encryption.Vendored.Miscreant +{ + /// + /// AES-SIV authenticated encryption mode, defined in + /// RFC 5297. + /// + public sealed class AesSiv : IDisposable + { + private const int BlockSize = Constants.BlockSize; + private const int MaxAssociatedDataItems = 126; + + private static readonly byte[] Empty = new byte[0]; + private static readonly byte[] Zero = new byte[BlockSize]; + + private readonly IMac mac; + private readonly AesCtr ctr; + private bool disposed; + + private AesSiv(Func macFactory, byte[] key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (key.Length != Constants.AesSiv256KeySize && key.Length != Constants.AesSiv512KeySize) + { + throw new CryptographicException("Specified key is not a valid size for this algorithm."); + } + + int halfKeySize = key.Length / 2; + + var K1 = new byte[halfKeySize]; + var K2 = new byte[halfKeySize]; + + Array.Copy(key, 0, K1, 0, halfKeySize); + Array.Copy(key, halfKeySize, K2, 0, halfKeySize); + + mac = macFactory(K1); + ctr = new AesCtr(K2); + } + + /// + /// Initializes a new instance of the AES-CMAC-SIV algorithm with the specified key. + /// + /// The secret key for AES-CMAC-SIV encryption. + /// An AES-CMAC-SIV instance. + public static AesSiv CreateAesCmacSiv(byte[] key) + { + return new AesSiv(AesCmac.Create, key); + } + + /// + /// Initializes a new instance of the AES-PMAC-SIV algorithm with the specified key. + /// + /// The secret key for AES-PMAC-SIV encryption. + /// An AES-PMAC-SIV instance. + public static AesSiv CreateAesPmacSiv(byte[] key) + { + return new AesSiv(AesPmac.Create, key); + } + + /// + /// Seal encrypts and authenticates plaintext, authenticates the given + /// associated data items, and returns the result. For nonce-based + /// encryption, the nonce should be the last associated data item. + /// + /// The plaintext to encrypt. + /// Associated data items to authenticate. + /// Concatenation of the authentication tag and the encrypted data. + public byte[] Seal(byte[] plaintext, params byte[][] data) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + if (data.Length > MaxAssociatedDataItems) + { + throw new CryptographicException($"Maximum number of associated data items is {MaxAssociatedDataItems}"); + } + + if (plaintext == null) + { + plaintext = Empty; + } + + byte[] iv = S2V(data, plaintext); + byte[] output = new byte[iv.Length + plaintext.Length]; + + Array.Copy(iv, output, iv.Length); + ZeroIvBits(iv); + + ctr.Reset(iv); + ctr.Encrypt(plaintext, 0, plaintext.Length, output, iv.Length); + + return output; + } + + /// + /// Open decrypts ciphertext, authenticates the decrypted plaintext + /// and the given associated data items and, if successful, returns + /// the result. For nonce-based encryption, the nonce should be the + /// last associated data item. In case of failed decryption, this + /// method throws . + /// + /// The ciphertext to decrypt. + /// Associated data items to authenticate. + /// The decrypted plaintext. + /// Thrown when the ciphertext is invalid. + public byte[] Open(byte[] ciphertext, params byte[][] data) + { + if (ciphertext == null) + { + throw new ArgumentNullException(nameof(ciphertext)); + } + + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + if (ciphertext.Length < BlockSize) + { + throw new CryptographicException("Malformed or corrupt ciphertext."); + } + + if (data.Length > MaxAssociatedDataItems) + { + throw new CryptographicException($"Maximum number of associated data items is {MaxAssociatedDataItems}"); + } + + byte[] iv = new byte[BlockSize]; + byte[] output = new byte[ciphertext.Length - iv.Length]; + + Array.Copy(ciphertext, 0, iv, 0, BlockSize); + ZeroIvBits(iv); + + ctr.Reset(iv); + ctr.Encrypt(ciphertext, BlockSize, output.Length, output, 0); + + byte[] v = S2V(data, output); + + if (!Subtle.ConstantTimeEquals(ciphertext, v, BlockSize)) + { + throw new CryptographicException("Malformed or corrupt ciphertext."); + } + + return output; + } + + /// + /// S2V operation, defined in the section 2.4 of + /// RFC 5297. + /// + private byte[] S2V(byte[][] headers, byte[] message) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + // The standalone S2V returns CMAC(1) if the number of + // passed vectors is zero, however in SIV contruction + // this case is never triggered, since we always pass + // plaintext as the last vector (even if it's zero-length), + // so we omit this case. + + mac.HashCore(Zero, 0, BlockSize); + byte[] v = mac.HashFinal(); + + foreach (var header in headers) + { + if (header == null) + { + continue; + } + + mac.HashCore(header, 0, header.Length); + Utils.Multiply(v); + Utils.Xor(mac.HashFinal(), v, BlockSize); + } + + if (message.Length >= BlockSize) + { + int n = message.Length - BlockSize; + + mac.HashCore(message, 0, n); + Utils.Xor(message, n, v, 0, BlockSize); + mac.HashCore(v, 0, BlockSize); + + return mac.HashFinal(); + } + + byte[] padded = new byte[BlockSize]; + + Array.Copy(message, padded, message.Length); + Utils.Multiply(v); + Utils.Pad(padded, message.Length); + Utils.Xor(padded, v, BlockSize); + mac.HashCore(v, 0, BlockSize); + + return mac.HashFinal(); + } + + private void ZeroIvBits(byte[] iv) + { + iv[iv.Length - 8] &= 0x7f; + iv[iv.Length - 4] &= 0x7f; + } + + /// + /// Disposes this object. + /// + public void Dispose() + { + if (!disposed) + { + mac.Dispose(); + ctr.Dispose(); + + disposed = true; + } + } + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Constants.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Constants.cs new file mode 100644 index 000000000..33db049c4 --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Constants.cs @@ -0,0 +1,12 @@ +namespace Confluent.SchemaRegistry.Encryption.Vendored.Miscreant +{ + internal static class Constants + { + public const int BlockSize = 16; + public const int R = 0x87; + public const int AesSiv256KeySize = 32; + public const int AesSiv512KeySize = 64; + public const int StreamNonceSize = 8; + public const int StreamCounterSize = 4; + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/IMac.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/IMac.cs new file mode 100644 index 000000000..9f3348aa1 --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/IMac.cs @@ -0,0 +1,24 @@ +using System; + +namespace Confluent.SchemaRegistry.Encryption.Vendored.Miscreant +{ + /// + /// Defines the basic operations of message authentication code. + /// + internal interface IMac : IDisposable + { + /// + /// Adds more data to the running hash. + /// + /// The input to hash. + /// The offset into the input byte array from which to begin using data. + /// The number of bytes in the input byte array to use as data. + void HashCore(byte[] input, int index, int size); + + /// + /// Returns the current hash and resets the hash state. + /// + /// The value of the computed hash. + byte[] HashFinal(); + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/LICENSE.txt b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/LICENSE.txt new file mode 100644 index 000000000..90ea836a6 --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/LICENSE.txt @@ -0,0 +1,25 @@ +Copyright (c) 2017-2018 The Miscreant Developers. The canonical list of project +contributors who hold copyright over the project can be found at: + +https://github.com/miscreant/miscreant/blob/master/AUTHORS.md + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/NonceEncoder.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/NonceEncoder.cs new file mode 100644 index 000000000..acd040a1e --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/NonceEncoder.cs @@ -0,0 +1,56 @@ +using System; +using System.Security.Cryptography; + +namespace Confluent.SchemaRegistry.Encryption.Vendored.Miscreant +{ + internal class NonceEncoder + { + private const int NonceSize = Constants.StreamNonceSize; + + private readonly byte[] nonce; + private uint counter; + + public NonceEncoder(byte[] nonce) + { + if (nonce == null) + { + throw new ArgumentNullException(nameof(nonce)); + } + + if (nonce.Length != NonceSize) + { + throw new CryptographicException("Specified nonce does not match the nonce size for this algorithm."); + } + + this.nonce = new byte[NonceSize + Constants.StreamCounterSize + 1]; + Array.Copy(nonce, this.nonce, NonceSize); + } + + public byte[] Next(bool last) + { + nonce[NonceSize] = (byte)((counter >> 24) & 0xff); + nonce[NonceSize + 1] = (byte)((counter >> 16) & 0xff); + nonce[NonceSize + 2] = (byte)((counter >> 8) & 0xff); + nonce[NonceSize + 3] = (byte)(counter & 0xff); + + if (last) + { + nonce[nonce.Length - 1] = 1; + } + + try + { + checked + { + ++counter; + } + } + catch (OverflowException ex) + { + throw new CryptographicException("STREAM counter overflowed.", ex); + } + + return nonce; + } + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/StreamDecryptor.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/StreamDecryptor.cs new file mode 100644 index 000000000..739f7384f --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/StreamDecryptor.cs @@ -0,0 +1,89 @@ +using System; +using System.Security.Cryptography; + +namespace Confluent.SchemaRegistry.Encryption.Vendored.Miscreant +{ + /// + /// STREAM online authenticated encryption, defined in the paper + /// + /// Online Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance + /// . + /// + public sealed class StreamDecryptor : IDisposable + { + private readonly AesSiv siv; + private readonly NonceEncoder nonce; + private bool finished; + private bool disposed; + + private StreamDecryptor(AesSiv siv, byte[] nonce) + { + this.siv = siv; + this.nonce = new NonceEncoder(nonce); + } + + /// + /// Initializes a new instance of the STREAM decryptor using the AES-CMAC-SIV algorithm. + /// + /// The secret key for decryption. + /// The nonce for decryption. + /// A STREAM decryptor instance. + public static StreamDecryptor CreateAesCmacSivDecryptor(byte[] key, byte[] nonce) + { + var siv = AesSiv.CreateAesCmacSiv(key); + return new StreamDecryptor(siv, nonce); + } + + /// + /// Initializes a new instance of the STREAM decryptor using the AES-PMAC-SIV algorithm. + /// + /// The secret key for decryption. + /// The nonce for decryption. + /// A STREAM decryptor instance. + public static StreamDecryptor CreateAesPmacSivDecryptor(byte[] key, byte[] nonce) + { + var siv = AesSiv.CreateAesPmacSiv(key); + return new StreamDecryptor(siv, nonce); + } + + /// + /// Open decrypts the next ciphertext in the STREAM, authenticates the + /// decrypted plaintext and the associated data and, if successful, returns + /// the result. In case of failed decryption, this method throws + /// . + /// + /// The ciphertext to decrypt. + /// Associated data items to authenticate. + /// True if this is the last block in the STREAM. + /// The decrypted plaintext. + /// Thrown when the ciphertext is invalid. + public byte[] Open(byte[] ciphertext, byte[] data = null, bool last = false) + { + if (disposed) + { + throw new ObjectDisposedException(nameof(StreamEncryptor)); + } + + if (finished) + { + throw new CryptographicException("STREAM is already finished."); + } + + finished = last; + + return siv.Open(ciphertext, data, nonce.Next(last)); + } + + /// + /// Disposes this object. + /// + public void Dispose() + { + if (!disposed) + { + siv.Dispose(); + disposed = true; + } + } + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/StreamEncryptor.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/StreamEncryptor.cs new file mode 100644 index 000000000..27aeb15a5 --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/StreamEncryptor.cs @@ -0,0 +1,95 @@ +using System; +using System.Security.Cryptography; + +namespace Confluent.SchemaRegistry.Encryption.Vendored.Miscreant +{ + /// + /// STREAM online authenticated encryption, defined in the paper + /// + /// Online Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance + /// . + /// + public sealed class StreamEncryptor : IDisposable + { + private readonly AesSiv siv; + private readonly NonceEncoder nonce; + private bool finished; + private bool disposed; + + private StreamEncryptor(AesSiv siv, byte[] nonce) + { + this.siv = siv; + this.nonce = new NonceEncoder(nonce); + } + + /// + /// Generates a random 8-byte STREAM nonce. + /// + /// Generated nonce. + public static byte[] GenerateNonce() + { + return Utils.GetRandomBytes(Constants.StreamNonceSize); + } + + /// + /// Initializes a new instance of the STREAM encryptor using the AES-CMAC-SIV algorithm. + /// + /// The secret key for encryption. + /// The nonce for encryption. + /// A STREAM encryptor instance. + public static StreamEncryptor CreateAesCmacSivEncryptor(byte[] key, byte[] nonce) + { + var siv = AesSiv.CreateAesCmacSiv(key); + return new StreamEncryptor(siv, nonce); + } + + /// + /// Initializes a new instance of the STREAM encryptor using the AES-PMAC-SIV algorithm. + /// + /// The secret key for encryption. + /// The nonce for encryption. + /// A STREAM encryptor instance. + public static StreamEncryptor CreateAesPmacSivEncryptor(byte[] key, byte[] nonce) + { + var siv = AesSiv.CreateAesPmacSiv(key); + return new StreamEncryptor(siv, nonce); + } + + /// + /// Seal encrypts and authenticates the next message in the STREAM, + /// authenticates the associated data, and returns the result. + /// + /// The plaintext to encrypt. + /// Associated data items to authenticate. + /// True if this is the last block in the STREAM. + /// Concatenation of the authentication tag and the encrypted data. + public byte[] Seal(byte[] plaintext, byte[] data = null, bool last = false) + { + if (disposed) + { + throw new ObjectDisposedException(nameof(StreamEncryptor)); + } + + if (finished) + { + throw new CryptographicException("STREAM is already finished."); + } + + finished = last; + + return siv.Seal(plaintext, data, nonce.Next(last)); + } + + /// + /// Disposes this object. + /// + public void Dispose() + { + if (!disposed) + { + siv.Dispose(); + disposed = true; + } + } + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Subtle.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Subtle.cs new file mode 100644 index 000000000..fbec1a3d1 --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Subtle.cs @@ -0,0 +1,30 @@ +using System.Runtime.CompilerServices; + +namespace Confluent.SchemaRegistry.Encryption.Vendored.Miscreant +{ + internal static class Subtle + { + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static bool ConstantTimeEquals(byte[] x, byte[] y, int count) + { + byte result = 0; + + for (int i = 0; i < count; ++i) + { + result |= (byte)(x[i] ^ y[i]); + } + + return result == 0; + } + + /// + /// ConstantTimeSelect returns x if v is 1 and y if v is 0. + /// See constant_time.go for more details. + /// + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static int ConstantTimeSelect(int v, int x, int y) + { + return ~(v - 1) & x | (v - 1) & y; + } + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Utils.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Utils.cs new file mode 100644 index 000000000..6efdb857d --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/Utils.cs @@ -0,0 +1,87 @@ +using System; +using System.Diagnostics; +using System.Security.Cryptography; + +namespace Confluent.SchemaRegistry.Encryption.Vendored.Miscreant +{ + internal static class Utils + { + private static readonly byte[] deBruijn = new byte[] { + 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, + 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9 + }; + + private static readonly RandomNumberGenerator random = RandomNumberGenerator.Create(); + + public static ArraySegment Slice(this ArraySegment seg, int index) + { + return new ArraySegment(seg.Array, seg.Offset + index, seg.Count - index); + } + + public static void Multiply(byte[] input) + { + Debug.Assert(input.Length == Constants.BlockSize); + + int carry = input[0] >> 7; + + for (int i = 0; i < Constants.BlockSize - 1; ++i) + { + input[i] = (byte)((input[i] << 1) | (input[i + 1] >> 7)); + } + + byte last = input[Constants.BlockSize - 1]; + input[Constants.BlockSize - 1] = (byte)((last << 1) ^ Subtle.ConstantTimeSelect(carry, Constants.R, 0)); + } + + public static void Xor(byte[] source, byte[] destination, int length) + { + Xor(source, 0, destination, 0, length); + } + + public static void Xor(byte[] source, int sourceIndex, byte[] destination, int destinationIndex, int length) + { + for (int i = 0; i < length; ++i) + { + destination[destinationIndex + i] ^= source[sourceIndex + i]; + } + } + + public static void Pad(byte[] buffer, int position) + { + buffer[position] = 0x80; + + for (int i = position + 1; i < Constants.BlockSize; ++i) + { + buffer[i] = 0; + } + } + + public static int Ceil(int dividend, int divisor) + { + return (dividend + divisor - 1) / divisor; + } + + public static byte[] GetRandomBytes(int size) + { + var bytes = new byte[size]; + random.GetBytes(bytes); + + return bytes; + } + + public static Aes CreateAes(CipherMode mode) + { + var aes = Aes.Create(); + + aes.Mode = mode; + aes.Padding = PaddingMode.None; + + return aes; + } + + public static int TrailingZeros(uint x) + { + return x > 0 ? deBruijn[(uint)((x & -x) * 0x077CB531) >> (32 - 5)] : 32; + } + } +} diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/README.md b/src/Confluent.SchemaRegistry.Encryption/Vendored/README.md new file mode 100644 index 000000000..1572d1e10 --- /dev/null +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/README.md @@ -0,0 +1,85 @@ +# Vendored Dependencies for Strong Naming + +## Overview + +The `Confluent.SchemaRegistry.Encryption` project requires all dependencies to be strongly named to maintain assembly signing compatibility. Two NuGet dependencies (`Miscreant` and `HKDF.Standard`) were not strongly named, so their source code has been vendored (copied) into this project. + +## Vendored Libraries + +### 1. Miscreant (v0.3.3) +- **Original Repository**: https://github.com/miscreant/miscreant.net +- **License**: MIT (see LICENSE.txt) +- **Purpose**: Provides AES-SIV (Synthetic Initialization Vector) encryption +- **Vendored Location**: `Vendored/Miscreant/` +- **Files Copied**: + - Aead.cs + - AesCmac.cs + - AesCtr.cs + - AesPmac.cs + - AesSiv.cs + - Constants.cs + - IMac.cs + - NonceEncoder.cs + - StreamDecryptor.cs + - StreamEncryptor.cs + - Subtle.cs + - Utils.cs + - LICENSE.txt + +- **Modifications**: + - Changed namespace from `Miscreant` to `Confluent.SchemaRegistry.Encryption.Vendored.Miscreant` + - Removed `AssemblyInfo.cs` (not needed) + +### 2. HKDF.Standard (v2.0.0) +- **Original Repository**: https://github.com/andreimilto/HKDF.Standard +- **License**: MIT (see LICENSE) +- **Purpose**: HMAC-based Extract-and-Expand Key Derivation Function (HKDF) +- **Vendored Location**: `Vendored/HkdfStandard/` +- **Files Copied**: + - Hkdf.cs (simplified implementation) + - LICENSE + +- **Modifications**: + - Changed namespace from `HkdfStandard` to `Confluent.SchemaRegistry.Encryption.Vendored.HkdfStandard` + - **Simplified implementation**: The original library had complex conditional compilation for different .NET versions and Span support. Since the Confluent.SchemaRegistry.Encryption project only uses the `Hkdf.DeriveKey()` method, a simplified implementation was created that: + - Implements only the `DeriveKey()` method + - Works across all target frameworks (netstandard2.1, net462, net6.0, net8.0) + - Uses standard byte[] arrays instead of Span for compatibility + - Maintains full RFC 5869 compliance for the DeriveKey operation + +## Usage in Project + +### Miscreant +Used in: +- `Cryptor.cs` - For AES-SIV encryption/decryption operations +- `KmsClients.cs` - For cryptographic operations + +### HKDF.Standard +Used in: +- `LocalKmsClient.cs` - For key derivation from secrets + +## Project Configuration + +The `Confluent.SchemaRegistry.Encryption.csproj` file has been updated to: +1. Remove NuGet package references for `Miscreant` and `HKDF.Standard` +2. Automatically include all C# files in the `Vendored/` directory (via .NET SDK auto-inclusion) +3. Include license files in the NuGet package + +## Attribution + +Both libraries are used under the MIT License and are attributed to their original authors: +- **Miscreant**: Copyright (c) 2017-2018 The Miscreant Developers +- **HKDF.Standard**: Original implementation by andreimilto + +## Maintenance Notes + +- These vendored libraries should be updated periodically by pulling the latest source from their respective repositories +- When updating, ensure to: + 1. Update the namespace references + 2. Test compilation across all target frameworks + 3. Update version information in this README + 4. For HKDF, maintain the simplified implementation unless additional methods are needed + +--- +Last Updated: 2025-11-27 + diff --git a/src/Confluent.SchemaRegistry.Rules/Confluent.SchemaRegistry.Rules.csproj b/src/Confluent.SchemaRegistry.Rules/Confluent.SchemaRegistry.Rules.csproj index ebc625362..ef492b524 100644 --- a/src/Confluent.SchemaRegistry.Rules/Confluent.SchemaRegistry.Rules.csproj +++ b/src/Confluent.SchemaRegistry.Rules/Confluent.SchemaRegistry.Rules.csproj @@ -34,7 +34,7 @@ - + From 075cf45be0616801a7ddbcf7595a2ffd1b174a4c Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Wed, 26 Nov 2025 17:02:50 -0800 Subject: [PATCH 2/3] Minor cleanup --- .../Vendored/Miscreant/AesPmac.cs | 4 ++-- .../Vendored/Miscreant/StreamDecryptor.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesPmac.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesPmac.cs index 8e1ab7df7..b7d3ef742 100644 --- a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesPmac.cs +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/AesPmac.cs @@ -78,7 +78,7 @@ public void HashCore(byte[] input, int index, int size) { if (disposed) { - throw new ObjectDisposedException(nameof(AesCmac)); + throw new ObjectDisposedException(nameof(AesPmac)); } var seg = new ArraySegment(input, index, size); @@ -120,7 +120,7 @@ public byte[] HashFinal() { if (disposed) { - throw new ObjectDisposedException(nameof(AesCmac)); + throw new ObjectDisposedException(nameof(AesPmac)); } if (position == BlockSize) diff --git a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/StreamDecryptor.cs b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/StreamDecryptor.cs index 739f7384f..d94afcff2 100644 --- a/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/StreamDecryptor.cs +++ b/src/Confluent.SchemaRegistry.Encryption/Vendored/Miscreant/StreamDecryptor.cs @@ -61,7 +61,7 @@ public byte[] Open(byte[] ciphertext, byte[] data = null, bool last = false) { if (disposed) { - throw new ObjectDisposedException(nameof(StreamEncryptor)); + throw new ObjectDisposedException(nameof(StreamDecryptor)); } if (finished) From 209581a26ed7a13b02f0b632a1d36f2c618a0be5 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Wed, 26 Nov 2025 17:58:25 -0800 Subject: [PATCH 3/3] Upgrade Microsoft.Bcl.Cryptography --- .../Confluent.SchemaRegistry.Encryption.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Confluent.SchemaRegistry.Encryption/Confluent.SchemaRegistry.Encryption.csproj b/src/Confluent.SchemaRegistry.Encryption/Confluent.SchemaRegistry.Encryption.csproj index c907d7cc3..b3195ea55 100644 --- a/src/Confluent.SchemaRegistry.Encryption/Confluent.SchemaRegistry.Encryption.csproj +++ b/src/Confluent.SchemaRegistry.Encryption/Confluent.SchemaRegistry.Encryption.csproj @@ -28,7 +28,7 @@ - +