2015-12-26 22:44:43 +00:00
|
|
|
|
namespace DSInternals.Common.Data
|
|
|
|
|
{
|
|
|
|
|
using DSInternals.Common.Cryptography;
|
|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
using System.Security.Cryptography.X509Certificates;
|
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
|
2017-03-25 17:25:22 +00:00
|
|
|
|
public class DPAPIBackupKey : DPAPIObject
|
2015-12-26 22:44:43 +00:00
|
|
|
|
{
|
|
|
|
|
private const int KeyVersionOffset = 0;
|
|
|
|
|
private const int KeyVersionSize = sizeof(int);
|
|
|
|
|
private const int RSAPrivateKeySizeOffset = KeyVersionOffset + KeyVersionSize;
|
|
|
|
|
private const int RSACertificateSizeOffset = RSAPrivateKeySizeOffset + sizeof(int);
|
|
|
|
|
private const int RSAPrivateKeyOffset = RSACertificateSizeOffset + sizeof(int);
|
|
|
|
|
private const string BackupKeyDNFormat = "CN=BCKUPKEY_{0} Secret,CN=System,{1}";
|
|
|
|
|
private const string BackupKeyDNRegex = "CN=BCKUPKEY_(.*) Secret,CN=System,.*";
|
|
|
|
|
private const string PreferredLegacyKeyPointerName = "P";
|
|
|
|
|
private const string PreferredRSAKeyPointerName = "PREFERRED";
|
|
|
|
|
private const string TemporaryKeyContainerName = "DSInternals";
|
2017-03-25 17:25:22 +00:00
|
|
|
|
private const string RSAKeyFileNameFormat = "ntds_capi_{0}.pvk";
|
|
|
|
|
private const string RSACertFileNameFormat = "ntds_capi_{0}.cer";
|
|
|
|
|
private const string RSAP12FileNameFormat = "ntds_capi_{0}.pfx";
|
2015-12-26 22:44:43 +00:00
|
|
|
|
private const string LegacyKeyFileNameFormat = "ntds_legacy_{0}.key";
|
|
|
|
|
private const string UnknownKeyFileNameFormat = "ntds_unknown_{0}_{1}.key";
|
2018-07-13 19:50:59 +00:00
|
|
|
|
private const string KiwiCommandFormat = "REM Add this parameter to at least the first dpapi::masterkey command: /pvk:\"{0}\"";
|
2017-03-25 17:25:22 +00:00
|
|
|
|
private const int PVKHeaderSize = 6 * sizeof(int);
|
|
|
|
|
private const uint PVKHeaderMagic = 0xb0b5f11e;
|
|
|
|
|
private const uint PVKHeaderVersion = 0;
|
|
|
|
|
private const uint PVKHeaderKeySpec = 1; // = AT_KEYEXCHANGE
|
|
|
|
|
|
2015-12-26 22:44:43 +00:00
|
|
|
|
public DPAPIBackupKey(DirectoryObject dsObject, DirectorySecretDecryptor pek)
|
|
|
|
|
{
|
|
|
|
|
// Parameter validation
|
|
|
|
|
Validator.AssertNotNull(dsObject, "dsObject");
|
|
|
|
|
Validator.AssertNotNull(pek, "pek");
|
|
|
|
|
// TODO: Test Object type
|
|
|
|
|
|
|
|
|
|
// Decrypt the secret value
|
|
|
|
|
byte[] encryptedSecret;
|
|
|
|
|
dsObject.ReadAttribute(CommonDirectoryAttributes.CurrentValue, out encryptedSecret);
|
2018-07-14 10:17:41 +00:00
|
|
|
|
byte[] decryptedBlob = pek.DecryptSecret(encryptedSecret);
|
2015-12-26 22:44:43 +00:00
|
|
|
|
|
2018-07-14 10:17:41 +00:00
|
|
|
|
// Initialize properties
|
|
|
|
|
this.Initialize(dsObject.DistinguishedName, decryptedBlob);
|
2015-12-26 22:44:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-07-14 10:17:41 +00:00
|
|
|
|
public DPAPIBackupKey(string distinguishedName, byte[] blob)
|
2018-07-13 19:50:59 +00:00
|
|
|
|
{
|
|
|
|
|
// Validate the input
|
2018-07-14 10:17:41 +00:00
|
|
|
|
Validator.AssertNotNullOrWhiteSpace(distinguishedName, "distinguishedName");
|
2018-07-13 19:50:59 +00:00
|
|
|
|
Validator.AssertNotNull(blob, "blob");
|
|
|
|
|
|
2018-07-14 10:17:41 +00:00
|
|
|
|
this.Initialize(distinguishedName, blob);
|
2018-07-13 19:50:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override string FilePath
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
switch(this.Type)
|
|
|
|
|
{
|
|
|
|
|
case DPAPIBackupKeyType.RSAKey:
|
|
|
|
|
// .pvk file
|
|
|
|
|
return String.Format(RSAKeyFileNameFormat, this.KeyId);
|
|
|
|
|
case DPAPIBackupKeyType.LegacyKey:
|
|
|
|
|
// .key file
|
|
|
|
|
return String.Format(LegacyKeyFileNameFormat, this.KeyId);
|
|
|
|
|
case DPAPIBackupKeyType.Unknown:
|
|
|
|
|
// Generate an additional random ID to prevent potential filename conflicts
|
|
|
|
|
int rnd = new Random().Next();
|
|
|
|
|
return String.Format(UnknownKeyFileNameFormat, this.KeyId, rnd);
|
|
|
|
|
default:
|
|
|
|
|
// Saving pointers or other domain key types to files is not supported.
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override string KiwiCommand
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return this.Type == DPAPIBackupKeyType.RSAKey ? String.Format(KiwiCommandFormat, this.FilePath) : null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-12-26 22:44:43 +00:00
|
|
|
|
public DPAPIBackupKeyType Type
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
private set;
|
|
|
|
|
}
|
|
|
|
|
public string DistinguishedName
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
private set;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Guid KeyId
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
private set;
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-13 19:50:59 +00:00
|
|
|
|
public override void Save(string directoryPath)
|
2015-12-26 22:44:43 +00:00
|
|
|
|
{
|
2017-03-25 17:25:22 +00:00
|
|
|
|
// The target directory must exist
|
|
|
|
|
Validator.AssertDirectoryExists(directoryPath);
|
|
|
|
|
|
|
|
|
|
string fullFilePath;
|
|
|
|
|
|
|
|
|
|
switch (this.Type)
|
2015-12-26 22:44:43 +00:00
|
|
|
|
{
|
|
|
|
|
case DPAPIBackupKeyType.RSAKey:
|
2017-03-25 17:25:22 +00:00
|
|
|
|
// Parse the public and private keys
|
|
|
|
|
int privateKeySize = BitConverter.ToInt32(this.Data, RSAPrivateKeySizeOffset);
|
|
|
|
|
int certificateSize = BitConverter.ToInt32(this.Data, RSACertificateSizeOffset);
|
|
|
|
|
|
|
|
|
|
byte[] privateKey = this.Data.Cut(RSAPrivateKeyOffset, privateKeySize);
|
|
|
|
|
byte[] certificate = this.Data.Cut(RSAPrivateKeyOffset + privateKeySize, certificateSize);
|
|
|
|
|
|
|
|
|
|
// Create PVK file
|
2018-07-13 19:50:59 +00:00
|
|
|
|
fullFilePath = Path.Combine(directoryPath, this.FilePath);
|
2017-03-25 17:25:22 +00:00
|
|
|
|
byte[] pvk = EncapsulatePvk(privateKey);
|
|
|
|
|
File.WriteAllBytes(fullFilePath, pvk);
|
|
|
|
|
|
|
|
|
|
// Create PFX file
|
|
|
|
|
byte[] pkcs12 = CreatePfx(certificate, privateKey);
|
|
|
|
|
var pfxFile = String.Format(RSAP12FileNameFormat, this.KeyId);
|
|
|
|
|
fullFilePath = Path.Combine(directoryPath, pfxFile);
|
|
|
|
|
File.WriteAllBytes(fullFilePath, pkcs12);
|
|
|
|
|
|
|
|
|
|
// Create CER file
|
|
|
|
|
var cerFile = String.Format(RSACertFileNameFormat, this.KeyId);
|
|
|
|
|
fullFilePath = Path.Combine(directoryPath, cerFile);
|
|
|
|
|
File.WriteAllBytes(fullFilePath, certificate);
|
2015-12-26 22:44:43 +00:00
|
|
|
|
break;
|
|
|
|
|
case DPAPIBackupKeyType.LegacyKey:
|
2017-03-25 17:25:22 +00:00
|
|
|
|
// We create one KEY file, while cropping out the key version.
|
2018-07-13 19:50:59 +00:00
|
|
|
|
fullFilePath = Path.Combine(directoryPath, this.FilePath);
|
2017-03-25 17:25:22 +00:00
|
|
|
|
File.WriteAllBytes(fullFilePath, this.Data.Cut(KeyVersionSize));
|
2015-12-26 22:44:43 +00:00
|
|
|
|
break;
|
|
|
|
|
case DPAPIBackupKeyType.Unknown:
|
2018-07-13 19:50:59 +00:00
|
|
|
|
fullFilePath = Path.Combine(directoryPath, this.FilePath);
|
2017-03-25 17:25:22 +00:00
|
|
|
|
File.WriteAllBytes(fullFilePath, this.Data);
|
2015-12-26 22:44:43 +00:00
|
|
|
|
break;
|
|
|
|
|
case DPAPIBackupKeyType.PreferredLegacyKeyPointer:
|
|
|
|
|
case DPAPIBackupKeyType.PreferredRSAKeyPointer:
|
|
|
|
|
default:
|
|
|
|
|
// Do not save these pointer keys
|
2017-03-25 17:25:22 +00:00
|
|
|
|
break;
|
2015-12-26 22:44:43 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-14 10:17:41 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Object initializer that is shared between multiple constructors.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="distinguishedName">Distinguished name of the DPAPI backup key object.</param>
|
|
|
|
|
/// <param name="blob">Decrypted data blob.</param>
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-12-26 22:44:43 +00:00
|
|
|
|
public static string GetKeyDN(Guid keyId, string domainDN)
|
|
|
|
|
{
|
|
|
|
|
return String.Format(BackupKeyDNFormat, keyId, domainDN);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string GetPreferredRSAKeyPointerDN(string domainDN)
|
|
|
|
|
{
|
|
|
|
|
return String.Format(BackupKeyDNFormat, PreferredRSAKeyPointerName, domainDN);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string GetPreferredLegacyKeyPointerDN(string domainDN)
|
|
|
|
|
{
|
|
|
|
|
return String.Format(BackupKeyDNFormat, PreferredLegacyKeyPointerName, domainDN);
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-25 17:25:22 +00:00
|
|
|
|
private static string GetSecretNameFromDN(string distinguishedName)
|
|
|
|
|
{
|
|
|
|
|
var match = Regex.Match(distinguishedName, BackupKeyDNRegex);
|
|
|
|
|
bool success = match.Success && (match.Groups.Count == 2);
|
|
|
|
|
return success ? match.Groups[1].Value : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static byte[] CreatePfx(byte[] certificate, byte[] privateKey)
|
2015-12-26 22:44:43 +00:00
|
|
|
|
{
|
|
|
|
|
// The PFX export only works if the key is stored in a named container
|
|
|
|
|
var cspParameters = new CspParameters();
|
|
|
|
|
cspParameters.KeyContainerName = TemporaryKeyContainerName;
|
2017-03-25 17:25:22 +00:00
|
|
|
|
using (var keyContainer = new RSACryptoServiceProvider(cspParameters))
|
2015-12-26 22:44:43 +00:00
|
|
|
|
{
|
|
|
|
|
// Make the key temporary
|
2017-03-25 17:25:22 +00:00
|
|
|
|
keyContainer.PersistKeyInCsp = false;
|
|
|
|
|
keyContainer.ImportCspBlob(privateKey);
|
2015-12-26 22:44:43 +00:00
|
|
|
|
// Combine the private and public keys
|
2017-03-25 17:25:22 +00:00
|
|
|
|
var combinedCertificate = new X509Certificate2(certificate);
|
|
|
|
|
combinedCertificate.PrivateKey = keyContainer;
|
2015-12-26 22:44:43 +00:00
|
|
|
|
// Convert to binary PFX
|
2017-03-25 17:25:22 +00:00
|
|
|
|
return combinedCertificate.Export(X509ContentType.Pfx);
|
2015-12-26 22:44:43 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-03-25 17:25:22 +00:00
|
|
|
|
|
|
|
|
|
private static byte[] EncapsulatePvk(byte[] privateKey)
|
2015-12-26 22:44:43 +00:00
|
|
|
|
{
|
2017-03-25 17:25:22 +00:00
|
|
|
|
// We do a quick and dirty encapsulation of the private key into the PVK format.
|
|
|
|
|
// See: http://www.drh-consultancy.demon.co.uk/pvk.html
|
|
|
|
|
// TODO: Extract PVK code to a distinct class.
|
|
|
|
|
int pvkSize = PVKHeaderSize + privateKey.Length;
|
|
|
|
|
byte[] pvk = new byte[pvkSize];
|
|
|
|
|
|
|
|
|
|
using (var stream = new MemoryStream(pvk, true))
|
|
|
|
|
{
|
|
|
|
|
using (var writer = new BinaryWriter(stream))
|
|
|
|
|
{
|
|
|
|
|
// Write PVK header
|
|
|
|
|
writer.Write(PVKHeaderMagic);
|
|
|
|
|
writer.Write(PVKHeaderVersion);
|
|
|
|
|
writer.Write(PVKHeaderKeySpec);
|
|
|
|
|
writer.Write((int)PrivateKeyEncryptionType.None);
|
|
|
|
|
writer.Write((int)0); // Size of salt
|
|
|
|
|
writer.Write(privateKey.Length);
|
|
|
|
|
|
|
|
|
|
// Write the actual data
|
|
|
|
|
writer.Write(privateKey);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pvk;
|
2015-12-26 22:44:43 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-07-13 19:50:59 +00:00
|
|
|
|
}
|