Private/Armor.psm1
|
#!/usr/bin/env pwsh using namespace System using namespace System.IO using namespace System.Text using namespace System.Security using namespace System.Security.Cryptography using namespace System.Runtime.InteropServices using namespace System.Collections.Generic using module ./Utilities.psm1 using module ./Enums.psm1 using module ./Crc24.psm1 class ArmorDecodeResult { [byte[]] $Data [ArmorType] $Type [Dictionary[string, string]] $Headers } class Armor : CryptobaseUtils { static [string[]] $LineSeparators = @("`r`n", "`n", "`r") static [string] $ARMOR_BEGIN = "-----BEGIN PGP " static [string] $ARMOR_END = "-----END PGP " static [string] $ARMOR_SUFFIX = "-----" static [int] $MAX_LINE_LENGTH = 76 static [string] Encode([byte[]]$data, [ArmorType]$armorType, [Dictionary[string, string]]$headers) { $sb = [StringBuilder]::new() $typeName = [Armor]::GetArmorTypeName($armorType) [void]$sb.Append([Armor]::ARMOR_BEGIN).Append($typeName).AppendLine([Armor]::ARMOR_SUFFIX) if ($null -ne $headers -and $headers.Count -gt 0) { foreach ($kvp in $headers.GetEnumerator()) { [void]$sb.Append($kvp.Key).Append(": ").AppendLine($kvp.Value) } } [void]$sb.AppendLine() $base64 = [Convert]::ToBase64String($data) for ($i = 0; $i -lt $base64.Length; $i += [Armor]::MAX_LINE_LENGTH) { $lineLength = [Math]::Min([Armor]::MAX_LINE_LENGTH, $base64.Length - $i) [void]$sb.AppendLine($base64.Substring($i, $lineLength)) } $crcBytes = [Crc24]::ComputeToBytes($data) [void]$sb.Append('=').AppendLine([Convert]::ToBase64String($crcBytes)) [void]$sb.Append([Armor]::ARMOR_END).Append($typeName).Append([Armor]::ARMOR_SUFFIX) return $sb.ToString() } static [ArmorDecodeResult] Decode([string]$armoredText) { if ([string]::IsNullOrWhiteSpace($armoredText)) { throw [ArgumentNullException]::new("armoredText") } $lines = $armoredText.Split([Armor]::LineSeparators, [StringSplitOptions]::None) $lineIndex = 0 while ($lineIndex -lt $lines.Length -and [string]::IsNullOrWhiteSpace($lines[$lineIndex])) { $lineIndex++ } if ($lineIndex -ge $lines.Length) { throw [FormatException]::new("No armor header found.") } $headerLine = $lines[$lineIndex].Trim() if (!$headerLine.StartsWith([Armor]::ARMOR_BEGIN) -or !$headerLine.EndsWith([Armor]::ARMOR_SUFFIX)) { throw [FormatException]::new("Invalid armor header: $headerLine") } $typeName = $headerLine.Substring([Armor]::ARMOR_BEGIN.Length, $headerLine.Length - [Armor]::ARMOR_BEGIN.Length - [Armor]::ARMOR_SUFFIX.Length) $armorType = [Armor]::ParseArmorType($typeName) $lineIndex++ $headers = [Dictionary[string, string]]::new() while ($lineIndex -lt $lines.Length) { $line = $lines[$lineIndex].Trim() if ([string]::IsNullOrEmpty($line)) { $lineIndex++ break } $colonIndex = $line.IndexOf(':') if ($colonIndex -gt 0) { $key = $line.Substring(0, $colonIndex).Trim() $value = $line.Substring($colonIndex + 1).Trim() $headers[$key] = $value } $lineIndex++ } $base64Builder = [StringBuilder]::new() [byte[]]$crcBytes = $null while ($lineIndex -lt $lines.Length) { $line = $lines[$lineIndex].Trim() if ($line.StartsWith([Armor]::ARMOR_END)) { break } if ($line.StartsWith('=') -and $line.Length -eq 5) { $crcBytes = [Convert]::FromBase64String($line.Substring(1)) $lineIndex++ continue } if (![string]::IsNullOrWhiteSpace($line)) { [void]$base64Builder.Append($line) } $lineIndex++ } $data = [Convert]::FromBase64String($base64Builder.ToString()) if ($null -ne $crcBytes) { if (![Crc24]::Verify($crcBytes, $data)) { throw [FormatException]::new("CRC24 checksum verification failed.") } } $result = [ArmorDecodeResult]::new() $result.Data = $data $result.Type = $armorType $result.Headers = $headers return $result } static hidden [string] GetArmorTypeName([ArmorType]$type) { [ValidateNotNull()][ArmorType]$type = $type $n = [string]::Empty $n = switch ($type) { ([ArmorType]::Message) { "MESSAGE"; break } ([ArmorType]::PublicKey) { "PUBLIC KEY BLOCK"; break } ([ArmorType]::PrivateKey) { "PRIVATE KEY BLOCK"; break } ([ArmorType]::Signature) { "SIGNATURE"; break } ([ArmorType]::SignedMessage) { "SIGNED MESSAGE"; break } default { throw [ArgumentException]::new("Unknown armor type: $type") } } return $n } static hidden [ArmorType] ParseArmorType([string]$typeName) { [ValidateNotNullOrWhiteSpace()][string]$typeName = $typeName $type = [ArmorType]::Message $type = switch ($typeName.ToUpperInvariant()) { "MESSAGE" { [ArmorType]::Message ; break } "PUBLIC KEY BLOCK" { [ArmorType]::PublicKey; break } "PRIVATE KEY BLOCK" { [ArmorType]::PrivateKey; break } "SECRET KEY BLOCK" { [ArmorType]::PrivateKey; break } "SIGNATURE" { [ArmorType]::Signature; break } "SIGNED MESSAGE" { [ArmorType]::SignedMessage; break } default { throw [FormatException]::new("Unknown armor type: $typeName") } } return $type } } |