I have developed a library to perform PGP signing/encryption and decryption/validation of files against one or more recipients. This part works great and works with large files using streams nicely and efficiently.
Part of the PGP Message Exchange Formats specification (RFC 1991) states the following:
...
6.7 User ID Packet
Purpose. A user ID packet identifies a user and is associated with a public or private key.
Definition. A user ID packet is the concatenation of the following fields:
(a) packet structure field (2 bytes);
(b) User ID string.
The User ID string may be any string of printable ASCII characters. However, since the purpose of this packet is to uniquely identify an individual, the usual practice is for the User ID string to consist of the user's name followed by an e-mail address for that user, the latter enclosed in angle brackets.
...
The application I am creating will need to attempt to identify the appropriate key for decrypting the files automatically so that I have as little user intervention as possible. If the key can not be identified (for example, if the recipient(s) are hidden) the application will prompt for the selection of the correct key. I am trying to make it as streamlined as possible.
The RFC suggests the packet is not part of the encrypted data which makes sense. PGP makes it easy to try and identify who encrypted the data. This is evident when you try and decrypt a file using Kleopatra when it has the relevant keys added to its key database. In this instance, it will prompt for the password protecting the secret key.
My specific question is:
How do I use the C# BouncyCastle library to read which recipients the encrypted data was intended to? In otherwords, which private key to use for decryption?
I have tried to find examples using the Bouncy Castle GitHub repository and couldn't see any that demonstrated this particular problem. I also looked at as many google search results for this question to no avail.
I found the answer to my question. I assumed that if it was part of the PGP specification then it must be possible without too much bother. I therefore decided to scrutinise the decryption process and all of the objects used throughout it.
Using the debugger I enumerated the items within the PgpEncryptedDataList
and found the key ID for the public key that encrypted it inside the individual PgpPublicKeyEncryptedData
object.
The object contains a property of type long
called KeyId
. This was the value I was looking for to match against the keys stored in the application.
The following snippet is just an example of what I used to reach the KeyId
property:
using (var inputFile = File.OpenRead(@"E:\Staging\6114d23c-2595abef\testfile.txt.gpg"))
using (var decoderStream = PgpUtilities.GetDecoderStream(inputFile))
{
var objectFactory = new PgpObjectFactory(decoderStream);
var encryptedList = (PgpEncryptedDataList)objectFactory.NextPgpObject();
foreach (var encryptedData in encryptedList.GetEncryptedDataObjects().Cast<PgpPublicKeyEncryptedData>())
{
var keyId = encryptedData.KeyId.ToString("X");
Console.WriteLine($"Encryption Key ID: {keyId}");
}
}
Setting a breakpoint after the first enumeration you can examine the encryptedData
variable and observe something similar to:
So, after all the struggle, it was actually very simple. Accessing the KeyId
during the decryption process is then straightforward and you can automagically go and grab the correct private key to do the decryption.
For completeness, it is common for PGP that files are encrypted for more than just one recipient. In this case, you will see more than one encrypted data object. It doesn't mean the data is encrypted twice. Only the session key. The session key is encrypted N number of times where N is the number of recipients. This allows each recipient to be able to decrypt one of the sessions keys and then to go ahead and decrypt the data.
Refer to the image below showing two objects, and as you would expect, two KeyId
properties :)
This snippet is from the PgpDecrypt.cs which already looks through the encrypted objects and checks the key identifier against the PgpSecretKeyRingBundle
passed in as a parameter:
foreach (PgpPublicKeyEncryptedData pked in encryptedDataList.GetEncryptedDataObjects())
{
privateKey = PgpKeyHelper.FindSecretKey(secretKeyRing, pked.KeyId, passPhrase.ToCharArray());
if (privateKey == null)
{
continue;
}
encryptedData = pked;
break;
}
For anyone wishing to have a head start with PGP, BouncyCastle and C#, please refer to my library which contains a compilation of many PGP functions. The PgpDecrypt
class can be changed to automatically incorporate the key discovery as discussed in this question.