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)