Skip to content

Commit

Permalink
KDS KDF L0
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelGrafnetter committed Sep 25, 2023
1 parent b387697 commit 13385b8
Show file tree
Hide file tree
Showing 12 changed files with 520 additions and 11 deletions.
3 changes: 3 additions & 0 deletions Src/DSInternals.Common.Test/DSInternals.Common.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Optimize>true</Optimize>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<UseVSHostingProcess>false</UseVSHostingProcess>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<Reference Include="CBOR, Version=4.5.2.0, Culture=neutral, PublicKeyToken=9cd62db60ea5554c, processorArchitecture=MSIL">
Expand Down Expand Up @@ -76,6 +78,7 @@
<Compile Include="KerberosCredentialTester.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Cryptography\SecureStringExtensionsTester.cs" />
<Compile Include="ProtectionKeyIdentifierTester.cs" />
<Compile Include="SearchableDeviceKeyTester.cs" />
<Compile Include="SecurityIdentifierExtensionsTester.cs" />
<Compile Include="KeyCredentialTester.cs" />
Expand Down
16 changes: 16 additions & 0 deletions Src/DSInternals.Common.Test/KdsRootKeyTester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,21 @@ public void ParseSecretAgreementParameters_EmptyInput()
KdsRootKey.ParseSecretAgreementParameters(new byte[0] { });
throw new AssertInconclusiveException();
}

[TestMethod]
public void ComputeL0Key_Vector1()
{
byte[] l0Key = KdsRootKey.ComputeL0Key(
Guid.Parse("7dc95c96-fa85-183a-dff5-f70696bf0b11"),
"814ad2f3928ff96d3650487967392feab3924f3d0dff8629d46a723640101cff8ca2cbd6aba40805cf03b380803b27837d80663eb4d18fd4cec414ebb2271fe2".HexToBinary(),
"SP800_108_CTR_HMAC",
"00000000010000000e000000000000005300480041003500310032000000".HexToBinary(),
361
);

Assert.AreEqual(
"76d7341bbf6f85f439a14d3f68c6de31a83d2c55b1371c9c122f5b6f0eccff282973da43349da2b21a0a89b050b49e9ace951323f27638ccbfce8b6a0ead782b",
l0Key.ToHex());
}
}
}
55 changes: 55 additions & 0 deletions Src/DSInternals.Common.Test/ProtectionKeyIdentifierTester.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using DSInternals.Common.Data;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;

namespace DSInternals.Common.Test
{
[TestClass]
public class ProtectionKeyIdentifierTester
{
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ProtectionKeyIdentifier_Null()
{
var obj = new ProtectionKeyIdentifier(null);
}

[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ProtectionKeyIdentifier_Empty()
{
var obj = new ProtectionKeyIdentifier(new byte[0]);
}

[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ProtectionKeyIdentifier_Truncated1()
{
// Only contains version
byte[] blob = "01000000".HexToBinary();
var parsed = new ProtectionKeyIdentifier(blob);
}

[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ProtectionKeyIdentifier_Truncated2()
{
// The last byte has been trimmed
byte[] blob = "010000004b44534b02000000690100001a00000018000000965cc97d85fa3a18dff5f70696bf0b1100000000180000001800000063006f006e0074006f0073006f002e0063006f006d00000063006f006e0074006f0073006f002e0063006f006d0000".HexToBinary();
var parsed = new ProtectionKeyIdentifier(blob);
}

[TestMethod]
public void ProtectionKeyIdentifier_Parse()
{
byte[] blob = "010000004b44534b02000000690100001a00000018000000965cc97d85fa3a18dff5f70696bf0b1100000000180000001800000063006f006e0074006f0073006f002e0063006f006d00000063006f006e0074006f0073006f002e0063006f006d000000".HexToBinary();
var parsed = new ProtectionKeyIdentifier(blob);
Assert.AreEqual("contoso.com", parsed.DomainName);
Assert.AreEqual("contoso.com", parsed.ForestName);
Assert.AreEqual("7dc95c96-fa85-183a-dff5-f70696bf0b11", parsed.RootKeyId.ToString());
Assert.AreEqual(361, parsed.L0KeyId);
Assert.AreEqual(26, parsed.L1KeyId);
Assert.AreEqual(24, parsed.L2KeyId);
}
}
}
6 changes: 5 additions & 1 deletion Src/DSInternals.Common/DSInternals.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<Compile Include="Cryptography\Pbkdf2.cs" />
<Compile Include="Cryptography\PrivateKeyEncryptionType.cs" />
<Compile Include="Data\DNWithBinary.cs" />
<Compile Include="Data\DPAPI\ProtectionKeyIdentifier.cs" />
<Compile Include="Data\DPAPI\DPAPIObject.cs" />
<Compile Include="Data\DPAPI\KdsRootKey.cs" />
<Compile Include="Data\DPAPI\RoamedCredential.cs" />
Expand All @@ -89,6 +90,7 @@
<Compile Include="Data\Hello\KeyMaterialFido.cs" />
<Compile Include="Data\Hello\VolumeType.cs" />
<Compile Include="Data\Hello\KeyUsage.cs" />
<Compile Include="Data\Principals\GroupManagedServiceAccount.cs" />
<Compile Include="Data\Principals\SupportedEncryptionTypes.cs" />
<Compile Include="Extensions\ByteArrayExtensions.cs" />
<Compile Include="Cryptography\Crc32.cs" />
Expand Down Expand Up @@ -130,6 +132,7 @@
<Compile Include="Extensions\NTAccountExtensions.cs" />
<Compile Include="Extensions\RSAExtensions.cs" />
<Compile Include="Interop\CryptoBuffer.cs" />
<Compile Include="Interop\Enums\GroupKeyLevel.cs" />
<Compile Include="Interop\Enums\NetCancelOptions.cs" />
<Compile Include="Interop\Enums\NetConnectOptions.cs" />
<Compile Include="Interop\Enums\NetResourceDisplayType.cs" />
Expand All @@ -145,6 +148,7 @@
<Compile Include="Interop\SafeOemStringPointer.cs" />
<Compile Include="Interop\Enums\Win32ErrorCode.cs" />
<Compile Include="Interop\Enums\NtStatus.cs" />
<Compile Include="Interop\SafeSidKeyProviderHandle.cs" />
<Compile Include="Interop\SafeUnicodeSecureStringPointer.cs" />
<Compile Include="Interop\OemString.cs" />
<Compile Include="Interop\SecureUnicodeString.cs" />
Expand Down Expand Up @@ -183,4 +187,4 @@
<Folder Include="Data\FIDO2\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
</Project>
65 changes: 59 additions & 6 deletions Src/DSInternals.Common/Data/DPAPI/KdsRootKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using DSInternals.Common.Interop;

namespace DSInternals.Common.Data
{
Expand All @@ -10,11 +11,16 @@ namespace DSInternals.Common.Data
/// </summary>
public class KdsRootKey
{
private const int L0KeyIteration = 1;
private const int L1KeyIteration = 32;
private const int L2KeyIteration = 32;

private int? version;
private DateTime ?creationTime;
private DateTime ?effectiveTime;
private byte[] privateKey;
private string kdfAlgorithmName;
private byte[] rawKdfParameters;
private string secretAgreementAlgorithmName;
private byte[] secretAgreementAlgorithmParam;
private int? privateKeyLength;
Expand Down Expand Up @@ -53,9 +59,7 @@ public KdsRootKey(DirectoryObject dsObject)
dsObject.ReadAttribute(CommonDirectoryAttributes.KdsKdfAlgorithm, out this.kdfAlgorithmName);

// KDF algorithm parameters (only 1 in current implementation)
byte[] rawKdfParams;
dsObject.ReadAttribute(CommonDirectoryAttributes.KdsKdfParameters, out rawKdfParams);
this.KdfParameters = ParseKdfParameters(rawKdfParams);
dsObject.ReadAttribute(CommonDirectoryAttributes.KdsKdfParameters, out this.rawKdfParameters);

// Secret agreement algorithm
dsObject.ReadAttribute(CommonDirectoryAttributes.KdsSecretAgreementAlgorithm, out this.secretAgreementAlgorithmName);
Expand Down Expand Up @@ -148,8 +152,10 @@ public string KdfAlgorithm
/// </summary>
public Dictionary<uint, string> KdfParameters
{
get;
private set;
get
{
return ParseKdfParameters(this.rawKdfParameters);
}
}

/// <summary>
Expand Down Expand Up @@ -196,6 +202,53 @@ public int SecretAgreementPrivateKeyLength
}
}

public byte[] ComputeL0Key(int l0KeyId)
{
return ComputeL0Key(
this.KeyId,
this.KeyValue,
this.KdfAlgorithm,
this.rawKdfParameters,
l0KeyId
);
}

public static byte[] ComputeL0Key(
Guid kdsRootKeyId,
byte[] kdsRootKey,
string kdfAlgorithm,
byte[] kdfParameters,
int l0KeyId)
{
var result = NativeMethods.GenerateKDFContext(
kdsRootKeyId,
l0KeyId,
-1,
-1,
GroupKeyLevel.L0,
out byte[] kdfContext,
out int counterOffset
);

Validator.AssertSuccess(result);

result = NativeMethods.GenerateDerivedKey(
kdfAlgorithm,
kdfParameters,
kdsRootKey,
kdfContext,
null,
null,
L0KeyIteration,
out byte[] l0Key,
out string invalidAtribute
);

Validator.AssertSuccess(result);

return l0Key;
}

public static Dictionary<uint,string> ParseKdfParameters(byte[] blob)
{
if(blob == null || blob.Length == 0)
Expand Down Expand Up @@ -259,4 +312,4 @@ public static void ParseSecretAgreementParameters(byte[] blob)
}
}
}
}
}
121 changes: 121 additions & 0 deletions Src/DSInternals.Common/Data/DPAPI/ProtectionKeyIdentifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
namespace DSInternals.Common.Data
{
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

/// <summary>
/// The Protection Key Identifier data structure is used to store metadata about keys used to cryptographically wrap DPAPI-NG encryption keys and to derive managed passwords.
/// </summary>
/// <see>https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/98a575da-ca48-4afd-ba79-f77a8bed4e4e</see>
public class ProtectionKeyIdentifier
{
private const string KdsKeyMagic = "KDSK";
private const int ExpectedVersion = 1;
private const int StructureHeaderLength = 9 * sizeof(int) + 16;

public int L0KeyId
{
get;
private set;
}

public int L1KeyId
{
get;
private set;
}

public int L2KeyId
{
get;
private set;
}

public Guid RootKeyId
{
get;
private set;
}

public string DomainName
{
get;
private set;
}

public string ForestName
{
get;
private set;
}

public ProtectionKeyIdentifier(byte[] blob)
{
Validator.AssertMinLength(blob, StructureHeaderLength, nameof(blob));

using (Stream stream = new MemoryStream(blob))
{
using (BinaryReader reader = new BinaryReader(stream))
{
// Version must be 0x00000001
int version = reader.ReadInt32();
Validator.AssertEquals(ExpectedVersion, version, nameof(version));

// Magic must be 0x4B53444B
byte[] binaryMagic = reader.ReadBytes(sizeof(int));
string magic = ASCIIEncoding.ASCII.GetString(binaryMagic);
Validator.AssertEquals(KdsKeyMagic, magic, nameof(magic));

// Flags must be 0x00000000
int flags = reader.ReadInt32();

// An L0 index
this.L0KeyId = reader.ReadInt32();

// An L1 index
this.L1KeyId = reader.ReadInt32();

// An L2 index
this.L2KeyId = reader.ReadInt32();

// A root key identifier
byte[] binaryRootKeyId = reader.ReadBytes(Marshal.SizeOf(typeof(Guid)));
this.RootKeyId = new Guid(binaryRootKeyId);

// Variable data lengths
int additionalInfoLength = reader.ReadInt32();
int domainNameLength = reader.ReadInt32();
int forestNameLength = reader.ReadInt32();

// Validate variable data length
int expectedLength = StructureHeaderLength + additionalInfoLength + domainNameLength + forestNameLength;
Validator.AssertMinLength(blob, expectedLength, nameof(blob));

if (additionalInfoLength > 0)
{
// Additional info used in key derivation
byte[] additionalInfo = reader.ReadBytes(additionalInfoLength);
}

if(domainNameLength > 0)
{
// DNS-style name of the Active Directory domain in which this identifier was created.
byte[] binaryDomainName = reader.ReadBytes(domainNameLength);
// Trim \0
this.DomainName = Encoding.Unicode.GetString(binaryDomainName, 0, binaryDomainName.Length - sizeof(char));
}

if(forestNameLength > 0)
{
// DNS-style name of the Active Directory forest in which this identifier was created.
byte[] binaryForestName = reader.ReadBytes(forestNameLength);
// Trim \0
this.ForestName = Encoding.Unicode.GetString(binaryForestName, 0, binaryForestName.Length - sizeof(char));
}
}
}
}
}
}
Loading

0 comments on commit 13385b8

Please sign in to comment.