Хранение секретов в Windows-приложениях — избегаем настроек в открытом виде с помощью DPAPI

· Обновлено: · · Разработка Windows, Безопасность, DPAPI, C# / .NET, Win32

История изменений (3 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Добавлены accTitle и accDescr к диаграммам Mermaid. Утверждения статьи не менялись.
Исправлен URL CryptProtectData в списке источников. Утверждения статьи не менялись.
Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619693)

Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.

Го Комура (2026). Хранение секретов в Windows-приложениях — избегаем настроек в открытом виде с помощью DPAPI. KomuraSoft LLC. https://comcomponent.com/ru/blog/2026/03/16/000-windows-app-secret-storage-best-practices-dpapi/

DOI (зарегистрированный архив)
10.5281/zenodo.21619693
DOI (последняя зарегистрированная версия)
10.5281/zenodo.21619694

В предыдущей статье «Минимальный чек-лист безопасности при разработке Windows-приложений» мы зафиксировали нижнюю планку: «не класть секреты в исходники и в настройки открытым текстом» и «в Win32 / .NET использовать DPAPI / ProtectedData».

Сейчас разберём одну из этих рекомендаций подробнее: «взять DPAPI и хотя бы перестать быть хуже открытого текста».

Речь о таких Windows-приложениях:

  • настольные приложения на WPF / WinForms / WinUI
  • Windows-клиенты на C# / .NET
  • приложения, которым хочется сохранять учётные данные подключения или API-токены в локальном конфигурационном файле

Тема статьи — практичное проектирование, чтобы секрет, который всё же приходится хранить локально, хотя бы не лежал открытым текстом в appsettings.json. Это не рассказ про «полную защиту, которая побеждает любого злоумышленника». Если туда зайти слишком далеко, разговор перестаёт быть практическим.

1. Сначала вывод

На практике удобнее думать в таком порядке.

  1. Вообще не отдавать клиенту долгоживущий секрет
    • в приоритете проверка подлинности Windows, встроенная проверка подлинности, интерактивный вход пользователя, хранение секретов на стороне сервера
  2. Если локальное хранение всё же нужно — не класть открытым текстом
    • на Windows первым кандидатом брать DPAPI / ProtectedData
  3. Для обычного настольного приложения база — DataProtectionScope.CurrentUser
    • у LocalMachine применение довольно узкое
  4. DPAPI не защищает до ситуации «устройство полностью скомпрометировано»
    • код с теми же правами пользователя в целом расшифровывает то, что может расшифровать этот пользователь

И самый важный тезис статьи вот какой:

«Секретный ключ всё равно нужно где-то хранить. Тогда открытый текст и DPAPI с точки зрения безопасности — одно и то же?»

Наполовину верно, а вывод — нет.

  • Свой AES и ключ в том же приложении или в той же конфигурации — это действительно почти открытый текст
  • Но DPAPI сдвигает управление ключом на ОС и привязывает того, кто может расшифровать, к «этому пользователю Windows» или к «этому компьютеру»
  • В результате устойчивость к утечке одного конфигурационного файла, выносу на другой ПК, ошибочной отправке, утечке резервной копии, попаданию в репозиторий меняется сильно

То есть если смотреть только на абстракцию «ключ где-то есть», варианты кажутся одинаковыми, а «кто, в каком контексте и насколько легко может воспользоваться» — совершенно разные вещи.

Сказать, что ключ под ковриком у двери и ключ, который выдают в администраторской после проверки личности, — это одно и то же, было бы слишком грубо.

«Ключ где-то есть» само по себе ещё не уравнивает вариантыНа абстракции «ключ где-то есть» открытый текст и DPAPI выглядят одинаково, но кто, в каком контексте и насколько легко может воспользоваться секретом — совсем другое; это центральный тезис статьи.если смотреть только сюдаесли смотреть сюдаКлюч где-то есть (абстракция)кажется, что одно и то жеКто, в каком контексте и насколько легко может воспользоватьсясовсем другое дело

Рис. 1: На абстракции одинаково, а по тому, кто и в каком контексте может воспользоваться, открытый текст и DPAPI расходятся.

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 21, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Почему настройки в открытом виде опасны

Опасность хранения открытым текстом гораздо прозаичнее теории криптографии. На практике утечки идут примерно такими путями.

  • конфигурационный файл как есть кладут в Git
  • в ZIP для разбора сбоя целиком попадает конфигурационный файл
  • к обращению в поддержку прикладывают конфигурационный файл
  • третьи лица читают его из резервной копии или общей папки
  • в лог уходит строка подключения или токен как есть
  • уволившийся сотрудник или другой пользователь читает файл на том же устройстве

Открытый текст теряет конфиденциальность в тот момент, когда его прочитала третья сторона.

  • открыли файл — всё
  • скопировали — всё
  • приложили к письму — всё
  • осталось в репозитории — почти вечная головная боль

Атакующему даже не нужно быть искусным. «Открывается в текстовом редакторе» само по себе уже очень слабо.

Открытый текст теряет конфиденциальность, как только его прочиталиЕсли конфигурационный файл прочитала третья сторона по прозаическим путям вроде попадания в Git, ZIP для разбора, вложения или утечки резервной копии, открытый текст уже не конфиденциален.Попадание в Git, ZIP для разбора, вложение, резервная копияКонфигурационный файл читаютКак только третья сторона прочитала, конфиденциальность потерянаАтакующему даже не нужно быть искусным

Рис. 2: Слабость открытого текста в том, что по повседневным путям инцидента третья сторона читает файл — и конфиденциальность уже потеряна.

3. Ответ на «ключ всё равно где-то хранится, значит, это одно и то же?»

Вопрос справедливый. Если ответить на него небрежно, статья про безопасность сразу становится размытой.

Ответ такой: в смысле «ключ где-то нужен» — да; в смысле «поэтому это одно и то же» — нет.

3.1. Что совпадает, а что нет

Да, шифрованию в конечном счёте нужен какой-то root of trust. Иначе говоря, шифрование не обходится без исходной точки доверия.

Но разница в безопасности определяется тремя пунктами.

  • держит ли приложение ключ напрямую
  • к кому ключ привязан
  • можно ли расшифровать, если украли только файл

Если свести это в таблицу, получается так.

Способ Прочитали конфигурационный файл Вынесли только файл на другой ПК Прочитал другой пользователь на том же ПК Код с теми же правами пользователя
Открытый текст Утекает сразу Утекает как есть Утекает как есть Разумеется, читает
Своё шифрование + ключ в той же конфигурации / в том же двоичном файле Почти наверняка утекает Почти наверняка утекает Почти наверняка утекает Разумеется, расшифрует
DPAPI + CurrentUser По одному файлу сразу не прочитать Обычно расшифровать трудно Обычно расшифровать трудно Расшифрует
DPAPI + LocalMachine По одному файлу сразу не прочитать Вне этого ПК обычно расшифровать трудно На этом ПК расшифровывается широко Расшифрует

Важно вот что: DPAPI отделяет «файл можно прочитать» от «секретом можно воспользоваться».

Унести можно только шифротекст, мастер-ключ остаётся на стороне ОСШифротекст может оказаться в конфигурационном файле, столбце БД или ZIP. Для расшифровки нужны пользовательский мастер-ключ при CurrentUser или мастер-ключ компьютера при LocalMachine; они остаются на стороне ОС. CurrentUser не даёт расшифровать другому пользователю, LocalMachine на этом ПК расшифровывается широко. Один шифротекст на другом ПК не расшифровать, а с перемещаемым профилем ключ едет вместе.Что нужно для расшифровки ── остаётся на стороне ОС, в шифротекст не входитзащита CurrentUserзащита LocalMachineПользовательский мастер-ключесли защищали CurrentUserМастер-ключ компьютераесли защищали LocalMachineШифротекстпопадает в конфигурационный файл / столбец БД / ZIP для разбора= то, что могут унестиКод от имени этого пользователя→ расшифровать можноДругой пользователь на том же ПК→ расшифровать нельзяКод на том же ПК→ расшифрует и другой пользовательКопируют только шифротекст на другой ПК→ расшифровать нельзяПереносят вместе с перемещаемым профилем→ ключевой материал едет вместе, расшифровать можно (п. 6.5)

Рис. 3: Унести можно только шифротекст, а мастер-ключ для расшифровки остаётся на стороне ОС. Но зона действия LocalMachine — весь этот ПК: другого пользователя на той же машине не отсечь

Открытым текстом эти две вещи совпадают. Прочитали файл — прочитали и секрет.

В DPAPI же, по крайней мере при CurrentUser, расшифровка должна идти:

  • от имени этого пользователя Windows
  • в контексте этого Windows
  • через защитный механизм ОС

На месте реального инцидента эта разница очень велика.

3.2. «Но тот же пользователь всё равно расшифрует?» — да, так и есть

Это нужно сказать прямо, без сглаживания.

Код с теми же правами пользователя в целом может расшифровать всё, что расшифровывает этот пользователь.

То есть DPAPI не рассчитан в первую очередь на такие ситуации:

  • устройство уже скомпрометировано вредоносным ПО
  • злоумышленник может выполнять код от имени этого пользователя
  • устройство полностью захвачено на уровне администратора

В такой ситуации приложение само умеет расшифровать — значит, умеет и код злоумышленника. Фраза «но ведь мы зашифровали» здесь мало что даёт.

DPAPI работает в основном против «утечки файлов, неверного размещения, офлайн-выноса и чтения другим пользователем».

Если это перепутать, случается и то и другое:

  • недооценить то, что реально защищается, и не использовать
  • переоценить то, что защитить нельзя, и успокоиться

Оба варианта по-тихому опасны.

Где DPAPI работает, а где нетDPAPI работает против утечки файлов, неверного размещения, офлайн-выноса и чтения другим пользователем и не работает против атакующего кода с теми же правами пользователя и уже скомпрометированного устройства.работаетне работаетОбласть защиты DPAPIутечка, неверное размещение, другой пользователькод с теми же правами пользователяесли перепутать — оценка будет неверной

Рис. 4: Не зная этой границы, либо отказываются от средства, либо слишком успокаиваются.

3.3. В чём тогда польза

Пользу DPAPI одной фразой:

«сам секрет можно отвязать от того, что конфигурационный файл читается».

Например, в таких инцидентах открытый текст и DPAPI расходятся:

  • пользователь отправил конфигурационный файл в поддержку
  • в ZIP для разбора попал конфигурационный файл
  • из резервной копии утёк только конфигурационный файл
  • файл скопировали в общую папку
  • разработчик видит только шифротекст и не может прочитать содержимое

Это вполне практичное преимущество. Не нужно представлять злоумышленника киношным суперменом — достаточно уменьшить радиус повседневных инцидентов.

Как уменьшается радиус инцидентаДаже если файл уходит наружу из-за ошибочной отправки в поддержку, ZIP для разбора или утечки резервной копии, наружу выходит только шифротекст, поэтому сам секрет не утекает и радиус повседневных инцидентов становится меньше.Ошибочная отправка, ZIP для разбора, утечка резервной копииНаружу уходит только шифротекстУтечки самого секрета удаётся избежатьРадиус повседневных инцидентов уменьшается

Рис. 5: Сам уход файла наружу не предотвратить, но уходящее можно сделать шифротекстом.

4. Почему DPAPI — разумная середина

Когда на Windows нужно хранить секрет локально, DPAPI на практике оказывается разумной серединой по следующим причинам.

4.1. Управление ключами можно отдать ОС

Самому сгенерировать ключ AES, сохранить его, назначить права, сделать ротацию, продумать последствия утечки и ещё добавить обнаружение подделки. Это тяжелее, чем кажется. А если сделать небрежно, ключ почти всегда кладут туда же, где данные.

С DPAPI вопрос «как сделать ключ шифрования и куда его положить» можно вынести из реализации приложения.

В этом смысле на DPAPI правильнее смотреть не как на «API выбора алгоритма шифрования», а как на «API, который делегирует управление ключами ОС».

Как смотреть на DPAPI по сутиDPAPI ближе к API, который отдаёт ОС вопрос, как сделать ключ и куда его положить, а не к API выбора алгоритма шифрования.так смотреть не стоиттак смотреть ближе к сутиAPI выбора алгоритмаDPAPIAPI, который отдаёт управление ключами ОСГенерацию и хранение ключа можно вынести из реализации

Рис. 6: Если видеть в DPAPI место, куда отдано управление ключами, становится ясно, что именно можно вынести из приложения.

4.2. Того, кто расшифровывает, можно привязать к пользователю Windows или к компьютеру

Для обычного настольного приложения часто достаточно выбрать CurrentUser.

Расшифровка возможна при условии, что:

  • этот пользователь выполнил вход
  • обработка идёт в его контексте

Поэтому получается свойство: скопировать только шифротекст на другой ПК и сразу им воспользоваться трудно.

4.3. Легко захватить и обнаружение подделки

Типичный промах своего шифрования: «зашифровали AES — и всё», а обнаружение подделки забыли.

У DPAPI есть и защита целостности зашифрованных данных, поэтому обнаружение того, что шифротекст произвольно переписали, тоже удобно опереть на механизм ОС — это практичный плюс.

Разница в обнаружении подделкиВ своём шифровании после AES часто забывают про обнаружение подделки, а у DPAPI есть защита целостности, поэтому обнаружение переписи шифротекста тоже ложится на ОС.Своё шифрованиелегко забыть про обнаружение подделкиDPAPIесть защита целостностиобнаружение переписи тоже ложится на ОС

Рис. 7: Плюс DPAPI в том, что на ОС ложится не только шифрование, но и обнаружение подделки.

4.4. Из C# / .NET вызывается прямо

В C# System.Security.Cryptography.ProtectedData можно взять как есть. Не тащить лишние библиотеки для приложения только под Windows — тоже заметное удобство.

5. Что DPAPI защищает, а что нет

Здесь лучше провести черту явно.

5.1. Что становится проще защитить

DPAPI полезен как минимум в таких ситуациях:

  • утечка конфигурационного файла открытым текстом
  • вынос файла на другой ПК
  • чтение другим пользователем на том же ПК (при CurrentUser)
  • утечка через резервную копию или вложение
  • состояние «случайно смогли прочитать» на разработке и сопровождении

5.2. Что не защищается или защищается слабо

С другой стороны, в следующих ситуациях на DPAPI лучше не опираться слишком сильно.

  • атакующий код с теми же правами пользователя
  • полная компрометация самого устройства
  • захват с правами администратора
  • открытый текст в памяти после того, как приложение расшифровало
  • долгоживущий секрет, который раздают одинаковым всем клиентам

Последний пункт — «долгоживущий секрет, общий для всех клиентов» — особенно важен.

Например, такие решения:

  • встроить один и тот же API-ключ всем заказчикам
  • держать один и тот же общий пароль на всех устройствах
  • раздавать фиксированный ключ расшифровки, который целиком живёт на клиенте

легко расходятся на всех, едва секрет сняли хотя бы с одной машины. Логика простая: если на какой-то одной машине приложение умеет расшифровать, этот секрет можно извлечь.

DPAPI помогает «сделать место хранения лучше открытого текста», но не оправдывает секрет, которому на клиенте вообще не место.

Как общий долгоживущий секрет расходится на всехЕсли всем клиентам раздают один долгоживущий секрет, приложение на какой-то одной машине умеет его расшифровать, значит секрет можно снять с этой машины и он расходится на всех.Долгоживущий секрет, общий для всех клиентовНа хотя бы одной машине приложение умеет расшифроватьС этой машины секрет можно извлечьРасходится на всехСохранение через DPAPI проблему с корня не закрывает

Рис. 8: У общего секрета падение одной машины бьёт по всем, поэтому пересматривать нужно не способ хранения, а само место, где секрет лежит.

Такие секреты правильнее не «изощрять в хранении», а уводить в другую сторону:

  • положить на сервер
  • на клиенте оставить только токен
  • сделать учётные данные per-user
  • сделать токен с ограниченным сроком

6. Как выбирать CurrentUser и LocalMachine

Это довольно важный момент. Небрежный выбор меняет смысл.

6.1. База — CurrentUser

Для обычного настольного приложения Windows сначала думайте от CurrentUser.

Подходящие примеры:

  • пользовательские настольные приложения на WPF / WinForms / WinUI
  • приложения с настройками и учётными данными на каждого пользователя
  • приложения, которые держат настройки под %LocalAppData% или %AppData%

Тогда данные естественно трактовать как «секрет этого пользователя Windows».

6.2. У LocalMachine применение довольно узкое

LocalMachine выглядит удобно, но для обычного настольного приложения слишком широк.

Подходит, например, в таких случаях:

  • служба Windows на доверенной машине одного назначения
  • секрет, которым пользуется только конкретный процесс на этой машине
  • нужно пользоваться на одном устройстве независимо от того, кто вошёл

Но оговорки тяжёлые.

  • расшифровать могут широко процессы, которые работают на этом ПК
  • опасно на общих терминалах, RDS, jump-хостах, в средах с несколькими пользователями
  • выбор «пока все могут пользоваться — так проще» почти всегда потом бьёт

А мотив выбрать LocalMachine обычно сводится к трём пунктам:

  • читается и после смены пользователя
  • читается и из службы
  • если заработало — удобно

Всё это про «удобно», а не про «защищено». Если в обычном настольном приложении взять LocalMachine, возможность расшифровки расширяется и на другие процессы этого ПК, и смысл решения сильно меняется.

Что будет, если взять LocalMachine ради удобстваЕсли выбрать LocalMachine потому, что читается при смене пользователя и из службы, возможность расшифровки расширяется на другие процессы этого ПК — это удобство, а не защита.Удобно: работает при смене пользователя и из службыВыбирают LocalMachineРасшифровать могут и другие процессы на этом ПКЭто удобство, а не защита

Рис. 9: Если взять LocalMachine ради удобства, расшифровать сможет уже почти весь этот ПК.

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.

Типичный сбой при impersonationDPAPI держит ключевые данные в профиле пользователя, поэтому impersonate без загруженного профиля даёт типичную ошибку расшифровки; профиль целевого пользователя нужно загрузить заранее.impersonate без загруженного профиляРасшифровка завершается ошибкойСначала загрузить профильПосле impersonate расшифровка возможнаКлюч лежит в профиле

Рис. 10: Ключ на стороне профиля, поэтому до impersonate профиль нужно загрузить.

6.5. Иметь в виду случаи, когда расшифровать уже нельзя

На практике больнее не само шифрование, а инцидент «расшифровать больше нельзя». DPAPI привязывает того, кто может расшифровать, к пользователю Windows / компьютеру; если эта связь рвётся, данные не читаются.

Заранее полезно знать эти пять случаев.

Случай Что происходит Как готовиться
Сброс пароля администратором Снимается защита, завязанная на пароль пользователя, и доступ к данным, защищённым DPAPI, может пропасть. В материалах поддержки Microsoft это описано как ситуация, когда после сброса пароля администратором к данным DPAPI доступа больше нет Проектировать секрет так, чтобы его можно было получить заново. При сбое расшифровки направлять на повторный ввод
Повторное создание профиля У нового профиля другой ключевой материал, прежний шифротекст не расшифровать Держать версию конфигурационного файла и не завершать процесс аварийно из-за сбоя расшифровки
Копируют только шифротекст на другой ПК Ключевой материал для расшифровки лежит в профиле пользователя, поэтому один шифротекст CurrentUser унести и прочитать нельзя (это оборотная сторона «сильной стороны» из п. 3.1) Заложить в проект, что на каждом устройстве сохраняют заново
Перемещаемый профиль Здесь как раз читается. Ключевой материал едет вместе с профилем; Microsoft Learn прямо пишет, что пользователь с перемещаемым профилем может расшифровать с другого компьютера в сети. Если обращаться с этим так же, как со строкой выше, в процедуре переноса учётные данные начнут пересоздавать без нужды Не решать заранее, что «другой ПК — значит не прочитать». Сначала проверить, есть ли перемещаемый профиль, и уже потом строить перенос
Смена учётной записи службы Если учётная запись при защите и при расшифровке разная, 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 сервера
  • имя пользователя
  • имя БД
  • флаги функций

А вот это — объект защиты:

  • пароли
  • API-токены
  • refresh-токены
  • учётные данные общей папки

При таком разделении:

  • конфигурацию проще править
  • проще смотреть diff
  • ясно, что именно секрет
  • проще вся эксплуатация
Защищать только секретные поляВместо шифрования всей конфигурации 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. Это удобно, но это не «волшебный второй ключ, который, будучи зашит в двоичный файл, всё сделает безопасным».

  • в том же файле секретом он не становится
  • фиксированное значение в двоичном файле тоже не сильный секрет
  • зато он полезен как идентификатор назначения и защита от неверного использования

На практике достаточно передать фиксированную последовательность байтов из:

  • имени приложения
  • имени назначения
  • идентификатора версии

и использовать это, чтобы «случайно не принять шифротекст другого назначения».

Какое место у optionalEntropyoptionalEntropy — не волшебный второй ключ, который достаточно зашить в двоичный файл; уместно передавать имя приложения и назначение фиксированной последовательностью байтов, чтобы не принять шифротекст другого назначения.этого ждать не стоиттакое применение как раз уместно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

Этот пакет не входит ни в один shared framework .NET Core / .NET 5 и новее. Даже для Windows-цели вроде net8.0-windows ссылку нужно добавить явно.

dotnet add package System.Security.Cryptography.ProtectedData

Ещё один момент, который лучше знать до реализации. ProtectedData только для Windows. Он опирается на DPAPI, поэтому вызов на .NET вне Windows даёт PlatformNotSupportedException. Если кодовая база кроссплатформенная, сразу проектируйте иначе, как в п. 10.1.

Что проверить до вызова ProtectedDataДля ProtectedData нужна ссылка под выбранный target, иначе имя типа не разрешается; плюс это API только для Windows, и вызов вне Windows даёт исключение во время выполнения.если не добавитьвызов не на WindowsХотим использовать ProtectedDataДобавить ссылку под выбранный targetимя типа не разрешаетсяПомнить, что это только Windowsисключение во время выполнения

Рис. 15: До реализации зафиксировать два условия: ссылка добавлена, и API только для Windows.

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.

9.4. Успокоиться, добавив своё шифрование

Вместо DPAPI ставить решения вроде:

  • зашить ключ AES в исходники
  • положить ключ AES в другое поле конфигурационного файла
  • считать ключом «слегка обфусцированную строку»

обычно мало что даёт.

Между «это не открытый текст» и «это безопасно» лежит довольно широкая пропасть.

Опасность успокоиться на своём шифрованииСвоё шифрование с ключом AES в исходниках, в другом поле конфигурации или в обфусцированной строке обычно слабо: «не открытый текст» и «безопасно» разделены широкой пропастью.между ними широкая пропастьСвоё шифрование с ключом в коде или конфигурациисостояние «уже не открытый текст»состояние «это безопасно»эффект обычно слабый

Рис. 16: Своё шифрование с ключом в том же месте даёт только «не открытый текст» и до «безопасно» не дотягивает.

10. Когда DPAPI недостаточно

DPAPI удобен, но не всесилен. В следующих случаях лучше смотреть другие варианты.

10.1. Нужно запускать не только на Windows

DPAPI / ProtectedData рассчитаны на Windows. Кроссплатформенное приложение на этом не построить.

10.2. Один секрет на нескольких машинах или у нескольких пользователей

Требование расшифровывать один шифротекст на нескольких ПК или совместно пользоваться им несколькими пользователями выходит за сильную сторону 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) Берётся как есть
Синхронизация Нет Roaming между устройствами через учётную запись Microsoft Нет (локальный набор учётных данных пользователя)
Ограничения По сути нет Не больше 20 записей на приложение. Не для больших данных Привязан к сеансу входа текущего токена
Кто управляет Приложение (и место хранения, и ACL задаёте сами) ОС (место хранения проектировать не нужно) ОС (можно вести из диспетчера учётных данных в панели управления)

Ориентир выбора такой.

  • Сохраняете пару имя пользователя + пароль, и записей немного -> первый кандидат — Credential Locker / Credential Manager. Место хранения и ACL сами не проектируете
  • Сохраняемое не выглядит как «имя пользователя + пароль» -> строка подключения, API-токен, refresh-токен, фрагмент конфигурационного файла естественнее на стороне DPAPI. Именно это и разбирает статья
  • Нужно переносить между устройствами -> работает roaming Credential Locker. У DPAPI CurrentUser как раз плюс в том, что не переносится, — цели противоположные
  • Много записей / большой размер -> упираетесь в лимит 20 у Credential Locker. Уходите в свой файл через DPAPI

И ещё: хранилище учётных данных тоже сидит на защитном механизме ОС, поэтому это не шкала «безопаснее DPAPI» / «DPAPI слабее». На практике выбирают по форме того, что сохраняют, и по тому, нужен ли roaming.

И какой бы вариант ни взять, для нового приложения имеет смысл сначала посмотреть беспарольные схемы вроде Windows Hello / passkey. Если долгоживущий пароль вообще не нужен, это самый сильный ход.

Центр этой статьи по-прежнему — практическая линия DPAPI, чтобы «на Windows-клиенте убрать открытый текст из конфигурационного файла».

11. Практичный порядок приоритетов

В конце: если на практике сомневаетесь, так думать проще. Смотрите сверху вниз и спускайтесь, только когда условие не выполняется.

Порядок выбора, хранить ли долгоживущий секретСначала проверяют, можно ли обойтись без долгоживущего секрета на устройстве и разделить секрет по пользователям; затем смотрят форму — пара имя пользователя плюс пароль или нет — и нужен ли перенос между устройствами. DPAPI плюс CurrentUser берут, когда секрет остаётся на этом ПК; LocalMachine — только как исключение, когда остальные варианты не сходятся.можнонельзянельзяможноэта пара, и записей малонужнодадомен / локальная учётная записьне нужнотокен или другая форманужноне нужнодостаточно одной (сам пользовательили выделенная служебная учётная запись)нужна расшифровкас нескольких учётных записейможнонельзяМожно ли обойтись бездолгоживущего секрета на устройстве?Приоритет 1: не хранить(проверка подлинности Windows, короткоживущие токены)Можно ли разделить секретпо пользователям?Не получился ли общий ключ?Пересмотреть проектированиеСохраняемое — это параимя пользователя + пароль? (п. 10.3)Нужно ли переноситьмежду устройствами?Устройства синхронизируютсячерез учётную запись Microsoft? (п. 10.3)Roaming Credential LockerDPAPI такое не перенесёт.Хранить на стороне сервера (п. 10.2)Credential Locker /Credential ManagerНужно ли переноситьмежду устройствами?Сколько учётных записейдолжны расшифровывать один секрет?Приоритет 3: DPAPI + CurrentUserдля работы без интерактивного входапроверить загрузку профиля (п. 6.4)Можно ли утверждать,что другие пользователи сюда не входят?Приоритет 4: DPAPI + LocalMachineкак исключение, с зафиксированным обоснованиемТогда расшифрует и другой пользователь.Пересмотреть способ проверки подлинности

Рис. 17: Порядок выбора способа хранения. Сначала форма секрета и нужен ли roaming, и только потом DPAPI. LocalMachine берут не «потому что удобно», а как исключение, когда остальные варианты не сходятся

Приоритет 1: вообще не хранить

  • проверка подлинности Windows
  • встроенная проверка подлинности
  • интерактивный вход
  • секрет остаётся на стороне сервера
  • короткоживущие токены

Приоритет 2: сдвигать секрет к пользователю

  • per-user вместо общего секрета
  • обновляемые токены вместо долгоживущих фиксированных учётных данных
  • не делать ключ, общий для всех клиентов

Приоритет 3: если нужно локальное хранение — DPAPI

  • как правило, CurrentUser
  • место хранения — per-user
  • защищать только секретные поля
  • не писать в лог

Приоритет 4: LocalMachine — как исключение

  • правда ли нужна привязка на уровне машины
  • не заходят ли на это устройство другие пользователи
  • выглядит ли это обоснованно как проектирование службы

12. Итог

Когда Windows-приложению нужно сохранить конфиденциальные сведения в конфигурационном файле, класть их открытым текстом не стоит.

А на вопрос:

«Ключ всё равно где-то хранится, значит, это одно и то же?»

практичнее ответить так.

  • своё шифрование с ключом в том же месте — действительно почти одно и то же
  • DPAPI — не одно и то же
    • управление ключами можно отдать ОС
    • того, кто расшифровывает, можно привязать к пользователю Windows / компьютеру
    • утечка одного файла перестаёт автоматически быть утечкой секрета
  • но это не закрывает
    • код с теми же правами пользователя
    • полностью скомпрометированное устройство
    • долгоживущий общий секрет, которому на клиенте не место

Иначе говоря, DPAPI — не всесильная крепость. Но эффект сопоставим с тем, чтобы заменить настежь прозрачное окно настроек открытым текстом хотя бы на нормально закрытое окно.

В практике Windows-клиентов эта разница очень велика. Самый реалистичный старт — не промахнуться именно здесь.

Практичное место DPAPIDPAPI — не всесильная крепость, но он заменяет настежь прозрачное окно настроек открытым текстом хотя бы на нормально закрытое, и с этого разумно начинать.заменить на DPAPIНастройки открытым текстом = окно настежьхотя бы нормально закрытое окноэто не всесильная крепостьс этого и разумно начинать

Рис. 18: DPAPI — не крепость, а замена окна, но на практике именно эта разница срабатывает сильнее всего.

13. Справочные материалы

  • Предыдущая статья: https://comcomponent.com/ru/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 (Win32 API Windows Credential Manager) 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: управление ключами шифрования он отдаёт операционной системе и привязывает того, кто может расшифровать, к этому пользователю Windows или к этому компьютеру. Из C# / .NET к нему обращаются через класс System.Security.Cryptography.ProtectedData, без лишних библиотек. Ближе к сути смотреть на него не как на API выбора алгоритма шифрования, а как на API, который делегирует управление ключами ОС. Это практичный способ не оставлять пароли и API-токены в конфигурационном файле открытым текстом.
Ключ всё равно где-то хранится. Значит, открытый текст и DPAPI — одно и то же?
Нет, не одно и то же. Если шифровать своим AES и класть ключ в то же приложение или тот же конфигурационный файл, это действительно почти открытый текст. DPAPI сдвигает управление ключом на ОС и привязывает того, кто может расшифровать, к пользователю Windows или к компьютеру. Поэтому устойчивость к таким инцидентам, как утечка одного конфигурационного файла, вынос на другой ПК, ошибочная отправка, утечка резервной копии или попадание в репозиторий, меняется сильно. Решающее отличие от открытого текста: DPAPI отделяет «файл можно прочитать» от «секретом можно воспользоваться».
От чего DPAPI не защищает?
Код с теми же правами пользователя в целом может расшифровать всё, что расшифровывает этот пользователь. Поэтому DPAPI не закрывает ситуацию, когда устройство уже скомпрометировано вредоносным ПО, захват с правами администратора и открытый текст в памяти после расшифровки. Кроме того, долгоживущий секрет, который раздают одинаковым всем клиентам, легко расходится на всех, едва его сняли с одной машины, — сохранение через DPAPI это с корня не лечит. DPAPI работает в основном против утечки файлов, неверного размещения, офлайн-выноса и чтения другим пользователем.
Что выбирать в DataProtectionScope: CurrentUser или LocalMachine?
Для обычного настольного приложения Windows база — CurrentUser. Тогда это секрет данного пользователя, и шифротекст, скопированный на другой ПК, как есть использовать трудно. LocalMachine расшифровывается широко процессами на этом ПК, поэтому опасен на общих терминалах и в средах с несколькими пользователями; применение узкое — например, служба Windows на доверенной машине одного назначения. Если выбрать LocalMachine, потому что «удобно, все смогут пользоваться», почти всегда потом будет больно.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог