← 기술 백서 목록으로 돌아가기

하드코딩 키 유출과 평문 탈취 0초 차단: ZeliDesk SecretBox의 Windows DPAPI 커널 암호화 및 무손실 왕복 보존(Round-trip) 아키텍처

"데스크톱 소프트웨어에서 API 키, 데이터베이스 접속 비밀번호, 교원 계정 토큰을 로컬 JSON이나 XML 설정 파일에 평문으로 저장하는 것은 교육망과 국방망에서 가장 빈번히 지적되는 취약점입니다. 반대로 별도의 마스터 비밀번호를 요구하면 사용자가 암호를 분실했을 때 전체 작업 데이터가 잠겨버리는 치명적인 사용성 문제가 발생합니다. ZeliDesk SecretBox는 Windows 운영체제 커널의 DPAPI(Data Protection API)와 전용 엔트로피 솔트(Salt)를 결합하여 사용자 계정 바운더리에 완벽히 격리된 온디바이스 암호화 저장소를 구축했습니다. 특히 일시적인 마스터키 손상이나 계정 환경 전이 시에도 원본 암호문을 안전하게 보존(Round-trip)하여, 재저장 시 비밀번호나 API 키가 영구 파기되는 비극을 원천 차단하는 무결점 결함 허용(Fault-Tolerant) 아키텍처를 완성했습니다."

1. 데스크톱 자격증명의 딜레마: 평문 노출의 위험 vs 마스터 비밀번호의 피로

학교 컴퓨터실, 교육청 교원 연구실, 그리고 군부대 전산실에 설치되는 전문 소프트웨어는 다양한 외부 서비스와 연동됩니다. 인공지능 LLM API 키(OpenAI, Anthropic 등), 온프레미스 데이터베이스 접속 계정, Git 저장소 토큰 등이 대표적입니다.

이러한 민감 정보를 데스크톱 앱에서 영속화할 때 소프트웨어 엔지니어는 극단적인 양자택일에 직면해 왔습니다:

  1. 평문(Plaintext) 저장의 치명성: appsettings.json이나 config.xml에 평문으로 저장할 경우, 악성코드나 화면 공유, USB 복사, 학생들의 파일 탐색기 열람만으로 최고 관리자 권한의 키가 그대로 유출됩니다.
  2. 자체 대칭키(AES) 하드코딩의 허구성: 소스코드에 암호화 키를 문자열로 박아두는 것은 dnSpy나 ILSpy 같은 디컴파일러로 1초 만에 복호화 키가 털리므로 '눈 가리고 아웅'에 불과합니다.
  3. 마스터 비밀번호 도입 시의 사용성 재앙: 앱을 켤 때마다 마스터 비밀번호 입력을 강제하면, 컴퓨터실 수업이나 작전 현장에서 암호를 잊어버려 도구 자체를 쓰지 못하는 심각한 관리 마비가 발생합니다.

ZeliDesk의 SecretBox는 Windows 커널이 제공하는 DPAPI (Data Protection API)를 채택하여, 사용자에게 단 한 번의 비밀번호 입력도 요구하지 않으면서 오직 "현재 Windows에 로그인한 정당한 사용자 계정 세션"에서만 복호화가 가능한 무침투 보안 파이프라인을 구축했습니다.

비교 항목 일반 설정 파일 저장 방식 하드코딩 AES 암호화 ZeliDesk SecretBox (DPAPI + Entropy)
저장 형태 순수 평문 문자열 고정 키로 암호화된 Base64 dpapi:{Base64} 커널 암호화 문자열
디컴파일 역공학 공격 불필요 (그대로 노출) 1초 만에 키 추출 가능 키 추출 불가능 (OS 커널 보안 서브시스템 보호)
타 PC 복사 공격 파일 복사 즉시 탈취 성공 키가 같아 복호화 성공 타 PC 복사 시 복호화 원천 실패 (암호학적 거부)
사용자 개입 없음 없음 없음 (Zero-Touch 투명 암복호화)
결함 허용 복원력 없음 키 손상 시 데이터 파기 무손실 왕복 보존(Round-trip)으로 데이터 파기 방지

2. 핵심 아키텍처 1: Windows DPAPI 커널 격리와 CurrentUser 바운더리

Windows DPAPI는 Windows 2000부터 검증된 운영체제 수준의 암호화 서비스입니다. 사용자의 Windows 로그인 자격증명(비밀번호 해시)과 LSA(Local Security Authority)가 생성한 머신 마스터 키를 기반으로 동적 대칭키를 파생하여 데이터를 암호화합니다.

C# (ZeliDesk.Services.SecretBox 암호화 코어)
public static class SecretBox
{
    private const string Prefix = "dpapi:";
    private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("ZeliDesk.SecretBox.v1");

    /// <summary>평문 → "dpapi:{base64}". 빈 값은 그대로, DPAPI 실패 시 기능 유지를 위해 평문 반환.</summary>
    public static string Protect(string plain)
    {
        if (string.IsNullOrEmpty(plain)) return plain;
        
        // 이미 보호된 값(복호 실패로 원본 암호문이 왕복된 경우)은 재암호화하지 않고 그대로 보존한다.
        if (plain.StartsWith(Prefix, StringComparison.Ordinal)) return plain;
        
        try
        {
            byte[] enc = ProtectedData.Protect(
                Encoding.UTF8.GetBytes(plain), Entropy, DataProtectionScope.CurrentUser);
            return Prefix + Convert.ToBase64String(enc);
        }
        catch { return plain; }
    }
}

DataProtectionScope.CurrentUser로 설정된 데이터는 오직 그 암호화를 수행한 사용자의 Windows SID(Security Identifier) 세션 안에서만 복호화됩니다. 다른 학생 계정으로 로그인한 사용자나, 설정 파일(JSON)을 USB에 담아 자신의 개인 노트북으로 가져간 외부인은 파일을 열어보아도 의미 없는 Base64 난수 바이트만 보게 되며, Windows OS 커널 수준에서 복호화를 거부합니다.

3. 핵심 아키텍처 2: 독자 엔트로피 솔트(Entropy Salt)와 프로세스 침해 방어

순수 DPAPI를 사용할 때의 한 가지 잠재적 위협은 "동일한 Windows 사용자 계정에서 실행되는 다른 서드파티 악성 앱"입니다. 동일 계정으로 실행되는 프로세스는 기본적으로 동일한 DPAPI 키 파생 권한을 갖기 때문입니다.

ZeliDesk SecretBox는 이를 방어하기 위해 독자적인 엔트로피 바이트 배열(Entropy = Encoding.UTF8.GetBytes("ZeliDesk.SecretBox.v1"))을 추가 솔트(Salt)로 투입합니다:

4. 핵심 아키텍처 3: 'dpapi:' 접두사 기반의 투명한 제로-마이그레이션

수많은 교실과 연구실에서 이미 구버전 ZeliDesk를 사용하고 있는 환경에서, 갑자기 암호화 시스템을 도입하면 기존에 평문으로 저장되어 있던 설정값을 읽지 못하거나 대규모 마이그레이션 스크립트를 실행해야 하는 부담이 생깁니다.

SecretBox는 "dpapi:" 접두사(Prefix) 휴리스틱을 통해 완벽한 하위 호환성을 달성했습니다:

C# (평문과 암호문을 투명하게 판별하는 Unprotect)
public static string Unprotect(string stored)
{
    if (string.IsNullOrEmpty(stored) ||
        !stored.StartsWith(Prefix, StringComparison.Ordinal))
        return stored; // 기존 평문 설정은 아무런 변형 없이 그대로 반환!

    try
    {
        byte[] dec = ProtectedData.Unprotect(
            Convert.FromBase64String(stored.Substring(Prefix.Length)),
            Entropy, DataProtectionScope.CurrentUser);
        return Encoding.UTF8.GetString(dec);
    }
    catch { return stored; } // 복호 실패 시에도 원본 암호문 보존!
}

저장된 문자열이 "dpapi:"로 시작하지 않으면 레거시 평문으로 간주하여 그대로 반환합니다. 사용자가 앱을 실행하여 작업을 마친 뒤 다시 설정을 저장(Protect)하는 순간, 평문 문자열은 자동으로 "dpapi:{base64}" 형태로 암호화되어 디스크에 영속화됩니다. 사용자나 관리자는 단 하나의 마이그레이션 버튼도 누를 필요 없이 100% 자연스럽게 보안 환경으로 전환됩니다.

5. 핵심 아키텍처 4: 데이터 소실을 방어하는 '무손실 왕복 보존(Round-trip)' 알고리즘

보안 구현에서 주니어 엔지니어들이 가장 쉽게 저지르는 치명적인 실수는 "복호화 실패 시 빈 문자열("")이나 null을 반환하는 것"입니다.

도메인 사용자 프로필이 일시적으로 로드되지 않았거나, Windows 업데이트 직후 LSA 서비스가 재시작되는 극히 드문 환경에서 일시적 복호화 오류가 발생할 수 있습니다. 이때 빈 문자열을 반환하면 다음과 같은 재앙이 발생합니다:

Catastrophic Scenario (복호 실패 시 빈 문자열을 반환할 때 발생하는 영구 파기)
// 1. 일시적 OS 결함으로 Unprotect가 빈 문자열("")을 반환
string apiKey = SecretBox.Unprotect(storedConfig.ApiKey); // apiKey = ""

// 2. 사용자가 다른 일반 설정을 바꾸고 저장을 누름
storedConfig.ApiKey = SecretBox.Protect(apiKey); // 빈 문자열이 저장됨!

// ➔ 결과: 사용자가 정성껏 등록해둔 수십 자리의 중요 API 키가 영구 파기되어 복구 불가!

SecretBox는 이 데이터 파기 사고를 수학적으로 원천 차단하는 무손실 왕복 보존(Round-trip Fault-Tolerance)을 탑재했습니다:

  1. 복호 실패 시 원본 암호문 반환: 복호화 과정에서 예외가 발생하면 빈 문자열 대신 stored 원본("dpapi:...")을 그대로 돌려줍니다.
  2. 재암호화 스킵 가드: Protect() 메서드의 첫머리에서 if (plain.StartsWith(Prefix, StringComparison.Ordinal)) return plain; 가드가 작동하여, 이미 보호된 원본 암호문이 다시 들어오면 이중 암호화하지 않고 그대로 통과시킵니다.

이 두 가지 가드가 결합됨으로써, 설령 OS 환경이 일시적으로 불안정하여 복호화에 실패하더라도 디스크에 저장된 암호문은 1바이트도 훼손되지 않고 온전히 유지됩니다. 이후 OS 환경이 정상화되면 다음 부팅 때 아무 일도 없었다는 듯 완벽하게 복호화가 재개됩니다.

6. 결론: "사용자가 의식하지 않아도 지켜지는 가장 완벽한 프라이버시"

진정한 보안은 사용자에게 복잡한 규칙과 암호 암기를 강요하는 것이 아니라, 사용자가 일상적인 업무를 수행하는 과정 속에서 시스템 스스로가 보이지 않게 데이터를 보호하는 것입니다.

ZeliDesk의 SecretBox는 Windows 커널 수준의 DPAPI 보호, 독자 엔트로피 솔트, 무손실 왕복 보존 결함 허용 설계, 그리고 투명한 제로 마이그레이션을 통해 대한민국 공교육 교실과 국방 워크스테이션 환경에 가장 신뢰할 수 있는 온디바이스 암호화 볼트(Vault)를 구축했습니다.

← 이전 기술 백서 #903 WinUI 3의 시스템 트레이 부재를 극복하는 순수 P/Invoke 서브클래싱: ZeliDesk TrayIconService의 Shell_NotifyIcon 및 0ms 백그라운드 상주 오케스트레이션 아키텍처 다음 기술 백서 → #901 관리자 권한 없이 완성하는 40대 PC 원클릭 배포: ZeliDesk UpdateService의 .appinstaller 프로토콜 연동 및 패키지 무결성 버전 거버넌스 아키텍처
⚡ ZeliDesk 안전한 온디바이스 볼트 체험하기

Windows DPAPI 커널 암호화와 무손실 왕복 보존으로 사내 비밀 키와 자격증명을 철저히 지켜내는 데스크톱 환경을 직접 경험하세요.

ZeliDesk Suite 무료 다운로드