ADComputerKeys.psm1
<#
.SYNOPSIS Copyright (c) Microsoft Corporation. All rights reserved. ADComputerKeys is Windows PowerShell module that is used to disable or remove the computer credential key from Active Directory. .DESCRIPTION ADComputerKeys is Windows PowerShell module that is used to disable or remove the computer credential key from Active Directory. Before credential keys, you would only need to reset the computer password to ensure the password was no longer usable. To ensure the existing credential key is unusable, the tool writes an unusable credential key to the specified computer object in Active Directory. This causes authentication using the credential key to fail for the computer. The computer then uses password authentication instead of the credential key. To fully evict a domain-joined computer from the domain, the computer object can be deleted. Deleting a computer object can be undesirable when access control would need to be reconfigured for a new computer object. NOTE it is important that the Group Policy to Force Device authentication using certificate is not configured. This module exports two Windows PowerShell Cmdlets: Get-DRComputerKey Used to read the credential key of the computer account in Active Directory. For details, do: Get-Help Get-DRComputerKey -Detailed Set-DRComputerKey Used to modify the credential key of to the computer account in Active Directory. There are two options: remove the credential key from the computer object or set an unusable credential key on the computer object. For details, do: Get-Help Set-DRComputerKey -Detailed EXAMPLES MODIFY SINGLE COMPUTER CREDENTIAL KEY In this example, we will set an unusable credential key on a single computer object in Active Directory: Import-Module .\ADComputerKeys.psm1; Set-DRComputerKey -SamAccountName "MyComputer$" -Domain "contoso.com" -ReplaceWithUnusableKey; MODIFY MULTIPLE COMPUTERS CREDENTIAL KEY In this example, we will set an unusable credential key on multiple computer objects in Active Directory. To do this, combine this module with the ActiveDirectory PowerShell module. For example, target all the computers in the fictional Shipping Department organizational unit. Import-Module .\ADComputerKeys.psm1; Import-Module ActiveDirectory; $computers = Get-ADComputer -SearchBase "OU=Shipping Department,DC=contoso,DC=com" -LDAPFilter "(CN=*)" -Server "contoso.com"; foreach($comp in $computers) { Set-DRComputerKey -SamAccountName $comp.SamAccountName -Domain "contoso.com" -ReplaceWithUnusableKey; } FORCE COMPUTER TO GENERATE A NEW CREDENTIAL KEY The module includes an option to remove the credential key from a computer object in Active Directory. Doing so will cause the computer to generate and register a new credential key in Active Directory. Import-Module .\ADComputerKeys.psm1; Set-DRComputerKey -SamAccountName "MyComputer$" -Domain "contoso.com" -RemoveKey; TROUBLESHOOT AND RETRY All operations are logged to .\ADComputerKey.log. The log is Tab delimited making it easy to parse into PowerShell objects. For example, to retry all failures you can do the following: $log = Import-Csv -Delimiter "`t" -Path ".\ADComputerKeys.log"; foreach($e in $log) { if($e.Result -eq "ERROR") { Set-DRComputerKey -SamAccountName $e.SamAccountName -Domain $e.Domain -ReplaceWithUnusableKey } } #> #Set error level to stop on all errors $ErrorActionPreference = "Stop"; <#---------------------------------------------------------------------- | .NET Types +---------------------------------------------------------------------#> Add-Type -TypeDefinition @" � �public enum KEY_OBJECT_ATTR_TYPE : byte � �{ KeyObjectValueIdMsDsKeyVersion = 0, KeyObjectValueIdMsDsKeyId = 1, KeyObjectValueIdMsDsKeyHash, KeyObjectValueIdMsDsKeyMaterial, KeyObjectValueIdMsDsKeyUsage, KeyObjectValueIdMsDsKeySource, KeyObjectValueIdMsDsDeviceId, KeyObjectValueIdMsDsCustomKeyInformation, KeyObjectValueIdMsDsKeyApproximateLastLogonTimeStamp, KeyObjectValueIdMsDsKeyCreationTime, KeyObjectValueIdMsDsKeyMax, � �} "@ Add-Type -TypeDefinition @" public enum KeyUsage : byte { AdminKey, // Key is an admin (pin-reset key) NGC, // Key is an NGC key attached to a user object STK, // Key is a transport key attached to a device object. BitlockerRecovery, // Key is bitlocker recovery key OTHER, // Key usage not recognized by DRS } "@ Add-Type -TypeDefinition @" public enum KEY_SOURCE : byte { AD = 0, AAD = 1, } "@ Add-Type -TypeDefinition @" public enum KEY_OBJECT_STORAGE_VERSION : uint { Version0 = 0, Version1 = 0x100, Version2 = 0x200, VersionLatest = Version2, } "@ Add-Type -ReferencedAssemblies "System.DirectoryServices" -TypeDefinition @" public enum DRLogLevel : int { Info = 0, Warning = 1, Error = 2 } public class DRKey { public string Id; public string Source; public int Version; public string Usage; public System.Guid DeviceId; public byte[] Data; public System.DateTime Created; public System.DateTime ApproximateLastUse; public byte[] CustomInfo; public string ComputerDN; public bool IsUnusableKey; public string RawValue; public DRComputerAccount ComputerAccount; public override string ToString() { string rv = string.Empty; if(!string.IsNullOrEmpty(RawValue) && RawValue.Length > 10) { rv = RawValue.Substring(0, 20); } string str = string.Format( System.Globalization.CultureInfo.InvariantCulture, "[ Id={0}, IsUnusableKey={1}, Source={2}, Version={3}, Usage={4}, RawValue={5}... ]", Id, IsUnusableKey, Source, Version, Usage, rv); return str; } } public class DRComputerAccount { public DRComputerAccount() { CredentialKey = new System.Collections.Generic.List<DRKey>(); } public string Name; public string SamAccountName; public string DistinguishedName; public string Path; public System.Collections.Generic.List<DRKey> CredentialKey; public System.DirectoryServices.DirectoryEntry DirEntry; } public class DRContext { public DRContext(string SamAccountName, string Domain) { this.Id = System.Guid.NewGuid(); this.SamAccountName = SamAccountName; this.Domain = Domain; } public System.Guid Id; public string Domain; public string SamAccountName; } "@ <#---------------------------------------------------------------------- | Constants +---------------------------------------------------------------------#> Set-Variable LDAP_NAME_NGCKEY "msDS-KeyCredentialLink" �Option ReadOnly -Force; Set-Variable LDAP_NAME_GUID "objectGUID" �Option ReadOnly -Force; Set-Variable LDAP_NAME_SAMNAME "sAMAccountName" �Option ReadOnly -Force; Set-Variable LDAP_NAME_CN "cn" �Option ReadOnly -Force; Set-Variable LDAP_NAME_DN "distinguishedName" �Option ReadOnly -Force; Set-Variable LDAP_NAME_SID "objectSid" �Option ReadOnly -Force; Set-Variable UNUSABLE_KEY_RAW "B:764:000200002000010D76D33954251DA969022D0D3B009939E256A6C9B3FF657907C72063F89AE79E200002F6B00E6A9BA3066ABDE0E4B23EB82D5E42898263AD46CA84BE0CFD20E81F91C00E01033082010A0282010100D6589A6FE210490583C1DCD57E3579AB24979D9B1A7118E3553DEDCFFA5CF5ABD41CF6C19CBBE598CE6F9140541E8FF8A778BD5CAADD8D038A49785A4D9031C98E26783E824BA3CF00D86C112A9A5C65A5ACF2B077E365D947BD41A437E7034CC00A77550B2EA8CEC18C1F7516DA4DC13177E1DE1D32FBBDDE1E1FD7395AAB71A8F302B985A64248C3A239E6943AEAFA9A8B591AE499F31723F7DC8A22A6D197445056DA4DF9D13443DB4A6201D52D82795A2F2FFA2F75B6F2605E213609A39DF33F26E023D83D9C4BDDD4879E234407833BA38460CBC66D9D31CDF2C5B3A042F321DA7F2140ECC4A5A190306ED51FE0EA5273DD83D5338B2554ABD3738A06A50203010001010004010100050002000701020800086254F138261CD3010800096254F138261CD301:{0}" �Option ReadOnly -Force; Set-Variable UNUSABLE_KEY_OBJECT $null -Option None -Visibility Public -Scope "Global" -Force; <#---------------------------------------------------------------------- | Functions +---------------------------------------------------------------------#> function Get-KeyTimeFromBytes { <#.SYNOPSIS Parse time from byte array. The time format is infered from the key source and version. .DESCRIPTION Parse time from byte array. The time format is infered from the key source and version. .PARAMETER TimeData Byte array containing the time information. .PARAMETER KeySource The time source (AD or AAD). .PARAMETER KeyVersion The NGC key version #> [CmdletBinding()] param ( [Parameter( Mandatory=$true, Position=0)] [byte[]]$TimeData, [Parameter( Mandatory=$true, Position=1)] [KEY_SOURCE]$KeySource, [Parameter( Mandatory=$true, Position=2)] [int]$KeyVersion ) PROCESS { $dateTime64 = [System.BitConverter]::ToInt64($TimeData, 0); $time = [DateTime]::MinValue; if(($KeyVersion -le 1) -or ($KeySource -eq [KEY_SOURCE]::AAD)) { $time = [DateTime]::FromBinary($dateTime64); } elseif ($KeySource -eq [KEY_SOURCE]::AD) { $time = [DateTime]::FromFileTime($dateTime64); } else { throw New-Object System.Exception -ArgumentList "Unexpected time format."; } Write-Output $time; } } function Get-ByteArrayFromHexString { <#.SYNOPSIS Convert a hex string to a byte array. .DESCRIPTION Convert a hex string to byte array. .PARAMETER HexString The hex string to convert. #> [CmdletBinding()] param ( [Parameter( Mandatory=$true, Position=0)] [String]$HexString ) PROCESS { $i = 0; $bytes = @(); while($i -lt $HexString.Length) { $chars = $HexString.SubString($i, 2); $b = [Convert]::ToByte($chars, 16); $bytes += $b; $i = $i+2; } Write-Output $bytes; } } function Get-HexStringFromByteArray { <#.SYNOPSIS Convert a byte array to a hex string. .DESCRIPTION Convert a byte array to a hex string. .PARAMETER Data The byte array to convert to hex string. .PARAMETER HexString Reference to a string that will be set to the data from the Data parameter. #> [CmdletBinding()] param ( [Parameter( Mandatory=$true, Position=0)] [byte[]]$Data, [Parameter( Mandatory=$true, Position=1)] [ref]$HexString ) PROCESS { $builder = New-Object System.Text.StringBuilder ($Data.Length * 2); foreach($b in $Data) { $builder.AppendFormat("{0:x2}", $b); } $HexString.Value = $builder.ToString().ToUpper([CultureInfo]::InvariantCulture); } } function Get-KeyIdentifierFromData { <#.SYNOPSIS Generate the key id from the key data. .DESCRIPTION Generate the key id from the key data. .PARAMETER KeyData The key data. #> [CmdletBinding()] param ( [Parameter( Mandatory=$true, Position=0)] [Byte[]] $KeyData ) PROCESS { $sha = New-Object System.Security.Cryptography.SHA256Cng; $kid = [String]::Empty; try { $id = $sha.ComputeHash($KeyData); $kid = [Convert]::ToBase64String($id); } finally { if($null -ne $sha) { $sha.Dispose(); } } Write-Output $kid; } } function Get-KeyFromRawValueBinary { <#.SYNOPSIS Parse NGC key from binary value. .DESCRIPTION Parse NGC key from binary value. .PARAMETER Context An object that maintains details about the current operation. .PARAMETER Reader The binary value loaded into a binary reader object. .PARAMETER Key Reference to a DRKey object that will be set using the value from Reader #> [CmdletBinding()] param ( [Parameter( Mandatory=$true)] [DRContext]$Context, [Parameter( Mandatory=$true)] [System.IO.BinaryReader] $Reader, [Parameter( Mandatory=$true)] [ref]$Key ) PROCESS { $Key.Value = New-Object DRKey; $key.Value.Usage = [String]::Empty; $Key.Value.Data = @(); $Key.Value.CustomInfo = @(); $Key.Value.DeviceId = [Guid]::Empty; $Key.Value.Id = [String]::Empty; $Key.Value.Created = [DateTime]::MinValue; $Key.Value.ApproximateLastUse = [DateTime]::MinValue; $keySourceString = [String]::Empty; $keySource = @(1); $lastReadKeyId = [KEY_OBJECT_ATTR_TYPE]::KeyObjectValueIdMsDsKeyVersion; # First four bytes is the key version $KeyVersionBytes = 0; $KeyVersionBytes = $Reader.ReadUInt32(); switch($KeyVersionBytes) { 0 { Set-LogEntry -Context $Context ` -StringFormat "Key version not supported. Version: {0}" ` -ArrgumentArray @($KeyVersionBytes) ` -LogLevel ([DRLogLevel]::Error); $Key.Value = $null; return; }; 0x100 { $Key.Value.Version = 1; break; }; 0x200 { $Key.Value.Version = 2; break; } default { Set-LogEntry -Context $Context ` -StringFormat "Unknown key version: {0}" ` -ArrgumentArray @($KeyVersionBytes) ` -LogLevel ([DRLogLevel]::Error); $Key.Value = $null; return; } } # Each set in this stream is in the form of: # { keyValueCount (2bytes), keyId (1byte), keyValue (keyValueCount bytes) } do { # Read the keyValueCount $keyValueCount = $Reader.ReadUInt16(); # Read the keyId $keyId = $Reader.ReadByte(); if ($keyId -ge 10 -or $keyId -lt 0) { Set-LogEntry -Context $Context ` -StringFormat "Unexpected KeyId: {0}" ` -ArrgumentArray @($keyId) ` -LogLevel ([DRLogLevel]::Error); $Key.Value = $null; return; } $readKeyId = [KEY_OBJECT_ATTR_TYPE]$keyId; if ($lastReadKeyId -ge $readKeyId) { Set-LogEntry -Context $Context ` -StringFormat "Unexpected keyId order: LastKeyRead{0}, CurrentKey:{1}" ` -ArrgumentArray @($lastReadKeyId, $readKeyId) ` -LogLevel ([DRLogLevel]::Error); $Key.Value = $null; return; } # Read the actual keyValue. $keyValue = $Reader.ReadBytes($keyValueCount); switch($readKeyId) { "KeyObjectValueIdMsDsKeyUsage" { if ($keyValueCount -eq 1) { $usage = [KeyUsage]$keyValue[0]; $Key.Value.Usage = $usage.ToString(); } else { $Key.Value.Usage = [System.Text.Encoding.UTF8]::GetString($keyValue); } break; } "KeyObjectValueIdMsDsKeyId" { $keyIdBytes = @(); $keyIdBytes = $keyValue; if($Key.Value.Version -eq 1) { $Key.Value.Id = [System.BitConverter]::ToString($keyIdBytes).Replace("-", ""); } else { $Key.Value.Id = [System.Convert]::ToBase64String($keyIdBytes); } break; } "KeyObjectValueIdMsDsKeyHash" { # Do nothing. break; } "KeyObjectValueIdMsDsKeyMaterial" { $Key.Value.Data = [byte[]]$keyValue; break; } "KeyObjectValueIdMsDsKeySource" { $keySource = $keyValue; if($Key.Value.Version -le 1) { $Key.Value.Source = "NA"; } elseif($keySource[0]-eq 0) { $Key.Value.Source = "AD"; } elseif($keySource[0]-eq 1) { $Key.Value.Source = "AzureAD"; } else { $Key.Value.Source = "Unknown"; Set-LogEntry -Context $Context ` -StringFormat "Unexpected key source: {0}" ` -ArrgumentArray @($keySource[0]) ` -LogLevel ([DRLogLevel]::Warning); } break; } "KeyObjectValueIdMsDsDeviceId" { $Key.Value.DeviceId = New-Object System.Guid (,$keyValue); break; } "KeyObjectValueIdMsDsCustomKeyInformation" { $Key.Value.CustomInfo = $keyValue; break; } "KeyObjectValueIdMsDsKeyApproximateLastLogonTimeStamp" { $Key.Value.ApproximateLastUse = Get-KeyTimeFromBytes ` -TimeData $keyValue ` -KeySource $keySource[0]` -KeyVersion $Key.Value.Version; break; } "KeyObjectValueIdMsDsKeyCreationTime" { $Key.Value.Created = Get-KeyTimeFromBytes ` -TimeData $keyValue ` -KeySource $keySource[0]` -KeyVersion $Key.Value.Version; break; } default { Set-LogEntry -Context $Context ` -StringFormat "Unexpected keyId: {0}" ` -ArrgumentArray @($readKeyId) ` -LogLevel ([DRLogLevel]::Warning); } } } while($Reader.PeekChar() -ne -1); } } function Get-KeyFromRawValue { <#.SYNOPSIS Parse NGC key using the value stored in the msDS-KeyCredentialLink AD attribute. .DESCRIPTION Parse NGC key using the value stored in the msDS-KeyCredentialLink AD attribute. .PARAMETER Context An object that maintains details about the current operation. .PARAMETER $RawValue The value stored in the msDS-KeyCredentialLink attribute in Active Directory. #> [CmdletBinding()] param ( [Parameter( Mandatory=$true)] [DRContext]$Context, [Parameter( Mandatory=$true)] [System.String]$RawValue ) PROCESS { $memStream = $null; $binReader = $null; $ffo = $Context.Id; try { $parsedLink = $RawValue.Split(':'); if($parsedLink.Length -ne 4) { Set-LogEntry -Context $Context ` -StringFormat "Value is not an expected DN binary value: {0}" ` -ArrgumentArray @($RawValue) ` -LogLevel ([DRLogLevel]::Error); Write-Output $null; return; } $valueCount = [Convert]::ToInt32($parsedLink[1]); if ($parsedLink[2].Length -ne $valueCount) { Set-LogEntry -Context $Context ` -StringFormat "Value has unexpected count: ParsedCount:{0} ValueCount:{1}" ` -ArrgumentArray @($parsedLink[2].Length, $valueCount) ` -LogLevel ([DRLogLevel]::Error); Write-Output $null; return; } $keyBytes = Get-ByteArrayFromHexString -HexString $parsedLink[2]; $memStream = New-Object System.IO.MemoryStream (,[byte[]]$keyBytes) $binReader = New-Object System.IO.BinaryReader $memStream; $key = [DRKey]$null; Get-KeyFromRawValueBinary -Context $Context ` -Reader $binReader ` -Key ([ref]$key); if($null -eq $key) { # Errors logged in Get-KeyFromRawValueBinary Write-Output $null; return; } $key.ComputerDN = $parsedLink[3]; $key.RawValue = $RawValue; # Check if the key is unusable I.e. it matches our well-known # unusable key. if($null -ne $global:UNUSABLE_KEY_OBJECT) { $key.IsUnusableKey = $false; $key.IsUnusableKey = Get-BytesAreEqual -Data1 $key.Data -Data2 $global:UNUSABLE_KEY_OBJECT.Data; } Write-Output $key; } finally { if($null -ne $binReader) { $binReader.Close(); $binReader.Dispose(); } if($null -ne $memStream) { $memStream.Close(); $memStream.Dispose(); } } } } function Get-UnusableKey { <#.SYNOPSIS Get the well-known unusable key and set it to a global variable. .DESCRIPTION Get the well-known unusable key and set it to a global variable. #> PROCESS { if($null -ne $global:UNUSABLE_KEY_OBJECT) { return; } $context = New-Object DRContext -ArgumentList @("NA", "NA"); $global:UNUSABLE_KEY_OBJECT = Get-KeyFromRawValue -RawValue $UNUSABLE_KEY_RAW ` -Context $context; Write-Output $key; } } function Get-DRComputerAccount { <#.SYNOPSIS Read a computer account from Active Directory. .DESCRIPTION Read computer account from Active Directory. .PARAMETER Context An object that maintains details about the current operation. .PARAMETER SamAccountName The samaccountname of the computer account. E.g. computer1$ .PARAMETER Domain The Active Directory domain name where the computer account is located. .PARAMETER DRComputer Reference to a DRComputerAccount object that will be set using the computer account in Active Directory #> [CmdletBinding()] param ( [Parameter( Mandatory=$true)] [DRContext]$Context, [Parameter( Mandatory=$true)] [string]$SamAccountName, [Parameter( Mandatory=$true)] [string]$Domain, [Parameter( Mandatory=$true)] [ref]$DRComputer ) PROCESS { $DRComputer.Value = $null; $filter = [String]::Format( [System.Globalization.CultureInfo]::InvariantCulture, "(&(objectCategory=computer)(objectClass=computer)(samaccountname={0}))", ` $SamAccountName); $path = [String]::Format( [System.Globalization.CultureInfo]::InvariantCulture, "LDAP://{0}", $Domain); try { $de = New-Object System.DirectoryServices.DirectoryEntry($path); $ds = New-Object System.DirectoryServices.DirectorySearcher; $ds.SearchRoot = $de; $ds.ClientTimeout = 30; $ds.Filter = $filter; $ds.SearchScope = "Subtree"; $ds.PropertiesToLoad.Add($LDAP_NAME_NGCKEY) | Out-Null; $ds.PropertiesToLoad.Add($LDAP_NAME_CN) | Out-Null; $ds.PropertiesToLoad.Add($LDAP_NAME_SAMNAME) | Out-Null; $ds.PropertiesToLoad.Add($LDAP_NAME_DN) | Out-Null; $ds.PropertiesToLoad.Add($LDAP_NAME_GUID) | Out-Null; $ds.PropertiesToLoad.Add($LDAP_NAME_SID) | Out-Null; $r = $ds.FindAll(); if($r.Count -le 0) { Set-LogEntry -Context $Context ` -StringFormat "Computer account not found using samAccountName: {0}" ` -ArrgumentArray @($SamAccountName) ` -LogLevel ([DRLogLevel]::Error); $DRComputer.Value = $null; return; } elseif($r.Count -gt 1) { Set-LogEntry -Context $Context ` -StringFormat "Multiple computer accounts found using samAccountName: {0}" ` -ArrgumentArray @($SamAccountName) ` -LogLevel ([DRLogLevel]::Error); $DRComputer.Value = $null; return; } # Make sure comptuer has the properties we need. These should # be required by schema. if($false -eq $r[0].Properties.Contains($LDAP_NAME_CN) -or $false -eq $r[0].Properties.Contains($LDAP_NAME_SAMNAME) -or $false -eq $r[0].Properties.Contains($LDAP_NAME_DN)) { Set-LogEntry -Context $Context ` -StringFormat "Computer account missing required properties." ` -ArrgumentArray @([string]::Empty) ` -LogLevel ([DRLogLevel]::Error); $DRComputer.Value = $null; return; } $DRComputer.Value = New-Object DRComputerAccount; $DRComputer.Value.Name = $r[0].Properties[$LDAP_NAME_CN][0]; $DRComputer.Value.SamAccountName = $r[0].Properties[$LDAP_NAME_SAMNAME][0]; $DRComputer.Value.DistinguishedName = $r[0].Properties[$LDAP_NAME_DN][0]; $DRComputer.Value.Path = $r[0].Path; $DRComputer.Value.DirEntry = $r[0].GetDirectoryEntry(); if($r[0].Properties.Contains($LDAP_NAME_NGCKEY)) { $keyLinks = $r[0].Properties[$LDAP_NAME_NGCKEY]; for($i=0; $i -lt $keyLinks.Count; $i++) { $key = Get-KeyFromRawValue -RawValue $keyLinks[$i] ` -Context $Context; if($null -eq $key) { # Errors logged in Get-KeyFromRawValueBinary continue; } $key.ComputerAccount = $DRComputer.Value; $DRComputer.Value.CredentialKey.Add($key); } } } catch { $DRComputer.Value = $null; Set-LogEntry -Context $Context ` -StringFormat "Hit an exception while looking up computer account. Exception: {0}" ` -ArrgumentArray @($_.Exception.Message) ` -LogLevel ([DRLogLevel]::Error); } } } function Get-BytesAreEqual { <#.SYNOPSIS Compare two byte arrays and return $true if they are equal. .DESCRIPTION Compare two byte arrays and return $true if they are equal. .PARAMETER Data1 The first byte array. .PARAMETER Data2 The second byte array. #> [CmdletBinding()] param ( [Parameter( Mandatory=$true, Position=0)] [byte[]] $Data1, [Parameter( Mandatory=$true, Position=1)] [byte[]] $Data2 ) PROCESS { if($Data1.Length -ne $Data2.Length) { Write-Output $false; return; } for($i=0; $i -lt $Data1.Length; $i++) { if($Data1[$i] -ne $Data2[$i]) { Write-Output $false; return; } } return $true; } } function Get-DRComputerKey { <#.SYNOPSIS Read a computer account credential key from Active Directory. .DESCRIPTION Read a computer account credential key from Active Directory. .PARAMETER SamAccountName The samaccountname of the computer account. E.g. computer1$ .PARAMETER Domain The Active Directory domain name where the computer account is located. #> [CmdletBinding()] param ( [Parameter( Mandatory=$true, Position=0)] [object] $SamAccountName, [Parameter( Mandatory=$true, Position=1)] [object] $Domain ) PROCESS { $context = New-Object DRContext -ArgumentList @($SamAccountName, $Domain); $comp = $null; Get-DRComputerAccount -Context $context ` -SamAccountName $SamAccountName ` -Domain $Domain ` -DRComputer ([ref]$comp); if($null -eq $comp) { # Errors logged in Get-DRComputerAccount Write-Output $null; return; } if($comp.CredentialKey.Count -le 0) { Set-LogEntry -Context $context ` -StringFormat "No credential keys found for computer object: {0}" ` -ArrgumentArray @($comp.DistinguishedName) ` -LogLevel ([DRLogLevel]::Info); Write-Output $null; return; } for($i=0; $i -lt $comp.CredentialKey.Count; $i++) { Write-Output $comp.CredentialKey[$i]; } } } function Set-DRComputerKey { <#.SYNOPSIS Modify the credential key registered to the computer account in Active Directory. .DESCRIPTION Modify the credential key registered to the computer account in Active Directory. There are two options: Force a computer to fall back to password authentication by setting an unusable credential key on the computer object in Active Directory. Force a computer to generate a new credential key by removing the existing credential key from the computer object in Active Directory. .PARAMETER SamAccountName The samaccountname of the computer account. E.g. computer1$ .PARAMETER Domain The Active Directory domain name where the computer account is located. .PARAMETER ReplaceWithUnusableKey Force a computer to fall back to password authentication by setting an unusable credential key on the computer object in Active Directory. .PARAMETER RemoveKey Force a computer to generate a new credential key by removing the existing credential key from the computer object in Active Directory. .PARAMETER Force The Force parameter is used in two scenarios: When setting an unusable key on a computer object, Force indicates that existing key will be overwritten even if it is already an unusable key. When setting an unusable key on a computer object, Force indicates that the unusable key should be set on the computer object even if the computer is not currently using a key credential. I.e. it does not have a key credential and is therefore already using password authentication. .PARAMETER WhatIf The WhatIf parameter can be used to see the results of the command without actually modifying the computer object in Active Directory. .EXAMPLE Set-DRComputerKey -SamAccountName "MyComputer$" -Domain "contoso.com" -ReplaceWithUnusableKey; Set an unusable credential key on a single computer object in Active Directory. .EXAMPLE Set-DRComputerKey -SamAccountName "MyComputer$" -Domain "contoso.com" -RemoveKey; Force a computer to generate a new credential key by removing the existing key from the computer object. #> [CmdletBinding()] param ( [Parameter(Mandatory=$true, ParameterSetName="RemoveKey", Position=0, ValueFromPipelineByPropertyName=$true)] [Parameter(Mandatory=$true, ParameterSetName="UnusableKey", Position=0)] [object]$SamAccountName, [Parameter(Mandatory=$true, ParameterSetName="RemoveKey", Position=1)] [Parameter(Mandatory=$true, ParameterSetName="UnusableKey", Position=1)] [object]$Domain, [Parameter(Mandatory=$true, ParameterSetName="UnusableKey", Position=2)] [switch]$ReplaceWithUnusableKey, [Parameter(Mandatory=$true, ParameterSetName="RemoveKey", Position=2)] [switch]$RemoveKey, [Parameter(Mandatory=$false, ParameterSetName="RemoveKey")] [Parameter(Mandatory=$false, ParameterSetName="UnusableKey")] [switch]$Force, [Parameter(Mandatory=$false, ParameterSetName="RemoveKey")] [Parameter(Mandatory=$false, ParameterSetName="UnusableKey")] [switch]$WhatIf ) PROCESS { try { $context = New-Object DRContext -ArgumentList @($SamAccountName, $Domain); $comp = New-Object DRComputerAccount; Get-DRComputerAccount -Context $context ` -SamAccountName $SamAccountName ` -Domain $Domain ` -DRComputer ([ref]$comp); if($null -eq $comp) { # Errors logged in Get-DRComputerAccount Write-Output $null; return; } $key = $null; if($comp.CredentialKey.Count -gt 0) { $key = $comp.CredentialKey[0]; } if($ReplaceWithUnusableKey) { if(($null -ne $key) -and ($key.IsUnusableKey) -and ($false -eq $Force)) { # Key already set to unusable key. Set-LogEntry -Context $context ` -StringFormat "Credential key already set to unusable key. Will not update computer object: {0}" ` -ArrgumentArray @($comp.DistinguishedName) ` -LogLevel ([DRLogLevel]::Warning); return; } if(($null -eq $key) -and ($false -eq $Force)) { # Only replace the key if one already exists. If a key does # not exist then the computer is doing password auth and should # not have a key. Set-LogEntry -Context $context ` -StringFormat "No existing credential key found. Will not update computer object: {0}" ` -ArrgumentArray @($comp.DistinguishedName) ` -LogLevel ([DRLogLevel]::Warning); return; } Get-UnusableKey; $rawValue = [string]::Format( [System.Globalization.CultureInfo]::InvariantCulture, $UNUSABLE_KEY_RAW, $comp.DistinguishedName); if($false -eq $WhatIf) { $comp.DirEntry.Properties[$LDAP_NAME_NGCKEY].Clear(); $comp.DirEntry.Properties[$LDAP_NAME_NGCKEY].Insert(0, $rawValue); $comp.DirEntry.CommitChanges(); } Set-LogEntry -Context $context ` -StringFormat "Set unusable credential key. Computer object: {0} Key object: {1}" ` -ArrgumentArray @($comp.DistinguishedName, $global:UNUSABLE_KEY_OBJECT) ` -LogLevel ([DRLogLevel]::Info); } if($RemoveKey) { if($null -eq $key) { Set-LogEntry -Context $context ` -StringFormat "No credential keys found for computer object: {0}" ` -ArrgumentArray @($comp.DistinguishedName) ` -LogLevel ([DRLogLevel]::Warning); return; } if($false -eq $WhatIf) { $comp.DirEntry.Properties[$LDAP_NAME_NGCKEY].Clear(); $comp.DirEntry.CommitChanges(); } Set-LogEntry -Context $context ` -StringFormat "Removed credential key. Computer object: {0}" ` -ArrgumentArray @($comp.DistinguishedName) ` -LogLevel ([DRLogLevel]::Info); } } finally { if($null -ne $comp -and $null -ne $comp.DirEntry) { $comp.DirEntry.Close(); $comp.DirEntry.Dispose(); } } } } function Set-LogEntry { <#.SYNOPSIS Add an entry to the log file. .DESCRIPTION Add an entry to the log file. .PARAMETER Context An object that maintains details about the current operation. .PARAMETER StringFormat A string formatted for substitution. E.g. "Hello {0}". .PARAMETER ArrgumentArray An array of arguments to substitute in StringFormat. E.g. object[] {"World"} .PARAMETER LogLevel The log level (info, warning, error). #> [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [DRContext]$Context, [Parameter(Mandatory=$true)] [string]$StringFormat, [Parameter(Mandatory=$true)] [object[]]$ArrgumentArray, [Parameter(Mandatory=$true)] [DRLogLevel]$LogLevel ) PROCESS { $color = $null; switch($LogLevel) { ("Info") { $content = "INFO"; $color = [ConsoleColor]::White; break; } ("Warning") { $content = "WARN"; $color = [ConsoleColor]::Yellow; break; } ("Error") { $content = "ERROR"; $color = [ConsoleColor]::Red; break; } } $content += [string]::Format( [System.Globalization.CultureInfo]::InvariantCulture, "`t{0}`t{1}`t{2}`t", [DateTime]::Now, $Context.SamAccountName, $Context.Domain); $content += [string]::Format( [System.Globalization.CultureInfo]::InvariantCulture, $StringFormat, $ArrgumentArray); Write-Host $content -ForegroundColor $color; $logPath = ".\ADComputerKeys.log"; if($false -eq (Test-Path $logPath)) { Add-Content -Path $logPath -Value "Result`tTime`tSamAccountName`tDomain`tDescription" -Force; } Add-Content -Path $logPath -Value $content -Force; } } # Get the global unusable key. Get-UnusableKey; <#---------------------------------------------------------------------- | Export Functions +---------------------------------------------------------------------#> Export-ModuleMember -Function Get-DRComputerKey,Set-DRComputerKey; |