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

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

수정 이력(2건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 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/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에 설정 파일이 통째로 들어간다
  • 지원 문의에 설정 파일을 첨부해 달라고 한다
  • 백업이나 파일 공유로 제3자가 읽을 수 있다
  • 로그에 연결 문자열이나 토큰이 그대로 나온다
  • 퇴직자나 다른 사용자가 같은 단말 위의 파일을 읽을 수 있다

평문은 제3자에게 읽힌 시점에 비밀성이 사라집니다.

  • 파일을 열면 끝
  • 복사되면 끝
  • 메일 첨부가 되면 끝
  • 리포지토리에 남으면 반영구적으로 뒤를 봐야 한다

공격자가 정교할 필요조차 없습니다.
텍스트 에디터로 열 수 있다는 것만으로도 꽤 약합니다.

평문은 읽힌 시점에 비밀성이 사라진다Git에 들어가거나 조사용 ZIP, 백업 유출, 첨부 같은 지저분한 일상 경로로 설정 파일이 제3자에게 읽히면, 평문은 비밀성이 사라진다는 것을 나타내는 그림.Git에 들어감·조사용 ZIP·첨부·백업설정 파일이 읽힌다제3자에게 읽힌 시점에 비밀성이 사라진다공격자가 정교할 필요조차 없다

그림 2: 평문의 약함은, 일상적인 사고 경로로 제3자에게 읽힌 시점에 비밀성이 사라진다는 점입니다.

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을 고르고 싶어지는 동기는, 대개 이 셋입니다.

  • 사용자를 바꿔도 읽을 수 있다
  • 서비스에서도 읽을 수 있다
  • 돌아가면 편하다

어느 것도 「편함」이지, 「지키고 있음」이 아닙니다.
보통의 데스크톱 앱에서 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 토큰
  • 리프레시 토큰
  • 공유 폴더 자격 증명

은 보호 대상입니다.

이렇게 나누면,

  • 설정 편집이 쉽다
  • 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를 넘길 수 있습니다.
편리하지만, “이것을 바이너리에 심으면 안전해지는 마법의 두 번째 키”가 아닙니다.

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

실무에서는,

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

를 고정 바이트열로 넘겨, 「다른 용도의 암호문을 잘못 받지 않게」 쓰기 정도가 알맞습니다.

optionalEntropy의 올바른 자리매김optionalEntropy는 바이너리에 심으면 안전해지는 마법의 두 번째 키가 아니라, 앱 이름이나 용도 이름을 고정 바이트열로 넘겨 다른 용도의 암호문을 잘못 받지 않기 위한 용도 식별에 쓰는 것이 알맞다는 것을 나타내는 그림.이 기대는 하지 않는다이 쓰임이 알맞다optionalEntropy마법의 두 번째 키앱 이름·용도 이름의 식별자다른 용도의 암호문을 잘못 받지 않는다

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

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

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

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

이유는 단순합니다.

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

때문입니다.

「평문보다 낫다」
「어디에 둬도 안전하다」
는 전혀 다릅니다.

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

그림 14: 「평문보다 낫다」는 두는 장소를 허술하게 해도 되는 이유가 되지 않습니다.

7.5. 로그에 내지 않는다

의외로 흔한 것이, 복호화한 뒤에 로그로 내서 전부 망치는 패턴입니다.

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

이것을 하면, 설정 파일의 평문을 그만둬도, 결국 로그가 평문 창고가 됩니다.
슬프지만, 꽤 실무입니다.

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

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

ProtectedData는 BCL에 들어 있는 것처럼 보이지만, 대상 프레임워크에 따라 「어디서 오는지」가 다릅니다. 여기서 막히면, ProtectedData라는 형식 이름을 해석하지 못합니다.

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

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

dotnet add package System.Security.Cryptography.ProtectedData

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

ProtectedData를 쓰기 전의 확인ProtectedData는 대상에 따른 참조 추가가 필요하고, 추가하지 않으면 형식 이름이 해석되지 않으며, 또한 Windows 전용이므로 Windows 이외에서 호출하면 예외가 난다는, 구현 전에 확인할 2가지를 나타내는 그림.추가하지 않으면Windows 이외에서 호출하면ProtectedData를 쓰고 싶다대상에 따른 참조를 추가한다형식 이름이 해석되지 않는다Windows 전용임을 알아 둔다런타임 예외가 난다

그림 15: 구현 전에, 참조 추가와 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. 여러 머신·여러 사용자에서 같은 비밀을 다루고 싶다

같은 암호문을 여러 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. 참고 자료

  • 앞선 글: https://comcomponent.com/ko/blog/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 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기