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:
@@ -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
|
||||
while ($currentTotalSize -ne $previousTotalSize -and $iteration -lt $maxIterations) {
|
||||
$previousTotalSize = $currentTotalSize
|
||||
$iteration++
|
||||
Write-Verbose "Compute-ExFatSize Iteration '$iteration': Starting size = '$previousTotalSize' bytes"
|
||||
|
||||
# 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
|
||||
# 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
|
||||
|
||||
# Return the minimum disk size needed to store the exFAT filesystem
|
||||
return $totalSize
|
||||
# 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'"
|
||||
|
||||
} catch {
|
||||
Write-Error "Error calculating exFAT size for path ${Path}: $_"
|
||||
return 0
|
||||
# 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."
|
||||
|
||||
Reference in New Issue
Block a user