본문 바로가기

C#

C# 암호화된 설정값을 안전하게 로드하기

API 키, 연결 문자열 같은 민감한 설정값은 평문 저장을 피하고 암호화하거나 시크릿 매니저를 사용해야 합니다. 이 글은 C#에서 암호화된 설정값을 안전하게 로드하는 실용적인 패턴을 정리합니다.

1. 기본 원칙

- 키와 사이퍼텍스트를 분리합니다(예: 키는 환경 변수/키 금고, 값은 파일).
- 저장소(Git)에는 평문 시크릿을 올리지 않습니다.
- 복호화는 필요한 시점에만 수행하고, 메모리에 오래 보관하지 않습니다.
- 키는 회전 가능하게 설계하고, 실패 시 안전하게 종료합니다.

2. Windows라면 DPAPI로 간단히

Windows 전용 시나리오에서는 DPAPI(ProtectedData)로 현재 사용자/머신 범위로 값 보호가 쉽습니다. 초기 1회 암호화 값(Base64)을 만들어 설정 파일에 저장하고, 런타임에 복호화합니다.

// 1) 오프라인/초기화 때 암호화(Windows 전용)
using System;
using System.Security.Cryptography;
using System.Text;

string plain = "P@ssw0rd!";
byte[] protectedBytes = ProtectedData.Protect(
    Encoding.UTF8.GetBytes(plain),
    optionalEntropy: null,
    scope: DataProtectionScope.CurrentUser);
string base64 = Convert.ToBase64String(protectedBytes);
Console.WriteLine($"DPAPI 암호문(Base64): {base64}");

// 2) 앱에서 복호화
string encBase64 = Environment.GetEnvironmentVariable("DB_PW_DPAPI_B64")!; // 또는 설정 파일에 저장된 Base64
byte[] cipher = Convert.FromBase64String(encBase64);
byte[] plainBytes = ProtectedData.Unprotect(cipher, null, DataProtectionScope.CurrentUser);
string password = Encoding.UTF8.GetString(plainBytes);

장점: 키 관리가 불필요합니다. 단점: Windows에 종속됩니다.

3. 크로스플랫폼: AES-GCM + 환경 변수 키

.NET(코어 3.0+)에서 AesGcm을 사용하면 검증 가능한 최신 대칭키 암호화를 사용할 수 있습니다. 키는 환경 변수나 키 금고(Azure Key Vault 등)에서 가져옵니다.

// 1) 키 생성(1회). Base64로 저장해 환경 변수(APP_AES_KEY)에 주입하세요.
using System;
using System.Security.Cryptography;

byte[] key = RandomNumberGenerator.GetBytes(32); // 256-bit
Console.WriteLine(Convert.ToBase64String(key));
// 2) AES-GCM 헬퍼
using System;
using System.Security.Cryptography;
using System.Text;

public static class CryptoGcm
{
    // 토큰 형식: "enc:gcm:" + Base64(nonce | cipher | tag)
    public static string Encrypt(string plaintext, byte[] key)
    {
        byte[] nonce = RandomNumberGenerator.GetBytes(12);
        byte[] plain = Encoding.UTF8.GetBytes(plaintext);
        byte[] cipher = new byte[plain.Length];
        byte[] tag = new byte[16];

        using var aes = new AesGcm(key);
        aes.Encrypt(nonce, plain, cipher, tag);

        byte[] payload = new byte[nonce.Length + cipher.Length + tag.Length];
        Buffer.BlockCopy(nonce, 0, payload, 0, nonce.Length);
        Buffer.BlockCopy(cipher, 0, payload, nonce.Length, cipher.Length);
        Buffer.BlockCopy(tag, 0, payload, nonce.Length + cipher.Length, tag.Length);

        return "enc:gcm:" + Convert.ToBase64String(payload);
    }

    public static string Decrypt(string token, byte[] key)
    {
        if (string.IsNullOrWhiteSpace(token) || !token.StartsWith("enc:gcm:"))
            return token;

        byte[] payload = Convert.FromBase64String(token.Substring(8));
        if (payload.Length < 12 + 16) throw new FormatException("잘못된 토큰 형식입니다.");

        int nonceLen = 12;
        int tagLen = 16;
        int cipherLen = payload.Length - nonceLen - tagLen;

        byte[] nonce = new byte[nonceLen];
        byte[] cipher = new byte[cipherLen];
        byte[] tag = new byte[tagLen];
        Buffer.BlockCopy(payload, 0, nonce, 0, nonceLen);
        Buffer.BlockCopy(payload, nonceLen, cipher, 0, cipherLen);
        Buffer.BlockCopy(payload, nonceLen + cipherLen, tag, 0, tagLen);

        byte[] plain = new byte[cipherLen];
        using var aes = new AesGcm(key);
        try
        {
            aes.Decrypt(nonce, cipher, tag, plain);
        }
        catch (CryptographicException)
        {
            throw new CryptographicException("복호화 실패(키 불일치 또는 데이터 손상)입니다.");
        }
        return Encoding.UTF8.GetString(plain);
    }
}
// 3) 사용 예: 환경 변수에서 키 로드 후 설정값 복호화
using Microsoft.Extensions.Configuration;
using System;

var config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", optional: false)
    .Build();

string keyB64 = Environment.GetEnvironmentVariable("APP_AES_KEY")
    ?? throw new InvalidOperationException("APP_AES_KEY 환경 변수를 설정하세요.");
byte[] key = Convert.FromBase64String(keyB64);

// appsettings.json: "Db:Password": "enc:gcm:..." 로 저장했다고 가정
string dbPassword = CryptoGcm.Decrypt(config["Db:Password"], key);

4. IConfiguration에 자연스럽게 통합

접근 시 자동 복호화하는 얇은 확장 메서드를 두면 호출부가 깔끔합니다.

using Microsoft.Extensions.Configuration;
using System;

public static class ConfigurationDecryptionExtensions
{
    public static string GetSecret(this IConfiguration config, string key, Func<string, string> decrypt)
    {
        var value = config[key];
        if (string.IsNullOrEmpty(value)) return value ?? string.Empty;
        return value.StartsWith("enc:gcm:") ? decrypt(value) : value;
    }
}

// Program.cs or composition root
var configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddEnvironmentVariables()
    .Build();

byte[] key = Convert.FromBase64String(Environment.GetEnvironmentVariable("APP_AES_KEY")!);

string conn = configuration.GetSecret("ConnectionStrings:Default", v => CryptoGcm.Decrypt(v, key));

5. 프로덕션: 클라우드 시크릿 매니저 사용

가능하면 클라우드 시크릿 매니저를 사용해 키와 값 모두를 안전하게 보관합니다. 예: Azure Key Vault + 관리형 ID.

// Azure Key Vault 연동(.NET 6+)
using Azure.Identity;
using Azure.Extensions.AspNetCore.Configuration.Secrets;
using Microsoft.Extensions.Configuration;

var builder = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", optional: true)
    .AddAzureKeyVault(new Uri("https://<your-vault-name>.vault.azure.net/"), new DefaultAzureCredential())
    .AddEnvironmentVariables();

var config = builder.Build();

// Key Vault에서 'Db--Password' 시크릿을 만들면 config["Db:Password"]로 접근 가능
string dbPassword = config["Db:Password"]; // 복호화 불필요(서버 측 저장 보호)

장점: 키 관리, 감사, 회전을 서비스가 지원합니다. 최소 권한의 관리형 ID로 접근을 제한하세요.

6. 체크리스트

- 저장소에는 절대 평문 시크릿을 커밋하지 않습니다.
- 키와 사이퍼텍스트를 분리하고, 환경 변수/키 금고를 사용합니다.
- AES-GCM 같은 인증된 암호를 사용하고 예외 시 안전하게 실패합니다.
- 로깅/덤프에 시크릿이 노출되지 않도록 필터링합니다.
- 정기적으로 키를 회전하고 접근 제어를 점검합니다.

상황이 단순하면 DPAPI(Windows), 범용이면 AES-GCM+환경 변수, 규모가 커지면 클라우드 시크릿 매니저를 선택하는 것이 실용적인 로드맵입니다.