diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d366ecf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,264 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+#*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+temp/
+*.msi
diff --git a/GMPPKConverter/GMPPKConverter.dll b/GMPPKConverter/GMPPKConverter.dll
new file mode 100644
index 0000000..1d3036b
Binary files /dev/null and b/GMPPKConverter/GMPPKConverter.dll differ
diff --git a/GMPPKConverter/GMPPKConverter.psd1 b/GMPPKConverter/GMPPKConverter.psd1
new file mode 100644
index 0000000..64aa98f
--- /dev/null
+++ b/GMPPKConverter/GMPPKConverter.psd1
@@ -0,0 +1,132 @@
+#
+# Module manifest for module 'GoogleDrive'
+#
+# Generated by: MVKozlov
+#
+# Generated on: 20.04.2022
+#
+# Version: $version$
+
+@{
+
+# Script module or binary module file associated with this manifest.
+RootModule = 'GMPPKConverter'
+
+# Version number of this module.
+ModuleVersion = '1.0.0.0'
+
+# Supported PSEditions
+# CompatiblePSEditions = @()
+
+# ID used to uniquely identify this module
+GUID = 'b3deb8eb-c2c3-4f22-9432-b367a04d4653'
+
+# Author of this module
+Author = 'Max Kozlov'
+
+# Company or vendor of this module
+CompanyName = 'NA'
+
+# Copyright statement for this module
+Copyright = '(c) 2022 Max Kozlov. All rights reserved.'
+
+# Description of the functionality provided by this module
+Description = 'Putty Key Converter'
+
+# Minimum version of the Windows PowerShell engine required by this module
+PowerShellVersion = '5.1'
+
+# Name of the Windows PowerShell host required by this module
+# PowerShellHostName = ''
+
+# Minimum version of the Windows PowerShell host required by this module
+# PowerShellHostVersion = ''
+
+# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+# DotNetFrameworkVersion = ''
+
+# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+# CLRVersion = ''
+
+# Processor architecture (None, X86, Amd64) required by this module
+# ProcessorArchitecture = ''
+
+# Modules that must be imported into the global environment prior to importing this module
+# RequiredModules = @()
+
+# Assemblies that must be loaded prior to importing this module
+# RequiredAssemblies = @()
+
+# Script files (.ps1) that are run in the caller's environment prior to importing this module.
+# ScriptsToProcess = @()
+
+# Type files (.ps1xml) to be loaded when importing this module
+# TypesToProcess = @()
+
+# Format files (.ps1xml) to be loaded when importing this module
+# FormatsToProcess = @()
+
+# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
+# NestedModules = @()
+
+# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
+FunctionsToExport = @(
+ "ConvertFrom-PPK"
+)
+
+# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
+CmdletsToExport = @()
+
+# Variables to export from this module
+# VariablesToExport = @()
+
+# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
+AliasesToExport = @()
+
+# DSC resources to export from this module
+# DscResourcesToExport = @()
+
+# List of all modules packaged with this module
+# ModuleList = @()
+
+# List of all files packaged with this module
+# FileList = @()
+
+# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
+PrivateData = @{
+
+ PSData = @{
+
+ # Tags applied to this module. These help with module discovery in online galleries.
+ Tags = @('Powershell','SSH', 'Putty', 'PPK')
+
+ # A URL to the license for this module.
+ LicenseUri = 'https://opensource.org/licenses/MIT'
+
+ # A URL to the main website for this project.
+ ProjectUri = 'https://github.com/MVKozlov/GMPPKConverter'
+
+ # A URL to an icon representing this module.
+ # IconUri = ''
+
+ # ReleaseNotes of this module
+ # ReleaseNotes = ''
+
+ # If true, the LicenseUrl points to an end-user license (not just a source license) which requires the user agreement before use.
+ RequireLicenseAcceptance = 'False'
+
+ # Indicates this is a pre-release/testing version of the module.
+ IsPrerelease = 'False'
+
+ } # End of PSData hashtable
+
+} # End of PrivateData hashtable
+
+# HelpInfo URI of this module
+# HelpInfoURI = ''
+
+# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
+# DefaultCommandPrefix = ''
+
+}
+
diff --git a/GMPPKConverter/GMPPKConverter.psm1 b/GMPPKConverter/GMPPKConverter.psm1
new file mode 100644
index 0000000..e99ef5d
--- /dev/null
+++ b/GMPPKConverter/GMPPKConverter.psm1
@@ -0,0 +1,40 @@
+function ConvertFrom-PPK {
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory, Position=0, ValueFromPipeline)]
+ [string[]]$KeyContent,
+ [Parameter(ParameterSetName="p")]
+ [switch]$AsPrivate,
+ [Parameter(ParameterSetName="o")]
+ [switch]$AsOpenSSH,
+ [Parameter(Position=1)]
+ [SecureString]$Password
+)
+ BEGIN {
+ $Key = New-Object System.Collections.ArrayList
+ }
+ PROCESS {
+ $Key.AddRange($KeyContent)
+ }
+ END {
+ try {
+ [void][GMax.Security.KeyConverter]
+ }
+ catch {
+ Add-Type -Path "$PSScriptRoot\GMPPKConverter.dll"
+ }
+ try {
+ $ppk = New-Object GMax.Security.KeyConverter
+ $ppk.ImportPPK($key, $password)
+ if ($PSCmdlet.ParameterSetName -eq 'p') {
+ $ppk.ExportPrivateKey()
+ }
+ else {
+ $ppk.ExportOpenSSH()
+ }
+ }
+ catch {
+ throw
+ }
+ }
+}
diff --git a/README.MD b/README.MD
new file mode 100644
index 0000000..6ac0f00
--- /dev/null
+++ b/README.MD
@@ -0,0 +1,26 @@
+# Powershell Module: Putty key parser/converter
+
+Putty key converter as _c# class_ can read [putty](https://www.putty.org/) v2/v3 keys and convert it to **UNPROTECTED** keys suitable to use with .NET SSH modules such as [Renci.SshNet](https://github.com/sshnet/SSH.NET/)
+
+Powershell module was written as companion to [Posh-SSH](https://github.com/darkoperator/Posh-SSH) module
+
+As well as _Posh-SSH_ this module is for Windows PowerShell 5.1 or PowerShell 7.x., On Windows Server, version 1709 or older .Net Framework 4.8 or above is required for the proper loading of the module.
+
+## Usage example
+
+``` powershell
+$cred = Get-Credential
+$KeyString = Get-Content D:\Putty.ppk | ConvertFrom-PPK -AsPrivate -Password $cred.Password
+New-SSHSession -ComputerName myserver -Credentials $cred -KeyString $KeyString
+```
+
+## Once again, output is **UNENCRYPTED**, so do not write it on disk
+
+### Notes
+
+- [Konscious.Security.Cryptography.Argon2](https://github.com/kmaragon/Konscious.Security.Cryptography) used for ppk v3 decryption used as source in library, it modified to disable hardware acceleration to work without _System.Numerics.Vectors_ which not available to _netstandard2.0_
+
+### TODO
+
+- Back Conversion
+- Key Encryption... may be... someday
diff --git a/Source/GMPPKConverter.Tests/GMPPKConverter.Tests.csproj b/Source/GMPPKConverter.Tests/GMPPKConverter.Tests.csproj
new file mode 100644
index 0000000..d6dbb0d
--- /dev/null
+++ b/Source/GMPPKConverter.Tests/GMPPKConverter.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ netcoreapp3.1
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/Source/GMPPKConverter.Tests/TestData.cs b/Source/GMPPKConverter.Tests/TestData.cs
new file mode 100644
index 0000000..40ed862
--- /dev/null
+++ b/Source/GMPPKConverter.Tests/TestData.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using System.Security;
+
+namespace GMPPKConverter.Tests
+{
+ public static partial class TestData
+ {
+ internal static SecureString GetPassword(string password) {
+ if (password == null)
+ return null;
+ SecureString result = new SecureString();
+ foreach (char c in password)
+ {
+ result.AppendChar(c);
+ }
+ return result;
+ }
+
+ public static IEnumerable Data_RSA => new List
+ {
+ new object[] { ppk_RSA2, null },
+ new object[] { ppk_RSA2, "" },
+ new object[] { ppk_RSA2, "test" },
+ new object[] { ppk_RSA2, "тест" },
+ new object[] { ppk_RSA2_test, "test" },
+ new object[] { ppk_RSA2_тест, "тест" },
+ new object[] { ppk_RSA3, null },
+ new object[] { ppk_RSA3, "" },
+ new object[] { ppk_RSA3, "test" },
+ new object[] { ppk_RSA3, "тест" },
+ new object[] { ppk_RSA3_test, "test" },
+ new object[] { ppk_RSA3_тест, "тест" },
+ };
+
+ public static IEnumerable Data_DSA => new List
+ {
+ new object[] { ppk_DSA2, null },
+ new object[] { ppk_DSA2, "" },
+ new object[] { ppk_DSA2, "test" },
+ new object[] { ppk_DSA2, "тест" },
+ new object[] { ppk_DSA2_test, "test" },
+ new object[] { ppk_DSA2_тест, "тест" },
+ new object[] { ppk_DSA3, null },
+ new object[] { ppk_DSA3, "" },
+ new object[] { ppk_DSA3, "test" },
+ new object[] { ppk_DSA3, "тест" },
+ new object[] { ppk_DSA3_test, "test" },
+ new object[] { ppk_DSA3_тест, "тест" },
+ };
+
+ public static IEnumerable Data_ECDSA => new List
+ {
+ new object[] { ppk_ECDSA2, null },
+ new object[] { ppk_ECDSA2, "" },
+ new object[] { ppk_ECDSA2, "test" },
+ new object[] { ppk_ECDSA2, "тест" },
+ new object[] { ppk_ECDSA2_test, "test" },
+ new object[] { ppk_ECDSA2_тест, "тест" },
+ new object[] { ppk_ECDSA3, null },
+ new object[] { ppk_ECDSA3, "" },
+ new object[] { ppk_ECDSA3, "test" },
+ new object[] { ppk_ECDSA3, "тест" },
+ new object[] { ppk_ECDSA3_test, "test" },
+ new object[] { ppk_ECDSA3_тест, "тест" },
+ };
+ public static IEnumerable Data_ECDSA_2 => new List
+ {
+ new object[] { ppk_ECDSA2_2, null },
+ new object[] { ppk_ECDSA3_2, null },
+ };
+ public static IEnumerable Data_ECDSA_3 => new List
+ {
+ new object[] { ppk_ECDSA2_3, null },
+ new object[] { ppk_ECDSA3_3, null },
+ };
+
+ public static IEnumerable Data_EDDSA => new List
+ {
+ new object[] { ppk_EDDSA2, null },
+ new object[] { ppk_EDDSA2, "" },
+ new object[] { ppk_EDDSA2, "test" },
+ new object[] { ppk_EDDSA2, "тест" },
+ new object[] { ppk_EDDSA2_test, "test" },
+ new object[] { ppk_EDDSA2_тест, "тест" },
+ new object[] { ppk_EDDSA3, null },
+ new object[] { ppk_EDDSA3, "" },
+ new object[] { ppk_EDDSA3, "test" },
+ new object[] { ppk_EDDSA3, "тест" },
+ new object[] { ppk_EDDSA3_test, "test" },
+ new object[] { ppk_EDDSA3_тест, "тест" },
+ };
+ public static IEnumerable Data_EDDSA_2 => new List
+ {
+ new object[] { ppk_EDDSA2_2, null },
+ new object[] { ppk_EDDSA3_2, null },
+ };
+ }
+
+}
diff --git a/Source/GMPPKConverter.Tests/TestData_DSA.cs b/Source/GMPPKConverter.Tests/TestData_DSA.cs
new file mode 100644
index 0000000..c86f95b
--- /dev/null
+++ b/Source/GMPPKConverter.Tests/TestData_DSA.cs
@@ -0,0 +1,252 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GMPPKConverter.Tests
+{
+ public static partial class TestData
+ {
+ internal static string ppk_DSA2 = @"
+PuTTY-User-Key-File-2: ssh-dss
+Encryption: none
+Comment: dsa-key-20220422
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAPjorHcXxvpgjTWTZTYUcZVwL+Q17jG3wjLlzP2oaTFG
+HnUuCwcObR412o4SIWrhVophC/cwTMUGi1BJAxVrxb5qgXXyrHv29V1PdiEPVUf4
+YRAaixc+8M7T3Iz3FfL0GKxSYFBda8SUXvjE3lf+qgM7DnLZ1laCK8MmDdyGoCOS
+w7ylVJErn1dnkXCTrP6JOlmCB91G4MshQX5cCamZBkYlkh3iqjOmb0DQjW3FWlpX
+K0oXNwzUxe56pwiPmfcb5JCwV5ulKeRlERJV6IOH+Krh5XKuXcwn93XqgAmRoGAm
+/T0pg2LU8z9sqSyxuh5LQTZwOkQaLmVroxCmtSILPSMAAAAVAJQb2kP+eWRUdRrK
+wICpa4X37rhHAAABAARul7CfaaWrpH7heegjispVPOXdg1eAGpwVxbZHLopmVO03
+O+vXeWZvfCSJpRXPDcITQtfzJxKPoUwzFEjDtE1CCSo0U87biy1wMxcZAU7TqZuY
+7NTWe0/a/i5XOVuP+m/+5ESo3qX/xHzMzTsB2dnE++STogZflTaL6gg5cfu5ua6P
+0jHNBf8X6ZSj0/QSHnXu9PI6Ug3AnxtnSbjmONWbaca1QWYSPoM1Y8jM+KhSH+7r
+UH5MUcZSaWmCkpPtbQXS0nUzAQy4k9PQVSRyYwvHHUoUX2hn+QHtjMr/tzepO307
+6Y7vog1ZpXbtOmYVhqmVxoh3u8bx+0m3t1tBoTQAAAEAC0JwOsm5CGgrSRpggZaq
+OEttMJGlchrXPHlJSLejcfmXjU3/AVKZ0DWLV0W+dcpPI8k2mWkdpHwcEKFxPgZw
+TqissfYK4D0spqvJ00zeZ1nhE007hZA9IPPBrPRZET8zeYe9Nfaf0iKGej4ShYcD
+Z+AN7HmlvHUnr0YQ8j9X6NWsG0bJj/uncM6q9eE9Jcym1AQEj230Y7xlmBVecqp0
+yDWbTQhFB9mSUNFuAFfDsxEC03BZjmhXu+SVMB0I4DqGH/88GVC/i0dEGjkCacYd
+F28KiHMXBSoWKLd3yzhPX81CBxfH1+ajBFyf4bXOaqMQaRClFcXUhpwBY2ELwen8
+4Q==
+Private-Lines: 1
+AAAAFQCC4CHRVlcAyw6edNWUoDIHAEB0RQ==
+Private-MAC: 85b14e6e0c2f6ac37d99243d944538f1b3da5b25
+";
+
+ internal static string ppk_DSA3 = @"
+PuTTY-User-Key-File-3: ssh-dss
+Encryption: none
+Comment: dsa-key-20220422
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAPjorHcXxvpgjTWTZTYUcZVwL+Q17jG3wjLlzP2oaTFG
+HnUuCwcObR412o4SIWrhVophC/cwTMUGi1BJAxVrxb5qgXXyrHv29V1PdiEPVUf4
+YRAaixc+8M7T3Iz3FfL0GKxSYFBda8SUXvjE3lf+qgM7DnLZ1laCK8MmDdyGoCOS
+w7ylVJErn1dnkXCTrP6JOlmCB91G4MshQX5cCamZBkYlkh3iqjOmb0DQjW3FWlpX
+K0oXNwzUxe56pwiPmfcb5JCwV5ulKeRlERJV6IOH+Krh5XKuXcwn93XqgAmRoGAm
+/T0pg2LU8z9sqSyxuh5LQTZwOkQaLmVroxCmtSILPSMAAAAVAJQb2kP+eWRUdRrK
+wICpa4X37rhHAAABAARul7CfaaWrpH7heegjispVPOXdg1eAGpwVxbZHLopmVO03
+O+vXeWZvfCSJpRXPDcITQtfzJxKPoUwzFEjDtE1CCSo0U87biy1wMxcZAU7TqZuY
+7NTWe0/a/i5XOVuP+m/+5ESo3qX/xHzMzTsB2dnE++STogZflTaL6gg5cfu5ua6P
+0jHNBf8X6ZSj0/QSHnXu9PI6Ug3AnxtnSbjmONWbaca1QWYSPoM1Y8jM+KhSH+7r
+UH5MUcZSaWmCkpPtbQXS0nUzAQy4k9PQVSRyYwvHHUoUX2hn+QHtjMr/tzepO307
+6Y7vog1ZpXbtOmYVhqmVxoh3u8bx+0m3t1tBoTQAAAEAC0JwOsm5CGgrSRpggZaq
+OEttMJGlchrXPHlJSLejcfmXjU3/AVKZ0DWLV0W+dcpPI8k2mWkdpHwcEKFxPgZw
+TqissfYK4D0spqvJ00zeZ1nhE007hZA9IPPBrPRZET8zeYe9Nfaf0iKGej4ShYcD
+Z+AN7HmlvHUnr0YQ8j9X6NWsG0bJj/uncM6q9eE9Jcym1AQEj230Y7xlmBVecqp0
+yDWbTQhFB9mSUNFuAFfDsxEC03BZjmhXu+SVMB0I4DqGH/88GVC/i0dEGjkCacYd
+F28KiHMXBSoWKLd3yzhPX81CBxfH1+ajBFyf4bXOaqMQaRClFcXUhpwBY2ELwen8
+4Q==
+Private-Lines: 1
+AAAAFQCC4CHRVlcAyw6edNWUoDIHAEB0RQ==
+Private-MAC: 798d56745ce4006428775de4c73994fabe9f616ca81eb24915529ca082e8427b
+";
+ internal static string ppk_DSA2_test = @"
+PuTTY-User-Key-File-2: ssh-dss
+Encryption: aes256-cbc
+Comment: dsa-key-20220422
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAPjorHcXxvpgjTWTZTYUcZVwL+Q17jG3wjLlzP2oaTFG
+HnUuCwcObR412o4SIWrhVophC/cwTMUGi1BJAxVrxb5qgXXyrHv29V1PdiEPVUf4
+YRAaixc+8M7T3Iz3FfL0GKxSYFBda8SUXvjE3lf+qgM7DnLZ1laCK8MmDdyGoCOS
+w7ylVJErn1dnkXCTrP6JOlmCB91G4MshQX5cCamZBkYlkh3iqjOmb0DQjW3FWlpX
+K0oXNwzUxe56pwiPmfcb5JCwV5ulKeRlERJV6IOH+Krh5XKuXcwn93XqgAmRoGAm
+/T0pg2LU8z9sqSyxuh5LQTZwOkQaLmVroxCmtSILPSMAAAAVAJQb2kP+eWRUdRrK
+wICpa4X37rhHAAABAARul7CfaaWrpH7heegjispVPOXdg1eAGpwVxbZHLopmVO03
+O+vXeWZvfCSJpRXPDcITQtfzJxKPoUwzFEjDtE1CCSo0U87biy1wMxcZAU7TqZuY
+7NTWe0/a/i5XOVuP+m/+5ESo3qX/xHzMzTsB2dnE++STogZflTaL6gg5cfu5ua6P
+0jHNBf8X6ZSj0/QSHnXu9PI6Ug3AnxtnSbjmONWbaca1QWYSPoM1Y8jM+KhSH+7r
+UH5MUcZSaWmCkpPtbQXS0nUzAQy4k9PQVSRyYwvHHUoUX2hn+QHtjMr/tzepO307
+6Y7vog1ZpXbtOmYVhqmVxoh3u8bx+0m3t1tBoTQAAAEAC0JwOsm5CGgrSRpggZaq
+OEttMJGlchrXPHlJSLejcfmXjU3/AVKZ0DWLV0W+dcpPI8k2mWkdpHwcEKFxPgZw
+TqissfYK4D0spqvJ00zeZ1nhE007hZA9IPPBrPRZET8zeYe9Nfaf0iKGej4ShYcD
+Z+AN7HmlvHUnr0YQ8j9X6NWsG0bJj/uncM6q9eE9Jcym1AQEj230Y7xlmBVecqp0
+yDWbTQhFB9mSUNFuAFfDsxEC03BZjmhXu+SVMB0I4DqGH/88GVC/i0dEGjkCacYd
+F28KiHMXBSoWKLd3yzhPX81CBxfH1+ajBFyf4bXOaqMQaRClFcXUhpwBY2ELwen8
+4Q==
+Private-Lines: 1
+U2UtSP7fGkXKVEEpRwBoK34Vki6VdTH3mBXsWfV9sOk=
+Private-MAC: 0faa00a6b8ffe29b9e69d51bfb17b395c34f211b
+";
+ internal static string ppk_DSA2_тест = @"
+PuTTY-User-Key-File-2: ssh-dss
+Encryption: aes256-cbc
+Comment: dsa-key-20220422
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAPjorHcXxvpgjTWTZTYUcZVwL+Q17jG3wjLlzP2oaTFG
+HnUuCwcObR412o4SIWrhVophC/cwTMUGi1BJAxVrxb5qgXXyrHv29V1PdiEPVUf4
+YRAaixc+8M7T3Iz3FfL0GKxSYFBda8SUXvjE3lf+qgM7DnLZ1laCK8MmDdyGoCOS
+w7ylVJErn1dnkXCTrP6JOlmCB91G4MshQX5cCamZBkYlkh3iqjOmb0DQjW3FWlpX
+K0oXNwzUxe56pwiPmfcb5JCwV5ulKeRlERJV6IOH+Krh5XKuXcwn93XqgAmRoGAm
+/T0pg2LU8z9sqSyxuh5LQTZwOkQaLmVroxCmtSILPSMAAAAVAJQb2kP+eWRUdRrK
+wICpa4X37rhHAAABAARul7CfaaWrpH7heegjispVPOXdg1eAGpwVxbZHLopmVO03
+O+vXeWZvfCSJpRXPDcITQtfzJxKPoUwzFEjDtE1CCSo0U87biy1wMxcZAU7TqZuY
+7NTWe0/a/i5XOVuP+m/+5ESo3qX/xHzMzTsB2dnE++STogZflTaL6gg5cfu5ua6P
+0jHNBf8X6ZSj0/QSHnXu9PI6Ug3AnxtnSbjmONWbaca1QWYSPoM1Y8jM+KhSH+7r
+UH5MUcZSaWmCkpPtbQXS0nUzAQy4k9PQVSRyYwvHHUoUX2hn+QHtjMr/tzepO307
+6Y7vog1ZpXbtOmYVhqmVxoh3u8bx+0m3t1tBoTQAAAEAC0JwOsm5CGgrSRpggZaq
+OEttMJGlchrXPHlJSLejcfmXjU3/AVKZ0DWLV0W+dcpPI8k2mWkdpHwcEKFxPgZw
+TqissfYK4D0spqvJ00zeZ1nhE007hZA9IPPBrPRZET8zeYe9Nfaf0iKGej4ShYcD
+Z+AN7HmlvHUnr0YQ8j9X6NWsG0bJj/uncM6q9eE9Jcym1AQEj230Y7xlmBVecqp0
+yDWbTQhFB9mSUNFuAFfDsxEC03BZjmhXu+SVMB0I4DqGH/88GVC/i0dEGjkCacYd
+F28KiHMXBSoWKLd3yzhPX81CBxfH1+ajBFyf4bXOaqMQaRClFcXUhpwBY2ELwen8
+4Q==
+Private-Lines: 1
+tLEA+ecOMlmXsMdTCTDFDEUGmBtjm1sQ8gtoM6NzaWE=
+Private-MAC: cfa77ae37c5a339b08b1789bd90f213de0d81505
+";
+ internal static string ppk_DSA3_test = @"
+PuTTY-User-Key-File-3: ssh-dss
+Encryption: aes256-cbc
+Comment: dsa-key-20220422
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAPjorHcXxvpgjTWTZTYUcZVwL+Q17jG3wjLlzP2oaTFG
+HnUuCwcObR412o4SIWrhVophC/cwTMUGi1BJAxVrxb5qgXXyrHv29V1PdiEPVUf4
+YRAaixc+8M7T3Iz3FfL0GKxSYFBda8SUXvjE3lf+qgM7DnLZ1laCK8MmDdyGoCOS
+w7ylVJErn1dnkXCTrP6JOlmCB91G4MshQX5cCamZBkYlkh3iqjOmb0DQjW3FWlpX
+K0oXNwzUxe56pwiPmfcb5JCwV5ulKeRlERJV6IOH+Krh5XKuXcwn93XqgAmRoGAm
+/T0pg2LU8z9sqSyxuh5LQTZwOkQaLmVroxCmtSILPSMAAAAVAJQb2kP+eWRUdRrK
+wICpa4X37rhHAAABAARul7CfaaWrpH7heegjispVPOXdg1eAGpwVxbZHLopmVO03
+O+vXeWZvfCSJpRXPDcITQtfzJxKPoUwzFEjDtE1CCSo0U87biy1wMxcZAU7TqZuY
+7NTWe0/a/i5XOVuP+m/+5ESo3qX/xHzMzTsB2dnE++STogZflTaL6gg5cfu5ua6P
+0jHNBf8X6ZSj0/QSHnXu9PI6Ug3AnxtnSbjmONWbaca1QWYSPoM1Y8jM+KhSH+7r
+UH5MUcZSaWmCkpPtbQXS0nUzAQy4k9PQVSRyYwvHHUoUX2hn+QHtjMr/tzepO307
+6Y7vog1ZpXbtOmYVhqmVxoh3u8bx+0m3t1tBoTQAAAEAC0JwOsm5CGgrSRpggZaq
+OEttMJGlchrXPHlJSLejcfmXjU3/AVKZ0DWLV0W+dcpPI8k2mWkdpHwcEKFxPgZw
+TqissfYK4D0spqvJ00zeZ1nhE007hZA9IPPBrPRZET8zeYe9Nfaf0iKGej4ShYcD
+Z+AN7HmlvHUnr0YQ8j9X6NWsG0bJj/uncM6q9eE9Jcym1AQEj230Y7xlmBVecqp0
+yDWbTQhFB9mSUNFuAFfDsxEC03BZjmhXu+SVMB0I4DqGH/88GVC/i0dEGjkCacYd
+F28KiHMXBSoWKLd3yzhPX81CBxfH1+ajBFyf4bXOaqMQaRClFcXUhpwBY2ELwen8
+4Q==
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: dc7c7c6001d0a56c2f82f83a3622f640
+Private-Lines: 1
+G8banbiVc82vIvtyXH6Asw34LHKHZvSvayhFxZi1HVQ=
+Private-MAC: 12403b8d34b933271d9ba05686ce5b241fa62a643ca4b43eabb4a5a980af861a
+";
+ internal static string ppk_DSA3_тест = @"
+PuTTY-User-Key-File-3: ssh-dss
+Encryption: aes256-cbc
+Comment: dsa-key-20220422
+Public-Lines: 18
+AAAAB3NzaC1kc3MAAAEBAPjorHcXxvpgjTWTZTYUcZVwL+Q17jG3wjLlzP2oaTFG
+HnUuCwcObR412o4SIWrhVophC/cwTMUGi1BJAxVrxb5qgXXyrHv29V1PdiEPVUf4
+YRAaixc+8M7T3Iz3FfL0GKxSYFBda8SUXvjE3lf+qgM7DnLZ1laCK8MmDdyGoCOS
+w7ylVJErn1dnkXCTrP6JOlmCB91G4MshQX5cCamZBkYlkh3iqjOmb0DQjW3FWlpX
+K0oXNwzUxe56pwiPmfcb5JCwV5ulKeRlERJV6IOH+Krh5XKuXcwn93XqgAmRoGAm
+/T0pg2LU8z9sqSyxuh5LQTZwOkQaLmVroxCmtSILPSMAAAAVAJQb2kP+eWRUdRrK
+wICpa4X37rhHAAABAARul7CfaaWrpH7heegjispVPOXdg1eAGpwVxbZHLopmVO03
+O+vXeWZvfCSJpRXPDcITQtfzJxKPoUwzFEjDtE1CCSo0U87biy1wMxcZAU7TqZuY
+7NTWe0/a/i5XOVuP+m/+5ESo3qX/xHzMzTsB2dnE++STogZflTaL6gg5cfu5ua6P
+0jHNBf8X6ZSj0/QSHnXu9PI6Ug3AnxtnSbjmONWbaca1QWYSPoM1Y8jM+KhSH+7r
+UH5MUcZSaWmCkpPtbQXS0nUzAQy4k9PQVSRyYwvHHUoUX2hn+QHtjMr/tzepO307
+6Y7vog1ZpXbtOmYVhqmVxoh3u8bx+0m3t1tBoTQAAAEAC0JwOsm5CGgrSRpggZaq
+OEttMJGlchrXPHlJSLejcfmXjU3/AVKZ0DWLV0W+dcpPI8k2mWkdpHwcEKFxPgZw
+TqissfYK4D0spqvJ00zeZ1nhE007hZA9IPPBrPRZET8zeYe9Nfaf0iKGej4ShYcD
+Z+AN7HmlvHUnr0YQ8j9X6NWsG0bJj/uncM6q9eE9Jcym1AQEj230Y7xlmBVecqp0
+yDWbTQhFB9mSUNFuAFfDsxEC03BZjmhXu+SVMB0I4DqGH/88GVC/i0dEGjkCacYd
+F28KiHMXBSoWKLd3yzhPX81CBxfH1+ajBFyf4bXOaqMQaRClFcXUhpwBY2ELwen8
+4Q==
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 7cd2ed78f668a95ddafe8de39d90d9be
+Private-Lines: 1
+PQ9IYfXK03xb9LTDH4SOT7demTw/aUqWwY8AUOKHZik=
+Private-MAC: c6d511a8a2dfb6dfacfd3c3494bea1e4c7d917c171a67ea526798304912314b4
+";
+
+ // Patched to checkint
+ internal static string result_DSA_openssh = @"
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAADMQAAAAdz
+c2gtZHNzAAABAQD46Kx3F8b6YI01k2U2FHGVcC/kNe4xt8Iy5cz9qGkxRh51LgsH
+Dm0eNdqOEiFq4VaKYQv3MEzFBotQSQMVa8W+aoF18qx79vVdT3YhD1VH+GEQGosX
+PvDO09yM9xXy9BisUmBQXWvElF74xN5X/qoDOw5y2dZWgivDJg3chqAjksO8pVSR
+K59XZ5Fwk6z+iTpZggfdRuDLIUF+XAmpmQZGJZId4qozpm9A0I1txVpaVytKFzcM
+1MXueqcIj5n3G+SQsFebpSnkZRESVeiDh/iq4eVyrl3MJ/d16oAJkaBgJv09KYNi
+1PM/bKkssboeS0E2cDpEGi5la6MQprUiCz0jAAAAFQCUG9pD/nlkVHUaysCAqWuF
+9+64RwAAAQAEbpewn2mlq6R+4XnoI4rKVTzl3YNXgBqcFcW2Ry6KZlTtNzvr13lm
+b3wkiaUVzw3CE0LX8ycSj6FMMxRIw7RNQgkqNFPO24stcDMXGQFO06mbmOzU1ntP
+2v4uVzlbj/pv/uREqN6l/8R8zM07AdnZxPvkk6IGX5U2i+oIOXH7ubmuj9IxzQX/
+F+mUo9P0Eh517vTyOlINwJ8bZ0m45jjVm2nGtUFmEj6DNWPIzPioUh/u61B+TFHG
+UmlpgpKT7W0F0tJ1MwEMuJPT0FUkcmMLxx1KFF9oZ/kB7YzK/7c3qTt9O+mO76IN
+WaV27TpmFYaplcaId7vG8ftJt7dbQaE0AAABAAtCcDrJuQhoK0kaYIGWqjhLbTCR
+pXIa1zx5SUi3o3H5l41N/wFSmdA1i1dFvnXKTyPJNplpHaR8HBChcT4GcE6orLH2
+CuA9LKarydNM3mdZ4RNNO4WQPSDzwaz0WRE/M3mHvTX2n9Iihno+EoWHA2fgDex5
+pbx1J69GEPI/V+jVrBtGyY/7p3DOqvXhPSXMptQEBI9t9GO8ZZgVXnKqdMg1m00I
+RQfZklDRbgBXw7MRAtNwWY5oV7vklTAdCOA6hh//PBlQv4tHRBo5AmnGHRdvCohz
+FwUqFii3d8s4T1/NQgcXx9fmowRcn+G1zmqjEGkQpRXF1IacAWNhC8Hp/OEAAANw
+7wAAAO8AAAAAAAAHc3NoLWRzcwAAAQEA+OisdxfG+mCNNZNlNhRxlXAv5DXuMbfC
+MuXM/ahpMUYedS4LBw5tHjXajhIhauFWimEL9zBMxQaLUEkDFWvFvmqBdfKse/b1
+XU92IQ9VR/hhEBqLFz7wztPcjPcV8vQYrFJgUF1rxJRe+MTeV/6qAzsOctnWVoIr
+wyYN3IagI5LDvKVUkSufV2eRcJOs/ok6WYIH3UbgyyFBflwJqZkGRiWSHeKqM6Zv
+QNCNbcVaWlcrShc3DNTF7nqnCI+Z9xvkkLBXm6Up5GURElXog4f4quHlcq5dzCf3
+deqACZGgYCb9PSmDYtTzP2ypLLG6HktBNnA6RBouZWujEKa1Igs9IwAAABUAlBva
+Q/55ZFR1GsrAgKlrhffuuEcAAAEABG6XsJ9ppaukfuF56COKylU85d2DV4AanBXF
+tkcuimZU7Tc769d5Zm98JImlFc8NwhNC1/MnEo+hTDMUSMO0TUIJKjRTztuLLXAz
+FxkBTtOpm5js1NZ7T9r+Llc5W4/6b/7kRKjepf/EfMzNOwHZ2cT75JOiBl+VNovq
+CDlx+7m5ro/SMc0F/xfplKPT9BIede708jpSDcCfG2dJuOY41ZtpxrVBZhI+gzVj
+yMz4qFIf7utQfkxRxlJpaYKSk+1tBdLSdTMBDLiT09BVJHJjC8cdShRfaGf5Ae2M
+yv+3N6k7fTvpju+iDVmldu06ZhWGqZXGiHe7xvH7Sbe3W0GhNAAAAQALQnA6ybkI
+aCtJGmCBlqo4S20wkaVyGtc8eUlIt6Nx+ZeNTf8BUpnQNYtXRb51yk8jyTaZaR2k
+fBwQoXE+BnBOqKyx9grgPSymq8nTTN5nWeETTTuFkD0g88Gs9FkRPzN5h7019p/S
+IoZ6PhKFhwNn4A3seaW8dSevRhDyP1fo1awbRsmP+6dwzqr14T0lzKbUBASPbfRj
+vGWYFV5yqnTINZtNCEUH2ZJQ0W4AV8OzEQLTcFmOaFe75JUwHQjgOoYf/zwZUL+L
+R0QaOQJpxh0XbwqIcxcFKhYot3fLOE9fzUIHF8fX5qMEXJ/htc5qoxBpEKUVxdSG
+nAFjYQvB6fzhAAAAFQCC4CHRVlcAyw6edNWUoDIHAEB0RQAAABBkc2Eta2V5LTIw
+MjIwNDIyAQIDBAUGBwgJCg==
+-----END OPENSSH PRIVATE KEY-----
+";
+
+ internal static string result_DSA_private = @"
+-----BEGIN DSA PRIVATE KEY-----
+MIIDPgIBAAKCAQEA+OisdxfG+mCNNZNlNhRxlXAv5DXuMbfCMuXM/ahpMUYedS4L
+Bw5tHjXajhIhauFWimEL9zBMxQaLUEkDFWvFvmqBdfKse/b1XU92IQ9VR/hhEBqL
+Fz7wztPcjPcV8vQYrFJgUF1rxJRe+MTeV/6qAzsOctnWVoIrwyYN3IagI5LDvKVU
+kSufV2eRcJOs/ok6WYIH3UbgyyFBflwJqZkGRiWSHeKqM6ZvQNCNbcVaWlcrShc3
+DNTF7nqnCI+Z9xvkkLBXm6Up5GURElXog4f4quHlcq5dzCf3deqACZGgYCb9PSmD
+YtTzP2ypLLG6HktBNnA6RBouZWujEKa1Igs9IwIVAJQb2kP+eWRUdRrKwICpa4X3
+7rhHAoIBAARul7CfaaWrpH7heegjispVPOXdg1eAGpwVxbZHLopmVO03O+vXeWZv
+fCSJpRXPDcITQtfzJxKPoUwzFEjDtE1CCSo0U87biy1wMxcZAU7TqZuY7NTWe0/a
+/i5XOVuP+m/+5ESo3qX/xHzMzTsB2dnE++STogZflTaL6gg5cfu5ua6P0jHNBf8X
+6ZSj0/QSHnXu9PI6Ug3AnxtnSbjmONWbaca1QWYSPoM1Y8jM+KhSH+7rUH5MUcZS
+aWmCkpPtbQXS0nUzAQy4k9PQVSRyYwvHHUoUX2hn+QHtjMr/tzepO3076Y7vog1Z
+pXbtOmYVhqmVxoh3u8bx+0m3t1tBoTQCggEAC0JwOsm5CGgrSRpggZaqOEttMJGl
+chrXPHlJSLejcfmXjU3/AVKZ0DWLV0W+dcpPI8k2mWkdpHwcEKFxPgZwTqissfYK
+4D0spqvJ00zeZ1nhE007hZA9IPPBrPRZET8zeYe9Nfaf0iKGej4ShYcDZ+AN7Hml
+vHUnr0YQ8j9X6NWsG0bJj/uncM6q9eE9Jcym1AQEj230Y7xlmBVecqp0yDWbTQhF
+B9mSUNFuAFfDsxEC03BZjmhXu+SVMB0I4DqGH/88GVC/i0dEGjkCacYdF28KiHMX
+BSoWKLd3yzhPX81CBxfH1+ajBFyf4bXOaqMQaRClFcXUhpwBY2ELwen84QIVAILg
+IdFWVwDLDp501ZSgMgcAQHRF
+-----END DSA PRIVATE KEY-----
+";
+
+ }
+}
diff --git a/Source/GMPPKConverter.Tests/TestData_ECDSA.cs b/Source/GMPPKConverter.Tests/TestData_ECDSA.cs
new file mode 100644
index 0000000..7246c91
--- /dev/null
+++ b/Source/GMPPKConverter.Tests/TestData_ECDSA.cs
@@ -0,0 +1,225 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GMPPKConverter.Tests
+{
+ public static partial class TestData
+ {
+ internal static string ppk_ECDSA2 = @"
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp256
+Encryption: none
+Comment: ecdsa-key-20220422
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFvQs0T8ou1p
+Tth4iOIG0Swml4HW1beHYw4/xbLGzQCy5x0OfXsAQeLJWueib27IWlV1BvPvk1nX
+EKK6EVDOFJk=
+Private-Lines: 1
+AAAAIQC+lNJcq6aurpVZ9M3QecPMCq/DE4872gWjCg7iDiuuag==
+Private-MAC: 98864eea1c55f2a28efa7a116f9f243892f3e4dd
+";
+
+ internal static string ppk_ECDSA3 = @"
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp256
+Encryption: none
+Comment: ecdsa-key-20220422
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFvQs0T8ou1p
+Tth4iOIG0Swml4HW1beHYw4/xbLGzQCy5x0OfXsAQeLJWueib27IWlV1BvPvk1nX
+EKK6EVDOFJk=
+Private-Lines: 1
+AAAAIQC+lNJcq6aurpVZ9M3QecPMCq/DE4872gWjCg7iDiuuag==
+Private-MAC: d19a6a9188ca78d26909512c57ef59262f2692629f2da304b270deaccdb94983
+";
+ internal static string ppk_ECDSA2_test = @"
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp256
+Encryption: aes256-cbc
+Comment: ecdsa-key-20220422
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFvQs0T8ou1p
+Tth4iOIG0Swml4HW1beHYw4/xbLGzQCy5x0OfXsAQeLJWueib27IWlV1BvPvk1nX
+EKK6EVDOFJk=
+Private-Lines: 1
+HvpWFriPH7qpwPehc7z32FSEG7ag9k1sjCMd6p4qdomSb2WN1bdzOC54XJr7M6TO
+Private-MAC: e46f95f1d96fc94c030a7573bec23c3262a40766
+";
+ internal static string ppk_ECDSA2_тест = @"
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp256
+Encryption: aes256-cbc
+Comment: ecdsa-key-20220422
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFvQs0T8ou1p
+Tth4iOIG0Swml4HW1beHYw4/xbLGzQCy5x0OfXsAQeLJWueib27IWlV1BvPvk1nX
+EKK6EVDOFJk=
+Private-Lines: 1
+UxHZqXy9yOBQIs6V5A+Z68q6Ktyf1qqyrhgyrUPLpC2dUBoJStscrqAmvNtTpeQ9
+Private-MAC: 43dea461683fc10bac5620683b1a81c22b4b1dee
+";
+ internal static string ppk_ECDSA3_test = @"
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp256
+Encryption: aes256-cbc
+Comment: ecdsa-key-20220422
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFvQs0T8ou1p
+Tth4iOIG0Swml4HW1beHYw4/xbLGzQCy5x0OfXsAQeLJWueib27IWlV1BvPvk1nX
+EKK6EVDOFJk=
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: cb7917ee92f02a5ae6134228de59ce1f
+Private-Lines: 1
+rlwFiFdikxIpRKH5a14454YjYhWutYaesT/qFgYMkoEAAYPG/MCnsUN5gbKltuXF
+Private-MAC: fa5662bc872b4a0c71affe04a656567835e48643cd31693ffc474bcf27d61c93
+";
+ internal static string ppk_ECDSA3_тест = @"
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp256
+Encryption: aes256-cbc
+Comment: ecdsa-key-20220422
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFvQs0T8ou1p
+Tth4iOIG0Swml4HW1beHYw4/xbLGzQCy5x0OfXsAQeLJWueib27IWlV1BvPvk1nX
+EKK6EVDOFJk=
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 1447ca0bc38ae65fac065ed6d3fffc3e
+Private-Lines: 1
+YaKMeute+IEsM5lioh5kx39m3+0FWvp96UtzF77LlFV5lwzcW+MLsEM1z7ry9iOT
+Private-MAC: d2cd077b5c0245d99423455d56928b2912ff67b5c4605af29bf2933f1df0f22e
+";
+
+ // Patched to checkint
+ internal static string result_ECDSA_openssh = @"
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNl
+Y2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRb0LNE/KLtaU7YeIji
+BtEsJpeB1tW3h2MOP8Wyxs0AsucdDn17AEHiyVrnom9uyFpVdQbz75NZ1xCiuhFQ
+zhSZAAAAsO8AAADvAAAAAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy
+NTYAAABBBFvQs0T8ou1pTth4iOIG0Swml4HW1beHYw4/xbLGzQCy5x0OfXsAQeLJ
+Wueib27IWlV1BvPvk1nXEKK6EVDOFJkAAAAhAL6U0lyrpq6ulVn0zdB5w8wKr8MT
+jzvaBaMKDuIOK65qAAAAEmVjZHNhLWtleS0yMDIyMDQyMgECAwQF
+-----END OPENSSH PRIVATE KEY-----
+";
+
+ internal static string result_ECDSA_private = @"
+-----BEGIN EC PRIVATE KEY-----
+MHgCAQEEIQC+lNJcq6aurpVZ9M3QecPMCq/DE4872gWjCg7iDiuuaqAKBggqhkjO
+PQMBB6FEA0IABFvQs0T8ou1pTth4iOIG0Swml4HW1beHYw4/xbLGzQCy5x0OfXsA
+QeLJWueib27IWlV1BvPvk1nXEKK6EVDOFJk=
+-----END EC PRIVATE KEY-----
+";
+
+
+ internal static string ppk_ECDSA2_2 = @"
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp384
+Encryption: none
+Comment: ecdsa-key-20220422
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBJ7EXKEsa7Yc
+AvwRrxvfytQtXo6wYAMZOocwKWfumByYBMvdRku1N+Em5USqLXeIH1dfFkLN4TjE
+gXYESMMQWESEICFgMeAi9YftcWHnOVoeiwbcazbNu4Nr/tyBELD8lQ==
+Private-Lines: 2
+AAAAMQD9uq//RlCokRvLKWTJJhR5KDv44/ZC2/b3Tkiazz52zL/RJN3TldFxfEbh
+v41wRtw=
+Private-MAC: ca730c4ec15ceef608fc397c7a4fc0794a0b325e
+";
+
+ internal static string ppk_ECDSA3_2 = @"
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp384
+Encryption: none
+Comment: ecdsa-key-20220422
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBJ7EXKEsa7Yc
+AvwRrxvfytQtXo6wYAMZOocwKWfumByYBMvdRku1N+Em5USqLXeIH1dfFkLN4TjE
+gXYESMMQWESEICFgMeAi9YftcWHnOVoeiwbcazbNu4Nr/tyBELD8lQ==
+Private-Lines: 2
+AAAAMQD9uq//RlCokRvLKWTJJhR5KDv44/ZC2/b3Tkiazz52zL/RJN3TldFxfEbh
+v41wRtw=
+Private-MAC: 4396170e144c512a79375149da93174f3a96b2f0212fd3c36a72f569025fe4d6
+";
+
+ internal static string result_ECDSA_openssh_2 = @"
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNl
+Y2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSexFyhLGu2HAL8Ea8b
+38rULV6OsGADGTqHMCln7pgcmATL3UZLtTfhJuVEqi13iB9XXxZCzeE4xIF2BEjD
+EFhEhCAhYDHgIvWH7XFh5zlaHosG3Gs2zbuDa/7cgRCw/JUAAADg7wAAAO8AAAAA
+AAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAAhuaXN0cDM4NAAAAGEEnsRcoSxrthwC
+/BGvG9/K1C1ejrBgAxk6hzApZ+6YHJgEy91GS7U34SblRKotd4gfV18WQs3hOMSB
+dgRIwxBYRIQgIWAx4CL1h+1xYec5Wh6LBtxrNs27g2v+3IEQsPyVAAAAMQD9uq//
+RlCokRvLKWTJJhR5KDv44/ZC2/b3Tkiazz52zL/RJN3TldFxfEbhv41wRtwAAAAS
+ZWNkc2Eta2V5LTIwMjIwNDIyAQIDBAU=
+-----END OPENSSH PRIVATE KEY-----
+";
+
+ internal static string result_ECDSA_private_2 = @"
+-----BEGIN EC PRIVATE KEY-----
+MIGlAgEBBDEA/bqv/0ZQqJEbyylkySYUeSg7+OP2Qtv2905Ims8+dsy/0STd05XR
+cXxG4b+NcEbcoAcGBSuBBAAioWQDYgAEnsRcoSxrthwC/BGvG9/K1C1ejrBgAxk6
+hzApZ+6YHJgEy91GS7U34SblRKotd4gfV18WQs3hOMSBdgRIwxBYRIQgIWAx4CL1
+h+1xYec5Wh6LBtxrNs27g2v+3IEQsPyV
+-----END EC PRIVATE KEY-----
+";
+
+ internal static string ppk_ECDSA2_3 = @"
+PuTTY-User-Key-File-2: ecdsa-sha2-nistp521
+Encryption: none
+Comment: ecdsa-key-20220422
+Public-Lines: 4
+AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACzQZE7Mj65
+yZINxC4XfoECymgogW7NxUwnF/ATm2v+65x5yK2Npgc+xSDazKvO6qB9iE+2FAeR
+mZKOysMaNQfOFwC8pQ5TLphlv5qGyl76SkL80uX4jIuOLUu2aG/MCNU4sV4lIva1
+5Kb9w2+acl7F70yURLbhEhEVER1aA5twAQsQJw==
+Private-Lines: 2
+AAAAQgE+vmr/YyN46wDnsmdzlJC6/epm26LezmyYYM+HXxjiNa79CUgEGBalICEy
+Oma4bwKGc3NJo7diy5QJ67cWwY5MLA==
+Private-MAC: 646b0fc2a5e6fe2896ef3e4d289755ec0da0008c
+";
+
+ internal static string ppk_ECDSA3_3 = @"
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp521
+Encryption: none
+Comment: ecdsa-key-20220422
+Public-Lines: 4
+AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACzQZE7Mj65
+yZINxC4XfoECymgogW7NxUwnF/ATm2v+65x5yK2Npgc+xSDazKvO6qB9iE+2FAeR
+mZKOysMaNQfOFwC8pQ5TLphlv5qGyl76SkL80uX4jIuOLUu2aG/MCNU4sV4lIva1
+5Kb9w2+acl7F70yURLbhEhEVER1aA5twAQsQJw==
+Private-Lines: 2
+AAAAQgE+vmr/YyN46wDnsmdzlJC6/epm26LezmyYYM+HXxjiNa79CUgEGBalICEy
+Oma4bwKGc3NJo7diy5QJ67cWwY5MLA==
+Private-MAC: b87aa5774961a47d2addd0191a822bbbdd6229e18ba136c87791e7fad19607f7
+";
+
+ internal static string result_ECDSA_openssh_3 = @"
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNl
+Y2RzYS1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQAs0GROzI+ucmSDcQu
+F36BAspoKIFuzcVMJxfwE5tr/uucecitjaYHPsUg2syrzuqgfYhPthQHkZmSjsrD
+GjUHzhcAvKUOUy6YZb+ahspe+kpC/NLl+IyLji1LtmhvzAjVOLFeJSL2teSm/cNv
+mnJexe9MlES24RIRFREdWgObcAELECcAAAEQ7wAAAO8AAAAAAAATZWNkc2Etc2hh
+Mi1uaXN0cDUyMQAAAAhuaXN0cDUyMQAAAIUEALNBkTsyPrnJkg3ELhd+gQLKaCiB
+bs3FTCcX8BOba/7rnHnIrY2mBz7FINrMq87qoH2IT7YUB5GZko7Kwxo1B84XALyl
+DlMumGW/mobKXvpKQvzS5fiMi44tS7Zob8wI1TixXiUi9rXkpv3Db5pyXsXvTJRE
+tuESERURHVoDm3ABCxAnAAAAQgE+vmr/YyN46wDnsmdzlJC6/epm26LezmyYYM+H
+XxjiNa79CUgEGBalICEyOma4bwKGc3NJo7diy5QJ67cWwY5MLAAAABJlY2RzYS1r
+ZXktMjAyMjA0MjI=
+-----END OPENSSH PRIVATE KEY-----
+";
+
+ internal static string result_ECDSA_private_3 = @"
+-----BEGIN EC PRIVATE KEY-----
+MIHcAgEBBEIBPr5q/2MjeOsA57Jnc5SQuv3qZtui3s5smGDPh18Y4jWu/QlIBBgW
+pSAhMjpmuG8ChnNzSaO3YsuUCeu3FsGOTCygBwYFK4EEACOhgYkDgYYABACzQZE7
+Mj65yZINxC4XfoECymgogW7NxUwnF/ATm2v+65x5yK2Npgc+xSDazKvO6qB9iE+2
+FAeRmZKOysMaNQfOFwC8pQ5TLphlv5qGyl76SkL80uX4jIuOLUu2aG/MCNU4sV4l
+Iva15Kb9w2+acl7F70yURLbhEhEVER1aA5twAQsQJw==
+-----END EC PRIVATE KEY-----
+";
+
+ }
+}
diff --git a/Source/GMPPKConverter.Tests/TestData_EDDSA.cs b/Source/GMPPKConverter.Tests/TestData_EDDSA.cs
new file mode 100644
index 0000000..e87b28b
--- /dev/null
+++ b/Source/GMPPKConverter.Tests/TestData_EDDSA.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GMPPKConverter.Tests
+{
+ public static partial class TestData
+ {
+ internal static string ppk_EDDSA2 = @"
+PuTTY-User-Key-File-2: ssh-ed25519
+Encryption: none
+Comment: eddsa-key-20220422
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIJ95m6T/B6B+R1HU1wD5Q4RWB2gF3VTf55+McY1K
+Coig
+Private-Lines: 1
+AAAAIOg0z7HntPXFH0iDi8any6GjYhdgzIlLhQRppLW1m90j
+Private-MAC: 315431b04ad0cbad6603e2f48ee6f7c6d453fe67
+";
+
+ internal static string ppk_EDDSA3 = @"
+PuTTY-User-Key-File-3: ssh-ed25519
+Encryption: none
+Comment: eddsa-key-20220422
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIJ95m6T/B6B+R1HU1wD5Q4RWB2gF3VTf55+McY1K
+Coig
+Private-Lines: 1
+AAAAIOg0z7HntPXFH0iDi8any6GjYhdgzIlLhQRppLW1m90j
+Private-MAC: 68b9d38fd42bef6ddc4640ecad04380559863a4d8bd7739e918a4ea93e2b72c0
+";
+ internal static string ppk_EDDSA2_test = @"
+PuTTY-User-Key-File-2: ssh-ed25519
+Encryption: aes256-cbc
+Comment: eddsa-key-20220422
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIJ95m6T/B6B+R1HU1wD5Q4RWB2gF3VTf55+McY1K
+Coig
+Private-Lines: 1
+VPjGHzBRbY46GhBG57jY8v6jg/vNmQPFK+AQFUKzEziQdbCTONxFKq1My5lVy0x0
+Private-MAC: e093e77e16919fea12f417f3785b0862af63dc9f
+";
+ internal static string ppk_EDDSA2_тест = @"
+PuTTY-User-Key-File-2: ssh-ed25519
+Encryption: aes256-cbc
+Comment: eddsa-key-20220422
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIJ95m6T/B6B+R1HU1wD5Q4RWB2gF3VTf55+McY1K
+Coig
+Private-Lines: 1
+hn0jkCKOTTswubflG7H3A+Bl25TMCUHfWimXnNSAn6Lltg/c4GW8KZ2oe7QvyEnG
+Private-MAC: bbaf2b3b2741ee869b461a3dd94ec4ed84f56692
+";
+ internal static string ppk_EDDSA3_test = @"
+PuTTY-User-Key-File-3: ssh-ed25519
+Encryption: aes256-cbc
+Comment: eddsa-key-20220422
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIJ95m6T/B6B+R1HU1wD5Q4RWB2gF3VTf55+McY1K
+Coig
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: ec2650bac37c97f76219d62de9b20201
+Private-Lines: 1
+S+Ahe1AXYUMkMIDfS4iuEz4BEzf2K6wJjA1CcdU5Z8L3GWpmHS1KJpQgFT1Exvko
+Private-MAC: 7a8fc025802136b85149b9dcf793d026d07ae3ef66c6f9b395740a0305aa508b
+";
+ internal static string ppk_EDDSA3_тест = @"
+PuTTY-User-Key-File-3: ssh-ed25519
+Encryption: aes256-cbc
+Comment: eddsa-key-20220422
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIJ95m6T/B6B+R1HU1wD5Q4RWB2gF3VTf55+McY1K
+Coig
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 75605ff7e52eac0b1c0acaf3ea5b9ddf
+Private-Lines: 1
+pTi23kcTiMBIH5oxPWRN/KHj+rRdRPIbxyH7R1ey9oU6p7KcSPfJK82Dx0RxFgmq
+Private-MAC: e5c43b4c4d66d507bc384e3f4bff1003438b29b09802367657b7c542626aeeba
+";
+
+ // Patched to checkint
+ internal static string result_EDDSA_openssh = @"
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz
+c2gtZWQyNTUxOQAAACCfeZuk/wegfkdR1NcA+UOEVgdoBd1U3+efjHGNSgqIoAAA
+AKDvAAAA7wAAAAAAAAtzc2gtZWQyNTUxOQAAACCfeZuk/wegfkdR1NcA+UOEVgdo
+Bd1U3+efjHGNSgqIoAAAAEDoNM+x57T1xR9Ig4vGp8uho2IXYMyJS4UEaaS1tZvd
+I595m6T/B6B+R1HU1wD5Q4RWB2gF3VTf55+McY1KCoigAAAAEmVkZHNhLWtleS0y
+MDIyMDQyMgECAwQFBgcICQoL
+-----END OPENSSH PRIVATE KEY-----
+";
+
+ internal static string result_EDDSA_private = @"
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz
+c2gtZWQyNTUxOQAAACCfeZuk/wegfkdR1NcA+UOEVgdoBd1U3+efjHGNSgqIoAAA
+AKDvAAAA7wAAAAAAAAtzc2gtZWQyNTUxOQAAACCfeZuk/wegfkdR1NcA+UOEVgdo
+Bd1U3+efjHGNSgqIoAAAAEDoNM+x57T1xR9Ig4vGp8uho2IXYMyJS4UEaaS1tZvd
+I595m6T/B6B+R1HU1wD5Q4RWB2gF3VTf55+McY1KCoigAAAAEmVkZHNhLWtleS0y
+MDIyMDQyMgECAwQFBgcICQoL
+-----END OPENSSH PRIVATE KEY-----
+";
+
+ // Seems on 04.2022 openssh still do not support it
+
+ internal static string ppk_EDDSA2_2 = @"
+PuTTY-User-Key-File-2: ssh-ed448
+Encryption: none
+Comment: eddsa-key-20220422
+Public-Lines: 2
+AAAACXNzaC1lZDQ0OAAAADkTj4bgSUcOjuf4JKU2Z4sVkoAaewLmhvdTDHg4XOt0
+fwRnhMIB59Xsy/z5h1yBbSPe/QocRKilCAA=
+Private-Lines: 2
+AAAAOU2RiQaWJ/TYhbs/kHwPW+ZVdDmXBGQuLiVIb2cMW9N0l3RQSIc5NSGbodmg
+fYotT4YX/429KVMHAA==
+Private-MAC: 195932ef1a1ea8a1957c1fa603dba92910de91a5
+";
+
+ internal static string ppk_EDDSA3_2 = @"
+PuTTY-User-Key-File-3: ssh-ed448
+Encryption: none
+Comment: eddsa-key-20220422
+Public-Lines: 2
+AAAACXNzaC1lZDQ0OAAAADkTj4bgSUcOjuf4JKU2Z4sVkoAaewLmhvdTDHg4XOt0
+fwRnhMIB59Xsy/z5h1yBbSPe/QocRKilCAA=
+Private-Lines: 2
+AAAAOU2RiQaWJ/TYhbs/kHwPW+ZVdDmXBGQuLiVIb2cMW9N0l3RQSIc5NSGbodmg
+fYotT4YX/429KVMHAA==
+Private-MAC: 4bb5fbaa82b2b415712d08b3f29e4ba94690870bd1aa4d392b40e6115e4862a4
+";
+
+ internal static string result_EDDSA_openssh_2 = @"
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz
+c2gtZWQ0NDgAAAA5E4+G4ElHDo7n+CSlNmeLFZKAGnsC5ob3Uwx4OFzrdH8EZ4TC
+AefV7Mv8+YdcgW0j3v0KHESopQgAAAAA4O8AAADvAAAAAAAACXNzaC1lZDQ0OAAA
+ADkTj4bgSUcOjuf4JKU2Z4sVkoAaewLmhvdTDHg4XOt0fwRnhMIB59Xsy/z5h1yB
+bSPe/QocRKilCAAAAAByTZGJBpYn9NiFuz+QfA9b5lV0OZcEZC4uJUhvZwxb03SX
+dFBIhzk1IZuh2aB9ii1Phhf/jb0pUwcAE4+G4ElHDo7n+CSlNmeLFZKAGnsC5ob3
+Uwx4OFzrdH8EZ4TCAefV7Mv8+YdcgW0j3v0KHESopQgAAAAAEmVkZHNhLWtleS0y
+MDIyMDQyMgEC
+-----END OPENSSH PRIVATE KEY-----
+";
+
+ internal static string result_EDDSA_private_2 = @"
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz
+c2gtZWQ0NDgAAAA5E4+G4ElHDo7n+CSlNmeLFZKAGnsC5ob3Uwx4OFzrdH8EZ4TC
+AefV7Mv8+YdcgW0j3v0KHESopQgAAAAA4O8AAADvAAAAAAAACXNzaC1lZDQ0OAAA
+ADkTj4bgSUcOjuf4JKU2Z4sVkoAaewLmhvdTDHg4XOt0fwRnhMIB59Xsy/z5h1yB
+bSPe/QocRKilCAAAAAByTZGJBpYn9NiFuz+QfA9b5lV0OZcEZC4uJUhvZwxb03SX
+dFBIhzk1IZuh2aB9ii1Phhf/jb0pUwcAE4+G4ElHDo7n+CSlNmeLFZKAGnsC5ob3
+Uwx4OFzrdH8EZ4TCAefV7Mv8+YdcgW0j3v0KHESopQgAAAAAEmVkZHNhLWtleS0y
+MDIyMDQyMgEC
+-----END OPENSSH PRIVATE KEY-----
+";
+ }
+}
diff --git a/Source/GMPPKConverter.Tests/TestData_RSA.cs b/Source/GMPPKConverter.Tests/TestData_RSA.cs
new file mode 100644
index 0000000..fb9ad00
--- /dev/null
+++ b/Source/GMPPKConverter.Tests/TestData_RSA.cs
@@ -0,0 +1,256 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GMPPKConverter.Tests
+{
+ public static partial class TestData
+ {
+ internal static string ppk_RSA2 = @"
+PuTTY-User-Key-File-2: ssh-rsa
+Encryption: none
+Comment: rsa-key-20220422
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCUbAncKDCFnGE9FZnliYUg/oXTs+F681Tv
+tP5iMdgxuZ3ZWe3kii9z6QI7rg2e9vgnYd7kQDPnTRrA5ZIFMRkKuhteqbbhBUk8
+RqIPL4WTmoLHEIOBbEjH5Ovf+PuvdFLz5Ft2lcr6P/W6SIO4JSplGckj2pgyW9N3
+xPPzvenPqL9mKPDWISeC30HfIrjbaGaBABBDtyoAOu7fpTjL+Wr9wYD89DWiEA9C
+9xvMbVuE3fmtjMUxH53JHnrhHy32lHW2OYAyHWvckpnJuvNtxrqahiN6LsruEyAw
+zEFjI7Ke3Wdp9HV+uusE9FaqzBRq7RAYpEU3WYx9G9Iltho5MDrR
+Private-Lines: 14
+AAABAHPZSHDxZb5Ea5nUTIVfq+wu/qJZ23uuhvN7YLugkFFvwoeurAMg4R0mGLog
+hz03JEcJG4Mpodm+000RY8JtqlA3J7nh2n8VMy67Ky+TNj4Z8k4S07KnpL9yLbJq
+k3lF4KHB8qpDDWX7mn/GrhmD2O2FDPzRWM6l4g+wSYJNED0nE72GWTJWT9q6RtjQ
+1fnyCctuHY0HHvcZgxmXmHVCCYjEHQcnONYuxZqOeBlq6SrhySFWctokhcaZ+h9i
+MgVB70sGxpRmOBFwG5EnztQKJzOkxj+1eZkZcbtcQzXhf465PI1MEpfxAOBTsySw
+BHxhuZ/iOzpbG46ypLgOwax4XIkAAACBAOVHtkZYrZf4t850U7dtI+/lMLruiTAR
+V6gZ6yIRRDiyl6BLykSC4e3z1wjGk5ul71mBY0CE6xADSXqfu5DvZVKgnEsLQOfi
+f5xwX8QCggHEV9Eh2KWj3tbXNcuASoOglMOR+r4J4kn17Lel6vKTxs6AkWeJtnUI
+ViiOrjDcFfb/AAAAgQCluAaD5FYX4IBDRLxqOJGxqhK6njY7111BSruhQm/wte5n
+wc664EQANnPoBliSDPqOa8Ls1VNKtPfWs4ey4/PkHPC5gT0Ud+gGqT24BcMozuoe
+9NvHeQLWg0PkuZkHW0iIliSn31LqN8Dj6LVTtAbeNOpS7kDiQmLGyu4ep8oeLwAA
+AIEA3zluPB7lt8SYkNCZucVFMCElwtn4XMybWI0CC45YT5wKdJ8ewNYfnWSg6b2p
+qlyxiFtj9lih9nQgN+a1VpbsIQYK8zaTFlqdLTdUaFrX2pOgDu83T00x5ZIDc3QM
+0AOuWFVuf89mkezGniTxmXy9T/jj3rqLmHCeDEdnB+27hu8=
+Private-MAC: 0ca1142ab9ec25644782b7e191580336cf62072f
+";
+
+ internal static string ppk_RSA3 = @"
+PuTTY-User-Key-File-3: ssh-rsa
+Encryption: none
+Comment: rsa-key-20220422
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCUbAncKDCFnGE9FZnliYUg/oXTs+F681Tv
+tP5iMdgxuZ3ZWe3kii9z6QI7rg2e9vgnYd7kQDPnTRrA5ZIFMRkKuhteqbbhBUk8
+RqIPL4WTmoLHEIOBbEjH5Ovf+PuvdFLz5Ft2lcr6P/W6SIO4JSplGckj2pgyW9N3
+xPPzvenPqL9mKPDWISeC30HfIrjbaGaBABBDtyoAOu7fpTjL+Wr9wYD89DWiEA9C
+9xvMbVuE3fmtjMUxH53JHnrhHy32lHW2OYAyHWvckpnJuvNtxrqahiN6LsruEyAw
+zEFjI7Ke3Wdp9HV+uusE9FaqzBRq7RAYpEU3WYx9G9Iltho5MDrR
+Private-Lines: 14
+AAABAHPZSHDxZb5Ea5nUTIVfq+wu/qJZ23uuhvN7YLugkFFvwoeurAMg4R0mGLog
+hz03JEcJG4Mpodm+000RY8JtqlA3J7nh2n8VMy67Ky+TNj4Z8k4S07KnpL9yLbJq
+k3lF4KHB8qpDDWX7mn/GrhmD2O2FDPzRWM6l4g+wSYJNED0nE72GWTJWT9q6RtjQ
+1fnyCctuHY0HHvcZgxmXmHVCCYjEHQcnONYuxZqOeBlq6SrhySFWctokhcaZ+h9i
+MgVB70sGxpRmOBFwG5EnztQKJzOkxj+1eZkZcbtcQzXhf465PI1MEpfxAOBTsySw
+BHxhuZ/iOzpbG46ypLgOwax4XIkAAACBAOVHtkZYrZf4t850U7dtI+/lMLruiTAR
+V6gZ6yIRRDiyl6BLykSC4e3z1wjGk5ul71mBY0CE6xADSXqfu5DvZVKgnEsLQOfi
+f5xwX8QCggHEV9Eh2KWj3tbXNcuASoOglMOR+r4J4kn17Lel6vKTxs6AkWeJtnUI
+ViiOrjDcFfb/AAAAgQCluAaD5FYX4IBDRLxqOJGxqhK6njY7111BSruhQm/wte5n
+wc664EQANnPoBliSDPqOa8Ls1VNKtPfWs4ey4/PkHPC5gT0Ud+gGqT24BcMozuoe
+9NvHeQLWg0PkuZkHW0iIliSn31LqN8Dj6LVTtAbeNOpS7kDiQmLGyu4ep8oeLwAA
+AIEA3zluPB7lt8SYkNCZucVFMCElwtn4XMybWI0CC45YT5wKdJ8ewNYfnWSg6b2p
+qlyxiFtj9lih9nQgN+a1VpbsIQYK8zaTFlqdLTdUaFrX2pOgDu83T00x5ZIDc3QM
+0AOuWFVuf89mkezGniTxmXy9T/jj3rqLmHCeDEdnB+27hu8=
+Private-MAC: 0048ae5d9ea96bc6d237d184911462e91e40910bc55b0a4e034878c9525d0f36
+";
+ internal static string ppk_RSA2_test = @"
+PuTTY-User-Key-File-2: ssh-rsa
+Encryption: aes256-cbc
+Comment: rsa-key-20220422
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCUbAncKDCFnGE9FZnliYUg/oXTs+F681Tv
+tP5iMdgxuZ3ZWe3kii9z6QI7rg2e9vgnYd7kQDPnTRrA5ZIFMRkKuhteqbbhBUk8
+RqIPL4WTmoLHEIOBbEjH5Ovf+PuvdFLz5Ft2lcr6P/W6SIO4JSplGckj2pgyW9N3
+xPPzvenPqL9mKPDWISeC30HfIrjbaGaBABBDtyoAOu7fpTjL+Wr9wYD89DWiEA9C
+9xvMbVuE3fmtjMUxH53JHnrhHy32lHW2OYAyHWvckpnJuvNtxrqahiN6LsruEyAw
+zEFjI7Ke3Wdp9HV+uusE9FaqzBRq7RAYpEU3WYx9G9Iltho5MDrR
+Private-Lines: 14
+FkMFN1uGdmexrzZbvmts+EvKgX7G/6H1EL44iGxsOzNMsXHoUCpckUEJoXv2AD3o
+HSdz88pIh+1Dz/lGeoRfjXX0C4sF1hEmrwmqg7See/BpmoeUVi50ZgPvCafk2hzN
+eG0T+z6KFTzBi4o7Psafujy72m3ucIXR45S3lJ5qcfsXyLYcnXqYLnBFUEpDCo1+
+9yAOpOM7sFQ+0l2dBG5bXjpCexefkbtIVch5QcVBFGRoDl9tau1Ht6XfWLX2wgq7
+LtDwnUYZ7vcB0pkXKRnvamDt8SpCInsP0D/oUFoFMErqZ3HW1tQPW/dDMuN3RUrX
+vmUVASUB5Xdbi1Ifp4HoEcFrqc8ulkpvekaD92AdV+/O21tLlX/zF0VA1+VuwhZP
+NWl2ucKS/8udmb6ishZ2jP3DdnwCVEqoxGhx8SE4uRwneqOPy8zWswWo9jg1pi/7
+JW5fHYanllpZe+j1PAt+nrtOXxdB2MyDS29OI36T3qCN9ZCZmdBL+XPqrHvO+NlR
+1UmZTofbvlz0SRkUP0S2GMRk6a4g8A1iR2hWN+70gch14Bh766J3LF1vU6Ikp2pf
+OGsdw8NRLdQq/04mun8EnscgQHeanNRMksdvsLph/WxqBqPMoXy0wEgge0rQZbcU
+m2i52+rEjeynuEkono3jVMGlizrhbqYSS9sn/EpXkBK+ZXZyC1GeaGIuQp9q0tjT
+kuoktk2hCbbL7R4/J6XfAIYwbG58ljkgGnND7PdmT6xM4LFJ4WVXrfQKxgD8IhYE
+yw+P9lf0r/MK+YYAnrCgKwv2HMzEkci035ptZ2XZmXtcg7kXkgr3zOl9u0iAxkWf
+bSYdiged5FzCpYhBZh31Ewpp25746pQyhOa3ZGVgeEpPhoyOK21zhA3GFdQaasvC
+Private-MAC: b9cf46e2d78c011256dedaa9bf7f03874177dde0
+";
+ internal static string ppk_RSA2_тест = @"
+PuTTY-User-Key-File-2: ssh-rsa
+Encryption: aes256-cbc
+Comment: rsa-key-20220422
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCUbAncKDCFnGE9FZnliYUg/oXTs+F681Tv
+tP5iMdgxuZ3ZWe3kii9z6QI7rg2e9vgnYd7kQDPnTRrA5ZIFMRkKuhteqbbhBUk8
+RqIPL4WTmoLHEIOBbEjH5Ovf+PuvdFLz5Ft2lcr6P/W6SIO4JSplGckj2pgyW9N3
+xPPzvenPqL9mKPDWISeC30HfIrjbaGaBABBDtyoAOu7fpTjL+Wr9wYD89DWiEA9C
+9xvMbVuE3fmtjMUxH53JHnrhHy32lHW2OYAyHWvckpnJuvNtxrqahiN6LsruEyAw
+zEFjI7Ke3Wdp9HV+uusE9FaqzBRq7RAYpEU3WYx9G9Iltho5MDrR
+Private-Lines: 14
+ejrWOe49BSn6EXaNnyJ3ek+qTBnUSNbN9kHyU2uJh8Ex8C2Opj21iJzHWJ9fUE8j
+04Wx6YTMnWJP4rtRMPiKPXRl/vYJ82Deze65ExFt9PflSBigU0mpQG85TQB2qv1N
+F18sFP2uQuZpeWsrHg5g+wAvNHtzTAgvYc3DFNGASjvtq2d1hdm9l6NsgL9DoVoB
+2VWzz1OFg+IaTQmuVuzSSJdm+OW98tQOdJC1kFPLNtrsAHjJ/PpgtQ8t2XstEiLS
+W90KFJhw1noYUQbmGCkor7te8UPTjxVaYPjh6nw80U+e3PA1gmMoGgKhLZLYA+9/
+6dZknX7Tu5ISMfs56lwjlUBLJzuFuhiSY0jGQ7n/5d7LM4NaUCVzdTfGyCwlN+im
+3i4DpJDenbkPnNwDnuPBE7mhO4JayHgbt3SloP5wrQRuWVA8GImWJRtpjU3DUPb6
+xg+CdDEIuAVucbiH+1jxtKqfS56j9nmhB2JK8TsfrPCJj/cex0X5KW6YkfWn0gtQ
+LIwuKWmLcw1FlpZgvkvjAhupFXorwfd21ArLuqacNWTnyOi20Ic6vGU/A7Fz+ean
+hC5bYfB7LsaksLaMwOWmuWGo9FQWjMvCSVXvSuQHkBV5H+ZeO49EYXj0rHYBr6XO
+UQjYK+TEFi4+ctTmd/jNkGTkvMP8nTgkLTvkVhDw5Pv4dZBPDO12oV1WY5Wn1VwM
+4TwxXqw9fSyj3fDeKDIkXhM1qe0E6TjsoP8j10TkzbgxkP+buUI+B2pQ7Ds25sah
+VH71w9/HDeu8S0Xw2I3QML8tgwHZrjlYf2dEn5ihKbDzwlj+UIXEXGthI7qzbpSr
+rD71ZfWyZdNNv5kp/6DF3BbwTffk3wcb+Bj74T9E6C7XZAIpUKAsOFu1wfMFu3DV
+Private-MAC: dec9546353acdf5e1a1c60d12e35cfd01c85396d
+";
+ internal static string ppk_RSA3_test = @"
+PuTTY-User-Key-File-3: ssh-rsa
+Encryption: aes256-cbc
+Comment: rsa-key-20220422
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCUbAncKDCFnGE9FZnliYUg/oXTs+F681Tv
+tP5iMdgxuZ3ZWe3kii9z6QI7rg2e9vgnYd7kQDPnTRrA5ZIFMRkKuhteqbbhBUk8
+RqIPL4WTmoLHEIOBbEjH5Ovf+PuvdFLz5Ft2lcr6P/W6SIO4JSplGckj2pgyW9N3
+xPPzvenPqL9mKPDWISeC30HfIrjbaGaBABBDtyoAOu7fpTjL+Wr9wYD89DWiEA9C
+9xvMbVuE3fmtjMUxH53JHnrhHy32lHW2OYAyHWvckpnJuvNtxrqahiN6LsruEyAw
+zEFjI7Ke3Wdp9HV+uusE9FaqzBRq7RAYpEU3WYx9G9Iltho5MDrR
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: 60c30784cdc7e2ecf3908265b30f5a85
+Private-Lines: 14
+psXsL0uJfHEFYs6zzU81yiLITAeXOCbkL9Vw1eTALMBXkBJDGjn92Z/FHTglct+f
+uEWMDsHKKYJPjIjRpEyCx807r/ty5nk2vtCGZCxibdEJIpnRcm4FkwKNdq6PiGCC
+6Oef+67gokIzAzWrrsbTDGNw4MCe9SQxOkhjer+ykLIpe3jyoCTc3oBlA98DpFqc
+qvx6alZ1l3hWcMSXh4CtegNu8s4VZ0WInnbg2lLegpjPSosTHX2sopFkBdwHGigJ
+fk5OXfgwKARo3fAhaaWRgqSJhj6bpLtd2YQQ/vsnBjnsYyy9X+wQ2kDW+JdByQeD
+7YvFrd/6rDPE1FNYmdsUs+AOlP/xwT35/G/Dt7/x97Qdbp/5FXsEIQPG1e415vig
+yPDl2T931qgAFJQgD4oZm9pmo0J0ASlwLvBL9k68LE2CEcYr6jmT6dUY3A3OROSY
+TM6TJ3AVS0A7vLG0ngCP2DWFU0qK0xMkZHnUNrCrsMDc/83W/J8+8+IUViEAMmaU
+qEYF5g6K2NRHhJhGaZvQGbfo9xc+qRs7mws56cjLUqcobGxtT0sA4d8Y1sY2YTvV
+tasKTaCUly+Zf8MxXRtOFifCOTuP0BpAVJc1NebnWl62Pyufgju/FJKq1/HykM1c
+CtWa5WEKJSSc7oh1cVlY1StayVpE5YA9zzRg9gPTaCGts83ZlCy1lIan0EkOjGBa
+0DExFl+gmPewzRVF8CUf0IYj2BzhBLWwkbG4k4LAAlsl13btk3XceWcoMQn+bc+m
+0cyvjm4MDSkGT1vZpLyCweUJkQWqWS3yEROTiPt15VP04N0rWY6+irS1d9u8Tt5w
+m/zz0RPRolANDkjAGiO+1cmSpVR/e3Iz/XAzcoWN0ipeVKfnQ14OrEnX0sj4qidb
+Private-MAC: b958b766d9a80cb19f29614c0f6dd5b3aa35adb09ebf1efb66e9d1263c6615c4
+";
+ internal static string ppk_RSA3_тест = @"
+PuTTY-User-Key-File-3: ssh-rsa
+Encryption: aes256-cbc
+Comment: rsa-key-20220422
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCUbAncKDCFnGE9FZnliYUg/oXTs+F681Tv
+tP5iMdgxuZ3ZWe3kii9z6QI7rg2e9vgnYd7kQDPnTRrA5ZIFMRkKuhteqbbhBUk8
+RqIPL4WTmoLHEIOBbEjH5Ovf+PuvdFLz5Ft2lcr6P/W6SIO4JSplGckj2pgyW9N3
+xPPzvenPqL9mKPDWISeC30HfIrjbaGaBABBDtyoAOu7fpTjL+Wr9wYD89DWiEA9C
+9xvMbVuE3fmtjMUxH53JHnrhHy32lHW2OYAyHWvckpnJuvNtxrqahiN6LsruEyAw
+zEFjI7Ke3Wdp9HV+uusE9FaqzBRq7RAYpEU3WYx9G9Iltho5MDrR
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: c9a719ad8ff5a90d6f6b27a1200136b5
+Private-Lines: 14
+teCCShPKmt4z0oLHB3euokTQw4sWNSkzG/xcZpB0Wimh4bfo4WZJaDdlA0rBsdjN
+OQqJMddB/PCWhnNmwggwTp2JXWpRapF27lUormvqB2NwJbQlV5P62mbkR8vWHtDf
+T425MVmv6rZV5UQaXToRtsJgzO7TfHCU6498Y/JgsUQSwqHKDn3Lk6qDwvcFPgre
++8x7N85Zd+5lNKlDuaBfH2lvZlQ+04YWKu2AbFoqLyB8EXtI287NhaL4b+Dek2XH
+z0kjLCZEa/4nANaFdfQUwwkbRqPTkYBi4w8rEzHxIPGRThx/pQnAUUto3s+myOpy
+fpKxgTuWL7hLd5VHoQdzCFqAitn0IFwBkosNu1PFKu4u/0iEGQyYVULlPz+yOSCY
+g4eIJ9KTd/YLgNZIogXEhx24VaJANiA0sNTO7hP4AaNctESAVSE6DSFU5mE8NDtM
+6VUam35X/ATOAm42Sn2hs1U7Q7PoZl6rIsKBA+WuGVnfF/WeouEalDOKBxsvXZTm
+JKnK3sHonBjCPqZrjqOkYUcXW9l29ixRSE9LHqEMUMtgnm4rp6PzCNxykNeMcsed
+W8y+dB7wKKiYbdPlXj99ZEMgSj0xW1i1Vuqp36l4iinecWMH1dbb1ryUEFA1FO6z
+FpRtE1Wb5aLOSLjULnd9EQyEk6h46bgxFkCV/4OFfUIksWJYuXOm6XvQTQH3M65i
+Z8al6JquaaSeueWKfCStahMMTwRkaohXo/igx5tehdkUQVrG0TRnzbrAXHS0K+UZ
+HfVPegNC6xgEQrgQtKnVP7wjep5TuAxQonv/0wZcI0E2fQZ10ph9bHWcznNm3eaX
+y0Qkutb9Dbao0JZCAQDXTKS27HNHyE/scm+pXZTyEc7F8L5VQVAA5c5jcDc7jUaH
+Private-MAC: b1fda4f1e603fb8a0ad3690c4701544d44d1f6f1dc4ec65edabcd3b2eb9d92a9
+";
+
+ // Patched to checkint
+ internal static string result_RSA_openssh = @"
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz
+c2gtcnNhAAAAAwEAAQAAAQEAlGwJ3CgwhZxhPRWZ5YmFIP6F07PhevNU77T+YjHY
+Mbmd2Vnt5Iovc+kCO64Nnvb4J2He5EAz500awOWSBTEZCrobXqm24QVJPEaiDy+F
+k5qCxxCDgWxIx+Tr3/j7r3RS8+RbdpXK+j/1ukiDuCUqZRnJI9qYMlvTd8Tz873p
+z6i/Zijw1iEngt9B3yK422hmgQAQQ7cqADru36U4y/lq/cGA/PQ1ohAPQvcbzG1b
+hN35rYzFMR+dyR564R8t9pR1tjmAMh1r3JKZybrzbca6moYjei7K7hMgMMxBYyOy
+nt1nafR1frrrBPRWqswUau0QGKRFN1mMfRvSJbYaOTA60QAAA9DvAAAA7wAAAAAA
+AAdzc2gtcnNhAAABAQCUbAncKDCFnGE9FZnliYUg/oXTs+F681TvtP5iMdgxuZ3Z
+We3kii9z6QI7rg2e9vgnYd7kQDPnTRrA5ZIFMRkKuhteqbbhBUk8RqIPL4WTmoLH
+EIOBbEjH5Ovf+PuvdFLz5Ft2lcr6P/W6SIO4JSplGckj2pgyW9N3xPPzvenPqL9m
+KPDWISeC30HfIrjbaGaBABBDtyoAOu7fpTjL+Wr9wYD89DWiEA9C9xvMbVuE3fmt
+jMUxH53JHnrhHy32lHW2OYAyHWvckpnJuvNtxrqahiN6LsruEyAwzEFjI7Ke3Wdp
+9HV+uusE9FaqzBRq7RAYpEU3WYx9G9Iltho5MDrRAAAAAwEAAQAAAQBz2Uhw8WW+
+RGuZ1EyFX6vsLv6iWdt7robze2C7oJBRb8KHrqwDIOEdJhi6IIc9NyRHCRuDKaHZ
+vtNNEWPCbapQNye54dp/FTMuuysvkzY+GfJOEtOyp6S/ci2yapN5ReChwfKqQw1l
++5p/xq4Zg9jthQz80VjOpeIPsEmCTRA9JxO9hlkyVk/aukbY0NX58gnLbh2NBx73
+GYMZl5h1QgmIxB0HJzjWLsWajngZaukq4ckhVnLaJIXGmfofYjIFQe9LBsaUZjgR
+cBuRJ87UCiczpMY/tXmZGXG7XEM14X+OuTyNTBKX8QDgU7MksAR8Ybmf4js6WxuO
+sqS4DsGseFyJAAAAgQDfOW48HuW3xJiQ0Jm5xUUwISXC2fhczJtYjQILjlhPnAp0
+nx7A1h+dZKDpvamqXLGIW2P2WKH2dCA35rVWluwhBgrzNpMWWp0tN1RoWtfak6AO
+7zdPTTHlkgNzdAzQA65YVW5/z2aR7MaeJPGZfL1P+OPeuouYcJ4MR2cH7buG7wAA
+AIEA5Ue2Rlitl/i3znRTt20j7+Uwuu6JMBFXqBnrIhFEOLKXoEvKRILh7fPXCMaT
+m6XvWYFjQITrEANJep+7kO9lUqCcSwtA5+J/nHBfxAKCAcRX0SHYpaPe1tc1y4BK
+g6CUw5H6vgniSfXst6Xq8pPGzoCRZ4m2dQhWKI6uMNwV9v8AAACBAKW4BoPkVhfg
+gENEvGo4kbGqErqeNjvXXUFKu6FCb/C17mfBzrrgRAA2c+gGWJIM+o5rwuzVU0q0
+99azh7Lj8+Qc8LmBPRR36AapPbgFwyjO6h7028d5AtaDQ+S5mQdbSIiWJKffUuo3
+wOPotVO0Bt406lLuQOJCYsbK7h6nyh4vAAAAEHJzYS1rZXktMjAyMjA0MjIBAgME
+BQYHCAkK
+-----END OPENSSH PRIVATE KEY-----
+";
+
+ internal static string result_RSA_private = @"
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAlGwJ3CgwhZxhPRWZ5YmFIP6F07PhevNU77T+YjHYMbmd2Vnt
+5Iovc+kCO64Nnvb4J2He5EAz500awOWSBTEZCrobXqm24QVJPEaiDy+Fk5qCxxCD
+gWxIx+Tr3/j7r3RS8+RbdpXK+j/1ukiDuCUqZRnJI9qYMlvTd8Tz873pz6i/Zijw
+1iEngt9B3yK422hmgQAQQ7cqADru36U4y/lq/cGA/PQ1ohAPQvcbzG1bhN35rYzF
+MR+dyR564R8t9pR1tjmAMh1r3JKZybrzbca6moYjei7K7hMgMMxBYyOynt1nafR1
+frrrBPRWqswUau0QGKRFN1mMfRvSJbYaOTA60QIDAQABAoIBAHPZSHDxZb5Ea5nU
+TIVfq+wu/qJZ23uuhvN7YLugkFFvwoeurAMg4R0mGLoghz03JEcJG4Mpodm+000R
+Y8JtqlA3J7nh2n8VMy67Ky+TNj4Z8k4S07KnpL9yLbJqk3lF4KHB8qpDDWX7mn/G
+rhmD2O2FDPzRWM6l4g+wSYJNED0nE72GWTJWT9q6RtjQ1fnyCctuHY0HHvcZgxmX
+mHVCCYjEHQcnONYuxZqOeBlq6SrhySFWctokhcaZ+h9iMgVB70sGxpRmOBFwG5En
+ztQKJzOkxj+1eZkZcbtcQzXhf465PI1MEpfxAOBTsySwBHxhuZ/iOzpbG46ypLgO
+wax4XIkCgYEA5Ue2Rlitl/i3znRTt20j7+Uwuu6JMBFXqBnrIhFEOLKXoEvKRILh
+7fPXCMaTm6XvWYFjQITrEANJep+7kO9lUqCcSwtA5+J/nHBfxAKCAcRX0SHYpaPe
+1tc1y4BKg6CUw5H6vgniSfXst6Xq8pPGzoCRZ4m2dQhWKI6uMNwV9v8CgYEApbgG
+g+RWF+CAQ0S8ajiRsaoSup42O9ddQUq7oUJv8LXuZ8HOuuBEADZz6AZYkgz6jmvC
+7NVTSrT31rOHsuPz5BzwuYE9FHfoBqk9uAXDKM7qHvTbx3kC1oND5LmZB1tIiJYk
+p99S6jfA4+i1U7QG3jTqUu5A4kJixsruHqfKHi8CgYEAgltkYdiXUhlB/+qZkmdG
+EGXhb9ahPygNC3E5A5SHkijQFn4g0RUM+Soy7zVfxRXx4JS05oDr2J3V0BczIDNM
+C09vrScHzw8y+LHXcOzwF6sXNrsknwBteP7BAiO9udq52fuMcTHTa7Ob08pMq4Cj
+ftMQWKquhxmnSsXcecEX5+cCgYAkkGH/n9XPFt21+eiIl0quJJRVUKsusOoGifK0
+NfB07+9WPVDbsrfORDV2sE/CidKOsgAkOT1TcnJskNgUBG+/mRMUGwvbBEnRjm3r
+uHnC+0R06BnF/VE++19zi+/Ty7RJTrdvwrqMqxiIMMQxX7tUM+Cvw5nVUkhxDD1V
+zjeuSQKBgQDfOW48HuW3xJiQ0Jm5xUUwISXC2fhczJtYjQILjlhPnAp0nx7A1h+d
+ZKDpvamqXLGIW2P2WKH2dCA35rVWluwhBgrzNpMWWp0tN1RoWtfak6AO7zdPTTHl
+kgNzdAzQA65YVW5/z2aR7MaeJPGZfL1P+OPeuouYcJ4MR2cH7buG7w==
+-----END RSA PRIVATE KEY-----
+";
+
+ }
+}
diff --git a/Source/GMPPKConverter.Tests/Test_DSA.cs b/Source/GMPPKConverter.Tests/Test_DSA.cs
new file mode 100644
index 0000000..53481f6
--- /dev/null
+++ b/Source/GMPPKConverter.Tests/Test_DSA.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Linq;
+using System.Security;
+using GMax.Security;
+using Xunit;
+
+namespace GMPPKConverter.Tests
+{
+ public class Test_DSA
+ {
+ [Theory]
+ [MemberData(nameof(TestData.Data_DSA), MemberType =typeof(TestData))]
+ public void Test_DSA_PrivateKey(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_DSA_private.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportPrivateKey();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData.Data_DSA), MemberType = typeof(TestData))]
+ public void Test_DSA_OpenSSH(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_DSA_openssh.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportOpenSSH();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("")]
+ public void Test2_EncryptedDSA_BadPassword(string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = TestData.ppk_DSA2_test.TrimStart().Split("\r\n");
+
+ // act
+ var ppk = new KeyConverter();
+ Action act = () => ppk.ImportPPK(key, secPass);
+
+ // assert
+ ArgumentException exception = Assert.Throws(act);
+ //The thrown exception can be used for even more detailed assertions.
+ Assert.Equal("Bad password", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public void Test3_EncryptedDSA_ArgonBadPassword(string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = TestData.ppk_DSA3_test.TrimStart().Split("\r\n");
+
+ // act
+ var ppk = new KeyConverter();
+ Action act = () => ppk.ImportPPK(key, secPass);
+
+ // assert
+ ArgumentException exception = Assert.Throws(act);
+ //The thrown exception can be used for even more detailed assertions.
+ Assert.Equal("Argon2 needs a password set (Parameter 'password')", exception.Message);
+ }
+
+ [Theory]
+ [InlineData("")]
+ public void Test3_EncryptedDSA_BadPassword(string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = TestData.ppk_DSA3_test.TrimStart().Split("\r\n");
+
+ // act
+ var ppk = new KeyConverter();
+ Action act = () => ppk.ImportPPK(key, secPass);
+
+ // assert
+ ArgumentException exception = Assert.Throws(act);
+ //The thrown exception can be used for even more detailed assertions.
+ Assert.Equal("Bad password", exception.Message);
+ }
+ }
+}
diff --git a/Source/GMPPKConverter.Tests/Test_ECDSA.cs b/Source/GMPPKConverter.Tests/Test_ECDSA.cs
new file mode 100644
index 0000000..10c282e
--- /dev/null
+++ b/Source/GMPPKConverter.Tests/Test_ECDSA.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Linq;
+using System.Security;
+using GMax.Security;
+using Xunit;
+
+namespace GMPPKConverter.Tests
+{
+ public class Test_ECDSA
+ {
+ [Theory]
+ [MemberData(nameof(TestData.Data_ECDSA), MemberType =typeof(TestData))]
+ public void Test_ECDSA_PrivateKey(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_ECDSA_private.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportPrivateKey();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData.Data_ECDSA), MemberType = typeof(TestData))]
+ public void Test_ECDSA_OpenSSH(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_ECDSA_openssh.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportOpenSSH();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData.Data_ECDSA_2), MemberType = typeof(TestData))]
+ public void Test_ECDSA_2_PrivateKey(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_ECDSA_private_2.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportPrivateKey();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData.Data_ECDSA_2), MemberType = typeof(TestData))]
+ public void Test_ECDSA_2_OpenSSH(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_ECDSA_openssh_2.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportOpenSSH();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData.Data_ECDSA_3), MemberType = typeof(TestData))]
+ public void Test_ECDSA_3_PrivateKey(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_ECDSA_private_3.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportPrivateKey();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData.Data_ECDSA_3), MemberType = typeof(TestData))]
+ public void Test_ECDSA_3_OpenSSH(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_ECDSA_openssh_3.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportOpenSSH();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("")]
+ public void Test2_EncryptedECDSA_BadPassword(string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = TestData.ppk_ECDSA2_test.TrimStart().Split("\r\n");
+
+ // act
+ var ppk = new KeyConverter();
+ Action act = () => ppk.ImportPPK(key, secPass);
+
+ // assert
+ ArgumentException exception = Assert.Throws(act);
+ //The thrown exception can be used for even more detailed assertions.
+ Assert.Equal("Bad password", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public void Test3_EncryptedECDSA_ArgonBadPassword(string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = TestData.ppk_ECDSA3_test.TrimStart().Split("\r\n");
+
+ // act
+ var ppk = new KeyConverter();
+ Action act = () => ppk.ImportPPK(key, secPass);
+
+ // assert
+ ArgumentException exception = Assert.Throws(act);
+ //The thrown exception can be used for even more detailed assertions.
+ Assert.Equal("Argon2 needs a password set (Parameter 'password')", exception.Message);
+ }
+
+ [Theory]
+ [InlineData("")]
+ public void Test3_EncryptedECDSA_BadPassword(string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = TestData.ppk_ECDSA3_test.TrimStart().Split("\r\n");
+
+ // act
+ var ppk = new KeyConverter();
+ Action act = () => ppk.ImportPPK(key, secPass);
+
+ // assert
+ ArgumentException exception = Assert.Throws(act);
+ //The thrown exception can be used for even more detailed assertions.
+ Assert.Equal("Bad password", exception.Message);
+ }
+ }
+}
diff --git a/Source/GMPPKConverter.Tests/Test_EDDSA.cs b/Source/GMPPKConverter.Tests/Test_EDDSA.cs
new file mode 100644
index 0000000..43e95ea
--- /dev/null
+++ b/Source/GMPPKConverter.Tests/Test_EDDSA.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Linq;
+using System.Security;
+using GMax.Security;
+using Xunit;
+
+namespace GMPPKConverter.Tests
+{
+ public class Test_EDDSA
+ {
+ [Theory]
+ [MemberData(nameof(TestData.Data_EDDSA), MemberType =typeof(TestData))]
+ public void Test_EDDSA_PrivateKey(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_EDDSA_private.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportPrivateKey();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData.Data_EDDSA), MemberType = typeof(TestData))]
+ public void Test_EDDSA_OpenSSH(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_EDDSA_openssh.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportOpenSSH();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData.Data_EDDSA_2), MemberType = typeof(TestData))]
+ public void Test_EDDSA_PrivateKey_2(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_EDDSA_private_2.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportPrivateKey();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData.Data_EDDSA_2), MemberType = typeof(TestData))]
+ public void Test_EDDSA_OpenSSH_2(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_EDDSA_openssh_2.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportOpenSSH();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("")]
+ public void Test2_EncryptedEDDSA_BadPassword(string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = TestData.ppk_EDDSA2_test.TrimStart().Split("\r\n");
+
+ // act
+ var ppk = new KeyConverter();
+ Action act = () => ppk.ImportPPK(key, secPass);
+
+ // assert
+ ArgumentException exception = Assert.Throws(act);
+ //The thrown exception can be used for even more detailed assertions.
+ Assert.Equal("Bad password", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public void Test3_EncryptedEDDSA_ArgonBadPassword(string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = TestData.ppk_EDDSA3_test.TrimStart().Split("\r\n");
+
+ // act
+ var ppk = new KeyConverter();
+ Action act = () => ppk.ImportPPK(key, secPass);
+
+ // assert
+ ArgumentException exception = Assert.Throws(act);
+ //The thrown exception can be used for even more detailed assertions.
+ Assert.Equal("Argon2 needs a password set (Parameter 'password')", exception.Message);
+ }
+
+ [Theory]
+ [InlineData("")]
+ public void Test3_EncryptedEDDSA_BadPassword(string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = TestData.ppk_EDDSA3_test.TrimStart().Split("\r\n");
+
+ // act
+ var ppk = new KeyConverter();
+ Action act = () => ppk.ImportPPK(key, secPass);
+
+ // assert
+ ArgumentException exception = Assert.Throws(act);
+ //The thrown exception can be used for even more detailed assertions.
+ Assert.Equal("Bad password", exception.Message);
+ }
+ }
+}
diff --git a/Source/GMPPKConverter.Tests/Test_RSA.cs b/Source/GMPPKConverter.Tests/Test_RSA.cs
new file mode 100644
index 0000000..6086caa
--- /dev/null
+++ b/Source/GMPPKConverter.Tests/Test_RSA.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Linq;
+using System.Security;
+using GMax.Security;
+using Xunit;
+
+namespace GMPPKConverter.Tests
+{
+ public class Test_RSA
+ {
+ [Theory]
+ [MemberData(nameof(TestData.Data_RSA), MemberType =typeof(TestData))]
+ public void Test_RSA_PrivateKey(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_RSA_private.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportPrivateKey();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData.Data_RSA), MemberType = typeof(TestData))]
+ public void Test_RSA_OpenSSH(string keyData, string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = keyData.TrimStart().Split("\r\n");
+ string expected = TestData.result_RSA_openssh.Replace("\r", "").TrimStart();
+
+ // act
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(key, secPass);
+ string result = ppk.ExportOpenSSH();
+
+ // assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("")]
+ public void Test2_EncryptedRSA_BadPassword(string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = TestData.ppk_RSA2_test.TrimStart().Split("\r\n");
+
+ // act
+ var ppk = new KeyConverter();
+ Action act = () => ppk.ImportPPK(key, secPass);
+
+ // assert
+ ArgumentException exception = Assert.Throws(act);
+ //The thrown exception can be used for even more detailed assertions.
+ Assert.Equal("Bad password", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public void Test3_EncryptedRSA_ArgonBadPassword(string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = TestData.ppk_RSA3_test.TrimStart().Split("\r\n");
+
+ // act
+ var ppk = new KeyConverter();
+ Action act = () => ppk.ImportPPK(key, secPass);
+
+ // assert
+ ArgumentException exception = Assert.Throws(act);
+ //The thrown exception can be used for even more detailed assertions.
+ Assert.Equal("Argon2 needs a password set (Parameter 'password')", exception.Message);
+ }
+
+ [Theory]
+ [InlineData("")]
+ public void Test3_EncryptedRSA_BadPassword(string password)
+ {
+ // arrange
+ SecureString secPass = TestData.GetPassword(password);
+ string[] key = TestData.ppk_RSA3_test.TrimStart().Split("\r\n");
+
+ // act
+ var ppk = new KeyConverter();
+ Action act = () => ppk.ImportPPK(key, secPass);
+
+ // assert
+ ArgumentException exception = Assert.Throws(act);
+ //The thrown exception can be used for even more detailed assertions.
+ Assert.Equal("Bad password", exception.Message);
+ }
+ }
+}
diff --git a/Source/GMPPKConverter/Argon2KDF/Argon2.cs b/Source/GMPPKConverter/Argon2KDF/Argon2.cs
new file mode 100644
index 0000000..06fc64d
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Argon2.cs
@@ -0,0 +1,116 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Konscious.Security.Cryptography
+{
+ using System;
+ using System.Threading.Tasks;
+ using System.Security.Cryptography;
+
+ ///
+ /// An implementation of Argon2 https://github.com/P-H-C/phc-winner-argon2
+ ///
+ [SuppressMessage("Microsoft.Performance", "CA1819")]
+ public abstract class Argon2 : DeriveBytes
+ {
+ ///
+ /// Create an Argon2 for encrypting the given password
+ ///
+ ///
+ public Argon2(byte[] password)
+ {
+ if (password == null || password.Length == 0)
+ throw new ArgumentException("Argon2 needs a password set", nameof(password));
+
+ _password = password;
+ }
+
+ ///
+ /// Implementation of Reset
+ ///
+ public override void Reset()
+ {
+ }
+
+ ///
+ /// Implementation of GetBytes
+ ///
+ public override byte[] GetBytes(int bc)
+ {
+ ValidateParameters(bc);
+ var task = System.Threading.Tasks.Task.Run(async () => await GetBytesAsyncImpl(bc).ConfigureAwait(false) );
+ return task.Result;
+ }
+
+
+ ///
+ /// Implementation of GetBytes
+ ///
+ public Task GetBytesAsync(int bc)
+ {
+ ValidateParameters(bc);
+ return GetBytesAsyncImpl(bc);
+ }
+
+ ///
+ /// The password hashing salt
+ ///
+ public byte[] Salt { get; set; }
+
+ ///
+ /// An optional secret to use while hashing the Password
+ ///
+ public byte[] KnownSecret { get; set; }
+
+ ///
+ /// Any extra associated data to use while hashing the password
+ ///
+ public byte[] AssociatedData { get; set; }
+
+ ///
+ /// The number of iterations to apply to the password hash
+ ///
+ public int Iterations { get; set; }
+
+ ///
+ /// The number of 1kB memory blocks to use while proessing the hash
+ ///
+ public int MemorySize { get; set; }
+
+ ///
+ /// The number of lanes to use while processing the hash
+ ///
+ public int DegreeOfParallelism { get; set; }
+
+ internal abstract Argon2Core BuildCore(int bc);
+
+ private void ValidateParameters(int bc)
+ {
+ if (bc > 1024)
+ throw new NotSupportedException("Current implementation of Argon2 only supports generating up to 1024 bytes");
+
+ if (Iterations < 1)
+ throw new InvalidOperationException("Cannot perform an Argon2 Hash with out at least 1 iteration");
+
+ if (MemorySize < 4)
+ throw new InvalidOperationException("Argon2 requires a minimum of 4kB of memory (MemorySize >= 4)");
+
+ if (DegreeOfParallelism < 1)
+ throw new InvalidOperationException("Argon2 requires at least 1 thread (DegreeOfParallelism)");
+ }
+
+ private Task GetBytesAsyncImpl(int bc)
+ {
+ var n = BuildCore(bc);
+ n.Salt = Salt;
+ n.Secret = KnownSecret;
+ n.AssociatedData = AssociatedData;
+ n.Iterations = Iterations;
+ n.MemorySize = MemorySize;
+ n.DegreeOfParallelism = DegreeOfParallelism;
+
+ return n.Hash(_password);
+ }
+
+ private byte[] _password;
+ }
+}
diff --git a/Source/GMPPKConverter/Argon2KDF/Argon2Core.cs b/Source/GMPPKConverter/Argon2KDF/Argon2Core.cs
new file mode 100644
index 0000000..1d8d10b
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Argon2Core.cs
@@ -0,0 +1,289 @@
+using System.Runtime.CompilerServices;
+
+namespace Konscious.Security.Cryptography
+{
+ using System;
+ using System.IO;
+ using System.Linq;
+ using System.Runtime.InteropServices;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ internal abstract class Argon2Core
+ {
+ public Argon2Core(int hashSize)
+ {
+ _tagLine = hashSize;
+ }
+
+ public int DegreeOfParallelism { get; set; }
+
+ public int MemorySize { get; set; }
+
+ public int Iterations { get; set; }
+
+ public abstract int Type { get; }
+
+ public byte[] AssociatedData { get; set; }
+
+ public byte[] Salt { get; set; }
+
+ public byte[] Secret { get; set; }
+
+ // private stuff starts here
+ internal async Task Hash(byte[] password)
+ {
+ var lanes = await InitializeLanes(password).ConfigureAwait(false);
+
+ var start = 2;
+ for (var i = 0; i < Iterations; ++i)
+ {
+ for (var s = 0; s < 4; s++)
+ {
+ var segment = Enumerable.Range(0, lanes.Length).Select(l => Task.Run(() =>
+ {
+ var lane = lanes[l];
+ var segmentLength = lane.BlockCount / 4;
+ var curOffset = s * segmentLength + start;
+
+ var prevLane = l;
+ var prevOffset = curOffset - 1;
+ if (curOffset == 0)
+ {
+ prevOffset = lane.BlockCount - 1;
+ }
+
+ var state = GenerateState(lanes, segmentLength, i, l, s);
+ for (var c = start; c < segmentLength; ++c, curOffset++)
+ {
+ var pseudoRand = state.PseudoRand(c, prevLane, prevOffset);
+ var refLane = (uint)(pseudoRand >> 32) % lanes.Length;
+
+ if (i == 0 && s == 0)
+ {
+ refLane = l;
+ }
+
+ var refIndex = IndexAlpha(l == refLane, (uint)pseudoRand, lane.BlockCount, segmentLength, i, s, c);
+ var refBlock = lanes[refLane][refIndex];
+ var curBlock = lane[curOffset];
+
+ Compress(curBlock, refBlock, lanes[prevLane][prevOffset]);
+ prevOffset = curOffset;
+ }
+ }));
+
+ await Task.WhenAll(segment).ConfigureAwait(false);
+ start = 0;
+ }
+ }
+
+ return Finalize(lanes);
+ }
+
+ private static void XorLanes(Argon2Lane[] lanes)
+ {
+ var data = lanes[0][lanes[0].BlockCount - 1];
+
+ foreach (var lane in lanes.Skip(1))
+ {
+ var block = lane[lane.BlockCount-1];
+
+ for (var b = 0; b < 128; ++b)
+ {
+ if (!BitConverter.IsLittleEndian)
+ {
+ block[b] = (block[b] >> 56) ^
+ ((block[b] >> 40) & 0xff00UL) ^
+ ((block[b] >> 24) & 0xff0000UL) ^
+ ((block[b] >> 8) & 0xff000000UL) ^
+ ((block[b] << 8) & 0xff00000000UL) ^
+ ((block[b] << 24) & 0xff0000000000UL) ^
+ ((block[b] << 40) & 0xff000000000000UL) ^
+ ((block[b] << 56) & 0xff00000000000000UL);
+ }
+
+ data[b] ^= block[b];
+ }
+ }
+ }
+
+ private byte[] Finalize(Argon2Lane[] lanes)
+ {
+ XorLanes(lanes);
+
+ var ds = new LittleEndianActiveStream();
+ ds.Expose(lanes[0][lanes[0].BlockCount - 1]);
+
+ ModifiedBlake2.Blake2Prime(lanes[0][1], ds, _tagLine);
+ var result = new byte[_tagLine];
+
+ using (var stream = new Argon2Memory.Stream(lanes[0][1]))
+ {
+ stream.Read(result, 0, _tagLine);
+ }
+
+ return result;
+ }
+
+ internal unsafe static void Compress(Argon2Memory dest, Argon2Memory refb, Argon2Memory prev)
+ {
+ var tmpblock = stackalloc ulong[dest.Length];
+ for (var n = 0; n < 128; ++n)
+ {
+ tmpblock[n] = refb[n] ^ prev[n];
+ dest[n] ^= tmpblock[n];
+ }
+
+ for (var i = 0; i < 8; ++i)
+ ModifiedBlake2.DoRoundColumns(tmpblock, i);
+ for (var i = 0; i < 8; ++i)
+ ModifiedBlake2.DoRoundRows(tmpblock, i);
+
+ for (var n = 0; n < 128; ++n)
+ dest[n] ^= tmpblock[n];
+ }
+
+ internal abstract IArgon2PseudoRands GenerateState(Argon2Lane[] lanes, int segmentLength, int pass, int lane, int slice);
+
+ internal async Task InitializeLanes(byte[] password)
+ {
+ var blockHash = Initialize(password);
+
+ var lanes = new Argon2Lane[DegreeOfParallelism];
+
+ // adjust memory size if needed so that each segment has
+ // an even size
+ var segmentLength = MemorySize / (lanes.Length * 4);
+ MemorySize = segmentLength * 4 * lanes.Length;
+ var blocksPerLane = MemorySize / lanes.Length;
+
+ if (blocksPerLane < 4)
+ {
+ throw new InvalidOperationException($"Memory should be enough to provide at least 4 blocks per {nameof(DegreeOfParallelism)}");
+ }
+
+ Task[] init = new Task[lanes.Length * 2];
+ for (var i = 0; i < lanes.Length; ++i)
+ {
+ lanes[i] = new Argon2Lane(blocksPerLane);
+
+ int taskIndex = i * 2;
+ int iClosure = i;
+ init[taskIndex] = Task.Run(() => {
+ var stream = new LittleEndianActiveStream();
+ stream.Expose(blockHash);
+ stream.Expose(0);
+ stream.Expose(iClosure);
+
+ ModifiedBlake2.Blake2Prime(lanes[iClosure][0], stream);
+ });
+
+ init[taskIndex + 1] = Task.Run(() => {
+ var stream = new LittleEndianActiveStream();
+ stream.Expose(blockHash);
+ stream.Expose(1);
+ stream.Expose(iClosure);
+
+ ModifiedBlake2.Blake2Prime(lanes[iClosure][1], stream);
+ });
+ }
+
+ await Task.WhenAll(init).ConfigureAwait(false);
+
+ Array.Clear(blockHash, 0, blockHash.Length);
+ return lanes;
+ }
+
+ internal byte[] Initialize(byte[] password)
+ {
+ // initialize the lanes
+ var blake2 = new HMACBlake2B(512);
+ var dataStream = new LittleEndianActiveStream();
+
+ dataStream.Expose(DegreeOfParallelism);
+ dataStream.Expose(_tagLine);
+ dataStream.Expose(MemorySize);
+ dataStream.Expose(Iterations);
+ dataStream.Expose((uint)0x13);
+ dataStream.Expose((uint)Type);
+ dataStream.Expose(password.Length);
+ dataStream.Expose(password);
+ dataStream.Expose(Salt?.Length ?? 0);
+ dataStream.Expose(Salt);
+ dataStream.Expose(Secret?.Length ?? 0);
+ dataStream.Expose(Secret);
+ dataStream.Expose(AssociatedData?.Length ?? 0);
+ dataStream.Expose(AssociatedData);
+
+ blake2.Initialize();
+ var blockhash = blake2.ComputeHash(dataStream);
+
+ dataStream.ClearBuffer();
+ return blockhash;
+ }
+
+ private static int IndexAlpha(bool sameLane, uint pseudoRand, int laneLength, int segmentLength, int pass, int slice, int index)
+ {
+ uint refAreaSize;
+ if (pass == 0)
+ {
+ if (slice == 0)
+ refAreaSize = (uint)index - 1;
+ else if (sameLane)
+ refAreaSize = (uint)(slice * segmentLength) + (uint)index - 1;
+ else
+ refAreaSize = (uint)(slice * segmentLength) - ((index == 0) ? 1U : 0);
+ }
+ else if (sameLane)
+ refAreaSize = (uint)laneLength - (uint)segmentLength + (uint)index - 1;
+ else
+ refAreaSize = (uint)laneLength - (uint)segmentLength - ((index == 0) ? 1U : 0);
+
+ ulong relativePos = pseudoRand;
+ relativePos = relativePos * relativePos >> 32;
+ relativePos = refAreaSize - 1 - (refAreaSize * relativePos >> 32);
+
+ uint startPos = 0;
+ if (pass != 0)
+ startPos = (slice == 3) ? 0 : ((uint)slice + 1U) * (uint)segmentLength;
+
+ return (int)(((ulong)startPos + relativePos) % (ulong)laneLength);
+ }
+
+#if DEBUG
+ private static void DebugWrite(Argon2Memory mem)
+ {
+ DebugWrite(mem.ToArray());
+ }
+
+ private unsafe static void DebugWrite(ulong[] data)
+ {
+ fixed (ulong *dat = &data[0])
+ {
+ DebugWrite(dat, data.Length);
+ }
+ }
+
+ private unsafe static void DebugWrite(ulong *data, int len)
+ {
+ int offset = 0;
+ while (offset < len)
+ {
+ for (int i = 0; i < 8; i++, offset++)
+ {
+ if (offset == len)
+ break;
+
+ Console.Write("0x{0:x16} ", data[offset]);
+ }
+ Console.WriteLine();
+ }
+ Console.WriteLine();
+ Console.WriteLine();
+ }
+#endif
+
+ private int _tagLine;
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/Argon2Lane.cs b/Source/GMPPKConverter/Argon2KDF/Argon2Lane.cs
new file mode 100644
index 0000000..8469bc9
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Argon2Lane.cs
@@ -0,0 +1,35 @@
+namespace Konscious.Security.Cryptography
+{
+ using System;
+
+ internal class Argon2Lane
+ {
+ public Argon2Lane(int blockCount)
+ {
+ _memory = new ulong[128 * blockCount];
+ }
+
+ public Argon2Memory this[int index]
+ {
+ get
+ {
+ if (index < 0 || index > BlockCount)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ return new Argon2Memory(_memory, 128 * index);
+ }
+ }
+
+ public int BlockCount
+ {
+ get
+ {
+ return _memory.Length / 128;
+ }
+ }
+
+ private ulong[] _memory;
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/Argon2Memory.cs b/Source/GMPPKConverter/Argon2KDF/Argon2Memory.cs
new file mode 100644
index 0000000..5e28118
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Argon2Memory.cs
@@ -0,0 +1,187 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Konscious.Security.Cryptography
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Runtime.InteropServices;
+
+ internal class Argon2Memory : IEnumerable
+ {
+ private ulong[] _data;
+ private int _offset;
+
+ public Argon2Memory(ulong[] data, int offset)
+ {
+ _data = data;
+ _offset = offset;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822")]
+ public int Length
+ {
+ get
+ {
+ return 128;
+ }
+ }
+
+ public void Blit(byte[] data, int destOffset = 0, int srcOffset = 0, int byteLength = -1)
+ {
+ int remainder = 0;
+ int length;
+ if (byteLength < 0)
+ {
+ length = Length;
+ }
+ else
+ {
+ length = byteLength / 8;
+ remainder = byteLength - (length * 8);
+ }
+
+ int readSize = Math.Min((data.Length / 8), length);
+
+ var mstream = new MemoryStream(data);
+ mstream.Seek(srcOffset, SeekOrigin.Begin);
+ var reader = new BinaryReader(mstream);
+
+ readSize += destOffset;
+ int i = destOffset;
+ for (; i < readSize; ++i)
+ {
+ this[i] = reader.ReadUInt64();
+ }
+
+ if (remainder > 0)
+ {
+ ulong extra = 0;
+
+ // get the remainder as a few bytes
+ for (var n = 0; n < remainder; ++n)
+ extra = extra | ((ulong)reader.ReadByte() << (8 * n));
+
+ this[i++] = extra;
+ }
+
+ for (; i < length; ++i)
+ {
+ this[i] = 0;
+ }
+ }
+
+ public void Set(ulong value)
+ {
+ var off = _offset;
+ for (var i = 0; i < 128; i++)
+ {
+ _data[off++] = value;
+ }
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return new Enumerator(_data, _offset);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return new Enumerator(_data, _offset);
+ }
+
+ public ulong this[int index]
+ {
+ get
+ {
+ if (index < 0 || index > 128)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ return _data[_offset + index];
+ }
+ set
+ {
+ if (index < 0 || index >= 128)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ _data[_offset + index] = value;
+ }
+ }
+
+ internal unsafe class Stream : UnmanagedMemoryStream
+ {
+ public Stream(Argon2Memory memory)
+ {
+ _data = GCHandle.Alloc(memory._data, GCHandleType.Pinned);
+ base.Initialize((byte*)_data.AddrOfPinnedObject() + (memory._offset * 8), 1024, 1024, FileAccess.Read);
+ }
+
+ protected override void Dispose(bool isDispose)
+ {
+ base.Dispose(isDispose);
+ _data.Free();
+ }
+
+ private GCHandle _data;
+ }
+
+ private class Enumerator : IEnumerator
+ {
+ private int _start;
+ private int _current;
+ private ulong[] _data;
+
+ public Enumerator(ulong[] data, int start)
+ {
+ _start = start;
+ _data = data;
+
+ Reset();
+ }
+
+ public ulong Current
+ {
+ get
+ {
+ if (_current >= (_start + 128))
+ return 0UL;
+
+ return _data[_current];
+ }
+ }
+
+ object IEnumerator.Current
+ {
+ get
+ {
+ return (object)this.Current;
+ }
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public bool MoveNext()
+ {
+ if (++_current >= (_start + 128))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public void Reset()
+ {
+ _current = _start - 1;
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/Argon2d.cs b/Source/GMPPKConverter/Argon2KDF/Argon2d.cs
new file mode 100644
index 0000000..b1f7f9d
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Argon2d.cs
@@ -0,0 +1,23 @@
+namespace Konscious.Security.Cryptography
+{
+ ///
+ /// An implementation of Argon2 https://github.com/P-H-C/phc-winner-argon2
+ ///
+ public class Argon2d : Argon2
+ {
+ ///
+ /// Create an Argon2 for encrypting the given password using Argon2d
+ ///
+ ///
+ public Argon2d(byte[] password)
+ : base(password)
+ {
+ }
+
+ internal override Argon2Core BuildCore(int bc)
+ {
+ return new Argon2dCore(bc);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/Argon2dCore.cs b/Source/GMPPKConverter/Argon2KDF/Argon2dCore.cs
new file mode 100644
index 0000000..b05462c
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Argon2dCore.cs
@@ -0,0 +1,44 @@
+namespace Konscious.Security.Cryptography
+{
+ using System;
+
+ ///
+ /// The implementation of Argon2d for use in the crypto library
+ ///
+ internal class Argon2dCore : Argon2Core
+ {
+ internal class PseudoRands : IArgon2PseudoRands
+ {
+ private Argon2Lane[] _lanes;
+
+ public PseudoRands(Argon2Lane[] lanes)
+ {
+ _lanes = lanes;
+ }
+
+ public ulong PseudoRand(int segment, int prevLane, int prevOffset)
+ {
+ return _lanes[prevLane][prevOffset][0];
+ }
+ }
+
+ public Argon2dCore(int hashSize)
+ : base(hashSize)
+ {
+
+ }
+
+ public override int Type
+ {
+ get
+ {
+ return 0;
+ }
+ }
+
+ internal override IArgon2PseudoRands GenerateState(Argon2Lane[] lanes, int segmentLength, int pass, int lane, int slice)
+ {
+ return new PseudoRands(lanes);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/Argon2i.cs b/Source/GMPPKConverter/Argon2KDF/Argon2i.cs
new file mode 100644
index 0000000..a5f519b
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Argon2i.cs
@@ -0,0 +1,23 @@
+namespace Konscious.Security.Cryptography
+{
+ ///
+ /// An implementation of Argon2 https://github.com/P-H-C/phc-winner-argon2
+ ///
+ public class Argon2i : Argon2
+ {
+ ///
+ /// Create an Argon2 for encrypting the given password using Argon2i
+ ///
+ ///
+ public Argon2i(byte[] password)
+ : base(password)
+ {
+ }
+
+ internal override Argon2Core BuildCore(int bc)
+ {
+ return new Argon2iCore(bc);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/Argon2iCore.cs b/Source/GMPPKConverter/Argon2KDF/Argon2iCore.cs
new file mode 100644
index 0000000..c512dbf
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Argon2iCore.cs
@@ -0,0 +1,75 @@
+namespace Konscious.Security.Cryptography
+{
+ using System;
+
+ ///
+ /// The implementation of Argon2i for use in the crypto library
+ ///
+ internal class Argon2iCore : Argon2Core
+ {
+ private static Argon2Memory _zeroBlock = new Argon2Memory(new ulong[128], 0);
+
+ internal class PseudoRands : IArgon2PseudoRands
+ {
+ private ulong[] _rands;
+
+ public PseudoRands(ulong[] rands)
+ {
+ _rands = rands;
+ }
+
+ public ulong PseudoRand(int segment, int prevLane, int prevOffset)
+ {
+ return _rands[segment];
+ }
+ }
+
+ public Argon2iCore(int hashSize)
+ : base(hashSize)
+ {
+ }
+
+ public override int Type
+ {
+ get
+ {
+ return 1;
+ }
+ }
+
+ internal override IArgon2PseudoRands GenerateState(Argon2Lane[] lanes, int segmentLength, int pass, int lane, int slice)
+ {
+ var rands = new ulong[segmentLength];
+
+ var ulongRaw = new ulong[384];
+ var inputBlock = new Argon2Memory(ulongRaw, 0);
+ var addressBlock = new Argon2Memory(ulongRaw, 128);
+ var tmpBlock = new Argon2Memory(ulongRaw, 256);
+
+ inputBlock[0] = (ulong)pass;
+ inputBlock[1] = (ulong)lane;
+ inputBlock[2] = (ulong)slice;
+ inputBlock[3] = (ulong)MemorySize;
+ inputBlock[4] = (ulong)Iterations;
+ inputBlock[5] = (ulong)Type;
+
+ for (var i = 0; i < segmentLength; i++)
+ {
+ var ival = i % 128;
+ if (ival == 0)
+ {
+ inputBlock[6]++;
+ tmpBlock.Set(0);
+ addressBlock.Set(0);
+
+ Compress(tmpBlock, inputBlock, _zeroBlock);
+ Compress(addressBlock, tmpBlock, _zeroBlock);
+ }
+
+ rands[i] = addressBlock[ival];
+ }
+
+ return new PseudoRands(rands);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/Argon2id.cs b/Source/GMPPKConverter/Argon2KDF/Argon2id.cs
new file mode 100644
index 0000000..3d7b70c
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Argon2id.cs
@@ -0,0 +1,23 @@
+namespace Konscious.Security.Cryptography
+{
+ ///
+ /// An implementation of Argon2 https://github.com/P-H-C/phc-winner-argon2
+ ///
+ public class Argon2id : Argon2
+ {
+ ///
+ /// Create an Argon2 for encrypting the given password using Argon2id
+ ///
+ ///
+ public Argon2id(byte[] password)
+ : base(password)
+ {
+ }
+
+ internal override Argon2Core BuildCore(int bc)
+ {
+ return new Argon2idCore(bc);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/Argon2idCore.cs b/Source/GMPPKConverter/Argon2KDF/Argon2idCore.cs
new file mode 100644
index 0000000..bf9ffde
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Argon2idCore.cs
@@ -0,0 +1,29 @@
+namespace Konscious.Security.Cryptography
+{
+ ///
+ /// The implementation of Argon2d for use in the crypto library
+ ///
+ internal class Argon2idCore : Argon2iCore
+ {
+ private const uint ARGON2_SYNC_POINTS = 4;
+
+ public Argon2idCore(int hashSize) :
+ base(hashSize)
+ { }
+
+ public override int Type
+ {
+ get
+ {
+ return 2;
+ }
+ }
+
+ internal override IArgon2PseudoRands GenerateState(Argon2Lane[] lanes, int segmentLength, int pass, int lane, int slice)
+ {
+ if ((pass == 0) && (slice < (ARGON2_SYNC_POINTS / 2)))
+ return base.GenerateState(lanes, segmentLength, pass, lane, slice);
+ return new Argon2dCore.PseudoRands(lanes);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/Blake2Constants.cs b/Source/GMPPKConverter/Argon2KDF/Blake2Constants.cs
new file mode 100644
index 0000000..59e6ff0
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Blake2Constants.cs
@@ -0,0 +1,32 @@
+namespace Konscious.Security.Cryptography
+{
+ internal static class Blake2Constants
+ {
+ public static readonly ulong[] IV = {
+ 0x6A09E667F3BCC908UL,
+ 0xBB67AE8584CAA73BUL,
+ 0x3C6EF372FE94F82BUL,
+ 0xA54FF53A5F1D36F1UL,
+ 0x510E527FADE682D1UL,
+ 0x9B05688C2B3E6C1FUL,
+ 0x1F83D9ABFB41BD6BUL,
+ 0x5BE0CD19137E2179UL
+ };
+
+
+ public static readonly int[][] Sigma = new int[][] {
+ new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
+ new int[] { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 },
+ new int[] { 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4 },
+ new int[] { 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8 },
+ new int[] { 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13 },
+ new int[] { 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9 },
+ new int[] { 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11 },
+ new int[] { 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10 },
+ new int[] { 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5 },
+ new int[] { 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0 },
+ new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
+ new int[] { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 },
+ };
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/Blake2bBase.cs b/Source/GMPPKConverter/Argon2KDF/Blake2bBase.cs
new file mode 100644
index 0000000..77f9711
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Blake2bBase.cs
@@ -0,0 +1,123 @@
+namespace Konscious.Security.Cryptography
+{
+ using System;
+
+ internal abstract class Blake2bBase
+ {
+ public Blake2bBase(int hashBytes)
+ {
+ _hashSize = (uint)hashBytes;
+ }
+
+ public int ByteSize
+ {
+ get
+ {
+ return (int)_hashSize;
+ }
+ }
+
+ public void Initialize(byte[] key)
+ {
+ if ((key?.Length ?? 0) > _b.Length)
+ throw new ArgumentException($"Blake2 key size is too large. Max size is {_b.Length} bytes", nameof(key));
+
+ Array.Copy(Blake2Constants.IV, _h, 8);
+ _h[0] ^= 0x01010000UL ^ (((ulong)(key?.Length ?? 0)) << 8) ^ _hashSize;
+
+ // start with the key
+ if (key?.Length > 0)
+ {
+ Array.Copy(key, _b, key.Length);
+ Update(_b, 0, _b.Length);
+ }
+ }
+
+ public void Update(byte[] data, int offset, int size)
+ {
+ while (size > 0)
+ {
+ if (_c == 128)
+ {
+ _t[0] += (ulong)_c;
+ if (_t[0] < (ulong)_c)
+ ++_t[1];
+
+ // we filled our buffer
+ this.Compress(false);
+ _c = 0;
+ }
+
+ int nextChunk = Math.Min(size, 128 - _c);
+
+ // copy the next batch of data
+ Array.Copy(data, offset, _b, _c, nextChunk);
+ _c += nextChunk;
+ offset += nextChunk;
+
+ size -= nextChunk;
+ }
+ }
+
+ public byte[] Final()
+ {
+ _t[0] += (ulong)_c;
+ if (_t[0] < (ulong)_c)
+ ++_t[1];
+
+ while (_c < 128)
+ _b[_c++] = 0;
+ _c = 0;
+
+ this.Compress(true);
+ var hashByteSize = _hashSize;
+ byte[] result = new byte[hashByteSize];
+ for (var i = 0; i < hashByteSize; ++i)
+ {
+ result[i] = (byte)((_h[i >> 3] >> (8 * (i & 7))) & 0xff);
+ }
+
+ return result;
+ }
+
+ public abstract void Compress(bool isFinal);
+
+ protected ulong[] Hash
+ {
+ get
+ {
+ return _h;
+ }
+ }
+
+ protected ulong TotalSegmentsLow
+ {
+ get
+ {
+ return _t[0];
+ }
+ }
+
+ protected ulong TotalSegmentsHigh
+ {
+ get
+ {
+ return _t[1];
+ }
+ }
+
+ protected byte[] DataBuffer
+ {
+ get
+ {
+ return _b;
+ }
+ }
+
+ private ulong[] _h = new ulong[8];
+ private ulong[] _t = new ulong[2];
+ private byte[] _b = new byte[128];
+ private int _c;
+ private uint _hashSize;
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/Blake2bNormal.cs b/Source/GMPPKConverter/Argon2KDF/Blake2bNormal.cs
new file mode 100644
index 0000000..e42cb90
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Blake2bNormal.cs
@@ -0,0 +1,151 @@
+namespace Konscious.Security.Cryptography
+{
+ internal class Blake2bNormal : Blake2bBase
+ {
+ public Blake2bNormal(int hashBytes)
+ : base(hashBytes)
+ {
+ }
+
+ public override void Compress(bool isFinal)
+ {
+ ulong[] v = new ulong[16];
+ ulong[] m = new ulong[16];
+
+ for (var i = 0; i < 8; ++i)
+ v[i] = Hash[i];
+ for (var i = 0; i < 8; ++i)
+ v[i + 8] = Blake2Constants.IV[i];
+
+ v[12] ^= TotalSegmentsLow;
+ v[13] ^= TotalSegmentsHigh;
+
+ if (isFinal)
+ v[14] = ~v[14];
+
+ for (var i = 0; i < 16; ++i)
+ {
+ int DataBufferOffset = 8 * i;
+
+ m[i] = ((ulong)DataBuffer[DataBufferOffset]) ^
+ (((ulong)DataBuffer[DataBufferOffset + 1]) << 8) ^
+ (((ulong)DataBuffer[DataBufferOffset + 2]) << 16) ^
+ (((ulong)DataBuffer[DataBufferOffset + 3]) << 24) ^
+ (((ulong)DataBuffer[DataBufferOffset + 4]) << 32) ^
+ (((ulong)DataBuffer[DataBufferOffset + 5]) << 40) ^
+ (((ulong)DataBuffer[DataBufferOffset + 6]) << 48) ^
+ (((ulong)DataBuffer[DataBufferOffset + 7]) << 56);
+ }
+
+ for (var i = 0; i < 12; ++i)
+ {
+ v[0] = v[0] + v[4] + m[Blake2Constants.Sigma[i][0]];
+ var temp = v[12] ^ v[0];
+ v[12] = (temp >> 32) ^ (temp << 32);
+ v[8] = v[8] + v[12];
+ temp = v[4] ^ v[8];
+ v[4] = (temp >> 24) ^ (temp << 40);
+ v[0] = v[0] + v[4] + m[Blake2Constants.Sigma[i][1]];
+ temp = v[12] ^ v[0];
+ v[12] = (temp >> 16) ^ (temp << 48);
+ v[8] = v[8] + v[12];
+ temp = v[4] ^ v[8];
+ v[4] = (temp >> 63) ^ (temp << 1);
+
+ v[1] = v[1] + v[5] + m[Blake2Constants.Sigma[i][2]];
+ temp = v[13] ^ v[1];
+ v[13] = (temp >> 32) ^ (temp << 32);
+ v[9] = v[9] + v[13];
+ temp = v[5] ^ v[9];
+ v[5] = (temp >> 24) ^ (temp << 40);
+ v[1] = v[1] + v[5] + m[Blake2Constants.Sigma[i][3]];
+ temp = v[13] ^ v[1];
+ v[13] = (temp >> 16) ^ (temp << 48);
+ v[9] = v[9] + v[13];
+ temp = v[5] ^ v[9];
+ v[5] = (temp >> 63) ^ (temp << 1);
+
+ v[2] = v[2] + v[6] + m[Blake2Constants.Sigma[i][4]];
+ temp = v[14] ^ v[2];
+ v[14] = (temp >> 32) ^ (temp << 32);
+ v[10] = v[10] + v[14];
+ temp = v[6] ^ v[10];
+ v[6] = (temp >> 24) ^ (temp << 40);
+ v[2] = v[2] + v[6] + m[Blake2Constants.Sigma[i][5]];
+ temp = v[14] ^ v[2];
+ v[14] = (temp >> 16) ^ (temp << 48);
+ v[10] = v[10] + v[14];
+ temp = v[6] ^ v[10];
+ v[6] = (temp >> 63) ^ (temp << 1);
+
+ v[3] = v[3] + v[7] + m[Blake2Constants.Sigma[i][6]];
+ temp = v[15] ^ v[3];
+ v[15] = (temp >> 32) ^ (temp << 32);
+ v[11] = v[11] + v[15];
+ temp = v[7] ^ v[11];
+ v[7] = (temp >> 24) ^ (temp << 40);
+ v[3] = v[3] + v[7] + m[Blake2Constants.Sigma[i][7]];
+ temp = v[15] ^ v[3];
+ v[15] = (temp >> 16) ^ (temp << 48);
+ v[11] = v[11] + v[15];
+ temp = v[7] ^ v[11];
+ v[7] = (temp >> 63) ^ (temp << 1);
+
+ v[0] = v[0] + v[5] + m[Blake2Constants.Sigma[i][8]];
+ temp = v[15] ^ v[0];
+ v[15] = (temp >> 32) ^ (temp << 32);
+ v[10] = v[10] + v[15];
+ temp = v[5] ^ v[10];
+ v[5] = (temp >> 24) ^ (temp << 40);
+ v[0] = v[0] + v[5] + m[Blake2Constants.Sigma[i][9]];
+ temp = v[15] ^ v[0];
+ v[15] = (temp >> 16) ^ (temp << 48);
+ v[10] = v[10] + v[15];
+ temp = v[5] ^ v[10];
+ v[5] = (temp >> 63) ^ (temp << 1);
+
+ v[1] = v[1] + v[6] + m[Blake2Constants.Sigma[i][10]];
+ temp = v[12] ^ v[1];
+ v[12] = (temp >> 32) ^ (temp << 32);
+ v[11] = v[11] + v[12];
+ temp = v[6] ^ v[11];
+ v[6] = (temp >> 24) ^ (temp << 40);
+ v[1] = v[1] + v[6] + m[Blake2Constants.Sigma[i][11]];
+ temp = v[12] ^ v[1];
+ v[12] = (temp >> 16) ^ (temp << 48);
+ v[11] = v[11] + v[12];
+ temp = v[6] ^ v[11];
+ v[6] = (temp >> 63) ^ (temp << 1);
+
+ v[2] = v[2] + v[7] + m[Blake2Constants.Sigma[i][12]];
+ temp = v[13] ^ v[2];
+ v[13] = (temp >> 32) ^ (temp << 32);
+ v[8] = v[8] + v[13];
+ temp = v[7] ^ v[8];
+ v[7] = (temp >> 24) ^ (temp << 40);
+ v[2] = v[2] + v[7] + m[Blake2Constants.Sigma[i][13]];
+ temp = v[13] ^ v[2];
+ v[13] = (temp >> 16) ^ (temp << 48);
+ v[8] = v[8] + v[13];
+ temp = v[7] ^ v[8];
+ v[7] = (temp >> 63) ^ (temp << 1);
+
+ v[3] = v[3] + v[4] + m[Blake2Constants.Sigma[i][14]];
+ temp = v[14] ^ v[3];
+ v[14] = (temp >> 32) ^ (temp << 32);
+ v[9] = v[9] + v[14];
+ temp = v[4] ^ v[9];
+ v[4] = (temp >> 24) ^ (temp << 40);
+ v[3] = v[3] + v[4] + m[Blake2Constants.Sigma[i][15]];
+ temp = v[14] ^ v[3];
+ v[14] = (temp >> 16) ^ (temp << 48);
+ v[9] = v[9] + v[14];
+ temp = v[4] ^ v[9];
+ v[4] = (temp >> 63) ^ (temp << 1);
+ }
+
+ for (var i = 0; i < 8; ++i)
+ Hash[i] ^= v[i] ^ v[i + 8];
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/Blake2bSimd.cs b/Source/GMPPKConverter/Argon2KDF/Blake2bSimd.cs
new file mode 100644
index 0000000..cfb6101
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/Blake2bSimd.cs
@@ -0,0 +1,193 @@
+namespace Konscious.Security.Cryptography
+{
+ using System;
+
+ internal class Blake2bSimd : Blake2bBase
+ {
+ public Blake2bSimd(int HashBytes)
+ : base(HashBytes)
+ {
+ }
+
+ public unsafe override void Compress(bool isFinal)
+ {
+ unchecked
+ {
+ ulong* v = stackalloc ulong[16];
+ ulong* m = stackalloc ulong[16];
+
+ fixed (ulong *hash = &Hash[0])
+ {
+ for (var i = 0; i < 8; i++)
+ v[i] = hash[i];
+ }
+
+ fixed (ulong *iv = &Blake2Constants.IV[0])
+ {
+ for (var i = 0; i < 8; i++)
+ v[i + 8] = Blake2Constants.IV[i];
+ }
+
+ v[12] ^= TotalSegmentsLow;
+ v[13] ^= TotalSegmentsHigh;
+
+ if (isFinal)
+ v[14] = ~v[14];
+
+ fixed (byte *dataBuffer = &DataBuffer[0])
+ {
+ ulong *buffer = (ulong*)dataBuffer;
+ for (var i = 0; i < 16; i++)
+ {
+ m[i] = buffer[i];
+ }
+
+ // this is necessary for proper function
+ // but definitely not ideal
+ if (!BitConverter.IsLittleEndian)
+ {
+ for (var i = 0; i < 16; i++)
+ {
+ m[i] = (m[i] >> 56) ^
+ ((m[i] >> 40) & 0xff00UL) ^
+ ((m[i] >> 24) & 0xff0000UL) ^
+ ((m[i] >> 8) & 0xff000000UL) ^
+ ((m[i] << 8) & 0xff00000000UL) ^
+ ((m[i] << 24) & 0xff0000000000UL) ^
+ ((m[i] << 40) & 0xff000000000000UL) ^
+ ((m[i] << 56) & 0xff00000000000000UL);
+ }
+ }
+ }
+
+ for (var i = 0; i < 12; ++i)
+ {
+ ulong *sigmaodd = stackalloc ulong[4];
+ sigmaodd[0] = m[Blake2Constants.Sigma[i][0]];
+ sigmaodd[1] = m[Blake2Constants.Sigma[i][2]];
+ sigmaodd[2] = m[Blake2Constants.Sigma[i][4]];
+ sigmaodd[3] = m[Blake2Constants.Sigma[i][6]];
+
+ ulong *u = &v[4];
+ // these guys should get JIT optimized into SIMD instructions
+ for (var x = 0; x < 4; x++)
+ v[x] += u[x];
+ for (var x = 0; x < 4; x++)
+ v[x] += sigmaodd[x];
+
+ // TODO keep optimizing
+ var temp = v[12] ^ v[0];
+ v[12] = (temp >> 32) ^ (temp << 32);
+ v[8] = v[8] + v[12];
+ temp = v[4] ^ v[8];
+ v[4] = (temp >> 24) ^ (temp << 40);
+ v[0] = v[0] + v[4] + m[Blake2Constants.Sigma[i][1]];
+ temp = v[12] ^ v[0];
+ v[12] = (temp >> 16) ^ (temp << 48);
+ v[8] = v[8] + v[12];
+ temp = v[4] ^ v[8];
+ v[4] = (temp >> 63) ^ (temp << 1);
+
+ temp = v[13] ^ v[1];
+ v[13] = (temp >> 32) ^ (temp << 32);
+ v[9] = v[9] + v[13];
+ temp = v[5] ^ v[9];
+ v[5] = (temp >> 24) ^ (temp << 40);
+ v[1] = v[1] + v[5] + m[Blake2Constants.Sigma[i][3]];
+ temp = v[13] ^ v[1];
+ v[13] = (temp >> 16) ^ (temp << 48);
+ v[9] = v[9] + v[13];
+ temp = v[5] ^ v[9];
+ v[5] = (temp >> 63) ^ (temp << 1);
+
+ temp = v[14] ^ v[2];
+ v[14] = (temp >> 32) ^ (temp << 32);
+ v[10] = v[10] + v[14];
+ temp = v[6] ^ v[10];
+ v[6] = (temp >> 24) ^ (temp << 40);
+ v[2] = v[2] + v[6] + m[Blake2Constants.Sigma[i][5]];
+ temp = v[14] ^ v[2];
+ v[14] = (temp >> 16) ^ (temp << 48);
+ v[10] = v[10] + v[14];
+ temp = v[6] ^ v[10];
+ v[6] = (temp >> 63) ^ (temp << 1);
+
+ temp = v[15] ^ v[3];
+ v[15] = (temp >> 32) ^ (temp << 32);
+ v[11] = v[11] + v[15];
+ temp = v[7] ^ v[11];
+ v[7] = (temp >> 24) ^ (temp << 40);
+ v[3] = v[3] + v[7] + m[Blake2Constants.Sigma[i][7]];
+ temp = v[15] ^ v[3];
+ v[15] = (temp >> 16) ^ (temp << 48);
+ v[11] = v[11] + v[15];
+ temp = v[7] ^ v[11];
+ v[7] = (temp >> 63) ^ (temp << 1);
+
+ sigmaodd[0] = m[Blake2Constants.Sigma[i][8]];
+ sigmaodd[1] = m[Blake2Constants.Sigma[i][10]];
+ sigmaodd[2] = m[Blake2Constants.Sigma[i][12]];
+
+ // "
+ u = &v[5];
+ for (var x = 0; x < 3; x++)
+ v[x] += u[x];
+ for (var x = 0; x < 3; x++)
+ v[x] += sigmaodd[x];
+
+ temp = v[15] ^ v[0];
+ v[15] = (temp >> 32) ^ (temp << 32);
+ v[10] = v[10] + v[15];
+ temp = v[5] ^ v[10];
+ v[5] = (temp >> 24) ^ (temp << 40);
+ v[0] = v[0] + v[5] + m[Blake2Constants.Sigma[i][9]];
+ temp = v[15] ^ v[0];
+ v[15] = (temp >> 16) ^ (temp << 48);
+ v[10] = v[10] + v[15];
+ temp = v[5] ^ v[10];
+ v[5] = (temp >> 63) ^ (temp << 1);
+
+ temp = v[12] ^ v[1];
+ v[12] = (temp >> 32) ^ (temp << 32);
+ v[11] = v[11] + v[12];
+ temp = v[6] ^ v[11];
+ v[6] = (temp >> 24) ^ (temp << 40);
+ v[1] = v[1] + v[6] + m[Blake2Constants.Sigma[i][11]];
+ temp = v[12] ^ v[1];
+ v[12] = (temp >> 16) ^ (temp << 48);
+ v[11] = v[11] + v[12];
+ temp = v[6] ^ v[11];
+ v[6] = (temp >> 63) ^ (temp << 1);
+
+ temp = v[13] ^ v[2];
+ v[13] = (temp >> 32) ^ (temp << 32);
+ v[8] = v[8] + v[13];
+ temp = v[7] ^ v[8];
+ v[7] = (temp >> 24) ^ (temp << 40);
+ v[2] = v[2] + v[7] + m[Blake2Constants.Sigma[i][13]];
+ temp = v[13] ^ v[2];
+ v[13] = (temp >> 16) ^ (temp << 48);
+ v[8] = v[8] + v[13];
+ temp = v[7] ^ v[8];
+ v[7] = (temp >> 63) ^ (temp << 1);
+
+ v[3] = v[3] + v[4] + m[Blake2Constants.Sigma[i][14]];
+ temp = v[14] ^ v[3];
+ v[14] = (temp >> 32) ^ (temp << 32);
+ v[9] = v[9] + v[14];
+ temp = v[4] ^ v[9];
+ v[4] = (temp >> 24) ^ (temp << 40);
+ v[3] = v[3] + v[4] + m[Blake2Constants.Sigma[i][15]];
+ temp = v[14] ^ v[3];
+ v[14] = (temp >> 16) ^ (temp << 48);
+ v[9] = v[9] + v[14];
+ temp = v[4] ^ v[9];
+ v[4] = (temp >> 63) ^ (temp << 1);
+ }
+
+ for (var i = 0; i < 8; ++i)
+ Hash[i] ^= v[i] ^ v[i + 8];
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/HMACBlake2B.cs b/Source/GMPPKConverter/Argon2KDF/HMACBlake2B.cs
new file mode 100644
index 0000000..2693db1
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/HMACBlake2B.cs
@@ -0,0 +1,140 @@
+namespace Konscious.Security.Cryptography
+{
+ using System;
+ using System.Numerics;
+ using System.Security.Cryptography;
+
+ ///
+ /// An implementation of Blake2b HMAC per RFC-7693
+ ///
+ public class HMACBlake2B : HMAC
+ {
+ ///
+ /// Construct an HMACBlake2B without a key
+ ///
+ /// the hash size in bits
+ public HMACBlake2B(int hashSize)
+ {
+ HashName = "Konscious.Security.Cryptography.HMACBlake2B";
+
+ if ((hashSize % 8) > 0)
+ {
+ throw new ArgumentException("Hash Size must be byte aligned", nameof(hashSize));
+ }
+
+ if (hashSize < 8 || hashSize > 512)
+ {
+ throw new ArgumentException("Hash Size must be between 8 and 512", nameof(hashSize));
+ }
+
+ _hashSize = hashSize;
+ _createImpl = CreateImplementation;
+ Key = Array.Empty();
+ }
+
+ ///
+ /// Construct an HMACBlake2B
+ ///
+ /// The key for the HMAC
+ /// The hash size in bits
+ public HMACBlake2B(byte[] keyData, int hashSize)
+ : this(hashSize)
+ {
+ if (keyData == null)
+ {
+ keyData = Array.Empty();
+ }
+
+ if (keyData.Length > 64)
+ {
+ throw new ArgumentException("Key needs to be between 0 and 64 bytes", nameof(keyData));
+ }
+
+ Key = keyData;
+ }
+
+ internal HMACBlake2B(byte[] keyData, int hashSize, Func baseCreator)
+ : this(keyData, hashSize)
+ {
+ _createImpl = baseCreator;
+ }
+
+ ///
+ /// Implementation of HashSize
+ ///
+ /// The hash
+ public override int HashSize
+ {
+ get
+ {
+ return (int)_hashSize;
+ }
+ }
+
+ ///
+ /// Overridden key to enforce size
+ ///
+ public override byte[] Key
+ {
+ get
+ {
+#if NET451
+ if (KeyValue == null)
+ return null;
+#endif
+ return base.Key;
+ }
+
+ set
+ {
+ base.Key = value;
+ }
+ }
+
+ ///
+ /// Implementation of Initialize - initializes the HMAC buffer
+ ///
+ public override void Initialize()
+ {
+ _implementation = CreateImplementation();
+ _implementation.Initialize(Key);
+ }
+
+ ///
+ /// Implementation of HashCore
+ ///
+ /// The data to hash
+ /// The offset to start hashing from
+ /// The amount of data in the hash to consume
+ protected override void HashCore(byte[] data, int offset, int size)
+ {
+ if (_implementation == null)
+ Initialize();
+
+ _implementation.Update(data, offset, size);
+ }
+
+ ///
+ /// Finish hashing and return the final hash
+ ///
+ /// The final hash from HashCore
+ protected override byte[] HashFinal()
+ {
+ return _implementation.Final();
+ }
+
+ private Blake2bBase CreateImplementation()
+ {
+ // Dirty fix for absence of System.Numerics.Vectors in net standart2.0
+ // I need no speed so let it be no accelerated
+ //if (Vector.IsHardwareAccelerated)
+ // return new Blake2bSimd(_hashSize / 8);
+
+ return new Blake2bNormal(_hashSize / 8);
+ }
+
+ Blake2bBase _implementation;
+ int _hashSize;
+ private Func _createImpl;
+ }
+}
diff --git a/Source/GMPPKConverter/Argon2KDF/IArgon2PseudoRands.cs b/Source/GMPPKConverter/Argon2KDF/IArgon2PseudoRands.cs
new file mode 100644
index 0000000..9ea7f3e
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/IArgon2PseudoRands.cs
@@ -0,0 +1,7 @@
+namespace Konscious.Security.Cryptography
+{
+ internal interface IArgon2PseudoRands
+ {
+ ulong PseudoRand(int segment, int prevLane, int prevOffset);
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/LittleEndianActiveStream.cs b/Source/GMPPKConverter/Argon2KDF/LittleEndianActiveStream.cs
new file mode 100644
index 0000000..19f683c
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/LittleEndianActiveStream.cs
@@ -0,0 +1,239 @@
+namespace Konscious.Security.Cryptography
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+
+ // this could be refactored to support either endianness
+ // but I only need little endian for this
+ internal class LittleEndianActiveStream : Stream
+ {
+ public LittleEndianActiveStream(byte[] buffer = null)
+ {
+ _bufferSetupActions = new LinkedList();
+ _buffer = buffer;
+ _bufferAvailable = _buffer?.Length ?? 0;
+ }
+
+ public void Expose(short data)
+ {
+ _bufferSetupActions.AddLast(() => BufferShort((ushort)data));
+ }
+
+ public void Expose(ushort data)
+ {
+ _bufferSetupActions.AddLast(() => BufferShort(data));
+ }
+
+ public void Expose(int data)
+ {
+ _bufferSetupActions.AddLast(() => BufferInt((uint)data));
+ }
+
+ public void Expose(uint data)
+ {
+ _bufferSetupActions.AddLast(() => BufferInt(data));
+ }
+
+ public void Expose(byte data)
+ {
+ _bufferSetupActions.AddLast(() => BufferByte(data));
+ }
+
+ public void Expose(byte[] data)
+ {
+ if (data != null)
+ {
+ _bufferSetupActions.AddLast(() => BufferArray(data, 0, data.Length));
+ }
+ }
+
+ public void Expose(Stream subStream)
+ {
+ if (subStream != null)
+ {
+ _bufferSetupActions.AddLast(() => BufferSubStream(subStream));
+ }
+ }
+
+ public void Expose(Argon2Memory memory)
+ {
+ if (memory != null)
+ {
+ Expose(new Argon2Memory.Stream(memory));
+ }
+ }
+
+ public void ClearBuffer()
+ {
+ for (int i = 0; i < _buffer.Length; ++i)
+ {
+ _buffer[i] = 0;
+ }
+
+ _bufferAvailable = 0;
+ }
+
+ public override void Flush()
+ {
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ int totalRead = 0;
+ while (totalRead < count)
+ {
+ int available = _bufferAvailable - _bufferOffset;
+ if (available == 0)
+ {
+ if (_bufferSetupActions.Count == 0)
+ {
+ // there's nothing left to queue up - we read what we could
+ return totalRead;
+ }
+
+ _bufferSetupActions.First.Value();
+ _bufferSetupActions.RemoveFirst();
+
+ // we are safe to assume that offset becomes 0 after that call
+ available = _bufferAvailable;
+ }
+
+ // if we only need to read part of available - reduce that
+ available = Math.Min(available, count - totalRead);
+ Array.Copy(_buffer, _bufferOffset, buffer, offset, available);
+
+ _bufferOffset += available;
+ offset += available;
+ totalRead += available;
+ }
+
+ return totalRead;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException("LittleEndianActiveStream is non-seekable");
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException("LittleEndianActiveStream is an actual Stream that doesn't support length");
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ _bufferSetupActions.AddLast(() => BufferArray(buffer, offset, count));
+ }
+
+ private void BufferSubStream(Stream stream)
+ {
+ ReserveBuffer(1024);
+ var result = stream.Read(_buffer, 0, 1024);
+ if (result == 1024)
+ {
+ _bufferSetupActions.AddFirst(() => BufferSubStream(stream));
+ }
+ else
+ {
+ stream.Dispose();
+ }
+
+ _bufferAvailable = result;
+ }
+
+ private void BufferByte(byte value)
+ {
+ ReserveBuffer(1);
+ _buffer[0] = value;
+ }
+
+ private void BufferArray(byte[] value, int offset, int length)
+ {
+ ReserveBuffer(value.Length);
+ Array.Copy(value, offset, _buffer, 0, length);
+ }
+
+ private void BufferShort(ushort value)
+ {
+ ReserveBuffer(sizeof(ushort));
+ _buffer[0] = (byte)value;
+ _buffer[1] = (byte)(value >> 8);
+ }
+
+ private void BufferInt(uint value)
+ {
+ ReserveBuffer(sizeof(uint));
+ _buffer[0] = (byte)value;
+ _buffer[1] = (byte)(value >> 8);
+ _buffer[2] = (byte)(value >> 16);
+ _buffer[3] = (byte)(value >> 24);
+ }
+
+ private void ReserveBuffer(int size)
+ {
+ if (_buffer == null)
+ {
+ _buffer = new byte[size];
+ }
+ else if (_buffer.Length < size)
+ {
+ Array.Resize(ref _buffer, size);
+ }
+
+ _bufferOffset = 0;
+ _bufferAvailable = size;
+ }
+
+ private LinkedList _bufferSetupActions;
+
+ private byte[] _buffer;
+ private int _bufferOffset;
+ private int _bufferAvailable;
+
+ public override bool CanRead
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override bool CanSeek
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override bool CanWrite
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override long Length
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override long Position
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+
+ set
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2KDF/ModifiedBlake2.cs b/Source/GMPPKConverter/Argon2KDF/ModifiedBlake2.cs
new file mode 100644
index 0000000..1317d4d
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2KDF/ModifiedBlake2.cs
@@ -0,0 +1,110 @@
+namespace Konscious.Security.Cryptography
+{
+ internal static class ModifiedBlake2
+ {
+
+ private static ulong Rotate(ulong x, int y)
+ {
+ return (((x) >> (y)) ^ ((x) << (64 - (y))));
+ }
+
+ private unsafe static void ModifiedG(ulong *v, int a, int b, int c, int d)
+ {
+ var t = (v[a] & 0xffffffff) * (v[b] & 0xffffffff);
+ v[a] = v[a] + v[b] + 2 * t;
+
+ v[d] = Rotate(v[d] ^ v[a], 32);
+
+ t = (v[c] & 0xffffffff) * (v[d] & 0xffffffff);
+ v[c] = v[c] + v[d] + 2 * t;
+
+ v[b] = Rotate(v[b] ^ v[c], 24);
+
+ t = (v[a] & 0xffffffff) * (v[b] & 0xffffffff);
+ v[a] = v[a] + v[b] + 2 * t;
+
+
+ v[d] = Rotate(v[d] ^ v[a], 16);
+
+ t = (v[c] & 0xffffffff) * (v[d] & 0xffffffff);
+ v[c] = v[c] + v[d] + 2 * t;
+
+ v[b] = Rotate(v[b] ^ v[c], 63);
+ }
+
+ public unsafe static void DoRoundColumns(ulong *v, int i)
+ {
+ i *= 16;
+ ModifiedG(v, i, i + 4, i + 8, i + 12);
+ ModifiedG(v, i + 1, i + 5, i + 9, i + 13);
+ ModifiedG(v, i + 2, i + 6, i + 10, i + 14);
+ ModifiedG(v, i + 3, i + 7, i + 11, i + 15);
+ ModifiedG(v, i, i + 5, i + 10, i + 15);
+ ModifiedG(v, i + 1, i + 6, i + 11, i + 12);
+ ModifiedG(v, i + 2, i + 7, i + 8, i + 13);
+ ModifiedG(v, i + 3, i + 4, i + 9, i + 14);
+ }
+
+
+ public unsafe static void DoRoundRows(ulong *v, int i)
+ {
+ i *= 2;
+ ModifiedG(v, i, i + 32, i + 64, i + 96);
+ ModifiedG(v, i + 1, i + 33, i + 65, i + 97);
+ ModifiedG(v, i + 16, i + 48, i + 80, i + 112);
+ ModifiedG(v, i + 17, i + 49, i + 81, i + 113);
+ ModifiedG(v, i, i + 33, i + 80, i + 113);
+ ModifiedG(v, i + 1, i + 48, i + 81, i + 96);
+ ModifiedG(v, i + 16, i + 49, i + 64, i + 97);
+ ModifiedG(v, i + 17, i + 32, i + 65, i + 112);
+ }
+
+ public static void Blake2Prime(Argon2Memory memory, LittleEndianActiveStream dataStream, int size = -1)
+ {
+ var hashStream = new LittleEndianActiveStream();
+
+ if (size < 0 || size > (memory.Length * 8))
+ {
+ size = memory.Length * 8;
+ }
+
+ hashStream.Expose(size);
+ hashStream.Expose(dataStream);
+
+
+ if (size <= 64)
+ {
+ var blake2 = new HMACBlake2B(8 * size);
+ blake2.Initialize();
+
+ memory.Blit(blake2.ComputeHash(hashStream), 0, 0, size);
+ }
+ else
+ {
+ var blake2 = new HMACBlake2B(512);
+ blake2.Initialize();
+
+ int offset = 0;
+ var chunk = blake2.ComputeHash(hashStream);
+
+ memory.Blit(chunk, offset, 0, 32); // copy half of the chunk
+ offset += 4;
+ size -= 32;
+
+ while (size > 64)
+ {
+ blake2.Initialize();
+ chunk = blake2.ComputeHash(chunk);
+ memory.Blit(chunk, offset, 0, 32); // half again
+
+ offset += 4;
+ size -= 32;
+ }
+
+ blake2 = new HMACBlake2B(size * 8);
+ blake2.Initialize();
+ memory.Blit(blake2.ComputeHash(chunk), offset, 0, size); // copy the rest
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/Argon2Params.cs b/Source/GMPPKConverter/Argon2Params.cs
new file mode 100644
index 0000000..a9a4ebe
--- /dev/null
+++ b/Source/GMPPKConverter/Argon2Params.cs
@@ -0,0 +1,17 @@
+namespace GMax.Security
+{
+ public enum Argon2Type
+ {
+ Argon2i,
+ Argon2d,
+ Argon2id,
+ }
+ public class Argon2Params
+ {
+ public Argon2Type KeyDerivation { get; set; }
+ public int Memory { get; set; }
+ public int Passes { get; set; }
+ public int Parallelism { get; set; }
+ public byte[] Salt { get; set; }
+ }
+}
diff --git a/Source/GMPPKConverter/AsymmetricKeyHelpers.cs b/Source/GMPPKConverter/AsymmetricKeyHelpers.cs
new file mode 100644
index 0000000..e926f5f
--- /dev/null
+++ b/Source/GMPPKConverter/AsymmetricKeyHelpers.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace GMax.Security
+{
+ internal static class AsymmetricKeyHelpers
+ {
+ internal static byte[] ReadWithLength(BinaryReader reader, bool skipFirstNull = false)
+ {
+ var length = BitConverter.ToInt32(reader.ReadBytes(4).Reverse().ToArray(), 0);
+ byte[] buffer;
+ if (skipFirstNull)
+ {
+ buffer = new byte[length - 1];
+ reader.Read(buffer, 0, 1);
+ reader.Read(buffer, 0, length - 1);
+ }
+ else
+ {
+ buffer = new byte[length];
+ reader.Read(buffer, 0, length);
+ }
+ return buffer;
+ }
+
+ // https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-sequence
+ // https://github.com/dotnet/corefx/blob/07e9caf00ea0f1893d4c25a5ee287000903fbbe2/src/Common/src/System/Security/Cryptography/DerEncoder.cs
+ internal static void WriteASN1Tag(BinaryWriter writer, byte tagId, Action contentWriter)
+ {
+ writer.Write(tagId);
+ using (var ms = new MemoryStream())
+ {
+ using (var bw = new BinaryWriter(ms, Encoding.ASCII, true))
+ {
+ contentWriter(bw);
+ }
+ WriteASN1EncodedLength(writer, (int)ms.Length);
+ writer.Write(ms.ToArray(), 0, (int)ms.Length);
+ }
+ }
+
+ internal static void WriteWithLength(BinaryWriter writer, byte[] bytes, bool addLeadingNull = false)
+ {
+ writer.Write(BitConverter.GetBytes(bytes.Length + (addLeadingNull ? 1 : 0)).Reverse().ToArray());
+ if (addLeadingNull)
+ writer.Write(new byte[] { 0x00 });
+ writer.Write(bytes);
+ }
+
+ internal static byte[] CopyAndReverse(byte[] data)
+ {
+ byte[] reversed = new byte[data.Length];
+ Array.Copy(data, 0, reversed, 0, data.Length);
+ Array.Reverse(reversed);
+ return reversed;
+ }
+ internal static byte[] FixLength(byte[] data)
+ {
+ // remove leading 0, RSA dowsn't like it
+ if (data.Length % 2 == 1)
+ return data.Skip(1).ToArray();
+ else
+ return data;
+ }
+
+ // https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-bit-string
+ internal static void WriteASN1BitString(BinaryWriter writer, byte[] value, byte unusedBits = 0)
+ {
+ writer.Write((byte)0x03); // BIT STRING
+ WriteASN1EncodedLength(writer, value.Length + 1);
+ writer.Write(unusedBits); // unused bits
+ writer.Write(value);
+ }
+
+ // https://stackoverflow.com/a/23739932/2860309
+ internal static void WriteASN1EncodedLength(BinaryWriter writer, int length)
+ {
+ if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative");
+ if (length < 0x80)
+ {
+ // Short form
+ writer.Write((byte)length);
+ }
+ else
+ {
+ // Long form
+ var temp = length;
+ var bytesRequired = 0;
+ while (temp > 0)
+ {
+ temp >>= 8;
+ bytesRequired++;
+ }
+ writer.Write((byte)(bytesRequired | 0x80));
+ for (var i = bytesRequired - 1; i >= 0; i--)
+ {
+ writer.Write((byte)(length >> (8 * i) & 0xff));
+ }
+ }
+ }
+
+ // https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-integer
+ internal static void WriteASN1Integer(BinaryWriter writer, byte[] value, bool forceUnsigned = true)
+ {
+ writer.Write((byte)0x02); // INTEGER
+ var prefixZeros = 0;
+ for (var i = 0; i < value.Length; i++)
+ {
+ if (value[i] != 0) break;
+ prefixZeros++;
+ }
+ if (value.Length - prefixZeros == 0)
+ {
+ WriteASN1EncodedLength(writer, 1);
+ writer.Write((byte)0);
+ }
+ else
+ {
+ if (forceUnsigned && value[prefixZeros] > 0x7f)
+ {
+ // Add a prefix zero to force unsigned if the MSB is 1
+ WriteASN1EncodedLength(writer, value.Length - prefixZeros + 1);
+ writer.Write((byte)0);
+ }
+ else
+ {
+ WriteASN1EncodedLength(writer, value.Length - prefixZeros);
+ }
+ for (var i = prefixZeros; i < value.Length; i++)
+ {
+ writer.Write(value[i]);
+ }
+ }
+ }
+
+ // https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-octet-string
+ internal static void WriteASN1OctetString(BinaryWriter writer, byte[] value)
+ {
+ writer.Write((byte)0x04); // OCTET STRING
+ WriteASN1EncodedLength(writer, value.Length);
+ writer.Write(value);
+ }
+
+ internal static void WriteASN1OidCompiled(BinaryWriter writer, byte[] value)
+ {
+ // now it just as octet string
+ writer.Write((byte)0x06); // OBJECT IDENTIFIER
+ WriteASN1EncodedLength(writer, value.Length);
+ writer.Write(value);
+ }
+
+ // https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier
+ internal static void WriteASN1OidGeneric(BinaryWriter writer, int[] value)
+ {
+ Debug.Assert(value.Length > 2);
+
+ writer.Write((byte)0x06); // OBJECT IDENTIFIER
+
+ using (var ms = new MemoryStream())
+ {
+ using (var bw = new BinaryWriter(ms))
+ {
+ byte v1 = (byte)((value[0] & 127) * 40 + (value[1] & 127));
+ bw.Write(v1);
+ for (int i = 2; i < value.Length; i++)
+ {
+ if (value[i] <= 127)
+ {
+ bw.Write((byte)value[i]);
+ }
+ else
+ {
+ var val = value[i];
+ Stack littleEndianBytes = new Stack();
+ byte continuance = 0;
+ do
+ {
+ int remainder;
+ remainder = val % 128;
+ val /= 128;
+
+ byte octet = (byte)remainder;
+ octet |= continuance;
+ // Any remaining (preceding) bytes need the continuance bit set.
+ continuance = 0x80;
+
+ littleEndianBytes.Push(octet);
+ }
+ while (val != 0);
+ bw.Write(littleEndianBytes.ToArray());
+ }
+ }
+ var bytes = ms.ToArray();
+ WriteASN1EncodedLength(writer, bytes.Length);
+ writer.Write(bytes);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/AsymmetricKeyParams.cs b/Source/GMPPKConverter/AsymmetricKeyParams.cs
new file mode 100644
index 0000000..882551f
--- /dev/null
+++ b/Source/GMPPKConverter/AsymmetricKeyParams.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace GMax.Security
+{
+ internal interface IAsymmetricKeyParams
+ {
+ void ImportKeyParamsFromPPK(byte[] publicData, byte[] privateData);
+ void ExportPublicKeyAsOpenSSH(BinaryWriter bw);
+ void ExportPrivateKeyAsOpenSSH(BinaryWriter bw);
+ void ExportPrivateKeyAsASN1(BinaryWriter bw);
+ }
+ internal abstract class AsymmetricKeyParams : IAsymmetricKeyParams
+ {
+ public string Algorithm { get; set; }
+
+ public AsymmetricKeyParams(string Algorithm)
+ {
+ this.Algorithm = Algorithm;
+ }
+
+ public abstract void ImportKeyParamsFromPPK(byte[] publicData, byte[] privateData);
+ public abstract void ExportPublicKeyAsOpenSSH(BinaryWriter bw);
+ public abstract void ExportPrivateKeyAsOpenSSH(BinaryWriter bw);
+ public abstract void ExportPrivateKeyAsASN1(BinaryWriter bw);
+
+ }
+}
diff --git a/Source/GMPPKConverter/DSAKeyParams.cs b/Source/GMPPKConverter/DSAKeyParams.cs
new file mode 100644
index 0000000..3c3771c
--- /dev/null
+++ b/Source/GMPPKConverter/DSAKeyParams.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace GMax.Security
+{
+ internal class DSAKeyParams : AsymmetricKeyParams
+ {
+ public DSAKeyParams(string Algorithm) : base(Algorithm)
+ {
+ }
+
+ public byte[] X { get; set; }
+ public byte[] Y { get; set; }
+ public byte[] P { get; set; }
+ public byte[] Q { get; set; }
+ public byte[] G { get; set; }
+
+ public override void ExportPrivateKeyAsASN1(BinaryWriter bw)
+ {
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, new byte[] { 0x00 }); // Version
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, P);
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, Q);
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, G);
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, Y);
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, X);
+ }
+
+ public override void ExportPrivateKeyAsOpenSSH(BinaryWriter bw)
+ {
+ AsymmetricKeyHelpers.
+ ////put_mp_ssh2(bs, dsa->x);
+ ////
+ //put_mp_ssh2(bs, dsa->p);
+ //314 put_mp_ssh2(bs, dsa->q);
+ //315 put_mp_ssh2(bs, dsa->g);
+ //316 put_mp_ssh2(bs, dsa->y);
+ //317 put_mp_ssh2(bs, dsa->x);
+ WriteWithLength(bw, Encoding.ASCII.GetBytes(Algorithm));
+ AsymmetricKeyHelpers.WriteWithLength(bw, P);
+ AsymmetricKeyHelpers.WriteWithLength(bw, Q);
+ AsymmetricKeyHelpers.WriteWithLength(bw, G);
+ AsymmetricKeyHelpers.WriteWithLength(bw, Y);
+ AsymmetricKeyHelpers.WriteWithLength(bw, X);
+ }
+
+ public override void ExportPublicKeyAsOpenSSH(BinaryWriter bw)
+ {
+ AsymmetricKeyHelpers.
+ // https://git.tartarus.org/?p=simon/putty.git;a=blob;f=crypto/dsa.c;hb=faf1601a5549eda9298f72f7c0f68f39c8f97764
+ //put_stringz(bs, "ssh-dss");
+ //221 put_mp_ssh2(bs, dsa->p);
+ //222 put_mp_ssh2(bs, dsa->q);
+ //223 put_mp_ssh2(bs, dsa->g);
+ //224 put_mp_ssh2(bs, dsa->y);
+ WriteWithLength(bw, Encoding.ASCII.GetBytes(Algorithm));
+ AsymmetricKeyHelpers.WriteWithLength(bw, P);
+ AsymmetricKeyHelpers.WriteWithLength(bw, Q);
+ AsymmetricKeyHelpers.WriteWithLength(bw, G);
+ AsymmetricKeyHelpers.WriteWithLength(bw, Y);
+ }
+
+ public override void ImportKeyParamsFromPPK(byte[] publicData, byte[] privateData)
+ {
+ //Helpers.Dump("publicData", publicData);
+ //Helpers.Dump("privateData", privateData);
+
+ using (var ms = new MemoryStream(publicData))
+ {
+ using (var br = new BinaryReader(ms))
+ {
+ AsymmetricKeyHelpers.ReadWithLength(br); // alg. name ssh-dss
+ P = AsymmetricKeyHelpers.ReadWithLength(br);
+ Q = AsymmetricKeyHelpers.ReadWithLength(br);
+ G = AsymmetricKeyHelpers.ReadWithLength(br);
+ Y = AsymmetricKeyHelpers.ReadWithLength(br);
+ }
+ }
+
+ using (var ms = new MemoryStream(privateData))
+ {
+ using (var br = new BinaryReader(ms))
+ {
+ X = AsymmetricKeyHelpers.ReadWithLength(br);
+ }
+ }
+ }
+ }
+}
diff --git a/Source/GMPPKConverter/ECDSAKeyParams.cs b/Source/GMPPKConverter/ECDSAKeyParams.cs
new file mode 100644
index 0000000..8c90c63
--- /dev/null
+++ b/Source/GMPPKConverter/ECDSAKeyParams.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace GMax.Security
+{
+ // asn-1
+ // https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-encoded-tag-bytes
+ // https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-bit-string
+ // https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-octet-string
+ internal class ECDSAKeyParams : AsymmetricKeyParams
+ {
+ public ECDSAKeyParams(string Algorithm) : base(Algorithm)
+ {
+ CurveName = Algorithm.Substring(10);
+ }
+
+ public string CurveName { get; set; }
+ public byte[] PrivateKey { get; set; }
+ public byte[] PublicKey { get; set; }
+
+ public override void ExportPrivateKeyAsASN1(BinaryWriter bw)
+ {
+ /*
+ * Structure of asn1:
+ ECPrivateKey ::= SEQUENCE {
+ version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
+ privateKey OCTET STRING,
+ parameters [0]
+ ECParameters {{ NamedCurve }} OPTIONAL,
+ publicKey [1]
+ BIT STRING OPTIONAL
+ }
+ */
+ // https://www.gnupg.org/documentation/manuals/gcrypt/ECC-key-parameters.html
+ //byte[] oid;
+ int[] oid;
+ if (CurveName.Equals("nistp256"))
+ {
+ // 1.2.840.10045.3.1.7
+ //oid = new byte[] { 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07 };
+ oid = new int[] { 1, 2, 840, 10045, 3, 1, 7 };
+ }
+ else if (CurveName.Equals("nistp384"))
+ {
+ // 1.3.132.0.34
+ //oid = new byte[] { 0x2B, 0x81, 0x04, 0x00, 0x22 };
+ oid = new int[] { 1, 3, 132, 0, 34 };
+ }
+ else if (CurveName.Equals("nistp521"))
+ {
+ // 1.3.132.0.35
+ //oid = new byte[] { 0x2B, 0x81, 0x04, 0x00, 0x23 };
+ oid = new int[] { 1, 3, 132, 0, 35 };
+ }
+ else
+ {
+ throw new NotImplementedException("Invalid curve name in export");
+ }
+
+ AsymmetricKeyHelpers.
+
+ //
+ WriteASN1Integer(bw, new byte[] { 0x01 });
+ AsymmetricKeyHelpers.WriteASN1OctetString(bw, PrivateKey);
+ AsymmetricKeyHelpers.WriteASN1Tag(bw, 0xA0, // Tag 0
+ (bw1) => AsymmetricKeyHelpers.WriteASN1OidGeneric(bw1, oid)
+ );
+ AsymmetricKeyHelpers.WriteASN1Tag(bw, 0xA1, // Tag 1
+ (bw1) => AsymmetricKeyHelpers.WriteASN1BitString(bw1, PublicKey)
+ );
+ }
+
+ public override void ExportPrivateKeyAsOpenSSH(BinaryWriter bw)
+ {
+ AsymmetricKeyHelpers.
+
+ ////put_mp_ssh2(bs, ek->privateKey);
+ ////
+ //put_stringz(bs, ek->curve->name);
+ //933 put_wpoint(bs, ek->publicKey, ek->curve, false);
+ //934 put_mp_ssh2(bs, ek->privateKey);
+ WriteWithLength(bw, Encoding.ASCII.GetBytes(Algorithm));
+ AsymmetricKeyHelpers.WriteWithLength(bw, Encoding.ASCII.GetBytes(CurveName));
+ AsymmetricKeyHelpers.WriteWithLength(bw, PublicKey);
+ AsymmetricKeyHelpers.WriteWithLength(bw, PrivateKey);
+ }
+
+ public override void ExportPublicKeyAsOpenSSH(BinaryWriter bw)
+ {
+ AsymmetricKeyHelpers.
+ // https://git.tartarus.org/?p=simon/putty.git;a=blob;f=crypto/ecc-ssh.c;hb=faf1601a5549eda9298f72f7c0f68f39c8f97764
+ //put_stringz(bs, ek->sshk.vt->ssh_id);
+ //769 put_stringz(bs, ek->curve->name);
+ //770 put_wpoint(bs, ek->publicKey, ek->curve, false);
+ WriteWithLength(bw, Encoding.ASCII.GetBytes(Algorithm));
+ AsymmetricKeyHelpers.WriteWithLength(bw, Encoding.ASCII.GetBytes(CurveName));
+ AsymmetricKeyHelpers.WriteWithLength(bw, PublicKey);
+ }
+
+ public override void ImportKeyParamsFromPPK(byte[] publicData, byte[] privateData)
+ {
+ using (var ms = new MemoryStream(publicData))
+ {
+ using (var br = new BinaryReader(ms))
+ {
+ AsymmetricKeyHelpers.ReadWithLength(br); // alg. name ecdsa-sha2-nistp256
+ CurveName = Encoding.ASCII.GetString(AsymmetricKeyHelpers.ReadWithLength(br)); // curveName
+ PublicKey = AsymmetricKeyHelpers.ReadWithLength(br);
+ }
+ }
+
+ using (var ms = new MemoryStream(privateData))
+ {
+ using (var br = new BinaryReader(ms))
+ {
+ PrivateKey = AsymmetricKeyHelpers.ReadWithLength(br);
+ }
+ }
+
+ }
+ }
+}
diff --git a/Source/GMPPKConverter/EDDSAKeyParams.cs b/Source/GMPPKConverter/EDDSAKeyParams.cs
new file mode 100644
index 0000000..974ffae
--- /dev/null
+++ b/Source/GMPPKConverter/EDDSAKeyParams.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace GMax.Security
+{
+ internal class EDDSAKeyParams : AsymmetricKeyParams
+ {
+ public EDDSAKeyParams(string Algorithm) : base(Algorithm)
+ {
+ }
+ public byte[] PrivateKey { get; set; }
+ public byte[] PublicKey { get; set; }
+
+ public override void ExportPrivateKeyAsASN1(BinaryWriter bw)
+ {
+ throw new NotImplementedException("ExportOpenSSH should be used");
+ }
+
+ public override void ExportPrivateKeyAsOpenSSH(BinaryWriter bw)
+ {
+ AsymmetricKeyHelpers.
+ ////put_mp_le_fixedlen(bs, ek->privateKey, ek->curve->fieldBytes);
+ ////
+ //* Encode the public and private points as strings */
+ //883 strbuf* pub_sb = strbuf_new();
+ //884 put_epoint(pub_sb, ek->publicKey, ek->curve, false);
+ //885 ptrlen pub = make_ptrlen(pub_sb->s + 4, pub_sb->len - 4);
+ //886
+ //887 strbuf* priv_sb = strbuf_new_nm();
+ //888 put_mp_le_fixedlen(priv_sb, ek->privateKey, ek->curve->fieldBytes);
+ //889 ptrlen priv = make_ptrlen(priv_sb->s + 4, priv_sb->len - 4);
+ //890
+ //891 put_stringpl(bs, pub);
+ //892
+ //893 /* Encode the private key as the concatenation of the
+ //894 * little-endian key integer and the public key again */
+ //895 put_uint32(bs, priv.len + pub.len);
+ //896 put_datapl(bs, priv);
+ //897 put_datapl(bs, pub);
+ WriteWithLength(bw, Encoding.ASCII.GetBytes(Algorithm));
+ AsymmetricKeyHelpers.WriteWithLength(bw, PublicKey);
+ using (var ms = new MemoryStream())
+ {
+ ms.Write(PrivateKey, 0, PrivateKey.Length);
+ ms.Write(PublicKey, 0, PublicKey.Length);
+ AsymmetricKeyHelpers.
+ WriteWithLength(bw, ms.ToArray());
+ }
+ }
+
+ public override void ExportPublicKeyAsOpenSSH(BinaryWriter bw)
+ {
+ AsymmetricKeyHelpers.
+ // https://git.tartarus.org/?p=simon/putty.git;a=blob;f=crypto/ecc-ssh.c;hb=faf1601a5549eda9298f72f7c0f68f39c8f97764
+ //put_stringz(bs, ek->sshk.vt->ssh_id);
+ //778 put_epoint(bs, ek->publicKey, ek->curve, false);
+ WriteWithLength(bw, Encoding.ASCII.GetBytes(Algorithm));
+ AsymmetricKeyHelpers.WriteWithLength(bw, PublicKey);
+ }
+
+ public override void ImportKeyParamsFromPPK(byte[] publicData, byte[] privateData)
+ {
+ using (var ms = new MemoryStream(publicData))
+ {
+ using (var br = new BinaryReader(ms))
+ {
+ AsymmetricKeyHelpers.ReadWithLength(br); // alg. name ssh-ed25519/ssh-ed448
+ PublicKey = AsymmetricKeyHelpers.ReadWithLength(br);
+ }
+ }
+
+ using (var ms = new MemoryStream(privateData))
+ {
+ using (var br = new BinaryReader(ms))
+ {
+ PrivateKey = AsymmetricKeyHelpers.ReadWithLength(br);
+ }
+ }
+ }
+ }
+}
diff --git a/Source/GMPPKConverter/GMPPKConverter.csproj b/Source/GMPPKConverter/GMPPKConverter.csproj
new file mode 100644
index 0000000..9d3b947
--- /dev/null
+++ b/Source/GMPPKConverter/GMPPKConverter.csproj
@@ -0,0 +1,9 @@
+
+
+
+ Library
+ netstandard2.0
+ True
+
+
+
diff --git a/Source/GMPPKConverter/Helpers.cs b/Source/GMPPKConverter/Helpers.cs
new file mode 100644
index 0000000..4e8ec5f
--- /dev/null
+++ b/Source/GMPPKConverter/Helpers.cs
@@ -0,0 +1,186 @@
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Security;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace GMax.Security
+{
+ internal static class Helpers
+ {
+ internal static byte[] AESDecrypt(byte[] Data, byte[] IV, byte[] keyBytes)
+ {
+ byte[] decrypted;
+
+ using (AesManaged cipher = new AesManaged())
+ {
+ cipher.KeySize = 256;
+ cipher.Mode = CipherMode.CBC;
+ try
+ {
+ cipher.Padding = PaddingMode.None;
+ cipher.Key = keyBytes;
+ cipher.IV = IV;
+ using (ICryptoTransform decryptor = cipher.CreateDecryptor())
+ {
+ decrypted = decryptor.TransformFinalBlock(Data, 0, Data.Length);
+ }
+ }
+ catch // (Exception ex)
+ {
+ //TODO?
+ return new byte[0];
+ }
+ finally
+ {
+ cipher.Clear();
+ }
+ return decrypted;
+ }
+ }
+
+ internal static T ProcessSecureStringAnsi(SecureString src, Func func, byte[] arrayPrefix = null)
+ {
+ IntPtr unmanagedBytes = IntPtr.Zero;
+ byte[] workArray = null;
+ GCHandle? handle = null; // Hats off to Tobias Bauer
+ try
+ {
+ if (src == null)
+ {
+ handle = GCHandle.Alloc(workArray, GCHandleType.Pinned);
+ if (arrayPrefix == null) {
+ return func(new byte[0]);
+ }
+ else
+ {
+ workArray = new byte[arrayPrefix.Length];
+ arrayPrefix.CopyTo(workArray, 0);
+ return func(workArray);
+ }
+ }
+ else
+ {
+ unmanagedBytes = Marshal.SecureStringToGlobalAllocAnsi(src);
+ unsafe
+ {
+ byte* byteArray = (byte*)unmanagedBytes;
+ int startIndex;
+ if (arrayPrefix == null)
+ {
+ workArray = new byte[src.Length];
+ startIndex = 0;
+ }
+ else
+ {
+ workArray = new byte[src.Length + arrayPrefix.Length];
+ arrayPrefix.CopyTo(workArray, 0);
+ startIndex = arrayPrefix.Length;
+ }
+ handle = GCHandle.Alloc(workArray, GCHandleType.Pinned); // Hats off to Tobias Bauer
+ for (int i = startIndex; i < workArray.Length; i++)
+ workArray[i] = *byteArray++;
+ }
+ return func(workArray);
+ }
+ }
+ finally
+ {
+ if (workArray != null)
+ for (int i = 0; i < workArray.Length; i++)
+ workArray[i] = 0;
+ if (unmanagedBytes != IntPtr.Zero)
+ Marshal.ZeroFreeGlobalAllocAnsi(unmanagedBytes);
+ handle?.Free();
+ }
+ }
+
+ // https://stackoverflow.com/questions/18392538/securestring-to-byte-c-sharp
+ // https://social.msdn.microsoft.com/Forums/vstudio/en-US/f6710354-32e3-4486-b866-e102bb495f86/converting-a-securestring-object-to-byte-array-in-net
+ internal static T ProcessSecureStringUni(SecureString src, Func func, byte[] arrayPrefix = null)
+ {
+ IntPtr bstr = IntPtr.Zero;
+ byte[] workArray = null;
+ GCHandle? handle = null; // Hats off to Tobias Bauer
+ try
+ {
+ if (src == null)
+ {
+ handle = GCHandle.Alloc(workArray, GCHandleType.Pinned);
+ workArray = new byte[arrayPrefix.Length];
+ arrayPrefix.CopyTo(workArray, 0);
+ return func(workArray);
+ }
+ else
+ {
+ /*** PLAINTEXT EXPOSURE BEGINS HERE ***/
+ bstr = Marshal.SecureStringToBSTR(src);
+ unsafe
+ {
+ byte* byteArray = (byte*)bstr;
+ int startIndex;
+ if (arrayPrefix == null)
+ {
+ workArray = new byte[src.Length * 2];
+ startIndex = 0;
+ }
+ else
+ {
+ workArray = new byte[src.Length * 2 + arrayPrefix.Length];
+ arrayPrefix.CopyTo(workArray, 0);
+ startIndex = arrayPrefix.Length;
+ }
+ handle = GCHandle.Alloc(workArray, GCHandleType.Pinned); // Hats off to Tobias Bauer
+ for (int i = startIndex; i < workArray.Length; i++)
+ workArray[i] = *byteArray++;
+ }
+ return func(workArray);
+ }
+ }
+ finally
+ {
+ if (workArray != null)
+ for (int i = 0; i < workArray.Length; i++)
+ workArray[i] = 0;
+ if (bstr != IntPtr.Zero)
+ Marshal.ZeroFreeBSTR(bstr);
+ handle?.Free();
+ /*** PLAINTEXT EXPOSURE ENDS HERE ***/
+ }
+ }
+
+ internal static void Dump(string header, byte[] data)
+ {
+ int i = 0;
+ StringBuilder hex = new StringBuilder();
+ StringBuilder text = new StringBuilder();
+ Console.WriteLine(header);
+ while (i < data.Length)
+ {
+ if ((i & 15) == 0)
+ {
+ Console.Write(hex.ToString());
+ Console.WriteLine(text.ToString());
+ hex = new StringBuilder(string.Format("{0:X8} ", i));
+ text = new StringBuilder(" ");
+ }
+ hex.Append(string.Format("{0:X2} ", data[i]));
+ if (data[i] == 9 || data[i] == 10 || data[i] == 13)
+ text.Append(" ");
+ else
+ text.Append(string.Format("{0}", (char)data[i]));
+ i++;
+ }
+ Console.Write(hex.ToString().PadRight(57));
+ Console.WriteLine(text.ToString());
+ }
+ internal static string SplitBase64(byte[] data, int length)
+ {
+ var s = Convert.ToBase64String(data);
+ return string.Join("\n", Regex.Matches(s, ".{1," + length + "}").Cast().Select(m => m.Value).ToArray());
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/KeyConverter.cs b/Source/GMPPKConverter/KeyConverter.cs
new file mode 100644
index 0000000..a9aa79f
--- /dev/null
+++ b/Source/GMPPKConverter/KeyConverter.cs
@@ -0,0 +1,293 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace GMax.Security
+{
+ public class KeyConverter
+ {
+ ///
+ /// Imported ppk params
+ ///
+ private PPKParams ppkParams;
+
+ ///
+ /// Imported asymmectic key params
+ ///
+ private AsymmetricKeyParams keyParams;
+
+ ///
+ /// Global ppk lines index
+ ///
+ private int index = 0;
+
+ public KeyConverter()
+ {
+
+ }
+
+ #region Private methods
+ private (int, string) ReadFirstLine(string[] lines)
+ {
+ var match = Regex.Match(lines[index++], $@"PuTTY-User-Key-File-(\d):\s+([-\w]+)");
+ if (!match.Success)
+ throw new FormatException($"Line {index} is invalid, await PuTTY-User-Key-File");
+ if (!int.TryParse(match.Groups[1].Value, out int version) || (version != 2 && version != 3))
+ throw new FormatException($"Line {index} is invalid, only Version 2 or 3 supported");
+ return (version, match.Groups[2].Value);
+ }
+ private string ReadLine(string[] lines, string Token)
+ {
+ var match = Regex.Match(lines[index++], $@"{Token}:\s+([-\w]+)");
+ if (match.Success)
+ return match.Groups[1].Value;
+ else
+ throw new FormatException($"Line {index} is invalid, await {Token}");
+ }
+ private int ReadInt(string[] lines, string Token)
+ {
+ var match = Regex.Match(lines[index++], $@"{Token}:\s+([-\w]+)");
+ if (match.Success)
+ if (int.TryParse(match.Groups[1].Value, out int value))
+ return value;
+ else
+ throw new FormatException($"Line {index} is invalid, {Token} not int");
+ else
+ throw new FormatException($"Line {index} is invalid, await {Token}");
+ }
+ private byte[] ReadBlock(string[] lines, string Token)
+ {
+ var match = Regex.Match(lines[index++], $@"{Token}:\s+([-\w]+)");
+ var sb = new StringBuilder();
+ if (!match.Success || !int.TryParse(match.Groups[1].Value, out int l))
+ throw new FormatException($"Line {index} is invalid, await {Token}");
+ else
+ {
+ for (int i = 0; i < l; i++)
+ {
+ sb.Append(lines[index++]);
+ }
+ }
+ return Convert.FromBase64String(sb.ToString());
+ }
+ private IEnumerable ConvertFromHex(string hex)
+ {
+ for (int i = 0; i < hex.Length; i += 2)
+ {
+ //yield return Convert.ToByte(hex[i..(i+2)], 16);
+ yield return Convert.ToByte(hex.Substring(i, 2), 16);
+ }
+ }
+ #endregion
+
+ #region Public Methods
+ ///
+ /// Import Asymmetric Keys from PPK lines array
+ ///
+ /// Array of PPK content
+ /// PPK passphrase
+ /// Not supported asymmetric key
+ /// PPK format errors
+ public void ImportPPK(string[] lines, SecureString securePassword)
+ {
+ ppkParams = new PPKParams();
+
+ index = 0;
+ (ppkParams.Version, ppkParams.KeyType) = ReadFirstLine(lines);
+
+ if (ppkParams.KeyType.Equals("ssh-rsa"))
+ {
+ keyParams = new RSAKeyParams(ppkParams.KeyType);
+ }
+ else if (ppkParams.KeyType.Equals("ssh-dss"))
+ {
+ keyParams = new DSAKeyParams(ppkParams.KeyType);
+ }
+ else if (ppkParams.KeyType.Equals("ecdsa-sha2-nistp256"))
+ {
+ keyParams = new ECDSAKeyParams(ppkParams.KeyType);
+ }
+ else if (ppkParams.KeyType.Equals("ecdsa-sha2-nistp384"))
+ {
+ keyParams = new ECDSAKeyParams(ppkParams.KeyType);
+ }
+ else if (ppkParams.KeyType.Equals("ecdsa-sha2-nistp521"))
+ {
+ keyParams = new ECDSAKeyParams(ppkParams.KeyType);
+ }
+ else if (ppkParams.KeyType.Equals("ssh-ed25519"))
+ {
+ keyParams = new EDDSAKeyParams(ppkParams.KeyType);
+ }
+ else if (ppkParams.KeyType.Equals("ssh-ed448"))
+ {
+ keyParams = new EDDSAKeyParams(ppkParams.KeyType);
+ }
+ else
+ {
+ throw new NotSupportedException("Not supported key format");
+ }
+
+ ppkParams.Encryption = ReadLine(lines, "Encryption");
+ ppkParams.Comment = ReadLine(lines, "Comment");
+ ppkParams.publicPart = ReadBlock(lines, "Public-Lines");
+
+ if (ppkParams.Version >= 3 && !ppkParams.Encryption.Equals("none")) {
+ ppkParams.Argon2Params = new Argon2Params();
+ string algorithm = ReadLine(lines, "Key-Derivation");
+ if (algorithm.Equals("Argon2d"))
+ {
+ ppkParams.Argon2Params.KeyDerivation = Argon2Type.Argon2d;
+ }
+ else if(algorithm.Equals("Argon2i"))
+ {
+ ppkParams.Argon2Params.KeyDerivation = Argon2Type.Argon2i;
+ }
+ else if (algorithm.Equals("Argon2id"))
+ {
+ ppkParams.Argon2Params.KeyDerivation = Argon2Type.Argon2id;
+ }
+ else
+ {
+ throw new FormatException($"Line {index} is invalid, await Key-Derivation in [Argon2d, Argon2i, Argon2id]");
+ }
+ ppkParams.Argon2Params.Memory = ReadInt(lines, "Argon2-Memory");
+ ppkParams.Argon2Params.Passes = ReadInt(lines, "Argon2-Passes");
+ ppkParams.Argon2Params.Parallelism = ReadInt(lines, "Argon2-Parallelism");
+ var salt = ReadLine(lines, "Argon2-Salt");
+ ppkParams.Argon2Params.Salt = ConvertFromHex(salt).ToArray();
+ }
+
+ ppkParams.privatePart = ReadBlock(lines, "Private-Lines");
+ ppkParams.PrivateMAC = ReadLine(lines, "Private-MAC");
+
+ ppkParams.Decrypt(securePassword);
+
+ if (ppkParams.Encryption.Equals("none") && securePassword?.Length > 0)
+ {
+ securePassword.Clear();
+ }
+
+ string hash = ppkParams.ComputeHash(securePassword);
+ if (!hash.Equals(ppkParams.PrivateMAC))
+ if (ppkParams.Encryption.Equals("none"))
+ throw new ArgumentException("Key was modified");
+ else
+ throw new ArgumentException("Bad password");
+
+ keyParams.ImportKeyParamsFromPPK(ppkParams.publicPart, ppkParams.privatePart);
+ }
+
+ ///
+ /// Exports private key to UNPROTECTED PEM form
+ ///
+ /// UNPROTECTED Private Key as multiline string
+ /// Unsupported key format
+ public string ExportPrivateKey()
+ {
+ var sb = new StringBuilder();
+ string keyEnd = string.Empty;
+ if (keyParams is RSAKeyParams) {
+ sb.Append("-----BEGIN RSA PRIVATE KEY-----\n");
+ keyEnd = "\n-----END RSA PRIVATE KEY-----\n";
+ }
+ else if (keyParams is DSAKeyParams) {
+ sb.Append("-----BEGIN DSA PRIVATE KEY-----\n");
+ keyEnd = "\n-----END DSA PRIVATE KEY-----\n";
+ }
+ else if (keyParams is ECDSAKeyParams)
+ {
+ sb.Append("-----BEGIN EC PRIVATE KEY-----\n");
+ keyEnd = "\n-----END EC PRIVATE KEY-----\n";
+ }
+ else if (keyParams is EDDSAKeyParams)
+ {
+ return ExportOpenSSH();
+ }
+ else
+ {
+ throw new NotImplementedException(string.Format("Export of {0} not implemented", keyParams.GetType().Name));
+ }
+
+ using (var ms = new MemoryStream())
+ {
+ using (var bw = new BinaryWriter(ms))
+ {
+ AsymmetricKeyHelpers.WriteASN1Tag(bw, 0x30, // SEQUENCE
+ (bw1) => keyParams.ExportPrivateKeyAsASN1(bw1)
+ );
+ }
+ sb.Append(Helpers.SplitBase64(ms.ToArray(), 64));
+ }
+
+ sb.Append(keyEnd);
+ return sb.ToString();
+ }
+
+ ///
+ /// Exports private key as UNPROTECTED OpenSSH PEM
+ ///
+ /// private key as UNPROTECTED OpenSSH PEM multiline string
+ public string ExportOpenSSH()
+ {
+ // https://git.tartarus.org/?p=simon/putty.git;a=blob_plain;f=import.c;hb=c0fba758e60e2ff4c1bebd566d9a0e56276d07ec
+ // static bool openssh_new_write
+ var sb = new StringBuilder("-----BEGIN OPENSSH PRIVATE KEY-----\n");
+ string keyEnd = "\n-----END OPENSSH PRIVATE KEY-----\n";
+ using (var ms = new MemoryStream())
+ {
+ using (var bw = new BinaryWriter(ms))
+ {
+ // writing info headers
+ bw.Write(Encoding.ASCII.GetBytes("openssh-key-v1\0"));
+ AsymmetricKeyHelpers.WriteWithLength(bw, Encoding.ASCII.GetBytes("none")); //kdfname
+ AsymmetricKeyHelpers.WriteWithLength(bw, Encoding.ASCII.GetBytes("none"));
+ var kdfoptions = new byte[0];
+ AsymmetricKeyHelpers.WriteWithLength(bw, kdfoptions);
+
+ // bw.Write(BitConverter.GetBytes(N).Reverse().ToArray()); // number of keys N
+ bw.Write(new byte[] { 0, 0, 0, 1 }); // number of keys N = 1
+
+ // writing public key
+ using (var ms1 = new MemoryStream())
+ {
+ using (var bw1 = new BinaryWriter(ms1))
+ {
+ keyParams.ExportPublicKeyAsOpenSSH(bw1);
+ }
+ AsymmetricKeyHelpers.WriteWithLength(bw, ms1.ToArray());
+ }
+
+ // writing private key
+ using (var ms1 = new MemoryStream())
+ {
+ // Because key unencrypited checkint is not random
+ var checkint = 0xef;
+ using (var bw1 = new BinaryWriter(ms1))
+ {
+ bw1.Write(BitConverter.GetBytes(checkint), 0, 4);
+ bw1.Write(BitConverter.GetBytes(checkint), 0, 4);
+ keyParams.ExportPrivateKeyAsOpenSSH(bw1);
+ AsymmetricKeyHelpers.WriteWithLength(bw1, Encoding.ASCII.GetBytes(ppkParams.Comment));
+ // pad out the encrypted section
+ byte padvalue = 1;
+ while ((ms1.Length & 15) != 0)
+ {
+ bw1.Write(padvalue++);
+ };
+ }
+ AsymmetricKeyHelpers.WriteWithLength(bw, ms1.ToArray());
+ }
+ }
+ sb.Append(Helpers.SplitBase64(ms.ToArray(), 64));
+ }
+ sb.Append(keyEnd);
+ return sb.ToString();
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Source/GMPPKConverter/PPKParams.cs b/Source/GMPPKConverter/PPKParams.cs
new file mode 100644
index 0000000..e5649bb
--- /dev/null
+++ b/Source/GMPPKConverter/PPKParams.cs
@@ -0,0 +1,156 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Security;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace GMax.Security
+{
+ internal class PPKParams
+ {
+ public int Version { get; set; } = 0;
+ public string KeyType { get; set; } = string.Empty;
+ public string Encryption { get; set; } = string.Empty;
+ public string Comment { get; set; } = string.Empty;
+ public string PrivateMAC { get; set; } = string.Empty;
+
+ public byte[] publicPart;
+ public byte[] privatePart;
+ public Argon2Params Argon2Params { get; set; }
+
+ private byte[] argon2Hash = new byte[0];
+
+ public void Decrypt(SecureString securePassword)
+ {
+ if (Encryption.Equals("aes256-cbc"))
+ {
+ switch (Version)
+ {
+ case 2:
+ {
+ byte[] IV = new byte[16] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+ using (SHA1Managed sha1 = new SHA1Managed())
+ {
+ var keyBytes = Helpers.ProcessSecureStringAnsi(securePassword,
+ (passwordBytes) =>
+ {
+ var buffer = new byte[32];
+ sha1.ComputeHash(passwordBytes).CopyTo(buffer, 0);
+ passwordBytes[3] = 1;
+ sha1.ComputeHash(passwordBytes).Take(12).ToArray().CopyTo(buffer, 20);
+ return buffer;
+ },
+ new byte[] { 0, 0, 0, 0 }
+ );
+ //Helpers.Dump("keyBytes", keyBytes);
+ privatePart = Helpers.AESDecrypt(privatePart, IV, keyBytes);
+ //Helpers.Dump("privatePart", privatePart);
+ }
+ break;
+ }
+ case 3:
+ {
+ // Konscious.Security.Cryptography only, other write garbage after byte[32]
+ var keyBytes = new byte[32];
+ var IV = new byte[16];
+ argon2Hash = Helpers.ProcessSecureStringAnsi(securePassword, (passwordBytes) =>
+ {
+ Konscious.Security.Cryptography.Argon2 argon2;
+ switch (Argon2Params.KeyDerivation)
+ {
+ case Argon2Type.Argon2i:
+ argon2 = new Konscious.Security.Cryptography.Argon2i(passwordBytes);
+ break;
+ case Argon2Type.Argon2d:
+ argon2 = new Konscious.Security.Cryptography.Argon2d(passwordBytes);
+ break;
+ case Argon2Type.Argon2id:
+ argon2 = new Konscious.Security.Cryptography.Argon2id(passwordBytes);
+ break;
+ default:
+ throw new ArgumentException("Unknown argon2 type");
+ };
+ argon2.Salt = Argon2Params.Salt;
+ argon2.DegreeOfParallelism = Argon2Params.Parallelism;
+ argon2.Iterations = Argon2Params.Passes;
+ argon2.MemorySize = Argon2Params.Memory;
+ var argonHashBytes = argon2.GetBytes(80);
+ //Helpers.Dump("argonHashBytes", argonHashBytes);
+ var hashBytes = new byte[32];
+
+ Array.Copy(argonHashBytes, 0, keyBytes, 0, keyBytes.Length);
+ Array.Copy(argonHashBytes, 32, IV, 0, IV.Length);
+ Array.Copy(argonHashBytes, 48, hashBytes, 0, hashBytes.Length);
+ return hashBytes;
+ });
+ privatePart = Helpers.AESDecrypt(privatePart, IV, keyBytes);
+ //Helpers.Dump("privatePart", privatePart);
+ break;
+ }
+ default:
+ throw new CryptographicException("Unsupported encryption Version");
+ }
+ }
+ else if (Encryption == "none")
+ {
+ }
+ else
+ {
+ new CryptographicException("Unknown Encryption");
+ }
+ }
+
+ public string ComputeHash(SecureString securePassword)
+ {
+ string hash;
+
+ byte[] bytesToHash;
+ using (var ms = new MemoryStream())
+ {
+ using (var bw = new BinaryWriter(ms))
+ {
+ AsymmetricKeyHelpers.WriteWithLength(bw, Encoding.ASCII.GetBytes(KeyType));
+ AsymmetricKeyHelpers.WriteWithLength(bw, Encoding.ASCII.GetBytes(Encryption));
+ AsymmetricKeyHelpers.WriteWithLength(bw, Encoding.ASCII.GetBytes(Comment));
+ AsymmetricKeyHelpers.WriteWithLength(bw, publicPart);
+ AsymmetricKeyHelpers.WriteWithLength(bw, privatePart);
+ }
+ bytesToHash = ms.ToArray();
+ }
+ switch (Version)
+ {
+ case 2:
+ {
+ using (var csp = new SHA1CryptoServiceProvider())
+ {
+ hash = Helpers.ProcessSecureStringAnsi(securePassword,
+ (passwordBytes) =>
+ {
+ using (HMACSHA1 hmacsha1 = new HMACSHA1(csp.ComputeHash(passwordBytes)))
+ {
+ return string.Join("", hmacsha1.ComputeHash(bytesToHash).Select(x => string.Format("{0:x2}", x)));
+ }
+ },
+ Encoding.ASCII.GetBytes("putty-private-key-file-mac-key")
+ );
+ break;
+ }
+ }
+
+ case 3:
+ {
+ using (HMACSHA256 hmacsha256 = new HMACSHA256(argon2Hash))
+ {
+ hash = string.Join("", hmacsha256.ComputeHash(bytesToHash).Select(x => string.Format("{0:x2}", x)));
+ break;
+ }
+ }
+ default:
+ throw new ArgumentException("Usupported MAC version");
+ }
+ return hash;
+ }
+ }
+}
diff --git a/Source/GMPPKConverter/RSAKeyParams.cs b/Source/GMPPKConverter/RSAKeyParams.cs
new file mode 100644
index 0000000..53b5d1a
--- /dev/null
+++ b/Source/GMPPKConverter/RSAKeyParams.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Numerics;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace GMax.Security
+{
+ internal class RSAKeyParams : AsymmetricKeyParams
+ {
+ public RSAKeyParams(string Algorithm) : base(Algorithm)
+ {
+ }
+
+ /*
+ RSAParameters.Exponent (and RSAParameters.Modulus), on the other hand, is an unsigned, big-endian integer using a minimum number of bytes.
+
+ RSAParameters.P (and Q, DP, DQ, InverseQ) is an unsigned, big-endian fixed-width integer
+ whose byte[].Length value must be exactly ((RSAParameters.Modulus.Length + 1) / 2).
+ And RSAParameters.D must have the same length as RSAParameters.Modulus.
+ */
+ public byte[] D { get; set; }
+ public byte[] P { get; set; }
+ public byte[] Q { get; set; }
+ public byte[] InverseQ { get; set; }
+ public byte[] DP { get; set; }
+ public byte[] DQ { get; set; }
+ public byte[] Exponent { get; set; }
+ public byte[] Modulus { get; set; }
+
+ public override void ExportPrivateKeyAsASN1(BinaryWriter bw)
+ {
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, new byte[] { 0x00 }); // Version
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, Modulus);
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, Exponent);
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, D);
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, P);
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, Q);
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, DP);
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, DQ);
+ AsymmetricKeyHelpers.WriteASN1Integer(bw, InverseQ);
+ }
+
+ public override void ExportPrivateKeyAsOpenSSH(BinaryWriter bw)
+ {
+ AsymmetricKeyHelpers.
+ ////put_mp_ssh2(bs, rsa->private_exponent);
+ ////539 put_mp_ssh2(bs, rsa->p);
+ ////540 put_mp_ssh2(bs, rsa->q);
+ ////541 put_mp_ssh2(bs, rsa->iqmp);
+ ////
+ //put_mp_ssh2(bs, rsa->modulus);
+ //599 put_mp_ssh2(bs, rsa->exponent);
+ //600 put_mp_ssh2(bs, rsa->private_exponent);
+ //601 put_mp_ssh2(bs, rsa->iqmp);
+ //602 put_mp_ssh2(bs, rsa->p);
+ //603 put_mp_ssh2(bs, rsa->q);
+
+ WriteWithLength(bw, Encoding.ASCII.GetBytes(Algorithm));
+ AsymmetricKeyHelpers.WriteWithLength(bw, Modulus); //, true
+ AsymmetricKeyHelpers.WriteWithLength(bw, Exponent);
+ AsymmetricKeyHelpers.WriteWithLength(bw, D); //, true
+ AsymmetricKeyHelpers.WriteWithLength(bw, InverseQ); //, true
+ AsymmetricKeyHelpers.WriteWithLength(bw, P); //, true
+ AsymmetricKeyHelpers.WriteWithLength(bw, Q); //, true
+ }
+
+ public override void ExportPublicKeyAsOpenSSH(BinaryWriter bw)
+ {
+ AsymmetricKeyHelpers.
+ // https://git.tartarus.org/?p=simon/putty.git;a=blob;f=crypto/rsa.c;hb=faf1601a5549eda9298f72f7c0f68f39c8f97764
+ //put_stringz(bs, "ssh-rsa");
+ //530 put_mp_ssh2(bs, rsa->exponent);
+ //531 put_mp_ssh2(bs, rsa->modulus);
+ WriteWithLength(bw, Encoding.ASCII.GetBytes(Algorithm));
+ AsymmetricKeyHelpers.WriteWithLength(bw, Exponent);
+ AsymmetricKeyHelpers.WriteWithLength(bw, Modulus); //, true
+ }
+
+ public override void ImportKeyParamsFromPPK(byte[] publicData, byte[] privateData)
+ {
+ if (privateData?.Length == 0)
+ throw new CryptographicException("Private key not decoded");
+ using (var ms = new MemoryStream(publicData))
+ {
+ using (var br = new BinaryReader(ms))
+ {
+ AsymmetricKeyHelpers.ReadWithLength(br); // alg. name ssh-rsa
+ // exponent
+ Exponent = AsymmetricKeyHelpers.ReadWithLength(br);
+ // modulus
+ Modulus = AsymmetricKeyHelpers.ReadWithLength(br); // FixLength
+ }
+ }
+ using (var ms = new MemoryStream(privateData))
+ {
+ using (var br = new BinaryReader(ms))
+ {
+ //D
+ D = AsymmetricKeyHelpers.ReadWithLength(br); // FixLength
+ //P
+ P = AsymmetricKeyHelpers.ReadWithLength(br); // FixLength
+ //Q
+ Q = AsymmetricKeyHelpers.ReadWithLength(br); // FixLength
+ //InverseQ
+ InverseQ = AsymmetricKeyHelpers.ReadWithLength(br); // FixLength
+
+ //var d = new BigInteger(rsaParams.D, true, true);
+ //var p = new BigInteger(rsaParams.P, true, true);
+ //var q = new BigInteger(rsaParams.Q, true, true);
+ //var e = new BigInteger(rsaParams.Exponent, true, true);
+ //var iq = new BigInteger(rsaParams.InverseQ, true, true);
+ // Эти записи эквивалентны, но верхний конструктор доступен тольок в netcore 2.1+
+ var d = new BigInteger(AsymmetricKeyHelpers.CopyAndReverse(D));
+ var p = new BigInteger(AsymmetricKeyHelpers.CopyAndReverse(P));
+ var q = new BigInteger(AsymmetricKeyHelpers.CopyAndReverse(Q));
+ var e = new BigInteger(AsymmetricKeyHelpers.CopyAndReverse(Exponent));
+
+ // DP = D mod(P - 1)
+ // DQ = D mod(Q - 1)
+
+ var dp = d % (p - 1);
+ var dq = d % (q - 1);
+
+ DP = AsymmetricKeyHelpers.CopyAndReverse(dp.ToByteArray()); // какого хрена эти не совпадают ?
+ DQ = AsymmetricKeyHelpers.CopyAndReverse(dq.ToByteArray());
+ }
+ }
+ }
+ }
+}
diff --git a/Source/GMPPKConverting.sln b/Source/GMPPKConverting.sln
new file mode 100644
index 0000000..009ec25
--- /dev/null
+++ b/Source/GMPPKConverting.sln
@@ -0,0 +1,43 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.1.32210.238
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GMPPKConverter", "GMPPKConverter\GMPPKConverter.csproj", "{8D08A965-C576-4A42-86C6-A0429F386A74}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPPKConverting", "TestPPKConverting\TestPPKConverting.csproj", "{41F9A874-A40F-45BC-9043-620E184BB11B}"
+ ProjectSection(ProjectDependencies) = postProject
+ {8D08A965-C576-4A42-86C6-A0429F386A74} = {8D08A965-C576-4A42-86C6-A0429F386A74}
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GMPPKConverter.Tests", "GMPPKConverter.Tests\GMPPKConverter.Tests.csproj", "{D8074B8D-B220-4643-9968-DDE944ED88F2}"
+ ProjectSection(ProjectDependencies) = postProject
+ {8D08A965-C576-4A42-86C6-A0429F386A74} = {8D08A965-C576-4A42-86C6-A0429F386A74}
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {8D08A965-C576-4A42-86C6-A0429F386A74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8D08A965-C576-4A42-86C6-A0429F386A74}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8D08A965-C576-4A42-86C6-A0429F386A74}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8D08A965-C576-4A42-86C6-A0429F386A74}.Release|Any CPU.Build.0 = Release|Any CPU
+ {41F9A874-A40F-45BC-9043-620E184BB11B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {41F9A874-A40F-45BC-9043-620E184BB11B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {41F9A874-A40F-45BC-9043-620E184BB11B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {41F9A874-A40F-45BC-9043-620E184BB11B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D8074B8D-B220-4643-9968-DDE944ED88F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D8074B8D-B220-4643-9968-DDE944ED88F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D8074B8D-B220-4643-9968-DDE944ED88F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D8074B8D-B220-4643-9968-DDE944ED88F2}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {726ADF90-013B-4446-A7FF-C8AE65F9355F}
+ EndGlobalSection
+EndGlobal
diff --git a/Source/TestPPKConverting/App.config b/Source/TestPPKConverting/App.config
new file mode 100644
index 0000000..56efbc7
--- /dev/null
+++ b/Source/TestPPKConverting/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/TestPPKConverting/Program.cs b/Source/TestPPKConverting/Program.cs
new file mode 100644
index 0000000..48f2af4
--- /dev/null
+++ b/Source/TestPPKConverting/Program.cs
@@ -0,0 +1,37 @@
+using System;
+using System.IO;
+using System.Security;
+using GMax.Security;
+
+namespace TestPPKConverting
+{
+ internal class Program
+ {
+ static void Main(string[] args)
+ {
+ if (args.Length < 1 || args.Length > 2) {
+ Console.WriteLine("Usage: TestPPKConverting []");
+ }
+ else
+ {
+ var ppkpath = args[0];
+ var password = args.Length == 2 ? args[1] : "";
+ var lines = File.ReadAllLines(ppkpath);
+ Console.WriteLine($" Read PPK from {ppkpath}");
+ SecureString secpass = new SecureString();
+ foreach (char c in password)
+ {
+ secpass.AppendChar(c);
+ }
+ var ppk = new KeyConverter();
+ ppk.ImportPPK(lines, secpass);
+
+ var cert1 = ppk.ExportPrivateKey();
+ var cert2 = ppk.ExportOpenSSH();
+
+ Console.WriteLine(cert1);
+ Console.WriteLine(cert2);
+ }
+ }
+ }
+}
diff --git a/Source/TestPPKConverting/Properties/AssemblyInfo.cs b/Source/TestPPKConverting/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..5484ee3
--- /dev/null
+++ b/Source/TestPPKConverting/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// Общие сведения об этой сборке предоставляются следующим набором
+// набора атрибутов. Измените значения этих атрибутов для изменения сведений,
+// связанные с этой сборкой.
+[assembly: AssemblyTitle("ConsoleApp1")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("ConsoleApp1")]
+[assembly: AssemblyCopyright("Copyright © 2022")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Установка значения False для параметра ComVisible делает типы в этой сборке невидимыми
+// для компонентов COM. Если необходимо обратиться к типу в этой сборке через
+// из модели COM задайте для атрибута ComVisible этого типа значение true.
+[assembly: ComVisible(false)]
+
+// Следующий GUID представляет идентификатор typelib, если этот проект доступен из модели COM
+[assembly: Guid("41f9a874-a40f-45bc-9043-620e184bb11b")]
+
+// Сведения о версии сборки состоят из указанных ниже четырех значений:
+//
+// Основной номер версии
+// Дополнительный номер версии
+// Номер сборки
+// Номер редакции
+//
+// Можно задать все значения или принять номера сборки и редакции по умолчанию
+// используя "*", как показано ниже:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Source/TestPPKConverting/TestPPKConverting.csproj b/Source/TestPPKConverting/TestPPKConverting.csproj
new file mode 100644
index 0000000..a467770
--- /dev/null
+++ b/Source/TestPPKConverting/TestPPKConverting.csproj
@@ -0,0 +1,59 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {41F9A874-A40F-45BC-9043-620E184BB11B}
+ Exe
+ TestPPKConverting
+ TestPPKConverting
+ v4.7.2
+ 512
+ true
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {8d08a965-c576-4a42-86c6-a0429f386a74}
+ GMPPKConverter
+
+
+
+
\ No newline at end of file