用 .NET 程式加解密不是新鮮話題,但如果是用 .NET 程式整合 USB 實體金鑰加密資料,做到沒實體金鑰不知 PIN 碼就解不開,聽起來是不是就有點意思了?

土砲 USB 金鑰 Side Project 持續進行,產生及設定金鑰、使用 GPG 加解密、登入 SSH 都沒啥問題,下一步我想拿來做些有趣應用。第一個想做的是用實體金鑰保護日常祕密,像是電腦密碼、金融密碼,甚至是 API Key。上回介紹用 ProtectedData 保護工具程式用的 API Key,ProtectedData 固然無腦方便又安全,但只要駭客能冒用你的身分遠端控制你的電腦,ProtectedData 便瞬間破功。若相關設定是用實體金鑰加密,則還有 PIN 碼跟按下實體按鈕這兩關,尤其是後者,不但要遠端控制你的電腦,還要用社交攻擊找人幫忙按 USB 金鑰上的開關,難度頓時上升 N 倍,讓人格外安心。

有名的 USB 實體金鑰 - YubiKey 其實已能整合密碼保管軟體 KeePass,但其做法對我來說複雜了點而且僅限應用於特定軟體,我希望自己有能力整合,需要什麼可以自己寫。我對保密軟體的要求很簡單 - 最好採用行之有年的公開加密標準,代表其安全性及方便性通過考驗;採用公開標準的好處是除了自己寫的程式,用第三方工具也能加解密,處理問題多了一些選擇。依此標準,GPG + 自製 USB 金鑰是絕佳選擇,做三把鑰匙不用兩百元,多兩支備份超安心。(YubiKey 要買到三支,口袋得夠深呀)

其實在 Windows 裝好 gpg4win,將保密資料寫進文字檔或 Word,下指令 gpg -ear Jeffrey -o secret.asc secret.txt 加密,gpg -d secret.asc 解密,就是最簡陋的加解密解決方案,不怎麼好用。

我的想法是寫支程式把 GPG 加解密程序包起來,並提供基本的資料檔管理、關鍵字查詢... 等功能,甚至比照 Azure Key Vault 做成超強防護的本機保險箱服務。想用 .NET 整合 GPG,最無腦的解法是用 Process.Start() 呼叫 gpg.exe,所有手敲指令辦得到的工作都能轉由用 .NET 執行。不過,GPG 有更優雅的解法 - GPG Made Easy,簡稱 GPGME,一套 C 開發的 GnuPG API 介面。想在 C# 使用 GPGME 的話,最簡單的方法是從 NuGet 下載 gpgme-sharp,在 Windows 依賴 Gpg4Win (限 32 位元)、在 Linux 要先裝 libgpgme11,便能透過 .NET 程直接執行金鑰管理、加解密、數位簽章等 GPG 作業。

參考 Github 上的範例,我寫了一支小程式完成加密及解密:

using System;
using System.Globalization;
using System.Text;
using System.IO;
using Libgpgme;
using System.Linq;

Context ctx = new Context();

// 如果要改用 Console.ReadLine() 輸入 PIN:
// ctx.PinentryMode = PinentryMode.Loopback;
// ctx.SetPassphraseFunction(MyPassphraseCallback);
// PassphraseResult MyPassphraseCallback(Context ctx, PassphraseInfo info, ref char[] passwd)

if (ctx.Protocol != Protocol.OpenPGP)
    ctx.SetEngineInfo(Protocol.OpenPGP, null, null);

// 尋找名為 Jeffrey 的金鑰
const string SEARCHSTR = "Jeffrey";
// 取得第一把符合的金鑰
var key = ctx.KeyStore.GetKeyList(SEARCHSTR, false)?
    .OfType<PgpKey>() // 需為 PgpKey 型別
    // Uid 及 Fingerprint 不可為 null
    .Where(o => o.Uid != null && o.Fingerprint != null).FirstOrDefault();

if (key == null)
{
    Console.WriteLine("未找到金鑰");
    return;
}
Console.WriteLine($"找到金鑰 {key.Uid.Name} / {key.Fingerprint}");

Action<string, string> print = (message, title) => {
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine($"** {title} **");
    Console.ResetColor();
    Console.WriteLine(message);
};

var secrettext = "Hello, world!";
print(secrettext, "明文內容");

// 宣告記憶體緩衝區存放明文
var plain = new GpgmeMemoryData();
//plain.FileName = "my_document.txt";
// 建立 BinaryWriter 物件,以 UTF8 編碼寫入記憶體緩衝區
var binwriter = new BinaryWriter(plain, Encoding.UTF8);
binwriter.Write(secrettext.ToCharArray());
binwriter.Flush();
plain.Seek(0, SeekOrigin.Begin);

// 指定純文字輸出 (Armor)
ctx.Armor = true;

// 宣告記憶體緩衝區存放密文
var cipher = new GpgmeMemoryData();
//cipher.FileName = "my_document.txt";
ctx.Encrypt(new Key[] { key }, EncryptFlags.AlwaysTrust, plain, cipher);

// 顯示加密結果
cipher.Seek(0, SeekOrigin.Begin);
string encrypted;
using (var srEnc = new StreamReader(cipher, Encoding.UTF8)) {
    encrypted = srEnc.ReadToEnd();
    print(encrypted, "加密結果");
}

// 將密文串流指向開頭
var encData = new GpgmeMemoryData();
var encWriter = new BinaryWriter(encData, Encoding.UTF8);
encWriter.Write(encrypted.ToCharArray());
encData.Seek(0, SeekOrigin.Begin);

var decryptedData = new GpgmeMemoryData();
var decrst = ctx.Decrypt(encData, decryptedData);

// 讀取解密結果
decryptedData.Seek(0, SeekOrigin.Begin);
using var srDes = new StreamReader(decryptedData, Encoding.UTF8);
print(srDes.ReadToEnd(), "解密結果");

// 顯示加密金鑰資訊
Console.ForegroundColor = ConsoleColor.Cyan;
if (decrst.Recipients != null) 
    Console.WriteLine(
        string.Join("\n",
            decrst.Recipients!
            .Select(r => $"金鑰={r.KeyId} 加密演算法={Gpgme.GetPubkeyAlgoName(r.KeyAlgorithm)}").ToArray()));
Console.ResetColor();

程式使用 gpgme-sharp 程式庫,先建立 Context 物件,從 KeyStore 以使用者名稱找到金鑰,進行加密。解密時由密文附加資訊識別為實體金鑰,要求 PIN 碼及按確認鈕才能解密。如此,一個用 C# 整合實體金鑰加解密的 PoC 就完成囉~

C# GPG 解密實測

Example of using gpgme-sharp library to encrypt and decrypt data with GPG USB tokey.


Comments

Be the first to post a comment

Post a comment