Windows 앱의 기밀 정보 저장 - DPAPI로 평문 설정 피하기

· 업데이트: · · Windows 개발, 보안, DPAPI, C# / .NET, Win32

수정 이력(1건, 최종 수정 2026년 09월 01일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635144)
최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635143)

이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.

小村 豪 (2026). 「Windows 앱의 기밀 정보 저장 - DPAPI로 평문 설정 피하기」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635143 https://comcomponent.com/ko/blog/2026/03/16/000-windows-app-secret-storage-best-practices-dpapi/

DOI(최신 버전)
10.5281/zenodo.21635143
DOI(이 버전)
10.5281/zenodo.22217442

이전 글 「Windows 앱 개발에서 최소한의 보안을 지키기 위한 체크리스트」에서는 「비밀 정보를 소스 코드나 평문 설정에 두지 않는다」「Win32 / .NET이라면 DPAPI / ProtectedData를 쓴다」라는 최소한의 선을 썼습니다.

이번에는 그중에서도 「DPAPI를 써서 최소한 평문보다는 낫게 한다」를 좀 더 파고듭니다.

대상은 다음과 같은 Windows 앱입니다.

  • WPF / WinForms / WinUI 데스크톱 앱
  • C# / .NET의 Windows 클라이언트
  • 로컬 설정 파일에 접속처 자격 증명이나 API 토큰을 저장하고 싶어지는 앱

여기서 다루는 것은 「로컬에 저장할 수밖에 없는 비밀을, 적어도 appsettings.json의 평문 그대로 방치하지 않기」위한 현실적인 설계입니다. 「어떤 공격자에게도 이기는 완전 방어」에 관한 이야기가 아닙니다. 이야기를 과하게 부풀리면 현실적인 보안 논의에서 멀어집니다.

1. 먼저 결론

실무에서는 이 순서로 생각하는 것이 이해하기 쉽습니다.

  1. 애초에 클라이언트에 장기 비밀을 두지 않는다
    • Windows 인증, 통합 인증, 사용자의 대화형 로그인, 서버 측 비밀 관리를 우선한다
  2. 어쩔 수 없이 로컬 저장이 필요하다면 평문으로 두지 않는다
    • Windows라면 먼저 DPAPI / ProtectedData를 제1 후보로 한다
  3. 일반 데스크톱 앱은 DataProtectionScope.CurrentUser를 기본으로 한다
    • LocalMachine은 용도가 꽤 제한적이다
  4. DPAPI는 「단말이 완전히 침해된 상황」까지 지키는 것은 아니다
    • 같은 사용자 권한으로 도는 코드는 기본적으로 그 사용자가 복호화할 수 있는 것을 복호화할 수 있다

그리고 이 글에서 가장 중요한 논점은 여기입니다.

「어차피 비밀 키는 어딘가에 저장해야 하니까, 평문이든 DPAPI든 보안상으로는 같은 것 아닌가?」

이것은 절반은 맞고, 결론은 틀립니다.

  • 자체 AES + 키를 같은 앱이나 같은 설정에 두는 경우라면 꽤 평문에 가깝습니다
  • 하지만 DPAPI는 키 관리를 OS에 맡기고, 복호화할 수 있는 주체를 “그 Windows 사용자” 또는 “그 컴퓨터”에 묶습니다
  • 그 결과 설정 파일 단체의 유출, 다른 PC로의 반출, 오송, 백업 유출, 리포지토리 혼입 같은 사고에 대한 내성이 크게 달라집니다

즉, “키가 어딘가에 있다”는 추상론만 보면 같아 보여도, “누가·어떤 맥락에서·얼마나 쉽게 쓸 수 있는가”가 전혀 다르다는 이야기입니다.

현관 매트 밑에 열쇠를 두는 것과 관리실에서 본인 확인을 거쳐 열쇠를 내주는 것을 같다고 단언하는 것은 다소 거칩니다.

"키는 어딘가에 있다"만으로는 같아지지 않는다키가 어딘가에 있다는 추상론에서는 평문도 DPAPI도 같아 보이지만, 누가 어떤 맥락에서 얼마나 쉽게 쓸 수 있는지는 전혀 다르다는, 이 글의 중심 논점을 보여주는 그림.여기만 보면여기를 보면키가 어딘가에 있음(추상론)같아 보임누가·어떤 맥락에서·얼마나 쉽게 쓸 수 있는가전혀 다름

그림 1: 추상론에서는 같아 보여도, “누가 어떤 맥락에서 쓸 수 있는가”로 평문과 DPAPI는 갈립니다.

이 글의 지식 맵

이 글은 DPAPI(ProtectedData)를 사용해 Windows 데스크톱 앱의 비밀 정보(접속 문자열이나 API 토큰 등)를 평문 설정보다 안전하게 저장하는 방법을 다룹니다. 평문 저장이나 키를 암호문과 같은 곳에 두는 자체 암호화는 설정 파일 단체의 유출만으로 그대로 비밀이 새어 나가지만, DPAPI는 복호화 주체를 Windows 사용자 또는 컴퓨터에 묶음으로써 이를 완화합니다. 일반적인 데스크톱 앱은 CurrentUser 스코프를 기본으로 하고, LocalMachine은 신뢰된 단일 용도의 Windows 서비스 등으로 용도를 좁혀야 합니다. 다만 비밀번호 재설정이나 프로필 재생성, 암호문만 다른 PC로 복사하는 것은 복호화 실패의 원인이 되며, 반대로 로밍 프로필이라면 키 자료도 함께 이동하기 때문에 다른 PC에서도 복호화할 수 있습니다. 사용자명과 비밀번호 조합을 저장하고 싶은 경우나 여러 머신에서 비밀을 공유해야 하는 경우에는 DPAPI보다 Credential Locker나 Credential Manager 쪽이 적합합니다.

DPAPI를 통한 Windows 앱 비밀 정보 보호 지식 맵DPAPI가 CurrentUser와 LocalMachine 스코프로 복호화 주체를 전환할 수 있다는 것, 평문 저장이나 자체 암호화가 초래하는 유출 위험을 DPAPI가 완화한다는 것, Credential Locker나 Credential Manager와의 구분 사용, 복호화 실패를 부르는 운용상의 함정 사이의 관계를 보여주는 그림원인이 될 수 있다완화한다원인이 될 수 있다사용은 비권장에서 구성할 수 있다에서 구성할 수 있다권장되는 대응권장되는 대응사용은 비권장원인이 될 수 있다권장되는 대응사용은 비권장권장되는 대응권장되는 대응전제로 한다양립하지 않는다권장되는 대응원인이 될 수 있다원인이 될 수 있다원인이 될 수 있다방지한다DPAPI앱 비밀 정보비밀 정보의 평문 저장설정 파일 유출로 인한 비밀 정보 노출암호문과 같은 곳에 키를 두는 자체 암호화전체 클라이언트 공용 장기 비밀DataProtectionScope.CurrentUserDataProtectionScope.LocalMachine대화형 사용자의 데스크톱 앱Windows 서비스DPAPI 복호화 실패사용자 이름과 암호 쌍Credential Locker(PasswordVault)Credential Manager(CredWrite/CredRead)Microsoft 계정여러 머신·여러 사용자 간 비밀 공유관리자에 의한 비밀번호 재설정프로필 재생성암호문만 다른 PC로 복사로밍 프로필

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 21건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

2. 왜 평문 설정이 위험한가

평문 저장이 위험한 이유는 암호 이론보다 훨씬 일상적인 이야기입니다. 실무에서는 대체로 다음과 같은 경로로 샙니다.

  • 설정 파일을 그대로 Git에 넣어 버린다
  • 장애 조사용 ZIP에 설정 파일이 통째로 들어간다
  • 지원 문의에 설정 파일을 첨부하게 한다
  • 백업이나 파일 공유로 제삼자가 읽을 수 있다
  • 로그에 접속 문자열이나 토큰이 그대로 찍힌다
  • 퇴사자나 다른 사용자가 같은 단말의 파일을 읽을 수 있다

평문은 제삼자에게 읽힌 시점에 비밀성을 잃습니다.

  • 파일을 열면 끝
  • 복사되면 끝
  • 메일에 첨부되면 끝
  • 리포지토리에 남으면 반영구적으로 뒷감당해야 한다

공격자가 고도의 기술을 가질 필요조차 없습니다. 텍스트 에디터로 열 수 있다는 것만으로 이미 꽤 약한 상태입니다.

평문은 읽힌 시점에 비밀성을 잃는다Git 혼입이나 조사용 ZIP, 백업 유출, 첨부 같은 일상적인 경로로 설정 파일이 제삼자에게 읽히면 평문은 비밀성을 잃는다는, 이 글의 중심 논점을 보여주는 그림.Git 혼입·조사용 ZIP·첨부·백업설정 파일이 읽힘제삼자에게 읽힌 시점에 비밀성을 잃음공격자가 고도의 기술을 가질 필요조차 없음

그림 2: 평문의 약점은 일상적인 사고 경로로 제삼자에게 읽힌 시점에 비밀성을 잃는다는 것입니다.

3. 「비밀 키는 어딘가에 저장되는 거니까 같은 것 아닌가?」에 대한 답

이 의문은 타당합니다. 그리고 여기를 대충 답하면 보안 글이 단숨에 흐릿해집니다.

답을 먼저 말하면, 「어딘가에 키가 필요하다」는 의미에서는 yes, 그러니까 같다는 의미에서는 no입니다.

3.1. 무엇이 같고 무엇이 다른가

확실히 암호화에는 최종적으로 어떤 root of trust가 필요합니다. 즉, 암호화에서는 최종적으로 어떤 신뢰의 기점이 필요합니다.

다만 보안상의 차이는 다음 3가지로 결정됩니다.

  • 키를 앱이 직접 가지고 있는가
  • 키가 어떤 주체에 묶여 있는가
  • 파일만 도둑맞았을 때 복호화할 수 있는가

이 차이를 대략 표로 정리하면 다음과 같습니다.

방식 설정 파일이 읽혔을 때 파일만 다른 PC로 반출됐을 때 같은 PC의 다른 사용자에게 읽혔을 때 같은 사용자 권한으로 도는 코드
평문 그 자리에서 샘 그대로 샘 그대로 샘 당연히 읽힘
자체 암호화 + 키를 같은 설정 / 바이너리에 둠 꽤 샘 꽤 샘 꽤 샘 당연히 복호화됨
DPAPI + CurrentUser 파일 단체로는 바로 안 읽힘 통상 복호화하기 어려움 통상 복호화하기 어려움 복호화 가능
DPAPI + LocalMachine 파일 단체로는 바로 안 읽힘 그 PC 이외에서는 통상 복호화하기 어려움 같은 PC라면 폭넓게 복호화 가능 복호화 가능

여기서 중요한 것은 DPAPI가 「파일을 읽을 수 있다」는 것과 「비밀을 쓸 수 있다」는 것을 분리한다는 점입니다.

복호화에 필요한 것 ── OS 쪽에 남고, 암호문에는 포함되지 않음CurrentUser로 보호LocalMachine으로 보호사용자의 마스터 키CurrentUser로 보호했을 때컴퓨터의 마스터 키LocalMachine으로 보호했을 때암호문설정 파일 / DB의 열 / 조사용 ZIP에 들어감= 반출될 수 있는 것그 사용자로 도는 코드→ 복호화 가능같은 PC의 다른 사용자→ 복호화 불가같은 PC 위의 코드→ 다른 사용자라도 복호화 가능암호문만 다른 PC로 복사→ 복호화 불가로밍 프로필째로 이동→ 키 자료도 함께 이동하므로 복호화 가능(6.5절)

그림 3: 암호문만 반출될 수 있는 쪽에 있고, 복호화에 필요한 마스터 키는 OS 쪽에 남습니다. 다만 LocalMachine이 미치는 범위는 그 PC 전체이며, 같은 PC의 다른 사용자를 막을 수는 없습니다.

평문에서는 이 둘이 같습니다. 파일을 읽을 수 있으면 비밀도 읽을 수 있습니다.

하지만 DPAPI에서는 적어도 CurrentUser라면

  • 그 Windows 사용자로서
  • 그 Windows의 맥락에서
  • OS의 보호 기구를 거쳐서

복호화해야 합니다.

이 차이는 사고 현장에서는 상당히 큽니다.

3.2. 「그래도 같은 사용자라면 복호화되지 않나?」는 맞는 말입니다

여기는 얼버무리지 않고 써야 할 지점입니다.

같은 사용자 권한으로 실행되는 코드는 그 사용자가 복호화할 수 있는 것을 기본적으로 복호화할 수 있습니다.

즉, DPAPI는 다음과 같은 상황을 주된 목적으로 삼지 않습니다.

  • 이미 그 단말이 악성코드에 침해되어 있다
  • 공격자가 그 사용자로서 코드를 실행할 수 있다
  • 단말 관리자 레벨에서 완전히 탈취되어 있다

이 상황에서는 앱 자신도 복호화할 수 있으므로, 공격자의 코드도 복호화할 수 있어 버립니다. 여기서 「그래도 암호화되어 있습니다」는 별로 든든하지 않습니다.

DPAPI가 효과를 발휘하는 것은 주로 “파일 유출·잘못된 배치·오프라인 반출·다른 사용자로부터의 참조” 쪽입니다.

여기를 혼동하면

  • 지킬 수 있는 것을 과소평가해서 쓰지 않는다
  • 지킬 수 없는 것을 과대평가해서 안심한다

두 가지가 모두 일어납니다. 둘 다 은근히 위험합니다.

DPAPI가 효과를 발휘하는 쪽과 발휘하지 못하는 쪽DPAPI가 효과를 발휘하는 것은 파일 유출이나 잘못된 배치, 오프라인 반출, 다른 사용자로부터의 참조 쪽이며, 같은 사용자 권한으로 도는 공격 코드나 이미 침해된 단말에는 효과가 없다는 경계를 보여주는 그림.효과가 있는 쪽효과가 없는 쪽DPAPI의 수비 범위유출·잘못된 배치·다른 사용자동일 사용자 권한의 코드혼동하면 평가를 그르침

그림 4: 효과가 있는 쪽과 없는 쪽의 경계를 모르면, 쓰지 않는 과소평가이거나 지나치게 안심하는 과대평가가 됩니다.

3.3. 그래서 무엇이 좋은가

DPAPI의 이점을 한마디로 말하면

「비밀 그 자체를 설정 파일의 가독성에서 분리할 수 있다」

는 것입니다.

예를 들어 다음과 같은 사고에서는 평문과 DPAPI의 차이가 드러납니다.

  • 이용자가 설정 파일을 지원 부서로 보내 버렸다
  • 조사용 ZIP에 설정 파일이 들어갔다
  • 백업에서 설정 파일만 유출됐다
  • 공유 폴더에 복사됐다
  • 개발자가 암호문만 보고 내용을 읽을 수 없는 상태로 만들어 두었다

이것은 꽤 현실적인 이점입니다. 공격자를 영화 속 초인으로 만들지 않아도 일상적인 사고의 반경을 줄일 수 있습니다.

사고 반경이 작아지는 구조지원 부서로의 오송이나 조사용 ZIP, 백업 유출 같은 일상적인 사고로 파일이 밖으로 나가도 나가는 것은 암호문뿐이므로 비밀 유출로 이어지지 않고 사고 반경이 작아진다는 것을 보여주는 그림.오송·조사 ZIP·백업 유출밖으로 나가는 것은 암호문뿐그대로 비밀 유출로 이어지지 않음일상적인 사고 반경이 작아짐

그림 5: 파일이 밖으로 나가는 사고는 막을 수 없어도, 나가는 것을 암호문으로 바꿀 수 있습니다.

4. DPAPI가 딱 알맞은 이유

Windows에서 로컬 저장 비밀을 다룰 때 DPAPI가 실무에 딱 맞는 이유는 다음과 같습니다.

4.1. 키 관리를 OS에 맡길 수 있다

직접 AES 키를 생성하고 저장하고 권한을 부여하고 로테이션하고 유출 시 영향을 고려하고 변조 탐지까지 넣는다. 이것은 생각보다 무겁습니다. 게다가 대충 하면 대개 키를 같은 곳에 두는 것으로 끝나 버립니다.

DPAPI를 쓰면 「암호 키를 어떻게 만들고 어디에 둘 것인가」라는 문제를 앱 구현에서 떼어낼 수 있습니다.

그런 의미에서 DPAPI는 「암호화 알고리즘을 고르는 API」가 아니라 「키 관리를 OS에 위임하는 API」 로 보는 편이 본질에 가깝습니다.

DPAPI를 보는 본질적인 관점DPAPI는 암호화 알고리즘을 고르는 API가 아니라, 키를 어떻게 만들고 어디에 둘 것인가 하는 키 관리 문제를 OS에 위임하는 API로 보는 것이 본질에 가깝다는 것을 보여주는 그림.이 관점이 아님이 관점이 본질에 가까움알고리즘을 고르는 APIDPAPI키 관리를 OS에 위임하는 API키 생성·보관 문제를 구현에서 떼어낼 수 있음

그림 6: DPAPI를 키 관리의 위임처로 보면 앱에서 무엇을 떼어낼 수 있는지 알 수 있습니다.

4.2. 복호화 주체를 Windows 사용자 또는 컴퓨터에 묶을 수 있다

일반 데스크톱 앱이라면 CurrentUser를 고르면 되는 경우가 많습니다.

  • 그 사용자가 로그온해 있을 것
  • 그 사용자 맥락에서 처리가 돌아갈 것

을 전제로 복호화할 수 있습니다.

그 덕분에 암호문만 다른 PC로 복사해도 그대로는 쓰기 어렵다는 성질을 얻을 수 있습니다.

4.3. 변조 탐지까지 포함하기 쉽다

자체 암호화에서 흔히 있는 것은 “AES로 암호화했으니 끝”이라며 변조 탐지를 잊는 것입니다.

DPAPI는 암호화 데이터에 대한 무결성 보호도 갖고 있으므로 암호문이 함부로 수정됐을 때의 탐지까지 OS 쪽 구조에 태우기 쉽다는 실무상의 이점이 있습니다.

변조 탐지의 차이자체 암호화에서는 AES로 암호화했으니 끝이라며 변조 탐지를 잊기 쉽지만, DPAPI는 암호화 데이터에 무결성 보호를 갖추고 있어 암호문 수정 탐지까지 OS 쪽 구조에 태우기 쉽다는 것을 보여주는 그림.자체 암호화변조 탐지를 잊기 쉬움DPAPI무결성 보호를 갖춤수정 탐지까지 OS 쪽에 얹힘

그림 7: 암호화뿐 아니라 변조 탐지까지 OS 쪽 구조에 태울 수 있는 것이 DPAPI의 이점입니다.

4.4. C# / .NET에서 그대로 쓸 수 있다

C#이라면 System.Security.Cryptography.ProtectedData를 그대로 쓸 수 있습니다. 불필요한 라이브러리를 추가하지 않아도 되는 점도 Windows 전용 앱에서는 상당히 도움이 됩니다.

5. DPAPI로 지킬 수 있는 것과 지킬 수 없는 것

여기는 확실히 나눠 두는 편이 안전합니다.

5.1. 지키기 쉬워지는 것

DPAPI는 적어도 다음과 같은 상황에서는 유효합니다.

  • 설정 파일의 평문 유출
  • 다른 PC로의 파일 반출
  • 같은 PC의 다른 사용자로부터의 참조(CurrentUser 전제)
  • 백업이나 첨부 파일로서의 유출
  • 개발·유지보수 현장에서의 「무심코 읽혀 버리는」 상태

5.2. 지킬 수 없거나 지키는 힘이 약한 것

한편 다음과 같은 상황에서는 과신하지 않는 것이 좋습니다.

  • 같은 사용자 권한으로 실행되는 공격 코드
  • 단말 자체의 완전한 침해
  • 관리자 권한에서의 탈취
  • 앱이 복호화한 뒤 메모리상의 평문
  • 모든 클라이언트에 공통으로 배포되는 장기 비밀

마지막의 「전 클라이언트 공통 장기 비밀」은 특히 중요합니다.

예를 들어

  • 모든 고객에게 같은 API 키를 심는다
  • 모든 단말에서 같은 공유 패스워드를 갖는다
  • 클라이언트만으로 완결되는 고정 복호화 키를 배포한다

같은 설계는 어느 1대에서 빠져나가는 순간 전체로 번지기 쉽습니다. 이유는 단순합니다. 어느 1대에서든 앱이 복호화할 수 있다면, 그 비밀은 꺼낼 수 있기 때문입니다.

DPAPI는 「그 저장 위치를 평문보다 낫게 만드는」 데는 유효하지만 애초에 클라이언트에 두어서는 안 되는 비밀을 정당화하는 것은 아닙니다.

공통 장기 비밀이 번지는 구조모든 클라이언트에 공통 장기 비밀을 배포하면 어느 1대에서든 앱이 복호화할 수 있는 이상 그 비밀을 꺼낼 수 있으므로, 1대에서 빠져나간 시점에 전체로 번지기 쉽다는 것을 보여주는 그림.전 클라이언트 공통 장기 비밀어느 1대에서든 앱이 복호화 가능그 1대에서 비밀을 꺼낼 수 있음전체로 번짐DPAPI로 저장해도 근본 해결이 안 됨

그림 8: 공통 비밀은 1대의 함락이 전체로 번지므로, 저장 방법이 아니라 두는 곳부터 다시 검토해야 합니다.

이런 종류의 비밀은 저장 방법을 궁리하기보다 다음 방향으로 옮기는 것이 본질적인 해법입니다.

  • 서버 쪽에 둔다
  • 클라이언트는 토큰만 가진다
  • 사용자별 자격 증명으로 한다
  • 기한부 토큰으로 한다

6. CurrentUserLocalMachine의 구분

여기는 꽤 중요합니다. 대충 고르면 의미가 달라집니다.

6.1. 기본은 CurrentUser

일반 Windows 데스크톱 앱에서는 먼저 CurrentUser를 기본으로 생각합니다.

적합한 예:

  • WPF / WinForms / WinUI의 사용자용 데스크톱 앱
  • 사용자별로 설정이나 자격 증명을 갖는 앱
  • %LocalAppData%%AppData% 아래에 설정을 두는 앱

이 경우 「그 Windows 사용자의 비밀」로서 다루기 쉬워집니다.

6.2. LocalMachine은 용도가 꽤 제한적이다

LocalMachine은 편리해 보이지만 일반 데스크톱 앱에서는 너무 넓습니다.

적합한 것은 예를 들어 다음과 같은 경우입니다.

  • 신뢰된 단일 용도 머신 위의 Windows 서비스
  • 그 머신 위의 특정 프로세스에서만 쓰는 기밀
  • 로그온 사용자를 넘나들며 같은 단말에서 써야 하는 경우

다만 주의점은 무겁습니다.

  • 그 PC 위에서 도는 프로세스로부터 폭넓게 복호화 가능
  • 공용 단말, RDS, 배스천 단말, 여러 사용자가 있는 환경에서는 위험해지기 쉽다
  • 「일단 모두가 쓸 수 있어서 편하니까」로 고르면 대개 나중에 곤란해진다

그리고 LocalMachine을 고르고 싶어지는 동기는 대개 다음 3가지입니다.

  • 사용자를 전환해도 읽힌다
  • 서비스에서도 읽힌다
  • 돌아가기만 하면 편리하다

어느 쪽이든 「편함」이지 「지켜지고 있음」은 아닙니다. 일반 데스크톱 앱에서 LocalMachine을 고르면 그 PC 위의 다른 프로세스에도 복호화 가능성을 넓히는 셈이 되므로 의미가 상당히 달라집니다.

LocalMachine을 편해서 고르면 어떻게 되는가사용자를 넘나들며 읽을 수 있다, 서비스에서도 읽을 수 있다 같은 편함으로 LocalMachine을 고르면 그 PC 위의 다른 프로세스에도 복호화 가능성이 넓어져, 지켜지고 있는 것과는 다른 상태가 된다는 것을 보여주는 그림.사용자 넘나듦·서비스 대응으로 편함LocalMachine을 고름PC 위의 다른 프로세스에도 복호화 가능성이 넓어짐편함이지 지켜지고 있음은 아님

그림 9: 편하다는 이유로 LocalMachine을 고르면 복호화할 수 있는 범위가 그 PC 전체로 넓어져 버립니다.

6.3. 망설여지면 이렇게 생각한다

  • 일반 UI 앱 -> CurrentUser
  • 정말로 머신 단위로 지키고 싶은 특수 케이스 -> LocalMachine
  • 어떤 사용자라도 복호화할 수 있어야 하는데 단말에 다른 사용자도 있다 -> 대개 설계부터 다시 보는 편이 좋다

6.4. 서비스나 impersonation에서는 주의가 조금 더 필요하다

Windows 서비스나 impersonation이 얽히면 CurrentUser의 의미가 조금 무거워집니다.

  • 실행 계정이 누구인가
  • 그 계정의 프로필이 로드되어 있는가
  • 복호화 시점이 어느 맥락인가

이 부분이 어긋나면 「암호화는 됐는데 복호화가 안 된다」가 되기 쉽습니다. 서비스 용도는 「일단 CurrentUser」로 끝나지 않는 경우가 있습니다.

impersonation의 경우 Microsoft Learn에도 명시된 전형적인 실패가 「Key not valid for use in specified state.」입니다. DPAPI는 키 데이터를 사용자 프로필에 보관하므로 프로필이 로드되어 있지 않으면 복호화할 수 없습니다. impersonate하기 전에 대상 사용자의 프로필을 로드해 두어야 합니다.

impersonation에서의 전형적인 실패DPAPI는 키 데이터를 사용자 프로필에 보관하므로 프로필이 로드되지 않은 상태로 impersonate해서 복호화하면 전형적인 오류가 나며, 미리 대상 사용자의 프로필을 로드해 두어야 한다는 것을 보여주는 그림.로드하지 않고 impersonate복호화가 오류로 실패먼저 프로필을 로드impersonate 후에도 복호화 가능키는 프로필에 있음

그림 10: 키는 프로필 쪽에 있으므로 impersonate하기 전에 프로필 로드가 필요합니다.

6.5. 운영에서 「복호화할 수 없게 되는」 경우를 알아 둔다

암호화 그 자체보다 복호화할 수 없게 되는 사고 쪽이 실무에서는 더 아픕니다. DPAPI는 복호화할 수 있는 주체를 Windows 사용자 / 컴퓨터에 묶는 구조이므로, 그 연결이 끊기면 읽을 수 없게 됩니다.

미리 알아 두어야 할 것은 다음 5가지입니다.

경우 무슨 일이 일어나는가 대비하는 법
관리자에 의한 패스워드 재설정 사용자의 패스워드에 연결된 보호가 풀려 DPAPI로 보호한 데이터에 접근할 수 없게 되는 일이 있습니다. Microsoft의 지원 정보에도 관리자가 패스워드를 재설정한 뒤 DPAPI 데이터에 접근할 수 없게 되는 현상으로 기재되어 있습니다 비밀을 「다시 얻을 수 있는」 설계로 해 둔다. 복호화 실패 시 재입력으로 유도한다
프로필 재작성 새 프로필은 다른 키 자료를 가지므로 이전 암호문은 복호화할 수 없습니다 설정 파일의 버전을 두고, 복호화 실패를 비정상 종료로 처리하지 않는다
암호문만 다른 PC로 복사 복호화에 필요한 키 자료는 사용자 프로필 쪽에 있으므로 CurrentUser의 암호문만 가져가도 읽을 수 없습니다(이것은 3.1.에서 든 「강점」의 이면입니다) 단말마다 다시 저장한다는 전제로 설계한다
로밍 프로필 이쪽은 읽을 수 있습니다. 프로필과 함께 키 자료도 이동하므로 Microsoft Learn도 「로밍 프로필을 가진 사용자는 네트워크상의 다른 컴퓨터에서 복호화할 수 있다」고 명시하고 있습니다. 앞 행과 같이 취급하면 이관 절차에서 불필요하게 자격 증명을 다시 만들게 됩니다 「다른 PC니까 읽을 수 없다」고 단정하지 않는다. 로밍 여부를 확인한 뒤 이관 절차를 정한다
서비스의 실행 계정 변경 보호했을 때와 복호화할 때 실행 계정이 바뀌면 CurrentUser로는 읽을 수 없습니다 계정 변경 시 다시 보호하는 절차를 운영에 넣는다

요컨대 ProtectedData.Unprotect는 실패할 수 있다는 전제로 코드를 작성합니다. 복호화에 실패하면 CryptographicException이 발생하므로 그것을 잡아서 재입력으로 유도합니다.

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

// protectedBase64: 설정 파일에서 읽은 암호문 (Base64)
// entropy: 보호할 때와 같은 값을 전달. null도 가능
static bool TryUnprotect(string protectedBase64, byte[]? entropy, out string plaintext)
{
    plaintext = string.Empty;

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

        plaintext = Encoding.UTF8.GetString(plainBytes);
        return true;
    }
    catch (CryptographicException)
    {
        // 복호화할 수 없다 = 환경이 바뀌었을 가능성이 높다.
        // 여기서 죽이지 말고, 호출 쪽에서 재입력 흐름으로 유도한다
        return false;
    }
    catch (FormatException)
    {
        // Base64로서 손상된 경우
        return false;
    }
}

「암호화했는데 복호화가 안 된다」는 문의가 들어오는 것은 대개 이 표 중 하나입니다.

복호화 실패를 전제로 한 코드의 흐름ProtectedData.Unprotect는 환경 변화로 실패할 수 있다는 전제로 코드를 작성하고, CryptographicException을 잡아서 비정상 종료시키지 않고 재입력 흐름으로 유도한다는 흐름을 보여주는 그림.성공예외로 실패Unprotect를 시도비밀을 사용예외를 잡아서 죽이지 않음재입력 흐름으로 유도연결이 끊기면 실패할 수 있음

그림 11: 복호화는 실패할 수 있다는 전제로 작성하고, 실패를 비정상 종료가 아니라 재입력으로 연결합니다.

7. 구현의 최소한의 지침

Windows 앱에서 「설정 파일의 평문을 그만둔다」 정도라면 설계를 그렇게까지 복잡하게 할 필요는 없습니다. 다만 놓치고 싶지 않은 포인트가 몇 가지 있습니다.

7.1. 비밀만 보호한다

설정 전체를 통째로 암호화하는 것보다 먼저 비밀 항목만을 보호하는 편이 다루기 쉽습니다.

예를 들어 다음처럼 나눕니다.

  • 서버 URL
  • 사용자명
  • DB명
  • 기능 플래그

는 평문 그대로 두어도 되는 경우가 많습니다.

한편

  • 패스워드
  • API 토큰
  • 리프레시 토큰
  • 공유 폴더 자격 증명

은 보호 대상입니다.

이렇게 나누면

  • 설정 편집이 쉽다
  • 차이 확인이 쉽다
  • 어디가 비밀인지 명확하다
  • 전체 운영이 단순하다

라는 효과를 얻습니다.

비밀 항목만 보호하는 구분법설정 전체를 통째로 암호화하는 것이 아니라 URL이나 사용자명 등은 평문 그대로 두고, 패스워드나 토큰 등 비밀 항목만 보호하면 편집하기 쉽고 어디가 비밀인지도 명확해진다는 것을 보여주는 그림.평문 그대로보호함설정 파일URL·사용자명·플래그 등패스워드·토큰 등어디가 비밀인지 명확하고 운영도 단순

그림 12: 통째로 암호화하는 대신 비밀 항목만 보호하면 다루기 쉬움과 안전을 함께 얻을 수 있습니다.

7.2. 저장 위치는 per-user를 기본으로 한다

일반 데스크톱 앱이라면 저장 위치는 per-user 위치를 기본으로 합니다.

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

적어도 설치 폴더 아래나 공유되기 쉬운 위치에 대충 두지 않는 편이 좋습니다.

DPAPI로 보호하고 있어도 저장 위치의 ACL이 허술하면 「암호문은 읽힌다」「설정 구조는 보인다」「운영 실수는 일어난다」 라는 이야기가 됩니다. 방어는 한 겹이 아니라 여러 겹으로 쌓는 편이 효과적입니다.

7.3. optionalEntropy는 만능의 제2 키가 아니다

ProtectedData에는 optionalEntropy를 넘길 수 있습니다. 편리하지만 “이것을 바이너리에 심으면 안전해지는 마법의 제2 키”는 아닙니다.

  • 같은 파일에 두면 비밀이 되지 않습니다
  • 바이너리에 고정값으로 심어도 강한 비밀이라고 할 수 없습니다
  • 그래도 용도 식별이나 오용 방지에는 도움이 됩니다

실무에서는

  • 앱 이름
  • 용도 이름
  • 버전 식별자

를 고정 바이트열로 전달해서 「다른 용도의 암호문을 실수로 받아들이지 않도록」하는 데 쓰는 정도가 딱 알맞습니다.

optionalEntropy의 올바른 위치optionalEntropy는 바이너리에 심으면 안전해지는 마법의 제2 키가 아니라, 앱 이름이나 용도 이름을 고정 바이트열로 전달해 다른 용도의 암호문을 실수로 받아들이지 않기 위한 용도 식별에 쓰는 것이 딱 알맞다는 것을 보여주는 그림.이런 기대는 하지 않음이 쓰임이 딱 알맞음optionalEntropy마법의 제2 키앱 이름·용도 이름의 식별자다른 용도의 암호문을 실수로 받아들이지 않음

그림 13: entropy는 비밀 키가 아니라 용도 식별과 오용 방지를 위한 태그로 사용합니다.

7.4. 암호문을 Git에 넣어도 된다, 는 아니다

여기도 은근히 중요합니다.

DPAPI의 암호문은 평문보다 훨씬 낫지만 그렇다고 설정 파일째로 리포지토리에 넣어도 된다는 것은 아닙니다.

이유는 단순합니다.

  • 암호문은 오래 남는다
  • 언젠가 같은 단말이나 같은 맥락이 재현될지도 모른다
  • 파일에는 비밀 이외의 정보도 들어 있다
  • 「보호되어 있으니 대충 다뤄도 된다」는 문화가 생긴다

는 것입니다.

「평문보다 낫다」「어디에 두어도 안전하다」 는 완전히 다른 이야기입니다.

암호문이라도 Git에 넣지 않는 이유DPAPI의 암호문은 평문보다 낫지만, 리포지토리에 넣으면 오래 남고 비밀 이외의 정보도 포함되며 보호되고 있으니 대충 다뤄도 된다는 문화가 생기므로, 어디에 두어도 안전한 것과는 별개의 이야기라는 것을 보여주는 그림.평문보다 나음그래도DPAPI의 암호문사고에 강해짐리포지토리에는 넣지 않음오래 남음·다른 정보도 포함·문화가 느슨해짐

그림 14: 「평문보다 낫다」는 것이 두는 곳을 대충 해도 되는 이유는 아닙니다.

7.5. 로그에 출력하지 않는다

의외로 흔한 것이 복호화한 뒤 로그에 출력해서 전부 헛수고가 되는 패턴입니다.

  • 접속 실패 시 접속 문자열을 통째로 출력한다
  • API 401 시 Authorization 헤더를 남긴다
  • 예외 메시지에 비밀을 섞는다

이런 것을 하면 설정 파일의 평문을 그만둬도 결국 로그가 평문 창고가 됩니다. 슬프지만 상당히 현실적인 실무입니다.

8. C# / .NET 최소 구현 예

8.1. 먼저 참조 추가가 필요합니다

ProtectedData는 BCL에 들어 있는 것처럼 보이지만 대상 프레임워크에 따라 「어디서 오는가」가 다릅니다. 여기서 걸려 넘어지면 ProtectedData라는 타입 이름을 해석할 수 없습니다.

대상 필요한 작업 제공처
.NET Framework 프로젝트에 System.Security 어셈블리 참조를 추가한다 System.Security.dll
.NET Core / .NET 5 이후(.NET 6 / 8 포함) NuGet의 System.Security.Cryptography.ProtectedData 패키지를 추가한다 System.Security.Cryptography.ProtectedData.dll

이 패키지는 .NET Core / .NET 5 이후의 어떤 공유 프레임워크에도 포함되어 있지 않습니다. net8.0-windows 같은 Windows용 대상이라도 명시적으로 참조가 필요합니다.

dotnet add package System.Security.Cryptography.ProtectedData

한 가지 더, 구현보다 먼저 알아 두고 싶은 것이 있습니다. ProtectedData는 Windows 전용입니다. DPAPI에 의존하기 때문에 Windows 이외의 플랫폼에서 .NET으로 호출하면 PlatformNotSupportedException이 발생합니다. 크로스 플랫폼을 전제로 한 코드베이스라면 10.1.과 같이 처음부터 다른 설계로 갑니다.

ProtectedData를 쓰기 전 확인ProtectedData는 대상에 맞는 참조 추가가 필요하며 추가하지 않으면 타입 이름을 해석할 수 없고, 또 Windows 전용이므로 Windows 이외에서 호출하면 예외가 된다는, 구현 전에 확인해야 할 2가지를 보여주는 그림.추가하지 않으면Windows 이외에서 호출하면ProtectedData를 쓰고 싶음대상에 맞는 참조를 추가타입 이름을 해석할 수 없음Windows 전용이라고 이해해 둠실행 시 예외가 됨

그림 15: 구현 전에 참조 추가와 Windows 전용이라는 2가지 전제를 짚어 둡니다.

8.2. 최소 구현

다음은 설정 파일에 저장할 문자열을 CurrentUser로 보호하는 최소 예제입니다. 용도 식별을 위해 고정 optionalEntropy를 넣고 있지만, 이것을 비밀 키라고 생각하지 마세요.

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

public static class DpapiSecretProtector
{
    // 용도 식별용. 제2의 비밀 키가 아님.
    private static readonly byte[] Entropy =
        Encoding.UTF8.GetBytes("ComComponent:DesktopApp:SettingsSecret:v1");

    public static string ProtectToBase64(string plaintext)
    {
        ArgumentNullException.ThrowIfNull(plaintext);

        byte[] plainBytes = Encoding.UTF8.GetBytes(plaintext);
        byte[] protectedBytes = Array.Empty<byte>();

        try
        {
            protectedBytes = ProtectedData.Protect(
                plainBytes,
                optionalEntropy: Entropy,
                scope: DataProtectionScope.CurrentUser);

            return Convert.ToBase64String(protectedBytes);
        }
        finally
        {
            Array.Clear(plainBytes, 0, plainBytes.Length);

            if (protectedBytes.Length > 0)
            {
                Array.Clear(protectedBytes, 0, protectedBytes.Length);
            }
        }
    }

    public static string UnprotectFromBase64(string protectedBase64)
    {
        ArgumentNullException.ThrowIfNull(protectedBase64);

        byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
        byte[] plainBytes = Array.Empty<byte>();

        try
        {
            plainBytes = ProtectedData.Unprotect(
                protectedBytes,
                optionalEntropy: Entropy,
                scope: DataProtectionScope.CurrentUser);

            return Encoding.UTF8.GetString(plainBytes);
        }
        finally
        {
            Array.Clear(protectedBytes, 0, protectedBytes.Length);

            if (plainBytes.Length > 0)
            {
                Array.Clear(plainBytes, 0, plainBytes.Length);
            }
        }
    }
}

사용법은 단순합니다.

string protectedPassword = DpapiSecretProtector.ProtectToBase64(password);

// JSON 등에 저장
// settings.DbPasswordProtected = protectedPassword;

string password = DpapiSecretProtector.UnprotectFromBase64(settings.DbPasswordProtected);

설정 파일은 예를 들어 이런 형태로 만들 수 있습니다.

{
  "ApiBaseUrl": "https://api.example.com/",
  "UserName": "app-user",
  "PasswordProtected": "AQAAANCMnd8BFdERjHoAwE..."
}

이 형태의 좋은 점은

  • URL이나 사용자명은 평범하게 편집할 수 있다
  • 패스워드만 보호할 수 있다
  • 설정 구조가 보기 쉽다
  • 평문 그대로 두는 것보다 사고가 덜 난다

는 것입니다.

9. 그래도 위험한 설계

DPAPI를 쓰고 있어도 다음 설계는 여전히 위험합니다.

9.1. 복호화한 값을 오래 갖고 다닌다

복호화한 값을

  • 로그에 출력한다
  • 화면에 표시한다
  • 예외에 포함시킨다
  • 장수명 객체에 계속 실어 둔다

는 것은 피하고 싶습니다.

「저장 시에는 암호화」「사용 중에도 안전」 은 별개의 문제입니다.

9.2. 전체 설치에 공통 비밀을 갖게 한다

전 사용자에게 같은 API 키를 갖게 하는 설계는 DPAPI로 저장해도 근본 해결이 되지 않습니다. 이유와 대신 어디로 옮길지는 5.2.에 정리되어 있습니다.

9.3. LocalMachine을 「편해서」 고른다

이것도 정말 흔합니다. 다만 「편함」이지 「지켜지고 있음」은 아닙니다. 고르고 싶어지는 동기와 그때 무엇이 넓어지는지는 6.2.와 같습니다. 판단이 망설여지면 6.3.의 3줄을 보십시오.

9.4. 자체 암호화를 더해서 안심한다

DPAPI 대신

  • AES 키를 소스 코드에 심는다
  • AES 키를 설정 파일의 다른 항목에 둔다
  • 「살짝 난독화한 문자열」을 키로 취급한다

같은 구현을 넣는 것은 대개 효과가 미미합니다.

“평문이 아니다”와 “안전하다” 사이에는 상당히 큰 도랑이 있습니다.

자체 암호화로 안심하는 위험함AES 키를 소스 코드나 설정 파일의 다른 항목에 두거나 난독화한 문자열을 키로 취급하는 자체 암호화는 효과가 미미하며, 평문이 아닌 것과 안전한 것 사이에는 큰 도랑이 있다는 것을 보여주는 그림.사이에 큰 도랑키를 코드나 설정에 심는 자체 암호화평문이 아닌 상태안전한 상태효과는 대개 미미함

그림 16: 키를 같은 곳에 두는 자체 암호화는 「평문이 아니다」에 그칠 뿐 「안전」에는 이르지 못합니다.

10. DPAPI로 충분하지 않은 경우

DPAPI는 편리하지만 만능은 아닙니다. 다음과 같은 경우에는 다른 선택지를 생각하는 편이 좋습니다.

10.1. Windows 이외에서도 돌리고 싶다

DPAPI / ProtectedData는 Windows 대상입니다. 크로스 플랫폼 앱이라면 그 전제로는 구성할 수 없습니다.

10.2. 여러 머신·여러 사용자에서 같은 비밀을 다루고 싶다

같은 암호문을 여러 PC에서 복호화하고 싶다, 여러 사용자가 함께 쓰고 싶다는 요건은 「그 단말·그 사용자에 묶는」 DPAPI의 특기 분야에서 벗어납니다.

이 경우에는

  • 서버 쪽의 비밀 관리
  • 자격 증명 기반
  • Windows 인증 / 통합 인증
  • 앱용 자격 증명 저장소

등 요건에 맞는 다른 설계를 생각해야 합니다.

10.3. 저장 대상이 사용자 자격 증명 그 자체인 경우

저장하고 싶은 것이 명확히

  • 사용자명
  • 패스워드

조합이라면, DPAPI로 자신의 파일에 쓰는 것보다 Windows가 제공하는 자격 증명 저장소 쪽이 더 자연스럽습니다. 여기는 실무에서 자주 망설이는 부분이므로 비교표를 둡니다.

관점 DPAPI (ProtectedData) Credential Locker (PasswordVault) Credential Manager (CredWrite / CredRead)
저장 위치 직접 정한 파일(암호문을 어떻게 둘지는 앱 나름) Windows가 관리하는 자격 증명 저장소 Windows가 관리하는 자격 증명 저장소
저장할 수 있는 것 임의의 바이트열(접속 문자열, 토큰, 설정의 일부라도 무방) 사용자명 + 패스워드의 조합 자격 증명(종류별 구조체)
API System.Security.Cryptography WinRT의 Windows.Security.Credentials Win32 (wincred.h / Advapi32.dll)
데스크톱 앱에서 쓸 수 있는가 그대로 쓸 수 있음 WinUI뿐 아니라 WPF / WinForms에서도 쓸 수 있음(WinRT API 호출 설정은 필요) 그대로 쓸 수 있음
동기화 없음 Microsoft 계정으로 단말 간 로밍됨 없음(로컬 사용자 자격 증명 세트)
제한 실질적으로 없음 앱당 20건까지. 대용량 데이터용은 아님 현재 토큰의 로그온 세션에 종속
관리 주체 앱(저장 위치도 ACL도 직접 정함) OS(저장 위치를 설계하지 않아도 됨) OS(제어판의 자격 증명 관리자에서 관리 가능)

구분해서 쓰는 기준은 다음과 같습니다.

  • 저장 대상이 사용자명 + 패스워드의 조합이고 건수도 적다 -> Credential Locker / Credential Manager가 제1 후보입니다. 저장 위치 설계도 ACL도 직접 갖지 않아도 됩니다
  • 저장 대상이 「사용자명 + 패스워드」의 형태가 아니다 -> 접속 문자열, API 토큰, 리프레시 토큰, 설정 파일의 일부 같은 것은 DPAPI 쪽이 자연스럽습니다. 이 글이 다루는 것은 이쪽입니다
  • 단말 간 인계가 필요하다 -> Credential Locker의 로밍이 효과를 발휘합니다. DPAPI의 CurrentUser는 반대로 「인계되지 않는다」는 것이 장점이므로 여기서는 목적이 정반대입니다
  • 건수가 많다 / 크기가 크다 -> Credential Locker의 20건 제한에 걸립니다. DPAPI로 자체 파일에 맡깁니다

또한 자격 증명 저장소도 내부적으로는 OS의 보호 기구를 타고 있으므로 「DPAPI보다 안전하다」「DPAPI가 뒤떨어진다」는 서열이 아닙니다. 저장하고 싶은 것의 형태와 로밍 필요 여부로 고르는 것이 실무적입니다.

그리고 어느 쪽을 고르든 신규 앱이라면 Windows Hello / 패스키 같은 패스워드리스를 먼저 검토할 가치가 있습니다. 애초에 장기 패스워드를 갖지 않아도 된다면 그것이 가장 강력합니다.

이 글의 중심은 어디까지나 「Windows 클라이언트에서 설정 파일 평문을 그만두기」위한 DPAPI의 실무 라인입니다.

11. 실무에서의 추천 우선순위

마지막으로 실무에서 망설여지면 이 순서로 생각하면 정리하기 쉽습니다. 위에서부터 순서대로 검토하고, 조건을 만족하지 않을 때만 아래로 내려갑니다.

된다안 된다나눌 수 없다나눌 수 있다그 조합이고 건수도 적다필요하다그렇다도메인 / 로컬 계정필요 없다토큰 등 다른 형태필요하다필요 없다하나면 된다(이용자 본인,또는 전용 서비스 계정)여러 계정에서복호화해야 한다단언할 수 있다단언할 수 없다장기 비밀을 단말에두지 않아도 되는가우선 1: 갖지 않는다(Windows 인증·단명 토큰)비밀을 이용자별로나눌 수 있는가공통 키가 되어 있지 않은지설계부터 다시 본다저장하고 싶은 것의 형태는사용자명 + 패스워드의 조합인가(10.3절)단말 간 인계가 필요한가Microsoft 계정으로동기화되는 단말인가(10.3절)Credential Locker의로밍을 사용한다DPAPI로는 인계할 수 없음.서버 쪽에서 관리한다(10.2절)Credential Locker /Credential Manager단말 간 인계가 필요한가같은 비밀을 복호화하는계정은 몇 개인가우선 3: DPAPI + CurrentUser무인 실행이라면 프로필로드를 확인한다(6.4절)다른 이용자는 들어오지 않는다고단언할 수 있는가우선 4: DPAPI + LocalMachine예외 취급으로 근거를 남긴다다른 사용자도 복호화할 수 있게 됨.인증 방식 쪽을 다시 본다

그림 17: 저장 방식의 선정 순서입니다. 비밀의 형태와 로밍 필요 여부를 먼저 본 다음 DPAPI로 들어갑니다. LocalMachine은 「편해서」가 아니라 다른 선택지가 성립하지 않을 때의 예외로 고릅니다.

우선 1: 애초에 갖지 않는다

  • Windows 인증
  • 통합 인증
  • 대화형 로그인
  • 서버 쪽에서 비밀 보유
  • 단명 토큰

우선 2: 사용자별 비밀에 맞춘다

  • 공통 비밀보다 per-user
  • 장기 고정 자격 증명보다 갱신 가능한 토큰
  • 전 클라이언트 공통 키를 피한다

우선 3: 로컬 저장이 필요하면 DPAPI

  • 기본은 CurrentUser
  • 저장 위치는 per-user
  • 비밀 항목만 보호
  • 로그에 출력하지 않는다

우선 4: LocalMachine은 예외 취급

  • 정말로 머신 단위여야 하는가
  • 그 단말에 다른 사용자가 들어오지 않는가
  • 서비스 설계로서 타당한가

12. 정리

Windows 앱에서 설정 파일에 기밀 정보를 저장해야 할 때 평문 그대로 두는 것은 피하고 싶습니다.

그리고

「어차피 키는 어딘가에 저장되는 거니까 같은 것 아닌가?」

라는 의문에는 이렇게 답하는 것이 실무적입니다.

  • 자체 암호화로 키를 같은 곳에 둔다면 꽤 같습니다
  • DPAPI는 같지 않습니다
    • 키 관리를 OS에 맡길 수 있습니다
    • 복호화 주체를 Windows 사용자 / 컴퓨터에 묶을 수 있습니다
    • 파일 단체의 유출을 그대로 비밀 유출로 만들지 않아도 됩니다
  • 다만
    • 같은 사용자 권한으로 도는 코드
    • 완전히 침해된 단말
    • 클라이언트에 두어서는 안 되는 장기 공통 비밀

까지는 해결하지 못합니다

요컨대 DPAPI는 만능의 성벽이 아닙니다. 그러나 설정 파일 평문이라는 훤히 뚫린 유리창을, 최소한 제대로 된 창으로 바꾸는 정도의 효과는 있습니다.

Windows 클라이언트의 실무에서는 이 차이가 상당히 큽니다. 먼저 여기를 놓치지 않는 것부터 시작하는 것이 가장 현실적입니다.

DPAPI의 현실적인 자리매김DPAPI는 만능의 성벽은 아니지만 설정 파일 평문이라는 훤히 뚫린 유리창을 최소한 제대로 된 창으로 바꾸는 효과가 있으며, 먼저 거기서부터 시작하는 것이 현실적이라는 것을 보여주는 그림.DPAPI로 바꿈평문 설정 = 훤히 뚫린 유리창최소한 제대로 된 창만능의 성벽은 아님먼저 여기서부터 시작하는 것이 현실적

그림 18: DPAPI는 성벽이 아니라 창문 교체이지만, 실무에서는 그 차이가 가장 크게 작용합니다.

13. 참고 자료

  • 이전 글: /ko/blog/2026/03/14/001-windows-app-security-minimum-checklist/
  • Microsoft Learn: CryptProtectData https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata
  • Microsoft Learn: ProtectedData https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata?view=windowsdesktop-10.0
  • Microsoft Learn: DataProtectionScope https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.dataprotectionscope?view=windowsdesktop-10.0
  • Microsoft Learn: How to: Use Data Protection https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection
  • Microsoft Learn: Credential locker for Windows apps https://learn.microsoft.com/en-us/windows/apps/develop/security/credential-locker
  • Microsoft Learn: CredWrite(Windows Credential Manager의 Win32 API) https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credwritew
  • NuGet: System.Security.Cryptography.ProtectedData https://www.nuget.org/packages/System.Security.Cryptography.ProtectedData
  • Microsoft 지원: 관리자가 패스워드를 재설정한 뒤 DPAPI 데이터에 접근할 수 없게 되는 현상 https://support.microsoft.com/en-us/topic/you-cannot-access-dpapi-data-after-an-administrator-resets-your-password-on-a-windows-server-2012-based-domain-controller-4aa890cd-12b5-fe5c-9e68-06244e70673d

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

DPAPI란 무엇입니까?
DPAPI(Data Protection API)는 Windows가 제공하는 데이터 보호 메커니즘으로, 암호 키 관리를 OS에 위임하고 복호화할 수 있는 주체를 그 Windows 사용자 또는 그 컴퓨터에 묶습니다. C# / .NET에서는 System.Security.Cryptography.ProtectedData 클래스를 통해 별도 라이브러리를 추가하지 않고 쓸 수 있습니다. 암호화 알고리즘을 고르는 API라기보다는 키 관리를 OS에 위임하는 API로 보는 편이 본질에 가깝습니다. 설정 파일에 저장하는 패스워드나 API 토큰을 평문 그대로 두지 않기 위한 현실적인 선택지입니다.
키는 어딘가에 저장되는 거니까 평문이든 DPAPI든 같은 것 아닌가요?
같지 않습니다. 자체 AES 암호화로 키를 같은 앱이나 설정 파일에 두는 경우는 꽤 평문에 가깝지만, DPAPI는 키 관리를 OS에 맡기고 복호화할 수 있는 주체를 Windows 사용자 또는 컴퓨터에 묶습니다. 그 결과 설정 파일 단체의 유출, 다른 PC로의 반출, 오송, 백업 유출, 리포지토리 혼입 같은 사고에 대한 내성이 크게 달라집니다. DPAPI는 「파일을 읽을 수 있다」는 것과 「비밀을 쓸 수 있다」는 것을 분리할 수 있다는 점이 평문과의 결정적인 차이입니다.
DPAPI로 지킬 수 없는 것은 무엇인가요?
같은 사용자 권한으로 실행되는 코드는 그 사용자가 복호화할 수 있는 것을 기본적으로 복호화할 수 있습니다. 그래서 단말이 악성코드에 침해된 상황이나 관리자 권한에서 탈취된 상황, 복호화 후 메모리상의 평문까지는 지키지 못합니다. 또 모든 클라이언트에 공통으로 배포하는 장기 비밀은 어느 1대에서 빠지는 순간 전체로 번지기 쉬우므로, DPAPI로 저장해도 근본 해결이 되지 않습니다. DPAPI가 효과를 발휘하는 것은 주로 파일 유출·잘못된 배치·오프라인 반출·다른 사용자로부터의 참조 쪽입니다.
DataProtectionScope의 CurrentUser와 LocalMachine 중 어느 것을 써야 합니까?
일반 Windows 데스크톱 앱에서는 CurrentUser가 기본입니다. 그 사용자의 비밀로 다룰 수 있고, 암호문만 다른 PC로 복사해도 그대로는 쓰기 어려운 성질을 얻습니다. LocalMachine은 그 PC 위에서 도는 프로세스로부터 폭넓게 복호화할 수 있으므로 공용 단말이나 여러 사용자가 있는 환경에서는 위험해지기 쉽고, 신뢰된 단일 용도 머신 위의 Windows 서비스 등 용도가 꽤 제한적입니다. 「모두가 쓸 수 있어서 편하니까」로 LocalMachine을 고르면 대개 나중에 곤란해집니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기