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

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

更新履歴(7件・最終更新 2026年08月25日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

不自然な口語・比喩を、意味を変えずに技術文書として自然な日本語に直しました。 更新前のバージョンを見る (DOI: 10.5281/zenodo.22064163)
平文が漏れる経路、DPAPIで変わる境界、スコープ選択の落とし穴、復号失敗時の運用などを図でも追えるように、Mermaid図を16点追加しました(地の文500〜750字につき1図の規約に合わせたものです)。あわせて既存の2図を含めて図番号を通しで振り直しました。本文の文章は変えていません。
記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
暗号文とマスターキーがどちら側に残るのかを示す図と、保存方式を上から順に検討する選定フローの図を追加しました。選定フローには、10.3節のとおり資格情報ストア(Credential Locker / Credential Manager)へ向かう分岐も入れています。本文の説明は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
復号できなくなるケースの表で「ローミングプロファイル / 別PCへのコピー」を1行にまとめていたのを分けました。ローミングプロファイルなら鍵材料も一緒に移動するため別PCでも復号できます(Microsoft Learnの`CryptProtectData`に明記があります)。同じ扱いにすると、移行手順で不要に資格情報を作り直すことになります。
重複していた記述を統合しました(逃がし先の一覧と、選びたくなる動機を本来の章へ移し、元の箇所は参照に圧縮。内容は失っていません)。参照の追加が必要なことを明示し(.NET Core以降はNuGetパッケージが必須で、どの共有フレームワークにも含まれません)、Credential LockerとCredential Managerとの比較表、運用で復号できなくなる4ケースと対処を追加しました。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589650)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「Windowsアプリの機密情報保存 - DPAPIで平文設定を避ける」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589650 https://comcomponent.com/blog/2026/03/16/000-windows-app-secret-storage-best-practices-dpapi/

DOI(最新版)
10.5281/zenodo.21589650
DOI(この版)
10.5281/zenodo.22093766

前回の「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 への持ち出し、誤送付、バックアップ流出、リポジトリ混入のような事故に対する強さが大きく変わります

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

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

「鍵はどこかにある」だけでは同じにならない鍵がどこかにあるという抽象論では平文もDPAPIも同じに見えるが、誰がどの文脈でどれだけ簡単に使えるかが全然違うという、この記事の中心的な論点を示す図。ここだけ見るとここを見ると鍵がどこかにある(抽象論)同じに見える誰が・どの文脈で・どれだけ簡単に使えるか全然違う

図1: 抽象論では同じでも、「誰がどの文脈で使えるか」で平文とDPAPIは分かれる。

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全21件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

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

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

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

平文は、第三者に読まれた時点で秘密性が失われます。

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

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

平文は読まれた時点で秘密性が失われるGit混入や調査用ZIP、バックアップ流出、添付といった泥臭い経路で設定ファイルが第三者に読まれると、平文は秘密性が失われることを示す図。Git 混入・調査用 ZIP・添付・バックアップ設定ファイルが読まれる第三者に読まれた時点で秘密性が失われる攻撃者が高度である必要すらない

図2: 平文の弱さは、日常的な事故経路で第三者に読まれた時点で秘密性が失われること。

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

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

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

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

確かに、暗号化には最終的に何らかの root of trust が必要です。 つまり、暗号化では最終的に何らかの信頼の起点が必要です。

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

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

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

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

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

復号に必要なもの ── OS の側に残り、暗号文には含まれないCurrentUser で保護LocalMachine で保護ユーザーのマスターキーCurrentUser で保護したときコンピューターのマスターキーLocalMachine で保護したとき暗号文設定ファイル / DBの列 / 調査用ZIP に入る= 持ち出されうるものそのユーザーとして動くコード→ 復号できる同じ PC の別ユーザー→ 復号できない同じ PC 上のコード→ 別ユーザーでも復号できる暗号文だけを別の PC へコピーする→ 復号できないローミングプロファイルごと移動する→ 鍵材料も一緒に動くので復号できる(6.5節)

図3: 暗号文だけが持ち出せる側にあり、復号に必要なマスターキーは OS の側に残る。ただし LocalMachine の届く範囲はその PC 全体で、同じ PC の別ユーザーは締め出せない

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

でも DPAPI では、少なくとも CurrentUser なら、

  • その Windows ユーザーとして
  • その Windows の文脈で
  • OS の保護機構を通して

復号する必要があります。

この差は、事故の現場だとかなり大きいです。

3.2. 「それでも同じユーザーなら復号できるのでは?」はその通り

ここは誤魔化さずに書くべき点です。

同じユーザー権限で実行されるコードは、そのユーザーが復号できるものを基本的に復号できます。

つまり、DPAPI は次のような状況を主目的にはしていません。

  • すでにその端末がマルウェアに侵害されている
  • 攻撃者がそのユーザーとしてコード実行できる
  • 端末管理者レベルで完全に乗っ取られている

この状況では、アプリ自身も復号できるのだから、攻撃者コードも復号できてしまいます。 ここで「でも暗号化してあります」は、あまり頼もしくありません。

DPAPI が効くのは、主に “ファイル流出・誤配置・オフライン持ち出し・別ユーザーからの参照” の側です。

ここを取り違えると、

  • 守れるものを過小評価して使わない
  • 守れないものを過大評価して安心する

の両方が起きます。どちらも地味に危ないです。

DPAPIが効く側と効かない側DPAPIが効くのはファイル流出や誤配置、オフライン持ち出し、別ユーザーからの参照の側で、同じユーザー権限で動く攻撃コードや侵害済み端末には効かないという線引きを示す図。効く側効かない側DPAPI の守備範囲流出・誤配置・別ユーザー同一ユーザー権限のコード取り違えると評価を誤る

図4: 効く側と効かない側の線を知らないと、使わない過小評価か安心しすぎの過大評価になる。

3.3. だから何がうれしいのか

DPAPI のうれしさを一言で言うと、

「秘密そのものを設定ファイルの可読性から切り離せる」

ことです。

たとえばこういう事故では、平文と DPAPI で差が出ます。

  • 利用者が設定ファイルをサポートへ送ってしまった
  • 調査用 ZIP に設定ファイルが入った
  • バックアップから設定ファイルだけ流出した
  • 共有フォルダ上にコピーされた
  • 開発者が暗号文だけを見て中身を読めない状態にできた

これはかなり現実的なメリットです。 攻撃者を映画みたいな超人にしなくても、日常の事故半径を小さくできます。

事故半径が小さくなる仕組みサポートへの誤送付や調査用ZIP、バックアップ流出のような日常の事故でファイルが外へ出ても、出ていくのは暗号文だけなので秘密の流出にならず、事故半径が小さくなることを示す図。誤送付・調査 ZIP・バックアップ流出外へ出るのは暗号文だけそのまま秘密流出にせずに済む日常の事故半径が小さくなる

図5: ファイルが外へ出る事故は防げなくても、出ていくものを暗号文に変えられる。

4. DPAPI がちょうどよい理由

Windows でローカル保存の秘密を扱うとき、DPAPI が実務でちょうどよい理由は次のとおりです。

4.1. 鍵管理を OS に寄せられる

自前で AES 鍵を生成し、保存し、権限を付け、ローテーションし、漏えい時の影響を考え、改ざん検知まで入れる。 これは思ったより重いです。しかも雑にやると、だいたい鍵を同じ場所に置いて終わります。

DPAPI を使うと、「暗号鍵をどう作ってどこに置くか」問題を、アプリ実装から切り離せます。

その意味で、DPAPI は 「暗号化アルゴリズムを選ぶ API」ではなく、「鍵管理を OS に委譲する API」 として見るほうが本質に近いです。

DPAPIの本質的な見方DPAPIは暗号化アルゴリズムを選ぶAPIではなく、鍵をどう作りどこに置くかという鍵管理の問題をOSに委譲するAPIとして見るのが本質に近いことを示す図。この見方ではないこの見方が本質に近いアルゴリズムを選ぶ APIDPAPI鍵管理を OS に委譲する API鍵の生成・保管の問題を実装から切り離せる

図6: DPAPIは鍵管理の委譲先として見ると、アプリから何を切り離せるかが分かる。

4.2. 復号主体を Windows ユーザーまたはコンピュータに結び付けられる

通常のデスクトップアプリなら、CurrentUser を選べばよい場面が多いです。

  • そのユーザーがログオンしていること
  • そのユーザー文脈で処理が動くこと

を前提に復号できます。

そのため、暗号文だけを別 PC にコピーしても、そのままでは使いにくいという性質を得られます。

4.3. 改ざん検知まで含めやすい

自前暗号でありがちなのは、 「AES で暗号化したから終わり」として、改ざん検知を忘れることです。

DPAPI は暗号化データに対して整合性保護も持つので、 暗号文を勝手に書き換えられたときの検知まで OS 側の仕組みに乗せやすい、という実務上の利点があります。

改ざん検知の差自前暗号ではAESで暗号化したら終わりとして改ざん検知を忘れがちだが、DPAPIは暗号化データに整合性保護を持つため、暗号文の書き換え検知までOS側の仕組みに乗せやすいことを示す図。自前暗号改ざん検知を忘れがちDPAPI整合性保護を持つ書き換えの検知まで OS 側に乗る

図7: 暗号化だけでなく改ざん検知まで、OS側の仕組みに乗せられるのがDPAPIの利点。

4.4. C# / .NET から素直に使える

C# なら System.Security.Cryptography.ProtectedData をそのまま使えます。 余計なライブラリを足さずに済むのも、Windows 専用アプリではかなり助かります。

5. DPAPI で守れるものと、守れないもの

ここははっきり分けておいたほうが安全です。

5.1. 守りやすくなるもの

DPAPI は、少なくともこうした場面では有効です。

  • 設定ファイルの平文漏えい
  • 別 PC へのファイル持ち出し
  • 同じ PC の別ユーザーからの参照(CurrentUser 前提)
  • バックアップや添付ファイルとしての流出
  • 開発・保守現場での「うっかり読めてしまう」状態

5.2. 守れない、または守りが弱いもの

一方で、次の状況では過信しないほうがよいです。

  • 同じユーザー権限で実行される攻撃コード
  • 端末自体の完全な侵害
  • 管理者権限での乗っ取り
  • アプリが復号した後のメモリ上の平文
  • すべてのクライアントに共通で配られる長期秘密

最後の「全クライアント共通の長期秘密」は特に重要です。

たとえば、

  • 全顧客に同じ API キーを埋め込む
  • 全端末で同じ共有パスワードを持つ
  • クライアントだけで完結する固定の復号キーを配る

といった設計は、どこか 1 台から抜かれた時点で全体に波及しやすいです。 理屈は単純で、どこか 1 台でアプリが復号できるなら、その秘密は取り出せるからです。

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

共通の長期秘密が波及する仕組み全クライアントに共通の長期秘密を配ると、どこか1台でアプリが復号できる以上その秘密は取り出せるため、1台から抜かれた時点で全体に波及しやすいことを示す図。全クライアント共通の長期秘密どこか 1 台でアプリが復号できるその 1 台から秘密を取り出せる全体に波及するDPAPI で保存しても根本解決にならない

図8: 共通秘密は1台の陥落が全体に波及するので、保存方法ではなく置き場所から見直す。

この手の秘密は、保存方法を工夫するのではなく、次の方向へ逃がすほうが本筋です。

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

6. CurrentUser と LocalMachine の使い分け

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

6.1. 基本は CurrentUser

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

向いている例:

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

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

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

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

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

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

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

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

そして、LocalMachine を選びたくなる動機は、たいていこの 3 つです。

  • ユーザー切り替えしても読める
  • サービスからも読める
  • 動けば便利

どれも「楽」であって、「守れている」ではありません。 普通のデスクトップアプリで LocalMachine を選ぶと、その PC 上の他のプロセスにも復号可能性を広げることになるので、意味がかなり変わります。

LocalMachineを楽で選ぶとどうなるかユーザーを跨いで読める、サービスからも読めるといった楽さでLocalMachineを選ぶと、そのPC上の他のプロセスにも復号可能性が広がり、守れているのとは別の状態になることを示す図。ユーザー跨ぎ・サービス対応で楽LocalMachine を選ぶPC 上の他プロセスにも復号可能性が広がる楽であって守れているではない

図9: 楽さでLocalMachineを選ぶと、復号できる範囲がそのPC全体に広がってしまう。

6.3. 迷ったらこう考える

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

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

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

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

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

impersonation の場合、Microsoft Learn にも明記されている典型的な失敗が「Key not valid for use in specified state.」です。DPAPI は鍵データをユーザープロファイルに保持するため、プロファイルがロードされていないと復号できません。impersonate する前に、対象ユーザーのプロファイルをロードしておく必要があります。

impersonationでの典型的な失敗DPAPIは鍵データをユーザープロファイルに保持するため、プロファイルがロードされていない状態でimpersonateして復号すると典型的なエラーになり、先に対象ユーザーのプロファイルをロードしておく必要があることを示す図。未ロードで impersonate復号がエラーで失敗先にプロファイルをロードimpersonate後も復号可鍵はプロファイルにある

図10: 鍵はプロファイル側にあるため、impersonateの前にプロファイルのロードが要る。

6.5. 運用で「復号できなくなる」ケースを知っておく

暗号化そのものより、復号できなくなる事故のほうが実務では痛いです。DPAPI は復号できる主体を Windows ユーザー / コンピュータに結び付ける仕組みなので、その結び付きが切れると読めなくなります。

先に知っておきたいのは、次の 5 つです。

ケース 何が起きるか 備えかた
管理者によるパスワードのリセット ユーザーのパスワードに紐づく保護が外れ、DPAPI で保護したデータへアクセスできなくなることがあります。Microsoft のサポート情報にも、管理者がパスワードをリセットした後に DPAPI データへアクセスできなくなる事象として記載があります 秘密を「再取得できる」設計にしておく。復号失敗時に再入力へ誘導する
プロファイルの再作成 新しいプロファイルは別の鍵材料を持つため、以前の暗号文は復号できません 設定ファイルのバージョンを持ち、復号失敗を異常終了にしない
暗号文だけを別 PC へコピー 復号に要る鍵材料はユーザープロファイル側にあるので、CurrentUser の暗号文だけを持っていっても読めません(これは 3.1. で挙げた「強み」の裏面です) 端末ごとに保存し直す前提で設計する
ローミングプロファイル こちらは読めます。プロファイルと一緒に鍵材料も移動するため、Microsoft Learn も「ローミングプロファイルを持つユーザーはネットワーク上の別のコンピューターから復号できる」と明記しています。前の行と同じ扱いにすると、移行手順で不要に資格情報を作り直すことになります 「別 PC だから読めない」と決めつけない。ローミングの有無を確認してから移行手順を決める
サービスの実行アカウント変更 保護したときと復号するときで実行アカウントが変われば、CurrentUser では読めません アカウント変更時に再保護する手順を運用に入れる

要するに、ProtectedData.Unprotect は失敗し得るという前提でコードを書きます。復号に失敗すると CryptographicException が飛ぶので、そこを握って再入力へ寄せます。

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

// protectedBase64: 設定ファイルから読んだ暗号文 (Base64)
// entropy: 保護時と同じ値を渡す。null でも可
static bool TryUnprotect(string protectedBase64, byte[]? entropy, out string plaintext)
{
    plaintext = string.Empty;

    try
    {
        byte[] plainBytes = ProtectedData.Unprotect(
            Convert.FromBase64String(protectedBase64),
            optionalEntropy: entropy,
            scope: DataProtectionScope.CurrentUser);

        plaintext = Encoding.UTF8.GetString(plainBytes);
        return true;
    }
    catch (CryptographicException)
    {
        // 復号できない = 環境が変わった可能性が高い。
        // ここで落とさず、呼び出し側で再入力の導線へ寄せる
        return false;
    }
    catch (FormatException)
    {
        // Base64 として壊れている場合
        return false;
    }
}

「暗号化したのに復号できない」で問い合わせが来るのは、だいたいこの表のどれかです。

復号失敗を前提にしたコードの流れProtectedData.Unprotectは環境の変化で失敗し得るという前提でコードを書き、CryptographicExceptionを握って異常終了させず、再入力の導線へ寄せるという流れを示す図。成功例外で失敗Unprotect を試みる秘密を使う例外を握って落とさない再入力の導線へ寄せる結び付きが切れると失敗し得る

図11: 復号は失敗し得る前提で書き、失敗を異常終了ではなく再入力へつなぐ。

7. 実装の最低限の指針

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

7.1. 秘密だけを保護する

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

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

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

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

一方で、

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

は保護対象です。

この分け方にすると、

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

になります。

秘密の項目だけを保護する分け方設定全体を丸ごと暗号化するのではなく、URLやユーザー名などは平文のままにし、パスワードやトークンなどの秘密の項目だけを保護すると、編集しやすくどこが秘密かも明確になることを示す図。平文のまま保護する設定ファイルURL・ユーザー名・フラグなどパスワード・トークンなどどこが秘密か明確で運用も単純

図12: 丸ごと暗号化ではなく、秘密の項目だけを保護すると扱いやすさと安全が両立する。

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

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

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

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

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

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

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

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

実務では、

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

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

optionalEntropyの正しい位置づけoptionalEntropyはバイナリに埋めれば安全になる魔法の第二鍵ではなく、アプリ名や用途名を固定バイト列として渡して別用途の暗号文を誤って受け付けないための用途識別に使うのがちょうどよいことを示す図。この期待はしないこの使い方がちょうどよいoptionalEntropy魔法の第二鍵アプリ名・用途名の識別子別用途の暗号文を誤って受け付けない

図13: entropyは秘密鍵ではなく、用途識別と誤用防止のためのタグとして使う。

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

ここも地味に大事です。

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

理由は単純で、

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

からです。

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

暗号文でもGitに入れない理由DPAPIの暗号文は平文よりましだが、リポジトリに入れると長く残り、秘密以外の情報も含まれ、保護されているから雑に扱ってよいという文化ができるため、どこに置いても安全とは別の話であることを示す図。平文よりましそれでもDPAPI の暗号文事故に強くなるリポジトリには入れない長く残る・他の情報も入る・文化が緩む

図14: 「平文よりまし」は置き場所を雑にしてよい理由にはならない。

7.5. ログに出さない

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

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

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

8. C# / .NET の最小実装例

8.1. 先に、参照の追加が必要です

ProtectedData は BCL に入っているように見えて、ターゲットフレームワークによって「どこから来るか」が違います。 ここでつまずくと、ProtectedData という型名が解決できません。

ターゲット 必要な作業 提供元
.NET Framework プロジェクトに System.Security アセンブリ参照を追加する System.Security.dll
.NET Core / .NET 5 以降(.NET 6 / 8 を含む) NuGet の System.Security.Cryptography.ProtectedData パッケージを追加する System.Security.Cryptography.ProtectedData.dll

このパッケージは、.NET Core / .NET 5 以降のどの共有フレームワークにも含まれていません。net8.0-windows のような Windows 向けターゲットでも、明示的に参照が必要です。

dotnet add package System.Security.Cryptography.ProtectedData

もう 1 点、実装より先に知っておきたいことがあります。 ProtectedData は Windows 専用です。 DPAPI に依存しているため、Windows 以外のプラットフォームの .NET 上で呼ぶと PlatformNotSupportedException が飛びます。クロスプラットフォーム前提のコードベースなら、10.1. のとおり最初から別設計にします。

ProtectedDataを使う前の確認ProtectedDataはターゲットに応じた参照の追加が要り、追加しないと型名が解決できず、またWindows専用のためWindows以外で呼ぶと例外になるという、実装前に確認すべき2点を示す図。追加しないとWindows 以外で呼ぶとProtectedData を使いたいターゲットに応じた参照を追加型名が解決できないWindows 専用と理解しておく実行時例外になる

図15: 実装前に、参照の追加とWindows専用という2つの前提を押さえておく。

8.2. 最小実装

以下は、設定ファイルへ保存する文字列を 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 で保存しても根本解決になりません。 理由と、代わりにどこへ逃がすかは 5.2. にまとめてあります。

9.3. LocalMachine を「楽だから」で選ぶ

これも本当にありがちです。ただし「楽」であって「守れている」ではありません。 選びたくなる動機と、そのときに何が広がるかは 6.2. のとおりです。 判断に迷ったら 6.3. の 3 行を見てください。

9.4. 自前暗号を足して安心する

DPAPI の代わりに、

  • AES 鍵をソースコードに埋める
  • AES 鍵を設定ファイルの別項目に置く
  • 「ちょっと難読化した文字列」を鍵扱いする

といった実装を入れるのは、たいてい効果が薄いです。

“平文ではない” と “安全である” の間には、かなり大きな溝があります。

自前暗号で安心する危うさAES鍵をソースコードや設定ファイルの別項目に置いたり難読化した文字列を鍵扱いする自前暗号は効果が薄く、平文ではないことと安全であることの間には大きな溝があることを示す図。間に大きな溝鍵をコードや設定に埋める自前暗号平文ではない状態安全である状態効果はたいてい薄い

図16: 鍵を同じ場所に置く自前暗号は「平文ではない」だけで、「安全」には届かない。

10. DPAPI で十分でないケース

DPAPI は便利ですが、万能ではありません。次のケースでは別の選択肢を考えたほうがよいです。

10.1. Windows 以外でも動かしたい

DPAPI / ProtectedData は Windows 向けです。 クロスプラットフォームのアプリなら、その前提では組めません。

10.2. 複数マシン・複数ユーザーで同じ秘密を扱いたい

同じ暗号文を複数の PC で復号したい、複数ユーザーで共用したい、という要件は、 「その端末・そのユーザーに結び付ける」DPAPI の得意分野から外れます。

この場合は、

  • サーバー側の秘密管理
  • 資格情報基盤
  • Windows 認証 / 統合認証
  • アプリ用の資格情報ストア

など、要件に合う別設計を考えるべきです。

10.3. 保存対象がユーザー資格情報そのもの

保存したいものが明確に

  • ユーザー名
  • パスワード

の組なら、DPAPI で自分のファイルへ書くより、Windows が用意している資格情報ストアのほうが素直です。ここは実務でよく迷うところなので、比較を置きます。

観点 DPAPI (ProtectedData) Credential Locker (PasswordVault) Credential Manager (CredWrite / CredRead)
保存する場所 自分で決めたファイル(暗号文をどう置くかはアプリ次第) Windows が管理する資格情報ストア Windows が管理する資格情報ストア
保存できるもの 任意のバイト列(接続文字列、トークン、設定の一部でもよい) ユーザー名 + パスワードの組 資格情報(種別ごとの構造体)
API System.Security.Cryptography WinRT の Windows.Security.Credentials Win32 (wincred.h / Advapi32.dll)
デスクトップアプリから使えるか そのまま使える WinUI だけでなく WPF / WinForms からも使える(WinRT API 呼び出しの設定は必要) そのまま使える
同期 なし Microsoft アカウントで端末間ローミングされる なし(ローカルのユーザー資格情報セット)
制限 実質なし 1 アプリあたり 20 件まで。大きなデータ用ではない 現在のトークンのログオンセッションに紐づく
管理の主体 アプリ(保存先も ACL も自分で決める) OS(保存場所を設計しなくてよい) OS(コントロール パネルの資格情報マネージャーから管理できる)

使い分けの目安はこうです。

  • 保存対象がユーザー名 + パスワードの組で、件数も少ない -> Credential Locker / Credential Manager が第一候補です。保存場所の設計も ACL も自分で持たずに済みます
  • 保存対象が「ユーザー名 + パスワード」の形をしていない -> 接続文字列、API トークン、リフレッシュトークン、設定ファイルの一部といったものは DPAPI 側が素直です。この記事が扱っているのはこちらです
  • 端末間で引き継ぎたい -> Credential Locker のローミングが効きます。DPAPI の CurrentUser は逆に「引き継がれない」ことが利点なので、ここは目的が正反対です
  • 件数が多い / サイズが大きい -> Credential Locker の 20 件制限に当たります。DPAPI で自前ファイルに寄せます

なお、資格情報ストアも中身は OS の保護機構に乗っているので、「DPAPI より安全」「DPAPI が劣る」という並びではありません。保存したいものの形と、ローミングの要否で選ぶのが実務的です。

そしてどちらを選んでも、新規アプリなら Windows Hello / パスキーのようなパスワードレスを先に検討する価値はあります。そもそも長期パスワードを持たなくて済むなら、それがいちばん強いです。

この記事の中心は、あくまで 「Windows クライアントで設定ファイル平文をやめる」ための DPAPI の実務線です。

11. 実務でのおすすめ優先順位

最後に、実務で迷ったらこの順で考えると整理しやすいです。上から順に検討して、条件を満たさないときだけ下へ降ります。

済む済まない分けられない分けられるその組で、件数も少ないあるそうだドメイン / ローカルアカウントないトークンなど別の形あるない1つでよい(利用者本人、または専用のサービスアカウント)複数のアカウントから復号する必要がある言い切れる言い切れない長期の秘密を端末に持たせずに済むか優先1: 持たない(Windows認証・短命トークン)秘密を利用者ごとに分けられるか共通の鍵になっていないか設計から見直す保存したいものの形はユーザー名 + パスワードの組か(10.3節)端末間で引き継ぐ必要があるかMicrosoft アカウントで同期される端末か(10.3節)Credential Locker のローミングを使うDPAPI では引き継げない。サーバー側で管理する(10.2節)Credential Locker /Credential Manager端末間で引き継ぐ必要があるか同じ秘密を復号するアカウントはいくつか優先3: DPAPI + CurrentUser無人実行ならプロファイルのロードを確認する(6.4節)他の利用者は入らないと言い切れるか優先4: DPAPI + LocalMachine例外扱いとして根拠を残す他ユーザーも復号できてしまう。認証方式のほうを見直す

図17: 保存方式の選定順。秘密の形とローミングの要否を先に見てから DPAPI に入る。LocalMachine は「楽だから」ではなく、他の選択肢が成立しないときの例外として選ぶ

優先 1: そもそも持たない

  • Windows 認証
  • 統合認証
  • 対話ログイン
  • サーバー側で秘密保持
  • 短命トークン

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

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

優先 3: ローカル保存が必要なら DPAPI

  • 通常は CurrentUser
  • 保存先は per-user
  • 秘密項目だけ保護
  • ログに出さない

優先 4: LocalMachine は例外扱い

  • 本当にマシン単位である必要があるか
  • その端末に他ユーザーは入らないか
  • サービス設計として妥当か

12. まとめ

Windows アプリで設定ファイルに機密情報を保存する必要があるとき、 平文のまま置くのは避けたいです。

そして、

「どうせ鍵はどこかに保存されるのだから同じでは?」

という疑問に対しては、こう答えるのが実務的です。

  • 自前暗号で鍵を同じ場所に置くなら、かなり同じ
  • DPAPI は同じではない
    • 鍵管理を OS に寄せられる
    • 復号主体を Windows ユーザー / コンピュータに結び付けられる
    • ファイル単体の流出を、そのまま秘密流出にしなくて済む
  • ただし
    • 同じユーザー権限で動くコード
    • 完全に侵害された端末
    • クライアントに置くべきでない長期共通秘密

までは解決しない

要するに、DPAPI は万能の城壁ではありません。 でも、設定ファイル平文という素通しの窓ガラスを、最低限まともな窓に替えるくらいの効果はあります。

Windows クライアントの実務では、この差がかなり大きいです。 まずはここを外さないところから始めるのが、いちばん現実的です。

DPAPIの現実的な位置づけDPAPIは万能の城壁ではないが、設定ファイル平文という素通しの窓ガラスを最低限まともな窓に替える効果があり、まずそこから始めるのが現実的であることを示す図。DPAPI に替える平文設定 = 素通しの窓ガラス最低限まともな窓万能の城壁ではないまずここから始めるのが現実的

図18: DPAPIは城壁ではなく窓の交換だが、実務ではその差が一番効く。

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
  • Microsoft Learn: CredWrite(Windows Credential Manager の Win32 API) https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credwritew
  • NuGet: System.Security.Cryptography.ProtectedData https://www.nuget.org/packages/System.Security.Cryptography.ProtectedData
  • Microsoft サポート: 管理者がパスワードをリセットした後に DPAPI データへアクセスできなくなる事象 https://support.microsoft.com/en-us/topic/you-cannot-access-dpapi-data-after-an-administrator-resets-your-password-on-a-windows-server-2012-based-domain-controller-4aa890cd-12b5-fe5c-9e68-06244e70673d

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

DPAPIとは何ですか?
DPAPI(Data Protection API)は、Windows が提供するデータ保護の仕組みで、暗号鍵の管理を OS に委譲し、復号できる主体をその Windows ユーザーまたはそのコンピュータに結び付けます。C# / .NET からは System.Security.Cryptography.ProtectedData クラスを通して余計なライブラリを足さずに使えます。暗号化アルゴリズムを選ぶ API というより、鍵管理を OS に委譲する API として見るのが本質に近いです。設定ファイルに保存するパスワードや API トークンを平文のまま置かないための現実的な選択肢です。
鍵はどこかに保存されるのだから、平文でもDPAPIでも同じではないですか?
同じではありません。自前の AES 暗号で鍵を同じアプリや設定ファイルに置くならかなり平文に近いですが、DPAPI は鍵管理を OS に寄せ、復号できる主体を Windows ユーザーまたはコンピュータに結び付けます。その結果、設定ファイル単体の流出、別 PC への持ち出し、誤送付、バックアップ流出、リポジトリ混入のような事故に対する強さが大きく変わります。DPAPI は「ファイルを読める」ことと「秘密を使える」ことを分離できる点が平文との決定的な違いです。
DPAPIで守れないものは何ですか?
同じユーザー権限で実行されるコードは、そのユーザーが復号できるものを基本的に復号できます。そのため、端末がマルウェアに侵害されている状況や、管理者権限で乗っ取られた状況、復号後のメモリ上の平文までは守れません。また、全クライアントに共通で配る長期秘密は、どこか 1 台から抜かれた時点で全体に波及しやすいため、DPAPI で保存しても根本解決になりません。DPAPI が効くのは主にファイル流出・誤配置・オフライン持ち出し・別ユーザーからの参照の側です。
DataProtectionScopeのCurrentUserとLocalMachineはどちらを使うべきですか?
通常の Windows デスクトップアプリでは CurrentUser が基本です。そのユーザーの秘密として扱え、暗号文だけを別 PC にコピーしてもそのままでは使いにくい性質が得られます。LocalMachine はその PC 上で動くプロセスから広く復号できるため、共用端末や複数ユーザーが入る環境では危険になりやすく、信頼された単一用途マシン上の Windows サービスなど用途がかなり限られます。「全員が使えて楽だから」で LocalMachine を選ぶと、だいたい後で困ります。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

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

ブログ一覧に戻る