├── Create-Encrypted-VHD.Tests.ps1 ├── Create-Encrypted-VHD.bat ├── Create-Encrypted-VHD.ps1 ├── LICENSE └── README.md /Create-Encrypted-VHD.Tests.ps1: -------------------------------------------------------------------------------- 1 | # ############################### 2 | # BitLocker encrypted vhd from batch or PowerShell 3 | # https://github.com/neil-sabol/bitlocker-encrypted-vhd-from-batch-or-powershell 4 | # Neil Sabol 5 | # neil.sabol@gmail.com 6 | # ############################### 7 | 8 | # Ensure tests are running as admin 9 | $myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent() 10 | $myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID) 11 | $adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator 12 | if (-not $myWindowsPrincipal.IsInRole($adminRole)) { 13 | write-host "" 14 | write-host "These tests must be run in an elevated (administrator) PowerShell session." 15 | write-host "Please re-launch PowerShell as administrator and try again." 16 | write-host "" 17 | exit 18 | } 19 | 20 | # Ensure the BitLocker PowerShell module is present 21 | if( -not (Get-Module -ListAvailable -Name "BitLocker")) { 22 | write-host "" 23 | write-host "These tests require the BitLocker PowerShell module." 24 | write-host "Please install the module and try again." 25 | write-host "" 26 | exit 27 | } 28 | 29 | # Capture the path the script and tests reside in 30 | $script:currentPath = (Split-Path -Parent $MyInvocation.MyCommand.Path) 31 | 32 | # Define test parameters 33 | $script:name = "TestContainer" 34 | $script:path = "C:\Temp" 35 | $script:sizeMB = "128" 36 | $script:driveletter = "X:" 37 | $script:cred = ("Abcd-1234" | ConvertTo-SecureString -AsPlainText -Force) 38 | $script:badCred = ("NOTAbcd-1234" | ConvertTo-SecureString -AsPlainText -Force) 39 | $script:createShortcuts = "y" 40 | 41 | # Begin tests 42 | Describe 'Create-Encrypted-VHD' { 43 | BeforeAll { 44 | # Create the test container path if it does not exist 45 | If(-Not (Test-Path "$path")) { New-Item -Path "$path" -ItemType "Directory" } 46 | # Generate the test encrypted container using the test parameters 47 | . $currentPath\Create-Encrypted-VHD.ps1 -vhdNam $name -vhdPath $path -vhdSize $sizeMB -vhdLetter $driveletter -vhdCredential $cred -confirmscriptcreation $createShortcuts 48 | # Wait 10 seconds to ensure Bitlocker encryption completes 49 | sleep 10 50 | } 51 | Context "Basic functionality" { 52 | It "Should create a VHD file in the location specified ($path\$name.vhd)" { 53 | "$path\$name.vhd" | Should -Exist 54 | } 55 | It "Should enable Bitlocker encryption on the mounte VHD file ($driveletter)" { 56 | Get-BitLockerVolume -MountPoint $driveletter | select -ExpandProperty VolumeStatus | Should -Be "FullyEncrypted" 57 | } 58 | It "Should mount the VHD file ($path\$name.vhd) by default to $driveletter" { 59 | "$driveletter" | Should -Exist 60 | } 61 | } 62 | Context "Input and output" { 63 | It "Should allow the creation of a text file in the encrypted VHD file" { 64 | New-Item -Path "$driveletter\" -Name "testfile1.txt" -ItemType "File" -Value "This is a text string." 65 | Get-Content -Path "$driveletter\testfile1.txt" | Should -Be "This is a text string." 66 | } 67 | It "Should allow the creation of a folder in the encrypted VHD file" { 68 | New-Item -Path "$driveletter\" -Name "testfolder" -ItemType "Directory" 69 | "$driveletter\testfolder" | Should -Exist 70 | } 71 | It "Should allow the creation of a text file in a folder in the encrypted VHD file" { 72 | New-Item -Path "$driveletter\testfolder\" -Name "testfile2.txt" -ItemType "File" -Value "This is a another text string." 73 | Get-Content -Path "$driveletter\testfolder\testfile2.txt" | Should -Be "This is a another text string." 74 | } 75 | } 76 | Context "Bitlocker encryption" { 77 | It "Should allow the VHD to be Bitlocker locked" { 78 | { Lock-BitLocker -MountPoint "$driveletter" -ForceDismount } | Should -Not -Throw 79 | } 80 | It "Should NOT allow access to the VHD contents while it is Bitlocker locked" { 81 | { Get-Content -Path "$driveletter\testfile1.txt" -ErrorAction Stop } | Should -Throw 82 | } 83 | It "Should NOT allow Bitlocker to unlock the VHD with an incorrect password" { 84 | { Unlock-BitLocker -MountPoint "$driveletter" -Password $badCred -ErrorAction Stop } | Should -Throw 85 | } 86 | It "Should allow Bitlocker to unlock the VHD with the correct password" { 87 | { Unlock-BitLocker -MountPoint "$driveletter" -Password $cred -ErrorAction Stop } | Should -Not -Throw 88 | } 89 | } 90 | Context "Data integrity" { 91 | It "Should produce the same test file content after locking and unlocking the VHD" { 92 | Get-Content -Path "$driveletter\testfile1.txt" | Should -Be "This is a text string." 93 | } 94 | It "Should produce the same test folder after locking and unlocking the VHD" { 95 | "$driveletter\testfolder" | Should -Exist 96 | } 97 | It "Should produce the same test file content within a folder after locking and unlocking the VHD" { 98 | Get-Content -Path "$driveletter\testfolder\testfile2.txt" | Should -Be "This is a another text string." 99 | } 100 | } 101 | Context "Mount and unmount scripts" { 102 | It "Should create a mount script on the user's desktop" { 103 | "$env:USERPROFILE\Desktop\MOUNT-$name.ps1" | Should -Exist 104 | } 105 | It "Should create an unmount script on the user's desktop" { 106 | "$env:USERPROFILE\Desktop\UNMOUNT-$name.ps1" | Should -Exist 107 | } 108 | It "Should create an unmount script than unmounts the VHD file when called" { 109 | . $env:USERPROFILE\Desktop\UNMOUNT-$name.ps1 110 | "$driveletter" | Should -Not -Exist 111 | } 112 | It "Should create an mount script than mounts the VHD file when called" { 113 | . $env:USERPROFILE\Desktop\MOUNT-$name.ps1 114 | Unlock-BitLocker -MountPoint "$driveletter" -Password $cred 115 | "$driveletter" | Should -Exist 116 | } 117 | } 118 | AfterAll { 119 | # Clean up - remove all artifacts created by the test 120 | sleep 2 121 | Remove-Item -Path "$env:USERPROFILE\Desktop\MOUNT-$name.ps1" -Force -ErrorAction SilentlyContinue | Out-Null 122 | Remove-Item -Path "$env:USERPROFILE\Desktop\UNMOUNT-$name.ps1" -Force -ErrorAction SilentlyContinue | Out-Null 123 | Dismount-DiskImage -ImagePath "$path\$name.vhd" -ErrorAction SilentlyContinue | Out-Null 124 | sleep 2 125 | Remove-Item -Path "$path\$name.vhd" -Force -ErrorAction SilentlyContinue | Out-Null 126 | } 127 | } -------------------------------------------------------------------------------- /Create-Encrypted-VHD.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | @REM ############################### 3 | @REM Neil Sabol 4 | @REM neil.sabol@gmail.com 5 | @REM ############################### 6 | 7 | @REM See http://stackoverflow.com/questions/7044985/how-can-i-auto-elevate-my-batch-file-so-that-it-requests-from-uac-admin-rights 8 | @REM This escalation code is GENIUS! 9 | @REM Thanks @Matt (http://stackoverflow.com/users/1016343/matt) 10 | 11 | :checkPrivileges 12 | NET FILE 1>NUL 2>NUL 13 | if '%errorlevel%' == '0' ( goto gotPrivileges ) else ( goto getPrivileges ) 14 | 15 | :getPrivileges 16 | if '%1'=='ELEV' (shift & goto gotPrivileges) 17 | 18 | setlocal DisableDelayedExpansion 19 | set "batchPath=%~0" 20 | setlocal EnableDelayedExpansion 21 | ECHO Set UAC = CreateObject^("Shell.Application"^) > "%temp%\OEgetPrivileges.vbs" 22 | ECHO UAC.ShellExecute "!batchPath!", "ELEV", "", "runas", 1 >> "%temp%\OEgetPrivileges.vbs" 23 | "%temp%\OEgetPrivileges.vbs" 24 | exit /B 25 | 26 | :gotPrivileges 27 | setlocal & pushd . 28 | 29 | @ECHO --------------------------------------------------------------------- 30 | @ECHO This script helps you create an encrypted container to store sensitive 31 | @ECHO information. You will be prompted to specify a location, name, size, 32 | @ECHO and password for the container. Scripts to mount and unmount 33 | @ECHO the container will be generated automatically and placed on your desktop. 34 | @ECHO. 35 | @ECHO DO NOT CLOSE THIS WINDOW - it will close automatically when the process is 36 | @ECHO is complete. 37 | @ECHO --------------------------------------------------------------------- 38 | @ECHO. 39 | @ECHO. 40 | :SetVHDParameters 41 | SET vhdName=%USERNAME%_private 42 | SET vhdPath=C:\Users\%USERNAME%\Desktop 43 | SET vhdSize=1024 44 | SET vhdLetter=Y: 45 | @ECHO To accept the default options, press enter four times and set a password 46 | @ECHO. 47 | @ECHO. 48 | @ECHO Helpful hints: 49 | @ECHO *Paths must be fully qualified (i.e. H:, C:\Users\yourname\Desktop, etc.) 50 | @ECHO *You can copy/paste paths from Explorer 51 | @ECHO *Size must be a number in MB (500, 1024, etc.) 52 | @ECHO *There are 1024 MB in 1 GB 53 | @ECHO. 54 | @ECHO. 55 | @SET /P vhdName=Encrypted container name? (default is %USERNAME%_private): %=% 56 | @SET /P vhdPath=Location? (default path is C:\Users\%USERNAME%\Desktop ): %=% 57 | @SET /P vhdSize=Size in MB? (default is 1024): %=% 58 | @SET /P vhdLetter=Drive letter? (default is Y: ): %=% 59 | @ECHO. 60 | @ECHO. 61 | @ECHO CREATE VDISK FILE="%vhdPath%\%vhdName%.vhd" MAXIMUM=%vhdSize% TYPE=expandable > diskpart.txt 62 | @ECHO SELECT VDISK FILE="%vhdPath%\%vhdName%.vhd" >> diskpart.txt 63 | @ECHO ATTACH VDISK >> diskpart.txt 64 | @ECHO CREATE PARTITION PRIMARY >> diskpart.txt 65 | @ECHO FORMAT QUICK FS=NTFS LABEL="%vhdName%" >> diskpart.txt 66 | @ECHO ASSIGN LETTER=%vhdLetter% >> diskpart.txt 67 | @diskpart /s diskpart.txt > nul 2>&1 68 | @IF %ERRORLEVEL% NEQ 0 GOTO SetVHDParameters 69 | @del diskpart.txt 70 | @REM diskpart accepts just a letter, manage-bde (later) needs letter colon, i.e. Y: 71 | @REM This is not a perfect fix, but appending a colon if one is missing catches some issues. 72 | @IF NOT [%vhdLetter:~-1%]==[:] SET vhdLetter=%vhdLetter%: 73 | :SetBitlockerPassword 74 | @ECHO. 75 | @ECHO --------------------------------------------------------------------- 76 | @ECHO PLEASE NOTE, IF YOU LOSE THE PASSWORD YOU SPECIFY FOR THIS CONTAINER, 77 | @ECHO YOUR DATA WILL BE UNRECOVERABLE. 78 | @ECHO --------------------------------------------------------------------- 79 | @ECHO. 80 | @manage-bde -on %vhdLetter% -used -Password 81 | @IF %ERRORLEVEL% NEQ 0 GOTO SetBitlockerPassword 82 | @ECHO select vdisk file="%vhdPath%\%vhdName%.vhd" > %USERPROFILE%\Desktop\MOUNT-%vhdName%.bat 83 | @ECHO attach vdisk >> %USERPROFILE%\Desktop\MOUNT-%vhdName%.bat 84 | @ECHO diskpart /s %USERPROFILE%\Desktop\MOUNT-%vhdName%.bat >> %USERPROFILE%\Desktop\MOUNT-%vhdName%.bat 85 | @ECHO select vdisk file="%vhdPath%\%vhdName%.vhd" > %USERPROFILE%\Desktop\UMOUNT-%vhdName%.bat 86 | @ECHO detach vdisk >> %USERPROFILE%\Desktop\UMOUNT-%vhdName%.bat 87 | @ECHO diskpart /s %USERPROFILE%\Desktop\UMOUNT-%vhdName%.bat >> %USERPROFILE%\Desktop\UMOUNT-%vhdName%.bat 88 | @ECHO. 89 | @ECHO. 90 | @ECHO Your encrypted container was created! You can use the "MOUNT-%vhdName%" 91 | @ECHO and "UMOUNT-%vhdName%" scripts on your desktop to mount and unmount the 92 | @ECHO container going forward. This script already mounted the container and will 93 | @ECHO open it when you press a key. 94 | @ECHO. 95 | @ECHO. 96 | pause 97 | EXPLORER.EXE %vhdLetter% 98 | EXIT 99 | -------------------------------------------------------------------------------- /Create-Encrypted-VHD.ps1: -------------------------------------------------------------------------------- 1 | # ############################### 2 | # BitLocker encrypted vhd from batch or PowerShell 3 | # https://github.com/neil-sabol/bitlocker-encrypted-vhd-from-batch-or-powershell 4 | # Neil Sabol 5 | # neil.sabol@gmail.com 6 | # ############################### 7 | 8 | # Allow parameters to be passed (for automation) - note that $vhdCredential must be 9 | # a PowerShell credential object. 10 | param ($vhdName, $vhdPath, $vhdSize, $vhdLetter, $vhdCredential, $confirmscriptcreation) 11 | 12 | # Ensure either the BitLocker PowerShell Module or manage-bde command line tool is available 13 | # https://docs.microsoft.com/en-us/powershell/module/bitlocker/?view=win10-ps 14 | # https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/manage-bde 15 | # One of these is required to programmatically encrypt the VHD container 16 | if((Get-Module -ListAvailable -Name "BitLocker")) { 17 | $encryptMethod = "powershell" 18 | } elseif((Test-Path "C:\Windows\System32\manage-bde.exe")) { 19 | $encryptMethod = "manage-bde" 20 | if($vhdCredential) { 21 | Write-Host "" 22 | Write-Host "WARNING: The -cred parameter will be ignored since the BitLocker" 23 | Write-Host "PowerShell Module is not available on this system. You will need" 24 | Write-Host "to set your container password manually when prompted." 25 | Write-Host "" 26 | } 27 | } else { 28 | Write-Host "" 29 | Write-Host "Sorry, no suitable BitLocker management commands are available. This script will now exit." 30 | Write-Host "" 31 | if(-not ($vhdName -and $vhdPath -and $vhdSize -and $vhdLetter -and $vhdCredential -and $confirmscriptcreation)) { pause } 32 | exit 1 33 | } 34 | 35 | # See https://blogs.msdn.microsoft.com/virtual_pc_guy/2010/09/23/a-self-elevating-powershell-script/ 36 | # This escalation code is GENIUS! 37 | # Thanks @Ben (https://social.msdn.microsoft.com/profile/Benjamin+Armstrong) 38 | 39 | # Get the ID and security principal of the current user account 40 | $myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent() 41 | $myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID) 42 | # Get the security principal for the Administrator role 43 | $adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator 44 | # Check to see if we are currently running "as Administrator" 45 | if ($myWindowsPrincipal.IsInRole($adminRole)) { 46 | # We are running "as Administrator" - no action needed 47 | } else { 48 | # We are not running "as Administrator" - so relaunch as administrator 49 | # Create a new process object that starts PowerShell 50 | $newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell"; 51 | # Specify the current script path and name as a parameter 52 | $newProcess.Arguments = $myInvocation.MyCommand.Definition; 53 | # Indicate that the process should be elevated 54 | $newProcess.Verb = "runas"; 55 | # Start the new process 56 | [System.Diagnostics.Process]::Start($newProcess); 57 | # Exit from the current, unelevated, process 58 | exit 59 | } 60 | 61 | # Begin the actual script (elevated) 62 | # Print friendly introduction unless running non-interactively 63 | if(-not ($vhdName -and $vhdPath -and $vhdSize -and $vhdLetter -and $vhdCredential -and $confirmscriptcreation)) { 64 | Write-Host "" 65 | Write-Host "---------------------------------------------------------------------" 66 | Write-Host "This script helps you create an encrypted container to store sensitive" 67 | Write-Host "information. You will be prompted to specify a location, name, size," 68 | Write-Host "and password for the container. If desired, scripts to mount and" 69 | Write-Host "unmount the container will be created and placed on your desktop." 70 | Write-Host "" 71 | Write-Host "DO NOT CLOSE THIS WINDOW - it will close automatically when the process is" 72 | Write-Host "is complete." 73 | Write-Host "---------------------------------------------------------------------" 74 | Write-Host "" 75 | } 76 | 77 | $vhdNameDefault=$env:UserName + "_private" 78 | $vhdPathDefault="C:\Users\$env:UserName\Desktop" 79 | $vhdSizeDefault=1024 80 | $vhdLetterDefault="Y:" 81 | 82 | # Print hints unless running non-interactively 83 | if(-not ($vhdName -and $vhdPath -and $vhdSize -and $vhdLetter -and $vhdCredential -and $confirmscriptcreation)) { 84 | Write-Host "To accept the default options, press enter four times and set a password" 85 | Write-Host "" 86 | Write-Host "" 87 | Write-Host "Helpful hints:" 88 | Write-Host "--------------" 89 | Write-Host "*Paths must be fully qualified (i.e. H:, C:\Users\yourname\Desktop, etc.)" 90 | Write-Host "*You can copy/paste paths from Explorer" 91 | Write-Host "*Size must be a number in MB (500, 1024, etc.)" 92 | Write-Host "*There are 1024 MB in 1 GB" 93 | Write-Host "" 94 | Write-Host "" 95 | } 96 | 97 | # Capture user preferences regarding container creation - skip setting supplied via parameters 98 | if(-not $vhdName) { 99 | $vhdName=Read-Host -Prompt "Encrypted container name? (default is $vhdNameDefault )" 100 | if($vhdName -eq ""){$vhdName=$vhdNameDefault} 101 | } 102 | 103 | if(-not $vhdPath) { 104 | $vhdPath=Read-Host -Prompt "Location? (default path is C:\Users\$env:UserName\Desktop )" 105 | if($vhdPath -eq ""){$vhdPath=$vhdPathDefault} 106 | } 107 | 108 | if(-not $vhdSize) { 109 | $vhdSize=Read-Host -Prompt "Size in MB? (default is 1024 )" 110 | if($vhdSize -eq ""){$vhdSize=$vhdSizeDefault} 111 | } 112 | 113 | if(-not $vhdLetter) { 114 | $vhdLetter=Read-Host -Prompt "Drive letter? (default is Y: )" 115 | if($vhdLetter -eq ""){$vhdLetter=$vhdLetterDefault} 116 | } 117 | 118 | # Create a diskpart script and execute diskpart 119 | if(-not ($vhdName -and $vhdPath -and $vhdSize -and $vhdLetter -and $vhdCredential -and $confirmscriptcreation)) { Write-Host "" } 120 | "CREATE VDISK FILE=`"$vhdPath\$vhdName.vhd`" MAXIMUM=$vhdSize TYPE=expandable" | Out-File -filepath diskpart.txt 121 | "SELECT VDISK FILE=`"$vhdPath\$vhdName.vhd`"" | Out-File -filepath diskpart.txt -Append 122 | "ATTACH VDISK" | Out-File -filepath diskpart.txt -Append 123 | "CREATE PARTITION PRIMARY" | Out-File -filepath diskpart.txt -Append 124 | "FORMAT QUICK FS=NTFS LABEL=`"$vhdName`"" | Out-File -filepath diskpart.txt -Append 125 | "ASSIGN LETTER=$vhdLetter" | Out-File -filepath diskpart.txt -Append 126 | Type diskpart.txt | diskpart | Out-Null 127 | if($lastExitCode -ne 0) { 128 | del diskpart.txt 129 | Write-Host "ERROR: Something went wrong while creating the VHD file - the script will now terminate." 130 | if(-not ($vhdName -and $vhdPath -and $vhdSize -and $vhdLetter -and $vhdCredential -and $confirmscriptcreation)) { pause } 131 | exit 1 132 | } 133 | del diskpart.txt 134 | 135 | # diskpart accepts a drive letter or drive letter with a colon (i.e. F -or- F:) - manage-bde DOES NOT and always requires a colon (i.e F:) 136 | # This is not a perfect fix, but appending a colon if one is missing catches some issues. 137 | if(! $vhdLetter.EndsWith(":") ) { 138 | $vhdLetter=$vhdLetter+":" 139 | } 140 | 141 | # Print password warning unless running non-interactively 142 | if(-not ($vhdName -and $vhdPath -and $vhdSize -and $vhdLetter -and $vhdCredential -and $confirmscriptcreation)) { 143 | Write-Host "" 144 | Write-Host "---------------------------------------------------------------------" 145 | Write-Host "PLEASE NOTE, IF YOU LOSE THE PASSWORD YOU SPECIFY FOR THIS CONTAINER," 146 | Write-Host "YOUR DATA WILL BE UNRECOVERABLE." 147 | Write-Host "---------------------------------------------------------------------" 148 | Write-Host "" 149 | } 150 | 151 | # Enable bitlocker using PowerShell (Enable-BitLocker) and prompt for a password on the new volume if not supplied via parameter 152 | if($encryptMethod -eq "powershell") { 153 | if($vhdCredential) { 154 | Enable-BitLocker -MountPoint $vhdLetter -EncryptionMethod Aes256 -UsedSpaceOnly -Password $vhdCredential -PasswordProtector | Out-Null 155 | } else { 156 | # Enable-Bitlocker oes not prompt for a password (it must be supplied as a secure string) 157 | # Replicate the password prompt and behavior from manage-bde and apply it to the enable-bitlocker cmdlet 158 | $vhdCredential=Read-Host -AsSecureString -Prompt "Type the password to use to protect the volume" 159 | $vhdCredentialConfirm=Read-Host -AsSecureString -Prompt "Confirm the password by typing it again" 160 | 161 | # This is not ideal, but secure strings must be converted back to plain text for comparison 162 | # See https://www.roelvanlisdonk.nl/2010/03/23/show-password-in-plaintext-by-using-get-credential-in-powershell/ 163 | $vhdCredentialAsText = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($vhdCredential)) 164 | $vhdCredentialConfirmAsText = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($vhdCredentialConfirm)) 165 | if($vhdCredentialAsText -eq $vhdCredentialConfirmAsText) { 166 | # Clear the plain-text password values as soon as possible 167 | $vhdCredentialAsText = "" 168 | $vhdCredentialConfirmAsText = "" 169 | Enable-BitLocker -MountPoint $vhdLetter -EncryptionMethod Aes256 -UsedSpaceOnly -Password $vhdCredential -PasswordProtector | Out-Null 170 | } else { 171 | # Clear the plain-text password values as soon as possible 172 | $vhdCredentialAsText = "" 173 | $vhdCredentialConfirmAsText = "" 174 | # Try to clean up the mounted VHD and file 175 | Dismount-DiskImage -ImagePath "$vhdPath\$vhdName.vhd" 176 | sleep 2 177 | Remove-Item -Path "$vhdPath\$vhdName.vhd" -Force | Out-Null 178 | write-host "" 179 | write-host "ERROR: The values you have entered do not match - the script will now terminate." 180 | write-host "You may need to open Disk Management and manually detach the VHD file prior" 181 | Write-Host "to deleting it." 182 | write-host "" 183 | if(-not ($vhdName -and $vhdPath -and $vhdSize -and $vhdLetter -and $vhdCredential -and $confirmscriptcreation)) { pause } 184 | exit 1 185 | } 186 | } 187 | if($lastExitCode -ne 0) { 188 | # Try to clean up the mounted VHD and file 189 | Dismount-DiskImage -ImagePath "$vhdPath\$vhdName.vhd" 190 | sleep 2 191 | Remove-Item -Path "$vhdPath\$vhdName.vhd" -Force | Out-Null 192 | Write-Host "" 193 | Write-Host "ERROR: Something went wrong while encrypting the VHD file - the script will now terminate. You may need to open" 194 | Write-Host "Disk Management and manually detach the VHD file prior to deleting it." 195 | Write-Host "" 196 | if(-not ($vhdName -and $vhdPath -and $vhdSize -and $vhdLetter -and $vhdCredential -and $confirmscriptcreation)) { pause } 197 | exit 1 198 | } 199 | } 200 | 201 | # Enable bitlocker using manage-bde if the PowerShell module is not available (and prompt for a password on the new volume) 202 | if($encryptMethod -eq "manage-bde") { 203 | manage-bde -on $vhdLetter -EncryptionMethod aes256 -used -Password 204 | if($lastExitCode -ne 0) { 205 | # Try to clean up the mounted VHD and file 206 | Dismount-DiskImage -ImagePath "$vhdPath\$vhdName.vhd" | Out-Null 207 | sleep 2 208 | Remove-Item -Path "$vhdPath\$vhdName.vhd" -Force 209 | Write-Host "" 210 | Write-Host "ERROR: Something went wrong while encrypting the VHD file - the script will now terminate." 211 | Write-Host "You may need to open Disk Management and manually detach the VHD file prior to deleting it." 212 | Write-Host "" 213 | if(-not ($vhdName -and $vhdPath -and $vhdSize -and $vhdLetter -and $vhdCredential -and $confirmscriptcreation)) { pause } 214 | exit 1 215 | } 216 | } 217 | 218 | if(-not ($vhdName -and $vhdPath -and $vhdSize -and $vhdLetter -and $vhdCredential -and $confirmscriptcreation)) { 219 | Write-Host "" 220 | Write-Host "" 221 | } 222 | 223 | # Ask the user if mount/unmount scripts are desired (unless supplied via parameter) 224 | if(-not $confirmscriptcreation) { 225 | $confirmscriptcreation=Read-Host -Prompt "Would you like scripts to mount and unmount this container created and placed on your desktop? (y/n)" 226 | } 227 | if($confirmscriptcreation -eq "y") { 228 | # Create a PowerShell script on the user's Desktop to MOUNT the container 229 | # This part is NASTY - ensure the MOUNT script can self-elevate 230 | "`$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 231 | "`$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal(`$myWindowsID)" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 232 | "`$adminRole=`[System.Security.Principal.WindowsBuiltInRole]::Administrator" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 233 | "if (`$myWindowsPrincipal.IsInRole(`$adminRole))" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 234 | "{" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 235 | "}" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 236 | "else" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 237 | "{" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 238 | "`$newProcess = new-object System.Diagnostics.ProcessStartInfo `"PowerShell`";" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 239 | "`$newProcess.Arguments = `$myInvocation.MyCommand.Definition;" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 240 | "`$newProcess.Verb = `"runas`";" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 241 | "[System.Diagnostics.Process]::Start(`$newProcess);" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 242 | "exit" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 243 | "}" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 244 | "Mount-DiskImage -ImagePath `"$vhdPath\$vhdName.vhd`"" | Out-File -filepath $env:USERPROFILE\Desktop\MOUNT-$vhdName.ps1 -Append 245 | 246 | # Create a PowerShell script on the user's Desktop to UNMOUNT the container 247 | # This part is NASTY - ensure the UNMOUNT script can self-elevate 248 | "`$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 249 | "`$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal(`$myWindowsID)" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 250 | "`$adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 251 | "if (`$myWindowsPrincipal.IsInRole(`$adminRole))" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 252 | "{" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 253 | "}" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 254 | "else" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 255 | "{" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 256 | "`$newProcess = new-object System.Diagnostics.ProcessStartInfo `"PowerShell`";" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 257 | "`$newProcess.Arguments = `$myInvocation.MyCommand.Definition;" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 258 | "`$newProcess.Verb = `"runas`";" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 259 | "[System.Diagnostics.Process]::Start(`$newProcess);" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 260 | "exit" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 261 | "}" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 262 | "Dismount-DiskImage -ImagePath `"$vhdPath\$vhdName.vhd`"" | Out-File -filepath $env:USERPROFILE\Desktop\UNMOUNT-$vhdName.ps1 -Append 263 | } 264 | 265 | # Print a friendly exit message and open the container in explorer unless running non-interactively 266 | if(-not ($vhdName -and $vhdPath -and $vhdSize -and $vhdLetter -and $vhdCredential -and $confirmscriptcreation)) { 267 | Write-Host "" 268 | Write-Host "Your encrypted container was created! If you opted for scripts to mount" 269 | Write-Host "and unmount your container, they can be found on your desktop (MOUNT-$vhdName" 270 | Write-Host "and UNMOUNT-$vhdName). You can use them to mount and unmount the container" 271 | Write-Host "going forward. This script already mounted the container and will open" 272 | Write-Host "it when you press a key." 273 | Write-Host "" 274 | Write-Host "" 275 | pause 276 | explorer.exe $vhdLetter 277 | } 278 | 279 | exit 0 280 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Neil Sabol 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BitLocker encrypted vhd "containers" from PowerShell 2 | This script provides a quick and easy way to create Bit Locker encrypted VHD (virtual hard disk) images using Power Shell. It automates creating an encrypted "container" on a local or network drive to store sensitive information. The script prompts for the location, name, size, and password for the container (vhd file); alternatively, the script can be executed unattended (silently) using parameters. "Shortcut" scripts to mount and unmount the container are generated automatically (if desired) and placed on the user's desktop. 3 | 4 | This project is geared toward end-user workstation usage. The idea is very similar to the functionality previously provided by [TrueCrypt](https://en.wikipedia.org/wiki/TrueCrypt). 5 | 6 | ```diff 7 | -The BAT script is dated and no longer actively maintained or supported. 8 | ``` 9 | 10 | Feel free to fork/improve - collaboration encouraged. If you encounter a problem, please start a [discussion](https://github.com/neil-sabol/bitlocker-encrypted-vhd-from-batch-or-powershell/discussions/new) and include your Windows version and the specific error or behavior. 11 | 12 | 13 | ## Requirements 14 | - A version of Windows Server 2012+ or Windows 8.1+ with Bitlocker support (generally Professional or higher flavor) 15 | 16 | - Administrator access on your Windows machine 17 | 18 | - PowerShell (and an execution policy or execution options that permit unsigned scripts) 19 | 20 | 21 | ## Usage 22 | 1. Download [Create-Encrypted-VHD.ps1](https://raw.githubusercontent.com/neil-sabol/bitlocker-encrypted-vhd-from-batch-or-powershell/master/Create-Encrypted-VHD.ps1) 23 | 24 | 2. Execute the script (bypass SmartScreen filter, accept UAC prompts, etc.) 25 | 26 | 3. In the simplest form, press *Enter* four times, then set a password - a default encrypted VHD container will be created as follows: 27 | 28 | * **VHD File Name:** *username*_private.vhd 29 | * **Location:** C:\Users\ *username* \Desktop 30 | * **Size:** 1024 MB (1 GB) 31 | * **Drive letter:** Y: 32 | 33 | 4. Alternatively: 34 | * Specify options interactively for VHD filename, location (path), size, and drive letter - when prompted, set a password for the encrypted container 35 | 36 | ![Screenshot of Create-Encrypted-VHD.ps1 with manually specifie options](https://blog.neilsabol.site/images/create-encrypted-VHD-screenshot-with-options.png) 37 | 38 | * Run the script non-interactively using the supported parameters (vhdName, vhdPath, vhdSize, vhdLetter, vhdCredential, confirmscriptcreation) 39 | 40 | $myPassword = ("Abcd-1234" | ConvertTo-SecureString -AsPlainText -Force) 41 | .\Create-Encrypted-VHD.ps1 -vhdName "MySecureContainer" -vhdPath C:\Users\Baymax" -vhdSize "256" -vhdLetter "X:" -vhdCredential $myPassword -confirmscriptcreation "y" 42 | 43 | If the *script creation* option is "y", Create-Encrypted-VHD.ps1 generates mount/unmount "shortcut" scripts on the user's Desktop. Once mounted, the user must enter the Bit Locker password to access the container. 44 | 45 | 46 | ## Notes 47 | The script places 2 *.ps1* files directly on the user's desktop for quick mounting and unmounting of the container (if desired). 48 | 49 | The script attempts to use the "best" Bitlocker management mechanism available. The preference is the [BitLocker PowerShell module](https://docs.microsoft.com/en-us/powershell/module/bitlocker/?view=win10-ps), but if that is unavailable, it falls back to the [manage-bde command](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/manage-bde). The advantage of the PowerShell module is the ability to use the *vhdCredential* parameter - *manage-bde* does not support that. 50 | 51 | If the script aborts after the user selects a password (example: the password does not meet the minimum requirements), an orphaned, unencrypted VHD may be left mounted. The script attempts to clean that up but if it fails, you may need to do the following: 52 | 53 | 1. Launch Computer Management (right-click Start menu, select `Computer Management`) 54 | 55 | 2. Open `Disk Management`, under `Storage` 56 | 57 | 3. Right-click the VHD disk and select `Detach` 58 | 59 | 4. Once detached, delete the .vhd file 60 | 61 | 62 | If the script does not execute on your computer, you may need to relax your execution policy. Launch a PowerShell prompt and enter the following: 63 | 64 | ``` 65 | Set-Executionpolicy -Scope CurrentUser -ExecutionPolicy UnRestricted 66 | ``` 67 | 68 | Alternatively, you can run `Create-Encrypted-VHD.ps1` as follows (but note, if you do not permanently adjust the execution policy for the user mounting/unmounting the encrypted container, the shortcuts may not work as intended): 69 | 70 | ``` 71 | powershell.exe -executionpolicy bypass -file Create-Encrypted-VHD.ps1 72 | ``` 73 | 74 | 75 | ## Integration testing 76 | `Create-Encrypted-VHD.Tests.ps1` contains basic PowerShell [Pester](https://github.com/pester/Pester) tests that I use to validate the functionality of this script as changes are introduced. To run the tests, you will need Pester 5+: 77 | 78 | ``` 79 | Install-Module -Name Pester -Force -SkipPublisherCheck 80 | ``` 81 | 82 | Once installed, the Pester tests can be invoked as follows (Note: these steps must be performed in an elevated/administrator PowerShell session): 83 | 84 | ``` 85 | cd 86 | invoke-pester -Output Detailed 87 | ``` 88 | 89 | Note, these tests are not mocked - they actually run the script to create an encrypted VHD container, perform basic operations and destroy it once done. 90 | --------------------------------------------------------------------------------