BitLocker Recovery Keys from AD Without RSAT (Pure ADSI)
Retrieve BitLocker recovery keys from Active Directory using pure ADSI on any domain-joined machine with PowerShell 2.0+. No RSAT required.
by Emanuel De Almeida
TL;DR
- Retrieve BitLocker recovery keys from Active Directory using pure ADSI, with zero module dependencies
- Works on any domain-joined machine running PowerShell 2.0 or later, no RSAT installation required
- Search by computer name or the 8-character password ID displayed on the BitLocker recovery screen
- Supports
-Domain,-Server, and-Credentialparameters for flexible authentication - If password fields return
N/A, your account lacks the delegated read permission for recovery secrets
Why Retrieve BitLocker Recovery Keys Without RSAT?
You need this script when someone is locked out at the BitLocker recovery screen and you are standing at a bare domain-joined workstation. The machine has no RSAT installed. The ActiveDirectory module is unavailable. You need the recovery key now.
This script solves that problem. It uses raw ADSI, specifically [ADSISearcher] and DirectoryEntry, to query Active Directory directly. When we tested this on Server 2019 and Windows 10 workstations without RSAT, the script returned recovery keys in under two seconds. Zero dependencies means zero excuses.
According to Microsoft, organizations can configure policy to prevent users from enabling BitLocker unless the backup of recovery information to AD DS succeeds. This script reads those backed-up keys.
When Should You Use the ADSI Approach?
Use this script when the ActiveDirectory module is not available on your current machine. The ADSI approach runs anywhere PowerShell 2.0 exists.
Choose this method when:
- The workstation lacks RSAT or the ActiveDirectory module entirely
- You need zero module dependencies for portability
- You want a single
.ps1file that works on any domain-joined system
If you do have the AD module installed, prefer the shorter module-based version. It is easier to read and maintain. But when RSAT is missing, this ADSI script becomes essential.
Approach | Dependencies | Best For |
|---|---|---|
ActiveDirectory module | RSAT installed | Admin workstations with full tooling |
Pure ADSI (this script) | None beyond PowerShell 2.0 | Field support, locked-down endpoints, jump boxes |
What Does the Script Return for BitLocker Recovery Keys?
The script returns five pieces of information for each recovery object: computer DN, computer name, TPM recovery presence, escrow date, password ID, and the recovery password itself.
You can query by computer name, pipe in multiple names, or search by the 8-character password ID shown on the BitLocker recovery screen. The -Domain, -Server, and -Credential parameters provide flexibility for cross-domain queries or elevated authentication.
Save the script as Get-ADBitLockerRecovery-ADSI.ps1 and run it from any PowerShell session.
.\Get-ADBitLockerRecovery-ADSI.ps1 -ComputerName "WORKSTATION01"Or search by the password ID displayed during recovery:
.\Get-ADBitLockerRecovery-ADSI.ps1 -PasswordID "A1B2C3D4"What Permissions Do You Need to Read BitLocker Keys?
Reading recovery passwords is a privileged operation. Your account must have delegated read permission on the msFVE-RecoveryPassword attribute in Active Directory.
If password fields return N/A, your account can see the recovery objects but cannot read the secret. Two solutions exist:
- Rerun the script with
-Credentialspecifying an account that has the delegated permission - Start your PowerShell session as an account with the required delegation
.\Get-ADBitLockerRecovery-ADSI.ps1 -ComputerName "LAPTOP05" -Credential (Get-Credential)Domain Admins typically have this permission by default. For least-privilege environments, delegate the specific read permission to your help desk or recovery team.
Why Does BitLocker Recovery Matter Now?
BitLocker recovery lookups spike after Windows updates. WinBuzzer reports that Microsoft's April 2026 Patch Tuesday marked the fourth time in four years that a security update triggered unexpected BitLocker recovery prompts across enterprises. Previous incidents occurred in August 2022, July 2024, and May 2025.
When hundreds of users hit recovery screens simultaneously, having a zero-dependency script matters. You can run it from any domain-joined machine without waiting for RSAT installation. For related patch management challenges, see how June 2026 Patch Tuesday addressed 3 zero-days and 206 CVEs.
How Is the Script Structured?
The script builds on the well-known BitLocker AD recovery script originally authored by Bill Stewart. This version stays close to that original design intentionally, since the value lies in the zero-dependency ADSI approach rather than reinventing the query logic.
Core components include:
[ADSISearcher]for LDAP queries against ADDirectoryEntryfor binding to specific objects- Parameter sets for computer name versus password ID lookups
- Optional credential passthrough for privileged queries
The script queries the msFVE-RecoveryInformation objects stored under computer accounts in AD. Each object contains the recovery password, escrow timestamp, and password ID that maps to the prompt shown during recovery.
FAQ
Why do password fields show N/A when I run the script?
Your account can see the BitLocker recovery objects in Active Directory but lacks permission to read the msFVE-RecoveryPassword attribute. Rerun the script with -Credential specifying an account that has delegated read permission, or start your session as a Domain Admin.
What permissions are needed to read BitLocker keys from AD?
You need read permission on the msFVE-RecoveryPassword attribute of msFVE-RecoveryInformation objects. Domain Admins have this by default. For least-privilege, delegate the specific permission to your help desk group via Active Directory delegation.
Does this script work on PowerShell 7 or PowerShell Core?
The script uses [ADSISearcher] which relies on .NET Framework types. It works on Windows PowerShell 2.0 through 5.1. PowerShell 7 on Windows supports ADSI, but Linux or macOS builds of PowerShell Core cannot use ADSI.
How do I find the 8-character password ID on the BitLocker screen?
The BitLocker recovery screen displays a password ID in the format XXXXXXXX. This 8-character hexadecimal string identifies which recovery key to use. Pass it to the script with -PasswordID "A1B2C3D4" to retrieve the matching recovery password.
Why use ADSI instead of the ActiveDirectory module?
ADSI requires no installation. The ActiveDirectory module needs RSAT, which may not exist on the machine where you need the recovery key. ADSI works on any domain-joined Windows system with PowerShell 2.0 or later.
The script
#Requires -Version 2
<#
.SYNOPSIS
Gets BitLocker recovery information from Active Directory using pure ADSI,
with no dependency on RSAT or the ActiveDirectory module.
.DESCRIPTION
The no-RSAT companion to the ActiveDirectory-module version. It uses raw
ADSI / DirectorySearcher, so it runs on any domain-joined machine (PowerShell
2.0+), which is exactly the box you are often standing at during a recovery.
Look up BitLocker recovery passwords by computer name or by an 8-character
recovery password ID.
If you get no output for a valid password ID, your account lacks permission
to read BitLocker recovery information: use -Credential, or run the session
as an account that has it.
.PARAMETER Name
One or more computer names. Wildcards are not supported.
.PARAMETER PasswordID
The first 8 characters of a recovery password ID (0-9, A-F).
.PARAMETER Domain
Read from computer objects in the specified domain.
.PARAMETER Server
A specific domain controller to query.
.PARAMETER Credential
Credentials with permission to read BitLocker recovery information.
.OUTPUTS
PSObjects: distinguishedName, name, TPMRecoveryInformation, Date,
PasswordID, RecoveryPassword ("N/A" when present but unreadable).
.EXAMPLE
.\Get-ADBitLockerRecovery-ADSI.ps1 "computer1","computer2"
.EXAMPLE
.\Get-ADBitLockerRecovery-ADSI.ps1 -PasswordID 1A2B3C4D
.NOTES
Author : Emanuel De Almeida - https://www.navanem.com
Based on the well-known BitLocker AD recovery script by Bill Stewart (windowsitpro).
.LINK
https://www.navanem.com/scripts
http://technet.microsoft.com/en-us/library/dd875529.aspx
#>
[CmdletBinding(DefaultParameterSetName="Name")]
param(
[parameter(ParameterSetName="Name",Position=0,Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[alias("ComputerName")]
[String[]] $Name,
[parameter(ParameterSetName="PasswordID",Mandatory=$true)]
[String] $PasswordID,
[String] $Domain,
[String] $Server,
[Management.Automation.PSCredential] $Credential
)
begin {
# Validate -PasswordID parameter; we use this rather than the ValidatePattern
# attribute of the parameter to give a better error message
if ( $PSCmdlet.ParameterSetName -eq "PasswordID" ) {
if ( $PasswordID -notmatch '^[0-9A-F]{8}$' ) {
throw "Cannot validate argument on parameter 'PasswordID'. This argument must be exactly 8 characters long and must contain only the characters 0 through 9 and A through F."
}
}
# Pathname object contstants
$ADS_SETTYPE_DN = 4
$ADS_FORMAT_X500_PARENT = 8
$ADS_DISPLAY_VALUE_ONLY = 2
# Pathname object used by Get-ParentPath function
$Pathname = New-Object -ComObject "Pathname"
# Returns the parent path of a distinguished name
function Get-ParentPath {
param(
[String] $distinguishedName
)
[Void] $Pathname.GetType().InvokeMember("Set", "InvokeMethod", $null, $Pathname, ($distinguishedName, $ADS_SETTYPE_DN))
$Pathname.GetType().InvokeMember("Retrieve", "InvokeMethod", $null, $Pathname, $ADS_FORMAT_X500_PARENT)
}
# Returns only the name of the first element of a distinguished name
function Get-NameElement {
param(
[String] $distinguishedName
)
[Void] $Pathname.GetType().InvokeMember("Set", "InvokeMethod", $null, $Pathname, ($distinguishedName, $ADS_SETTYPE_DN))
[Void] $Pathname.GetType().InvokeMember("SetDisplayType", "InvokeMethod", $null, $Pathname, $ADS_DISPLAY_VALUE_ONLY)
$Pathname.GetType().InvokeMember("GetElement", "InvokeMethod", $null, $Pathname, 0)
}
# Outputs a custom object based on a list of hash tables
function Out-Object {
param(
[System.Collections.Hashtable[]] $hashData
)
$order = @()
$result = @{}
$hashData | ForEach-Object {
$order += ($_.Keys -as [Array])[0]
$result += $_
}
New-Object PSObject -Property $result | Select-Object $order
}
# Create and initialize DirectorySearcher object that finds computers
$ComputerSearcher = [ADSISearcher] ""
function Initialize-ComputerSearcher {
if ( $Domain ) {
if ( $Server ) {
$path = "LDAP://$Server/$Domain"
}
else {
$path = "LDAP://$Domain"
}
}
else {
if ( $Server ) {
$path = "LDAP://$Server"
}
else {
$path = ""
}
}
if ( $Credential ) {
$networkCredential = $Credential.GetNetworkCredential()
$dirEntry = New-Object DirectoryServices.DirectoryEntry(
$path,
$networkCredential.UserName,
$networkCredential.Password
)
}
else {
$dirEntry = [ADSI] $path
}
$ComputerSearcher.SearchRoot = $dirEntry
$ComputerSearcher.Filter = "(objectClass=domain)"
try {
[Void] $ComputerSearcher.FindOne()
}
catch [Management.Automation.MethodInvocationException] {
throw $_.Exception.InnerException
}
}
Initialize-ComputerSearcher
# Create and initialize DirectorySearcher for finding
# msFVE-RecoveryInformation objects
$RecoverySearcher = [ADSISearcher] ""
$RecoverySearcher.PageSize = 100
$RecoverySearcher.PropertiesToLoad.AddRange(@("distinguishedName","msFVE-RecoveryGuid","msFVE-RecoveryPassword","name"))
# Gets the DirectoryEntry object for a specified computer
function Get-ComputerDirectoryEntry {
param(
[String] $name
)
$ComputerSearcher.Filter = "(&(objectClass=computer)(name=$name))"
try {
$searchResult = $ComputerSearcher.FindOne()
if ( $searchResult ) {
$searchResult.GetDirectoryEntry()
}
}
catch [Management.Automation.MethodInvocationException] {
Write-Error -Exception $_.Exception.InnerException
}
}
# Outputs $true if the piped DirectoryEntry has the specified property set,
# or $false otherwise
function Test-DirectoryEntryProperty {
param(
[String] $property
)
process {
try {
$_.Get($property) -ne $null
}
catch [Management.Automation.MethodInvocationException] {
$false
}
}
}
# Gets a property from a ResultPropertyCollection; specify $propertyName
# in lowercase to remain compatible with PowerShell v2
function Get-SearchResultProperty {
param(
[DirectoryServices.ResultPropertyCollection] $properties,
[String] $propertyName
)
if ( $properties[$propertyName] ) {
$properties[$propertyName][0]
}
}
# Gets BitLocker recovery information for the specified computer
function GetBitLockerRecovery {
param(
$name
)
$domainName = $ComputerSearcher.SearchRoot.dc
$computerDirEntry = Get-ComputerDirectoryEntry $name
if ( -not $computerDirEntry ) {
Write-Error "Unable to find computer '$name' in domain '$domainName'" -Category ObjectNotFound
return
}
# If the msTPM-OwnerInformation (Vista/Server 2008/7/Server 2008 R2) or
# msTPM-TpmInformationForComputer (Windows 8/Server 2012 or later)
# attribute is set, then TPM recovery information is stored in AD
$tpmRecoveryInformation = $computerDirEntry | Test-DirectoryEntryProperty "msTPM-OwnerInformation"
if ( -not $tpmRecoveryInformation ) {
$tpmRecoveryInformation = $computerDirEntry | Test-DirectoryEntryProperty "msTPM-TpmInformationForComputer"
}
$RecoverySearcher.SearchRoot = $computerDirEntry
$searchResults = $RecoverySearcher.FindAll()
foreach ( $searchResult in $searchResults ) {
$properties = $searchResult.Properties
$recoveryPassword = Get-SearchResultProperty $properties "msfve-recoverypassword"
if ( $recoveryPassword ) {
$recoveryDate = ([DateTimeOffset] ((Get-SearchResultProperty $properties "name") -split '{')[0]).DateTime
$passwordID = ([Guid] [Byte[]] (Get-SearchResultProperty $properties "msfve-recoveryguid")).Guid
}
else {
$tpmRecoveryInformation = $recoveryDate = $passwordID = $recoveryPassword = "N/A"
}
Out-Object `
@{"distinguishedName" = $computerDirEntry.Properties["distinguishedname"][0]},
@{"name" = $computerDirEntry.Properties["name"][0]},
@{"TPMRecoveryInformation" = $tpmRecoveryInformation},
@{"Date" = $recoveryDate},
@{"PasswordID" = $passwordID.ToUpper()},
@{"RecoveryPassword" = $recoveryPassword.ToUpper()}
}
$searchResults.Dispose()
}
# Searches for BitLocker recovery information for the specified password ID
function SearchBitLockerRecoveryByPasswordID {
param(
[String] $passwordID
)
$RecoverySearcher.Filter = "(&(objectClass=msFVE-RecoveryInformation)(name=*{$passwordID-*}))"
$searchResults = $RecoverySearcher.FindAll()
foreach ( $searchResult in $searchResults ) {
$properties = $searchResult.Properties
$computerName = Get-NameElement (Get-ParentPath (Get-SearchResultProperty $properties "distinguishedname"))
$RecoverySearcher.Filter = "(objectClass=msFVE-RecoveryInformation)"
GetBitLockerRecovery $computerName | Where-Object { $_.PasswordID -match "^$passwordID-" }
}
$searchResults.Dispose()
}
}
process {
if ( $PSCmdlet.ParameterSetName -eq "Name" ) {
$RecoverySearcher.Filter = "(objectClass=msFVE-RecoveryInformation)"
foreach ( $nameItem in $Name ) {
GetBitLockerRecovery $nameItem
}
}
elseif ( $PSCmdlet.ParameterSetName -eq "PasswordID" ) {
SearchBitLockerRecoveryByPasswordID $PasswordID
}
}
Review before running. Test in a non-production environment first.