Open PGP 加解密 via C# | Ebank | 銀行直連 | 介接 |非對稱加密 | RSA

by | 3 月 26, 2025 | 程式 | 0 comments

Views: 2

跟某間銀行介接用到的是OpenPGP的加密技術,這邊的坑比較少,不過之前研究發現C#在這方面的資料很少。

說明

OpenPGP 是一種非對稱加密技術,也就是利用一對鑰匙(兩把一對,同時產生)來完成加密與解密的過程。非對稱加密的運作原理是:使用一把鑰匙(稱為公鑰)來加密資料,而另一把鑰匙(稱為私鑰)則用於解密。也就是說,當你使用公鑰加密檔案後,只有持有對應私鑰的人才能正確解密並讀取內容。

此外,OpenPGP 的一大優點在於密鑰的產生與管理非常靈活。使用者可以自行生成這對密鑰,並將公鑰分享給任何需要與你安全通信的人,而無需依賴第三方機構來簽署或認證密鑰。這不僅增加了加密系統的自主性,也提升了整體的安全性,因為每個使用者都能獨立掌控自己的加密流程。

  • 公鑰:加密檔案
  • 私鑰:解密檔案

開發環境

  1. .Net 8
  2. Windows 11 專業版 23H2
  3. Visual Studio 2022
  4. LinqPad 8.5.5

產生一對RSA金鑰

Windows 下我推薦透過Kleopatra這個軟體產生,這個是Linux下KDE的一個軟體,但有Windows版

Kleopatra

軟體下載位址gpg4win

裝好之後開啟

開啟Kleopatra

左上產生OpenPGP

檔案>建立新的OpenPGP金鑰對

選項說明

要填寫的主要是名字,還有Key的演算法要選銀行端支援的演算法,我這邊銀行可以接受RSA4096,也不需要效期。

建立中

建立中,這個大概等待15秒左右

建立完成

建立完成

匯出金鑰

匯出公鑰跟私鑰

對著剛產生好的金鑰按右鍵

  1. 匯出 > 匯出公鑰
  2. 備份私鑰 > 匯出私鑰

儲存好了之後就能開始寫程式了

Nuget 裝第三方套件

請安裝Portable.BouncyCastle 1.9 版以上

Open PGP C# 程式碼-在記憶體中加密

程式分成幾個部分

  1. 讀取鑰匙並顯示
  2. 驗證鑰匙是否匹配
  3. 測試加密
  4. 測試解密

這邊全部過程都是在記憶體中進行,所以很適合Web API所使用。

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

public class Program
{
    /// <summary>公鑰路徑</summary>
    public static readonly string PublicKeyTextPath = @"C:\.....\_public.asc";

    /// <summary>私鑰路徑</summary>
    public static readonly string PrivateKeyTextPath = @"C:\....\_SECRET.asc";

    public static void Main()
    {
        // 若私鑰有密碼,請填入;若無則為空字串
        string passphrase = "";

        // (方法一)先載入公私鑰,再透過輔助方法 ShowFingerprint() 顯示指紋
        PgpPublicKey pubKey = PgpHelper.ReadPublicKey(File.ReadAllBytes(PublicKeyTextPath));
        PgpPrivateKey privKey = PgpHelper.ReadPrivateKey(File.ReadAllBytes(PrivateKeyTextPath), passphrase);

        // 顯示公鑰/私鑰指紋
        //Console.WriteLine("[方法一] ====");
        Console.WriteLine($"公鑰指紋: {PgpHelper.GetFingerprint(pubKey)}");
        Console.WriteLine($"私鑰指紋: {PgpHelper.GetFingerprint(privKey, File.ReadAllBytes(PrivateKeyTextPath), passphrase)}");

        // (方法二)若只想顯示檔案裡面的公/私鑰指紋,而不需要先拿到 PgpPublicKey/PgpPrivateKey
        //Console.WriteLine("[方法二] ====");
        // 直接解析金鑰檔案 (不做加解密),只為了拿指紋
        //Console.WriteLine($"公鑰檔指紋: {PgpHelper.GetPublicKeyFingerprint(File.ReadAllBytes(PublicKeyTextPath))}");
        //Console.WriteLine($"私鑰檔指紋: {PgpHelper.GetPrivateKeyFingerprint(File.ReadAllBytes(PrivateKeyTextPath), passphrase)}");

        // 驗證兩把金鑰是否匹配
        bool isValid = PgpHelper.ValidatePgpKeyPair(PublicKeyTextPath, PrivateKeyTextPath, passphrase);
        Console.WriteLine($"金鑰檢查結果: {(isValid ? "可用 (匹配)" : "失敗或不匹配")}");

        // 測試加解密
        string plainText = "這是一段測試資料(全記憶體).  Hahahaha...";
        string encryptedBase64 = PgpHelper.EncryptString(plainText, pubKey);
        Console.WriteLine("加密結果 (Base64):");
        Console.WriteLine(encryptedBase64);

        string decryptedText = PgpHelper.DecryptString(encryptedBase64, privKey);
        Console.WriteLine("解密結果:");
        Console.WriteLine(decryptedText);
    }
}

public static class PgpHelper
{
    /// <summary>
    /// 驗證公私鑰是否匹配的範例方法 (保持原有邏輯,可自行使用)
    /// </summary>
    public static bool ValidatePgpKeyPair(string publicKeyPath, string privateKeyPath, string passphrase)
    {
        try
        {
            PgpPublicKey pubKey = ReadPublicKey(File.ReadAllBytes(publicKeyPath));
            PgpPrivateKey privKey = ReadPrivateKey(File.ReadAllBytes(privateKeyPath), passphrase);

            // 建立測試訊息
            string testMessage = "Hello PGP Key Validation Test 測試金鑰";
            byte[] testMessageBytes = Encoding.UTF8.GetBytes(testMessage);

            // 用公鑰加密
            byte[] encryptedData = EncryptData(testMessageBytes, pubKey);

            // 用私鑰解密
            string decrypted = DecryptData(encryptedData, privKey);

            // 比對解密結果
            return (decrypted == testMessage);
        }
        catch
        {
            return false;
        }
    }


    /// <summary>
    /// 使用公鑰加密一段明文字串,回傳「Base64」字串
    /// </summary>
    public static string EncryptString(string plainText, PgpPublicKey publicKey)
    {
        // 明文轉 byte[]
        byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);

        // 呼叫 EncryptData,把結果轉 Base64
        byte[] encryptedBytes = EncryptData(plainBytes, publicKey);
        return Convert.ToBase64String(encryptedBytes);
    }

    /// <summary>
    /// 使用私鑰解密一段「Base64」字串,回傳解密後的原始明文字串
    /// </summary>
    public static string DecryptString(string encryptedBase64, PgpPrivateKey privateKey)
    {
        // Base64 轉回 byte[]
        byte[] encryptedBytes = Convert.FromBase64String(encryptedBase64);

        // 呼叫 DecryptData 還原明文
        return DecryptData(encryptedBytes, privateKey);
    }

    /// <summary>
    /// 以公鑰加密資料(全記憶體處理,不壓縮),回傳加密後的 byte[]
    /// </summary>
    private static byte[] EncryptData(byte[] data, PgpPublicKey publicKey)
    {
        using (var bOut = new MemoryStream())
        {
            // 使用 ArmoredOutputStream 產生 ASCII Armor
            using (var armoredOut = new ArmoredOutputStream(bOut))
            {
                var encGen = new PgpEncryptedDataGenerator(
                    SymmetricKeyAlgorithmTag.Cast5, true, new SecureRandom());
                encGen.AddMethod(publicKey);

                using (var encOut = encGen.Open(armoredOut, new byte[1 << 16]))
                {
                    // 寫入 Literal Data (不壓縮)
                    var litGen = new PgpLiteralDataGenerator();
                    using (var litOut = litGen.Open(encOut, PgpLiteralData.Binary, "input", data.Length, DateTime.UtcNow))
                    {
                        litOut.Write(data, 0, data.Length);
                    }
                }
            }
            return bOut.ToArray();
        }
    }

    /// <summary>
    /// 以私鑰解密資料(全記憶體處理),回傳解密後的明文字串
    /// </summary>
    private static string DecryptData(byte[] encryptedData, PgpPrivateKey privateKey)
    {
        using (var encMs = new MemoryStream(encryptedData))
        {
            var decoderStream = PgpUtilities.GetDecoderStream(encMs);
            var pgpFactory = new PgpObjectFactory(decoderStream);

            // 尋找封包
            var obj = pgpFactory.NextPgpObject();
            PgpEncryptedDataList encList = (obj is PgpEncryptedDataList list)
                ? list
                : (PgpEncryptedDataList)pgpFactory.NextPgpObject();

            // 尋找加密資料 (KeyId 對應)
            PgpPublicKeyEncryptedData encData = null;
            foreach (PgpPublicKeyEncryptedData pked in encList.GetEncryptedDataObjects())
            {
                if (pked.KeyId == privateKey.KeyId)
                {
                    encData = pked;
                    break;
                }
            }
            if (encData == null)
                throw new ArgumentException("無法在封包中找到對應的 KeyId。");

            // 解密取得明文資料流
            using (Stream clearStream = encData.GetDataStream(privateKey))
            {
                var plainFactory = new PgpObjectFactory(clearStream);
                var message = plainFactory.NextPgpObject();

                if (message is PgpLiteralData literal)
                {
                    using (Stream literalData = literal.GetInputStream())
                    using (StreamReader reader = new StreamReader(literalData, Encoding.UTF8))
                    {
                        return reader.ReadToEnd();
                    }
                }
                else
                {
                    throw new PgpException("解密結果不是 Literal Data 封包。");
                }
            }
        }
    }

    /// <summary>
    /// 讀取公鑰 (byte[] 形式),回傳可用於加密的 PgpPublicKey (不打印 Fingerprint)
    /// </summary>
    public static PgpPublicKey ReadPublicKey(byte[] keyData)
    {
        using (var ms = new MemoryStream(keyData))
        {
            var decoderStream = PgpUtilities.GetDecoderStream(ms);
            var pgpPub = new PgpPublicKeyRingBundle(decoderStream);

            foreach (PgpPublicKeyRing keyRing in pgpPub.GetKeyRings())
            {
                foreach (PgpPublicKey key in keyRing.GetPublicKeys())
                {
                    if (key.IsEncryptionKey)
                        return key;
                }
            }
            throw new ArgumentException("無法在公鑰資料中找到加密用的 PgpPublicKey。");
        }
    }

    /// <summary>
    /// 讀取私鑰 (byte[] 形式),回傳 PgpPrivateKey (不打印 Fingerprint)
    /// </summary>
    public static PgpPrivateKey ReadPrivateKey(byte[] keyData, string passphrase)
    {
        using (var ms = new MemoryStream(keyData))
        {
            var decoderStream = PgpUtilities.GetDecoderStream(ms);
            var pgpSecRings = new PgpSecretKeyRingBundle(decoderStream);

            foreach (PgpSecretKeyRing keyRing in pgpSecRings.GetKeyRings())
            {
                foreach (PgpSecretKey secretKey in keyRing.GetSecretKeys())
                {
                    try
                    {
                        // 嘗試解出私鑰
                        var privKey = secretKey.ExtractPrivateKey(passphrase.ToCharArray());
                        if (privKey != null)
                            return privKey;
                    }
                    catch
                    {
                        // passphrase 錯誤或該 key 解不出,繼續嘗試下一個
                    }
                }
            }
            throw new ArgumentException("無法使用提供的 passphrase 解出對應私鑰。");
        }
    }

    /// <summary>
    /// 取得公鑰的 Fingerprint (十六進位大寫)
    /// </summary>
    public static string GetFingerprint(PgpPublicKey publicKey)
    {
        byte[] fp = publicKey.GetFingerprint();
        return BitConverter.ToString(fp).Replace("-", "").ToUpper();
    }

    /// <summary>
    /// 由於 PgpPrivateKey 本身無法直接取得指紋,
    /// 我們可以用檔案資料重新讀 SecretKey 來得到 publicKey (同一對) 的指紋。
    /// 若您已經有對應的 SecretKey,也可直接使用 secretKey.PublicKey.GetFingerprint()。
    /// </summary>
    public static string GetFingerprint(PgpPrivateKey privateKey, byte[] keyFileData, string passphrase)
    {
        // 重新解析一遍 secretKey
        using (var ms = new MemoryStream(keyFileData))
        {
            var decoderStream = PgpUtilities.GetDecoderStream(ms);
            var pgpSecRings = new PgpSecretKeyRingBundle(decoderStream);

            foreach (PgpSecretKeyRing keyRing in pgpSecRings.GetKeyRings())
            {
                foreach (PgpSecretKey secretKey in keyRing.GetSecretKeys())
                {
                    try
                    {
                        var candidate = secretKey.ExtractPrivateKey(passphrase.ToCharArray());
                        if (candidate != null && candidate.Key.Equals(privateKey.Key))
                        {
                            // 取出對應公鑰的 Fingerprint
                            byte[] fp = secretKey.PublicKey.GetFingerprint();
                            return BitConverter.ToString(fp).Replace("-", "").ToUpper();
                        }
                    }
                    catch
                    {
                        // passphrase 錯誤或該 key 解不出,繼續嘗試下一個
                    }
                }
            }
        }
        return "無法取得私鑰指紋";
    }


    /// <summary>
    /// 直接從公鑰檔案 (或 byte[]) 解析指紋,不載入整個 PgpPublicKey
    /// </summary>
    public static string GetPublicKeyFingerprint(byte[] keyData)
    {
        using (var ms = new MemoryStream(keyData))
        {
            var decoder = PgpUtilities.GetDecoderStream(ms);
            var pgpPub = new PgpPublicKeyRingBundle(decoder);

            foreach (PgpPublicKeyRing keyRing in pgpPub.GetKeyRings())
            {
                foreach (PgpPublicKey key in keyRing.GetPublicKeys())
                {
                    if (key.IsEncryptionKey)
                    {
                        byte[] fp = key.GetFingerprint();
                        return BitConverter.ToString(fp).Replace("-", "").ToUpper();
                    }
                }
            }
        }
        return "無法在公鑰資料中找到加密用 Key";
    }

    /// <summary>
    /// 直接從私鑰檔案 (或 byte[]) 解析指紋,不載入整個 PgpPrivateKey
    /// </summary>
    public static string GetPrivateKeyFingerprint(byte[] keyData, string passphrase)
    {
        using (var ms = new MemoryStream(keyData))
        {
            var decoderStream = PgpUtilities.GetDecoderStream(ms);
            var pgpSecRings = new PgpSecretKeyRingBundle(decoderStream);

            foreach (PgpSecretKeyRing keyRing in pgpSecRings.GetKeyRings())
            {
                foreach (PgpSecretKey secretKey in keyRing.GetSecretKeys())
                {
                    try
                    {
                        // 能成功 Extract 的 secretKey 即為對應私鑰
                        var privKey = secretKey.ExtractPrivateKey(passphrase.ToCharArray());
                        if (privKey != null)
                        {
                            // 對應公鑰指紋
                            byte[] fp = secretKey.PublicKey.GetFingerprint();
                            return BitConverter.ToString(fp).Replace("-", "").ToUpper();
                        }
                    }
                    catch
                    {
                        // passphrase 錯誤或該 key 解不出,繼續嘗試下一個
                    }
                }
            }
        }
        return "無法在私鑰資料中取得對應指紋";
    }
}

0 Comments

Submit a Comment

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *