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.Length -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.Length; $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;