Хранение секретов в 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. Сначала вывод
На практике удобнее думать в таком порядке.
- Вообще не отдавать клиенту долгоживущий секрет
- в приоритете проверка подлинности Windows, встроенная проверка подлинности, интерактивный вход пользователя, хранение секретов на стороне сервера
- Если локальное хранение всё же нужно — не класть открытым текстом
- на Windows первым кандидатом брать DPAPI /
ProtectedData
- на Windows первым кандидатом брать DPAPI /
- Для обычного настольного приложения база —
DataProtectionScope.CurrentUser- у
LocalMachineприменение довольно узкое
- у
- DPAPI не защищает до ситуации «устройство полностью скомпрометировано»
- код с теми же правами пользователя в целом расшифровывает то, что может расшифровать этот пользователь
И самый важный тезис статьи вот какой:
«Секретный ключ всё равно нужно где-то хранить. Тогда открытый текст и DPAPI с точки зрения безопасности — одно и то же?»
Наполовину верно, а вывод — нет.
- Свой AES и ключ в том же приложении или в той же конфигурации — это действительно почти открытый текст
- Но DPAPI сдвигает управление ключом на ОС и привязывает того, кто может расшифровать, к «этому пользователю Windows» или к «этому компьютеру»
- В результате устойчивость к утечке одного конфигурационного файла, выносу на другой ПК, ошибочной отправке, утечке резервной копии, попаданию в репозиторий меняется сильно
То есть если смотреть только на абстракцию «ключ где-то есть», варианты кажутся одинаковыми, а «кто, в каком контексте и насколько легко может воспользоваться» — совершенно разные вещи.
Сказать, что ключ под ковриком у двери и ключ, который выдают в администраторской после проверки личности, — это одно и то же, было бы слишком грубо.
flowchart TB
accTitle: «Ключ где-то есть» само по себе ещё не уравнивает варианты
accDescr: На абстракции «ключ где-то есть» открытый текст и DPAPI выглядят одинаково, но кто, в каком контексте и насколько легко может воспользоваться секретом — совсем другое; это центральный тезис статьи.
abs1["Ключ где-то есть (абстракция)"] -.->|"если смотреть только сюда"| same1["кажется, что одно и то же"]
who1["Кто, в каком контексте и насколько легко может воспользоваться"] -->|"если смотреть сюда"| diff1["совсем другое дело"]
Рис. 1: На абстракции одинаково, а по тому, кто и в каком контексте может воспользоваться, открытый текст и DPAPI расходятся.
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 21, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Почему настройки в открытом виде опасны
Опасность хранения открытым текстом гораздо прозаичнее теории криптографии. На практике утечки идут примерно такими путями.
- конфигурационный файл как есть кладут в Git
- в ZIP для разбора сбоя целиком попадает конфигурационный файл
- к обращению в поддержку прикладывают конфигурационный файл
- третьи лица читают его из резервной копии или общей папки
- в лог уходит строка подключения или токен как есть
- уволившийся сотрудник или другой пользователь читает файл на том же устройстве
Открытый текст теряет конфиденциальность в тот момент, когда его прочитала третья сторона.
- открыли файл — всё
- скопировали — всё
- приложили к письму — всё
- осталось в репозитории — почти вечная головная боль
Атакующему даже не нужно быть искусным. «Открывается в текстовом редакторе» само по себе уже очень слабо.
flowchart TB
accTitle: Открытый текст теряет конфиденциальность, как только его прочитали
accDescr: Если конфигурационный файл прочитала третья сторона по прозаическим путям вроде попадания в Git, ZIP для разбора, вложения или утечки резервной копии, открытый текст уже не конфиденциален.
rt1["Попадание в Git, ZIP для разбора, вложение, резервная копия"] --> read1["Конфигурационный файл читают"]
read1 --> end1["Как только третья сторона прочитала, конфиденциальность потеряна"]
end1 -.-> low1["Атакующему даже не нужно быть искусным"]
Рис. 2: Слабость открытого текста в том, что по повседневным путям инцидента третья сторона читает файл — и конфиденциальность уже потеряна.
3. Ответ на «ключ всё равно где-то хранится, значит, это одно и то же?»
Вопрос справедливый. Если ответить на него небрежно, статья про безопасность сразу становится размытой.
Ответ такой: в смысле «ключ где-то нужен» — да; в смысле «поэтому это одно и то же» — нет.
3.1. Что совпадает, а что нет
Да, шифрованию в конечном счёте нужен какой-то root of trust. Иначе говоря, шифрование не обходится без исходной точки доверия.
Но разница в безопасности определяется тремя пунктами.
- держит ли приложение ключ напрямую
- к кому ключ привязан
- можно ли расшифровать, если украли только файл
Если свести это в таблицу, получается так.
| Способ | Прочитали конфигурационный файл | Вынесли только файл на другой ПК | Прочитал другой пользователь на том же ПК | Код с теми же правами пользователя |
|---|---|---|---|---|
| Открытый текст | Утекает сразу | Утекает как есть | Утекает как есть | Разумеется, читает |
| Своё шифрование + ключ в той же конфигурации / в том же двоичном файле | Почти наверняка утекает | Почти наверняка утекает | Почти наверняка утекает | Разумеется, расшифрует |
DPAPI + CurrentUser |
По одному файлу сразу не прочитать | Обычно расшифровать трудно | Обычно расшифровать трудно | Расшифрует |
DPAPI + LocalMachine |
По одному файлу сразу не прочитать | Вне этого ПК обычно расшифровать трудно | На этом ПК расшифровывается широко | Расшифрует |
Важно вот что: DPAPI отделяет «файл можно прочитать» от «секретом можно воспользоваться».
flowchart TB
accTitle: Унести можно только шифротекст, мастер-ключ остаётся на стороне ОС
accDescr: Шифротекст может оказаться в конфигурационном файле, столбце БД или ZIP. Для расшифровки нужны пользовательский мастер-ключ при CurrentUser или мастер-ключ компьютера при LocalMachine; они остаются на стороне ОС. CurrentUser не даёт расшифровать другому пользователю, LocalMachine на этом ПК расшифровывается широко. Один шифротекст на другом ПК не расшифровать, а с перемещаемым профилем ключ едет вместе.
CT["Шифротекст<br/>попадает в конфигурационный файл / столбец БД / ZIP для разбора<br/>= то, что могут унести"]
subgraph WIN["Что нужно для расшифровки ── остаётся на стороне ОС, в шифротекст не входит"]
UMK["Пользовательский мастер-ключ<br/>если защищали CurrentUser"]
MMK["Мастер-ключ компьютера<br/>если защищали LocalMachine"]
end
CT -->|"защита CurrentUser"| UMK
CT -->|"защита LocalMachine"| MMK
UMK --> A1["Код от имени этого пользователя<br/>→ расшифровать можно"]
UMK --> A2["Другой пользователь на том же ПК<br/>→ расшифровать нельзя"]
MMK --> B1["Код на том же ПК<br/>→ расшифрует и другой пользователь"]
UMK --> C1["Копируют только шифротекст на другой ПК<br/>→ расшифровать нельзя"]
MMK --> C1
UMK --> C2["Переносят вместе с перемещаемым профилем<br/>→ ключевой материал едет вместе, расшифровать можно (п. 6.5)"]
Рис. 3: Унести можно только шифротекст, а мастер-ключ для расшифровки остаётся на стороне ОС. Но зона действия LocalMachine — весь этот ПК: другого пользователя на той же машине не отсечь
Открытым текстом эти две вещи совпадают. Прочитали файл — прочитали и секрет.
В DPAPI же, по крайней мере при CurrentUser, расшифровка должна идти:
- от имени этого пользователя Windows
- в контексте этого Windows
- через защитный механизм ОС
На месте реального инцидента эта разница очень велика.
3.2. «Но тот же пользователь всё равно расшифрует?» — да, так и есть
Это нужно сказать прямо, без сглаживания.
Код с теми же правами пользователя в целом может расшифровать всё, что расшифровывает этот пользователь.
То есть DPAPI не рассчитан в первую очередь на такие ситуации:
- устройство уже скомпрометировано вредоносным ПО
- злоумышленник может выполнять код от имени этого пользователя
- устройство полностью захвачено на уровне администратора
В такой ситуации приложение само умеет расшифровать — значит, умеет и код злоумышленника. Фраза «но ведь мы зашифровали» здесь мало что даёт.
DPAPI работает в основном против «утечки файлов, неверного размещения, офлайн-выноса и чтения другим пользователем».
Если это перепутать, случается и то и другое:
- недооценить то, что реально защищается, и не использовать
- переоценить то, что защитить нельзя, и успокоиться
Оба варианта по-тихому опасны.
flowchart TB
accTitle: Где DPAPI работает, а где нет
accDescr: DPAPI работает против утечки файлов, неверного размещения, офлайн-выноса и чтения другим пользователем и не работает против атакующего кода с теми же правами пользователя и уже скомпрометированного устройства.
dp2["Область защиты DPAPI"] -->|"работает"| eff1["утечка, неверное размещение, другой пользователь"]
dp2 -.->|"не работает"| noef1["код с теми же правами пользователя"]
dp2 -.-> mis1["если перепутать — оценка будет неверной"]
Рис. 4: Не зная этой границы, либо отказываются от средства, либо слишком успокаиваются.
3.3. В чём тогда польза
Пользу DPAPI одной фразой:
«сам секрет можно отвязать от того, что конфигурационный файл читается».
Например, в таких инцидентах открытый текст и DPAPI расходятся:
- пользователь отправил конфигурационный файл в поддержку
- в ZIP для разбора попал конфигурационный файл
- из резервной копии утёк только конфигурационный файл
- файл скопировали в общую папку
- разработчик видит только шифротекст и не может прочитать содержимое
Это вполне практичное преимущество. Не нужно представлять злоумышленника киношным суперменом — достаточно уменьшить радиус повседневных инцидентов.
flowchart TB
accTitle: Как уменьшается радиус инцидента
accDescr: Даже если файл уходит наружу из-за ошибочной отправки в поддержку, ZIP для разбора или утечки резервной копии, наружу выходит только шифротекст, поэтому сам секрет не утекает и радиус повседневных инцидентов становится меньше.
ac1["Ошибочная отправка, ZIP для разбора, утечка резервной копии"] --> out3["Наружу уходит только шифротекст"]
out3 --> sm2["Утечки самого секрета удаётся избежать"]
sm2 --> rad1["Радиус повседневных инцидентов уменьшается"]
Рис. 5: Сам уход файла наружу не предотвратить, но уходящее можно сделать шифротекстом.
4. Почему DPAPI — разумная середина
Когда на Windows нужно хранить секрет локально, DPAPI на практике оказывается разумной серединой по следующим причинам.
4.1. Управление ключами можно отдать ОС
Самому сгенерировать ключ AES, сохранить его, назначить права, сделать ротацию, продумать последствия утечки и ещё добавить обнаружение подделки. Это тяжелее, чем кажется. А если сделать небрежно, ключ почти всегда кладут туда же, где данные.
С DPAPI вопрос «как сделать ключ шифрования и куда его положить» можно вынести из реализации приложения.
В этом смысле на DPAPI правильнее смотреть не как на «API выбора алгоритма шифрования», а как на «API, который делегирует управление ключами ОС».
flowchart TB
accTitle: Как смотреть на DPAPI по сути
accDescr: DPAPI ближе к API, который отдаёт ОС вопрос, как сделать ключ и куда его положить, а не к API выбора алгоритма шифрования.
v1["API выбора алгоритма"] -.->|"так смотреть не стоит"| dpv1["DPAPI"]
v2["API, который отдаёт управление ключами ОС"] -->|"так смотреть ближе к сути"| dpv1
dpv1 --> off1["Генерацию и хранение ключа можно вынести из реализации"]
Рис. 6: Если видеть в DPAPI место, куда отдано управление ключами, становится ясно, что именно можно вынести из приложения.
4.2. Того, кто расшифровывает, можно привязать к пользователю Windows или к компьютеру
Для обычного настольного приложения часто достаточно выбрать CurrentUser.
Расшифровка возможна при условии, что:
- этот пользователь выполнил вход
- обработка идёт в его контексте
Поэтому получается свойство: скопировать только шифротекст на другой ПК и сразу им воспользоваться трудно.
4.3. Легко захватить и обнаружение подделки
Типичный промах своего шифрования: «зашифровали AES — и всё», а обнаружение подделки забыли.
У DPAPI есть и защита целостности зашифрованных данных, поэтому обнаружение того, что шифротекст произвольно переписали, тоже удобно опереть на механизм ОС — это практичный плюс.
flowchart TB
accTitle: Разница в обнаружении подделки
accDescr: В своём шифровании после AES часто забывают про обнаружение подделки, а у DPAPI есть защита целостности, поэтому обнаружение переписи шифротекста тоже ложится на ОС.
diy1["Своё шифрование"] -.-> forget1["легко забыть про обнаружение подделки"]
dpi1["DPAPI"] --> integ1["есть защита целостности"]
integ1 --> det2["обнаружение переписи тоже ложится на ОС"]
Рис. 7: Плюс DPAPI в том, что на ОС ложится не только шифрование, но и обнаружение подделки.
4.4. Из C# / .NET вызывается прямо
В C# System.Security.Cryptography.ProtectedData можно взять как есть.
Не тащить лишние библиотеки для приложения только под Windows — тоже заметное удобство.
5. Что DPAPI защищает, а что нет
Здесь лучше провести черту явно.
5.1. Что становится проще защитить
DPAPI полезен как минимум в таких ситуациях:
- утечка конфигурационного файла открытым текстом
- вынос файла на другой ПК
- чтение другим пользователем на том же ПК (при
CurrentUser) - утечка через резервную копию или вложение
- состояние «случайно смогли прочитать» на разработке и сопровождении
5.2. Что не защищается или защищается слабо
С другой стороны, в следующих ситуациях на DPAPI лучше не опираться слишком сильно.
- атакующий код с теми же правами пользователя
- полная компрометация самого устройства
- захват с правами администратора
- открытый текст в памяти после того, как приложение расшифровало
- долгоживущий секрет, который раздают одинаковым всем клиентам
Последний пункт — «долгоживущий секрет, общий для всех клиентов» — особенно важен.
Например, такие решения:
- встроить один и тот же API-ключ всем заказчикам
- держать один и тот же общий пароль на всех устройствах
- раздавать фиксированный ключ расшифровки, который целиком живёт на клиенте
легко расходятся на всех, едва секрет сняли хотя бы с одной машины. Логика простая: если на какой-то одной машине приложение умеет расшифровать, этот секрет можно извлечь.
DPAPI помогает «сделать место хранения лучше открытого текста», но не оправдывает секрет, которому на клиенте вообще не место.
flowchart TB
accTitle: Как общий долгоживущий секрет расходится на всех
accDescr: Если всем клиентам раздают один долгоживущий секрет, приложение на какой-то одной машине умеет его расшифровать, значит секрет можно снять с этой машины и он расходится на всех.
com1["Долгоживущий секрет, общий для всех клиентов"] --> one4["На хотя бы одной машине приложение умеет расшифровать"]
one4 --> ext1["С этой машины секрет можно извлечь"]
ext1 --> all1["Расходится на всех"]
com1 -.-> np2["Сохранение через 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, возможность расшифровки расширяется и на другие процессы этого ПК, и смысл решения сильно меняется.
flowchart TB
accTitle: Что будет, если взять LocalMachine ради удобства
accDescr: Если выбрать LocalMachine потому, что читается при смене пользователя и из службы, возможность расшифровки расширяется на другие процессы этого ПК — это удобство, а не защита.
ease1["Удобно: работает при смене пользователя и из службы"] --> pick4["Выбирают LocalMachine"]
pick4 --> wide1["Расшифровать могут и другие процессы на этом ПК"]
wide1 -.-> not1["Это удобство, а не защита"]
Рис. 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.
flowchart TB
accTitle: Типичный сбой при impersonation
accDescr: DPAPI держит ключевые данные в профиле пользователя, поэтому impersonate без загруженного профиля даёт типичную ошибку расшифровки; профиль целевого пользователя нужно загрузить заранее.
imp1["impersonate без загруженного профиля"] --> ferr1["Расшифровка завершается ошибкой"]
ld1["Сначала загрузить профиль"] --> okp1["После impersonate расшифровка возможна"]
imp1 -.-> whyp1["Ключ лежит в профиле"]
Рис. 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;
}
}
Обращения вида «зашифровали, а расшифровать не можем» почти всегда из этой таблицы.
flowchart TB
accTitle: Поток кода из посылки, что расшифровка может не удаться
accDescr: Код пишут из посылки, что ProtectedData.Unprotect может не удаться из-за смены среды: CryptographicException перехватывают, процесс не роняют и направляют на повторный ввод.
upx1["Пробуем Unprotect"] -->|"успех"| use2["Пользуемся секретом"]
upx1 -->|"сбой с исключением"| ctc1["Перехватываем исключение и не роняем процесс"]
ctc1 --> rein1["Направляем на повторный ввод"]
upx1 -.-> why2["Если связь с ключом оборвалась, вызов может не удаться"]
Рис. 11: Расшифровку пишут как операцию, которая может не удаться, и со сбоя ведут не в аварийное завершение, а на повторный ввод.
7. Минимальные ориентиры реализации
Если цель Windows-приложения — просто «убрать открытый текст из конфигурационного файла», проектирование не обязано быть сложным. Но несколько пунктов лучше не пропускать.
7.1. Защищать только секреты
Вместо шифрования всей конфигурации целиком проще сначала защитить только секретные поля.
Например, разделить так.
Часто можно оставить открытым текстом:
- URL сервера
- имя пользователя
- имя БД
- флаги функций
А вот это — объект защиты:
- пароли
- API-токены
- refresh-токены
- учётные данные общей папки
При таком разделении:
- конфигурацию проще править
- проще смотреть diff
- ясно, что именно секрет
- проще вся эксплуатация
flowchart TB
accTitle: Защищать только секретные поля
accDescr: Вместо шифрования всей конфигурации URL, имя пользователя и флаги оставляют открытым текстом, а защищают только секреты вроде пароля и токена — так проще править и ясно, что является секретом.
cfg2["Конфигурационный файл"] -->|"оставить открытым текстом"| pl1["URL, имя пользователя, флаги и т. д."]
cfg2 -->|"защитить"| sc1["пароль, токен и т. д."]
sc1 --> mr1["Ясно, что секрет, и эксплуатация проще"]
Рис. 12: Не шифровать всё целиком, а защитить только секретные поля — так удобство и безопасность уживаются.
7.2. Место хранения по умолчанию — per-user
Для обычного настольного приложения место хранения по умолчанию — каталог per-user.
%LocalAppData%\Vendor\App\settings.json%AppData%\Vendor\App\settings.json
Как минимум не стоит небрежно класть файл в каталог установки или в место, которое легко расшарить.
Даже при защите DPAPI, если ACL места хранения небрежные, получается: «шифротекст читают», «структура конфигурации видна», «ошибки эксплуатации случаются». Защита лучше работает не одним слоем, а несколькими.
7.3. optionalEntropy — не волшебный второй ключ
В ProtectedData можно передать optionalEntropy.
Это удобно, но это не «волшебный второй ключ, который, будучи зашит в двоичный файл, всё сделает безопасным».
- в том же файле секретом он не становится
- фиксированное значение в двоичном файле тоже не сильный секрет
- зато он полезен как идентификатор назначения и защита от неверного использования
На практике достаточно передать фиксированную последовательность байтов из:
- имени приложения
- имени назначения
- идентификатора версии
и использовать это, чтобы «случайно не принять шифротекст другого назначения».
flowchart TB
accTitle: Какое место у optionalEntropy
accDescr: optionalEntropy — не волшебный второй ключ, который достаточно зашить в двоичный файл; уместно передавать имя приложения и назначение фиксированной последовательностью байтов, чтобы не принять шифротекст другого назначения.
ent1["optionalEntropy"] -.->|"этого ждать не стоит"| key2["волшебный второй ключ"]
ent1 -->|"такое применение как раз уместно"| tag1["идентификатор: имя приложения и назначение"]
tag1 --> guard1["Не принять по ошибке шифротекст другого назначения"]
Рис. 13: entropy — не секретный ключ, а метка назначения и защита от неверного использования.
7.4. Это не значит, что шифротекст можно класть в Git
И это по-тихому важно.
Шифротекст DPAPI гораздо лучше открытого текста, но это не разрешение класть в репозиторий конфигурационный файл целиком.
Причины простые:
- шифротекст живёт долго
- когда-нибудь могут воспроизвести то же устройство или тот же контекст
- в файле есть и сведения помимо самого секрета
- появляется культура «раз защищено — можно обращаться небрежно»
«Лучше открытого текста» и «безопасно, где ни лежи» — совершенно разные вещи.
flowchart TB
accTitle: Почему шифротекст тоже не кладут в Git
accDescr: Шифротекст DPAPI лучше открытого текста, но в репозитории он живёт долго, содержит и другие сведения и расслабляет культуру обращения, поэтому это не то же самое, что «безопасно где угодно».
enc1["Шифротекст DPAPI"] -->|"лучше открытого текста"| bet2["устойчивее к инцидентам"]
enc1 -.->|"и всё же"| git1["в репозиторий не класть"]
git1 --> rs1["долго живёт, есть и другие данные, культура обращений слабеет"]
Рис. 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.
flowchart TB
accTitle: Что проверить до вызова ProtectedData
accDescr: Для ProtectedData нужна ссылка под выбранный target, иначе имя типа не разрешается; плюс это API только для Windows, и вызов вне Windows даёт исключение во время выполнения.
use3["Хотим использовать ProtectedData"] --> ref1["Добавить ссылку под выбранный target"]
ref1 -.->|"если не добавить"| unres1["имя типа не разрешается"]
use3 --> winonly1["Помнить, что это только Windows"]
winonly1 -.->|"вызов не на Windows"| pnse1["исключение во время выполнения"]
Рис. 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 в другое поле конфигурационного файла
- считать ключом «слегка обфусцированную строку»
обычно мало что даёт.
Между «это не открытый текст» и «это безопасно» лежит довольно широкая пропасть.
flowchart TB
accTitle: Опасность успокоиться на своём шифровании
accDescr: Своё шифрование с ключом AES в исходниках, в другом поле конфигурации или в обфусцированной строке обычно слабо: «не открытый текст» и «безопасно» разделены широкой пропастью.
hm1["Своё шифрование с ключом в коде или конфигурации"] --> npl1["состояние «уже не открытый текст»"]
npl1 -.->|"между ними широкая пропасть"| sfe1["состояние «это безопасно»"]
hm1 -.-> thin1["эффект обычно слабый"]
Рис. 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. Практичный порядок приоритетов
В конце: если на практике сомневаетесь, так думать проще. Смотрите сверху вниз и спускайтесь, только когда условие не выполняется.
flowchart TD
accTitle: Порядок выбора, хранить ли долгоживущий секрет
accDescr: Сначала проверяют, можно ли обойтись без долгоживущего секрета на устройстве и разделить секрет по пользователям; затем смотрят форму — пара имя пользователя плюс пароль или нет — и нужен ли перенос между устройствами. DPAPI плюс CurrentUser берут, когда секрет остаётся на этом ПК; LocalMachine — только как исключение, когда остальные варианты не сходятся.
Q1{"Можно ли обойтись без<br/>долгоживущего секрета на устройстве?"}
Q1 -->|"можно"| A1["Приоритет 1: не хранить<br/>(проверка подлинности Windows, короткоживущие токены)"]
Q1 -->|"нельзя"| Q2{"Можно ли разделить секрет<br/>по пользователям?"}
Q2 -->|"нельзя"| A2["Не получился ли общий ключ?<br/>Пересмотреть проектирование"]
Q2 -->|"можно"| QF{"Сохраняемое — это пара<br/>имя пользователя + пароль? (п. 10.3)"}
QF -->|"эта пара, и записей мало"| QR1{"Нужно ли переносить<br/>между устройствами?"}
QR1 -->|"нужно"| QA{"Устройства синхронизируются<br/>через учётную запись Microsoft? (п. 10.3)"}
QA -->|"да"| CL2["Roaming Credential Locker"]
QA -->|"домен / локальная учётная запись"| SRV
QR1 -->|"не нужно"| CL["Credential Locker /<br/>Credential Manager"]
QF -->|"токен или другая форма"| QR2{"Нужно ли переносить<br/>между устройствами?"}
QR2 -->|"нужно"| SRV["DPAPI такое не перенесёт.<br/>Хранить на стороне сервера (п. 10.2)"]
QR2 -->|"не нужно"| Q3{"Сколько учётных записей<br/>должны расшифровывать один секрет?"}
Q3 -->|"достаточно одной (сам пользователь<br/>или выделенная служебная учётная запись)"| A3["Приоритет 3: DPAPI + CurrentUser<br/>для работы без интерактивного входа<br/>проверить загрузку профиля (п. 6.4)"]
Q3 -->|"нужна расшифровка<br/>с нескольких учётных записей"| Q4{"Можно ли утверждать,<br/>что другие пользователи сюда не входят?"}
Q4 -->|"можно"| A4["Приоритет 4: DPAPI + LocalMachine<br/>как исключение, с зафиксированным обоснованием"]
Q4 -->|"нельзя"| A5["Тогда расшифрует и другой пользователь.<br/>Пересмотреть способ проверки подлинности"]
Рис. 17: Порядок выбора способа хранения. Сначала форма секрета и нужен ли roaming, и только потом DPAPI. LocalMachine берут не «потому что удобно», а как исключение, когда остальные варианты не сходятся
Приоритет 1: вообще не хранить
- проверка подлинности Windows
- встроенная проверка подлинности
- интерактивный вход
- секрет остаётся на стороне сервера
- короткоживущие токены
Приоритет 2: сдвигать секрет к пользователю
- per-user вместо общего секрета
- обновляемые токены вместо долгоживущих фиксированных учётных данных
- не делать ключ, общий для всех клиентов
Приоритет 3: если нужно локальное хранение — DPAPI
- как правило,
CurrentUser - место хранения — per-user
- защищать только секретные поля
- не писать в лог
Приоритет 4: LocalMachine — как исключение
- правда ли нужна привязка на уровне машины
- не заходят ли на это устройство другие пользователи
- выглядит ли это обоснованно как проектирование службы
12. Итог
Когда Windows-приложению нужно сохранить конфиденциальные сведения в конфигурационном файле, класть их открытым текстом не стоит.
А на вопрос:
«Ключ всё равно где-то хранится, значит, это одно и то же?»
практичнее ответить так.
- своё шифрование с ключом в том же месте — действительно почти одно и то же
- DPAPI — не одно и то же
- управление ключами можно отдать ОС
- того, кто расшифровывает, можно привязать к пользователю Windows / компьютеру
- утечка одного файла перестаёт автоматически быть утечкой секрета
- но это не закрывает
- код с теми же правами пользователя
- полностью скомпрометированное устройство
- долгоживущий общий секрет, которому на клиенте не место
Иначе говоря, DPAPI — не всесильная крепость. Но эффект сопоставим с тем, чтобы заменить настежь прозрачное окно настроек открытым текстом хотя бы на нормально закрытое окно.
В практике Windows-клиентов эта разница очень велика. Самый реалистичный старт — не промахнуться именно здесь.
flowchart TB
accTitle: Практичное место DPAPI
accDescr: DPAPI — не всесильная крепость, но он заменяет настежь прозрачное окно настроек открытым текстом хотя бы на нормально закрытое, и с этого разумно начинать.
glass1["Настройки открытым текстом = окно настежь"] -->|"заменить на DPAPI"| win2["хотя бы нормально закрытое окно"]
win2 -.-> notwall1["это не всесильная крепость"]
win2 --> first1["с этого и разумно начинать"]
Рис. 18: DPAPI — не крепость, а замена окна, но на практике именно эта разница срабатывает сильнее всего.
13. Справочные материалы
- Предыдущая статья: https://comcomponent.com/ru/blog/2026/03/14/001-windows-app-security-minimum-checklist/
- Microsoft Learn:
CryptProtectDatahttps://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata - Microsoft Learn:
ProtectedDatahttps://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata?view=windowsdesktop-10.0 - Microsoft Learn:
DataProtectionScopehttps://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
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Как в Windows-приложении вынести только операции, которым нужны права администратора
Разбираем, как оставить UI Windows-приложения asInvoker и вынести операции с правами администратора в helper EXE: UAC, runas, именованные...
Минимальный чек-лист безопасности при разработке Windows-приложений
Чек-лист базовых мер безопасности для бизнес-приложений на WPF / WinForms / WinUI / C++ / C#: права, подпись кода, обновления, секреты, H...
Глубины ввода-вывода Windows (часть 6, финал) — фильтры и минифильтры: почему Procmon и антивирусное сканирование могут перехватывать I/O
Финал серии со схемами драйверов фильтров и минифильтров Windows. Диспетчер фильтров и высота (altitude), обратные вызовы pre/post, как P...
Безопасный вызов Win32 API из C# — практическое руководство по P/Invoke (DllImport / LibraryImport / CsWin32)
Разбираем, как на практике вызывать Win32 API из C# через P/Invoke: чем DllImport отличается от LibraryImport, как CsWin32 генерирует сиг...
Как работать с токенами олицетворения Windows — олицетворение на уровне потока и безопасный откат
В статье собраны практические правила безопасной работы с токенами олицетворения Windows: токен доступа, первичный токен, токен потока, у...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Способ хранения учётных данных, место per-user настроек и то, что попадает в лог, относятся к проектированию Windows-приложения в целом, поэтому тема хорошо стыкуется с разработкой Windows-приложений.
Технические консультации и ревью дизайна
Если хотите начать с пересмотра настроек в открытом виде в существующем приложении или с того, когда брать DPAPI, а когда Credential Locker, тему удобно вести как техническую консультацию и ревью архитектуры.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Что такое 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, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.