using System.Globalization;
using System.Net;
using System.Security.Principal;
using DSInternals.Common;
using DSInternals.Common.Cryptography;
using DSInternals.Common.Data;
using DSInternals.Common.Exceptions;
using DSInternals.Common.Kerberos;
using DSInternals.Common.Schema;
using DSInternals.Replication.Interop;
using DSInternals.Replication.Model;
namespace DSInternals.Replication;
///
/// Provides methods for connecting to a domain controller and replicating directory objects.
///
public class DirectoryReplicationClient : IDisposable, IKdsRootKeyResolver
{
///
/// Service principal name (SPN) of the destination server.
///
private const string ServicePrincipalNameFormat = "ldap/{0}";
///
/// Named pipe used for DRSR communication.
///
private const string DrsNamedPipeName = @"\pipe\lsass";
///
/// Prefix of the configuration naming context.
///
private const string ConfigurationNamingContextPrefix = "CN=Configuration,DC=";
///
/// Prefix of the schema naming context.
///
private const string SchemaNamingContextPrefix = "CN=Schema,CN=Configuration,DC=";
///
/// Identifier of Windows Server 2000 dcpromo.
///
private static readonly Guid DcPromoGuid2k = new("6abec3d1-3054-41c8-a362-5a0c5b7d5d71");
///
/// Identifier of Windows Server 2003+ dcpromo.
///
private static readonly Guid DcPromoGuid2k3 = new("6afab99c-6e26-464a-975f-f58f105218bc");
///
/// Non-DC client identifier.
///
private static readonly Guid NtdsApiClientGuid = new("e24d201a-4fd6-11d1-a3da-0000f875ae0d");
private bool _isFullSchemaLoaded = false;
private RpcBinding _rpcBinding;
private DrsConnection _drsConnection;
private IKdsRootKeyResolver _rootKeyResolver;
private readonly Lazy<(string DomainNamingContext, string DNSDomainName, string NetBIOSDomainName)> _domainInfo;
private readonly Lazy _namingContexts;
private readonly Lazy _secretDecryptor;
private EventHandler _sessionKeyChangedHandler;
///
/// The domain naming context of the connected server.
///
public string DomainNamingContext => _domainInfo.Value.DomainNamingContext;
///
/// The DNS domain name of the connected server.
///
public string DNSDomainName => _domainInfo.Value.DNSDomainName;
///
/// The NetBIOS domain name of the connected server.
///
public string NetBIOSDomainName => _domainInfo.Value.NetBIOSDomainName;
///
/// The naming contexts (partitions) hosted by the connected server.
///
public string[] NamingContexts => _namingContexts.Value;
///
/// The configuration naming context of the connected server.
///
public string ConfigurationNamingContext
{
get
{
// TODO: It would be more elegant to load the ConfigNC based on its GUID.
return this.NamingContexts.
Where(context => context.StartsWith(ConfigurationNamingContextPrefix, StringComparison.InvariantCultureIgnoreCase)).
First();
}
}
///
/// The schema naming context of the connected server.
///
public string SchemaNamingContext
{
get
{
return this.NamingContexts.
Where(context => context.StartsWith(SchemaNamingContextPrefix, StringComparison.InvariantCultureIgnoreCase)).
First();
}
}
///
/// Initializes a new instance of the class.
///
/// The FQDN or IP address of the domain controller.
/// The credentials to use for authentication.
public DirectoryReplicationClient(string server, NetworkCredential credential = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(server);
var schema = BaseSchema.Create();
this._rpcBinding = new RpcBinding(server, RpcProtseq.ncacn_ip_tcp);
string spn = String.Format(CultureInfo.InvariantCulture, ServicePrincipalNameFormat, server);
this._rpcBinding.AuthenticateAs(spn, credential, RpcAuthenticationLevel.PacketPrivacy, RpcAuthenticationType.Negotiate);
this._drsConnection = new DrsConnection(this._rpcBinding.DangerousGetHandle(), NtdsApiClientGuid, schema);
// The replication client can fetch root keys. Cache them for performance.
this._rootKeyResolver = new KdsRootKeyCache(this);
// Lazily fetch domain info, naming contexts, and the secret decryptor on first access.
this._domainInfo = new Lazy<(string, string, string)>(this.LoadDomainInfo);
this._namingContexts = new Lazy(() => this._drsConnection.ListNamingContexts());
this._secretDecryptor = new Lazy(() =>
{
var decryptor = new ReplicationSecretDecryptor(this._drsConnection.SessionKey);
// The RPC session key can be renegotiated mid-replication. Forward those changes to the
// decryptor so it always tries the current key (while retaining the previous ones).
this._sessionKeyChangedHandler = (sender, e) => decryptor.ChangeSessionKey(e.SessionKey);
this._drsConnection.SessionKeyChanged += this._sessionKeyChangedHandler;
return decryptor;
});
}
///
/// Gets the replication cursors for the specified naming context.
///
/// The naming context to retrieve replication cursors for.
/// An array of replication cursors.
public ReplicationCursor[] GetReplicationCursors(string namingContext)
{
ArgumentException.ThrowIfNullOrWhiteSpace(namingContext);
return this._drsConnection.GetReplicationCursors(namingContext);
}
///
/// Retrieves all accounts from the current domain partition.
///
/// Optional progress reporter invoked after each replication cycle.
/// The set of properties to retrieve for each account.
/// Token used to cooperatively cancel the replication between cycles.
/// An enumerable collection of directory service accounts.
public IEnumerable GetAccounts(IProgress progress = null, AccountPropertySets propertySets = AccountPropertySets.All, CancellationToken cancellationToken = default)
{
string domainNamingContext = this.DomainNamingContext;
return ReplicateAllObjects(domainNamingContext, progress, cancellationToken)
.Select(dsObject => AccountFactory.CreateAccount(dsObject, this.NetBIOSDomainName, _secretDecryptor.Value, _rootKeyResolver, propertySets))
.Where(account => account != null); // CreateAccount returns null for other object types
}
///
/// Retrieves all directory objects from the specified naming context.
///
/// Partition to replicate.
/// Optional progress reporter invoked after each replication cycle.
/// Token used to cooperatively cancel the replication between cycles.
/// An enumerable collection of directory service objects.
public IEnumerable ReplicateAllObjects(string namingContext, IProgress progress = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(namingContext);
ReplicationCookie currentCookie = new(namingContext);
ReplicationResult result;
int processedObjectCount = 0;
do
{
// Check for cancellation between replication cycles (the native DRS call itself is not interruptible).
cancellationToken.ThrowIfCancellationRequested();
// Perform one replication cycle
result = this._drsConnection.ReplicateAllObjects(currentCookie);
// Report replication progress
processedObjectCount += result.Objects.Count;
progress?.Report(new ReplicationProgress(result.Cookie, processedObjectCount, result.TotalObjectCount));
// Pass-through the returned objects
foreach (var obj in result.Objects)
{
yield return obj;
}
// Update the position of the replication cursor
currentCookie = result.Cookie;
} while (result.HasMoreData);
}
///
/// Retrieves a single account by its object GUID.
///
/// The object GUID of the account to retrieve.
/// The set of properties to retrieve for the account.
/// An enumerable collection of directory service accounts.
/// Thrown when the object is not an account.
public DSAccount GetAccount(Guid objectGuid, AccountPropertySets propertySets = AccountPropertySets.All)
{
ReplicaObject obj = this._drsConnection.ReplicateSingleObject(objectGuid);
var account = AccountFactory.CreateAccount(obj, this.NetBIOSDomainName, _secretDecryptor.Value, _rootKeyResolver, propertySets);
if (account == null)
{
// If the target object is not an account, CreateAccount returns null
throw new DirectoryObjectOperationException("The object is not an account.", objectGuid);
}
return account;
}
///
/// Retrieves a single account by its distinguished name.
///
/// The distinguished name of the account to retrieve.
/// The set of properties to retrieve for the account.
/// An enumerable collection of directory service accounts.
/// Thrown when the object is not an account.
public DSAccount GetAccount(string distinguishedName, AccountPropertySets propertySets = AccountPropertySets.All)
{
ReplicaObject obj = this._drsConnection.ReplicateSingleObject(distinguishedName);
return AccountFactory.CreateAccount(obj, this.NetBIOSDomainName, _secretDecryptor.Value, _rootKeyResolver, propertySets) ??
throw new DirectoryObjectOperationException("The object is not an account.", distinguishedName);
}
///
/// Retrieves a single account by its NT account name.
///
/// The NT account name of the account to retrieve.
/// The set of properties to retrieve for the account.
/// An enumerable collection of directory service accounts.
public DSAccount GetAccount(NTAccount accountName, AccountPropertySets propertySets = AccountPropertySets.All)
{
Guid objectGuid = this._drsConnection.ResolveGuid(accountName);
return this.GetAccount(objectGuid, propertySets);
}
///
/// Retrieves a single account by its security identifier (SID).
///
/// The security identifier (SID) of the account to retrieve.
/// The set of properties to retrieve for the account.
/// An enumerable collection of directory service accounts.
public DSAccount GetAccount(SecurityIdentifier sid, AccountPropertySets propertySets = AccountPropertySets.All)
{
Guid objectGuid = this._drsConnection.ResolveGuid(sid);
return this.GetAccount(objectGuid, propertySets);
}
///
/// Retrieves a single trusted-domain object by name from the current domain.
///
/// The trust object name (CN) to retrieve.
/// The trusted-domain object.
public TrustedDomain GetTrustedDomain(string name)
{
return this.GetTrustedDomain(name, this.DomainNamingContext, this.DNSDomainName);
}
///
/// Retrieves a single trusted-domain object by name.
///
/// The trust object name (CN) to retrieve.
/// The DNS name of the domain containing the trust object.
/// The trusted-domain object.
/// Thrown when the object is not a trusted-domain object.
public TrustedDomain GetTrustedDomain(string name, string domain)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentException.ThrowIfNullOrWhiteSpace(domain);
string domainNamingContext = DistinguishedName.GetDNFromDNSName(domain).ToString();
return this.GetTrustedDomain(name, domainNamingContext, domain);
}
///
/// Retrieves a single trusted-domain object by name.
///
/// The trust object name (CN) to retrieve.
/// The domain naming context containing the trust object.
/// The DNS domain name containing the trust object.
/// The trusted-domain object.
/// Thrown when the object is not a trusted-domain object.
public TrustedDomain GetTrustedDomain(string name, string domainNamingContext, string dnsDomainName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentException.ThrowIfNullOrWhiteSpace(domainNamingContext);
ArgumentException.ThrowIfNullOrWhiteSpace(dnsDomainName);
// TODO: validate the input against injection attacks.
// TODO: Consider using the DistinguishedName class for DN concatenation.
string trustDN = $"CN={name},CN=System,{domainNamingContext}";
var trustObject = this._drsConnection.ReplicateSingleObject(trustDN);
return new TrustedDomain(
trustObject,
dnsDomainName,
this.NetBIOSDomainName,
_secretDecryptor.Value);
}
///
/// Retrieves the KDS root key with the specified identifier.
///
/// The identifier of the KDS root key to retrieve.
/// Whether to suppress the not found exception.
///
public KdsRootKey? GetKdsRootKey(Guid rootKeyId, bool suppressNotFoundException = false)
{
// Derive the full path to the object
// Example: CN=4dd60361-9394-492a-b11d-51a955f02b06,CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services,CN=Configuration,DC=contoso,DC=com
string rootKeyDN = KdsRootKey.GetDistinguishedName(rootKeyId, this.ConfigurationNamingContext);
try
{
var rootKeyObject = _drsConnection.ReplicateSingleObject(rootKeyDN);
return new KdsRootKey(rootKeyObject);
}
catch (DirectoryObjectNotFoundException)
{
if (suppressNotFoundException)
{
return null;
}
else
{
throw;
}
}
}
///
/// Retrieves all DPAPI backup keys from the specified domain partition.
///
/// The distinguished name of the domain partition.
/// An enumerable collection of DPAPI backup keys.
public IEnumerable GetDPAPIBackupKeys(string domainNamingContext)
{
// TODO: Split this function into RSA and Legacy Part so that exception in one of them does not crash the whole process
// Fetch the legacy pointer first, because there is a higher chance that it is present than the RSA one.
string legacyPointerDN = DPAPIBackupKey.GetPreferredLegacyKeyPointerDN(domainNamingContext);
var legacyPointer = this.GetLSASecret(legacyPointerDN);
yield return legacyPointer;
string legacyKeyDN = DPAPIBackupKey.GetKeyDN(legacyPointer.KeyId, domainNamingContext);
var legacyKey = this.GetLSASecret(legacyKeyDN);
yield return legacyKey;
string rsaPointerDN = DPAPIBackupKey.GetPreferredRSAKeyPointerDN(domainNamingContext);
var rsaPointer = this.GetLSASecret(rsaPointerDN);
yield return rsaPointer;
string rsaKeyDN = DPAPIBackupKey.GetKeyDN(rsaPointer.KeyId, domainNamingContext);
var rsaKey = this.GetLSASecret(rsaKeyDN);
yield return rsaKey;
}
///
/// Retrieves a single LSA secret by its distinguished name.
///
/// The distinguished name of the LSA secret.
/// The DPAPI backup key.
private DPAPIBackupKey GetLSASecret(string distinguishedName)
{
var secretObj = this._drsConnection.ReplicateSingleObject(distinguishedName);
return new DPAPIBackupKey(secretObj, _secretDecryptor.Value);
}
///
/// Writes the NGC public key to the specified account.
///
/// The GUID of the account to write the key to.
/// The NGC public key to write.
public void WriteNgcKey(Guid objectGuid, byte[] publicKey)
{
string distinguishedName = this._drsConnection.ResolveDistinguishedName(objectGuid);
this.WriteNgcKey(distinguishedName, publicKey);
}
///
/// Writes the NGC public key to the specified account.
///
/// The name of the account to write the key to.
/// The NGC public key to write.
public void WriteNgcKey(NTAccount accountName, byte[] publicKey)
{
string distinguishedName = this._drsConnection.ResolveDistinguishedName(accountName);
this.WriteNgcKey(distinguishedName, publicKey);
}
///
/// Writes the NGC public key to the specified account.
///
/// The security identifier of the account to write the key to.
/// The NGC public key to write.
public void WriteNgcKey(SecurityIdentifier sid, byte[] publicKey)
{
string distinguishedName = this._drsConnection.ResolveDistinguishedName(sid);
this.WriteNgcKey(distinguishedName, publicKey);
}
///
/// Writes the NGC public key to the specified account.
///
/// The distinguished name of the account to write the key to.
/// The NGC public key to write.
public void WriteNgcKey(string accountDN, byte[] publicKey)
{
this._drsConnection.WriteNgcKey(accountDN, publicKey);
}
///
/// Adds the SID history of a source principal to a destination principal through MS-DRSR.
///
/// Name of the source domain (FQDN or NetBIOS).
/// Name of the source principal in the source domain. Not used when contains .
/// Name of the source domain controller (PDC).
/// Required unless contains or .
/// Credentials for the source domain.
/// Name of the destination domain (FQDN or NetBIOS).
/// Name of the destination principal in the destination domain. Not used when contains .
/// Behavior flags for the operation.
public void AddSidHistory(
string sourceDomain = null,
string sourcePrincipal = null,
string sourceDomainController = null,
NetworkCredential sourceCredential = null,
string destinationDomain = null,
string destinationPrincipal = null,
AddSidHistoryOptions flags = AddSidHistoryOptions.None)
{
this._drsConnection.AddSidHistory(
sourceDomain,
sourcePrincipal,
sourceDomainController,
sourceCredential,
destinationDomain,
destinationPrincipal,
flags);
}
///
/// Verifies whether the RPC channel is secure.
///
public void AddSidHistory()
{
this.AddSidHistory(flags: AddSidHistoryOptions.CheckSecureChannel);
}
///
/// Adds SID history within the same domain and deletes the source object.
///
/// Distinguished name of the source principal.
/// Distinguished name of the destination principal.
/// Name of the source domain controller (PDC).
public void AddSidHistory(string sourcePrincipal, string destinationPrincipal, string? sourceDomainController = null)
{
this.AddSidHistory(
sourcePrincipal: sourcePrincipal,
sourceDomainController: sourceDomainController,
destinationPrincipal: destinationPrincipal,
flags: AddSidHistoryOptions.DeleteSourceObject);
}
///
/// Adds SID history across forests.
///
/// Name of the source domain (FQDN or NetBIOS).
/// Name of the source principal in the source domain.
/// Name of the destination domain (FQDN or NetBIOS).
/// Name of the destination principal in the destination domain.
/// Name of the source domain controller (PDC), if specified.
/// Credentials for the source domain, if specified.
public void AddSidHistory(
string sourceDomain,
string sourcePrincipal,
string destinationDomain,
string destinationPrincipal,
string sourceDomainController = null,
NetworkCredential sourceCredential = null)
{
this.AddSidHistory(
sourceDomain: sourceDomain,
sourcePrincipal: sourcePrincipal,
sourceDomainController: sourceDomainController,
sourceCredential: sourceCredential,
destinationDomain: destinationDomain,
destinationPrincipal: destinationPrincipal,
flags: AddSidHistoryOptions.None);
}
///
/// Replicates the entire schema partition.
///
/// Optional progress reporter invoked after each replication cycle.
/// Token used to cooperatively cancel the replication between cycles.
public void FetchFullSchema(IProgress progress = null, CancellationToken cancellationToken = default)
{
if (_isFullSchemaLoaded)
{
// Full schema only needs to be replicated once.
return;
}
// Create a blank schema representation
ReplicationSchema schema = new();
// Replicate the entire schema partition
ReplicationCookie currentCookie = new(SchemaNamingContext);
ReplicationResult result;
int processedObjectCount = 0;
do
{
// Check for cancellation between replication cycles (the native DRS call itself is not interruptible).
cancellationToken.ThrowIfCancellationRequested();
// Perform one replication cycle
result = this._drsConnection.ReplicateAllObjects(currentCookie);
// Report replication progress
processedObjectCount += result.Objects.Count;
progress?.Report(new ReplicationProgress(result.Cookie, processedObjectCount, result.TotalObjectCount));
// Merge the prefix tables
if (result.PrefixTable != null)
{
schema.PrefixTable.Add(result.PrefixTable);
}
// Try to add the object to the schema if it is an attribute or class definition
foreach (var schemaObject in result.Objects)
{
schema.AddSchemaObject(schemaObject);
}
// Update the position of the replication cursor
currentCookie = result.Cookie;
} while (result.HasMoreData);
_drsConnection.UpdateSchemaCache(schema);
_isFullSchemaLoaded = true;
}
///
/// Releases all resources used by the .
///
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Releases the unmanaged resources used by the and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
if (this._drsConnection != null)
{
if (this._sessionKeyChangedHandler != null)
{
this._drsConnection.SessionKeyChanged -= this._sessionKeyChangedHandler;
this._sessionKeyChangedHandler = null;
}
this._drsConnection.Dispose();
this._drsConnection = null;
}
if (this._rpcBinding != null)
{
this._rpcBinding.Dispose();
this._rpcBinding = null;
}
}
///
/// Loads the domain naming context, DNS domain name, and NetBIOS domain name of the connected server.
///
private (string DomainNamingContext, string DNSDomainName, string NetBIOSDomainName) LoadDomainInfo()
{
// These is no direct way of retrieving current DC's domain info, so we are using a combination of 3 calls.
// We first retrieve FSMO roles. The PDC emulator lies in the same domain as the current server.
var fsmoRoles = _drsConnection.ListRoles();
// We need the DC object of the PDC Emulator. It is the parent of the NTDS Settings object.
string pdcEmulator = new DistinguishedName(fsmoRoles.PdcEmulator).Parent.ToString();
// Get the PDC account object from the domain partition.
var pdcInfo = _drsConnection.ListInfoForServer(pdcEmulator);
string pdcAccountDN = pdcInfo.ServerReference;
// Get the PDC Emulator's domain naming context.
string domainNamingContext = new DistinguishedName(pdcAccountDN).RootNamingContext.ToString();
string dnsDomainName = new DistinguishedName(domainNamingContext).GetDnsName();
// Get the PDC Emulator's NetBIOS account name and extract the domain part.
NTAccount pdcAccount = _drsConnection.ResolveAccountName(pdcAccountDN);
string netBIOSDomainName = pdcAccount.NetBIOSDomainName();
return (domainNamingContext, dnsDomainName, netBIOSDomainName);
}
#region IKdsRootKeyResolver
///
/// Gets the KDS root key with the specified identifier.
///
/// The identifier of the KDS root key.
/// The KDS root key, or null if not found.
KdsRootKey? IKdsRootKeyResolver.GetKdsRootKey(Guid id) => this.GetKdsRootKey(id, suppressNotFoundException: true);
///
/// Gets a value indicating whether the resolver supports looking up all root keys.
///
bool IKdsRootKeyResolver.SupportsLookupAll => false;
///
/// Gets a value indicating whether the resolver supports looking up root keys by effective time.
///
bool IKdsRootKeyResolver.SupportsLookupByEffectiveTime => false;
///
/// Gets the KDS root key that was effective at the specified time.
///
/// Search by effective time is not supported by the MS-DRSR protocol.
KdsRootKey? IKdsRootKeyResolver.GetKdsRootKey(DateTime effectiveTime) => throw new NotSupportedException("Search by effective time is not supported by the MS-DRSR protocol.");
///
/// Gets all KDS root keys.
///
/// Search by class type is not supported by the MS-DRSR protocol.
IEnumerable IKdsRootKeyResolver.GetKdsRootKeys() => throw new NotSupportedException("Search by class type is not supported by the MS-DRSR protocol.");
#endregion IKdsRootKeyResolver
}