1
0
mirror of https://github.com/veracrypt/VeraCrypt.git synced 2025-11-11 02:58:02 -06:00

Windows: Refactor EncryptData.ps1 script. Parameterize, add cluster size auto, robust exFAT sizing, and safety features

- Added parameters for cluster size (auto/manual), encryption/hash, safety margin, VeraCrypt overhead, and VeraCrypt path override
- Switched to iterative exFAT size calculation for accurate FAT/bitmap sizing
- Auto-selects optimal cluster size based on data size
- Supports -WhatIf/-Confirm (SupportsShouldProcess) for safe operation
- Allows password via pipeline or prompt; improved error handling and cleanup
- Enhanced output, free space checks, and force-overwrite option
- Improved code structure, comments, and user feedback
This commit is contained in:
Mounir IDRASSI
2025-04-30 18:38:05 +09:00
parent 43ea5108e5
commit 798985bf25

View File

@@ -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 4512 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
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
)
function ConvertTo-AbsolutePath {
param (
[Parameter(Mandatory=$true)]
[string]$Path
[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 0100 %
[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
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Get-AbsolutePath([string]$Path) {
if ([System.IO.Path]::IsPathRooted($Path)) {
return $Path
}
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
}
$inputPath = (Resolve-Path -Path $inputPath).Path
# Set container path if not specified
if ([string]::IsNullOrWhiteSpace($containerPath)) {
$containerPath = "${inputPath}.hc"
return [System.IO.Path]::GetFullPath($Path)
} else {
$containerPath = ConvertTo-AbsolutePath -Path $containerPath
$combined = Join-Path -Path (Get-Location) -ChildPath $Path
return [System.IO.Path]::GetFullPath($combined)
}
}
# 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
# 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
}
# 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 for exFAT sizing
$EXFAT_MIN_CLUSTERS = 2
$EXFAT_PRACTICAL_MIN_CLUSTERS = 65533
$INITIAL_VBR_SIZE = 32KB
$BACKUP_VBR_SIZE = 32KB
$RAW_UPCASE_BYTES = 128KB
# 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
#----------------------- 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.' }
}
function Get-ExFATSizeRec {
$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 = Get-AbsolutePath $ContainerPath
}
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 {
throw "Operation cancelled by user. Cannot overwrite '$ContainerPath'."
}
} else {
throw "Container '$ContainerPath' already exists. Use -Force to overwrite."
}
}
#----------------------------- Cluster Size -------------------------------------
[UInt32]$ClusterSize = if ($ClusterSizeKB -eq 'Auto') { $null } else { [int]$ClusterSizeKB * 1KB }
#--------------------------- exFAT Size Helpers --------------------------------
function Get-DirectoryStats {
param([string]$Path)
$fileLengths = [System.Collections.Generic.List[UInt64]]::new()
$dirLengths = [System.Collections.Generic.List[UInt64]]::new()
[UInt64]$metaSum = 0
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)"
}
}
}
# At least one 32-byte entry so the directory is not “empty”
if ($thisDirBytes -lt 32) { $thisDirBytes = 32 }
$dirLengths.Add($thisDirBytes)
}
$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 Compute-ExFatSize {
param(
[string]$Path,
[uint64] $TotalSize
[Parameter(Mandatory)][UInt64[]] $FileLengths,
[Parameter(Mandatory)][UInt64[]] $DirLengths,
[Parameter(Mandatory)][UInt32] $Cluster
)
# Constants
$BaseMetadataSize = 32
$DirectoryEntrySize = 32
# --- 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
foreach ($len in $FileLengths) { $baseSize += [math]::Ceiling($len / $Cluster) * $Cluster }
foreach ($len in $DirLengths ) { $baseSize += [math]::Ceiling($len / $Cluster) * $Cluster }
# --- 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.
[UInt64]$currentTotalSize = $baseSize # Initial guess: size without FAT/Bitmap
[UInt64]$previousTotalSize = 0
$maxIterations = 10 # Safety break to prevent infinite loops
$iteration = 0
while ($currentTotalSize -ne $previousTotalSize -and $iteration -lt $maxIterations) {
$previousTotalSize = $currentTotalSize
$iteration++
Write-Verbose "Compute-ExFatSize Iteration '$iteration': Starting size = '$previousTotalSize' bytes"
# 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
# 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 = @()
}
}
function Get-RecommendedCluster {
param([UInt64]$VolumeBytes)
switch ($VolumeBytes) {
{ $_ -le 256MB } { return 4KB }
{ $_ -le 32GB } { return 32KB }
{ $_ -le 256TB } { return 128KB }
default { return 512KB }
}
}
#----------------------------------------------- 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."
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 {
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
}
#---------------------------- 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 }
$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
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)
#---- 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
if ([string]::IsNullOrWhiteSpace($plainPassword)) {
Write-Host "Error: Password cannot be empty. Please provide a non-empty password." -ForegroundColor Red
exit 1
}
#---- Main Action ----
$mounted = $false
$driveLetter = $null
$errFile = New-VcErrFile
$mountFile = New-VcErrFile
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
} else {
$numDirEntries = 2
}
$dirEntriesSize = $numDirEntries * $DirectoryEntrySize
# Add metadata, file size, and directory entries size to $TotalSize
$TotalSize += $metadataSize + $dirEntriesSize
if ($item.PSIsContainer) {
# It's a directory
$childItems = Get-ChildItem -Path $Path -ErrorAction Stop
foreach ($childItem in $childItems) {
# Recursively call this function for each child item
$TotalSize = Get-ExFATSizeRec -Path $childItem.FullName -TotalSize $TotalSize
}
} 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}: $_"
}
return $TotalSize
}
function Get-ExFATSize {
param(
[string]$Path
#--- 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 {
# Initialize total size
$totalSize = $InitialVBRSize + $BackupVBRSize + $InitialFATSize + $UpCaseTableSize
# Call the recursive function
$totalSize = Get-ExFATSizeRec -Path $Path -TotalSize $totalSize
# Add the root directory to $totalSize
$totalSize += $ClusterSize
# Calculate the size of the Bitmap Allocation Table
$numClusters = [math]::Ceiling($totalSize / $ClusterSize)
$bitmapSize = [math]::Ceiling($numClusters / 8)
$totalSize += $bitmapSize
# 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)
$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."
}
$totalSize += $safetyFactor
# Return the minimum disk size needed to store the exFAT filesystem
return $totalSize
} catch {
Write-Error "Error calculating exFAT size for path ${Path}: $_"
return 0
Write-Warning "Could not verify free space on drive $driveLetter after copy. Error: $($_.Exception.Message)"
}
}
# Calculate size of the container
$containerSize = Get-ExFATSize -Path $inputPath
# Convert to MB and round up to the nearest MB
$containerSize = [math]::Ceiling($containerSize / 1MB)
# 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
Write-Host "Data copy completed."
} else {
$containerSize += [math]::Ceiling($containerSize * 0.01) # Add 1% for larger datasets
Write-Host "Data copy skipped due to -WhatIf."
}
# Ensure a minimum container size of 2 MB
$containerSize = [math]::Max(2, $containerSize)
# Specify encryption algorithm, and hash algorithm
$encryption = "AES"
$hash = "sha512"
# Create a SecureString password
$password = Read-Host -AsSecureString -Prompt "Enter your password"
# Create a PSCredential object
$cred = New-Object System.Management.Automation.PSCredential ("username", $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."
exit 1
}
# Get a list of currently used drive letters
$driveLetter = Get-Volume | Where-Object { $_.DriveLetter -ne $null } | Select-Object -ExpandProperty DriveLetter
# Find the first available drive letter
$unusedDriveLetter = (70..90 | ForEach-Object { [char]$_ } | Where-Object { $_ -notin $driveLetter })[0]
# 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
}
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
} 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 {
# For files
Copy-Item -Path $inputPath -Destination "$($unusedDriveLetter):\"
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 "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."
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 {
Write-Host ("Script finished (simulation mode).")
}