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;
public class DPAPIBackupKey : DPAPIObject
{
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 BackupKeyNameFormat = "G$BCKUPKEY_{0}";
private const string BackupKeyDNFormat = "CN=BCKUPKEY_{0} Secret,CN=System,{1}";
// Examples:
// CN=BCKUPKEY_P Secret,CN=System,DC=contoso,DC=com
// CN=BCKUPKEY_PREFERRED Secret,CN=System,DC=contoso,DC=com
// CN=BCKUPKEY_PREFERRED Secret\0ACNF:26c8edbb-6b48-4f11-9e13-9ddbccedab5a,CN=System,DC=contoso,DC=com
// CN=BCKUPKEY_ac9e427c-fa85-4b78-8db1-771d94c03bad Secret,CN=System,DC=contoso,DC=com
private const string BackupKeyDNRegex = "CN=BCKUPKEY_(.+) Secret(\\\\0ACNF:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})?,CN=System,.+";
private const string PreferredLegacyKeyPointerName = "P";
private const string PreferredRSAKeyPointerName = "PREFERRED";
private const string TemporaryKeyContainerName = "DSInternals";
private const string RSAKeyFileNameFormat = "ntds_capi_{0}.pvk";
private const string RSACertFileNameFormat = "ntds_capi_{0}.cer";
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 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;
private const uint PVKHeaderVersion = 0;
private const uint PVKHeaderKeySpec = 1; // = AT_KEYEXCHANGE
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);
byte[] decryptedBlob = pek.DecryptSecret(encryptedSecret);
// Initialize properties
this.Initialize(dsObject.DistinguishedName, decryptedBlob);
}
public DPAPIBackupKey(string distinguishedName, byte[] blob)
{
// Validate the input
Validator.AssertNotNullOrWhiteSpace(distinguishedName, "distinguishedName");
Validator.AssertNotNull(blob, "blob");
this.Initialize(distinguishedName, blob);
}
public DPAPIBackupKey(Guid keyId, byte[] blob)
{
Validator.AssertNotNull(blob, "blob");
this.KeyId = keyId;
this.Type = GetKeyType(blob);
this.Data = blob;
}
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;
}
}
public DPAPIBackupKeyType Type
{
get;
private set;
}
public string DistinguishedName
{
get;
private set;
}
public Guid KeyId
{
get;
private set;
}
public override void Save(string directoryPath)
{
// The target directory must exist
Validator.AssertDirectoryExists(directoryPath);
string fullFilePath;
switch (this.Type)
{
case DPAPIBackupKeyType.RSAKey:
// 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
fullFilePath = Path.Combine(directoryPath, this.FilePath);
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);
break;
case DPAPIBackupKeyType.LegacyKey:
// We create one KEY file, while cropping out the key version.
fullFilePath = Path.Combine(directoryPath, this.FilePath);
File.WriteAllBytes(fullFilePath, this.Data.Cut(KeyVersionSize));
break;
case DPAPIBackupKeyType.Unknown:
fullFilePath = Path.Combine(directoryPath, this.FilePath);
File.WriteAllBytes(fullFilePath, this.Data);
break;
case DPAPIBackupKeyType.PreferredLegacyKeyPointer:
case DPAPIBackupKeyType.PreferredRSAKeyPointer:
default:
// Do not save these pointer keys
break;
}
}
///
/// 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 = GetKeyType(blob);
break;
}
}
public static string PreferredRSAKeyName
{
get
{
return String.Format(BackupKeyNameFormat, PreferredRSAKeyPointerName);
}
}
public static string PreferredLegacyKeyName
{
get
{
return String.Format(BackupKeyNameFormat, PreferredLegacyKeyPointerName);
}
}
public static string GetKeyDN(Guid keyId, string domainDN)
{
return String.Format(BackupKeyDNFormat, keyId, domainDN);
}
public static string GetKeyName(Guid keyId)
{
return String.Format(BackupKeyNameFormat, keyId);
}
public static string GetPreferredRSAKeyPointerDN(string domainDN)
{
return String.Format(BackupKeyDNFormat, PreferredRSAKeyPointerName, domainDN);
}
public static string GetPreferredLegacyKeyPointerDN(string domainDN)
{
return String.Format(BackupKeyDNFormat, PreferredLegacyKeyPointerName, domainDN);
}
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)
{
// The PFX export only works if the key is stored in a named container
var cspParameters = new CspParameters();
cspParameters.KeyContainerName = TemporaryKeyContainerName;
using (var keyContainer = new RSACryptoServiceProvider(cspParameters))
{
// Make the key temporary
keyContainer.PersistKeyInCsp = false;
keyContainer.ImportCspBlob(privateKey);
// Combine the private and public keys
var combinedCertificate = new X509Certificate2(certificate);
combinedCertificate.PrivateKey = keyContainer;
// Convert to binary PFX
return combinedCertificate.Export(X509ContentType.Pfx);
}
}
private static byte[] EncapsulatePvk(byte[] privateKey)
{
// 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;
}
private static DPAPIBackupKeyType GetKeyType(byte[] blob)
{
return (DPAPIBackupKeyType)BitConverter.ToInt32(blob, KeyVersionOffset);
}
}
}