Search code examples
c#encryptionbouncycastlepgp

BouncyCastle in-memory PGP encryption in C#


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();
}

Solution

  • 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();
            }
        }