In a recent software development project, I was required to be able to read PGP encrypted content, do something, and write PGP encrypted result. The project allowed calling of PGP (or GPG) command line to do the encryption and decryption needed. However that did not feel right to me. Googled a bit and then I found the Bouncy Castle C# Crypto APIs, that among everything else, included support for PGP without having any external dependency.

The compiled assembly for the Bouncy Castle APIs can be downloaded from the website, however it was compiled for the .NET 1.1 runtime version. You can easily download the source code instead and compile it for the other .NET versions if you want to. I managed to compile the source code for the .NET 2.0 runtime version without any changes to the original source code.

The bigger issue at using the Bouncy Castle APIs is that it does not seem to have any proper documentation for it. There are examples provided in the downloadable source code which you can read through to figure out how to do things. I was lazy at that time, and managed to find this blog entry: PGP Single Pass Sign and Encrypt with Bouncy Castle that provided me with the code to encrypt file contents using PGP.

From that code, I refactored it bit by bit:

  • Factored out the actualFileName (string) parameter so that the actual input content can be from any source, not just physical files.
  • Factored out the keyIn (Stream) and keyId (long) parameters so that the handling of the secret key that will be used to sign the input content can be externalized from the sign and encrypted method itself.
  • Removed the armor (bool) and withIntegrityCheck (bool) parameters and made both of it as constants of true instead so that the generated output will always be armored and integrity-checked. It can easily be made into parameters back again if need to.
  • Removed the embeddedFileName (string) parameter and made it as a constant of empty string since the file name value is not important to generate proper signed-and-encrypted output.
  • Made it into a .NET Stream so that allow it to be used as any other .NET Streams and the output can be chained to any imaginable .NET Streams such as MemoryStream, HttpResponse’s OutputStream, etc. Usage example will be provided at the end of this blog post.

The Code for the .NET Stream

using System;
using System.IO;
using Org.BouncyCastle.Bcpg;
using Org.BouncyCastle.Bcpg.OpenPgp;
using Org.BouncyCastle.Security;

class PgpSignAndEncryptStream : Stream
{
	readonly Stream outputStream;
	readonly Stream armoredOutputStream;
	readonly PgpEncryptedDataGenerator encryptedDataGenerator;
	readonly Stream encryptedOutputStream;
	readonly PgpCompressedDataGenerator compressedDataGenerator;
	readonly Stream compressedOutputStream;
	readonly PgpSignatureGenerator signatureGenerator;
	readonly PgpLiteralDataGenerator literalDataGenerator;
	readonly Stream literalOutputStream;
	bool closed = false;

	public PgpSignAndEncryptStream(Stream outputStream, PgpPublicKey encryptionPublicKey, PgpSecretKey signatureSecretKey, string signatureSecretPassword)
	{
		//
		// literal + signature -> compress -> encrypt -> armored -> output
		//

		const int BUFFER_SIZE = 1 << 16; // should always be power of 2
		const bool armor = true;
		const bool withIntegrityCheck = true;
		const string fileName = "";
		DateTime modificationTime = DateTime.UtcNow;

		this.outputStream = outputStream;

		//
		// Always create armored output
		//
		if (armor) {
			outputStream = new ArmoredOutputStream(outputStream);
			armoredOutputStream = outputStream;
		}

		//
		// Init encrypted data generator
		//
		encryptedDataGenerator =
			new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.Cast5, withIntegrityCheck, new SecureRandom());
		encryptedDataGenerator.AddMethod(encryptionPublicKey);
		encryptedOutputStream = encryptedDataGenerator.Open(outputStream, new byte[BUFFER_SIZE]);

		//
		// Init compression
		//
		compressedDataGenerator = new PgpCompressedDataGenerator(CompressionAlgorithmTag.Zip);
		compressedOutputStream = compressedDataGenerator.Open(encryptedOutputStream);

		//
		// Init signature
		//
		signatureGenerator = new PgpSignatureGenerator(signatureSecretKey.PublicKey.Algorithm, HashAlgorithmTag.Sha1);
        PgpPrivateKey signaturePrivateKey = signatureSecretKey.ExtractPrivateKey(signatureSecretPassword.ToCharArray());
		signatureGenerator.InitSign(PgpSignature.BinaryDocument, signaturePrivateKey);
		foreach (string userId in signatureSecretKey.PublicKey.GetUserIds()) {
			PgpSignatureSubpacketGenerator signatureSubpacketGenerator = new PgpSignatureSubpacketGenerator();
			signatureSubpacketGenerator.SetSignerUserId(false, userId);
			signatureGenerator.SetHashedSubpackets(signatureSubpacketGenerator.Generate());
			// Just the first one!
			break;
		}
		signatureGenerator.GenerateOnePassVersion(false).Encode(compressedOutputStream);

		//
		// Create the literal data generator output stream
		//
		literalDataGenerator = new PgpLiteralDataGenerator();
		literalOutputStream = literalDataGenerator.Open(compressedOutputStream, PgpLiteralData.Binary,
											   fileName, modificationTime, new byte[BUFFER_SIZE]);
	}

	public override void Flush()
	{
		literalOutputStream.Flush();
	}

	public override long Seek(long offset, SeekOrigin origin)
	{
		throw new NotSupportedException();
	}

	public override void SetLength(long value)
	{
		throw new NotSupportedException();
	}

	public override int Read(byte[] buffer, int offset, int count)
	{
		throw new NotSupportedException();
	}

	public override void Write(byte[] buffer, int offset, int count)
	{
		literalOutputStream.Write(buffer, offset, count);
		signatureGenerator.Update(buffer, offset, count);
	}

	public override void Close()
	{
		if (closed) {
			return;
		}

		closed = true;

		literalOutputStream.Close();
		literalDataGenerator.Close();
		signatureGenerator.Generate().Encode(compressedOutputStream);
		compressedOutputStream.Close();
		compressedDataGenerator.Close();
		encryptedOutputStream.Close();
		encryptedDataGenerator.Close();

		if (armoredOutputStream != null) {
			armoredOutputStream.Close();
		}

		outputStream.Close();
	}

	public override bool CanRead
	{
		get { return false; }
	}

	public override bool CanSeek
	{
		get { return false; }
	}

	public override bool CanWrite
	{
		get { return true; }
	}

	public override long Length
	{
		get { throw new NotSupportedException(); }
	}

	public override long Position
	{
		get { throw new NotSupportedException(); }
		set { throw new NotSupportedException(); }
	}
}

Usage Example of the .NET Stream

Assumption made for now: you know how to actually get the public key and the secret key out of the keyring files. Later I will write another post on how to get the keys.

Using StreamWriter to write encrypted string to a file

using System.IO;
using Org.BouncyCastle.Bcpg.OpenPgp;

...

PgpPublicKey publicKey = null; // get this value from somewhere
PgpSecretKey secretKey = null; // get this value from somewhere
string password = null; // get this value from somewhere

Stream outputStream = File.OpenWrite("output.txt"); // this can be replaced with any other output stream e.g. MemoryStream
PgpSignAndEncryptStream pgpOutStream = new PgpSignAndEncryptStream(outputStream, publicKey, secretKey, password);
StreamWriter writer = new StreamWriter(pgpOutStream);

writer.Write("This text will be signed and encrypted into output.txt");

writer.Close();
pgpOutStream.Close();
outputStream.Close();

Sign and encrypt one whole input file

using System.IO;
using Org.BouncyCastle.Bcpg.OpenPgp;

...

PgpPublicKey publicKey = null; // get this value from somewhere
PgpSecretKey secretKey = null; // get this value from somewhere
string password = null; // get this value from somewhere

Stream inputStream = File.OpenRead("input.txt");
Stream outputStream = File.OpenWrite("output.txt");
PgpSignAndEncryptStream pgpOutStream = new PgpSignAndEncryptStream(outputStream, publicKey, secretKey, password);

byte[] buffer = new byte[4096];
int readCount;
while ((readCount = inputStream.Read(buffer, 0, buffer.Length)) > 0) {
    outputStream.Write(buffer, 0, readCount);
}

outputStream.Close();
inputStream.Close();

Pending Related Posts

  • As mentioned earlier, a post on how to get the public keys and secret keys out of the keyring files. And probably on how to use GPG to import the keys into the keyring files as I can’t seem to find out how to programmatically import a public key block into a public key ring file.
  • PGP Decrypt and Single Pass Signature Verify .NET Stream. The working code is already in my hard disk, will share it later.