From 4ee432bdb0660b472eca374b94a67299613419c4 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 25 Apr 2026 14:05:55 +0100 Subject: [PATCH 1/3] Align read of keyMDVersion with TDS ULONGLONG datatype --- .../SqlClient/AlwaysEncryptedHelperClasses.cs | 20 +++++++-------- .../Data/SqlClient/ColumnEncryptionKeyInfo.cs | 25 +++++++------------ .../Data/SqlClient/SqlCommand.Encryption.cs | 3 ++- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 12 +++++---- 4 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncryptedHelperClasses.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncryptedHelperClasses.cs index 659d2e9f0f..cb37486a42 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncryptedHelperClasses.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncryptedHelperClasses.cs @@ -1,8 +1,9 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; using Microsoft.Data.SqlClient.Server; @@ -20,7 +21,7 @@ internal class SqlEncryptionKeyInfo internal int databaseId; internal int cekId; internal int cekVersion; - internal byte[] cekMdVersion; + internal ulong cekMdVersion; internal string keyPath; internal string keyStoreName; internal string algorithmName; @@ -63,7 +64,7 @@ internal class SqlTceCipherInfoEntry /// /// Cek MD Version /// - private byte[] _cekMdVersion; + private ulong _cekMdVersion; /// /// Return the ordinal. @@ -112,7 +113,7 @@ internal int CekVersion /// /// Return the CEK MD Version. /// - internal byte[] CekMdVersion + internal ulong CekMdVersion { get { @@ -142,7 +143,7 @@ internal List ColumnEncryptionKeyValues /// /// /// - internal void Add(byte[] encryptedKey, int databaseId, int cekId, int cekVersion, byte[] cekMdVersion, string keyPath, string keyStoreName, string algorithmName) + internal void Add(byte[] encryptedKey, int databaseId, int cekId, int cekVersion, ulong cekMdVersion, string keyPath, string keyStoreName, string algorithmName) { Debug.Assert(_columnEncryptionKeyValues != null, "_columnEncryptionKeyValues should already be initialized."); @@ -172,7 +173,6 @@ internal void Add(byte[] encryptedKey, int databaseId, int cekId, int cekVersion Debug.Assert(_databaseId == databaseId); Debug.Assert(_cekId == cekId); Debug.Assert(_cekVersion == cekVersion); - Debug.Assert(_cekMdVersion != null && cekMdVersion != null && _cekMdVersion.Length == _cekMdVersion.Length); } } @@ -186,7 +186,7 @@ internal SqlTceCipherInfoEntry(int ordinal = 0) _databaseId = 0; _cekId = 0; _cekVersion = 0; - _cekMdVersion = null; + _cekMdVersion = 0; _columnEncryptionKeyValues = new List(); } } @@ -550,7 +550,7 @@ private byte[] SerializeToWriteFormat() totalLength += sizeof(int); // Metadata version of the encryption key. - totalLength += _cipherMetadata.EncryptionKeyInfo.cekMdVersion.Length; + totalLength += sizeof(ulong); // Normalization Rule Version. totalLength += sizeof(byte); @@ -576,8 +576,8 @@ private byte[] SerializeToWriteFormat() SerializeIntIntoBuffer(_cipherMetadata.EncryptionKeyInfo.cekVersion, serializedWireFormat, ref consumedBytes); // 6 - Write the metadata version of the encryption key. - Buffer.BlockCopy(_cipherMetadata.EncryptionKeyInfo.cekMdVersion, 0, serializedWireFormat, consumedBytes, _cipherMetadata.EncryptionKeyInfo.cekMdVersion.Length); - consumedBytes += _cipherMetadata.EncryptionKeyInfo.cekMdVersion.Length; + BinaryPrimitives.WriteUInt64LittleEndian(serializedWireFormat.AsSpan(consumedBytes), _cipherMetadata.EncryptionKeyInfo.cekMdVersion); + consumedBytes += sizeof(ulong); // 7 - Write Normalization Rule Version. serializedWireFormat[consumedBytes++] = _cipherMetadata.NormalizationRuleVersion; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ColumnEncryptionKeyInfo.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ColumnEncryptionKeyInfo.cs index 6c99dd8432..f738a465c0 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ColumnEncryptionKeyInfo.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ColumnEncryptionKeyInfo.cs @@ -1,8 +1,9 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; +using System.Buffers.Binary; namespace Microsoft.Data.SqlClient { @@ -16,10 +17,9 @@ internal class ColumnEncryptionKeyInfo internal readonly byte[] DecryptedKeyBytes; internal readonly byte[] KeyIdBytes; internal readonly byte[] DatabaseIdBytes; - internal readonly byte[] KeyMetadataVersionBytes; + internal readonly ulong KeyMetadataVersion; private static readonly string _decryptedKeyName = "DecryptedKey"; - private static readonly string _keyMetadataVersionName = "KeyMetadataVersion"; private static readonly string _className = "ColumnEncryptionKeyInfo"; private static readonly string _bytePackageName = "BytePackage"; private static readonly string _serializeToBufferMethodName = "SerializeToBuffer"; @@ -32,7 +32,7 @@ internal class ColumnEncryptionKeyInfo /// database id for this column encryption key /// key metadata version for this column encryption key /// key id for this column encryption key - internal ColumnEncryptionKeyInfo(byte[] decryptedKey, int databaseId, byte[] keyMetadataVersion, int keyid) + internal ColumnEncryptionKeyInfo(byte[] decryptedKey, int databaseId, ulong keyMetadataVersion, int keyid) { if (decryptedKey == null) @@ -43,19 +43,11 @@ internal ColumnEncryptionKeyInfo(byte[] decryptedKey, int databaseId, byte[] key { throw SQL.EmptyArgumentInConstructorInternal(_decryptedKeyName, _className); } - if (keyMetadataVersion == null) - { - throw SQL.NullArgumentInConstructorInternal(_keyMetadataVersionName, _className); - } - if (0 == keyMetadataVersion.Length) - { - throw SQL.EmptyArgumentInConstructorInternal(_keyMetadataVersionName, _className); - } KeyId = keyid; DatabaseId = databaseId; DecryptedKeyBytes = decryptedKey; - KeyMetadataVersionBytes = keyMetadataVersion; + KeyMetadataVersion = keyMetadataVersion; //Covert keyId to Bytes ushort keyIdUShort; @@ -96,7 +88,8 @@ internal int GetLengthForSerialization() lengthForSerialization += DecryptedKeyBytes.Length; lengthForSerialization += KeyIdBytes.Length; lengthForSerialization += DatabaseIdBytes.Length; - lengthForSerialization += KeyMetadataVersionBytes.Length; + // KeyMetadataVersion is of type ulong which is 8 bytes + lengthForSerialization += sizeof(ulong); return lengthForSerialization; } @@ -131,8 +124,8 @@ internal int SerializeToBuffer(byte[] bytePackage, int startOffset) Buffer.BlockCopy(DatabaseIdBytes, 0, bytePackage, startOffset, DatabaseIdBytes.Length); startOffset += DatabaseIdBytes.Length; - Buffer.BlockCopy(KeyMetadataVersionBytes, 0, bytePackage, startOffset, KeyMetadataVersionBytes.Length); - startOffset += KeyMetadataVersionBytes.Length; + BinaryPrimitives.WriteUInt64LittleEndian(bytePackage.AsSpan(startOffset), KeyMetadataVersion); + startOffset += sizeof(ulong); Buffer.BlockCopy(KeyIdBytes, 0, bytePackage, startOffset, KeyIdBytes.Length); startOffset += KeyIdBytes.Length; Buffer.BlockCopy(DecryptedKeyBytes, 0, bytePackage, startOffset, DecryptedKeyBytes.Length); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 59a2fddadc..fa5cfb5ab2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Buffers.Binary; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -1016,7 +1017,7 @@ private bool ReadDescribeEncryptionParameterResultsKeys( databaseId: ds.GetInt32((int)DescribeParameterEncryptionResultSet1.DbId), cekId: ds.GetInt32((int)DescribeParameterEncryptionResultSet1.KeyId), cekVersion: ds.GetInt32((int)DescribeParameterEncryptionResultSet1.KeyVersion), - cekMdVersion: keyMdVersion, + cekMdVersion: BinaryPrimitives.ReadUInt64LittleEndian(keyMdVersion), keyPath: keyPath, keyStoreName: providerName, algorithmName: ds.GetString((int)DescribeParameterEncryptionResultSet1.KeyEncryptionAlgorithm)); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 2d1807e7b5..64aa722437 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -5367,6 +5367,10 @@ internal TdsOperationStatus TryProcessAltMetaData(int cColumns, TdsParserStateOb /// /// Parses the TDS message to read single CIPHER_INFO entry. /// + /// + /// The CIPHER_INFO structure is represented as an EK_INFO structure in the TDS protocol. + /// + /// . internal TdsOperationStatus TryReadCipherInfoEntry(TdsParserStateObject stateObj, out SqlTceCipherInfoEntry entry) { byte cekValueCount = 0; @@ -5397,8 +5401,7 @@ internal TdsOperationStatus TryReadCipherInfoEntry(TdsParserStateObject stateObj } // Read the key MD Version - byte[] keyMDVersion = new byte[8]; - result = stateObj.TryReadByteArray(keyMDVersion, 8); + result = stateObj.TryReadInt64(out long keyMDVersion); if (result != TdsOperationStatus.Done) { return result; @@ -5493,7 +5496,7 @@ internal TdsOperationStatus TryReadCipherInfoEntry(TdsParserStateObject stateObj databaseId: dbId, cekId: keyId, cekVersion: keyVersion, - cekMdVersion: keyMDVersion, + cekMdVersion: unchecked((ulong)keyMDVersion), keyPath: keyPath, keyStoreName: keyStoreName, algorithmName: algorithmName); @@ -11525,8 +11528,7 @@ internal void WriteEncryptionEntries(ref SqlTceCipherInfoTable cekTable, TdsPars WriteInt(cekTable[i].CekVersion, stateObj); // Write 8 bytes of key MD Version - Debug.Assert(8 == cekTable[i].CekMdVersion.Length); - stateObj.WriteByteArray(cekTable[i].CekMdVersion, 8, 0); + WriteUnsignedLong(cekTable[i].CekMdVersion, stateObj); // We don't really need to send the keys stateObj.WriteByte(0x00); From d9e9f3acb617169dcd13c76c6fbc9967649b10cf Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 25 Apr 2026 14:18:21 +0100 Subject: [PATCH 2/3] Align read of EkValueCount in COLMETADATA with TDS USHORT datatype --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 64aa722437..10f57e7fc5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -5511,9 +5511,8 @@ internal TdsOperationStatus TryReadCipherInfoEntry(TdsParserStateObject stateObj internal TdsOperationStatus TryProcessCipherInfoTable(TdsParserStateObject stateObj, out SqlTceCipherInfoTable cipherTable) { // Read count - short tableSize = 0; cipherTable = null; - TdsOperationStatus result = stateObj.TryReadInt16(out tableSize); + TdsOperationStatus result = stateObj.TryReadUInt16(out ushort tableSize); if (result != TdsOperationStatus.Done) { return result; From 2f27d3dbee6f3374eef338578b98f35a7da4ac3b Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 10 May 2026 07:59:21 +0100 Subject: [PATCH 3/3] Address test failures Test code uses reflection to call SqlTceCipherInfoEntry.Add --- .../ExceptionsAlgorithmErrors.cs | 14 +++++++------- .../AlwaysEncryptedTests/Utility.cs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AlwaysEncryptedTests/ExceptionsAlgorithmErrors.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AlwaysEncryptedTests/ExceptionsAlgorithmErrors.cs index caa98cc686..c12f73e29f 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AlwaysEncryptedTests/ExceptionsAlgorithmErrors.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AlwaysEncryptedTests/ExceptionsAlgorithmErrors.cs @@ -72,7 +72,7 @@ public void TestInvalidEncryptionType() { const byte invalidEncryptionType = 3; Object cipherMD = GetSqlCipherMetadata(0, 2, null, invalidEncryptionType, 0x01); - AddEncryptionKeyToCipherMD(cipherMD, _encryptedCek, 0, 0, 0, new byte[] { 0x01, 0x02, 0x03 }, _certificatePath, "MSSQL_CERTIFICATE_STORE", "RSA_OAEP"); + AddEncryptionKeyToCipherMD(cipherMD, _encryptedCek, 0, 0, 0, 0x010203, _certificatePath, "MSSQL_CERTIFICATE_STORE", "RSA_OAEP"); byte[] plainText = Encoding.Unicode.GetBytes("HelloWorld"); byte[] cipherText = EncryptDataUsingAED(plainText, _cek, CColumnEncryptionType.Deterministic); @@ -135,7 +135,7 @@ public void TestNullColumnEncryptionAlgorithm() string expectedMessage = string.Format(SystemDataResourceManager.Instance.TCE_NullColumnEncryptionAlgorithm, "'AEAD_AES_256_CBC_HMAC_SHA256'"); Object cipherMD = GetSqlCipherMetadata(0, 0, null, 1, 0x01); - AddEncryptionKeyToCipherMD(cipherMD, _encryptedCek, 0, 0, 0, new byte[] { 0x01, 0x02, 0x03 }, _certificatePath, "MSSQL_CERTIFICATE_STORE", "RSA_OAEP"); + AddEncryptionKeyToCipherMD(cipherMD, _encryptedCek, 0, 0, 0, 0x010203, _certificatePath, "MSSQL_CERTIFICATE_STORE", "RSA_OAEP"); byte[] plainText = Encoding.Unicode.GetBytes("HelloWorld"); byte[] cipherText = EncryptDataUsingAED(plainText, _cek, CColumnEncryptionType.Deterministic); @@ -153,7 +153,7 @@ public void TestUnknownEncryptionAlgorithmId() string expectedMessage = string.Format(SystemDataResourceManager.Instance.TCE_UnknownColumnEncryptionAlgorithmId, unknownEncryptionAlgoId, "'1', '2'"); Object cipherMD = GetSqlCipherMetadata(0, unknownEncryptionAlgoId, null, 1, 0x01); - AddEncryptionKeyToCipherMD(cipherMD, _encryptedCek, 0, 0, 0, new byte[] { 0x01, 0x02, 0x03 }, _certificatePath, "MSSQL_CERTIFICATE_STORE", "RSA_OAEP"); + AddEncryptionKeyToCipherMD(cipherMD, _encryptedCek, 0, 0, 0, 0x010203, _certificatePath, "MSSQL_CERTIFICATE_STORE", "RSA_OAEP"); byte[] plainText = Encoding.Unicode.GetBytes("HelloWorld"); byte[] cipherText = EncryptDataUsingAED(plainText, _cek, CColumnEncryptionType.Deterministic); @@ -177,7 +177,7 @@ public void TestUnknownCustomKeyStoreProvider() string expectedMessage = string.Format(SystemDataResourceManager.Instance.TCE_UnrecognizedKeyStoreProviderName, invalidProviderName, "'MSSQL_CERTIFICATE_STORE', 'MSSQL_CNG_STORE', 'MSSQL_CSP_PROVIDER'", ""); Object cipherMD = GetSqlCipherMetadata(0, 1, null, 1, 0x03); - AddEncryptionKeyToCipherMD(cipherMD, _encryptedCek, 0, 0, 0, new byte[] { 0x01, 0x02, 0x03 }, _certificatePath, invalidProviderName, "RSA_OAEP"); + AddEncryptionKeyToCipherMD(cipherMD, _encryptedCek, 0, 0, 0, 0x010203, _certificatePath, invalidProviderName, "RSA_OAEP"); byte[] plainText = Encoding.Unicode.GetBytes("HelloWorld"); byte[] cipherText = EncryptDataUsingAED(plainText, _cek, CColumnEncryptionType.Deterministic); @@ -199,7 +199,7 @@ public void TestTceUnknownEncryptionAlgorithm() string expectedMessage = string.Format(SystemDataResourceManager.Instance.TCE_UnknownColumnEncryptionAlgorithm, unknownEncryptionAlgorithm, "'AEAD_AES_256_CBC_HMAC_SHA256'"); Object cipherMD = GetSqlCipherMetadata(0, 0, "Dummy", 1, 0x01); - AddEncryptionKeyToCipherMD(cipherMD, _encryptedCek, 0, 0, 0, new byte[] { 0x01, 0x02, 0x03 }, _certificatePath, "MSSQL_CERTIFICATE_STORE", "RSA_OAEP"); + AddEncryptionKeyToCipherMD(cipherMD, _encryptedCek, 0, 0, 0, 0x010203, _certificatePath, "MSSQL_CERTIFICATE_STORE", "RSA_OAEP"); byte[] plainText = Encoding.Unicode.GetBytes("HelloWorld"); byte[] cipherText = EncryptDataUsingAED(plainText, _cek, CColumnEncryptionType.Deterministic); @@ -220,7 +220,7 @@ public void TestExceptionsFromCertStore() "MSSQL_CERTIFICATE_STORE", BitConverter.ToString(corruptedCek, corruptedCek.Length - 10, 10)); Object cipherMD = GetSqlCipherMetadata(0, 1, null, 1, 0x01); - AddEncryptionKeyToCipherMD(cipherMD, corruptedCek, 0, 0, 0, new byte[] { 0x01, 0x02, 0x03 }, _certificatePath, "MSSQL_CERTIFICATE_STORE", "RSA_OAEP"); + AddEncryptionKeyToCipherMD(cipherMD, corruptedCek, 0, 0, 0, 0x010203, _certificatePath, "MSSQL_CERTIFICATE_STORE", "RSA_OAEP"); byte[] plainText = Encoding.Unicode.GetBytes("HelloWorld"); byte[] cipherText = EncryptDataUsingAED(plainText, _cek, CColumnEncryptionType.Deterministic); @@ -244,7 +244,7 @@ public void TestExceptionsFromCustomKeyStore() SqlConnection.RegisterColumnEncryptionKeyStoreProviders(customProviders); object cipherMD = GetSqlCipherMetadata(0, 1, null, 1, 0x01); - AddEncryptionKeyToCipherMD(cipherMD, _encryptedCek, 0, 0, 0, new byte[] { 0x01, 0x02, 0x03 }, _certificatePath, "DummyProvider", "DummyAlgo"); + AddEncryptionKeyToCipherMD(cipherMD, _encryptedCek, 0, 0, 0, 0x010203, _certificatePath, "DummyProvider", "DummyAlgo"); byte[] plainText = Encoding.Unicode.GetBytes("HelloWorld"); byte[] cipherText = EncryptDataUsingAED(plainText, _cek, CColumnEncryptionType.Deterministic); diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AlwaysEncryptedTests/Utility.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AlwaysEncryptedTests/Utility.cs index e53fa6491e..c51dcecf99 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AlwaysEncryptedTests/Utility.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AlwaysEncryptedTests/Utility.cs @@ -217,7 +217,7 @@ internal static void AddEncryptionKeyToCipherMD( int databaseId, int cekId, int cekVersion, - byte[] cekMdVersion, + ulong cekMdVersion, string keyPath, string keyStoreName, string algorithmName)