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)