現代 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}");

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

Tips of how to use ProtectedData.Protect() to keep API key securely in environment variable.


Comments

Be the first to post a comment

Post a comment