From 77e7717e960cb60a74899b0b7336fb5d7b61e900 Mon Sep 17 00:00:00 2001 From: Michael Grafnetter Date: Thu, 28 Sep 2023 03:40:15 +0200 Subject: [PATCH] KDS calculations --- .../KdsRootKeyTester.cs | 36 +- Src/DSInternals.Common/Cryptography/NTHash.cs | 17 +- .../Data/DPAPI/KdsRootKey.cs | 461 +++++++++++++++++- .../Interop/NativeMethods.cs | 16 +- .../Interop/SafeUnicodeSecureStringPointer.cs | 24 +- .../Interop/UnicodeString.cs | 4 +- Src/DSInternals.Common/Validator.cs | 9 + 7 files changed, 551 insertions(+), 16 deletions(-) diff --git a/Src/DSInternals.Common.Test/KdsRootKeyTester.cs b/Src/DSInternals.Common.Test/KdsRootKeyTester.cs index 1994219c..2cbdf393 100644 --- a/Src/DSInternals.Common.Test/KdsRootKeyTester.cs +++ b/Src/DSInternals.Common.Test/KdsRootKeyTester.cs @@ -1,7 +1,8 @@ using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using DSInternals.Common.Data; +using System.Security.Principal; using DSInternals.Common.Cryptography; +using DSInternals.Common.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace DSInternals.Common.Test { @@ -68,5 +69,36 @@ public void ComputeL0Key_Vector1() "76d7341bbf6f85f439a14d3f68c6de31a83d2c55b1371c9c122f5b6f0eccff282973da43349da2b21a0a89b050b49e9ace951323f27638ccbfce8b6a0ead782b", l0Key.ToHex()); } + + [TestMethod] + public void GetGmsaPassword_Vector1() + { + byte[] binaryPassword = KdsRootKey.GetPassword( + new SecurityIdentifier("S-1-5-21-2468531440-3719951020-3687476655-1109"), + null, + Guid.Parse("7dc95c96-fa85-183a-dff5-f70696bf0b11"), + "814ad2f3928ff96d3650487967392feab3924f3d0dff8629d46a723640101cff8ca2cbd6aba40805cf03b380803b27837d80663eb4d18fd4cec414ebb2271fe2".HexToBinary(), + "SP800_108_CTR_HMAC", + "00000000010000000e000000000000005300480041003500310032000000".HexToBinary(), + DateTime.FromFileTimeUtc(133387453261266352)); + + Assert.AreEqual("0b5fbfb646dd7bce4f160ad69edb86ba", NTHash.ComputeHash(binaryPassword).ToHex()); + } + + [TestMethod] + public void GetGmsaPassword_Vector2() + { + var managedPasswordId = new ProtectionKeyIdentifier("010000004b44534b02000000690100001a00000018000000965cc97d85fa3a18dff5f70696bf0b1100000000180000001800000063006f006e0074006f0073006f002e0063006f006d00000063006f006e0074006f0073006f002e0063006f006d000000".HexToBinary()); + byte[] binaryPassword = KdsRootKey.GetPassword( + new SecurityIdentifier("S-1-5-21-2468531440-3719951020-3687476655-1109"), + managedPasswordId, + Guid.Parse("7dc95c96-fa85-183a-dff5-f70696bf0b11"), + "814ad2f3928ff96d3650487967392feab3924f3d0dff8629d46a723640101cff8ca2cbd6aba40805cf03b380803b27837d80663eb4d18fd4cec414ebb2271fe2".HexToBinary(), + "SP800_108_CTR_HMAC", + "00000000010000000e000000000000005300480041003500310032000000".HexToBinary(), + DateTime.FromFileTimeUtc(133403352475182719)); + + Assert.AreEqual("0b5fbfb646dd7bce4f160ad69edb86ba", NTHash.ComputeHash(binaryPassword).ToHex()); + } } } diff --git a/Src/DSInternals.Common/Cryptography/NTHash.cs b/Src/DSInternals.Common/Cryptography/NTHash.cs index 21a055ac..6b0790a9 100644 --- a/Src/DSInternals.Common/Cryptography/NTHash.cs +++ b/Src/DSInternals.Common/Cryptography/NTHash.cs @@ -20,7 +20,7 @@ public static class NTHash public static byte[] ComputeHash(SecureString password) { - Validator.AssertMaxLength(password, MaxInputLength, "password"); + Validator.AssertMaxLength(password, MaxInputLength, nameof(password)); byte[] hash; using(SafeUnicodeSecureStringPointer passwordPtr = new SafeUnicodeSecureStringPointer(password)) @@ -31,9 +31,22 @@ public static byte[] ComputeHash(SecureString password) return hash; } + public static byte[] ComputeHash(byte[] password) + { + Validator.AssertMaxLength(password, MaxInputLength*sizeof(char), nameof(password)); + + byte[] hash; + using (SafeUnicodeSecureStringPointer passwordPtr = new SafeUnicodeSecureStringPointer(password)) + { + NtStatus result = NativeMethods.RtlCalculateNtOwfPassword(passwordPtr, out hash); + Validator.AssertSuccess(result); + } + return hash; + } + public static byte[] ComputeHash(string password) { - Validator.AssertMaxLength(password, MaxInputLength, "password"); + Validator.AssertMaxLength(password, MaxInputLength, nameof(password)); byte[] hash; NtStatus result = NativeMethods.RtlCalculateNtOwfPassword(password, out hash); diff --git a/Src/DSInternals.Common/Data/DPAPI/KdsRootKey.cs b/Src/DSInternals.Common/Data/DPAPI/KdsRootKey.cs index ebe09eba..5f784554 100644 --- a/Src/DSInternals.Common/Data/DPAPI/KdsRootKey.cs +++ b/Src/DSInternals.Common/Data/DPAPI/KdsRootKey.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Security.Principal; using System.Text; using DSInternals.Common.Interop; @@ -14,6 +15,18 @@ public class KdsRootKey private const int L0KeyIteration = 1; private const int L1KeyIteration = 32; private const int L2KeyIteration = 32; + private const long KdsKeyCycleDuration = 360000000000; // 10 hrs in FileTime + private const long MaxClockSkew = 3000000000; // 5 min in FileTime + private const string GmsaKdfLabel = "GMSA PASSWORD"; + private const int DefaultKdsKeySize = 64; + private const int GmsaPasswordLength = 256; + + // TODO: Move to GMSA + private static readonly byte[] DefaultGMSASecurityDescriptor = { + 0x1, 0x0, 0x4, 0x80, 0x30, 0x0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x0, 0x1C, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x14, 0x0, 0x9F, 0x1, 0x12, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x9, + 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x12, 0x0, 0x0, 0x0 }; // Enterprise Domain Controllers private int? version; private DateTime ?creationTime; @@ -33,9 +46,13 @@ public KdsRootKey(DirectoryObject dsObject) // TODO: Validate object type // Key format version - // TODO: Check that format == 1 dsObject.ReadAttribute(CommonDirectoryAttributes.KdsVersion, out this.version); + if(this.version.HasValue) + { + Validator.AssertEquals(1, this.version.Value, nameof(version)); + } + // Domain controller DN DistinguishedName dcDN; dsObject.ReadAttribute(CommonDirectoryAttributes.KdsDomainController, out dcDN); @@ -240,6 +257,7 @@ out int counterOffset null, null, L0KeyIteration, + DefaultKdsKeySize, out byte[] l0Key, out string invalidAtribute ); @@ -249,6 +267,447 @@ out string invalidAtribute return l0Key; } + public static (byte[] l1KeyCurrent, byte[] l1KeyPrevious) GenerateL1Key( + Guid kdsRootKeyId, + string kdfAlgorithm, + byte[] kdfParameters, + byte[] l0Key, + int l0KeyId, + int l1KeyId, + byte[] securityDescriptor) + { + Validator.AssertNotNull(securityDescriptor, nameof(securityDescriptor)); + + var result = NativeMethods.GenerateKDFContext( + kdsRootKeyId, + l0KeyId, + L1KeyIteration - 1, + -1, + GroupKeyLevel.L1, + out byte[] context, + out int counterOffset); + Validator.AssertSuccess(result); + + // Append the security descriptor to the context for the last key + byte[] lastKeyContext = new byte[context.Length + securityDescriptor.Length]; + context.CopyTo(lastKeyContext, 0); + securityDescriptor.CopyTo(lastKeyContext, context.Length); + + result = NativeMethods.GenerateDerivedKey( + kdfAlgorithm, + kdfParameters, + l0Key, + lastKeyContext, + counterOffset, + null, + 1, + DefaultKdsKeySize, + out byte[] l1KeyLast, + out string invalidAttribute); + + Validator.AssertSuccess(result); + + byte[] l1KeyCurrent; + + int iteration = L1KeyIteration - l1KeyId - 1; + + if(iteration > 0) + { + // Decrease the counter + context[counterOffset]--; + + result = NativeMethods.GenerateDerivedKey( + kdfAlgorithm, + kdfParameters, + l1KeyLast, + context, + counterOffset, + null, + iteration, + DefaultKdsKeySize, + out l1KeyCurrent, + out string invalidAttribute2); + + Validator.AssertSuccess(result); + } + else + { + l1KeyCurrent = l1KeyLast; + } + + byte[] l1KeyPrevious; + + if(l1KeyId > 0) + { + // Set L1 key ID + context[counterOffset] = (byte)(l1KeyId - 1); + + result = NativeMethods.GenerateDerivedKey( + kdfAlgorithm, + kdfParameters, + l1KeyCurrent, + context, + counterOffset, + null, + 1, + DefaultKdsKeySize, + out l1KeyPrevious, + out string invalidAttribute3); + + Validator.AssertSuccess(result); + } + else + { + l1KeyPrevious = null; + } + + return (l1KeyCurrent, l1KeyPrevious); + } + + public static byte[] GenerateL2Key( + Guid kdsRootKeyId, + string kdfAlgorithm, + byte[] kdfParameters, + byte[] l1Key, + int l0KeyId, + int l1KeyId, + int l2KeyId) + { + + var result = NativeMethods.GenerateKDFContext( + kdsRootKeyId, + l0KeyId, + l1KeyId, + L2KeyIteration - 1, + GroupKeyLevel.L2, + out byte[] context, + out int counterOffset); + + Validator.AssertSuccess(result); + + result = NativeMethods.GenerateDerivedKey( + kdfAlgorithm, + kdfParameters, + l1Key, + context, + counterOffset, + null, + L2KeyIteration - l2KeyId, + DefaultKdsKeySize, + out byte[] l2Key, + out string invalidAttribute); + + Validator.AssertSuccess(result); + + return l2Key; + } + + public static byte[] ClientComputeL2Key( + ProtectionKeyIdentifier managedPasswordId, + Guid kdsRootKeyId, + string kdfAlgorithm, + byte[] kdfParameters, + byte[] l1Key, + byte[] l2Key, + int l0KeyId, + int l1KeyId, + int l2KeyId, + int l1KeyIteration, + int l2KeyIteration, + int nextL1KeyId, + int nextL2KeyId) + { + byte[] nextL1Key = l1Key; + + if (l1KeyIteration > 0) + { + // Recalculate L1 key + var result = NativeMethods.GenerateKDFContext( + kdsRootKeyId, + l0KeyId, + nextL1KeyId, + -1, + GroupKeyLevel.L1, + out byte[] l1Context, + out int l1CounterOffset + ); + Validator.AssertSuccess(result); + + result = NativeMethods.GenerateDerivedKey( + kdfAlgorithm, + kdfParameters, + l1Key, + l1Context, + l1CounterOffset, + null, + l1KeyIteration, + DefaultKdsKeySize, + out nextL1Key, + out string invalidAttributeName); + Validator.AssertSuccess(result); + } + + byte[] l2KeyBase = l2Key; + if(l2KeyBase == null || (managedPasswordId != null && l1KeyId > managedPasswordId.L1KeyId)) + { + // There is either no L2 key available or an older L1 key is needed + l2KeyBase = nextL1Key; + } + + byte[] nextL2Key = l2Key; + + if (l2KeyIteration > 0) + { + // Recalculate L2 key + var result = NativeMethods.GenerateKDFContext( + kdsRootKeyId, + l0KeyId, + managedPasswordId?.L1KeyId ?? l1KeyId, + nextL2KeyId, + GroupKeyLevel.L2, + out byte[] l2Context, + out int l2CounterOffset + ); + Validator.AssertSuccess(result); + + result = NativeMethods.GenerateDerivedKey( + kdfAlgorithm, + kdfParameters, + l2KeyBase, + l2Context, + l2CounterOffset, + null, + l2KeyIteration, + DefaultKdsKeySize, + out nextL2Key, + out string invalidAttributeName); + Validator.AssertSuccess(result); + } + + return nextL2Key; + } + + public static (byte[] l1Key, byte[] l2Key) ComputeSidPrivateKey( + Guid kdsRootKeyId, + string kdfAlgorithm, + byte[] kdfParameters, + byte[] l0Key, + byte[] securityDescriptor, + int l0KeyId, + int l1KeyId, + int l2KeyId, + bool isPublicKey + ) + { + (byte[] l1KeyCurrent, byte[] l1KeyPrevious) = + GenerateL1Key(kdsRootKeyId, kdfAlgorithm, kdfParameters, l0Key, l0KeyId, l1KeyId, securityDescriptor); + + if(l2KeyId == L2KeyIteration - 1 && isPublicKey == false) + { + return (l1KeyCurrent, null); + } + else + { + byte[] l1Key = l1KeyId != 0 ? l1KeyPrevious : null; + byte[] l2Key = GenerateL2Key(kdsRootKeyId, kdfAlgorithm, kdfParameters, l1KeyCurrent, l0KeyId, l1KeyId, l2KeyId); + return (l1Key, l2Key); + } + } + + public static (byte[] l0Key, byte[] l1Key, byte[] l2Key) GetSidKeyLocal( + Guid kdsRootKeyId, + byte[] kdsRootKey, + string kdfAlgorithm, + byte[] kdfParameters, + byte[] securityDescriptor, + int l0KeyId, + int l1KeyId, + int l2KeyId + ) + { + byte[] l0Key = ComputeL0Key(kdsRootKeyId, kdsRootKey, kdfAlgorithm, kdfParameters, l0KeyId); + + (byte[] l1Key, byte[] l2Key) = ComputeSidPrivateKey(kdsRootKeyId, kdfAlgorithm, kdfParameters, l0Key, securityDescriptor, l0KeyId, l1KeyId, l2KeyId, false); + + return (l0Key, l1Key, l2Key); + } + + public static byte[] GetPassword( + SecurityIdentifier sid, + ProtectionKeyIdentifier managedPasswordId, + Guid kdsRootKeyId, + byte[] kdsRootKey, + string kdfAlgorithm, + byte[] kdfParameters, + DateTime effectiveTime + ) + { + (int l0KeyId, int l1KeyId, int l2KeyId) = GetIntervalId(effectiveTime); + (byte[] l0Key, byte[] l1Key, byte[] l2Key) = GetSidKeyLocal(kdsRootKeyId, kdsRootKey, kdfAlgorithm, kdfParameters, DefaultGMSASecurityDescriptor, l0KeyId, l1KeyId, l2KeyId); + + return GenerateGmsaPassword( + managedPasswordId, + sid, + kdsRootKeyId, + kdsRootKey, + kdfAlgorithm, + kdfParameters, + l0KeyId, + l1KeyId, + l2KeyId, + l0Key, + l1Key, + l2Key); + } + + public static void ParseSIDKeyResult( + ProtectionKeyIdentifier managedPasswordId, + int l0KeyId, + int l1KeyId, + int l2KeyId, + bool isL2KeyEmpty, + out int l1KeyIteration, + out int nextL1KeyId, + out int l2KeyIteration, + out int nextL2KeyId) + { + l1KeyIteration = 0; + l2KeyIteration = 0; + nextL1KeyId = 0; + nextL2KeyId = 0; + + if (managedPasswordId != null) + { + if (isL2KeyEmpty) + { + l1KeyIteration = l1KeyId - managedPasswordId.L1KeyId; + if (l1KeyIteration > 0) + { + nextL1KeyId = l1KeyId - 1; + } + } + else + { + l1KeyIteration = l1KeyId - managedPasswordId.L1KeyId - 1; + if (l1KeyIteration > 0) + { + nextL1KeyId = l1KeyId - 2; + } + } + + if (isL2KeyEmpty || l1KeyId > managedPasswordId.L1KeyId) + { + l2KeyIteration = L2KeyIteration - managedPasswordId.L2KeyId; + nextL2KeyId = L2KeyIteration - 1; + } + else + { + l2KeyIteration = l2KeyId - managedPasswordId.L2KeyId; + if (l2KeyIteration > 0) + { + nextL2KeyId = l2KeyId - 1; + } + } + } + else + { + if (isL2KeyEmpty) + { + l2KeyIteration = 1; + nextL2KeyId = L2KeyIteration - 1; + } + } + } + + public static byte[] GenerateGmsaPassword( + ProtectionKeyIdentifier managedPasswordId, + SecurityIdentifier sid, + Guid kdsRootKeyId, + byte[] kdsRootKey, + string kdfAlgorithm, + byte[] kdfParameters, + int l0KeyId, + int l1KeyId, + int l2KeyId, + byte[] l0Key, + byte[] l1Key, + byte[] l2Key + ) + { + ParseSIDKeyResult( + managedPasswordId, + l0KeyId, + l1KeyId, + l2KeyId, + l2Key == null, + out int l1KeyIteration, + out int nextL1KeyId, + out int l2KeyIteration, + out int nextL2KeyId); + + byte[] nextL2Key = l2Key; + + if (l1KeyIteration > 0 || l2KeyIteration > 0) + { + nextL2Key = ClientComputeL2Key( + managedPasswordId, + kdsRootKeyId, + kdfAlgorithm, + kdfParameters, + l1Key, + l2Key, + l0KeyId, + l1KeyId, + l2KeyId, + l1KeyIteration, + l2KeyIteration, + nextL1KeyId, + nextL2KeyId); + } + + NativeMethods.GenerateDerivedKey( + kdfAlgorithm, + kdfParameters, + nextL2Key, + sid.GetBinaryForm(), + null, + GmsaKdfLabel, + 1, + GmsaPasswordLength, + out byte[] generatedPassword, + out string invalidAttribute + ); + + return generatedPassword; + } + + public static (int l0KeyId, int l1KeyId, int l2KeyId) GetCurrentIntervalId( + bool isClockSkewConsidered = false + ) + { + return GetIntervalId(DateTime.Now, isClockSkewConsidered); + } + + public static (int l0KeyId, int l1KeyId, int l2KeyId) GetIntervalId( + DateTime effectiveTime, + bool isClockSkewConsidered = false + ) + { + long effectiveFileTime = effectiveTime.ToFileTimeUtc(); + + if(isClockSkewConsidered) + { + effectiveFileTime += MaxClockSkew; + } + + int effectiveCycleId = (int)(effectiveFileTime / KdsKeyCycleDuration); + + int l0KeyId = effectiveCycleId / (L1KeyIteration * L2KeyIteration); + int l1KeyId = (effectiveCycleId / L2KeyIteration) % L1KeyIteration; + int l2KeyId = effectiveCycleId % L2KeyIteration; + + return (l0KeyId, l1KeyId, l2KeyId); + } public static Dictionary ParseKdfParameters(byte[] blob) { if(blob == null || blob.Length == 0) diff --git a/Src/DSInternals.Common/Interop/NativeMethods.cs b/Src/DSInternals.Common/Interop/NativeMethods.cs index 36f9f718..8775185e 100644 --- a/Src/DSInternals.Common/Interop/NativeMethods.cs +++ b/Src/DSInternals.Common/Interop/NativeMethods.cs @@ -15,7 +15,6 @@ internal static class NativeMethods internal const int LMHashNumBytes = NTHashNumBits / 8; internal const int LMPasswordMaxChars = 14; internal const int NTPasswordMaxChars = 128; - internal const int KdsRootKeySize = 64; private const int MaxRegistryKeyClassSize = 256; private const string Advapi = "advapi32.dll"; @@ -348,8 +347,9 @@ internal static Win32ErrorCode GenerateDerivedKey( byte[] secret, byte[] context, int? counterOffset, - byte[] label, + string label, int iteration, + int desiredKeyLength, out byte[] derivedKey, out string invalidAttribute) { @@ -357,13 +357,13 @@ internal static Win32ErrorCode GenerateDerivedKey( int kdfParametersLength = kdfParameters?.Length ?? 0; int secretLength = secret?.Length ?? 0; int contextLength = context?.Length ?? 0; - int labelLength = label?.Length ?? 0; - byte[] derivedKeyBuffer = new byte[KdsRootKeySize]; + int labelLength = label!= null ? Encoding.Unicode.GetMaxByteCount(label.Length) : 0; // size of the unicode string, including the trailing zero + byte[] derivedKeyBuffer = new byte[desiredKeyLength]; StringBuilder invalidAttributeBuffer = new StringBuilder(byte.MaxValue); // Deal with the optional int parameter int counterOffsetValue = counterOffset.GetValueOrDefault(); - var counterOffsetHandle = GCHandle.Alloc(counterOffsetValue); + var counterOffsetHandle = GCHandle.Alloc(counterOffsetValue, GCHandleType.Pinned); try { @@ -375,12 +375,12 @@ internal static Win32ErrorCode GenerateDerivedKey( secretLength, context, contextLength, - (counterOffset.HasValue ? (IntPtr) counterOffsetHandle : IntPtr.Zero), + (counterOffset.HasValue ? counterOffsetHandle.AddrOfPinnedObject() : IntPtr.Zero), label, labelLength, iteration, derivedKeyBuffer, - KdsRootKeySize, + desiredKeyLength, ref invalidAttributeBuffer ); @@ -404,7 +404,7 @@ private static extern Win32ErrorCode GenerateDerivedKey( byte[] context, int contextLength, IntPtr counterOffset, - byte[] label, + string label, int labelLength, int iteration, [MarshalAs(UnmanagedType.LPArray)] byte[] key, diff --git a/Src/DSInternals.Common/Interop/SafeUnicodeSecureStringPointer.cs b/Src/DSInternals.Common/Interop/SafeUnicodeSecureStringPointer.cs index 1a812c95..21deb1a8 100644 --- a/Src/DSInternals.Common/Interop/SafeUnicodeSecureStringPointer.cs +++ b/Src/DSInternals.Common/Interop/SafeUnicodeSecureStringPointer.cs @@ -25,6 +25,28 @@ public SafeUnicodeSecureStringPointer(SecureString password) } } + public SafeUnicodeSecureStringPointer(byte[] password) + : base(true) + { + if (password != null) + { + if(password.Length % sizeof(char) == 1) + { + // Unicode strings must have even number of bytes + new ArgumentOutOfRangeException(nameof(password)); + } + + IntPtr buffer = Marshal.AllocHGlobal(password.Length + sizeof(char)); + Marshal.Copy(password, 0, buffer, password.Length); + + // Add the trailing zero + Marshal.WriteInt16(buffer, password.Length, 0); + + this.SetHandle(buffer); + this.numChars = password.Length / sizeof(char); + } + } + public int NumChars { get @@ -71,4 +93,4 @@ public override string ToString() return Marshal.PtrToStringUni(this.handle); } } -} \ No newline at end of file +} diff --git a/Src/DSInternals.Common/Interop/UnicodeString.cs b/Src/DSInternals.Common/Interop/UnicodeString.cs index abffa243..011af907 100644 --- a/Src/DSInternals.Common/Interop/UnicodeString.cs +++ b/Src/DSInternals.Common/Interop/UnicodeString.cs @@ -26,7 +26,7 @@ public UnicodeString(string text) } else if (text.Length > MaxLength) { - throw new ArgumentOutOfRangeException("text"); + throw new ArgumentOutOfRangeException(nameof(text)); } else { @@ -52,4 +52,4 @@ public UnicodeString(string text) [MarshalAs(UnmanagedType.LPWStr)] public string Buffer; } -} \ No newline at end of file +} diff --git a/Src/DSInternals.Common/Validator.cs b/Src/DSInternals.Common/Validator.cs index ccb36409..2934aad2 100644 --- a/Src/DSInternals.Common/Validator.cs +++ b/Src/DSInternals.Common/Validator.cs @@ -150,6 +150,15 @@ public static void AssertMaxLength(string input, int maxLength, string paramName } } + public static void AssertMaxLength(byte[] input, int maxLength, string paramName) + { + AssertNotNull(input, paramName); + if (input.Length > maxLength) + { + throw new ArgumentOutOfRangeException(paramName, input.Length, Resources.InputLongerThanMaxMessage); + } + } + public static void AssertMinLength(byte[] data, int minLength, string paramName) { AssertNotNull(data, paramName);