Private/OpenPgp.psm1
|
#!/usr/bin/env pwsh using namespace System using namespace System.Collections.Generic using namespace System.Text using namespace System.Numerics using module ./Enums.psm1 using module ./Utilities.psm1 using module ./Armor.psm1 using module ./S2K.psm1 #region Mpi class Mpi { static [BigInteger] Read([byte[]]$data, [ref]$offset) { if ($data.Length -lt $offset.Value + 2) { throw [ArgumentException]::new("Data too short for MPI bit count.") } # Bit count is big-endian 2 octets [int]$bitCount = ([int]$data[$offset.Value] -shl 8) -bor [int]$data[$offset.Value + 1] [int]$byteCount = [Math]::Ceiling($bitCount / 8.0) if ($data.Length -lt $offset.Value + 2 + $byteCount) { throw [ArgumentException]::new("Data too short for MPI value.") } $mpiData = [byte[]]::new($byteCount) [Array]::Copy($data, $offset.Value + 2, $mpiData, 0, $byteCount) $offset.Value += 2 + $byteCount if ($byteCount -eq 0) { return [BigInteger]::Zero } # BigInteger expects little-endian, MPI is big-endian [Array]::Reverse($mpiData) # Ensure positive by checking high bit of original MSB (now at end of reversed array) if (($mpiData[$byteCount - 1] -band 0x80) -ne 0) { $unsignedData = [byte[]]::new($byteCount + 1) [Array]::Copy($mpiData, $unsignedData, $byteCount) # $unsignedData[$byteCount] is already 0 return [BigInteger]::new($unsignedData) } return [BigInteger]::new($mpiData) } static [byte[]] Write([BigInteger]$value) { if ($value -lt 0) { throw [ArgumentOutOfRangeException]::new("MPI values must be non-negative.") } if ($value.IsZero) { return [byte[]]@(0, 0) } $bytes = $value.ToByteArray() # Little-endian $length = $bytes.Length # Remove trailing zero if it was added for sign bit if ($bytes[$length - 1] -eq 0 -and $length -gt 1) { $length-- } # Calculate bit count $msb = $bytes[$length - 1] $bitsInMsb = 0 $tempMsb = $msb while ($tempMsb -gt 0) { $tempMsb = $tempMsb -shr 1 $bitsInMsb++ } $bitCount = ($length - 1) * 8 + $bitsInMsb $res = [byte[]]::new(2 + $length) $res[0] = [byte]($bitCount -shr 8) $res[1] = [byte]($bitCount -band 0xFF) # Write bytes in big-endian for ($i = 0; $i -lt $length; $i++) { $res[2 + $i] = $bytes[$length - 1 - $i] } return $res } } class PgpPacketHeader { [PgpPacketTag] $Tag [PgpPacketFormat] $Format [long] $Length [bool] $IsPartial [int] $HeaderLength static [PgpPacketHeader] Read([byte[]]$data, [int]$offset) { if ($data.Length -le $offset) { return $null } $headerByte = $data[$offset] if (($headerByte -band 0x80) -eq 0) { throw [FormatException]::new("Invalid packet header: bit 7 not set.") } $header = [PgpPacketHeader]::new() $header.Format = ($headerByte -band 0x40) -eq 0x40 ? [PgpPacketFormat]::New : [PgpPacketFormat]::Old if ($header.Format -eq [PgpPacketFormat]::New) { $header.Tag = [PgpPacketTag]($headerByte -band 0x3F) $lenInfo = [PgpPacketHeader]::ReadNewLength($data, $offset + 1) $header.Length = $lenInfo.Length $header.IsPartial = $lenInfo.IsPartial $header.HeaderLength = 1 + $lenInfo.BytesConsumed } else { $header.Tag = [PgpPacketTag](($headerByte -band 0x3C) -shr 2) $lenType = $headerByte -band 0x03 switch ($lenType) { 0 { # 1-byte length $header.Length = [long]$data[$offset + 1] $header.HeaderLength = 2 } 1 { # 2-byte length $header.Length = ([long]$data[$offset + 1] -shl 8) -bor [long]$data[$offset + 2] $header.HeaderLength = 3 } 2 { # 4-byte length $header.Length = ([long]$data[$offset + 1] -shl 24) -bor ([long]$data[$offset + 2] -shl 16) -bor ([long]$data[$offset + 3] -shl 8) -bor [long]$data[$offset + 4] $header.HeaderLength = 5 } 3 { # Indeterminate length $header.Length = -1 $header.HeaderLength = 1 } } } return $header } static hidden [hashtable] ReadNewLength([byte[]]$data, [int]$offset) { $first = $data[$offset] if ($first -lt 192) { return @{ Length = [long]$first; BytesConsumed = 1; IsPartial = $false } } if ($first -lt 224) { $len = (([long]$first - 192) -shl 8) + [long]$data[$offset + 1] + 192 return @{ Length = $len; BytesConsumed = 2; IsPartial = $false } } if ($first -eq 255) { $len = ([long]$data[$offset + 1] -shl 24) -bor ([long]$data[$offset + 2] -shl 16) -bor ([long]$data[$offset + 3] -shl 8) -bor [long]$data[$offset + 4] return @{ Length = $len; BytesConsumed = 5; IsPartial = $false } } # Partial body length $len = 1L -shl ($first -band 0x1F) return @{ Length = $len; BytesConsumed = 1; IsPartial = $true } } } #endregion OpenPgpCore #region OpenPgpPackets class PgpPublicKeyPacket { [byte] $Version [DateTimeOffset] $CreationTime [PgpPublicKeyAlgorithm] $Algorithm [byte[]] $KeyMaterial [bool] $IsSubkey PgpPublicKeyPacket([byte]$version, [DateTimeOffset]$creationTime, [PgpPublicKeyAlgorithm]$algorithm, [byte[]]$keyMaterial, [bool]$isSubkey) { $this.Version = $version $this.CreationTime = $creationTime $this.Algorithm = $algorithm $this.KeyMaterial = $keyMaterial $this.IsSubkey = $isSubkey } static [PgpPublicKeyPacket] Read([byte[]]$data, [bool]$isSubkey = $false) { if ($data.Length -lt 1) { throw [ArgumentException]::new("Data too short for public key packet.") } $v = $data[0] $offset = 1 if ($v -eq 4) { if ($data.Length -lt 6) { throw [ArgumentException]::new("Data too short for V4 public key.") } $ts = ([long]$data[1] -shl 24) -bor ([long]$data[2] -shl 16) -bor ([long]$data[3] -shl 8) -bor [long]$data[4] $ct = [DateTimeOffset]::FromUnixTimeSeconds($ts) $alg = [PgpPublicKeyAlgorithm]$data[5] $material = [byte[]]::new($data.Length - 6) [Array]::Copy($data, 6, $material, 0, $material.Length) return [PgpPublicKeyPacket]::new($v, $ct, $alg, $material, $isSubkey) } elseif ($v -eq 6) { if ($data.Length -lt 10) { throw [ArgumentException]::new("Data too short for V6 public key.") } $ts = ([long]$data[1] -shl 24) -bor ([long]$data[2] -shl 16) -bor ([long]$data[3] -shl 8) -bor [long]$data[4] $ct = [DateTimeOffset]::FromUnixTimeSeconds($ts) $alg = [PgpPublicKeyAlgorithm]$data[5] $keyLen = ([long]$data[6] -shl 24) -bor ([long]$data[7] -shl 16) -bor ([long]$data[8] -shl 8) -bor [long]$data[9] if ($data.Length -lt 10 + $keyLen) { throw [ArgumentException]::new("Data too short for V6 key material.") } $material = [byte[]]::new($keyLen) [Array]::Copy($data, 10, $material, 0, $keyLen) return [PgpPublicKeyPacket]::new($v, $ct, $alg, $material, $isSubkey) } else { throw [NotSupportedException]::new("Unsupported public key version: $v") } } [byte[]] ToArray() { $res = $null if ($this.Version -eq 4) { $res = [byte[]]::new(6 + $this.KeyMaterial.Length) $res[0] = $this.Version $ts = [uint32]$this.CreationTime.ToUnixTimeSeconds() $res[1] = [byte](($ts -shr 24) -band 0xFF) $res[2] = [byte](($ts -shr 16) -band 0xFF) $res[3] = [byte](($ts -shr 8) -band 0xFF) $res[4] = [byte]($ts -band 0xFF) $res[5] = [byte]$this.Algorithm [Array]::Copy($this.KeyMaterial, 0, $res, 6, $this.KeyMaterial.Length) } elseif ($this.Version -eq 6) { $res = [byte[]]::new(10 + $this.KeyMaterial.Length) $res[0] = $this.Version $ts = [uint32]$this.CreationTime.ToUnixTimeSeconds() $res[1] = [byte](($ts -shr 24) -band 0xFF) $res[2] = [byte](($ts -shr 16) -band 0xFF) $res[3] = [byte](($ts -shr 8) -band 0xFF) $res[4] = [byte]($ts -band 0xFF) $res[5] = [byte]$this.Algorithm $kl = [uint32]$this.KeyMaterial.Length $res[6] = [byte](($kl -shr 24) -band 0xFF) $res[7] = [byte](($kl -shr 16) -band 0xFF) $res[8] = [byte](($kl -shr 8) -band 0xFF) $res[9] = [byte]($kl -band 0xFF) [Array]::Copy($this.KeyMaterial, 0, $res, 10, $this.KeyMaterial.Length) } return $res } } class PgpSecretKeyPacket { [PgpPublicKeyPacket] $PublicKey [PgpS2KUsage] $S2KUsage [byte] $CipherAlgorithm [PgpS2KSpecifier] $S2KSpecifier [byte[]] $IV [byte[]] $SecretKeyMaterial PgpSecretKeyPacket([PgpPublicKeyPacket]$publicKey, [PgpS2KUsage]$s2kUsage, [byte]$cipherAlgorithm, [PgpS2KSpecifier]$s2kSpecifier, [byte[]]$iv, [byte[]]$secretKeyMaterial) { $this.PublicKey = $publicKey $this.S2KUsage = $s2kUsage $this.CipherAlgorithm = $cipherAlgorithm $this.S2KSpecifier = $s2kSpecifier $this.IV = $iv $this.SecretKeyMaterial = $secretKeyMaterial } static [PgpSecretKeyPacket] Read([byte[]]$data, [bool]$isSubkey = $false) { # This is complex because we need to parse the public key first, but the public key length is not always known for V4 # For V6, it is known. # For V4, we have to parse the public key material. $v = $data[0] if ($v -eq 4) { # Parse V4 Public Key header (6 bytes) $ts = ([long]$data[1] -shl 24) -bor ([long]$data[2] -shl 16) -bor ([long]$data[3] -shl 8) -bor [long]$data[4] $ct = [DateTimeOffset]::FromUnixTimeSeconds($ts) $alg = [PgpPublicKeyAlgorithm]$data[5] # Now parse MPIs to find where it ends $offset = [ref]6 # This part is algorithm specific. For RSA: n, e $keyMaterialStart = 6 if ($alg -eq [PgpPublicKeyAlgorithm]::RsaEncryptOrSign -or $alg -eq [PgpPublicKeyAlgorithm]::RsaEncryptOnly -or $alg -eq [PgpPublicKeyAlgorithm]::RsaSignOnly) { [Mpi]::Read($data, $offset) # n [Mpi]::Read($data, $offset) # e } else { throw [NotSupportedException]::new("Parsing non-RSA V4 secret keys not yet implemented.") } $keyMaterialLen = $offset.Value - $keyMaterialStart $material = [byte[]]::new($keyMaterialLen) [Array]::Copy($data, $keyMaterialStart, $material, 0, $keyMaterialLen) $pubKey = [PgpPublicKeyPacket]::new($v, $ct, $alg, $material, $isSubkey) $usage = [PgpS2KUsage]$data[$offset.Value] $offset.Value++ $cipher = 0 $spec = $null $ivData = [byte[]]::new(0) if ($usage -ne [PgpS2KUsage]::None) { $cipher = $data[$offset.Value] $offset.Value++ $spec = [PgpS2KSpecifier]::Read($data, $offset) $ivSize = [PgpSecretKeyPacket]::GetCipherBlockSize($cipher) $ivData = [byte[]]::new($ivSize) [Array]::Copy($data, $offset.Value, $ivData, 0, $ivSize) $offset.Value += $ivSize } $secretMaterialLen = $data.Length - $offset.Value $secretMaterial = [byte[]]::new($secretMaterialLen) [Array]::Copy($data, $offset.Value, $secretMaterial, 0, $secretMaterialLen) return [PgpSecretKeyPacket]::new($pubKey, $usage, $cipher, $spec, $ivData, $secretMaterial) } elseif ($v -eq 6) { # V6 is easier because public key length is in the header $ts = ([long]$data[1] -shl 24) -bor ([long]$data[2] -shl 16) -bor ([long]$data[3] -shl 8) -bor [long]$data[4] $ct = [DateTimeOffset]::FromUnixTimeSeconds($ts) $alg = [PgpPublicKeyAlgorithm]$data[5] $pubKeyLen = ([long]$data[6] -shl 24) -bor ([long]$data[7] -shl 16) -bor ([long]$data[8] -shl 8) -bor [long]$data[9] $material = [byte[]]::new($pubKeyLen) [Array]::Copy($data, 10, $material, 0, $pubKeyLen) $pubKey = [PgpPublicKeyPacket]::new($v, $ct, $alg, $material, $isSubkey) $offset = [ref](10 + $pubKeyLen) $scalarOctetCount = $data[$offset.Value] $offset.Value++ $usage = [PgpS2KUsage]::None $cipher = 0 $spec = $null $ivData = [byte[]]::new(0) if ($scalarOctetCount -gt 0) { $usage = [PgpS2KUsage]$data[$offset.Value] $offset.Value++ $cipher = $data[$offset.Value] $offset.Value++ # S2K Specifier and IV are within scalarOctetCount $spec = [PgpS2KSpecifier]::Read($data, $offset) # Remainder of scalarOctetCount is IV $ivSize = (10 + $pubKeyLen + 1 + $scalarOctetCount) - $offset.Value $ivData = [byte[]]::new($ivSize) [Array]::Copy($data, $offset.Value, $ivData, 0, $ivSize) $offset.Value += $ivSize } $secretMaterialLen = $data.Length - $offset.Value $secretMaterial = [byte[]]::new($secretMaterialLen) [Array]::Copy($data, $offset.Value, $secretMaterial, 0, $secretMaterialLen) return [PgpSecretKeyPacket]::new($pubKey, $usage, $cipher, $spec, $ivData, $secretMaterial) } else { throw [NotSupportedException]::new("Unsupported secret key version: $v") } } static hidden [int] GetCipherBlockSize([byte]$cipherAlg) { return $(switch ($cipherAlg) { 7 { 16 } # AES-128 8 { 16 } # AES-192 9 { 16 } # AES-256 default { 8 } # legacy ciphers } ) } [byte[]] ToArray() { $pubData = $this.PublicKey.ToArray() $resList = [System.Collections.Generic.List[byte]]::new() $resList.AddRange($pubData) if ($this.PublicKey.Version -eq 4) { $resList.Add([byte]$this.S2KUsage) if ($this.S2KUsage -ne [PgpS2KUsage]::None) { $resList.Add($this.CipherAlgorithm) $resList.AddRange($this.S2KSpecifier.Write()) $resList.AddRange($this.IV) } } else { if ($this.S2KUsage -eq [PgpS2KUsage]::None) { $resList.Add(0) } else { $s2kData = [System.Collections.Generic.List[byte]]::new() $s2kData.Add([byte]$this.S2KUsage) $s2kData.Add($this.CipherAlgorithm) $s2kData.AddRange($this.S2KSpecifier.Write()) $s2kData.AddRange($this.IV) $resList.Add([byte]$s2kData.Count) $resList.AddRange($s2kData) } } $resList.AddRange($this.SecretKeyMaterial) return $resList.ToArray() } } class PgpUserIdPacket { [string] $UserId PgpUserIdPacket([string]$userId) { $this.UserId = $userId } static [PgpUserIdPacket] Read([byte[]]$data) { return [PgpUserIdPacket]::new([System.Text.Encoding]::UTF8.GetString($data)) } [byte[]] ToArray() { return [System.Text.Encoding]::UTF8.GetBytes($this.UserId) } } class PgpLiteralDataPacket { [PgpLiteralDataFormat] $Format [string] $FileName [DateTimeOffset] $Date [byte[]] $Data PgpLiteralDataPacket([PgpLiteralDataFormat]$format, [string]$fileName, [DateTimeOffset]$date, [byte[]]$data) { $this.Format = $format $this.FileName = $fileName $this.Date = $date $this.Data = $data } static [PgpLiteralDataPacket] Read([byte[]]$data) { $fmt = [PgpLiteralDataFormat]$data[0] $nameLen = $data[1] $name = [System.Text.Encoding]::UTF8.GetString($data, 2, $nameLen) $tsOffset = 2 + $nameLen $timestamp = ([long]$data[$tsOffset] -shl 24) -bor ([long]$data[$tsOffset + 1] -shl 16) -bor ([long]$data[$tsOffset + 2] -shl 8) -bor [long]$data[$tsOffset + 3] $dt = [DateTimeOffset]::FromUnixTimeSeconds($timestamp) $dataOffset = $tsOffset + 4 $contentLen = $data.Length - $dataOffset $content = [byte[]]::new($contentLen) [Array]::Copy($data, $dataOffset, $content, 0, $contentLen) return [PgpLiteralDataPacket]::new($fmt, $name, $dt, $content) } [byte[]] ToArray() { $nameBytes = [System.Text.Encoding]::UTF8.GetBytes($this.FileName) if ($nameBytes.Length -gt 255) { throw [ArgumentException]::new("Filename too long.") } $res = [byte[]]::new(1 + 1 + $nameBytes.Length + 4 + $this.Data.Length) $res[0] = [byte]$this.Format $res[1] = [byte]$nameBytes.Length [Array]::Copy($nameBytes, 0, $res, 2, $nameBytes.Length) $tsOffset = 2 + $nameBytes.Length $ts = [uint32]$this.Date.ToUnixTimeSeconds() $res[$tsOffset] = [byte](($ts -shr 24) -band 0xFF) $res[$tsOffset + 1] = [byte](($ts -shr 16) -band 0xFF) $res[$tsOffset + 2] = [byte](($ts -shr 8) -band 0xFF) $res[$tsOffset + 3] = [byte]($ts -band 0xFF) [Array]::Copy($this.Data, 0, $res, $tsOffset + 4, $this.Data.Length) return $res } } #endregion OpenPgpPackets class OpenPgp : CryptobaseUtils { static [string] ArmorMessage([byte[]]$data, [Dictionary[string, string]]$headers) { return [Armor]::Encode($data, [ArmorType]::Message, $headers) } static [byte[]] DearmorMessage([string]$armoredText) { $decoded = [Armor]::Decode($armoredText) return $decoded.Data } } |