diff --git a/contrib/EncryptData.ps1 b/contrib/EncryptData.ps1 index 841baa88..5e02bdb5 100644 --- a/contrib/EncryptData.ps1 +++ b/contrib/EncryptData.ps1 @@ -1,278 +1,444 @@ <# .SYNOPSIS -This PowerShell script is used to create a VeraCrypt container with minimal size to hold a copy of the given input file or directory. + Create a VeraCrypt container just large enough for the supplied file or + directory and copy the data into it. .DESCRIPTION -This script takes as input a file path or directory path and a container path. -If the container path is not specified, it defaults to the same as the input path with a ".hc" extension. -The script calculates the minimal size needed to hold the input file or directory in a VeraCrypt container. -It then creates a VeraCrypt container with the specified path and the calculated size using exFAT filesystem. -Finally, the container is mounted, the input file or directory is copied to the container and the container is dismounted. + • Chooses an exFAT cluster size (auto or explicit). + • Calculates the minimum container size using an iterative approach for FAT/Bitmap sizing, plus safety margin. + • Creates, mounts, copies, verifies, and dismounts – all guarded by -WhatIf/-Confirm (SupportsShouldProcess). + • Finds VeraCrypt automatically or takes a -VeraCryptDir override. + • Encryption and hash algorithms are parameters. + • Password can be passed via SecureString prompt or pipeline. + • Enhanced parameterization for safety margins and VeraCrypt overhead. -.PARAMETER inputPath -The file path or directory path to be encrypted in the VeraCrypt container. - -.PARAMETER containerPath -The desired path for the VeraCrypt container. If not specified, it defaults to the same as the input path with a ".hc" extension. +.PARAMETER InputPath File or directory to store in the container. +.PARAMETER ContainerPath Dest *.hc* file. Default: InputPath + '.hc'. +.PARAMETER ClusterSizeKB 4–512 KiB or 'Auto' (default 32). +.PARAMETER VeraCryptDir Optional folder containing VeraCrypt *.exe* files. +.PARAMETER EncryptionAlg Any algorithm VeraCrypt accepts (default AES). +.PARAMETER HashAlg VeraCrypt hash (default SHA512). +.PARAMETER SafetyPercent Safety margin as percentage of calculated size (default 1.0 for small, 0.1 for large). +.PARAMETER VCOverheadMiB VeraCrypt overhead in MiB (default varies by size). +.PARAMETER Force If specified, allows overwriting the output container if it already exists. +.PARAMETER Password Optional SecureString password for automation (prompts if not provided). .EXAMPLE -.\EncryptData.ps1 -inputPath "C:\MyFolder" -containerPath "D:\MyContainer.hc" -.\EncryptData.ps1 "C:\MyFolder" "D:\MyContainer.hc" -.\EncryptData.ps1 "C:\MyFolder" - + .\EncryptData.ps1 -InputPath C:\Data -ContainerPath C:\EncryptedData.hc -ClusterSizeKB Auto .NOTES -Author: Mounir IDRASSI -Email: mounir.idrassi@idrix.fr -Date: 26 July 2024 -License: This script is licensed under the Apache License 2.0 + Author: Mounir IDRASSI + Email: mounir.idrassi@idrix.fr + Date: 30 April 2024 + License: This script is licensed under the Apache License 2.0 #> -# parameters +[CmdletBinding(SupportsShouldProcess)] param( - [Parameter(Mandatory=$true)] - [string]$inputPath, - [string]$containerPath + [Parameter(Mandatory)][string]$InputPath, + [string]$ContainerPath, + [ValidateSet('Auto','4','8','16','32','64','128','256','512')] + [string]$ClusterSizeKB = '32', + [string]$VeraCryptDir, + [string]$EncryptionAlg = 'AES', + [string]$HashAlg = 'SHA512', + # 0 ⇒ use built-in logic; otherwise 0–100 % + [ValidateRange(0.0,100.0)] + [double]$SafetyPercent = 0, + # 0 ⇒ auto. 1-8192 MiB accepted. + [ValidateRange(0,8192)] + [int]$VCOverheadMiB = 0, + [Parameter(ValueFromPipeline = $true)][System.Security.SecureString]$Password, + [switch]$Force ) -function ConvertTo-AbsolutePath { - param ( - [Parameter(Mandatory=$true)] - [string]$Path - ) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Get-AbsolutePath([string]$Path) { if ([System.IO.Path]::IsPathRooted($Path)) { - return $Path + return [System.IO.Path]::GetFullPath($Path) + } else { + $combined = Join-Path -Path (Get-Location) -ChildPath $Path + return [System.IO.Path]::GetFullPath($combined) } - - return Join-Path -Path (Get-Location) -ChildPath $Path } -# Convert input path to fully qualified path -$inputPath = ConvertTo-AbsolutePath -Path $inputPath - -# Check if input path exists -if (-not (Test-Path $inputPath)) { - Write-Host "The specified input path does not exist. Please provide a valid input path." - exit 1 +# Helper creates a temp file, registers it for secure deletion in finally +function New-VcErrFile { + $tmp = [System.IO.Path]::GetTempFileName() + # Pre-allocate 0-byte file; caller overwrites + return $tmp } -$inputPath = (Resolve-Path -Path $inputPath).Path +# Constants for exFAT sizing +$EXFAT_MIN_CLUSTERS = 2 +$EXFAT_PRACTICAL_MIN_CLUSTERS = 65533 +$INITIAL_VBR_SIZE = 32KB +$BACKUP_VBR_SIZE = 32KB +$RAW_UPCASE_BYTES = 128KB -# Set container path if not specified -if ([string]::IsNullOrWhiteSpace($containerPath)) { - $containerPath = "${inputPath}.hc" +#----------------------- Locate VeraCrypt executables -------------------------- +if (-not $VeraCryptDir) { + $candidates = @(Join-Path $env:ProgramFiles 'VeraCrypt') + $VeraCryptDir = $candidates | + Where-Object { Test-Path (Join-Path $_ 'VeraCrypt.exe') } | + Select-Object -First 1 + if (-not $VeraCryptDir) { + $cmd = Get-Command 'VeraCrypt.exe' -ErrorAction SilentlyContinue + if ($cmd) { $VeraCryptDir = Split-Path $cmd.Path } + } + if (-not $VeraCryptDir) { throw 'VeraCrypt executables not found – specify -VeraCryptDir.' } +} + +$VeraCryptExe = Join-Path $VeraCryptDir 'VeraCrypt.exe' +$VeraCryptFormatExe = Join-Path $VeraCryptDir 'VeraCrypt Format.exe' +if (-not (Test-Path $VeraCryptExe) -or -not (Test-Path $VeraCryptFormatExe)) { + throw 'VeraCrypt executables missing.' +} + +#--------------------------- Input / Output Paths ------------------------------ +$InputPath = Get-AbsolutePath $InputPath +if (-not (Test-Path $InputPath)) { throw "InputPath '$InputPath' does not exist." } + +if (-not $ContainerPath) { + $ContainerPath = ($InputPath.TrimEnd('\') + '.hc') } else { - $containerPath = ConvertTo-AbsolutePath -Path $containerPath + $ContainerPath = Get-AbsolutePath $ContainerPath } - -# Check if container path already exists -if (Test-Path $containerPath) { - Write-Host "The specified container path already exists. Please provide a unique path for the new container." - exit 1 -} - -# Full path to VeraCrypt executables -$veracryptPath = "C:\Program Files\VeraCrypt" # replace with your actual path -$veraCryptExe = Join-Path $veracryptPath "VeraCrypt.exe" -$veraCryptFormatExe = Join-Path $veracryptPath "VeraCrypt Format.exe" - -# Constants used to calculate the size of the exFAT filesystem -$InitialVBRSize = 32KB -$BackupVBRSize = 32KB -$InitialFATSize = 128KB -$ClusterSize = 32KB # TODO : make this configurable -$UpCaseTableSize = 128KB # Typical size - -function Get-ExFATSizeRec { - param( - [string]$Path, - [uint64] $TotalSize - ) - - # Constants - $BaseMetadataSize = 32 - $DirectoryEntrySize = 32 - - try { - # Get the item (file or directory) at the provided path - $item = Get-Item -Path $Path -ErrorAction Stop - - # Calculate metadata size - $fileNameLength = $item.Name.Length - $metadataSize = $BaseMetadataSize + ($fileNameLength * 2) - - # Calculate directory entries - if ($fileNameLength -gt 15) { - $numDirEntries = [math]::Ceiling($fileNameLength / 15) + 1 +if (Test-Path $ContainerPath) { + if ($Force) { + Write-Verbose "Container '$ContainerPath' already exists and will be overwritten." + if ($PSCmdlet.ShouldProcess("File '$ContainerPath'", "Remove existing container")) { + Remove-Item $ContainerPath -Force } else { - $numDirEntries = 2 + throw "Operation cancelled by user. Cannot overwrite '$ContainerPath'." } - $dirEntriesSize = $numDirEntries * $DirectoryEntrySize + } else { + throw "Container '$ContainerPath' already exists. Use -Force to overwrite." + } +} - # Add metadata, file size, and directory entries size to $TotalSize - $TotalSize += $metadataSize + $dirEntriesSize +#----------------------------- Cluster Size ------------------------------------- +[UInt32]$ClusterSize = if ($ClusterSizeKB -eq 'Auto') { $null } else { [int]$ClusterSizeKB * 1KB } +#--------------------------- exFAT Size Helpers -------------------------------- +function Get-DirectoryStats { + param([string]$Path) - if ($item.PSIsContainer) { - # It's a directory - $childItems = Get-ChildItem -Path $Path -ErrorAction Stop + $fileLengths = [System.Collections.Generic.List[UInt64]]::new() + $dirLengths = [System.Collections.Generic.List[UInt64]]::new() + [UInt64]$metaSum = 0 - foreach ($childItem in $childItems) { - # Recursively call this function for each child item - $TotalSize = Get-ExFATSizeRec -Path $childItem.FullName -TotalSize $TotalSize + function WalkDir { + param([System.IO.DirectoryInfo]$Dir) + + [UInt64]$thisDirBytes = 0 + + # Use -ErrorAction SilentlyContinue for potentially inaccessible items (like system junctions) + Get-ChildItem -LiteralPath $Dir.FullName -Force -ErrorAction SilentlyContinue | ForEach-Object { + + $nameLen = $_.Name.Length + $nameSlots = [math]::Ceiling($nameLen / 15) # name entries + $dirSlots = 2 + $nameSlots # File + Stream + Names + $entryBytes = $dirSlots * 32 + + $metaSum += $entryBytes + $thisDirBytes += $entryBytes + + if ($_.PSIsContainer) { + WalkDir $_ + } else { + # Handle potential errors reading length (e.g., locked files) + try { + $fileLengths.Add([UInt64]$_.Length) + } catch { + Write-Warning "Could not get length for file: $($_.FullName). Error: $($_.Exception.Message)" + } } - } else { - # It's a file - - # Calculate actual file size and round it up to the nearest multiple of $ClusterSize - $fileSize = $item.Length - $totalFileSize = [math]::Ceiling($fileSize / $ClusterSize) * $ClusterSize - - # Add metadata, file size, and directory entries size to $TotalSize - $TotalSize += $totalFileSize } - } catch { - Write-Error "Error processing item at path ${Path}: $_" + + # At least one 32-byte entry so the directory is not “empty” + if ($thisDirBytes -lt 32) { $thisDirBytes = 32 } + + $dirLengths.Add($thisDirBytes) } - return $TotalSize + $startItem = Get-Item -LiteralPath $Path -Force + if ($startItem -isnot [System.IO.DirectoryInfo]) { + # Handle case where InputPath is a single file + $nameLen = $startItem.Name.Length + $nameSlots = [math]::Ceiling($nameLen / 15) + $dirSlots = 2 + $nameSlots + $entryBytes = $dirSlots * 32 + $metaSum = $entryBytes + $fileLengths.Add([UInt64]$startItem.Length) + $dirLengths.Add(32) # Minimal directory entry size for the root + } else { + WalkDir $startItem + } + + [PSCustomObject]@{ + MetadataSum = $metaSum # pure 32-byte entry bytes (used for info, not size calc) + FileLengths = $fileLengths # payload of regular files + DirLengths = $dirLengths # payload of *directory files* (containing entries) + } } -function Get-ExFATSize { +function Compute-ExFatSize { param( - [string]$Path + [Parameter(Mandatory)][UInt64[]] $FileLengths, + [Parameter(Mandatory)][UInt64[]] $DirLengths, + [Parameter(Mandatory)][UInt32] $Cluster ) - try { - # Initialize total size - $totalSize = $InitialVBRSize + $BackupVBRSize + $InitialFATSize + $UpCaseTableSize + # --- Calculate base size (VBR, UpCase Table, File Payloads, Directory Payloads) --- + # These parts don't depend on the total cluster count directly. + [UInt64]$baseSize = $INITIAL_VBR_SIZE + $BACKUP_VBR_SIZE + $baseSize += [math]::Ceiling($RAW_UPCASE_BYTES / $Cluster) * $Cluster # up-case tbl aligned - # Call the recursive function - $totalSize = Get-ExFATSizeRec -Path $Path -TotalSize $totalSize + foreach ($len in $FileLengths) { $baseSize += [math]::Ceiling($len / $Cluster) * $Cluster } + foreach ($len in $DirLengths ) { $baseSize += [math]::Ceiling($len / $Cluster) * $Cluster } - # Add the root directory to $totalSize - $totalSize += $ClusterSize + # --- Iterative FAT/Bitmap Calculation --- + # The size of FAT and Bitmap depends on the total cluster count, which depends on the total size (including FAT/Bitmap). + # We iterate until the calculated total size stabilizes. - # Calculate the size of the Bitmap Allocation Table - $numClusters = [math]::Ceiling($totalSize / $ClusterSize) - $bitmapSize = [math]::Ceiling($numClusters / 8) - $totalSize += $bitmapSize + [UInt64]$currentTotalSize = $baseSize # Initial guess: size without FAT/Bitmap + [UInt64]$previousTotalSize = 0 + $maxIterations = 10 # Safety break to prevent infinite loops + $iteration = 0 - # Adjust the size of the FAT - $fatSize = $numClusters * 4 - $totalSize += $fatSize - $InitialFATSize - - # Add safety factor to account for potential filesystem overhead - # For smaller datasets (<100MB), we add 1% or 64KB (whichever is larger) - # For larger datasets (>=100MB), we add 0.1% or 1MB (whichever is larger) - # This scaled approach ensures adequate extra space without excessive overhead - $safetyFactor = if ($totalSize -lt 100MB) { - [math]::Max(64KB, $totalSize * 0.01) - } else { - [math]::Max(1MB, $totalSize * 0.001) - } - $totalSize += $safetyFactor + while ($currentTotalSize -ne $previousTotalSize -and $iteration -lt $maxIterations) { + $previousTotalSize = $currentTotalSize + $iteration++ + Write-Verbose "Compute-ExFatSize Iteration '$iteration': Starting size = '$previousTotalSize' bytes" - # Return the minimum disk size needed to store the exFAT filesystem - return $totalSize + # Calculate cluster count based on the size from the *start* of this iteration + $clusterCount = [math]::Ceiling($previousTotalSize / $Cluster) + # Ensure minimum cluster count if needed (exFAT has minimums, though usually covered by VBR etc.) + if ($clusterCount -lt $EXFAT_PRACTICAL_MIN_CLUSTERS) { $clusterCount = $EXFAT_PRACTICAL_MIN_CLUSTERS } # Practical minimum for FAT entries > sector size - } catch { - Write-Error "Error calculating exFAT size for path ${Path}: $_" - return 0 + # Allocation bitmap (1 bit per cluster, aligned to cluster size) + $bitmapBytes = [math]::Ceiling($clusterCount / 8) + $bitmapBytesAligned = [math]::Ceiling($bitmapBytes / $Cluster) * $Cluster + Write-Verbose " Clusters: '$clusterCount', Bitmap Bytes: '$bitmapBytes', Aligned Bitmap: '$bitmapBytesAligned'" + + # FAT (4 bytes per cluster, +2 reserved entries, aligned to cluster size) + $fatBytes = ([UInt64]$clusterCount + 2) * 4 # Use UInt64 to avoid overflow on large volumes + $fatBytesAligned = [math]::Ceiling($fatBytes / $Cluster) * $Cluster + Write-Verbose " FAT Bytes: '$fatBytes', Aligned FAT: '$fatBytesAligned'" + + # Calculate the new total size estimate including FAT and Bitmap + $currentTotalSize = $baseSize + $bitmapBytesAligned + $fatBytesAligned + Write-Verbose " New Estimated Total Size: '$currentTotalSize' bytes" + } + + if ($iteration -ge $maxIterations) { + Write-Warning "FAT/Bitmap size calculation did not converge after '$maxIterations' iterations. Using last calculated size ('$currentTotalSize' bytes). This might indicate an issue or extremely large dataset." + } + + Write-Verbose "Compute-ExFatSize Converged Size: '$currentTotalSize' bytes after '$iteration' iterations." + return [PSCustomObject]@{ + TotalSize = $currentTotalSize + ClusterCount = $clusterCount + IterationHistory = @() } } -# Calculate size of the container -$containerSize = Get-ExFATSize -Path $inputPath +function Get-RecommendedCluster { + param([UInt64]$VolumeBytes) + switch ($VolumeBytes) { + { $_ -le 256MB } { return 4KB } + { $_ -le 32GB } { return 32KB } + { $_ -le 256TB } { return 128KB } + default { return 512KB } + } +} -# Convert to MB and round up to the nearest MB -$containerSize = [math]::Ceiling($containerSize / 1MB) +#----------------------------------------------- Drive the two-pass logic +Write-Host "Calculating required size for '$InputPath'..." +$stats = Get-DirectoryStats -Path $InputPath +Write-Verbose "Stats: $($stats.FileLengths.Count) files, $($stats.DirLengths.Count) directories, Metadata: $($stats.MetadataSum) bytes." -# Add extra space for VeraCrypt headers, reserved areas, and potential alignment requirements -# We use a sliding scale to balance efficiency for small datasets and adequacy for large ones: -# - For very small datasets (<10MB), add 1MB -# - For small to medium datasets (10-100MB), add 2MB -# - For larger datasets (>100MB), add 1% of the total size -# This approach ensures sufficient space across a wide range of dataset sizes -if ($containerSize -lt 10) { - $containerSize += 1 # Add 1 MB for very small datasets -} elseif ($containerSize -lt 100) { - $containerSize += 2 # Add 2 MB for small datasets +if (-not $ClusterSize) { + Write-Verbose "Cluster size set to 'Auto'. Performing first pass calculation with 4KB cluster..." + $firstPassResult = Compute-ExFatSize -FileLengths $stats.FileLengths -DirLengths $stats.DirLengths -Cluster 4KB + $firstPassSize = $firstPassResult.TotalSize + Write-Verbose "First pass estimated size: '$firstPassSize' bytes" + + $ClusterSize = Get-RecommendedCluster -VolumeBytes $firstPassSize + Write-Host "Auto-selected Cluster size: $($ClusterSize / 1KB) KiB based on estimated size." + Write-Verbose "Performing second pass calculation with selected cluster size ($($ClusterSize / 1KB) KiB)..." + $sizeResult = Compute-ExFatSize -FileLengths $stats.FileLengths -DirLengths $stats.DirLengths -Cluster $ClusterSize + $rawSize = $sizeResult.TotalSize } else { - $containerSize += [math]::Ceiling($containerSize * 0.01) # Add 1% for larger datasets + Write-Host "Using specified Cluster size: $($ClusterSize / 1KB) KiB." + Write-Verbose "Performing calculation with specified cluster size..." + $sizeResult = Compute-ExFatSize -FileLengths $stats.FileLengths -DirLengths $stats.DirLengths -Cluster $ClusterSize + $rawSize = $sizeResult.TotalSize } -# Ensure a minimum container size of 2 MB -$containerSize = [math]::Max(2, $containerSize) +#---------------------------- Container Sizing --------------------------------- +$safetyPercentUsed = if ($SafetyPercent -gt 0.0) { $SafetyPercent } else { if ($rawSize -lt 100MB) { 1.0 } else { 0.1 } } +$safety = if ($rawSize -lt 100MB) { [math]::Max(64KB, [math]::Ceiling($rawSize * $safetyPercentUsed / 100)) } + else { [math]::Max(1MB, [math]::Ceiling($rawSize * $safetyPercentUsed / 100)) } +$contBytes = $rawSize + [UInt64]$safety +$contMiB = [int][math]::Ceiling($contBytes / 1MB) +if ($contMiB -lt 2) { $contMiB = 2 } -# Specify encryption algorithm, and hash algorithm -$encryption = "AES" -$hash = "sha512" +$vcOverheadMiBUsed = if ($VCOverheadMiB -gt 0) { $VCOverheadMiB } else { + if ($contMiB -lt 10) { 1 } elseif ($contMiB -lt 100) { 2 } else { [math]::Ceiling($contMiB * 0.01) } +} +$finalContMiB = $contMiB + $vcOverheadMiBUsed -# Create a SecureString password -$password = Read-Host -AsSecureString -Prompt "Enter your password" +Write-Host ("Cluster Size : {0} KiB`nCalculated FS: {1:N0} bytes`nSafety Margin: {2:N0} bytes ({3}%)`nVC Overhead : {4} MiB`nFinal Size : {5} MiB" -f + ($ClusterSize/1KB), $rawSize, $safety, $safetyPercentUsed, $vcOverheadMiBUsed, $finalContMiB) -# Create a PSCredential object -$cred = New-Object System.Management.Automation.PSCredential ("username", $password) +#---- Secure Password Prompt ---- +if (-not $Password) { + $Password = Read-Host -AsSecureString -Prompt "Enter container password" +} +$cred = New-Object System.Management.Automation.PSCredential ("VeraCryptUser", $Password) +$plainPassword = $cred.GetNetworkCredential().Password -Write-Host "Creating VeraCrypt container `"$containerPath`" ..." - -# Create file container using VeraCrypt Format -# TODO: Add a switch to VeraCrypt Format to allow specifying the cluster size to use for the container -$veraCryptFormatArgs = "/create `"$containerPath`" /size `"${containerSize}M`" /password $($cred.GetNetworkCredential().Password) /encryption $encryption /hash $hash /filesystem `"exFAT`" /quick /silent" -Start-Process $veraCryptFormatExe -ArgumentList $veraCryptFormatArgs -NoNewWindow -Wait - -# Check that the container was successfully created -if (-not (Test-Path $containerPath)) { - Write-Host "An error occurred while creating the VeraCrypt container." +if ([string]::IsNullOrWhiteSpace($plainPassword)) { + Write-Host "Error: Password cannot be empty. Please provide a non-empty password." -ForegroundColor Red exit 1 } -# Get a list of currently used drive letters -$driveLetter = Get-Volume | Where-Object { $_.DriveLetter -ne $null } | Select-Object -ExpandProperty DriveLetter +#---- Main Action ---- +$mounted = $false +$driveLetter = $null -# Find the first available drive letter -$unusedDriveLetter = (70..90 | ForEach-Object { [char]$_ } | Where-Object { $_ -notin $driveLetter })[0] +$errFile = New-VcErrFile +$mountFile = New-VcErrFile -# If no available drive letter was found, print an error message and exit the script -if ($null -eq $unusedDriveLetter) { - # delete the file container that was created - Remove-Item -Path $containerPath -Force - Write-Error "No available drive letters found. Please free up a drive letter and try again." - exit 1 +try { + #--- Create Container ---- + if ($PSCmdlet.ShouldProcess("File '$ContainerPath'", "Create VeraCrypt container ($finalContMiB MiB)")) { + $formatArgs = @( + '/create', $ContainerPath, + '/size', "$($finalContMiB)M", + '/password', $plainPassword, + '/encryption', $EncryptionAlg, + '/hash', $HashAlg, + '/filesystem', 'exFAT', + '/quick', '/silent', + '/force' + ) + $maskedArgs = $formatArgs.Clone() + $pwIndex = [array]::IndexOf($maskedArgs, '/password') + if ($pwIndex -ge 0 -and $pwIndex + 1 -lt $maskedArgs.Length) { $maskedArgs[$pwIndex+1] = '********' } + Write-Verbose "Executing: `"$VeraCryptFormatExe`" $($maskedArgs -join ' ')" + + $proc = Start-Process -FilePath $VeraCryptFormatExe -ArgumentList $formatArgs -NoNewWindow -Wait -PassThru -RedirectStandardError $errFile + if ($proc.ExitCode -ne 0) { + $errMsg = if (Test-Path $errFile) { Get-Content $errFile -Raw } else { "No error output captured." } + throw "VeraCrypt Format failed (code $($proc.ExitCode)). Error: $errMsg" + } + Write-Verbose "VeraCrypt Format completed successfully." + } else { + Write-Host "Container creation skipped due to -WhatIf." + exit + } + + #--- Choose Drive Letter ---- + $used = (Get-PSDrive -PSProvider FileSystem).Name + $driveLetter = (67..90 | ForEach-Object {[char]$_}) | + Where-Object { $_ -notin $used } | + Select-Object -First 1 + if (-not $driveLetter) { throw 'No free drive letters found (C-Z).' } + Write-Verbose "Selected drive letter: $driveLetter" + + #--- Mount ---- + if ($PSCmdlet.ShouldProcess("Drive $driveLetter", "Mount VeraCrypt volume '$ContainerPath'")) { + $mountArgs = @( + '/volume', $ContainerPath, + '/letter', $driveLetter, + '/m', 'rm', + '/password', $plainPassword, + '/quit', '/silent' + ) + $maskedArgs = $mountArgs.Clone() + $pwIndex = [array]::IndexOf($maskedArgs, '/password') + if ($pwIndex -ge 0 -and $pwIndex + 1 -lt $maskedArgs.Length) { $maskedArgs[$pwIndex+1] = '********' } + Write-Verbose "Executing: `"$VeraCryptExe`" $($maskedArgs -join ' ')" + + $mountProc = Start-Process -FilePath $VeraCryptExe -ArgumentList $mountArgs -NoNewWindow -Wait -PassThru -RedirectStandardError $mountFile + if ($mountProc.ExitCode -ne 0) { + $errMsg = if (Test-Path $mountFile) { Get-Content $mountFile -Raw } else { "No error output captured." } + throw "VeraCrypt mount failed (code $($mountProc.ExitCode)). Error: $errMsg" + } + + $root = "$($driveLetter):\" + Write-Verbose "Waiting for drive $root to become available..." + $mountTimeoutSeconds = 30 + $mountCheckInterval = 0.5 + $elapsed = 0 + while (-not (Test-Path $root) -and $elapsed -lt $mountTimeoutSeconds) { + Start-Sleep -Seconds $mountCheckInterval + $elapsed += $mountCheckInterval + } + + if (-not (Test-Path $root)) { throw "Drive $driveLetter did not appear within $mountTimeoutSeconds seconds." } + $mounted = $true + Write-Verbose "Drive $root mounted successfully." + } else { + Write-Host "Mounting skipped due to -WhatIf." + exit + } + + #--- Copy Data ---- + $destinationPath = "$($driveLetter):\" + if ($PSCmdlet.ShouldProcess("'$InputPath' -> '$destinationPath'", "Copy input data into container")) { + Write-Verbose "Starting data copy..." + if (Test-Path $InputPath -PathType Container) { + Copy-Item -Path "$InputPath\*" -Destination $destinationPath -Recurse -Force -ErrorAction Stop + Write-Verbose "Copied directory contents recursively." + } else { + Copy-Item -Path $InputPath -Destination $destinationPath -Force -ErrorAction Stop + Write-Verbose "Copied single file." + } + try { + $driveInfo = Get-PSDrive $driveLetter -ErrorAction Stop + $freeSpace = $driveInfo.Free + Write-Verbose "Free space after copy: $freeSpace bytes." + if ($freeSpace -lt 0) { + Write-Warning 'Reported free space is negative. This might indicate an issue, but the copy might still be okay.' + } elseif ($freeSpace -lt 1MB) { + Write-Warning "Very low free space remaining ($freeSpace bytes). The container might be too small if data changes slightly." + } + } catch { + Write-Warning "Could not verify free space on drive $driveLetter after copy. Error: $($_.Exception.Message)" + } + Write-Host "Data copy completed." + } else { + Write-Host "Data copy skipped due to -WhatIf." + } +} catch { + Write-Error "An error occurred: $($_.Exception.Message)" +} finally { + if (Test-Path variable:plainPassword) { Clear-Variable plainPassword -ErrorAction SilentlyContinue } + if ($mounted) { + if ($PSCmdlet.ShouldProcess("Drive $driveLetter", "Dismount VeraCrypt volume")) { + Write-Verbose "Dismounting drive $driveLetter..." + $dismountArgs = @('/dismount', $driveLetter, '/force', '/quit', '/silent') + Start-Process -FilePath $VeraCryptExe -ArgumentList $dismountArgs -NoNewWindow -Wait -ErrorAction SilentlyContinue + Write-Verbose "Dismount command issued." + } else { + Write-Host "Dismount skipped due to -WhatIf." + } + } + foreach($f in @($errFile,$mountFile) | Where-Object { $_ }) { + if(Test-Path $f){ + try { Set-Content -Path $f -Value ($null) -Encoding Byte -Force } catch{} + Remove-Item $f -Force -ErrorAction SilentlyContinue + } + } } -Write-Host "Mounting the newly created VeraCrypt container..." - -# Mount the container to the chosen drive letter as removable media -Start-Process $veraCryptExe -ArgumentList "/volume `"$containerPath`" /letter $unusedDriveLetter /m rm /password $($cred.GetNetworkCredential().Password) /quit" -NoNewWindow -Wait - -# Check if the volume has been mounted successfully -$mountedDriveRoot = "${unusedDriveLetter}:\" -if (-not (Test-Path -Path $mountedDriveRoot)) { - # Volume mount failed - Write-Error "Failed to mount the volume. Please make sure VeraCrypt.exe is working correctly." - # delete the file container that was created - Remove-Item -Path $containerPath -Force - exit 1 -} - -Write-Host "Copying data to the mounted VeraCrypt container..." - -# Copy the file or directory to the mounted drive -if (Test-Path -Path $inputPath -PathType Container) { - # For directories - Copy-Item -Path $inputPath -Destination "$($unusedDriveLetter):\" -Recurse +if ($PSCmdlet.ShouldProcess("File '$ContainerPath'", "Create VeraCrypt container ($finalContMiB MiB)")) { + Write-Host ("Script finished. VeraCrypt container created at '{0}' ({1} MiB)." -f $ContainerPath, $finalContMiB) } else { - # For files - Copy-Item -Path $inputPath -Destination "$($unusedDriveLetter):\" + Write-Host ("Script finished (simulation mode).") } - -Write-Host "Copying completed. Dismounting the VeraCrypt container..." - -# give some time for the file system to flush the data to the disk -Start-Sleep -Seconds 5 - -# Dismount the volume -Start-Process $veraCryptExe -ArgumentList "/dismount $unusedDriveLetter /quit" -NoNewWindow -Wait - -Write-Host "VeraCrypt container created successfully."