using System.Security.AccessControl; using System.Security.Principal; using DSInternals.Common.Cryptography; using DSInternals.Common.Schema; namespace DSInternals.Common.Data; /// /// Represents an Active Directory security principal (user, computer, or trust account). /// public class DSAccount { /// /// Initializes a new instance of the class. /// /// The directory object containing the account data. /// The NetBIOS domain name. /// The secret decryptor used to decrypt password hashes. Can be if decryption is not needed. /// A bitwise combination of the enumeration values that specifies which property sets to load. /// The or parameter is . /// The object is not an account. public DSAccount(DirectoryObject dsObject, string netBIOSDomainName, DirectorySecretDecryptor? pek, AccountPropertySets propertySets = AccountPropertySets.All) { // Parameter validation ArgumentNullException.ThrowIfNull(dsObject); ArgumentNullException.ThrowIfNull(netBIOSDomainName); // Load and validate SamAccountType dsObject.ReadAttribute(CommonDirectoryAttributes.SamAccountType, out SamAccountType? accountType); if (propertySets.HasFlag(AccountPropertySets.KeyCredentials) && !propertySets.HasFlag(AccountPropertySets.DistinguishedName)) { // Object DN is needed for key credential construction propertySets |= AccountPropertySets.DistinguishedName; } switch (accountType) { case SamAccountType.User: case SamAccountType.Computer: case SamAccountType.Trust: this.SamAccountType = accountType.Value; break; default: throw new ArgumentException("The object is not an account."); } // Common properties this.LoadAccountInfo(dsObject, netBIOSDomainName, propertySets); // Hashes and Supplemental Credentials if (pek != null) { // Only continue if we have a decryption key this.LoadSecrets(dsObject, pek, propertySets); } if (propertySets.HasFlag(AccountPropertySets.KeyCredentials)) { // Windows Hello for Business this.LoadKeyCredentials(dsObject); } } /// /// Gets the distinguished name (DN) for this . /// public string? DistinguishedName { get; private set; } /// /// Gets the Security ID (SID) of the . /// public SecurityIdentifier Sid { get; private set; } = null!; /// /// Gets the SID history for this . /// /// /// An array of security identifiers representing the SID history, or if not available. /// public SecurityIdentifier[]? SidHistory { get; private set; } /// /// Gets the security descriptor of the object. /// public RawSecurityDescriptor? SecurityDescriptor { get; private set; } /// /// Gets the GUID associated with this . /// /// /// The unique identifier. /// public Guid Guid { get; private set; } /// /// Gets the description of the . /// /// /// The description. /// public string? Description { get; private set; } /// /// Gets a boolean value indicating whether this is enabled. /// /// /// true if enabled; otherwise, false. /// public bool Enabled { get { return !this.UserAccountControl.HasFlag(UserAccountControl.Disabled); } } /// /// Gets the flags that control the behavior of the user account. /// /// /// The value can be zero or a combination of one or more flags. /// public UserAccountControl UserAccountControl { get; private set; } /// /// Gets the encryption types supported by this trust relationship. /// /// Implemented on Windows Server 2008 operating system and later. public SupportedEncryptionTypes? SupportedEncryptionTypes { get; private set; } public bool SupportsKerberosAESEncryption { get { if (!this.SupportedEncryptionTypes.HasValue) { // An unset msDS-SupportedEncryptionTypes attribute means the account // inherits the domain default. Since KB5021131, DCs honour the // DefaultDomainSupportedEncTypes setting, which includes AES on all // currently supported Windows Server versions. return true; } return this.SupportedEncryptionTypes.Value.HasFlag(Data.SupportedEncryptionTypes.AES128_CTS_HMAC_SHA1_96) || this.SupportedEncryptionTypes.Value.HasFlag(Data.SupportedEncryptionTypes.AES256_CTS_HMAC_SHA1_96) || this.SupportedEncryptionTypes.Value.HasFlag(Data.SupportedEncryptionTypes.AES256_CTS_HMAC_SHA1_96_SK); } } /// /// Gets a boolean value indicating whether this is deleted. /// /// /// true if deleted; otherwise, false. /// public bool Deleted { get; private set; } /// /// Gets the Nullable DateTime that specifies the date and time of the last logon for this . /// /// /// Local, nonreplicated value. /// public DateTime? LastLogon { get; private set; } /// /// Gets the Nullable DateTime that specifies the date and time of the last logon for this . /// /// /// Replicated value /// public DateTime? LastLogonTimestamp { get; private set; } public DateTime? LastLogonDate { get { // lastLogon is not replicated, lastLogonTimestamp is but it's not as accurate, so if we can't find lastLogon, try using lastLogonTimestamp instead return this.LastLogon ?? this.LastLogonTimestamp; } } /// /// Gets the date and time when the password was set for this account. /// public DateTime? PasswordLastSet { get; private set; } /// /// Gets or sets the user principal name (UPN) associated with this . /// public string? UserPrincipalName { get; private set; } /// /// Gets or sets the SAM account name for this . /// public string? SamAccountName { get; private set; } /// /// Gets or sets the pre-Windows 2000 logon name of this this . /// public string? LogonName { get; private set; } /// /// Gets the primary group identifier for this . /// /// /// The relative identifier (RID) of the primary group. /// public int PrimaryGroupId { get; private set; } /// /// Gets the type of the account object. /// public SamAccountType SamAccountType { get; private set; } /// /// Indicates that a given object has had its ACLs changed to a more secure value /// by the system because it was a member of one of the administrative groups /// (directly or transitively). /// public bool AdminCount { get; private set; } /// /// List of principal names used for mutual authentication with an instance of a service. /// public string[]? ServicePrincipalName { get; private set; } /// /// Gets the account's password in Windows NT operating system one-way format (OWF). /// public byte[]? NTHash { get; private set; } /// /// Gets the account's password in LAN Manager (LM) one-way format (OWF). /// /// The LM OWF is used for compatibility with LAN Manager 2.x clients, Windows 95, and Windows 98. public byte[]? LMHash { get; private set; } /// /// Gets password history of the user in Windows NT operating system one-way format (OWF). /// public byte[][]? NTHashHistory { get; private set; } /// /// Gets the password history of the user in LAN Manager (LM) one-way format (OWF). /// /// /// The lm hash history. /// public byte[][]? LMHashHistory { get; private set; } /// /// Gets the stored credentials for use in authenticating. /// public SupplementalCredentials? SupplementalCredentials { get; private set; } /// /// Device Registration / Windows Hello for Business Keys /// public KeyCredential[]? KeyCredentials { get; private set; } protected void LoadAccountInfo(DirectoryObject dsObject, string netBIOSDomainName, AccountPropertySets propertySets) { // SamAccountName: dsObject.ReadAttribute(CommonDirectoryAttributes.SamAccountName, out string samAccountName); this.SamAccountName = samAccountName; // LogonName (DOMAIN\SamAccountName): if (!string.IsNullOrEmpty(samAccountName)) { this.LogonName = new NTAccount(netBIOSDomainName, samAccountName).ToString(); } // Service Principal Name(s): dsObject.ReadAttribute(CommonDirectoryAttributes.ServicePrincipalName, out string[] spn); this.ServicePrincipalName = spn; // ObjectGuid: this.Guid = dsObject.Guid; // ObjectSid: this.Sid = dsObject.Sid; // UAC: dsObject.ReadAttribute(CommonDirectoryAttributes.UserAccountControl, out int? numericUac); this.UserAccountControl = (UserAccountControl)numericUac.Value; // Deleted: dsObject.ReadAttribute(CommonDirectoryAttributes.IsDeleted, out bool isDeleted); this.Deleted = isDeleted; // AdminCount (Although the schema defines it as Int32, it can only have values 0 and 1, so we directly convert it to bool) dsObject.ReadAttribute(CommonDirectoryAttributes.AdminCount, out bool adminCount); this.AdminCount = adminCount; // SuportedEncryptionTypes: dsObject.ReadAttribute(CommonDirectoryAttributes.SupportedEncryptionTypes, out int? numericSupportedEncryptionTypes); // Note: The value is stored as int in the DB, but the documentation says that it is an unsigned int this.SupportedEncryptionTypes = (SupportedEncryptionTypes?)numericSupportedEncryptionTypes; // PrimaryGroupId: dsObject.ReadAttribute(CommonDirectoryAttributes.PrimaryGroupId, out int? groupId); this.PrimaryGroupId = groupId.Value; if (propertySets.HasFlag(AccountPropertySets.DistinguishedName)) { // Note: DN loading from the DB involves one or more seeks. this.DistinguishedName = dsObject.DistinguishedName; } if (propertySets.HasFlag(AccountPropertySets.SecurityDescriptor)) { // Note: Security descriptor loading from the DB involves a seek and binary data parsing. dsObject.ReadAttribute(CommonDirectoryAttributes.SecurityDescriptor, out RawSecurityDescriptor securityDescriptor); this.SecurityDescriptor = securityDescriptor; } if (propertySets.HasFlag(AccountPropertySets.GenericAccountInfo)) { // SidHistory: dsObject.ReadAttribute(CommonDirectoryAttributes.SidHistory, out SecurityIdentifier[] sidHistory); this.SidHistory = sidHistory; // UPN: dsObject.ReadAttribute(CommonDirectoryAttributes.UserPrincipalName, out string upn); this.UserPrincipalName = upn; // LastLogon: dsObject.ReadAttribute(CommonDirectoryAttributes.LastLogon, out DateTime? lastLogon, false); this.LastLogon = lastLogon; // LastLogonTimestamp: dsObject.ReadAttribute(CommonDirectoryAttributes.LastLogonTimestamp, out DateTime? lastLogonTimestamp, false); this.LastLogonTimestamp = lastLogonTimestamp; // PwdLastSet dsObject.ReadAttribute(CommonDirectoryAttributes.PasswordLastSet, out DateTime? pwdLastSet, false); this.PasswordLastSet = pwdLastSet; // Description dsObject.ReadAttribute(CommonDirectoryAttributes.Description, out string description); this.Description = description; } } protected void LoadSecrets(DirectoryObject dsObject, DirectorySecretDecryptor pek, AccountPropertySets propertySets) { if (propertySets.HasFlag(AccountPropertySets.LMHash)) { // LM Hash: byte[] encryptedLmHash; dsObject.ReadAttribute(CommonDirectoryAttributes.LMHash, out encryptedLmHash); if (encryptedLmHash != null) { this.LMHash = pek.DecryptHash(encryptedLmHash, this.Sid.GetRid()); } } if (propertySets.HasFlag(AccountPropertySets.LMHashHistory)) { // LM Hash History: byte[] encryptedLmHashHistory; dsObject.ReadAttribute(CommonDirectoryAttributes.LMHashHistory, out encryptedLmHashHistory); if (encryptedLmHashHistory != null) { this.LMHashHistory = pek.DecryptHashHistory(encryptedLmHashHistory, this.Sid.GetRid()); } } if (propertySets.HasFlag(AccountPropertySets.NTHash)) { // NT Hash: byte[] encryptedNtHash; dsObject.ReadAttribute(CommonDirectoryAttributes.NTHash, out encryptedNtHash); if (encryptedNtHash != null) { this.NTHash = pek.DecryptHash(encryptedNtHash, this.Sid.GetRid()); } } if (propertySets.HasFlag(AccountPropertySets.NTHashHistory)) { // NT Hash History: byte[] encryptedNtHashHistory; dsObject.ReadAttribute(CommonDirectoryAttributes.NTHashHistory, out encryptedNtHashHistory); if (encryptedNtHashHistory != null) { this.NTHashHistory = pek.DecryptHashHistory(encryptedNtHashHistory, this.Sid.GetRid()); } } if (propertySets.HasFlag(AccountPropertySets.SupplementalCredentials)) { // Supplemental Credentials: byte[] encryptedSupplementalCredentials; dsObject.ReadAttribute(CommonDirectoryAttributes.SupplementalCredentials, out encryptedSupplementalCredentials); if (encryptedSupplementalCredentials != null) { byte[] binarySupplementalCredentials = pek.DecryptSecret(encryptedSupplementalCredentials); this.SupplementalCredentials = new SupplementalCredentials(binarySupplementalCredentials); } } } /// /// Loads key credentials. /// protected void LoadKeyCredentials(DirectoryObject dsObject) { // This attribute has been added in Windows Server 2016, so it might not be present on older DCs. byte[][] keyCredentialBlobs; dsObject.ReadLinkedValues(CommonDirectoryAttributes.KeyCredentialLink, out keyCredentialBlobs); if (keyCredentialBlobs != null) { // Parse the blobs and combine them into one array. this.KeyCredentials = new KeyCredential[keyCredentialBlobs.Length]; for (int i = 0; i < keyCredentialBlobs.Length; i++) { this.KeyCredentials[i] = new KeyCredential(keyCredentialBlobs[i], this.DistinguishedName); } } } }