Windowsアプリの機密情報保存 - DPAPIで平文設定を避ける

· · Windows開発, セキュリティ, DPAPI, C# / .NET, Win32

前回の「Windowsアプリ開発における最低限のセキュリティを守るためのチェックリスト」では、 「秘密情報をソースコードや平文設定に置かない」「Win32 / .NET なら DPAPI / ProtectedData を使う」という最低限の線を書きました。

今回はその中でも、「DPAPI を使って最低限平文よりはましにする」をもう少し掘り下げます。

対象は次のような Windows アプリです。

  • WPF / WinForms / WinUI のデスクトップアプリ
  • C# / .NET の Windows クライアント
  • ローカル設定ファイルに、接続先資格情報や API トークンを保存したくなるアプリ

ここで扱うのは、「ローカルに保存せざるを得ない秘密を、少なくとも appsettings.json の平文のまま放置しない」ための現実的な設計です。 「どんな攻撃者にも勝てる完全防御」の話ではありません。そこは話を盛りすぎると、セキュリティが急に怪談になります。

1. まず結論

実務では、この順番で考えるのが分かりやすいです。

  1. そもそもクライアントに長期秘密を持たせない
    • Windows 認証、統合認証、ユーザーの対話ログイン、サーバー側の秘密管理を優先する
  2. どうしてもローカル保存が必要なら、平文では置かない
    • Windows ならまず DPAPI / ProtectedData を第一候補にする
  3. 通常のデスクトップアプリは DataProtectionScope.CurrentUser を基本にする
    • LocalMachine は用途がかなり限られる
  4. DPAPI は「端末が完全に侵害された状況」まで守るものではない
    • 同じユーザー権限で動くコードは、基本的にそのユーザーが復号できるものを復号できる

そして、この記事で一番大事な論点はここです。

「どうせ秘密鍵はどこかに保存しないといけないのだから、平文でも DPAPI でもセキュリティ的には同じではないか?」

これは半分正しくて、結論は間違いです。

  • 自前 AES + 鍵を同じアプリや同じ設定に置くなら、かなり平文に近いです
  • でも DPAPI は鍵管理を OS に寄せ、復号できる主体を“その Windows ユーザー”または“そのコンピュータ”に結び付けます
  • その結果、設定ファイル単体の流出、別 PC への持ち出し、誤送付、バックアップ流出、リポジトリ混入のような事故に対する強さが大きく変わります

つまり、 「鍵がどこかにある」という抽象論だけを見ると同じに見えても、 「誰が・どの文脈で・どれだけ簡単に使えるか」が全然違う、ということです。

玄関マットの下に鍵を置くのと、管理室で本人確認してから鍵を出すのを、同じと言い切るのは少し乱暴です。

2. なぜ平文設定が危ないのか

平文保存が危ない理由は、暗号理論よりずっと泥臭いです。実務では、だいたいこういう経路で漏れます。

  • 設定ファイルをそのまま Git に入れてしまう
  • 障害調査用 ZIP に設定ファイルが丸ごと入る
  • サポート問い合わせに設定ファイルを添付してもらう
  • バックアップやファイル共有で第三者が読める
  • ログに接続文字列やトークンがそのまま出る
  • 退職者や別ユーザーが同じ端末上のファイルを読める

平文の最大の問題は、「読めた瞬間に秘密として終わる」ことです。

  • ファイルを開けたら終わり
  • コピーできたら終わり
  • メール添付されたら終わり
  • リポジトリに残ったら半永久的に面倒を見る羽目になる

攻撃者が高度である必要すらありません。 テキストエディタで開ける、というのはそれだけでかなり弱いです。

3. 「秘密鍵はどこかに保存されるのだから同じでは?」への答え

この疑問はもっともです。 そして、ここを雑に答えると、セキュリティ記事が一気にふわっとします。

答えとしては、「どこかに鍵が必要」という意味では yes、だから同じという意味では no です。

3.1. 何が同じで、何が違うのか

確かに、暗号化には最終的に何らかの root of trust が必要です。 秘密は宇宙のどこかから無料では湧いてきません。そこは厳しい世界です。

ただし、セキュリティ上の差は次の 3 点で決まります。

  • 鍵をアプリが直接持つのか
  • 鍵がどの主体に結び付いているのか
  • ファイルだけ盗まれたときに復号できるのか

この違いをざっくり表にすると、こうなります。

方式 設定ファイルを読まれた ファイルだけ別 PC に持ち出された 同じ PC の別ユーザーに読まれた 同じユーザー権限で動くコード
平文 その場で漏れる そのまま漏れる そのまま漏れる 当然読める
自前暗号 + 鍵を同じ設定 / バイナリに置く かなり漏れる かなり漏れる かなり漏れる 当然復号できる
DPAPI + CurrentUser ファイル単体ではすぐ読めない 通常は復号しにくい 通常は復号しにくい 復号できる
DPAPI + LocalMachine ファイル単体ではすぐ読めない その PC 以外では通常は復号しにくい 同じ PC 上なら広く復号できる 復号できる

ここで重要なのは、DPAPI は「ファイルを読める」ことと「秘密を使える」ことを分離する点です。

平文では、この 2 つが同じです。 ファイルが読めたら、秘密も読めます。

でも 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 キーを埋め込む
  • 全端末で同じ共有パスワードを持つ
  • クライアントだけで完結する固定の復号キーを配る

といった設計は、どこか 1 台から抜かれた時点で全体に波及しやすいです。

DPAPI は「その保存場所を平文よりましにする」には有効ですが、 そもそもクライアントに置くべきでない秘密を正当化するものではありません。

6. CurrentUserLocalMachine の使い分け

ここはかなり重要です。雑に選ぶと意味が変わります。

6.1. 基本は CurrentUser

通常の Windows デスクトップアプリでは、まず CurrentUser を基本に考えます。

向いている例:

  • WPF / WinForms / WinUI のユーザー向けデスクトップアプリ
  • ユーザーごとに設定や資格情報を持つアプリ
  • %LocalAppData%%AppData% 配下に設定を持つアプリ

この場合、「その Windows ユーザーの秘密」として扱いやすくなります。

6.2. LocalMachine は用途がかなり限られる

LocalMachine は便利に見えますが、普通のデスクトップアプリでは広すぎます。

向いているのは、たとえばこんな場合です。

  • 信頼された単一用途マシン上の Windows サービス
  • そのマシン上の特定プロセスだけで使う機密
  • ログオンユーザーを跨いで同じ端末で使う必要があるケース

ただし注意点は重いです。

  • その PC 上で動くプロセスから広く復号できる
  • 共用端末、RDS、踏み台端末、複数ユーザーが入る環境では危険になりやすい
  • 「とりあえず全員が使えるから楽」で選ぶと、だいたい後で困る

6.3. 迷ったらこう考える

  • 通常の UI アプリ -> CurrentUser
  • 本当にマシン単位で守りたい特殊ケース -> LocalMachine
  • どのユーザーでも復号できる必要があるが、端末上に他ユーザーもいる -> たいてい設計から見直したほうがよい

6.4. サービスや impersonation では少し注意が増える

Windows サービスや impersonation を絡めると、CurrentUser の意味は少し重くなります。

  • 実行アカウントは誰か
  • そのアカウントのプロファイルがロードされているか
  • 復号タイミングはどの文脈か

このあたりがずれると、「暗号化できたのに復号できない」になりやすいです。 サービス用途は「とりあえず CurrentUser」で済まないことがあります。

7. 実装の最低限の指針

Windows アプリで「設定ファイルの平文をやめる」だけなら、設計はそこまで複雑にしなくて構いません。 ただし、いくつか外したくないポイントがあります。

7.1. 秘密だけを保護する

設定全体を丸ごと暗号化するより、まずは秘密の項目だけを保護するほうが扱いやすいです。

たとえば、次のように分けます。

  • サーバー URL
  • ユーザー名
  • DB 名
  • 機能フラグ

は平文のままでもよいことが多いです。

一方で、

  • パスワード
  • API トークン
  • リフレッシュトークン
  • 共有フォルダ資格情報

は保護対象です。

この分け方にすると、

  • 設定編集がしやすい
  • 差分確認がしやすい
  • どこが秘密かが明確
  • 全体の運用が単純

になります。

7.2. 保存先は per-user を基本にする

通常のデスクトップアプリなら、保存先は per-user の場所を基本にします。

  • %LocalAppData%\Vendor\App\settings.json
  • %AppData%\Vendor\App\settings.json

少なくとも、インストールフォルダ配下や共有しやすい場所に雑に置かないほうがよいです。

DPAPI で保護していても、保存先の ACL が雑だと、 「暗号文は読まれる」「設定構造は見える」「運用ミスは起きる」 という話になります。防御は一段ではなく、重ねたほうが効きます。

7.3. optionalEntropy は万能の第二鍵ではない

ProtectedData には optionalEntropy を渡せます。 これは便利ですが、“これをバイナリに埋めれば安全になる魔法の第二鍵” ではありません。

  • 同じファイルに置けば秘密にはなりません
  • バイナリに固定値で埋めても、強い秘密とは言えません
  • それでも、用途識別や誤用防止には役立ちます

実務では、

  • アプリ名
  • 用途名
  • バージョン識別子

を固定のバイト列として渡し、「別用途の暗号文を誤って受け付けない」ために使う、くらいがちょうどよいです。

7.4. 暗号文を Git に入れてよい、ではない

ここも地味に大事です。

DPAPI の暗号文は平文よりずっとましですが、 だからといって設定ファイルごとリポジトリに入れてよい、ではありません。

理由は単純で、

  • 暗号文は長く残る
  • いつか同じ端末や同じ文脈が再現されるかもしれない
  • ファイルには秘密以外の情報も入る
  • 「保護されているから雑に扱ってよい」という文化ができる

からです。

「平文よりまし」「どこに置いても安全」 はまったく別です。

7.5. ログに出さない

意外とよくあるのが、復号した後にログへ出して全部台無しになるパターンです。

  • 接続失敗時に接続文字列を丸ごと出す
  • API 401 時に Authorization ヘッダーを残す
  • 例外メッセージに秘密を混ぜる

このへんをやると、設定ファイルの平文をやめても、結局ログが平文倉庫になります。 悲しいけれど、かなり実務です。

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 で保存しても根本解決になりません。

なぜなら、どこか 1 台でアプリが復号できるなら、その秘密は取り出せるからです。

この手の秘密は、

  • サーバー側に置く
  • クライアントはトークンだけ持つ
  • ユーザーごとの資格情報にする
  • 期限付きトークンにする

といった方向へ寄せたほうがよいです。

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 認証
  • 統合認証
  • 対話ログイン
  • サーバー側で秘密保持
  • 短命トークン

優先 2: ユーザーごとの秘密に寄せる

  • 共通秘密より per-user
  • 長期固定資格情報より更新可能なトークン
  • 全クライアント共通鍵を避ける

優先 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 ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る