DSInternals/Src/DSInternals.Common/Data/DPAPIBackupKey.cs

179 lines
7.4 KiB
C#

namespace DSInternals.Common.Data
{
using DSInternals.Common.Cryptography;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.Text.RegularExpressions;
public class DPAPIBackupKey
{
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";
private const string RSAKeyFileNameFormat = "ntds_capi_{0}.pfx";
private const string LegacyKeyFileNameFormat = "ntds_legacy_{0}.key";
private const string UnknownKeyFileNameFormat = "ntds_unknown_{0}_{1}.key";
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);
this.RawKeyData = 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.RawKeyData);
break;
case PreferredLegacyKeyPointerName:
this.Type = DPAPIBackupKeyType.PreferredLegacyKeyPointer;
// Interpret the raw data as Guid
this.KeyId = new Guid(this.RawKeyData);
break;
default:
// Actual Key, so we parse its Guid and version
this.KeyId = Guid.Parse(keyName);
int version = BitConverter.ToInt32(this.RawKeyData, KeyVersionOffset);
switch(version)
{
case 1:
this.Type = DPAPIBackupKeyType.LegacyKey;
// Cut the version out of the data
this.RawKeyData = this.RawKeyData.Cut(KeyVersionSize);
break;
case 2:
this.Type = DPAPIBackupKeyType.RSAKey;
// Combine the certificate and key into PFX and replace the original decrypted data
this.RawKeyData = ConvertRSASecretToPFX(this.RawKeyData);
break;
}
break;
}
}
public DPAPIBackupKeyType Type
{
get;
private set;
}
public string DistinguishedName
{
get;
private set;
}
public byte[] RawKeyData
{
get;
private set;
}
public Guid KeyId
{
get;
private set;
}
public void SaveTo(string directoryPath)
{
string fileName;
switch(this.Type)
{
case DPAPIBackupKeyType.RSAKey:
fileName = String.Format(RSAKeyFileNameFormat, this.KeyId);
break;
case DPAPIBackupKeyType.LegacyKey:
fileName = String.Format(LegacyKeyFileNameFormat, this.KeyId);
break;
case DPAPIBackupKeyType.Unknown:
// Generate an additional random ID to prevent potential filename conflicts
int rnd = new Random().Next();
fileName = String.Format(UnknownKeyFileNameFormat, this.KeyId, rnd);
break;
case DPAPIBackupKeyType.PreferredLegacyKeyPointer:
case DPAPIBackupKeyType.PreferredRSAKeyPointer:
default:
// Do not save these pointer keys
return;
}
Validator.AssertDirectoryExists(directoryPath);
string filePath = Path.Combine(directoryPath, fileName);
File.WriteAllBytes(filePath, this.RawKeyData);
}
public void Save()
{
this.SaveTo(Directory.GetCurrentDirectory());
}
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);
}
private static byte[] ConvertRSASecretToPFX(byte[] secret)
{
int privateKeySize = BitConverter.ToInt32(secret, RSAPrivateKeySizeOffset);
int certificateSize = BitConverter.ToInt32(secret, RSACertificateSizeOffset);
// TODO: Test that sizes match the array
byte[] binaryPrivateKey = secret.Cut(RSAPrivateKeyOffset, privateKeySize);
byte[] binaryCertificate = secret.Cut(RSAPrivateKeyOffset + privateKeySize, certificateSize);
// The PFX export only works if the key is stored in a named container
var cspParameters = new CspParameters();
cspParameters.KeyContainerName = TemporaryKeyContainerName;
using(var privateKey = new RSACryptoServiceProvider(cspParameters))
{
// Make the key temporary
privateKey.PersistKeyInCsp = false;
privateKey.ImportCspBlob(binaryPrivateKey);
// Combine the private and public keys
var certificate = new X509Certificate2(binaryCertificate);
certificate.PrivateKey = privateKey;
// Convert to binary PFX
return certificate.Export(X509ContentType.Pfx);
}
}
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;
}
}
}