Backing Up BitLocker Keys to OneDrive as a Scheduled Task

While I can’t say I love Bitlocker, I do understand it as a requirement for any machine with corporate data.  That said, it seems quite capricious when the BitLocker gremlin decides to require the 48-digit recovery key.   I’ve taken to saving my recovery keys to OneDrive, so I can bring up the data on my phone when I need it. 

Easter Eggs

These might not be worth a blog post by themselves, but are noteworthy enough for some text on their own.

SkyDrive, Uh, OneDrive

I dropped a second drive into my laptop (gotta love how the W530 can sport 3 hard disks!), so I pointed OneDrive to the existing files, now on drive D: instead of C:.  My prior version of this hardcoded the path to $home\OneDrive\BitlockerRecovery, but that wasn’t the case anymore.  And, I had to find out the hard way.  So, here’s the first Easter egg: how to get your OneDrive path.

001 002 003 004 005 006 007 008 009 010 $oneDriveRegKeyPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\SkyDrive'; if (Test-Path -Path $oneDriveRegKeyPath) { $path = (Get-ItemProperty -Path $oneDriveRegKeyPath).UserFolder; } else  { $path = "$home\OneDrive"; }

Yes, it’s still stored in the registry as SkyDrive, at least under Windows 8.1  Please note that the Registry is what is termed a private interface – no one is making any guarantee that this regkey will be valid in the upcoming Windows 10.   I’m just using what I found lying around.

Run As Admin

I could see requiring you to be admin to enable / disable / modify BitLocker settings, but even to view the recovery key requires you to be admin.  Here’s a quick-and-dirty way to test if you’re running elevated or not:

001 002 003 004 005 006 007 008 009 $user = New-Object -TypeName Security.Principal.WindowsPrincipal -ArgumentList ( [Security.Principal.WindowsIdentity]::GetCurrent() ); if (!$user.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) { Write-Warning 'Not running as Administrator. Exiting'; return; }

This Easter egg was used in some earlier scripts, too.

Base64 Encoded Commands

PowerShell.exe -Help details how to generate a Base64 encoded command from a string, and this shows how to auto-generate it (and how to test it) from a file.

001 002 003 004 005 006 007 008 009 010 011 012 function Convert-ThisFileToEncodedCommand { if ($scriptName = (&{$MyInvocation}).ScriptName) { $command = [string](Get-Content $scriptName) -replace '\s\s+', ' '; [Convert]::ToBase64String( [System.Text.Encoding]::Unicode.GetBytes($command)); } } function Test-ThisFileAsEncodedCommand  { powershell.exe -NoProfile -EncodedCommand (Convert-ThisFileToEncodedCommand) }

We’ll use the Convert-ThisFileToEncodedCommand by itself  a little later.  Test-ThisFileAsEncodedCommand makes sure our script will run when it’s encoded in base64.  Here are some of the gotchas:

  • No comments.  -EncodedCommand expects a single command, so an unquoted ‘#’ in anything will cause the rest of the line to be ignored.  Ungood.
  • No heretext.  @’. . .’@ and @”. . .”@ won’t work because the terminating string is expected to start at column 0.
  • Semi; colons; everywhere;  I; mean; EVERYWHERE;.  Remember, –EncodedCommand expects it to be all in one line.  This line can have multiple commands.  It can even have functions, trap{}, etc.  PowerShell, like Perl, is quite happy to be written in an obfuscated format, such as everything in a single line.  We humans have a harder time.

The Script

Without further ado, here’s the script to back up your keys to OneDrive.

001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 function Backup-BitLockerRecoveryKeys { trap { Write-Warning $_.Exception.Message; return; } $user = New-Object -TypeName Security.Principal.WindowsPrincipal -ArgumentList ( [Security.Principal.WindowsIdentity]::GetCurrent() ); if (!$user.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) { Write-Warning 'Not running as Administrator. Exiting'; return; } $error.clear(); $oneDriveRegKeyPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\SkyDrive'; if (Test-Path -Path $oneDriveRegKeyPath) { $path = (Get-ItemProperty -Path $oneDriveRegKeyPath).UserFolder; } else { $path = "$home\OneDrive"; } $domainName = (Get-WmiObject -Class Win32_Computersystem).Domain; $computerName = "$env:Computername.$domainName".ToLower(); $path = "$path\BitLockerRecovery\$computerName.txt"; if (Test-Path -Path $path) { try { $lastData = Import-Clixml -Path $path; } catch { Remove-Item -Path $path; } } else { New-Item -Path $path -ItemType Directory | Out-Null; Remove-Item -Path $path; Export-Clixml -Path $path -InputObject $null; } Get-BitLockerVolume | Sort-Object -Property MountPoint | % { $bitlockerVolume = $_; $bitlockerVolume.KeyProtector | % { $outputObject = New-Object -TypeName PsObject | Select-Object -Property Drive, Guid, Key; $outputObject.Drive = $bitlockerVolume.MountPoint; $outputObject.GUID = $_.KeyProtectorId -replace '[{}]'; if ($outputObject.Key = $_.RecoveryPassword) { $outputObject; } } } | Tee-Object -Variable currentdata; if ( $lastData -and (Compare-Object -ReferenceObject $currentData -DifferenceObject $lastData) ) { $lastWriteTime = (Get-Item -Path $path).LastAccessTime.ToString('yyyy-MM-dd'); $destination = $path -replace '\.txt', "($lastWriteTime).txt"; Move-Item -Path $path -Destination $destination; Write-Host -ForegroundColor Green "'$path' moved to '$destination'" } Export-Clixml -Path $path -InputObject $currentData; Write-Host -ForegroundColor Green "Saved to '$path'"; } function Convert-ThisFileToEncodedCommand { if ($scriptName = (&{$MyInvocation}).ScriptName) { $command = [string](Get-Content $scriptName) -replace '\s\s+', ' '; [Convert]::ToBase64String( [System.Text.Encoding]::Unicode.GetBytes($command)); } } function Test-ThisFileAsEncodedCommand { powershell.exe -NoProfile -EncodedCommand (Convert-ThisFileToEncodedCommand); } Backup-BitLockerRecoveryKeys | Format-List; Start-Sleep 3;


The Scheduled Task

The file below has three separate placeholders:

%USERDNSDOMAN% is your AD domain, or your computer name if your computer is not AD-joined. 

%USERNAME% is your username.  You need both to specify who you want your script to run as (you), which in turn is needed so you can run it with highest privileges (run as administrator, in other words.)

%ENCODEDCOMMAND% is the output of Convert-ThisFileToEncodedCommand.  This seems the most transparent way of sharing the string so you can be sure I’m not telling you to install a malicious scheduled task.

Replace the strings, save it as a XML file, open Task Scheduler and import the task.  I have a new folder ‘User’ for my own scripts, but you can drop them into the main ‘Task Scheduler Library’ folder if you want.

001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 <?xml version="1.0" encoding="UTF-16"?> <Task version="1.2" xmlns="https://schemas.microsoft.com/windows/2004/02/mit/task"> <RegistrationInfo> <Date>2014-08-11T12:14:26.4481786</Date> <Author>%USERDNSDOMAIN%\%USERNAME%</Author> </RegistrationInfo> <Triggers> <CalendarTrigger> <StartBoundary>2014-08-11T10:00:00</StartBoundary> <Enabled>true</Enabled> <ScheduleByDay> <DaysInterval>1</DaysInterval> </ScheduleByDay> </CalendarTrigger> <LogonTrigger> <Enabled>true</Enabled> <UserId>%USERDNSDOMAIN%\%USERNAME%</UserId> </LogonTrigger> </Triggers> <Principals> <Principal id="Author"> <UserId>%USERDNSDOMAIN%\%USERNAME%</UserId> <LogonType>InteractiveToken</LogonType> <RunLevel>HighestAvailable</RunLevel> </Principal> </Principals> <Settings> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries> <AllowHardTerminate>true</AllowHardTerminate> <StartWhenAvailable>false</StartWhenAvailable> <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable> <IdleSettings> <StopOnIdleEnd>true</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings> <AllowStartOnDemand>true</AllowStartOnDemand> <Enabled>true</Enabled> <Hidden>true</Hidden> <RunOnlyIfIdle>false</RunOnlyIfIdle> <WakeToRun>false</WakeToRun> <ExecutionTimeLimit>P3D</ExecutionTimeLimit> <Priority>7</Priority> </Settings> <Actions Context="Author"> <Exec> <Command>C:\windows\System32\WindowsPowerShell\v1.0\powershell.exe</Command> <Arguments> -NoProfile -EncodedCommand %ENCODEDCOMMAND%       </Arguments> <WorkingDirectory>c:\temp</WorkingDirectory> </Exec> </Actions> </Task>

An interesting gotcha comes from this as well: when you look at the task properties, you can see it’s a PowerShell task, but the encoded command string is so long that the arguments window is empty  The encoded command string is visible only when you re-export the task.