.NET Geek

"It is upon the Trunk that a gentleman works" - Confucius

PGP Zip Encrypted Files With C#

On a recent project here at Renaissance, we needed to send files over FTP to some third party vendor. One of the requirements was that the files had to be encrypted using PGP (Pretty Good Privacy). After some research we decided to use Bouncy Castle. Bouncy Castle is an open source C# implementation of the OpenPGP standard. It is available in Java as well.
An additional requirement was that the PGP Encrypted files needed to be signed as well.

If you have no background in cryptology or PGP and this sounds like gibberish, here’s a short simplified background on symmetric key encryption.

To share PGP encrypted files the sender and recipient both need two keys. One public and one private. The sender encrypts the file to send with the recipients public key and sign with his private key. Both parties then exchange public keys. Each party can decrypt using its own private key and it can verify who sent the file using the senders public key.

If this still sounds gibberish, I found this illustration on the LinomaSoftware site a good visual explanation. (never used it, just searched Google for PGP image)

With that out of the way, how hard can it be to encrypt and sign a file? Not very hard, but far too much code to write. We found a few samples online, but nothing I felt comfortable to use in our codebase. Credits to John Opincar who published a post on single pass encryption and signing. We used the blog post of his, the Bouncy test suites and a some trial and failure to get it working.

One of the issues with all the sample code out there, is that there are so many responsibilities squeezed together that unless you know what the code is doing beforehand, it is hard to grasp. It was to me at least. That might be partially related to me having no significant background in cryptology or PGP.

Lets see some code. No matter if I’m doing TDD or not, I always try to write the client code before the API. That way I shape the API from the point of view of the consuming code and avoid surprising and clunky interfaces later. I wanted the calling code to look like this.

private static void EncryptAndSign()

{

    PgpEncryptionKeys encryptionKeys = new PgpEncryptionKeys(

                                   PublicKeyFileName, PrivateKeyFileName, "PasswordOfMyPrivateKey");

    PgpEncrypt encrypter = new PgpEncrypt(encryptionKeys);

    using (Stream outputStream = File.Create(EncryptedFileName))

    {

        encrypter.EncryptAndSign(outputStream, new FileInfo(FileToEncrypt));

    }

}

From the sample code above you can see that we have separated Key management code from the actual encryption code. The PgpEncryptionKeys class instantiates and deals with the intricacies of key management. The PgpEncrypt class does this actual encryption. There were two reasons for this separation. The first is that key management is a separate concern conceptually. Another is that while we currently point to the location of the key files, we might want to change that in the future. I want to be able to change the way we instantiate the keys without touching the encryption code. No efforts were made at this point to create interfaces and/or abstract classes for evolution or extensibility. We’ll do that when/if we’ll need it.

Next we will have a look at the actual implementation. I will not walk through and explain all the code. We tried to make the code as self explanatory as possible. However, if you have no other background related to encryption and PGP besides this blog post, you should probably spend a few hours reading up on that before considering using this code. Treat this code As-Is with no commitment on my side to keep it up-to-date with bug fixes and improvements.

using System;

using System.IO;

using System.Linq;

using Org.BouncyCastle.Bcpg.OpenPgp;

namespace Renaissance.Common.Encryption

{

    public class PgpEncryptionKeys

    {

        public PgpPublicKey PublicKey { get; private set; }

        public PgpPrivateKey PrivateKey { get; private set; }

        public PgpSecretKey SecretKey { get; private set; }

        /// <summary>

        /// Initializes a new instance of the EncryptionKeys class.

        /// Two keys are required to encrypt and sign data. Your private key and the recipients public key.

        /// The data is encrypted with the recipients public key and signed with your private key.

        /// </summary>

        /// <param name="publicKeyPath">The key used to encrypt the data</param>

        /// <param name="privateKeyPath">The key used to sign the data.</param>

        /// <param name="passPhrase">The (your) password required to access the private key</param>

        /// <exception cref="ArgumentException">Public key not found. Private key not found. Missing password</exception>

        public PgpEncryptionKeys(string publicKeyPath, string privateKeyPath, string passPhrase)

        {

            if (!File.Exists(publicKeyPath))

                throw new ArgumentException("Public key file not found", "publicKeyPath");

            if (!File.Exists(privateKeyPath))

                throw new ArgumentException("Private key file not found", "privateKeyPath");

            if (String.IsNullOrEmpty(passPhrase))

                throw new ArgumentException("passPhrase is null or empty.", "passPhrase");

            PublicKey = ReadPublicKey(publicKeyPath);

            SecretKey = ReadSecretKey(privateKeyPath);

            PrivateKey = ReadPrivateKey(passPhrase);

        }

        #region Secret Key

        private PgpSecretKey ReadSecretKey(string privateKeyPath)

        {

            using (Stream keyIn = File.OpenRead(privateKeyPath))

            using (Stream inputStream = PgpUtilities.GetDecoderStream(keyIn))

            {

                PgpSecretKeyRingBundle secretKeyRingBundle = new PgpSecretKeyRingBundle(inputStream);

                PgpSecretKey foundKey = GetFirstSecretKey(secretKeyRingBundle);

                if (foundKey != null)

                    return foundKey;

            }

            throw new ArgumentException("Can't find signing key in key ring.");

        }

        /// <summary>

        /// Return the first key we can use to encrypt.

        /// Note: A file can contain multiple keys (stored in "key rings")

        /// </summary>

        private PgpSecretKey GetFirstSecretKey(PgpSecretKeyRingBundle secretKeyRingBundle)

        {

            foreach (PgpSecretKeyRing kRing in secretKeyRingBundle.GetKeyRings())

            {

                PgpSecretKey key = kRing.GetSecretKeys()

                    .Cast<PgpSecretKey>()

                    .Where(k => k.IsSigningKey)

                    .FirstOrDefault();

                if (key != null)

                    return key;

            }

            return null;

        }

        #endregion

        #region Public Key

        private PgpPublicKey ReadPublicKey(string publicKeyPath)

        {

            using (Stream keyIn = File.OpenRead(publicKeyPath))

            using (Stream inputStream = PgpUtilities.GetDecoderStream(keyIn))

            {

                PgpPublicKeyRingBundle publicKeyRingBundle = new PgpPublicKeyRingBundle(inputStream);

                PgpPublicKey foundKey = GetFirstPublicKey(publicKeyRingBundle);

                if (foundKey != null)

                    return foundKey;

            }

            throw new ArgumentException("No encryption key found in public key ring.");

        }

        private PgpPublicKey GetFirstPublicKey(PgpPublicKeyRingBundle publicKeyRingBundle)

        {

            foreach (PgpPublicKeyRing kRing in publicKeyRingBundle.GetKeyRings())

            {

                PgpPublicKey key = kRing.GetPublicKeys()

                    .Cast<PgpPublicKey>()

                    .Where(k => k.IsEncryptionKey)

                    .FirstOrDefault();

                if (key != null)

                    return key;

            }

            return null;

        }

        #endregion

        #region Private Key

        private PgpPrivateKey ReadPrivateKey(string passPhrase)

        {

            PgpPrivateKey privateKey = SecretKey.ExtractPrivateKey(passPhrase.ToCharArray());

            if (privateKey != null)

                return privateKey;

            throw new ArgumentException("No private key found in secret key.");

        }

        #endregion

    }

}

As you can see from the code and comments, PGP has a concept of key rings. In other words there can be many keys. We assume a single key.

Now to the PGP encryption class

using System;

using System.IO;

using Org.BouncyCastle.Bcpg;

using Org.BouncyCastle.Bcpg.OpenPgp;

using Org.BouncyCastle.Security;

namespace Renaissance.Common.Encryption

{

    /// <summary>

    /// Wrapper around Bouncy Castle OpenPGP library.

    /// Bouncy documentation can be found here: http://www.bouncycastle.org/docs/pgdocs1.6/index.html

    /// </summary>

    public class PgpEncrypt

    {

        private PgpEncryptionKeys m_encryptionKeys;

        private const int BufferSize = 0x10000; // should always be power of 2 

        /// <summary>

        /// Instantiate a new PgpEncrypt class with initialized PgpEncryptionKeys.

        /// </summary>

        /// <param name="encryptionKeys"></param>

        /// <exception cref="ArgumentNullException">encryptionKeys is null</exception>

        public PgpEncrypt(PgpEncryptionKeys encryptionKeys)

        {

            if (encryptionKeys == null)

                throw new ArgumentNullException("encryptionKeys", "encryptionKeys is null.");

            m_encryptionKeys = encryptionKeys;

        }

        /// <summary>

        /// Encrypt and sign the file pointed to by unencryptedFileInfo and

        /// write the encrypted content to outputStream.

        /// </summary>

        /// <param name="outputStream">The stream that will contain the

        /// encrypted data when this method returns.</param>

        /// <param name="fileName">FileInfo of the file to encrypt</param>

        public void EncryptAndSign(Stream outputStream, FileInfo unencryptedFileInfo)

        {

            if (outputStream == null)

                throw new ArgumentNullException("outputStream", "outputStream is null.");

            if (unencryptedFileInfo == null)

                throw new ArgumentNullException("unencryptedFileInfo", "unencryptedFileInfo is null.");

            if (!File.Exists(unencryptedFileInfo.FullName))

                throw new ArgumentException("File to encrypt not found.");

            using (Stream encryptedOut = ChainEncryptedOut(outputStream))

            using (Stream compressedOut = ChainCompressedOut(encryptedOut))

            {

                PgpSignatureGenerator signatureGenerator = InitSignatureGenerator(compressedOut);

                using (Stream literalOut = ChainLiteralOut(compressedOut, unencryptedFileInfo))

                using (FileStream inputFile = unencryptedFileInfo.OpenRead())

                {

                    WriteOutputAndSign(compressedOut, literalOut, inputFile, signatureGenerator);

                }

            }

        }

        private static void WriteOutputAndSign(Stream compressedOut,

            Stream literalOut,

            FileStream inputFile,

            PgpSignatureGenerator signatureGenerator)

        {

            int length = 0;

            byte[] buf = new byte[BufferSize];

            while ((length = inputFile.Read(buf, 0, buf.Length)) > 0)

            {

                literalOut.Write(buf, 0, length);

                signatureGenerator.Update(buf, 0, length);

            }

            signatureGenerator.Generate().Encode(compressedOut);

        }

        private Stream ChainEncryptedOut(Stream outputStream)

        {

            PgpEncryptedDataGenerator encryptedDataGenerator;

            encryptedDataGenerator =

                new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.TripleDes,
                                              new SecureRandom());

            encryptedDataGenerator.AddMethod(m_encryptionKeys.PublicKey);

            return encryptedDataGenerator.Open(outputStream, new byte[BufferSize]);

        }

        private static Stream ChainCompressedOut(Stream encryptedOut)

        {

            PgpCompressedDataGenerator compressedDataGenerator =

                new PgpCompressedDataGenerator(CompressionAlgorithmTag.Zip);

            return compressedDataGenerator.Open(encryptedOut);

        }

        private static Stream ChainLiteralOut(Stream compressedOut, FileInfo file)

        {

            PgpLiteralDataGenerator pgpLiteralDataGenerator = new PgpLiteralDataGenerator();

            return pgpLiteralDataGenerator.Open(compressedOut, PgpLiteralData.Binary, file);

        }

        private PgpSignatureGenerator InitSignatureGenerator(Stream compressedOut)

        {

            const bool IsCritical = false;

            const bool IsNested = false;

            PublicKeyAlgorithmTag tag = m_encryptionKeys.SecretKey.PublicKey.Algorithm;

            PgpSignatureGenerator pgpSignatureGenerator =

                new PgpSignatureGenerator(tag, HashAlgorithmTag.Sha1);

            pgpSignatureGenerator.InitSign(PgpSignature.BinaryDocument, m_encryptionKeys.PrivateKey);

            foreach (string userId in m_encryptionKeys.SecretKey.PublicKey.GetUserIds())

            {

                PgpSignatureSubpacketGenerator subPacketGenerator =
                   new PgpSignatureSubpacketGenerator();

                subPacketGenerator.SetSignerUserId(IsCritical, userId);

                pgpSignatureGenerator.SetHashedSubpackets(subPacketGenerator.Generate());

                // Just the first one!

                break;

            }

            pgpSignatureGenerator.GenerateOnePassVersion(IsNested).Encode(compressedOut);

            return pgpSignatureGenerator;

        }

    }

}

It should be clear from the code above, but one concept that helped understand the implementation of the Bouncy classes was that they basically just creates a pipeline of streams. We expressed these as XXX ChainXXX(innerStream){} where the ChainXXX methods take the stream to wrap and returns the wrapped stream. Encapsulating this concept into small ChainXXX classes made the resulting code much more readable IMHO.

Comments, corrections and improvements are welcome as always…

kick it on DotNetKicks.com
Posted: Jan 23 2009, 12:14 PM by Kim | with 20 comment(s)
תגים:, , , ,

Comments

DotNetKicks.com said:

You've been kicked (a good thing) - Trackback from DotNetKicks.com

# January 23, 2009 12:18 PM

LaPo said:

Hi,

thank you for your article. You wrote about to encrypt, but do you have also an example from decrypt an pgp file? You divide it with me?

Thanks...

Bye Ralf

# January 31, 2009 3:27 PM

Kim said:

No I don't since we only had to send data. I have been playing with the thought of expanding the code above with some more functionality.

I thought of supporting at least:

1) Encoding (more flexible than the one above) e.g including arbitrary text and not only files.

2) Decode

3) Key management.

4) ???

If there's enough demand I'll try to find the time to expand on this.

Please leave a comment if you would like this and/or any feature you would like to see.

# January 31, 2009 9:10 PM

LaPo said:

Hello Kim,

thank's for your answer.

It will be fine, if you expand your code sample.

I have the problem, that i'm became an gpg file and i have to decrypt this file in a DotNet program.  I have search about this in the web, with little succeed. Your blog was the only...

Bye

Ralf

# February 2, 2009 12:01 PM

Firoz Ozman said:

Hi Kim

This code so useful as I am working on exactly same stuff. Everything works fine except the last step where I am trying to convert the OutputStream to a Memorystream to be used inside the Pipeline. I get System.ObjectDisposedException: Cannot access a closed file.

Please help as I need figure this out soon.

Please mail me at firozozman@gmail.com

Thanks

Firoz Ozman

# February 21, 2009 3:38 AM

Firoz Ozman said:

Kim,

No worries.. I figured out it was coz of the test windows app I was using to test. I was accessing a file from Windows App and sending across to encrypt/sign.

Hey I have a complete version of this ready.

It does Encrypt/Decrypt and Encrypt with Sign. I will post this soon on http://www.firozozman.com.

Take care

Firoz Ozman

# February 21, 2009 5:59 AM

.NET Geek said:

I can’t decide what I think about the following implementation so I decided to throw it out here. Any

# February 23, 2009 11:35 PM

Girish said:

Do you have the decrypted method too?

# February 27, 2009 5:03 AM

Girish said:

I got the decrypt method working, used the one that is available in the BouncyCastle sample and wrote a wrapper around it. Works with both signed and unsigned. Shoot me a email if you want the code

girishSIXSIX @ h0tma1l . c0m      

Tried to obfusicate it a little bit, replace SIX SIX with the number SIXTYSIX the 0's are o's and the 1 is a i

# February 27, 2009 6:22 PM

Tracy said:

Thank you for posting this.  I am still trying to figure how this works.  I had a dll that required keyid and no passPhrase but it encypts file in Unix format.  We need PC format.  Can you point me a direction on how to use bouncy castle without using passPhrase but use a keyid instead of getFirstPublicKey?

Thank you.

# April 17, 2009 2:19 AM

pavankumar said:

thnx for the code you ve given.but for pgpencryptionkeys you are passing the files right so can you please specify the file format.

# May 6, 2009 4:03 PM

pavankumar said:

can u please provide code for decryption.

# May 8, 2009 1:38 PM

Chris Porter said:

Thank you very much for this resource. My client was looking for the exact same functionality (file -> encryption -> ftp) so this was a perfect fit. I used the code almost completely as-is and it worked great. This code was combined with a small command line utility that supported the FTP side of things. We haven't done a full end-to-end test yet but I was successful in my own test environment.

I will post in the future with the end-to-end results.

# May 11, 2009 10:54 PM

Chris Porter said:

Its been awhile since I have posted my initial comment but I wanted to give a follow-up.  The code here worked with very few changes.  After pretty extensive testing, I found nothing wrong with my implementation of this code or the BouncyCastle.Crypto library.  I did have problems with the FTP library I'm using but that's a different topic.

Thanks Kim!!

# July 29, 2009 8:05 PM

Ian Patrick Hughes said:

I used the C# Bouncy library for a PGP project over a year ago. Now that I have to revisit it, I am refreshing myself and see, lo and behold, I went down a very similar path. That's reassuring if nothing else.

On the FTP side of things, what problems have you encountered? I have not found any issues using WinSCP in my managed code.

# November 4, 2009 7:54 PM

Kim said:

We have not encountered any issues with FTP so far. (running in production for a few months)

The FTP upload is part of the application and we use the .NET FtpWebRequest. Most of the files are < 100MB and transferred over a high bandwidth line. In short, no issues...

# November 5, 2009 12:43 AM

Tarek said:

How can I get data from compressedOut (WrappedGeneratorStream)?

It's still empty !

# November 5, 2009 4:13 PM

Kim said:

I'm not sure I understand your question. If you want to retrieve the original content (as it was before encryption) you can look at the samples included with Bouncy. There are many samples online.

I haven't looked at the below link thoroughly, but it should get you started.

karym6.blogspot.com/.../pgp-decryption-with-c.html

# November 5, 2009 7:47 PM

Tarek said:

Thanks Kim for you reply.

My question is when I run your "EncryptAndSign(..)" function, the stream compressedout is still empty !

Is it a pb with my keys, please see below my key generator function :

private static void ExportKeyPair(Stream secretOut, Stream publicOut,

           AsymmetricKeyParameter publicKey, AsymmetricKeyParameter privateKey,

           string identity, char[] passPhrase, bool armor)

       {            

           if (armor)

           {  

               secretOut = new ArmoredOutputStream(secretOut);

           }            

           PgpSecretKey secretKey = new PgpSecretKey(PgpSignature.DefaultCertification,

               PublicKeyAlgorithmTag.RsaGeneral, publicKey, privateKey, DateTime.Now,

               identity, SymmetricKeyAlgorithmTag.TripleDes, passPhrase, null, null,

               new SecureRandom());            

           secretKey.Encode(secretOut);

           secretOut.Close();            

           if (armor)

           {        

               publicOut = new ArmoredOutputStream(publicOut);

           }            

           PgpPublicKey key = secretKey.PublicKey;

           key.Encode(publicOut);

           publicOut.Close();

       }

       public static void GenerateKey(string username, string password, string keyStoreUrl)

       {

           IAsymmetricCipherKeyPairGenerator kpg = new RsaKeyPairGenerator();

           kpg.Init(new RsaKeyGenerationParameters(BigInteger.ValueOf(0x13), new SecureRandom(), 1024, 8));

           AsymmetricCipherKeyPair kp = kpg.GenerateKeyPair();

           FileStream out1 =

               new FileInfo(string.Format("{0}secret.asc", keyStoreUrl)).OpenWrite();

           FileStream out2 = new FileInfo(string.Format("{0}pub.asc", keyStoreUrl)).OpenWrite();

           ExportKeyPair(out1, out2, kp.Public, kp.Private, username, password.ToCharArray(), true);

       }

Thanks

# November 6, 2009 8:39 AM

Kim said:

Possibly, but it's hard for me to say without the source. Make sure the literal stream is being written to in WriteOutputAndSign(). What probably would be most helpful, is to run your code with the Bouncy code added as a project reference. Then you can step into

signatureGenerator.Generate().Encode(compressedOut);" (the last line in WriteOutputAndSign and where compressedOut is written to)

and maybe spot the problem.

# November 7, 2009 8:59 PM
Leave a Comment

(required) 

(required) 

(optional)

(required) 


Enter the numbers above: