SharpKind.Cryptography.psm1
|
# 'SharpKind Libraries' - Andy Hawkins 2023-2026. using namespace System.Collections.Generic using namespace SharpKind.Security.Cryptography using namespace SharpKind.Security.Cryptography.Cryptanalysis # Each cipher's class, its settings class (none for the keyless ciphers), and, where a hashtable # cannot simply set the settings' properties, a script block that builds the settings from one. $script:Ciphers = [ordered]@{ Adfgvx = @{ Type = [Adfgvx]; SettingsType = [AdfgvxSettings]; Convert = { ConvertTo-AdfgvxSettings $args[0] } } Affine = @{ Type = [Affine]; SettingsType = [AffineSettings] } Atbash = @{ Type = [Atbash] } Autokey = @{ Type = [Autokey]; SettingsType = [VigenereSettings] } Beaufort = @{ Type = [Beaufort]; SettingsType = [VigenereSettings] } Book = @{ Type = [BookCipher]; SettingsType = [BookCipherSettings]; Convert = { ConvertTo-BookCipherSettings $args[0] } } Caesar = @{ Type = [Caesar]; SettingsType = [CaesarSettings] } Columnar = @{ Type = [Columnar]; SettingsType = [ColumnarSettings] } Enigma = @{ Type = [Enigma]; SettingsType = [EnigmaSettings]; Convert = { ConvertTo-EnigmaSettings $args[0] } } Homophonic = @{ Type = [Homophonic]; SettingsType = [HomophonicSettings]; Convert = { ConvertTo-HomophonicSettings $args[0] } } MonoAlphabetic = @{ Type = [MonoAlphabetic]; SettingsType = [MonoAlphabeticSettings] } Playfair = @{ Type = [Playfair]; SettingsType = [PlayfairSettings] } RailFence = @{ Type = [RailFence]; SettingsType = [RailFenceSettings] } Reflector = @{ Type = [Reflector]; SettingsType = [ReflectorSettings]; Convert = { ConvertTo-ReflectorSettings $args[0] } } Rot13 = @{ Type = [Rot13] } Vigenere = @{ Type = [Vigenere]; SettingsType = [VigenereSettings] } } class CipherNames : System.Management.Automation.IValidateSetValuesGenerator { [string[]] GetValidValues() { return [string[]]$script:Ciphers.Keys } } function Assert-SettingsKey { param([System.Collections.IDictionary] $Settings, [string[]] $Allowed) foreach ($key in $Settings.Keys) { if ($key -notin $Allowed) { throw "Unknown setting '$key'. Valid settings are: $($Allowed -join ', ')." } } } function ConvertTo-AdfgvxSettings { param([System.Collections.IDictionary] $Settings) Assert-SettingsKey $Settings 'Labels', 'Square', 'Keyword' $default = [AdfgvxSettings]::new() [AdfgvxSettings]::new( ($Settings.Labels ?? $default.Labels), ($Settings.Square ?? $default.Square), ($Settings.Keyword ?? $default.Keyword)) } function ConvertTo-BookCipherSettings { param([System.Collections.IDictionary] $Settings) Assert-SettingsKey $Settings 'KeyText' [BookCipherSettings]::new($Settings.KeyText) } function ConvertTo-HomophonicSettings { param([System.Collections.IDictionary] $Settings) $homophones = [Dictionary[char, IReadOnlyList[string]]]::new() foreach ($key in $Settings.Keys) { if ("$key".Length -ne 1) { throw "Homophonic setting '$key' must be a single plaintext letter." } $homophones[[char]"$key"] = [string[]]@($Settings[$key]) } [HomophonicSettings]::new($homophones) } function ConvertTo-ReflectorSettings { param([System.Collections.IDictionary] $Settings) Assert-SettingsKey $Settings 'CharacterSet', 'Substitutions' $properties = [ordered]@{} # The character set must be set before the substitutions. foreach ($name in 'CharacterSet', 'Substitutions') { if ($Settings.Contains($name)) { $properties[$name] = [List[char]][char[]]"$($Settings[$name])" } } New-Object ReflectorSettings -Property $properties } function ConvertTo-EnigmaSettings { param([System.Collections.IDictionary] $Settings) Assert-SettingsKey $Settings 'Reflector', 'Rotors', 'Rings', 'Positions', 'Plugboard' # Rotors, rings and positions read left to right, as on the machine, so the last is the fastest. $positions = [EnigmaRotorPosition]::Third, [EnigmaRotorPosition]::Second, [EnigmaRotorPosition]::Fastest $rotorNumbers = @($Settings.Rotors ?? ('I', 'II', 'III')) $rings = @($Settings.Rings ?? (1, 1, 1)) $letters = [char[]]"$($Settings.Positions ?? 'AAA')" foreach ($list in @{ Rotors = $rotorNumbers; Rings = $rings; Positions = $letters }.GetEnumerator()) { if ($list.Value.Count -ne 3) { throw "Enigma setting '$($list.Key)' must have 3 values, left to right." } } $rotors = [Dictionary[EnigmaRotorPosition, IEnigmaRotor]]::new() for ($i = 0; $i -lt 3; $i++) { $rotor = [EnigmaRotor]::new() # Setting the rotor number resets the ring and position, so it goes first. $rotor.RotorNumber = [EnigmaRotorNumber]"$($rotorNumbers[$i])" $rotor.RingPosition = $rings[$i] $rotor.CurrentSetting = [char]::ToUpperInvariant($letters[$i]) $rotors[$positions[$i]] = $rotor } $pairs = [List[EnigmaPlugboardPair]]::new() foreach ($pair in "$($Settings.Plugboard)".Split(' ', [StringSplitOptions]::RemoveEmptyEntries)) { if ($pair.Length -ne 2) { throw "Enigma plugboard pair '$pair' must be 2 letters." } $pairs.Add([EnigmaPlugboardPair]@{ From = [char]::ToUpperInvariant($pair[0]); To = [char]::ToUpperInvariant($pair[1]) }) } $reflector = [EnigmaReflector]::new() $reflector.ReflectorNumber = [EnigmaReflectorNumber]"$($Settings.Reflector ?? 'B')" New-Object EnigmaSettings -Property @{ Reflector = $reflector Rotors = [EnigmaRotors]::new($rotors) Plugboard = [EnigmaPlugboard]::new($pairs) } } function ConvertTo-CipherSettings { param([string] $Cipher, [object] $Settings) $definition = $script:Ciphers[$Cipher] if ($null -eq $definition.SettingsType) { if ($null -ne $Settings) { throw "Cipher '$Cipher' has no settings." } return $null } if ($null -eq $Settings) { throw "Cipher '$Cipher' needs -Settings." } if ($Settings -isnot [System.Collections.IDictionary]) { # Already a settings object, from New-CipherSettings or Find-CipherKey. return $Settings } if ($definition.Convert) { return & $definition.Convert $Settings } # The character set must be set before the substitutions. $properties = [ordered]@{} $Settings.Keys | Sort-Object { $_ -ne 'CharacterSet' } | ForEach-Object { $properties[$_] = $Settings[$_] } New-Object $definition.SettingsType -Property $properties } function New-CipherInstance { param([string] $Cipher, [object] $Settings) $definition = $script:Ciphers[$Cipher] $converted = ConvertTo-CipherSettings $Cipher $Settings if ($null -eq $definition.SettingsType) { return $definition.Type::new() } $definition.Type::new($converted) } function Invoke-Cipher { param([ICipher] $Instance, [string] $Text, [switch] $Decrypt) $result = $Decrypt ? $Instance.Decrypt($Text) : $Instance.Encrypt($Text) # Enigma advances its rotors as it runs. Winding them back makes each piece of text a separate # message with the same key, and leaves the caller's settings object as it was. if ($Instance -is [Enigma]) { $Instance.Reset() } $result } <# .SYNOPSIS Gets settings for a cipher. .DESCRIPTION Returns the cipher's default settings, or random settings with -Random. Pass the result to the -Settings parameter of Protect-CipherText or Unprotect-CipherText. .EXAMPLE $settings = New-CipherSettings -Cipher Enigma -Random #> function New-CipherSettings { [CmdletBinding()] [OutputType([object])] param( [Parameter(Mandatory, Position = 0)] [ValidateSet([CipherNames])] [string] $Cipher, [switch] $Random ) $definition = $script:Ciphers[$Cipher] if ($null -eq $definition.SettingsType) { throw "Cipher '$Cipher' has no settings." } if ($definition.Type -eq [BookCipher]) { throw "Cipher 'Book' has no default or random settings. Pass -Settings @{ KeyText = '...' } instead." } $instance = $definition.Type::new($definition.SettingsType::new()) if ($Random) { $instance.GenerateSettings() } $instance.Settings } <# .SYNOPSIS Encrypts text with a classical cipher. .DESCRIPTION Settings are a settings object from New-CipherSettings or Find-CipherKey, or a hashtable of the settings' properties. Each piece of pipeline input is encrypted as a separate message. .EXAMPLE 'Attack at dawn' | Protect-CipherText -Cipher Caesar -Settings @{ RightShift = 3 } .EXAMPLE Protect-CipherText -Cipher Enigma -Settings @{ Rotors = 'I', 'II', 'III'; Positions = 'AAA' } -Text 'AAAAA' #> function Protect-CipherText { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory, Position = 0)] [ValidateSet([CipherNames])] [string] $Cipher, [Parameter(Position = 1)] [object] $Settings, [Parameter(Mandatory, ValueFromPipeline)] [AllowEmptyString()] [string] $Text ) begin { $instance = New-CipherInstance $Cipher $Settings } process { Invoke-Cipher $instance $Text } } <# .SYNOPSIS Decrypts text with a classical cipher. .DESCRIPTION Takes the same settings as Protect-CipherText. Each piece of pipeline input is decrypted as a separate message. .EXAMPLE 'DWWDFN DW GDZQ' | Unprotect-CipherText -Cipher Caesar -Settings @{ RightShift = 3 } #> function Unprotect-CipherText { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory, Position = 0)] [ValidateSet([CipherNames])] [string] $Cipher, [Parameter(Position = 1)] [object] $Settings, [Parameter(Mandatory, ValueFromPipeline)] [AllowEmptyString()] [string] $Text ) begin { $instance = New-CipherInstance $Cipher $Settings } process { Invoke-Cipher $instance $Text -Decrypt } } <# .SYNOPSIS Cracks ciphertext without the key. .DESCRIPTION Finds the most likely settings by statistical analysis of the plaintext language, and returns them with the plaintext they give. Caesar and MonoAlphabetic assume English. Homophonic takes -Language. Cracking needs a reasonable length of ciphertext: a few hundred letters for the substitution ciphers. .EXAMPLE 'WKH TXLFN EURZQ IRA' | Find-CipherKey -Cipher Caesar #> function Find-CipherKey { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory, Position = 0)] [ValidateSet('Caesar', 'MonoAlphabetic', 'Homophonic')] [string] $Cipher, [Parameter(Mandatory, ValueFromPipeline)] [string] $Text, [Language] $Language = [Language]::English ) process { if ($Cipher -ne 'Homophonic' -and $Language -ne [Language]::English) { throw "Cipher '$Cipher' can only be cracked in English." } $settings = switch ($Cipher) { 'Caesar' { [CaesarSettings]@{ RightShift = [CaesarCryptanalysis]::Crack($Text).Item1 } } 'MonoAlphabetic' { [MonoAlphabeticCryptanalysis]::Crack($Text) } 'Homophonic' { [HomophonicCryptanalysis]::Crack($Text, $Language) } } [pscustomobject]@{ Cipher = $Cipher Settings = $settings Plaintext = Invoke-Cipher (New-CipherInstance $Cipher $settings) $Text -Decrypt } } } Export-ModuleMember -Function New-CipherSettings, Protect-CipherText, Unprotect-CipherText, Find-CipherKey |