DEV Community

ibenge Uforo
ibenge Uforo

Posted on

PGP(Pretty Good Privacy) Encryption/Decryption c#

One way or another, we would encounter a situation wherein we need to protect the privacy of data being shared over the internet or personal spaces.

PGP (short for Pretty Good Privacy), created by Phillip Zimmerman, has been touted as one of the encryption mechanisms, even considered unbreakable by governments—yes, at least known by our mortal info(source: trust me).

The decision to use PGP was driven by the limitations of asymmetric encryption, which had a size constraint on the data it could encrypt and other moving parts to sign or ensure the encrypted data was intended for the recipient (i.e., Web of trust). PGP, being asymmetric with a public and private key pair, provided the flexibility I needed.

My initial research involved exploring examples from the book by D. Hook, Bouncy Castle FIPS Java, specifically focusing on AEAD (Authenticated Encryption with Associated Data) and AE (Authenticated Encryption). Additionally, I referred to Stack Overflow discussions to address challenges, such as encountering an "invalid header" issue during decryption.

So, in basic steps, to encrypt and decrypt an item in PGP, one could summarize as follows:

  • Obtain the public and private key - This can be obtained from PGPGenerator.
  • Encrypt and Decrypt.

Obtain the Public and Private key.

After downloading the ascii-armoured file for the public and private key, which is the text-based representation of binary data (such as cryptographic keys) using ASCII characters. we would perform the following operations on it.

  1. Open the specified PGP armored file (.asc) as a stream. This could be a text file as well, just ensure the content structure remains the same
  2. Create an ArmoredInputStream from the file stream which is used to read PGP armored data.
  3. Read the contents of the ArmoredInputStream into a byte array.
  4. Create a new stream from the byte array.
  5. Initialize a PgpPublicKeyRingBundle with the byte stream which is a container holding a set of public and private key rings.
  6. Retrieve the key rings from the bundle.
  7. Extract the public key ring from the key rings which is a set of keys with associated metadata.
  8. Ensure that a public key ring is found in the specified key file.
  9. Extract the public keys from the public key ring.
  10. Return the first PGP public key from the extracted public keys.

Putting all these together, we would have.

/// <summary>
        /// Gets the public key from an pgp amoured file ie. asc
        /// </summary>
        /// <param name="publicKeyFilePath"></param>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException"></exception>
        public static PgpPublicKey GetPgpPubKey(string publicKeyFilePath)
        {
            using (Stream keyFileStream = File.OpenRead(publicKeyFilePath))
            using (ArmoredInputStream armoredInputStream = new(keyFileStream))
            {
                try
                {
                    // Read the contents of the ArmoredInputStream into a byte array
                    byte[] keyData = Streams.ReadAll(armoredInputStream);

                    // Create a new stream from the byte array
                    using Stream keyDataStream = new MemoryStream(keyData);
                    PgpPublicKeyRingBundle publicKeyRingBundle = new(keyDataStream);

                    // Assuming you want the first public key in the file
                    PgpPublicKeyRing publicKeyRing = publicKeyRingBundle.GetKeyRings().OfType<PgpPublicKeyRing>().FirstOrDefault();

                    if (publicKeyRing != null)
                    {
                        return publicKeyRing.GetPublicKeys().OfType<PgpPublicKey>().FirstOrDefault();
                    }
                    else
                    {
                        throw new InvalidOperationException("No public key ring found in the specified key file.");
                    }
                }
                catch (PgpException ex)
                {
                    Console.WriteLine($"Error reading the key file: {ex.Message}");
                    throw;
                }
                catch (IOException ex)
                {
                    Console.WriteLine($"IO error reading the key file: {ex.Message}");
                    throw;
                }
            }
        }
Enter fullscreen mode Exit fullscreen mode

Retrieving the private key is a different process as we need to add in the passphrase used in signing the private key.

  • Create a file stream to read the specified PGP armored private key file.
  • Use PgpSecretKeyRing to read the secret key ring from the file stream, decoding the armored data
    • Get the secret key from the secret key ring. In my case, I would be retrieving the first key.
  • Use the ExtractPrivateKey method of PgpSecretKey to obtain the private key, providing the passphrase as a character array

Once more, putting this together.

/// <summary>
        /// Gets the private key from a pgp amoured file ie. asc
        /// </summary>
        /// <param name="privateKeyFilePath"></param>
        /// <param name="passphrase"></param>
        /// <returns></returns>
        public static PgpPrivateKey GetPgpPrivateKey(string privateKeyFilePath, string passphrase)
        {
            try
            {
                PgpSecretKeyRing secretKeyRing;
                using (var keyFileStream = new FileStream(privateKeyFilePath, FileMode.Open))
                {
                    secretKeyRing = new PgpSecretKeyRing(PgpUtilities.GetDecoderStream(keyFileStream));
                }

                PgpSecretKey secretKey = secretKeyRing.GetSecretKey();
                PgpPrivateKey privateKey = secretKey.ExtractPrivateKey($"{passphrase}".ToCharArray());

                return privateKey;
            }
            catch (Exception ex)
            {
                // Handle exceptions, log errors, or take appropriate action.
                Console.WriteLine($"Error reading PGP private key from file: {ex.Message}");
                throw;
            }
        }
Enter fullscreen mode Exit fullscreen mode

Some issues encountered within this process

  • Invalid header exception - this was resolved by first using pgp.utilities.getdecoderstream to handle the memory stream and pass it to the underlying method.

Resources for this section.

Encrypting

In order to encrypt, we could follow these steps:

  • Create a memory stream, which enables us to buffer the object while reading.
  • Open a stream with the PgpLiteralDataGenerator, which writes the plain text data into a literal data packet.
  • Initialize a PgpEncryptedDataGenerator, configured to use AES-256 as the symmetric key algorithm, enable integrity checking, and utilize a secure random number generator for cryptographic operations.
  • Add the encryption type, which in my case is using a public key. At the time of writing, there are other methods such as using a passphrase.
  • Write the encrypted data to your specified stream.

That's enough theory, see what I mean below.

/// <summary>
/// Encrypts the provided plaintext data using the specified PGP public key.
/// </summary>
/// <param name="encryptionKey">The PGP public key used for encryption.</param>
/// <param name="plainText">The plaintext data to be encrypted.</param>
/// <returns>The encrypted data as a byte array.</returns>
public static byte[] EncryptDataWithPublicKey(PgpPublicKey encryptionKey, byte[] plainText)
{
    // Create a MemoryStream to store the encrypted data.
    using (MemoryStream encryptedStream = new())
    {
        // Create a PgpLiteralDataGenerator to generate literal data packets in PGP.
        using (PgpLiteralDataGenerator literalDataGenerator = new PgpLiteralDataGenerator())
        {
            // Open a stream with the PgpLiteralDataGenerator and write the plaintext data into a literal data packet.
            using (Stream literalDataStream = literalDataGenerator.Open(encryptedStream,
            PgpLiteralData.Binary,
            PgpLiteralData.Console,
            plainText.Length,
            DateTime.UtcNow))
            {
                literalDataStream.Write(plainText, 0, plainText.Length);
            }
        }

        // Create a MemoryStream to store the final result.
        using (MemoryStream resultStream = new())
        {
            // Create a PgpEncryptedDataGenerator with the specified symmetric key algorithm, enabling integrity protection.
            PgpEncryptedDataGenerator encryptedDataGenerator = new PgpEncryptedDataGenerator(
                SymmetricKeyAlgorithmTag.Aes256, true, new SecureRandom());

            // Add the encryption key to the generator.
            encryptedDataGenerator.AddMethod(encryptionKey);

            // Open a stream with the encryptedDataGenerator and write the encrypted data into the result stream.
            using (Stream compressedOut = encryptedDataGenerator.Open(resultStream, encryptedStream.ToArray().Length))
            {
                compressedOut.Write(encryptedStream.ToArray(), 0, encryptedStream.ToArray().Length);
            }

            // Return the encrypted data as a byte array.
            return resultStream.ToArray();
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

Decrypting.

  • We initialize a PgpObjectFactory to parse the PGP encrypted data.
  • Get the first object from the factory, which bouncycastle expects to be a PgpEncryptedDataList.
  • For situations where there are multiple recipients looping through PgpPublicKeyEncryptedData gets us the public keyid used in encrypting the data and matches it with the keyid provided by the private key. enabling us correctly decrypt the data.
  • The rest is history.

/// <summary>
/// Decrypts PGP encrypted data using the provided private key and returns the decrypted bytes.
/// </summary>
/// <param name="privateKey">The PGP private key used for decryption.</param>
/// <param name="encryptedB64String">The base64-encoded PGP encrypted data.</param>
/// <returns>The decrypted data as a byte array.</returns>
/// <exception cref="Exception">Thrown in case of decryption failure or IO exceptions.</exception>
public static byte[] DecryptDataWithPrivateKey(PgpPrivateKey privateKey, string encryptedB64String)
{
try
{
// Convert the base64-encoded PGP encrypted data to a byte array
byte[] pgpEncryptedData = Convert.FromBase64String(encryptedB64String);

    // Create a PgpObjectFactory for parsing the PGP encrypted data
    PgpObjectFactory pgpFact = new PgpObjectFactory(pgpEncryptedData);

    // Obtain the first object, which should be a PgpEncryptedDataList
    PgpEncryptedDataList encList = (PgpEncryptedDataList)pgpFact.NextPgpObject();

    // Find the correct PgpPublicKeyEncryptedData using the provided private key
    PgpPublicKeyEncryptedData? encData = null;
    foreach (PgpPublicKeyEncryptedData data in encList.GetEncryptedDataObjects())
    {
        if (data.KeyId == privateKey.KeyId)
        {
            encData = data;
            break;
        }
    }

    if (encData == null)
    {
        throw new PgpException("Provided private key not found in encrypted data.");
    }

    // Create a decrypted stream
    Stream clear = encData.GetDataStream(privateKey);

    // Create a new MemoryStream to hold the decrypted data
    using (MemoryStream decryptedStream = new MemoryStream())
    {
        // Read the decrypted data into the MemoryStream
        Streams.PipeAll(clear, decryptedStream);

        // Verify the integrity of the decrypted data
        if (encData.Verify())
        {
            // Create a PgpObjectFactory for parsing the decrypted literal data
            PgpObjectFactory litFact = new PgpObjectFactory(decryptedStream.ToArray());

            // Obtain the literal data packet
            PgpLiteralData litData = (PgpLiteralData)litFact.NextPgpObject();

            // Read the actual data from the literal data input stream
            using (Stream dataStream = litData.GetInputStream())
            {
                byte[] data = Streams.ReadAll(dataStream);
                return data;
            }
        }
        else
        {
            throw new PgpException("Modification check failed.");
        }
    }
}
catch (PgpException ex)
{
    throw new Exception("Decryption failed: " + ex.Message, ex);
}
catch (IOException ex)
{
    throw new Exception("IO exception during decryption: " + ex.Message, ex);
}

}
Enter fullscreen mode Exit fullscreen mode

It was a lengthy war, but I emerged with a wealth of knowledge on the encryption process in terms of Symmetric and Asymmetric encryption, cryptographic algorithms such as RSA and in my opinion the improvement ECC (Elliptic curve cryptography) which offers greater security with a smaller key size, wherein the equivalent of an ECC 521 key size is equal to 15360 key size!.

This is not to claim that I am now a security expert or that I've covered the entire field. as usual there are some considerations here and there, which you should be aware of and can be addressed by conducting your own security and penetration testing.

Thanks and Happy Encrypting!

External Links

Top comments (0)