Public/Unprotect-VellumPdfDocument.ps1
|
function Unprotect-VellumPdfDocument { <# .SYNOPSIS Writes a decrypted copy of a password-protected PDF. .DESCRIPTION EXPERIMENTAL - built on VellumPdf.Reader, which is still a Preview package. Opens an encrypted PDF with its password and writes an unencrypted copy to -DestinationPath; the original is never modified. Use it to remove the password from files you are entitled to open, for example before handing them to a tool that cannot read encrypted PDFs. A digitally signed PDF cannot be decrypted without invalidating its signatures (they cover the encrypted bytes). The cmdlet refuses by default; pass -AllowInvalidatingSignatures to accept that. One file at a time: a path that matches several files is refused (counted before any file is read). A failure for the file itself (not found, unreadable, not encrypted, wrong or missing password, malformed) is a non-terminating error with the file as TargetObject, so it follows -ErrorAction (use -ErrorAction Stop to throw). Invalid parameters (a destination equal to the source, an existing destination without -Force) terminate. .PARAMETER Path The encrypted PDF. Accepts wildcards, but the path must match exactly one file. Piped strings bind here, and so do piped objects with a Path property; piped FileInfo objects bind to -LiteralPath through their PSPath. .PARAMETER LiteralPath The encrypted PDF, taken literally. Aliases PSPath and FullName, so objects with either property bind by name. .PARAMETER DestinationPath The decrypted copy to write. Must differ from the source. .PARAMETER Password The user or owner password (a SecureString). .PARAMETER AllowInvalidatingSignatures Decrypt even though existing digital signatures become invalid. .PARAMETER AllowReconstruction Try to recover a damaged file (missing or broken cross-reference table) by scanning for objects before decrypting it. .PARAMETER Force Overwrite an existing destination file. .PARAMETER MaxDecodedStreamBytes Refuse to decode any stream larger than this many bytes (1 MiB to 512 MiB, the default ceiling). Lower it for untrusted files. .PARAMETER MaxDiagnostics Stop collecting reader diagnostics after this many (1 to 1000). .PARAMETER MaxFormXObjectDepth Deepest nesting of form XObjects followed while reading content (1 to 32). .PARAMETER ReconstructionBudgetMultiplier How many times the file size reconstruction may scan (1 to 8); used with -AllowReconstruction. .EXAMPLE Unprotect-VellumPdfDocument ./locked.pdf -DestinationPath ./open.pdf -Password (Read-Host -AsSecureString) .OUTPUTS System.IO.FileInfo for the decrypted copy. #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Path')] [OutputType([System.IO.FileInfo])] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Path')] [string]$Path, [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'LiteralPath')] [Alias('PSPath', 'FullName')] [string]$LiteralPath, [Parameter(Mandatory)] [string]$DestinationPath, [securestring]$Password, [switch]$AllowInvalidatingSignatures, [switch]$Force, [switch]$AllowReconstruction, # Resource limits for hostile or huge files. They can only tighten the reader's # built-in ceilings (512 MiB decoded, 1000 diagnostics, 32 form levels, budget 8). [ValidateRange(1MB, 512MB)] [long]$MaxDecodedStreamBytes, [ValidateRange(1, 1000)] [int]$MaxDiagnostics, [ValidateRange(1, 32)] [int]$MaxFormXObjectDepth, [ValidateRange(1, 8)] [int]$ReconstructionBudgetMultiplier ) process { try { $limits = @{} foreach ($n in 'MaxDecodedStreamBytes', 'MaxDiagnostics', 'MaxFormXObjectDepth', 'ReconstructionBudgetMultiplier') { if ($PSBoundParameters.ContainsKey($n)) { $limits[$n] = $PSBoundParameters[$n] } } # Resolve only; no file is read until the input count and the destination are checked. $sources = @(Get-VellumPdfInput -Path $Path -LiteralPath $LiteralPath -CommandName 'Unprotect-VellumPdfDocument') if ($sources.Count -ne 1) { throw "Unprotect-VellumPdfDocument: the path matches $($sources.Count) files; decrypt one file at a time (each needs its own -DestinationPath)." } $source = $sources[0] $target = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath) if (-not $source.Error) { if ([string]::Equals($target, $source.Name, [System.StringComparison]::OrdinalIgnoreCase)) { throw 'Unprotect-VellumPdfDocument: -DestinationPath must be a different file than the source.' } if ((Test-Path -LiteralPath $target) -and -not $Force) { throw "Unprotect-VellumPdfDocument: '$target' exists; use -Force to overwrite." } } $plain = if ($Password) { [System.Net.NetworkCredential]::new('', $Password).Password } try { Read-VellumPdfInput -Source $source # a missing or unreadable file is a non-terminating error for that file $reader = Open-VellumPdfReader -Limit $limits -Bytes $source.Data -Password $plain -AllowReconstruction:$AllowReconstruction ` -CommandName 'Unprotect-VellumPdfDocument' -Source $source.Name try { if (-not $reader.Encryption) { throw "Unprotect-VellumPdfDocument: '$($source.Name)' is not encrypted; nothing to remove." } if ($PSCmdlet.ShouldProcess($target, "Write decrypted copy of '$($source.Name)'")) { $options = [VellumPdf.Reader.PdfSaveDecryptedOptions]::new() if ($AllowInvalidatingSignatures) { $options.AllowInvalidatingSignatures = $true } $temp = "$target.$([guid]::NewGuid().ToString('N')).tmp" try { $stream = [System.IO.File]::Create($temp) try { $reader.SaveDecrypted($stream, $options) } catch { throw "Unprotect-VellumPdfDocument: could not decrypt '$($source.Name)': $($_.Exception.Message)" } finally { $stream.Dispose() } [System.IO.File]::Move($temp, $target, $true) } finally { if (Test-Path -LiteralPath $temp) { Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue } } Get-Item -LiteralPath $target } } finally { $reader.Dispose() } } catch { Write-VellumPdfFileError -Cmdlet $PSCmdlet -ErrorRecord (ConvertTo-VellumPdfFileError -ErrorRecord $_ -Target $source.Name -CommandName 'Unprotect-VellumPdfDocument') -ErrorActionPreferenceValue $ErrorActionPreference } finally { $source.Data = $null } } catch { $PSCmdlet.ThrowTerminatingError((Resolve-VellumPdfErrorRecord -ErrorRecord $_ -CommandName 'Unprotect-VellumPdfDocument')) } } } |