179 lines
7.4 KiB
C#
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;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|