Introduction
I am assuming in this article some knowledge of PGP, GnuPG,
or asymmetric 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 and .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
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 signing 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 asymmetric 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
items to you. To accomplish this, I had to play around quite a bit to find the
proper options allowing automatic generation so that 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 eight seconds in my environment.
Notes:
1. User is my source of input for password etc, you need to
come up with your own here.
2. You may want to expire your keys, etc.
3. 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.
4. The reason I use stdin here to supply parameters versus 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 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 the ExecuteCommand Method
Here, the gpgOptions string is built dynamically from
BuildOptions. Note the inline comments.
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
' write the passphrase into std input
_processObject.StandardInput.WriteLine(_passphrase)
_processObject.StandardInput.Flush()
End If
' write the input for decryption or encryption
_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)
' note custom exception adds to GnuPG's already rich error output
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 email inbox with the following code. Note that I have handled decryption
and importation of a public key with two 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.
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 plus 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.
Notes
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 out the
line:
pinfo.CreateNoWindow = True
This enables you to see what is happening in the command
line window. 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]
Thanks to the Author: Emmanuel KARTMANN
(The above article supported two areas in C# of the five areas I support in
VB.NET.)