7

如何在 .NET 工具程式安全儲存 API Key 設定

 1 year ago
source link: https://blog.darkthread.net/blog/secure-apikey-for-console-app/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

如何在 .NET 工具程式安全儲存 API Key 設定-黑暗執行緒

現代 API 服務大都是靠 API Key 管控存取權限及計算費用,因此貌似亂碼的 API Key 字串必須妥善保管,若是落入賊人之手,對方有可能看光你的資料、替你發文、幫你交易,或是大方享用服務由你買單。例如就有駭客鎖定 OpenAI 的會員服務,掃瞄 Github 原始碼用 "sk-**" 特徵蒐集開發者粗心上傳的 API Key,拿來玩 ChatGPT 玩到爽。(新聞:OpenAI 付費用戶小心──有人正開心竊取 GPT-4 金鑰,還讓他人免費使用)

因此,用明碼將 API Key 寫進設定檔很不安全,要嚴防不小心隨原始碼被複製、備份、上傳,即便有加密也要小心金鑰外流被反推解密。我心中安全的做法是完全跟原始碼及設定檔分離,改存入外部機制,像是環境變數、Registry,比存成檔案安全。像是微軟的 Azure OpenAI 程式範例,便選擇存在環境變數:

using Azure;
using Azure.AI.OpenAI;
using static System.Environment;

string endpoint = GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
string key = GetEnvironmentVariable("AZURE_OPENAI_KEY");

// Enter the deployment name you chose when you deployed the model.
string engine = "text-davinci-003";

OpenAIClient client = new(new Uri(endpoint), new AzureKeyCredential(key));

註:正式環境的 Azure 網站,可考慮將連線字串、帳號密碼、API Key 等機密資訊寫入雲端保險箱 - Azure Key Vault保存。

手邊有些 .NET 寫的 CLI 小工具有 API Key 需求,設成環境變數是不錯選擇,但 API Key 存明碼過不了我心裡的關卡,總覺得下個 CMD 或 PowerShell 指令直接曝光還不夠安全,加個密更保險。

類似需要用環境變數保存帳密 API Key 的需求不少,我決定寫成公用函式,輸入環境變數名稱,自動讀取解密;若未設定則提示輸入,加密後儲存。第一次使用 CLI 工具時設定,將 API Key 加密存入環境變數,日後沿用。至於加密演算法,由於我主要都在 Windows 執行,安全又方便的 .NET 無腦加解密 API - ProtectedData 是首選,由 Windows 內建機制保管金鑰,金鑰專屬特定機器特定使用者,加密內容拿到其他地方也解不開,很適合用來保存快取性質機密。(快取性質是指不怕遺失,重設即可)

程式邏輯很簡單,我寫成 Func<string, string> GetSecureEnvVar,輸入環境變數名稱,函式嘗試讀取並解密,不存在或解密失敗就讓使用者重新設定。未來有其他程式要用,將 GetSecureEnvVar 這段複製過去,呼叫 GetSecureEnvVar("SOME_API_KEY") 取值即可。加密部分為避免被人在本機下指令輕易解開,我再加了額外密碼熵 optionalEntropy 參數,再稍稍增加一些破解難度。(但老實說,若對方已侵門踏戶自由執行指令,能防的有限,灑幾顆樂高扎扎腳也好)

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

byte[] additionalEntropy = { 2, 8, 8, 2, 5, 2, 5, 2 };

Func<string, string> GetSecureEnvVar = (varName) => {
    var val = Environment.GetEnvironmentVariable(varName, EnvironmentVariableTarget.User);
    if (!string.IsNullOrEmpty(val)) {
        try {
            val = Encoding.UTF8.GetString(
                ProtectedData.Unprotect(Convert.FromBase64String(val), additionalEntropy, DataProtectionScope.CurrentUser));
            return val;
        }
        catch {
            Console.WriteLine("非有效加密格式,請重新輸入");
        }
    }
    Console.Write($"請設定[{varName}]:");
    val = Console.ReadLine() ?? string.Empty;
    //加密後存入環境變數
    var enc = 
        Convert.ToBase64String(
            ProtectedData.Protect(Encoding.UTF8.GetBytes(val), additionalEntropy, DataProtectionScope.CurrentUser));
    Environment.SetEnvironmentVariable(varName, enc, EnvironmentVariableTarget.User);
    return val;
};

var myApiKey = GetSecureEnvVar("MY_API_KEY");
Console.WriteLine($"ApiKey 讀取測試成功 - {myApiKey}");

範例程式執行結果如下,成功。

Fig1_638277927900307880.png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK