GnuPG Encryption for email/xml/etc in VB.NET Wrapper

 

Introduction  

I am assuming in this article some knowledge of PGP, GnuPG, or Antisymmetric encryption:
If you don't have this prerequisite, you may garner it at:
http://www.gnupg.org

PGP or GnuPG (the public domain near equivalent) are important encryptions because they are standards that are being used in email and secure xml transfers and other areas.  They were originally developed in the Linux/C language environ but can be used in Windows .NET easily with a wrapper that is easily modified to support any features you want to use.

Wrapper Source     GnuPg Source link to FTP link for 4.2.1 version


Support

Commands/Options Supported by this Article's wrapper:

1) Initial key generation

2) Signing and Encryption

3) Decryption

4) Importing of a friend's public key to your public key ring

5) Exporting of your public key to a friend's public key ring

These features will support secure signed and encryption with any number of clients. This wrapper is easily modifiable to support any portion of the many commands and options available in the GnuPG interface.

 

Looking at the code for initial Generation of Keys:

GnuPG is antisymmetric encryption so there will be a private key that you must secure for yourself and a public key that you will want to disseminate on your website or by emailing to allow people to send encrypted to you. To accomplish this I had to play around quite a bit to find the proper options allowing an automatic generation so my commercial email program can generate for each client with no knowledge of GnuPG. I would not suggest changing the options on this command. You can change the size of your subkey to 2048 for more security, but slower generation times.  Generation took about 8 seconds in my environ. 

User is my source of input for password etc, you need to come up with your own here.

You may want to expire your keys, etc.

Note that when you redirect stdin, and/or stdout, and/or stderr you must not use shellexecute or have the window open, except to test for errors.

The reason I use stdin here to supply parameters vs a file parameter which is allowed, is for added security of not having the passphrase sitting in a file that could be hacked.

 

 

Code Listing #1

  Public Sub GenerateInitialKeys(ByVal path As String)

    Dim user As SetupxEntity = Util.User

    Dim paramContents As String = String.Empty

    paramContents &= "%echo Generating keys..." & CrLf

    paramContents &= "Key-Type: DSA" & CrLf

    paramContents &= "Subkey-Type: ELG-E" & CrLf

    paramContents &= "Subkey-Length: 1024" & CrLf

    paramContents &= "Passphrase: tester999" & CrLf '& user.Password.Trim & CrLf

    paramContents &= "Key-Length: 1024" & CrLf

    paramContents &= "Name-Real: " & Me.GetNameReal() & CrLf

    paramContents &= "Name-Email: " & user.Email.Trim & CrLf

    paramContents &= "Name-Comment: autogenerated" & CrLf

    paramContents &= "Expire-Date: 0" & CrLf

    paramContents &= "%commit" & CrLf

    paramContents &= "%echo Completed Successfully" & CrLf

    Dim gpgOptions As String

gpgOptions = "--homedir . “

gpgOptions &= “--no-tty “

gpgOptions &= “--status-fd=2 “

gpgOptions &= “--no-secmem-warning “

gpgOptions &= “--batch --gen-key"

    Dim gpgExecutable As String = path & "gpg.exe"

    Dim pinfo As New ProcessStartInfo(gpgExecutable, gpgOptions)

    pinfo.WorkingDirectory = path

    pinfo.CreateNoWindow = True

    pinfo.UseShellExecute = False

    ' Redirect stdin to input parameters, stderr in case of errors

    pinfo.RedirectStandardInput = True

    pinfo.RedirectStandardError = True

    _processObject = Process.Start(pinfo)

    _processObject.StandardInput.WriteLine(paramContents)

    _processObject.StandardInput.Flush()

    _processObject.StandardInput.Close()

    _errorString = ""

    Dim errorEntry As New ThreadStart(AddressOf StandardErrorReader)

    Dim errorThread As New Thread(errorEntry)

    errorThread.Start()

    If _processObject.WaitForExit(ProcessTimeOutMilliseconds) Then

      If Not errorThread.Join(ProcessTimeOutMilliseconds / 2) Then

        errorThread.Abort()

      End If

    Else

      ' Process timeout: PGP hung somewhere... kill it (as well as the threads!)

      _outputString = ""

      _errorString = "Timed out after "

_errorString &= ProcessTimeOutMilliseconds.ToString & " milliseconds"

      _processObject.Kill()

      If errorThread.IsAlive Then

        errorThread.Abort()

      End If

    End If

    ' Check results and prepare output

    _exitcode = _processObject.ExitCode

    If Not _exitcode = 0 Then

      If _errorString = "" Then

        _errorString = "GPGNET: ["

  _errorString &= _processObject.ExitCode.ToString() & "]: Unknown error"

      End If

      Throw New GnupgException(_errorString)

    End If

  End Sub

 

Looking at the code for BuildingOptions:

The ExecuteCommand Method is examined next and you will see that that method handles both encryption and decryption. I could have easily created an Encryption method and a Decryption method, but wanted to show how options could be built in a BuildOptions method called by ExecuteCommand to use properties to set and use many of the powerfull commands of GnuPG.

In the BuildOptions method property values are examined to build an option string fed to ExecuteCommand. I put some unused cases in there to show how you might be able to unify all GnuPG functionality within one ExecuteCommand method if you wanted to.

The --armor option means ascii versus binary, and –trust-model always means take the simple version of trust so that we don’t have to get overly concerned about levels of trust. We will be trusting visually by seeing the emails sender and thinking that, “Yes, I am receiving encrypted from that person”.

 

Code Listing#2

 

  Protected Function BuildOptions() As String

    Dim optionsBuilder As New StringBuilder("", 255)

    Dim recipientNeeded As Boolean = False

    Dim passphraseNeeded As Boolean = False

    If _homedirectory IsNot Nothing And Not _homedirectory = "" Then

      optionsBuilder.Append("--homedir " & Quote)

      optionsBuilder.Append(_homedirectory)

      optionsBuilder.Append(Quote & " ")

    End If

    If _yes Then optionsBuilder.Append("--yes ")

    If _batch Then optionsBuilder.Append("--batch ")

    Select Case _command

      Case Commands.SignAndEncrypt

        optionsBuilder.Append("--sign ")

        optionsBuilder.Append("--encrypt --armor ")

        If _trust Then

          optionsBuilder.Append("--trust-model always ")

        End If

        recipientNeeded = True

        passphraseNeeded = True

      Case Commands.Decrypt

        optionsBuilder.Append("--decrypt ")

        If _trust Then

          optionsBuilder.Append("--trust-model always ")

        End If

      Case Commands.Import

        optionsBuilder.Append("--import ")

      Case Commands.Export

        optionsBuilder.Append("--armor --export " & Util.User.Email.Trim)

      Case Commands.Genkey

        optionsBuilder.Append("--batch --gen-key ")

    End Select

    If _recipient IsNot Nothing And Not _recipient = "" Then

      optionsBuilder.Append("--recipient ")

      optionsBuilder.Append(_recipient)

      optionsBuilder.Append(" ")

    Else

      If recipientNeeded Then

        Throw New GnupgException("GPGNET: Missing 'recipient' parameter")

      End If

    End If

    If _originator IsNot Nothing And Not _originator = "" Then

      optionsBuilder.Append("--default-key ")

      optionsBuilder.Append(_originator)

      optionsBuilder.Append(" ")

    End If

    If _passphrase Is Nothing Or _passphrase = "" Then

      If passphraseNeeded Then

        Throw New GnupgException("GPGNET: Missing 'passphrase' parameter")

      End If

    End If

    If _passphrase IsNot Nothing And Not _passphrase = "" Then

      optionsBuilder.Append("--passphrase-fd ")

      optionsBuilder.Append(_passphrasefd)

      optionsBuilder.Append(" ")

    Else

      If passphraseNeeded _

And (_passphrase Is Nothing Or Not _passphrase = "") Then

        Throw New GnupgException("GPGNET: Missing 'passphrase' parameter")

      End If

    End If

    Select Case Verbose

      Case VerboseLevel.NoVerbose

        optionsBuilder.Append("--no-verbose ")

      Case VerboseLevel.Verbose

        optionsBuilder.Append("--verbose ")

      Case VerboseLevel.VeryVerbose

        optionsBuilder.Append("--verbose --verbose ")

    End Select

    Return optionsBuilder.ToString

  End Function

 

Looking at the code for ExecuteCommand method:

Here the gpgOptions string is built dynamically from BuildOptions.

Code Listing#3

 

  Public Sub ExecuteCommand(ByVal inputText As String, ByRef outputtext As String)

    outputtext = ""

    Dim gpgOptions As String = BuildOptions()

    Dim gpgExecutable As String = Util.GetGpgPath & "\gpg.exe"

    Dim pinfo As New ProcessStartInfo(gpgExecutable, gpgOptions)

    pinfo.WorkingDirectory = Util.GetGpgPath & "\"

    pinfo.CreateNoWindow = True

    pinfo.UseShellExecute = False

    ' Redirect everything: stdin for passphrase, stdout for encrypted, stderr

    pinfo.RedirectStandardInput = True

    pinfo.RedirectStandardOutput = True

    pinfo.RedirectStandardError = True

    _processObject = Process.Start(pinfo)

    If _passphrase IsNot Nothing And Not _passphrase = "" Then

      _processObject.StandardInput.WriteLine(_passphrase)

      _processObject.StandardInput.Flush()

    End If

    _processObject.StandardInput.WriteLine(inputText)

    _processObject.StandardInput.Flush()

    _processObject.StandardInput.Close()

    _outputString = ""

    _errorString = ""

    ' Create two threads to read both output/error streams w/o deadlock

    Dim outputEntry As New ThreadStart(AddressOf StandardOutputReader)

    Dim outputThread As New Thread(outputEntry)

    outputThread.Start()

    Dim errorEntry As New ThreadStart(AddressOf StandardErrorReader)

    Dim errorThread As New Thread(errorEntry)

    errorThread.Start()

    If _processObject.WaitForExit(ProcessTimeOutMilliseconds) Then

      ' process exited before timeout.
      ‘ Wait for the threads to complete reading output/error (but use a timeout)

      If Not outputThread.Join(ProcessTimeOutMilliseconds / 2) Then

        outputThread.Abort()

      End If

      If Not errorThread.Join(ProcessTimeOutMilliseconds / 2) Then

        errorThread.Abort()

      End If

    Else

      ' Process timeout: PGP hung somewhere... kill it (as well as the threads!)

      _outputString = ""

      _errorString = "Timed out after " & ProcessTimeOutMilliseconds.ToString

      _errorString &= " milliseconds"

      _processObject.Kill()

      If outputThread.IsAlive Then

        outputThread.Abort()

      End If

      If errorThread.IsAlive Then

        errorThread.Abort()

      End If

    End If

    ' Check results and prepare output

    _exitcode = _processObject.ExitCode

    If _exitcode = 0 Then

      outputtext = _outputString

    Else

      If _errorString = "" Then

        _errorString = "GPGNET: [" & _processObject.ExitCode.ToString()

        _errorString &= "]: Unknown error"

      End If

      Throw New GnupgException(_errorString)

    End If

  End Sub

 

Looking at the code for Using the GnuPG class to handle the main functions:

Since the code for Import and Export are very similar to those shown above, let’s consider how to use the class. The path is very important for all GnuPG commands and is fed to the –homedir parameter plus other places as well.

First we need to generate the initial keys.

 

Code Listing#4

 

  Private Sub genkey1_Click(ByVal sender As System.Object, ByVal e As _ System.EventArgs) Handles genkey1.Click

    Dim gpg As New GnuPG()

    If MessageBox.Show(gpg.GetNameReal & "  and   " & Util.User.Email.Trim & CrLf_ & "Is this okay?", "Important Keys will be created with Name and Email below",_ MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation) = 1 Then

      Me.setupstatus.Text = "Starting Key Generation: Please wait..."

      Application.DoEvents()

      Dim outputText As String = String.Empty

      Dim path As String = Util.GetGpgPath() & "\"

      gpg.GenerateInitialKeys(path)

      Thread.Sleep(12000)
      Me.setupstatus.Text = "Key Generation: complete."

    End If

  End Sub

 

Second we will use the keys to encrypt a message to email self since that is the only person our public key we just created will encrypt to.

Code Listing#5

 

  Private Function EncryptBody(ByVal body As String) As String

    Dim inputText As String = body

    Dim outputText As String = ""

    Try

      Dim gpg As New GnuPG()

      gpg.Homedirectory = Util.GetGpgPath

      gpg.Trust = True

      gpg.Verbose = VerboseLevel.NoVerbose

      gpg.Passphrase = Util.User.Password.Trim

      gpg.Originator = Util.User.Email.Trim

      gpg.Recipient = eaddr.Text.Trim

      gpg.Command = Commands.SignAndEncrypt

      gpg.ExecuteCommand(inputText, outputText)

      status.Text = "Encryption Succeeded"

    Catch gpge As GnupgException

      status.Text = gpge.Message.Replace(CrLf, "")

    End Try

    Return outputText

  End Function

 

Thirdly we will decrypt the message that arrives back in our emai inbox with the following code. Note that I have handled decryption and importation of a public key with 2 cases here since receiving a public key or an encrypted message differ by a header type.

Code Listing#6

 

  Private Sub edecrypt_Click(ByVal sender As System.Object, ByVal e As_ System.EventArgs) Handles edecrypt.Click

    Dim phrase As New Regex("-----.*?-----")

    Dim matchs As MatchCollection = phrase.Matches(browser.DocumentText)

    If matchs.Count > 0 Then

      Dim firstMatch As String = matchs(0).ToString

      Select Case firstMatch

        Case "-----BEGIN PGP PUBLIC KEY BLOCK-----"
          Dim pkHeader As string = string.empty

          pkHeader &= "-----BEGIN PGP PUBLIC KEY BLOCK-----.*?”
          pkHeader &= “END PGP PUBLIC KEY BLOCK-----"

          Dim body As New Regex(pkHeader)         
          Dim publicKey As String = body.Match(browser.DocumentText).ToString

          publicKey = publicKey.Replace("<br>", CrLf) ‘content is in browser

          Try

            Dim gpg As New GnuPG()

            gpg.Homedirectory = Util.GetGpgPath

            gpg.Command = Commands.Import

            gpg.Import(publicKey)

            status.Text = "Public Key imported"

          Catch gpge As GnupgException

            status.Text = gpge.Message.Replace(CrLf, "")

          End Try

        Case "-----BEGIN PGP MESSAGE-----"

          Dim msgHeader As string = string.empty

          msgHeader &= "-----BEGIN PGP MESSAGE-----.*?”
         
msgHeader &= “-----END PGP MESSAGE-----"

          Dim body As New Regex(msgHeader)

          Dim encryptedMsg As String = _

          body.Match(browser.DocumentText).ToString.Replace("<br>", CrLf)

          Dim decryptedMsg As String = String.Empty

          Try

            Dim gpg As New GnuPG

            gpg.Homedirectory = Util.GetGpgPath

            gpg.Trust = True

            gpg.Passphrase = Util.User.Password.Trim

            gpg.Verbose = VerboseLevel.NoVerbose

            gpg.Command = Commands.Decrypt

            gpg.ExecuteCommand(encryptedMsg, decryptedMsg)

            browser.DocumentText = Me.GetHtmlPage(decryptedMsg)

            status.Text = "Message Decrypted"

          Catch gpge As GnupgException

            status.Text = gpge.Message.Replace(CrLf, ", ")

          End Try

        Case Else

          status.Text = "Unknown Request"

      End Select

    Else

      status.Text = "Nothing to Decrypt"

    End If

  End Sub

 

Fourthly we will export our public key so that people can send encrypted mail to us. I had a lot of problems getting other Linux based email programs to accept my generated public key, but it turned out to be simply that the Linux computers were on GMT time and were seeing my keys as having been created in the future so they would not accept the key. Security is everything in this world J

Note that the Export method has one input variable that is passed ByRef so we can modify the variable with our thread output.

Code Listing#6

 

  Private Sub export_Click(ByVal sender As System.Object, ByVal e As _ System.EventArgs) Handles export.Click

    Dim outputText As String = ""

    Try

      Dim gpg As New GnuPG()

      gpg.Homedirectory = Util.GetGpgPath

      gpg.Command = Commands.Export

      gpg.Export(outputText)

      status.Text = "Export Succeeded"

    Catch gpge As GnupgException

      status.Text = gpge.Message.Replace(CrLf, "")

    End Try

    eintro.Text = outputText

    subject.Text = "My Public Key"

    Me.useeintro.Checked = True

    Me.usecompose.Checked = False

  End Sub

 

Conclusion

The way you would modify this wrapper is to play with the gpg.exe execution from the command line prompt. It is important to note that when you are prompted for input, sometimes you need to enter the info and then tell gpg.exe that you are done with a Linux ctrl-D, which is ctrl-Z and enter in windows. Without this info you may have major problems completing entry of successful tests. Once you have successful tests, with certain options selected (using the gpg manual of options) you know how to modify my wrapper's BuildOptions method to add the options that worked for you on the command line.

If you get in a situation where gpg.exe does a nice thing, but with the same options, your wrapper doesn't work, try commenting the line:

pinfo.CreateNoWindow = True

so that you can see in the command line window to see what is happening. Install to a directory like c:\gnupg. Use Start, Run, CMD, cd\, cd gnupg, cls, gpg and press return to run the gpg.exe executable once to create some startup files.

Now copy gpg.exe and these key ring files to your application to prepare for your initial generation.

This article relies upon the following article:
Using the Gnu Privacy Guard (GnuPG/PGP) within ASP.NET [v1.0]
Located at:
http://www.codeguru.com/Csharp/.NET/net_security/pgp/article.php/c4699/
Thanks to the Author: Emmanuel KARTMANN
(The above article supported 2 areas in C# of the 5 areas I support in VB.NET)