Private/ConvertFrom-EncodedWord.ps1
|
# RFC 2047 encoded-words and Unicode direction controls. function ConvertFrom-QuotedPrintableWord { [CmdletBinding()] [OutputType([byte[]])] param([Parameter(Mandatory)][AllowEmptyString()][string]$Text) $out = New-Object System.Collections.Generic.List[byte] for ($i = 0; $i -lt $Text.Length; $i++) { $ch = $Text[$i] if ($ch -eq '_') { $out.Add(0x20); continue } if ($ch -eq '=' -and ($i + 2) -lt $Text.Length -and $Text.Substring($i + 1, 2) -match '^[0-9a-fA-F]{2}$') { $out.Add([Convert]::ToByte($Text.Substring($i + 1, 2), 16)) $i += 2 continue } $out.Add([byte]([int][char]$ch -band 0xff)) } return ,$out.ToArray() } function ConvertFrom-EncodedWord { <# .SYNOPSIS Decodes RFC 2047 encoded-words (B and Q) in a header value. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()][AllowEmptyString()][string]$Text) if ([string]::IsNullOrEmpty($Text)) { return $Text } # Adjacent encoded-words: the whitespace between them is dropped (RFC 2047 section 6.2). $joined = [regex]::Replace($Text, '(=\?[^?\s]+\?[BbQq]\?[^?]*\?=)\s+(?==\?)', '$1') $evaluator = [System.Text.RegularExpressions.MatchEvaluator] { param($m) # RFC 2231 allows a language suffix on the charset ("utf-8*de"). $charset = [regex]::Replace($m.Groups[1].Value, '\*.*$', '') $encodingKind = $m.Groups[2].Value.ToUpperInvariant() $data = $m.Groups[3].Value try { if ($encodingKind -eq 'B') { $bytes = [Convert]::FromBase64String(([regex]::Replace($data, '\s+', ''))) } else { $bytes = ConvertFrom-QuotedPrintableWord -Text $data } $encoding = [System.Text.Encoding]::GetEncoding($charset) return $encoding.GetString($bytes) } catch { return $m.Value } } return [regex]::Replace($joined, '=\?([^?\s]+)\?([BbQq])\?([^?]*)\?=', $evaluator) } # Bidi controls: LRM/RLM, ALM, the embedded overrides U+202A to U+202E and # the isolates U+2066 to U+2069. "Invoice <U+202E>fdp.exe" otherwise reads # as "Invoice exe.pdf": exactly the deception this tool exposes. # Written as escapes on purpose: the characters themselves would reorder this # source file when read (Trojan Source). $script:BidiPattern = '[\u200E\u200F\u061C\u202A-\u202E\u2066-\u2069]' function Test-BidiControl { [CmdletBinding()] [OutputType([bool])] param([AllowNull()][AllowEmptyString()][string]$Text) if ([string]::IsNullOrEmpty($Text)) { return $false } return [regex]::IsMatch($Text, $script:BidiPattern) } function Show-ControlCharacter { <# .SYNOPSIS Replaces direction control characters with their code point notation. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()][AllowEmptyString()][string]$Text) if ([string]::IsNullOrEmpty($Text)) { return $Text } if (-not [regex]::IsMatch($Text, $script:BidiPattern)) { return $Text } $evaluator = [System.Text.RegularExpressions.MatchEvaluator] { param($m) return ('<U+{0:X4}>' -f [int][char]$m.Value[0]) } return [regex]::Replace($Text, $script:BidiPattern, $evaluator) } |