Public/New-HDTBitLockerService.ps1
|
function New-HDTBitLockerService { <# .SYNOPSIS The real IBitLockerService: a thin adapter over the BitLocker module. .DESCRIPTION DESIGN 10.3's encryption service. Rule 5 forbids engine logic from calling Enable-BitLocker directly, so the EnableBitLocker step receives this object and can be handed New-HDTFakeBitLockerService in a test. IT IS BRANCH-FREE, WHICH IS WHY IT IS NOT UNIT TESTED (rule 1's adapter exception) - AND THERE IS NO SAFE WAY TO TEST IT ANYWAY. Three of its four methods change the encryption state of a physical disk, and the disk in front of this code during development is the developer's own. The contract file runs the real row for SHAPE ONLY and never calls a method on it; the fake carries the behaviour. Every decision - which protector, whether escrow succeeded, whether to encrypt at all - is made by the step. This projects four cmdlets and nothing else. THE PROTECTOR TYPE IS A STRING THE STEP CHOOSES, mapping to the switch Add-BitLockerKeyProtector wants. The mapping is here rather than in the step because it is the adapter's job to know the cmdlet's shape, and the step's job to know DESIGN 10.3's vocabulary. .PARAMETER Journal The shared cross-service operation journal. When supplied, every call is appended to it as well as to $Operations. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject carrying GetVolume, AddProtector, BackupProtector and Enable, plus Operations, GetOperationName and ServiceName. .EXAMPLE $bitlocker = New-HDTBitLockerService $bitlocker.GetVolume('C:') #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds the service object; encrypting is done by the caller through it, and the step that calls it refuses an ambiguous target.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The TPM PIN is authored in sequence.yaml as readable text, exactly as HDTAdminPassword is (DESIGN 4.5.2): a value WinPE must use with no human present cannot be protected by a key that also ships in the boot image. Add-BitLockerKeyProtector requires a SecureString, so the conversion happens here, at the last possible moment, rather than the interface pretending the value was ever secret. The real control is the one DESIGN 4.5.2 names - treat the workspace and the boot media as credentials.')] [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter()] [AllowNull()] [System.Collections.ArrayList] $Journal = $null ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $service = [pscustomobject] @{ Operations = [System.Collections.ArrayList]::new() Journal = $Journal ServiceName = 'BitLockerService' } $service | Add-Member -MemberType ScriptMethod -Name Record -Value { param([string] $Operation, [object[]] $Argument) [void] $this.Operations.Add([pscustomobject] @{ Sequence = $this.Operations.Count + 1 Operation = $Operation Arguments = $Argument }) if ($null -ne $this.Journal) { [void] $this.Journal.Add([pscustomobject] @{ Sequence = $this.Journal.Count + 1 Service = $this.ServiceName Operation = $Operation Arguments = $Argument }) } } $service | Add-Member -MemberType ScriptMethod -Name GetOperationName -Value { return [string[]] @($this.Operations | ForEach-Object { $_.Operation }) } $service | Add-Member -MemberType ScriptMethod -Name GetVolume -Value { param([string] $Drive) $this.Record('GetVolume', @($Drive)) $volume = Get-BitLockerVolume -MountPoint $Drive return [pscustomobject] @{ VolumeStatus = [string] $volume.VolumeStatus ProtectionStatus = [string] $volume.ProtectionStatus KeyProtector = [object[]] @($volume.KeyProtector | ForEach-Object { [pscustomobject] @{ KeyProtectorId = [string] $_.KeyProtectorId KeyProtectorType = [string] $_.KeyProtectorType RecoveryPassword = [string] $_.RecoveryPassword } }) } } $service | Add-Member -MemberType ScriptMethod -Name AddProtector -Value { param([string] $Drive, [string] $Type, [string] $Argument) $this.Record('AddProtector', @($Drive, $Type, $Argument)) $parameter = @{ MountPoint = $Drive } # The vocabulary is DESIGN 10.3's; the switch names are the cmdlet's. if ($Type -eq 'RecoveryPassword') { $parameter['RecoveryPasswordProtector'] = $true } if ($Type -eq 'Tpm') { $parameter['TpmProtector'] = $true } if ($Type -eq 'TpmPin') { $parameter['TpmAndPinProtector'] = $true $parameter['Pin'] = ConvertTo-SecureString -String $Argument -AsPlainText -Force } if ($Type -eq 'TpmStartupKey') { $parameter['TpmAndStartupKeyProtector'] = $true $parameter['StartupKeyPath'] = $Argument } $volume = Add-BitLockerKeyProtector @parameter $added = @($volume.KeyProtector)[-1] return [pscustomobject] @{ KeyProtectorId = [string] $added.KeyProtectorId KeyProtectorType = [string] $added.KeyProtectorType RecoveryPassword = [string] $added.RecoveryPassword } } $service | Add-Member -MemberType ScriptMethod -Name BackupProtector -Value { param([string] $Drive, [string] $KeyProtectorId, [string] $Target) $this.Record('BackupProtector', @($Drive, $KeyProtectorId, $Target)) if ($Target -eq 'ad') { Backup-BitLockerKeyProtector -MountPoint $Drive -KeyProtectorId $KeyProtectorId | Out-Null } if ($Target -eq 'entra') { BackupToAAD-BitLockerKeyProtector -MountPoint $Drive -KeyProtectorId $KeyProtectorId | Out-Null } } $service | Add-Member -MemberType ScriptMethod -Name Enable -Value { param([string] $Drive, [string] $Method, [bool] $UsedSpaceOnly) $this.Record('Enable', @($Drive, $Method, $UsedSpaceOnly)) $parameter = @{ MountPoint = $Drive EncryptionMethod = $Method SkipHardwareTest = $true } if ($UsedSpaceOnly) { $parameter['UsedSpaceOnly'] = $true } Enable-BitLocker @parameter | Out-Null } return $service } |