Windows 應用程式不要把機密資訊以明文存進設定檔的最佳實踐
前一篇 「Windows 應用程式開發中為了守住最底限資安的檢查表」 裡,
寫下了「不要把機密資訊放進原始碼或明文設定」「Win32 / .NET 的話就用 DPAPI / ProtectedData」這條最底限。
這次把其中 「用 DPAPI 讓它至少比明文好一點」 這一塊再挖深一點。
適用對象是下列這類 Windows 應用程式。
- WPF / WinForms / WinUI 的桌面應用程式
- C# / .NET 的 Windows 客戶端
- 會忍不住把連線端憑證或 API Token 存進本機設定檔的應用程式
這篇要談的是,「對於不得不存在本機的機密,至少不要放任它在 appsettings.json 裡以明文放著」 的現實設計。
這不是「任何攻擊者都打不穿的完全防禦」的話題。那種說法一旦過頭,資訊安全會瞬間變成怪談。
1. 先說結論
先把結論寫出來,實務上依下列順序思考會比較清楚。
- 根本上不要讓客戶端持有長期機密
- 優先採用 Windows 驗證、整合驗證、使用者互動式登入、伺服端機密管理
- 如果真的必須存在本機,就不要以明文存放
- Windows 上首先考慮 DPAPI /
ProtectedData
- Windows 上首先考慮 DPAPI /
- 一般桌面應用程式基本用
DataProtectionScope.CurrentUserLocalMachine的適用範圍相當窄
- DPAPI 不是用來擋「端末已被完全淪陷」這種狀況的
- 以相同使用者權限執行的程式碼,原則上能解密該使用者能解密的東西
而這篇文章最重要的論點是下面這個。
「反正金鑰總得存在某處,那不管是明文還是 DPAPI,就資安來說不都一樣嗎?」
這句話一半對,但結論錯了。
- 自己做 AES + 金鑰放在同一個應用程式或同一個設定檔 的話,其實相當接近明文
- 但 DPAPI 是把金鑰管理交給 OS,並把能解密的主體綁定到「那個 Windows 使用者」或「那台電腦」
- 結果,對於 設定檔單獨外流、被帶到別台 PC、誤寄、備份外流、不小心混進儲存庫 這類事故的防禦力會有很大的差異
換言之,
只看「金鑰放在某處」這個抽象說法的話是一樣,但
「誰,在什麼情境下,能多簡單地用得到」這件事完全不同。
把「把鑰匙放在玄關踏墊下」和「到管理室本人認證後才拿鑰匙」當成同一件事,有點粗暴。
2. 為什麼明文設定危險
明文保存危險的理由,比起密碼學,更多是很「土」的現實。實務上大致是以下幾種管道漏出去。
- 設定檔就這樣被放進 Git
- 故障調查用的 ZIP 裡整個設定檔被打包進去
- 回報問題時被要求附上設定檔
- 備份或檔案分享讓第三者讀到
- 日誌裡直接印出連線字串或 Token
- 離職員工或其他使用者能讀到同一台電腦上的檔案
明文最大的問題是 「被讀到的那一瞬間,機密就已經結束」。
- 檔案被打開就結束
- 被複製就結束
- 被附在信件就結束
- 一旦留在儲存庫,就要半永久地背著它
攻擊者根本不需要很高深的技術。
「用文字編輯器就打得開」這件事本身就已經相當弱。
3. 對「反正金鑰總會存在某處,那不是一樣嗎?」的回答
這個疑問很合理。
而且如果這裡回答得太草率,整篇資安文章會立刻變得朦朧。
先講結論:以「某處一定要有金鑰」這層意思來說 yes,但要說「所以都一樣」就是 no。
3.1. 哪裡一樣、哪裡不一樣
的確,加密最後總得有某個 root of trust。
機密不會從宇宙某處免費冒出來。這一塊沒有寬待。
但資安上的差異,是由下列 3 點決定的。
- 金鑰是不是應用程式自己在持有
- 金鑰綁在哪個主體身上
- 只有檔案被拿走時是否能解開
把這些差異粗略整理成表,大致如下。
| 方式 | 設定檔被讀到 | 只有檔案被帶到別台 PC | 被同一台 PC 的其他使用者讀到 | 以相同使用者權限執行的程式碼 |
|---|---|---|---|---|
| 明文 | 當場外流 | 直接外流 | 直接外流 | 當然讀得到 |
| 自做加密+金鑰放在同一設定/執行檔 | 相當會漏 | 相當會漏 | 相當會漏 | 當然解得開 |
DPAPI + CurrentUser |
檔案單獨無法立刻讀 | 通常難以解密 | 通常難以解密 | 可以解密 |
DPAPI + LocalMachine |
檔案單獨無法立刻讀 | 在別台 PC 通常難以解密 | 同一台 PC 上廣泛可解密 | 可以解密 |
這裡重要的是,DPAPI 把「能讀檔案」和「能使用機密」分開。
在明文情境下,這兩件事是一樣的。
檔案讀得到,機密也就讀得到。
但在 DPAPI 下,至少 CurrentUser 時,必須滿足:
- 以該 Windows 使用者身分
- 在該 Windows 的上下文中
- 透過 OS 的保護機制
才能解密。
這個差距在事故現場會相當大。
3.2. 「但同一個使用者的話還是解得開吧?」——沒錯
這一點要老老實實地寫出來。
以相同使用者權限執行的程式碼,原則上能解開該使用者可解的內容。
也就是說,DPAPI 並不是為下列情境設計的。
- 端末已經被惡意軟體攻陷
- 攻擊者能以那個使用者的身分執行程式碼
- 端末管理者層級已完全被接管
這種狀態下,既然應用程式本身也能解密,攻擊者程式碼也就解得開。
在這個場景下說「可是我有加密」其實不太能讓人安心。
DPAPI 比較能發揮的是「檔案外流、放錯位置、離線帶走、被其他使用者看到」這些側面。
這邊如果搞錯,會同時發生:
- 把能守的東西低估而不用
- 把守不住的東西高估而安心
兩種狀況。兩邊都默默地很危險。
3.3. 所以到底好在哪
DPAPI 的好處一句話講完就是:
「能把機密本身,從設定檔的可讀性裡切開。」
舉例來說,下列事故在明文和 DPAPI 之間就會出現差異。
- 使用者把設定檔寄給客服
- 調查用 ZIP 裡塞了設定檔
- 備份中只有設定檔外流
- 被複製到共享資料夾
- 開發者只看得到密文而看不到內容
這些是相當現實的好處。
不需要把攻擊者想成電影裡的超人,也能把日常事故的半徑縮小。
4. DPAPI 剛剛好的理由
在 Windows 上處理本機保存的機密時,DPAPI 在實務上剛剛好的理由如下。
4.1. 可以把金鑰管理推給 OS
自己生成 AES 金鑰、保存、給權限、輪替、考慮外洩影響,還要加上竄改偵測。
比想像中沉重。而且做得不認真的話,大概就是把金鑰放在同一個地方結束。
用 DPAPI 就能 把「加密金鑰怎麼做、放哪裡」這個問題,從應用程式實作中切開。
從這個意義上,與其把 DPAPI 看成
「挑選加密演算法的 API」,更接近它本質的說法是「把金鑰管理委託給 OS 的 API」。
4.2. 能把解密主體綁在 Windows 使用者或電腦上
一般桌面應用程式,絕大多數情境都可以選 CurrentUser。
- 以該使用者已登入
- 以該使用者上下文在執行
為前提解密。
這使得 只把密文複製到別台 PC 也很難直接使用 的性質變成可得。
4.3. 較容易順便附帶竄改偵測
自做加密常見的是,
「反正我 AES 加密了就結束」然後 忘了加上竄改偵測。
DPAPI 對加密資料本身就具備完整性保護,所以
暗中偷改密文的偵測 也順便交給 OS 的機制,實務上是一個好處。
4.4. 從 C# / .NET 自然就能使用
C# 直接用 System.Security.Cryptography.ProtectedData 就可以。
不用再加掛多餘的程式庫,對 Windows 專用程式來說幫助不小。
5. DPAPI 能守、不能守的東西
這裡最好明確分開,比較安全。
5.1. 變得比較能守的
DPAPI 至少在下列場面是有效的。
- 設定檔明文外洩
- 把檔案帶到別台 PC
- 同一台 PC 上其他使用者嘗試讀取(
CurrentUser前提) - 以備份或附件形式外流
- 開發/維運現場「不小心就被讀到」的狀態
5.2. 守不住或很弱的
另一方面,在下列情境就不要太相信它。
- 以相同使用者權限執行的攻擊程式碼
- 端末本身完全淪陷
- 被管理員權限接管
- 應用程式解密後,記憶體裡的明文
- 被灑給所有客戶端、共用的長期機密
最後那個「所有客戶端共用的長期機密」特別重要。
例如:
- 所有客戶嵌入同一把 API 金鑰
- 所有端末用同一把共用密碼
- 只靠客戶端就能完成的固定解密金鑰
這類設計 只要有一台被破,就很容易波及整體。
DPAPI 對於「把保存地點做得比明文好」是有效的,
但 它無法替「本來就不該放在客戶端的機密」背書。
6. CurrentUser 與 LocalMachine 的使用區分
這裡相當重要。隨便選會讓意義完全不同。
6.1. 基本是 CurrentUser
一般 Windows 桌面應用程式,首先以 CurrentUser 為基本。
適合的例子:
- WPF / WinForms / WinUI 的使用者向桌面應用程式
- 每位使用者各自持有設定或憑證的應用程式
- 把設定放在
%LocalAppData%或%AppData%底下的應用程式
這種情況下,會變成 「那個 Windows 使用者的機密」,處理起來比較自然。
6.2. LocalMachine 的用途相當有限
LocalMachine 看起來很方便,但對一般桌面應用程式來說範圍太廣。
適合的場景,例如:
- 信任的單一用途機器上的 Windows 服務
- 只會在那台機器的特定 Process 使用的機密
- 必須跨登入使用者在同一台機器上共用的情境
不過注意事項相當沉重。
- 那台 PC 上執行的各種 Process 都能廣泛解密
- 共用端末、RDS、跳板、多使用者共用環境容易變得危險
- 以「反正大家都能用很方便」為由選它,通常後面都會後悔
6.3. 猶豫時這樣想
- 一般 UI 應用程式 ->
CurrentUser - 真的需要以機器為單位守住的特殊情況 ->
LocalMachine - 任何使用者都要能解密、但端末上還有其他使用者在 -> 通常最好從設計重看一次
6.4. 服務或 impersonation 時注意事項會變多
把 Windows 服務或 impersonation 攪在一起時,CurrentUser 的意思會變重。
- 執行帳號是誰
- 該帳號的 Profile 是不是已被載入
- 解密時機是在哪個上下文
只要這些對不上,就很容易 「加密得起來但解不開」。
服務用途通常不能「反正先 CurrentUser」就解決。
7. 實作的最底限方針
在 Windows 應用程式中,光是「不要再用明文設定」,設計其實不用搞得太複雜。
不過有幾個不想漏掉的要點。
7.1. 只保護機密本身
與其整個設定全都加密,不如先只保護 機密欄位,比較好處理。
例如,像下面這樣分開。
- Server URL
- 使用者名稱
- DB 名稱
- 功能旗標
這些通常可以維持明文。
另一方面,
- 密碼
- API Token
- Refresh Token
- 共享資料夾憑證
是保護對象。
這樣分之後,
- 編輯設定比較容易
- 比對差異比較容易
- 清楚知道哪裡是機密
- 整體運維比較簡單
會同時成立。
7.2. 儲存位置以 per-user 為基本
一般桌面應用程式,儲存位置基本選在下列這類 per-user 位置。
%LocalAppData%\Vendor\App\settings.json%AppData%\Vendor\App\settings.json
至少 不要隨便放在安裝目錄底下或容易被共享的地方。
就算用 DPAPI 保護,儲存位置 ACL 亂七八糟的話,
就會變成「密文被讀到」「設定結構被看光」「運維失誤照樣發生」的故事。防禦不要只有一層,要疊起來才有效。
7.3. optionalEntropy 不是萬能的第二把金鑰
ProtectedData 可以傳入 optionalEntropy。
很方便,但 「把它嵌進執行檔就會變安全的第二把魔法金鑰」這種東西是不存在的。
- 放在同一個檔案裡,就不會是秘密
- 用固定值塞進執行檔,也稱不上是強機密
- 不過對用途識別或誤用防止仍有幫助
實務上用法,比如:
- 應用程式名稱
- 用途名稱
- 版本識別字
以固定的 byte 陣列傳入,
為了「不要不小心接受到其他用途的密文」 使用,拿捏這種程度就剛好。
7.4. 不是說「密文就可以丟進 Git」
這點低調但重要。
DPAPI 的密文比明文好得多,但
並不代表設定檔可以整個放進儲存庫。
理由很直白:
- 密文會留很久
- 總有一天同一台端末、同一個上下文可能會被重現
- 檔案裡還有機密以外的資訊
- 容易養出「反正有保護,可以隨便對待」的文化
「比明文好」 和
「放哪裡都安全」
是完全不同的兩件事。
7.5. 不要寫進日誌
意外常見的陷阱,是解密之後把東西寫進日誌,前面的努力就全白費。
- 連線失敗時把整個連線字串印出來
- API 401 時保留 Authorization header
- 把機密混進例外訊息
只要做出上面那些事,就算不再用明文設定檔,日誌最後還是會變成明文倉庫。
雖然很悲哀,但相當寫實。
8. C# / .NET 的最小實作範例
下面是一個用 CurrentUser 保護準備存進設定檔的字串的最小例子。
為了用途識別放了固定的 optionalEntropy,但 千萬不要把它當成秘密金鑰。
using System;
using System.Security.Cryptography;
using System.Text;
public static class DpapiSecretProtector
{
// 用途識別用。不是第二把秘密金鑰。
private static readonly byte[] Entropy =
Encoding.UTF8.GetBytes("ComComponent:DesktopApp:SettingsSecret:v1");
public static string ProtectToBase64(string plaintext)
{
ArgumentNullException.ThrowIfNull(plaintext);
byte[] plainBytes = Encoding.UTF8.GetBytes(plaintext);
byte[] protectedBytes = Array.Empty<byte>();
try
{
protectedBytes = ProtectedData.Protect(
plainBytes,
optionalEntropy: Entropy,
scope: DataProtectionScope.CurrentUser);
return Convert.ToBase64String(protectedBytes);
}
finally
{
Array.Clear(plainBytes, 0, plainBytes.Length);
if (protectedBytes.Length > 0)
{
Array.Clear(protectedBytes, 0, protectedBytes.Length);
}
}
}
public static string UnprotectFromBase64(string protectedBase64)
{
ArgumentNullException.ThrowIfNull(protectedBase64);
byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
byte[] plainBytes = Array.Empty<byte>();
try
{
plainBytes = ProtectedData.Unprotect(
protectedBytes,
optionalEntropy: Entropy,
scope: DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(plainBytes);
}
finally
{
Array.Clear(protectedBytes, 0, protectedBytes.Length);
if (plainBytes.Length > 0)
{
Array.Clear(plainBytes, 0, plainBytes.Length);
}
}
}
}
使用方式很單純。
string protectedPassword = DpapiSecretProtector.ProtectToBase64(password);
// 儲存到 JSON 等
// settings.DbPasswordProtected = protectedPassword;
string password = DpapiSecretProtector.UnprotectFromBase64(settings.DbPasswordProtected);
設定檔可以做成例如下面這樣。
{
"ApiBaseUrl": "https://api.example.com/",
"UserName": "app-user",
"PasswordProtected": "AQAAANCMnd8BFdERjHoAwE..."
}
這種形狀的好處在於:
- URL 和使用者名稱可以照常編輯
- 只有密碼被保護
- 設定結構很好讀
- 比明文擺在那邊不容易出事
9. 即使如此仍然危險的設計
即便用了 DPAPI,下列設計仍然危險。
9.1. 把解密後的值到處長時間帶著走
把解密後的值:
- 寫進日誌
- 顯示在畫面上
- 塞進例外
- 長時間掛在長壽命物件上
這些都是想避免的行為。
「存的時候加密」 和
「使用期間也安全」
是兩件不同的事。
9.2. 讓所有安裝共用同一份機密
例如讓所有使用者共用同一把 API 金鑰,這種設計就算用 DPAPI 存也不能根本解決。
因為 只要任何一台上應用程式能解密,那份機密就能被抽出來。
這類機密應該:
- 放到伺服端
- 客戶端只持有 Token
- 改成以使用者為單位的憑證
- 採用帶有期限的 Token
往這些方向挪動比較好。
9.3. 因為「比較輕鬆」就選 LocalMachine
這種情況真的很常見。
- 使用者切換還是讀得到
- 服務也能讀
- 能動就方便
這些理由都會讓人很想選 LocalMachine。
但對一般桌面應用程式來說,這個選擇會
「把解密可能性擴大到那台 PC 上的其他行程」,
意義相當不同。
9.4. 再疊一層自家加密就以為安心
用自己的加密取代 DPAPI,例如:
- 把 AES 金鑰寫死在原始碼
- 把 AES 金鑰寫在設定檔的另一個欄位
- 把「稍微混淆過的字串」當金鑰用
這些做法大多效果有限。
「不是明文」和「安全」之間還有一條相當大的溝。
10. DPAPI 不夠用的情境
DPAPI 方便,但不是萬能。下列情境應該考慮其他方案。
10.1. 想在 Windows 以外也跑
DPAPI / ProtectedData 是 Windows 專用的。
跨平台應用程式無法以此為前提建構。
10.2. 想要在多台機器、多位使用者間處理同一份機密
想讓同一份密文在多台 PC 都能解開、想讓多位使用者共用,
都跳出了「把機密綁在那台端末、那位使用者身上」這個 DPAPI 的拿手領域。
這種情況應該考慮:
- 伺服端的機密管理
- 憑證管理基礎架構
- Windows 驗證 / 整合驗證
- 應用程式用的憑證庫
依需求採用其他設計。
10.3. 要保存的就是使用者的帳號密碼本身
如果是 packaged desktop app / WinUI 系,而且保存對象明確是:
- 使用者名稱
- 密碼
這組,那麼 Credential Locker 也是可選之一。
不過本文的主軸仍然是 「Windows 客戶端不要再用明文設定檔」 的 DPAPI 實務線。
11. 實務上建議的優先順序
最後,如果在實務中猶豫,依下列順序思考會比較好整理。
優先 1:一開始就不要持有
- Windows 驗證
- 整合驗證
- 互動式登入
- 在伺服端保管機密
- 短命 Token
優先 2:向「每位使用者的機密」靠攏
- 捨棄共用機密、改走 per-user
- 捨棄長期固定憑證、改用可更新的 Token
- 避免所有客戶端共用同一把金鑰
優先 3:如果必須本機保存,就用 DPAPI
- 通常是
CurrentUser - 儲存位置以 per-user
- 只保護機密欄位
- 不要寫進日誌
優先 4:LocalMachine 當例外處理
- 是否真的必須以機器為單位
- 那台端末會不會有其他使用者登入
- 就服務設計而言是否合理
12. 結語
在 Windows 應用程式中必須把機密資訊存進設定檔時,
應盡量避免以明文放著。
然後對於:
「反正金鑰總得存在某處,那不都一樣嗎?」
這個疑問,比較實務的回答是:
- 自己做加密、金鑰放在同一個地方的話,幾乎一樣
- DPAPI 並不一樣
- 可以把金鑰管理推給 OS
- 可以把解密主體綁在 Windows 使用者/電腦上
- 能避免讓檔案單獨外流直接等於機密外流
- 但是
- 以相同使用者權限執行的程式碼
- 完全被攻陷的端末
- 根本不該放在客戶端的長期共用機密
這幾件事,它解決不了。
換言之,DPAPI 不是 萬能的城牆。
但它至少能 把「設定檔明文」這面整個透光的玻璃,換成一面起碼像樣的窗戶。
在 Windows 客戶端的實務中,這個差距相當大。
先從這裡不要搞錯開始,才是最現實的切入點。
13. 參考資料
- 前一篇文章: https://comcomponent.com/blog/2026/03/14/001-windows-app-security-minimum-checklist/
- Microsoft Learn:
CryptProtectData
https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata - Microsoft Learn:
ProtectedData
https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata?view=windowsdesktop-10.0 - Microsoft Learn:
DataProtectionScope
https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.dataprotectionscope?view=windowsdesktop-10.0 - Microsoft Learn: How to: Use Data Protection
https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection - Microsoft Learn: Credential locker for Windows apps
https://learn.microsoft.com/en-us/windows/apps/develop/security/credential-locker
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
Windows 應用程式中把「僅需要系統管理員權限的處理」分離出來的具體寫法
本文以 .NET 8 桌面應用程式為例,具體展示如何讓 UI 保持 asInvoker,把僅需系統管理員權限的處理切到 helper EXE。涵蓋 manifest、runas 啟動、具名管道 ACL、PID 驗證、固定 operation 與請求驗證,以及 Explore...
Windows 應用程式開發中遵守最低限度安全性的檢核表
用檢核表形式整理 WPF / WinForms / WinUI / C++ / C# 等 Windows 應用程式發佈前最低限不想漏的安全性要點。涵蓋避免不必要的管理員權限、EXE 與更新物簽章加時間戳、改用 DPAPI 與 Credential Locker、保留 HTT...
哪些應該用單元測試驗證,哪些該留給整合測試 - 切界線的方法與實務判斷表
本文以「想消弭哪種不確定性」為主軸,整理單元測試與整合測試該各自承擔什麼。從純邏輯、格式、接線、環境、時間五個切面歸納成判斷表,並列出 Repository 全 mock、Controller 連框架一起驗等常見誤區,幫讀者在實務上不再為界線猶豫。
Windows 的 DLL 名稱解析機制 - 以實務角度整理搜尋順序、Known DLLs、API set、SxS
從實務角度整理 Windows 的 DLL 名稱解析,說明 loader 在掃描檔案系統前會先處理 DLL redirection、API set、SxS、Known DLLs,並用 SetDefaultDllDirectories 與 LoadLibraryEx 旗標縮小...
Windows 什麼時候需要系統管理員權限 - UAC、保護區、設計上的分辨方式
從邊界與儲存位置的角度,整理 Windows 何時真正需要系統管理員權限:UAC、保護區、HKLM、服務、驅動、防火牆。同時說明 per-user 與 per-machine 的差異,以及把管理員處理切成獨立 EXE、服務或工作的設計取捨,幫讀者判斷該不該提權。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。