BouncyCastle in-memory PGP encryption in C#

Synaps3 picture Synaps3 · Aug 22, 2014 · Viewed 12.8k times · Source

I've been trying to use the BouncyCastle library to do PGP encryption/decryption. I have some code that I need to modify to use streams only - no files.

I tried removing the PgpUtilities.WriteFileToLiteralData() and then making it return a stream, but it didn't work (output stream was empty).

To be more clear here is what the method should be:

public static Stream EncryptFile(MemoryStream inputStream, PgpPublicKey encKey, bool withIntegrityCheck)

Here is the code I need to modify:

private static void EncryptFile(Stream outputStream, string fileName, PgpPublicKey encKey, bool armor, bool withIntegrityCheck)
{

    if (armor)
        outputStream = new ArmoredOutputStream(outputStream);

    try
    {
        MemoryStream bOut = new MemoryStream();
        PgpCompressedDataGenerator comData = new PgpCompressedDataGenerator(
        CompressionAlgorithmTag.Zip);
        PgpUtilities.WriteFileToLiteralData(
        comData.Open(bOut),
        PgpLiteralData.Binary,
        new FileInfo(fileName));
        comData.Close();
        PgpEncryptedDataGenerator cPk = new PgpEncryptedDataGenerator(
        SymmetricKeyAlgorithmTag.Cast5, withIntegrityCheck, new SecureRandom());
        cPk.AddMethod(encKey);
        byte[] bytes = bOut.ToArray();
        Stream cOut = cPk.Open(outputStream, bytes.Length);
        cOut.Write(bytes, 0, bytes.Length);
        cOut.Close();
        if (armor)
            outputStream.Close();
    }

    catch (PgpException e)
    {

        Console.Error.WriteLine(e);
        Exception underlyingException = e.InnerException;
        if (underlyingException != null)
        {

            Console.Error.WriteLine(underlyingException.Message);
            Console.Error.WriteLine(underlyingException.StackTrace);

        }
    }
}

public void EncryptFile(string filePath, string publicKeyFile, string pathToSaveFile)
{
    Stream keyIn, fos;
    keyIn = File.OpenRead(publicKeyFile);
    string[] fileSplit = filePath.Split('\\');
    string fileName = fileSplit[fileSplit.Length - 1];
    fos = File.Create(pathToSaveFile + fileName + ".asc");
    EncryptFile(fos, filePath, ReadPublicKey(keyIn), true, true);
    keyIn.Close();
    fos.Close();
}

Answer

Synaps3 picture Synaps3 · Aug 24, 2014

I got it working. The code uses byte[] for input and output of both decryption and encryption - no files.

Here is the full class:

class PGP
{
    public PGP() { }

    /**
    * A simple routine that opens a key ring file and loads the first available key suitable for
    * encryption.
    *
    * @param in
    * @return
    * @m_out
    * @
    */
    public static PgpPublicKey ReadPublicKey(Stream inputStream)
    {
        inputStream = PgpUtilities.GetDecoderStream(inputStream);
        PgpPublicKeyRingBundle pgpPub = new PgpPublicKeyRingBundle(inputStream);
        //
        // we just loop through the collection till we find a key suitable for encryption, in the real
        // world you would probably want to be a bit smarter about this.
        //
        //
        // iterate through the key rings.
        //
        foreach (PgpPublicKeyRing kRing in pgpPub.GetKeyRings())
        {
            foreach (PgpPublicKey k in kRing.GetPublicKeys())
            {
                if (k.IsEncryptionKey)
                    return k;
            }
        }

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

    /**
    * Search a secret key ring collection for a secret key corresponding to
    * keyId if it exists.
    *
    * @param pgpSec a secret key ring collection.
    * @param keyId keyId we want.
    * @param pass passphrase to decrypt secret key with.
    * @return
    */
    private static PgpPrivateKey FindSecretKey(PgpSecretKeyRingBundle pgpSec, long keyId, char[] pass)
    {
        PgpSecretKey pgpSecKey = pgpSec.GetSecretKey(keyId);
        if (pgpSecKey == null)
            return null;

        return pgpSecKey.ExtractPrivateKey(pass);
    }

    /**
    * Decrypt the byte array passed into inputData and return it as
    * another byte array.
    *
    * @param inputData - the data to decrypt
    * @param keyIn - a stream from your private keyring file
    * @param passCode - the password
    * @return - decrypted data as byte array
    */
    public static byte[] Decrypt(byte[] inputData, Stream keyIn, string passCode)
    {
        byte[] error = Encoding.ASCII.GetBytes("ERROR");

        Stream inputStream = new MemoryStream(inputData);
        inputStream = PgpUtilities.GetDecoderStream(inputStream);
        MemoryStream decoded = new MemoryStream();

        try
        {
            PgpObjectFactory pgpF = new PgpObjectFactory(inputStream);
            PgpEncryptedDataList enc;
            PgpObject o = pgpF.NextPgpObject();

            //
            // the first object might be a PGP marker packet.
            //
            if (o is PgpEncryptedDataList)
                enc = (PgpEncryptedDataList)o;
            else
                enc = (PgpEncryptedDataList)pgpF.NextPgpObject();

            //
            // find the secret key
            //
            PgpPrivateKey sKey = null;
            PgpPublicKeyEncryptedData pbe = null;
            PgpSecretKeyRingBundle pgpSec = new PgpSecretKeyRingBundle(
            PgpUtilities.GetDecoderStream(keyIn));
            foreach (PgpPublicKeyEncryptedData pked in enc.GetEncryptedDataObjects())
            {
                sKey = FindSecretKey(pgpSec, pked.KeyId, passCode.ToCharArray());
                if (sKey != null)
                {
                    pbe = pked;
                    break;
                }
            }
            if (sKey == null)
                throw new ArgumentException("secret key for message not found.");

                Stream clear = pbe.GetDataStream(sKey);
                PgpObjectFactory plainFact = new PgpObjectFactory(clear);
                PgpObject message = plainFact.NextPgpObject();

                if (message is PgpCompressedData)
                {
                    PgpCompressedData cData = (PgpCompressedData)message;
                    PgpObjectFactory pgpFact = new PgpObjectFactory(cData.GetDataStream());
                    message = pgpFact.NextPgpObject();
                }
                if (message is PgpLiteralData)
                {
                    PgpLiteralData ld = (PgpLiteralData)message;
                    Stream unc = ld.GetInputStream();
                    Streams.PipeAll(unc, decoded);
                }
                else if (message is PgpOnePassSignatureList)
                    throw new PgpException("encrypted message contains a signed message - not literal data.");
                else
                    throw new PgpException("message is not a simple encrypted file - type unknown.");

                if (pbe.IsIntegrityProtected())
                {
                    if (!pbe.Verify())
                        MessageBox.Show(null, "Message failed integrity check.", "PGP Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    else
                        MessageBox.Show(null, "Message integrity check passed.", "PGP Error", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                else
                {
                    MessageBox.Show(null, "No message integrity check.", "PGP Error", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }

                return decoded.ToArray();
            }
            catch (Exception e)
            {
                if (e.Message.StartsWith("Checksum mismatch"))
                    MessageBox.Show(null, "Likely invalid passcode. Possible data corruption.", "Invalid Passcode", MessageBoxButtons.OK, MessageBoxIcon.Error);
                else if (e.Message.StartsWith("Object reference not"))
                    MessageBox.Show(null, "PGP data does not exist.", "PGP Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                else if (e.Message.StartsWith("Premature end of stream"))
                    MessageBox.Show(null, "Partial PGP data found.", "PGP Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                else
                    MessageBox.Show(null, e.Message, "PGP Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                Exception underlyingException = e.InnerException;
                if (underlyingException != null)
                    MessageBox.Show(null, underlyingException.Message, "PGP Error", MessageBoxButtons.OK, MessageBoxIcon.Error);

                return error;
            }
        }

        /**
        * Encrypt the data.
        *
        * @param inputData - byte array to encrypt
        * @param passPhrase - the password returned by "ReadPublicKey"
        * @param withIntegrityCheck - check the data for errors
        * @param armor - protect the data streams
        * @return - encrypted byte array
        */
        public static byte[] Encrypt(byte[] inputData, PgpPublicKey passPhrase, bool withIntegrityCheck, bool armor)
        {
            byte[] processedData = Compress(inputData, PgpLiteralData.Console, CompressionAlgorithmTag.Uncompressed);

            MemoryStream bOut = new MemoryStream();
            Stream output = bOut;

            if (armor)
                output = new ArmoredOutputStream(output);

            PgpEncryptedDataGenerator encGen = new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.Cast5, withIntegrityCheck, new SecureRandom());
            encGen.AddMethod(passPhrase);

            Stream encOut = encGen.Open(output, processedData.Length);

            encOut.Write(processedData, 0, processedData.Length);
            encOut.Close();

            if (armor)
                output.Close();

            return bOut.ToArray();
        }

        private static byte[] Compress(byte[] clearData, string fileName, CompressionAlgorithmTag algorithm)
        {
            MemoryStream bOut = new MemoryStream();

            PgpCompressedDataGenerator comData = new PgpCompressedDataGenerator(algorithm);
            Stream cos = comData.Open(bOut); // open it with the final destination
            PgpLiteralDataGenerator lData = new PgpLiteralDataGenerator();

            // we want to Generate compressed data. This might be a user option later,
            // in which case we would pass in bOut.
            Stream pOut = lData.Open(
            cos,                    // the compressed output stream
            PgpLiteralData.Binary,
            fileName,               // "filename" to store
            clearData.Length,       // length of clear data
            DateTime.UtcNow         // current time
            );

            pOut.Write(clearData, 0, clearData.Length);
            pOut.Close();

            comData.Close();

            return bOut.ToArray();
        }
    }