mirror of
https://github.com/MichaelGrafnetter/DSInternals
synced 2024-12-14 02:15:50 +00:00
460 lines
19 KiB
C#
460 lines
19 KiB
C#
namespace DSInternals.DataStore
|
|
{
|
|
using DSInternals.Common;
|
|
using DSInternals.Common.Cryptography;
|
|
using DSInternals.Common.Data;
|
|
using DSInternals.Common.Exceptions;
|
|
using DSInternals.Common.Properties;
|
|
using Microsoft.Database.Isam;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Security.Principal;
|
|
public class DirectoryAgent : IDisposable
|
|
{
|
|
// 2^30
|
|
public const int RidMax = 1 << 30;
|
|
|
|
//TODO: SidCompatibilityVersion?
|
|
// TODO: Add Rid range checks
|
|
public const int RidMin = 1;
|
|
|
|
private DirectoryContext context;
|
|
private Cursor dataTableCursor;
|
|
private bool ownsContext;
|
|
|
|
public DirectoryAgent(DirectoryContext context, bool ownsContext = false)
|
|
{
|
|
this.context = context;
|
|
this.ownsContext = ownsContext;
|
|
this.dataTableCursor = context.OpenDataTable();
|
|
}
|
|
|
|
public void SetDomainControllerEpoch(int epoch)
|
|
{
|
|
using (var transaction = this.context.BeginTransaction())
|
|
{
|
|
this.context.DomainController.Epoch = epoch;
|
|
transaction.Commit(true);
|
|
}
|
|
}
|
|
|
|
public void SetDomainControllerUsn(long highestCommittedUsn)
|
|
{
|
|
using (var transaction = this.context.BeginTransaction())
|
|
{
|
|
this.context.DomainController.HighestCommittedUsn = highestCommittedUsn;
|
|
transaction.Commit(true);
|
|
}
|
|
}
|
|
|
|
public void ChangeBootKey(byte[] oldBootKey, byte[] newBootKey)
|
|
{
|
|
// Validate
|
|
Validator.AssertLength(oldBootKey, BootKeyRetriever.BootKeyLength, "oldBootKey");
|
|
if (!this.context.DomainController.DomainNamingContextDNT.HasValue)
|
|
{
|
|
// The domain object must exist
|
|
throw new DirectoryObjectNotFoundException("domain");
|
|
}
|
|
|
|
// Execute
|
|
using (var transaction = this.context.BeginTransaction())
|
|
{
|
|
// Retrieve and decrypt
|
|
var domain = this.FindObject(this.context.DomainController.DomainNamingContextDNT.Value);
|
|
byte[] encryptedPEK;
|
|
domain.ReadAttribute(CommonDirectoryAttributes.PEKList, out encryptedPEK);
|
|
var pekList = (DataStoreSecretDecryptor) new DataStoreSecretDecryptor(encryptedPEK, oldBootKey);
|
|
|
|
// Encrypt with the new boot key (if blank, plain encoding is done instead)
|
|
byte[] binaryPekList = pekList.ToByteArray(newBootKey);
|
|
|
|
// Save the new value
|
|
this.dataTableCursor.BeginEditForUpdate();
|
|
bool hasChanged = domain.SetAttribute(CommonDirectoryAttributes.PEKList, binaryPekList);
|
|
this.CommitAttributeUpdate(domain, CommonDirectoryAttributes.PEKList, transaction, hasChanged, true);
|
|
}
|
|
}
|
|
|
|
public IEnumerable<DSAccount> GetAccounts(byte[] bootKey)
|
|
{
|
|
var pek = this.GetSecretDecryptor(bootKey);
|
|
// TODO: Use a more suitable index?
|
|
string samAccountTypeIndex = this.context.Schema.FindIndexName(CommonDirectoryAttributes.SamAccountType);
|
|
this.dataTableCursor.CurrentIndex = samAccountTypeIndex;
|
|
// Find all objects with the right sAMAccountType that are writable and not deleted:
|
|
// TODO: Lock cursor?
|
|
while (this.dataTableCursor.MoveNext())
|
|
{
|
|
var obj = new DatastoreObject(this.dataTableCursor, this.context);
|
|
// TODO: This probably does not work on RODCs:
|
|
if(obj.IsDeleted || !obj.IsWritable || !obj.IsAccount)
|
|
{
|
|
continue;
|
|
}
|
|
yield return new DSAccount(obj, pek);
|
|
}
|
|
}
|
|
|
|
public DSAccount GetAccount(DistinguishedName dn, byte[] bootKey)
|
|
{
|
|
var obj = this.FindObject(dn);
|
|
return this.GetAccount(obj, dn, bootKey);
|
|
}
|
|
|
|
public DSAccount GetAccount(SecurityIdentifier objectSid, byte[] bootKey)
|
|
{
|
|
var obj = this.FindObject(objectSid);
|
|
return this.GetAccount(obj, objectSid, bootKey);
|
|
}
|
|
|
|
public DSAccount GetAccount(string samAccountName, byte[] bootKey)
|
|
{
|
|
var obj = this.FindObject(samAccountName);
|
|
return this.GetAccount(obj, samAccountName, bootKey);
|
|
}
|
|
|
|
public DSAccount GetAccount(Guid objectGuid, byte[] bootKey)
|
|
{
|
|
var obj = this.FindObject(objectGuid);
|
|
return this.GetAccount(obj, objectGuid, bootKey);
|
|
}
|
|
|
|
protected DSAccount GetAccount(DatastoreObject foundObject, object objectIdentifier, byte[] bootKey)
|
|
{
|
|
if (!foundObject.IsAccount)
|
|
{
|
|
throw new DirectoryObjectOperationException(Resources.ObjectNotSecurityPrincipalMessage, objectIdentifier);
|
|
}
|
|
|
|
var pek = GetSecretDecryptor(bootKey);
|
|
return new DSAccount(foundObject, pek);
|
|
}
|
|
|
|
protected DirectorySecretDecryptor GetSecretDecryptor(byte[] bootKey)
|
|
{
|
|
if (bootKey == null && ! this.context.DomainController.IsADAM)
|
|
{
|
|
// This is an AD DS DB, so the BootKey is stored in the registry. Stop processing if it is not provided.
|
|
return null;
|
|
|
|
}
|
|
if(this.context.DomainController.State == DatabaseState.Boot)
|
|
{
|
|
// The initial DB definitely does not contain any secrets.
|
|
return null;
|
|
}
|
|
|
|
// HACK: Save the current cursor position, because it is shared.
|
|
var originalLocation = this.dataTableCursor.SaveLocation();
|
|
try
|
|
{
|
|
int pekListDNT;
|
|
if(this.context.DomainController.IsADAM)
|
|
{
|
|
// This is a AD LDS DB, so the BootKey is stored directly in the DB.
|
|
// Retrieve the pekList attribute of the root object:
|
|
byte[] rootPekList;
|
|
var rootObject = this.FindObject(ADConstants.RootDNTag);
|
|
rootObject.ReadAttribute(CommonDirectoryAttributes.PEKList, out rootPekList);
|
|
|
|
// Retrieve the pekList attribute of the schema object:
|
|
byte[] schemaPekList;
|
|
var schemaObject = this.FindObject(this.context.DomainController.SchemaNamingContextDNT);
|
|
schemaObject.ReadAttribute(CommonDirectoryAttributes.PEKList, out schemaPekList);
|
|
|
|
// Combine these things together into the BootKey/SysKey
|
|
bootKey = BootKeyRetriever.GetBootKey(rootPekList, schemaPekList);
|
|
|
|
// The actual PEK list is located on the Configuration NC object.
|
|
pekListDNT = this.context.DomainController.ConfigurationNamingContextDNT;
|
|
}
|
|
else
|
|
{
|
|
// This is an AD DS DB, so the PEK list is located on the Domain NC object.
|
|
pekListDNT = this.context.DomainController.DomainNamingContextDNT.Value;
|
|
}
|
|
|
|
// Load the PEK List attribute from the holding object and decrypt it using Boot Key.
|
|
var pekListHolder = this.FindObject(pekListDNT);
|
|
byte[] encryptedPEK;
|
|
pekListHolder.ReadAttribute(CommonDirectoryAttributes.PEKList, out encryptedPEK);
|
|
return new DataStoreSecretDecryptor(encryptedPEK, bootKey);
|
|
}
|
|
finally
|
|
{
|
|
this.dataTableCursor.RestoreLocation(originalLocation);
|
|
}
|
|
}
|
|
|
|
public IEnumerable<DPAPIBackupKey> GetDPAPIBackupKeys(byte[] bootKey)
|
|
{
|
|
Validator.AssertNotNull(bootKey, "bootKey");
|
|
var pek = this.GetSecretDecryptor(bootKey);
|
|
// TODO: Refactor using Linq
|
|
foreach(var secret in this.FindObjectsByCategory(CommonDirectoryClasses.Secret))
|
|
{
|
|
yield return new DPAPIBackupKey(secret, pek);
|
|
}
|
|
}
|
|
|
|
public IEnumerable<DirectoryObject> FindObjectsByCategory(string className, bool includeDeleted = false)
|
|
{
|
|
// Find all objects with the right objectCategory
|
|
string objectCategoryIndex = this.context.Schema.FindIndexName(CommonDirectoryAttributes.ObjectCategory);
|
|
this.dataTableCursor.CurrentIndex = objectCategoryIndex;
|
|
int classId = this.context.Schema.FindClassId(className);
|
|
this.dataTableCursor.FindRecords(MatchCriteria.EqualTo, Key.Compose(classId));
|
|
// TODO: Lock cursor?
|
|
while (this.dataTableCursor.MoveNext())
|
|
{
|
|
var obj = new DatastoreObject(this.dataTableCursor, this.context);
|
|
// Optionally skip deleted objects
|
|
if (!includeDeleted && obj.IsDeleted)
|
|
{
|
|
continue;
|
|
}
|
|
yield return obj;
|
|
}
|
|
}
|
|
|
|
public bool AddSidHistory(DistinguishedName dn, SecurityIdentifier[] sidHistory, bool skipMetaUpdate)
|
|
{
|
|
var obj = this.FindObject(dn);
|
|
return this.AddSidHistory(obj, dn, sidHistory, skipMetaUpdate);
|
|
}
|
|
|
|
public bool AddSidHistory(string samAccountName, SecurityIdentifier[] sidHistory, bool skipMetaUpdate)
|
|
{
|
|
var obj = this.FindObject(samAccountName);
|
|
return this.AddSidHistory(obj, samAccountName, sidHistory, skipMetaUpdate);
|
|
}
|
|
|
|
public bool AddSidHistory(SecurityIdentifier objectSid, SecurityIdentifier[] sidHistory, bool skipMetaUpdate)
|
|
{
|
|
var obj = this.FindObject(objectSid);
|
|
return this.AddSidHistory(obj, objectSid, sidHistory, skipMetaUpdate);
|
|
}
|
|
|
|
public bool AddSidHistory(Guid objectGuid, SecurityIdentifier[] sidHistory, bool skipMetaUpdate)
|
|
{
|
|
var obj = this.FindObject(objectGuid);
|
|
return this.AddSidHistory(obj, objectGuid, sidHistory, skipMetaUpdate);
|
|
}
|
|
|
|
public void AuthoritativeRestore(Guid objectGuid, string[] attributeNames)
|
|
{
|
|
// TODO: Implement attribute-level authorirative restore.
|
|
// TODO: Check attribute names
|
|
// TODO: Check attribute types (not linked and not system?)
|
|
var obj = this.FindObject(objectGuid);
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public void AuthoritativeRestore(DistinguishedName dn, string[] attributeNames)
|
|
{
|
|
// TODO: Check attribute names
|
|
// TODO: Check attribute types (not linked and not system?)
|
|
this.FindObject(dn);
|
|
throw new NotImplementedException();
|
|
}
|
|
public void Dispose()
|
|
{
|
|
this.Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="samAccountName"></param>
|
|
/// <exception cref="DirectoryObjectNotFoundException"></exception>
|
|
public DatastoreObject FindObject(string samAccountName)
|
|
{
|
|
string samAccountNameIndex = this.context.Schema.FindIndexName(CommonDirectoryAttributes.SAMAccountName);
|
|
this.dataTableCursor.CurrentIndex = samAccountNameIndex;
|
|
this.dataTableCursor.FindRecords(MatchCriteria.EqualTo, Key.Compose(samAccountName));
|
|
|
|
// Find first object with the right sAMAccountName, that is writable and not deleted:
|
|
while (this.dataTableCursor.MoveNext())
|
|
{
|
|
var currentObject = new DatastoreObject(this.dataTableCursor, this.context);
|
|
if (currentObject.IsWritable && !currentObject.IsDeleted)
|
|
{
|
|
return currentObject;
|
|
}
|
|
}
|
|
// If the code execution comes here, we have not found any object matching the criteria.
|
|
throw new DirectoryObjectNotFoundException(samAccountName);
|
|
}
|
|
|
|
/// <exception cref="DirectoryObjectNotFoundException"></exception>
|
|
public DatastoreObject FindObject(SecurityIdentifier objectSid)
|
|
{
|
|
string sidIndex = this.context.Schema.FindIndexName(CommonDirectoryAttributes.ObjectSid);
|
|
this.dataTableCursor.CurrentIndex = sidIndex;
|
|
byte[] binarySid = objectSid.GetBinaryForm(true);
|
|
bool found = this.dataTableCursor.GotoKey(Key.Compose(binarySid));
|
|
if (found)
|
|
{
|
|
return new DatastoreObject(this.dataTableCursor, this.context);
|
|
}
|
|
else
|
|
{
|
|
throw new DirectoryObjectNotFoundException(objectSid);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="dn"></param>
|
|
/// <exception cref="DirectoryObjectNotFoundException"></exception>
|
|
public DatastoreObject FindObject(DistinguishedName dn)
|
|
{
|
|
// This throws exception if the DN does not get resolved to dnt:
|
|
int dnTag = this.context.DistinguishedNameResolver.Resolve(dn);
|
|
return this.FindObject(dnTag);
|
|
}
|
|
|
|
public DatastoreObject FindObject(int dnTag)
|
|
{
|
|
string dntIndex = this.context.Schema.FindIndexName(CommonDirectoryAttributes.DNTag);
|
|
this.dataTableCursor.CurrentIndex = dntIndex;
|
|
bool found = this.dataTableCursor.GotoKey(Key.Compose(dnTag));
|
|
if (found)
|
|
{
|
|
return new DatastoreObject(this.dataTableCursor, this.context);
|
|
}
|
|
else
|
|
{
|
|
throw new DirectoryObjectNotFoundException(dnTag);
|
|
}
|
|
}
|
|
|
|
|
|
/// <exception cref="DirectoryObjectNotFoundException"></exception>
|
|
public DatastoreObject FindObject(Guid objectGuid)
|
|
{
|
|
string objectGuidIndex = this.context.Schema.FindIndexName(CommonDirectoryAttributes.ObjectGUID);
|
|
this.dataTableCursor.CurrentIndex = objectGuidIndex;
|
|
bool found = this.dataTableCursor.GotoKey(Key.Compose(objectGuid.ToByteArray()));
|
|
if (found)
|
|
{
|
|
return new DatastoreObject(this.dataTableCursor, this.context);
|
|
}
|
|
else
|
|
{
|
|
throw new DirectoryObjectNotFoundException(objectGuid);
|
|
}
|
|
}
|
|
|
|
public void RemoveObject(Guid objectGuid)
|
|
{
|
|
var obj = this.FindObject(objectGuid);
|
|
obj.Delete();
|
|
}
|
|
|
|
public void RemoveObject(DistinguishedName dn)
|
|
{
|
|
var obj = this.FindObject(dn);
|
|
obj.Delete();
|
|
}
|
|
|
|
public bool SetPrimaryGroupId(DistinguishedName dn, int groupId, bool skipMetaUpdate)
|
|
{
|
|
var obj = this.FindObject(dn);
|
|
return this.SetPrimaryGroupId(obj, dn, groupId, skipMetaUpdate);
|
|
}
|
|
|
|
public bool SetPrimaryGroupId(string samAccountName, int groupId, bool skipMetaUpdate)
|
|
{
|
|
var obj = this.FindObject(samAccountName);
|
|
return this.SetPrimaryGroupId(obj, samAccountName, groupId, skipMetaUpdate);
|
|
}
|
|
|
|
public bool SetPrimaryGroupId(SecurityIdentifier objectSid, int groupId, bool skipMetaUpdate)
|
|
{
|
|
var obj = this.FindObject(objectSid);
|
|
return this.SetPrimaryGroupId(obj, objectSid, groupId, skipMetaUpdate);
|
|
}
|
|
|
|
public bool SetPrimaryGroupId(Guid objectGuid, int groupId, bool skipMetaUpdate)
|
|
{
|
|
var obj = this.FindObject(objectGuid);
|
|
return this.SetPrimaryGroupId(obj, objectGuid, groupId, skipMetaUpdate);
|
|
}
|
|
|
|
protected bool AddSidHistory(DatastoreObject targetObject, object targetObjectIdentifier, SecurityIdentifier[] sidHistory, bool skipMetaUpdate)
|
|
{
|
|
if (!targetObject.IsSecurityPrincipal)
|
|
{
|
|
throw new DirectoryObjectOperationException(Resources.ObjectNotSecurityPrincipalMessage, targetObjectIdentifier);
|
|
}
|
|
using (var transaction = this.context.BeginTransaction())
|
|
{
|
|
this.dataTableCursor.BeginEditForUpdate();
|
|
bool hasChanged = targetObject.AddAttribute(CommonDirectoryAttributes.SIDHistory, sidHistory);
|
|
this.CommitAttributeUpdate(targetObject, CommonDirectoryAttributes.SIDHistory, transaction, hasChanged, skipMetaUpdate);
|
|
return hasChanged;
|
|
}
|
|
}
|
|
|
|
protected void CommitAttributeUpdate(DatastoreObject obj, string attributeName, IsamTransaction transaction, bool hasChanged, bool skipMetaUpdate)
|
|
{
|
|
if (hasChanged)
|
|
{
|
|
if (!skipMetaUpdate)
|
|
{
|
|
// Increment the current USN
|
|
long currentUsn = ++this.context.DomainController.HighestCommittedUsn;
|
|
DateTime now = DateTime.Now;
|
|
obj.UpdateAttributeMeta(attributeName, currentUsn, now);
|
|
}
|
|
this.dataTableCursor.AcceptChanges();
|
|
transaction.Commit();
|
|
}
|
|
else
|
|
{
|
|
// No changes have been made to the object
|
|
this.dataTableCursor.RejectChanges();
|
|
transaction.Abort();
|
|
}
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (!disposing)
|
|
{
|
|
return;
|
|
}
|
|
if (this.dataTableCursor != null)
|
|
{
|
|
this.dataTableCursor.Dispose();
|
|
this.dataTableCursor = null;
|
|
}
|
|
if (this.ownsContext && this.context != null)
|
|
{
|
|
this.context.Dispose();
|
|
this.context = null;
|
|
}
|
|
}
|
|
|
|
protected bool SetPrimaryGroupId(DatastoreObject targetObject, object targetObjectIdentifier, int groupId, bool skipMetaUpdate)
|
|
{
|
|
if (!targetObject.IsAccount)
|
|
{
|
|
throw new DirectoryObjectOperationException(Resources.ObjectNotAccountMessage, targetObjectIdentifier);
|
|
}
|
|
// TODO: Validator.ValidateRid
|
|
// TODO: Test if the rid exists?
|
|
using (var transaction = this.context.BeginTransaction())
|
|
{
|
|
this.dataTableCursor.BeginEditForUpdate();
|
|
bool hasChanged = targetObject.SetAttribute<int>(CommonDirectoryAttributes.PrimaryGroupId, groupId);
|
|
this.CommitAttributeUpdate(targetObject, CommonDirectoryAttributes.PrimaryGroupId, transaction, hasChanged, skipMetaUpdate);
|
|
return hasChanged;
|
|
}
|
|
}
|
|
}
|
|
} |