From 285a3720e350d65434cf1cb618c2425375fd95c1 Mon Sep 17 00:00:00 2001 From: Michael Grafnetter Date: Sat, 14 Jul 2018 12:17:41 +0200 Subject: [PATCH] Added unit tests for DPAPI backup keys --- .../DPAPIBackupKeyTester.cs | 77 ++++++++++++++++++ .../DSInternals.Common.Test.csproj | 1 + .../Data/DPAPI/DPAPIBackupKey.cs | 80 ++++++++++--------- 3 files changed, 120 insertions(+), 38 deletions(-) create mode 100644 Src/DSInternals.Common.Test/DPAPIBackupKeyTester.cs diff --git a/Src/DSInternals.Common.Test/DPAPIBackupKeyTester.cs b/Src/DSInternals.Common.Test/DPAPIBackupKeyTester.cs new file mode 100644 index 0000000..4f97ad3 --- /dev/null +++ b/Src/DSInternals.Common.Test/DPAPIBackupKeyTester.cs @@ -0,0 +1,77 @@ +using DSInternals.Common.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Security.Principal; + +namespace DSInternals.Common.Test +{ + [TestClass] + public class DPAPIBackupKeyTester + { + [TestMethod] + public void DPAPIBackupKey_RSAKey() + { + // Test vector + byte[] blob = "0200000094040000EC0200000702000000A400005253413200080000010001007F7236400B68C070E3767D331080B5136B7AF17F4A8FC220CA4C73F6BC65EEE194D5015DDFD83D0980463C6218A7B8E1E42947439B5B904028ECA5DEDD57F1C5C8A7A05715894D4690D9C9444D76D75832D4261842B9F897F6A311412C7A8DBE1FAEE513F9960ADB8FB68D00E4D8D7F35F86486FCE3A75773E7F8260829319ACC389277F25C9F26C1BF0902710460FDA8C675650761FF2A40AE67896BEB416FC994826E4B4B6971E2AE42BD901B42D23BB01809AA1C304404972C20D1CB768BE4631E113293E82DE5CE862B010F4EB450D2D92F640A6A115A8A9E21CA6709BE56A4A88A12540821B6D33BBEBEC2B2A5329EB308F9E487729F9438193B45FFDBA31F1A3FFCABF75E85A14B1FF863260F2D5483BB8260E38F2F212988673D2369535257A84E1B5E6CBAB4BB90263E8E18DF751ED99C37CAEC2BFF81F77047284AEEC6472636CDCCC36AE5C5634CA83A67BDB6774BE1F1551C01D2530D7D55C92273A619942E5DD3D67F1FFC7333D8F49B30BF1572EF1E86C592BE4F83943F63DE2AF3259A08B7ADE2446D0B42A260E4DD740F85C06C343ECB152D12F2B3183A384EF2E263442F089219581C43ED91FAC602AC316A5973844DBB1BC6F8F7BE4A02F430466586C31CD0E73904143D0CD8902D7F941BF9A96F81099813EEBD8101060661AE53442C1EECD85E2F9B4E531E3ED466A49B0C3B5134FCF46ED29ABB495D35139F953D39A6A23109A406EFB48CCC545A98DAD9B2ACACFDF5C57527CC006AB419B787E23F837C234FC2A5FFBFEEF369E056EBD7AD5EE53BB87CE7927EBD7345E1C52A0C95BAA449BBDDCE645728FFE0B70A21BF6A4667664FC5AC7F4DB89EC803CD5FA105973580CE6DE55FC5E5FDE2AA2AB191B82A949399473AB1AEEB4C5E900B7FE0C273BFA7F0E0595A5496A844D459F7A23524EA8816F4599115BA991FB4E2C1ADD1C973E4233B21C0B94433ADABACB86D2592CF1CC0CD6CA391EC0274C13BE17D998F99300E4AFB6A59888DA69FD4D2BBBB0885E4064AE49E621C4D24DE617E161C2447F4B1953E90C35D5BE3538633D628B5C2242E9A649EA63877C733CCAC3BF50F7ED657B30BA2836155026B5124BE28B4CA75ECEA01B86EEC37101B2F828484015E356CEB1CF718E57B48047706BCCAFB2E9F5CEC9C60EA6F4A9180429DF9D815CCD4A84C81D2FE8CADFAAF2B97AF7D4B62B4A137D497F70F8CD8B1C8AF049C57142578AB075E971DE63714A82DD74EBD2793F1751E01C3B8C84B11BDB0C79CA59079EF27A4D5754C7AA6B185B04A41609A651E85A00C4194960B02CE39EF3AACF8C2C6F23CF40F780A51EA4DB610FCB2BE63C5BC29EFC14654AAC7231A5FD12647682697376808DCB540DAC1D1E2FB4BDA228495CA9F1987D48DE092BC01D4797651FE18ED0B0BB448F2D262AB4185FA10B9AAB9C66D9551902CCABB1D0473C9B0FF91DD850971ABF7AA2DEA5E4F10A9A93A263A715AB4CBEB7C4B445131E98B3A2117CA09931CFD9AF8DBC0683F0BAB4ED4EE608A121FE3065C223A4ED4634B45BE774BA1C4B49A6C7AFD68F908210177A333C1F7DB6EBE3BC278C80B07339FB10D64A5BE44E805AB5668C3B67A9B6F25AE99BA3B9976EA259308202E8308201D4A00302010202104534203090252F90493AF87CEFE756EC300906052B0E03021D05003021311F301D06035504031316410064006100740075006D002E0063006F006D000000301E170D3136313032363136333630375A170D3137313032363136333630375A3021311F301D06035504031316410064006100740075006D002E0063006F006D00000030820122300D06092A864886F70D01010105000382010F003082010A0282010100BAFD5FB4938143F92977489E8F30EB29532A2BECEBBB336D1B824025A1884A6AE59B70A61CE2A9A815A1A640F6922D0D45EBF410B062E85CDE823E2913E13146BE68B71C0DC272494004C3A19A8001BB232DB401D92BE42A1E97B6B4E4264899FC16B4BE9678E60AA4F21F765056678CDA0F46102790F01B6CF2C9257F2789C3AC19938260827F3E77753ACE6F48865FF3D7D8E4008DB68FDB0A96F913E5AE1FBE8D7A2C4111A3F697F8B9421826D43258D7764D44C9D990464D891557A0A7C8C5F157DDDEA5EC2840905B9B434729E4E1B8A718623C4680093DD8DF5D01D594E1EE65BCF6734CCA20C28F4A7FF17A6B13B58010337D76E370C0680B4036727F0203010001811100EC56E7EF7CF83A49902F259030203445821100EC56E7EF7CF83A49902F259030203445300906052B0E03021D050003820101009FDA525ABEC73C47FAC9E77588D36F8D505B2B0558FF9D2B0EE13295974598CB7003836C037493218F032AD7CFED40DA6F99A7620A9CE0517752A18D56D7D6F47969FDFED28DE8C982E499924C5F8843528C099C8D183D6DC4D77283B9700773084B7A4F11CDF4A31A32B198B824C904626D4C3C192133D18AA8C376C1E49C342FEC949E3C8D1CB1C6E9E914015AB61B59BD7CA5E9C61CCD02D65D09DC511D7F0F8B7E63501498F942DBE564139848BE3C15931CC85D599E9CC1638E4E1DD65F25F2CACD2F40542341E2438E4762DFE80A96851EAB51D0E792CFFEA855E7563B770F8FD31AB288FDFA60A9B4B785A32AC2D5644FD32241BB340406DAF38B45BE".HexToBinary(); + string distinguishedName = "CN=BCKUPKEY_efe756ec-f87c-493a-902f-259030203445 Secret,CN=System,DC=Adatum,DC=com"; + + // Parse the input + var key = new DPAPIBackupKey(distinguishedName, blob); + + // Validate the results + Assert.AreEqual(DPAPIBackupKeyType.RSAKey, key.Type); + Assert.AreEqual(Guid.Parse("efe756ec-f87c-493a-902f-259030203445"), key.KeyId); + Assert.AreEqual("ntds_capi_efe756ec-f87c-493a-902f-259030203445.pvk", key.FilePath); + } + + [TestMethod] + public void DPAPIBackupKey_LegacyKey() + { + // Test vector + byte[] blob = "01000000B97B2D146BB28CB7BAE5E6D31079AA88DAE5A771844FFC7E27EC55633AE513EBDDE29BE619E4D0CF7308DC360F4B63ED87CF9CBC382A56B924C1ADFF4958100AE441B79B83BB8F5649E18C13532831F892C86FE47D71C3AC554A029F3A61841D93CF8208429FE130F059AA40D368B770CDD4BF1EB17876109DCE0F4E6087658994F3EDF5E1589F3C8484370A351741D1BDDF2BA14444E2D822A4A085606F09E341D407969F742D498C462E9147CFBEDF035CF4B49413860C402C4139EACD81C7508EEE1F061D3C40F6B811E89178C617CD598D76D0A776F7D3A31BD8124EDF55B197DBBE5F30416FC62B6A3327148DDC23312D61FEC0CB3E572EC32346BC9A91".HexToBinary(); + string distinguishedName = "CN=BCKUPKEY_585c52d5-1778-4d43-9d7b-c22b4f7e5fa4 Secret,CN=System,DC=Adatum,DC=com"; + + // Parse the input + var key = new DPAPIBackupKey(distinguishedName, blob); + + // Validate the results + Assert.AreEqual(DPAPIBackupKeyType.LegacyKey, key.Type); + Assert.AreEqual(Guid.Parse("585c52d5-1778-4d43-9d7b-c22b4f7e5fa4"), key.KeyId); + Assert.AreEqual("ntds_legacy_585c52d5-1778-4d43-9d7b-c22b4f7e5fa4.key", key.FilePath); + } + + [TestMethod] + public void DPAPIBackupKey_PreferredRSAKeyPointer() + { + // Test vector + byte[] blob = "ec56e7ef7cf83a49902f259030203445".HexToBinary(); + string distinguishedName = "CN=BCKUPKEY_PREFERRED Secret,CN=System,DC=Adatum,DC=com"; + + // Parse the input + var key = new DPAPIBackupKey(distinguishedName, blob); + + // Validate the results + Assert.AreEqual(DPAPIBackupKeyType.PreferredRSAKeyPointer, key.Type); + Assert.AreEqual(Guid.Parse("efe756ec-f87c-493a-902f-259030203445"), key.KeyId); + Assert.IsNull(key.FilePath); + Assert.IsNull(key.KiwiCommand); + } + + [TestMethod] + public void DPAPIBackupKey_PreferredLegacyKeyPointer() + { + // Test vector + byte[] blob = "d5525c587817434d9d7bc22b4f7e5fa4".HexToBinary(); + string distinguishedName = "CN=BCKUPKEY_P Secret,CN=System,DC=Adatum,DC=com"; + + // Parse the input + var key = new DPAPIBackupKey(distinguishedName, blob); + + // Validate the results + Assert.AreEqual(DPAPIBackupKeyType.PreferredLegacyKeyPointer, key.Type); + Assert.AreEqual(Guid.Parse("585c52d5-1778-4d43-9d7b-c22b4f7e5fa4"), key.KeyId); + Assert.IsNull(key.FilePath); + Assert.IsNull(key.KiwiCommand); + } + } +} \ No newline at end of file diff --git a/Src/DSInternals.Common.Test/DSInternals.Common.Test.csproj b/Src/DSInternals.Common.Test/DSInternals.Common.Test.csproj index d044bcc..1b64c7a 100644 --- a/Src/DSInternals.Common.Test/DSInternals.Common.Test.csproj +++ b/Src/DSInternals.Common.Test/DSInternals.Common.Test.csproj @@ -54,6 +54,7 @@ + diff --git a/Src/DSInternals.Common/Data/DPAPI/DPAPIBackupKey.cs b/Src/DSInternals.Common/Data/DPAPI/DPAPIBackupKey.cs index 8bebb4b..3eda70a 100644 --- a/Src/DSInternals.Common/Data/DPAPI/DPAPIBackupKey.cs +++ b/Src/DSInternals.Common/Data/DPAPI/DPAPIBackupKey.cs @@ -24,7 +24,6 @@ private const string RSAP12FileNameFormat = "ntds_capi_{0}.pfx"; private const string LegacyKeyFileNameFormat = "ntds_legacy_{0}.key"; private const string UnknownKeyFileNameFormat = "ntds_unknown_{0}_{1}.key"; - private const string DummyNamingContext = "DC=unknown,DC=int"; private const string KiwiCommandFormat = "REM Add this parameter to at least the first dpapi::masterkey command: /pvk:\"{0}\""; private const int PVKHeaderSize = 6 * sizeof(int); private const uint PVKHeaderMagic = 0xb0b5f11e; @@ -41,50 +40,19 @@ // Decrypt the secret value byte[] encryptedSecret; dsObject.ReadAttribute(CommonDirectoryAttributes.CurrentValue, out encryptedSecret); - this.Data = pek.DecryptSecret(encryptedSecret); + byte[] decryptedBlob = pek.DecryptSecret(encryptedSecret); - // Parse DN to get key ID or pointer type: - this.DistinguishedName = dsObject.DistinguishedName; - var keyName = GetSecretNameFromDN(this.DistinguishedName); - - switch(keyName) - { - case null: - // We could not parse the DN, so exit with Unknown as the key type - this.Type = DPAPIBackupKeyType.Unknown; - break; - case PreferredRSAKeyPointerName: - this.Type = DPAPIBackupKeyType.PreferredRSAKeyPointer; - // Interpret the raw data as Guid - this.KeyId = new Guid(this.Data); - break; - case PreferredLegacyKeyPointerName: - this.Type = DPAPIBackupKeyType.PreferredLegacyKeyPointer; - // Interpret the raw data as Guid - this.KeyId = new Guid(this.Data); - break; - default: - // Actual Key, so we parse its Guid and version - this.KeyId = Guid.Parse(keyName); - this.Type = (DPAPIBackupKeyType)BitConverter.ToInt32(this.Data, KeyVersionOffset); - break; - } + // Initialize properties + this.Initialize(dsObject.DistinguishedName, decryptedBlob); } - // TODO: Create unit tests for DPAPIBackupKey - public DPAPIBackupKey(Guid id, byte[] blob) + public DPAPIBackupKey(string distinguishedName, byte[] blob) { // Validate the input + Validator.AssertNotNullOrWhiteSpace(distinguishedName, "distinguishedName"); Validator.AssertNotNull(blob, "blob"); - Validator.AssertMinLength(blob, KeyVersionSize, "blob"); - // Process the parameters - this.KeyId = id; - this.Data = blob; - this.Type = (DPAPIBackupKeyType)BitConverter.ToInt32(this.Data, KeyVersionOffset); - - // Generate some dummy distinguished name - this.DistinguishedName = String.Format(BackupKeyDNFormat, this.KeyId, DummyNamingContext); + this.Initialize(distinguishedName, blob); } public override string FilePath @@ -185,6 +153,42 @@ } } + /// + /// Object initializer that is shared between multiple constructors. + /// + /// Distinguished name of the DPAPI backup key object. + /// Decrypted data blob. + private void Initialize(string distinguishedName, byte[] blob) + { + this.DistinguishedName = distinguishedName; + this.Data = blob; + + // Parse DN to get key ID or pointer type: + var keyName = GetSecretNameFromDN(distinguishedName); + switch (keyName) + { + case null: + // We could not parse the DN, so exit with Unknown as the key type + this.Type = DPAPIBackupKeyType.Unknown; + break; + case PreferredRSAKeyPointerName: + this.Type = DPAPIBackupKeyType.PreferredRSAKeyPointer; + // Interpret the raw data as Guid + this.KeyId = new Guid(blob); + break; + case PreferredLegacyKeyPointerName: + this.Type = DPAPIBackupKeyType.PreferredLegacyKeyPointer; + // Interpret the raw data as Guid + this.KeyId = new Guid(blob); + break; + default: + // Actual Key, so we parse its Guid and version + this.KeyId = Guid.Parse(keyName); + this.Type = (DPAPIBackupKeyType)BitConverter.ToInt32(blob, KeyVersionOffset); + break; + } + } + public static string GetKeyDN(Guid keyId, string domainDN) { return String.Format(BackupKeyDNFormat, keyId, domainDN);