C# における PGP および RSA 暗号化処理の実装事例

サーバーサイド開発において、機密性の高いデータを扱う際には適切な暗号化アルゴリズムの選定と実装が不可欠です。ここでは、ファイル転送などに適した PGP と、データ通信向けの RSA について、C# での実装方法を解説します。

PGP によるファイル暗号化

PGP(Pretty Good Privacy)は、公開鍵暗号方式を用いてファイルの confidentiality と integrity を保証します。.NET 環境では Bouncy Castle ライブラリを利用するのが一般的です。以下の実装では、公開鍵による暗号化と、秘密鍵による復号化の処理をカプセル化しています。

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

namespace SecurityModules
{
    public static class OpenPgpHandler
    {
        private const int StreamBufferSize = 0x10000;

        public static void EncryptContent(string sourcePath, string destPath, string pubKeyPath, bool isArmor, bool integrityCheck)
        {
            using (var pubKeyStream = File.OpenRead(pubKeyPath))
            {
                var publicEncKey = LoadPublicKey(pubKeyStream);
                
                using (var compressedStream = new MemoryStream())
                {
                    var compressionGen = new PgpCompressedDataGenerator(CompressionAlgorithmTag.Zip);
                    PgpUtilities.WriteFileToLiteralData(
                        compressionGen.Open(compressedStream), 
                        PgpLiteralData.Binary, 
                        new FileInfo(sourcePath));
                    
                    compressionGen.Close();
                    var dataBytes = compressedStream.ToArray();

                    var encryptor = new PgpEncryptedDataGenerator(
                        SymmetricKeyAlgorithmTag.Cast5, 
                        integrityCheck, 
                        new SecureRandom());
                    
                    encryptor.AddMethod(publicEncKey);

                    using (var outStream = File.Create(destPath))
                    {
                        Stream finalStream = outStream;
                        if (isArmor)
                        {
                            finalStream = new ArmoredOutputStream(outStream);
                        }

                        using (var cryptoStream = encryptor.Open(finalStream, dataBytes.Length))
                        {
                            cryptoStream.Write(dataBytes, 0, dataBytes.Length);
                        }
                        
                        if (isArmor)
                        {
                            finalStream.Close();
                        }
                    }
                }
            }
        }

        public static void DecryptContent(string sourcePath, string privKeyPath, string passphrase, string destPath)
        {
            if (!File.Exists(sourcePath)) throw new FileNotFoundException("Encrypted file missing.", sourcePath);
            if (!File.Exists(privKeyPath)) throw new FileNotFoundException("Private key missing.", privKeyPath);

            using (var inputStream = File.OpenRead(sourcePath))
            using (var keyStream = File.OpenRead(privKeyPath))
            {
                ProcessDecryption(inputStream, keyStream, passphrase, destPath);
            }
        }

        private static void ProcessDecryption(Stream input, Stream keyIn, string pass, string output)
        {
            var pgpFactory = new PgpObjectFactory(PgpUtilities.GetDecoderStream(input));
            var secretKeyRing = new PgpSecretKeyRingBundle(PgpUtilities.GetDecoderStream(keyIn));
            
            PgpObject obj = pgpFactory.NextPgpObject();
            PgpEncryptedDataList encryptedList = obj as PgpEncryptedDataList;
            
            if (encryptedList == null)
            {
                encryptedList = (PgpEncryptedDataList)pgpFactory.NextPgpObject();
            }

            PgpPrivateKey privateKey = null;
            PgpPublicKeyEncryptedData encryptedData = null;

            foreach (PgpPublicKeyEncryptedData pked in encryptedList.GetEncryptedDataObjects())
            {
                privateKey = RetrievePrivateKey(secretKeyRing, pked.KeyId, pass.ToCharArray());
                if (privateKey != null)
                {
                    encryptedData = pked;
                    break;
                }
            }

            if (privateKey == null) throw new ArgumentException("Valid secret key not found.");

            using (var clearStream = encryptedData.GetDataStream(privateKey))
            {
                var plainFactory = new PgpObjectFactory(clearStream);
                PgpObject message = plainFactory.NextPgpObject();

                if (message is PgpCompressedData)
                {
                    var compressedData = (PgpCompressedData)message;
                    using (var compStream = compressedData.GetDataStream())
                    {
                        var innerFactory = new PgpObjectFactory(compStream);
                        message = innerFactory.NextPgpObject();
                        if (message is PgpOnePassSignatureList)
                        {
                            message = innerFactory.NextPgpObject();
                        }
                    }
                }

                if (message is PgpLiteralData)
                {
                    var literalData = (PgpLiteralData)message;
                    using (var outStream = File.Create(output))
                    {
                        var uncStream = literalData.GetInputStream();
                        Streams.PipeAll(uncStream, outStream);
                    }
                }
                else
                {
                    throw new PgpException("Unsupported message format.");
                }
            }
        }

        private static PgpPublicKey LoadPublicKey(Stream input)
        {
            var pgpPub = new PgpPublicKeyRingBundle(PgpUtilities.GetDecoderStream(input));
            foreach (PgpPublicKeyRing kRing in pgpPub.GetKeyRings())
            {
                foreach (PgpPublicKey key in kRing.GetPublicKeys())
                {
                    if (key.IsEncryptionKey) return key;
                }
            }
            throw new ArgumentException("Encryption key not found.");
        }

        private static PgpPrivateKey RetrievePrivateKey(PgpSecretKeyRingBundle bundle, long keyId, char[] pass)
        {
            var secKey = bundle.GetSecretKey(keyId);
            return secKey?.ExtractPrivateKey(pass);
        }
    }
}

利用側では、パスと鍵ファイルを指定してメソッドを呼び出すだけで処理が完結します。Armored 形式かどうかは出力先の要件に合わせて切り替えてください。

RSA による非対称暗号化

RSA は公開鍵と秘密鍵のペアを用いた暗号化方式です。従来の実装では BigInteger クラスを独自に用意する必要がありましたが、現代の .NET フレームワークでは標準ライブラリを用いることで、より安全かつ簡潔に実装できます。ここでは文字列データの暗号化・復号化ラッパーを示します。

using System;
using System.Security.Cryptography;
using System.Text;

namespace SecurityModules
{
    public static class AsymmetricCipher
    {
        public struct KeyPair
        {
            public string PublicKeyXml { get; set; }
            public string PrivateKeyXml { get; set; }
        }

        public static KeyPair GenerateKeys(int keySize = 2048)
        {
            using (var rsa = RSA.Create())
            {
                rsa.KeySize = keySize;
                return new KeyPair
                {
                    PublicKeyXml = rsa.ToXmlString(false),
                    PrivateKeyXml = rsa.ToXmlString(true)
                };
            }
        }

        public static string EncryptData(string plainText, string publicKeyXml)
        {
            try
            {
                using (var rsa = RSA.Create())
                {
                    rsa.FromXmlString(publicKeyXml);
                    var data = Encoding.UTF8.GetBytes(plainText);
                    
                    // 長すぎるデータは分割暗号化する必要があるため、ここでは AES 併用を推奨
                    // 本例では簡易的に RSA 直接暗号化を示す
                    var encrypted = rsa.Encrypt(data, RSAEncryptionPadding.Pkcs1);
                    return Convert.ToBase64String(encrypted);
                }
            }
            catch
            {
                return plainText; // エラー時は原データを返す(運用では例外処理を適切に)
            }
        }

        public static string DecryptData(string encryptedText, string privateKeyXml)
        {
            try
            {
                using (var rsa = RSA.Create())
                {
                    rsa.FromXmlString(privateKeyXml);
                    var data = Convert.FromBase64String(encryptedText);
                    var decrypted = rsa.Decrypt(data, RSAEncryptionPadding.Pkcs1);
                    return Encoding.UTF8.GetString(decrypted);
                }
            }
            catch
            {
                return encryptedText;
            }
        }
    }
}

実行例として、JSON 形式のデータを暗号化し、再度復号して整合性を確認するテストコードは以下のようになります。

static void RunSecurityTest()
{
    var payload = "{\"user\":\"admin\",\"role\":\"operator\"}";
    Console.WriteLine($"Original: {payload}");

    var keys = AsymmetricCipher.GenerateKeys(2048);
    Console.WriteLine($"Public Key: {keys.PublicKeyXml.Substring(0, 50)}...");

    var encrypted = AsymmetricCipher.EncryptData(payload, keys.PublicKeyXml);
    Console.WriteLine($"Encrypted: {encrypted}");

    var decrypted = AsymmetricCipher.DecryptData(encrypted, keys.PrivateKeyXml);
    Console.WriteLine($"Decrypted: {decrypted}");
}

RSA 単体では加密可能なデータサイズに制限があるため、実際の運用ではハイブリッド暗号方式(RSA で AES 鍵を暗号化し、AES でデータを暗号化)を採用することが推奨されます。

タグ: PGP RSA BouncyCastle C# 暗号化,セキュリティ,非対称暗号

5月16日 07:39 投稿