Quipu-Log 교과서
파트 6 · 기밀성: 비밀을 지키면서 검색하기 (보안 ②)

24 · 필드 보호 4단계: 평문/SHA-256/HMAC/RSA

데이터베이스도 "컬럼 암호화"를 지원합니다. 하지만 실무에서 잘 안 쓰는 이유가 있습니다 — 암호화한 순간 WHERE 절로 검색할 수 없게 되거든요. Quipu-Log는 이 딜레마를 "필드마다 용도에 맞는 보호 수준을 고른다"는 접근으로 해결합니다. 이 챕터에서는 그 4단계 보호 모드를 비교하고, 어떤 필드에 무엇을 골라야 하는지를 정리합니다.

한 문장 요약

필드 보호는 전역 스위치가 아닙니다. 각 필드는 None / Sha256 / Hmac / Rsa 중 하나를 선택하고, 검색성과 키 의존성의 트레이드오프를 감수합니다.

DB에서는 어떻게 했나 — TDE와 컬럼 암호화의 한계

관계형 DB가 민감한 데이터를 보호하는 방법은 크게 둘입니다.

  • TDE(Transparent Data Encryption) — 디스크 파일 전체를 OS 레벨에서 암호화합니다. DB 엔진이 읽고 쓸 때는 투명하게 복호화되므로 쿼리가 그대로 동작합니다. 하지만 DB 프로세스에 접근할 수 있는 내부자(DBA, 로그 수집 시스템, 백업 읽기 권한자)에게는 완전히 투명하기 때문에 내부 위협에는 무력합니다.
  • 컬럼 암호화 — 특정 컬럼 값을 암호화해 저장합니다. 내부자가 파일을 열어도 암호문만 보입니다. 대신 암호화한 컬럼은 WHERE col = '홍길동' 같은 검색이 안 됩니다. 값을 비교하려면 복호화가 먼저 필요한데, 그러면 DB 엔진이 키를 갖고 있어야 하니 내부자 보호가 무너지죠.
DB ↔ 파일시스템

DB 컬럼 암호화는 "암호화하면 검색 안 됨"이라는 딜레마를 피할 수 없습니다. Quipu-Log는 이 딜레마를 두 가지 방법으로 돌파합니다 — 단방향 해시로 평문 없이 일치 검색을 허용하거나, 26장 블라인드 인덱스로 부분 검색까지 가능하게 합니다. 암호화와 검색성이 동시에 필요하다면, 다음 장들이 그 방법을 설명합니다.

4단계 비교: 무엇을 저장하고, 무엇을 얻나

Quipu-Log의 FieldProtection은 4단계로 나뉩니다. 아래 비교표로 먼저 전체 그림을 잡아 봅시다.

보호 수준 디스크에 저장되는 것 검색성 키 필요 주요 용도
None 평문 그대로 exact · contains · prefix 모두 없음 민감하지 않은 일반 필드
Sha256 SHA-256 다이제스트(64자 hex) exact 일치만 없음 중간 엔트로피 값(이메일, 의사번호 등)
Hmac HMAC-SHA-256 다이제스트(64자 hex) + key_version exact 일치만 HMAC 키 저엔트로피 값(SSN, 주민번호, 전화번호)
Rsa 암호문(AES-256-GCM) + 래핑된 키 + nonce 기본 없음(블라인드 인덱스 추가 가능) RSA 공개키(쓰기) + 개인키(읽기) 복호화가 필요한 값(이름, 주소 전체 등)

코드에서 이 4단계는 FieldProtection enum으로 표현됩니다.

crates/quipu-core/src/schema.rspub enum FieldProtection {
    None,
    Sha256,  // keyless — 키 없이도 동작, 저엔트로피에 취약
    Hmac,    // keyed — HMAC 키 없으면 offline brute-force 불가
    Rsa,     // hybrid AES-256-GCM + RSA-OAEP, 복호화 가능
}

그리고 이 보호 결과물은 StoredValue로 디스크에 남습니다.

crates/quipu-core/src/model.rspub enum StoredValue {
    Plain(Value),                       // None → 평문
    Sha256(String),                     // hex 다이제스트
    Hmac { key_version: u32, digest: String }, // keyed 다이제스트 + 버전
    Rsa {
        key_version: u32,               // 암호화에 쓴 공개키 버전
        wrapped_key: String,           // RSA로 감싼 AES 키 (b64)
        nonce: String, ciphertext: String,
    },
}

각 단계를 하나씩 뜯어보기

평문 값 "Alice Smith" Plain("Alice Smith") None 평문 값 "010-1234-5678" SHA-256 → "3a9f2b..." (hex) Sha256 평문 값 "123-45-6789" (SSN) HMAC + 키 → keyed 다이제스트 Hmac 평문 값 "홍길동" (이름) AES-GCM 암호문 + RSA 래핑 키 Rsa 검색 ✓ 전부 검색 ✓ exact만 검색 ✓ exact만 검색 ✗ (기본) 키 없음 키 없음 HMAC 키 RSA 공개키 ← 개인키로 복호화 가능 ← 복호화 불가 (일방향) ← 복호화 불가 (일방향) ← 복호화 불가 (일방향)
FieldProtection 4단계: 평문이 어떻게 변환돼 저장되고, 무엇이 가능하고 불가능한지.

None — 평문

아무 변환 없이 그대로 저장합니다. 민감하지 않은 필드에 씁니다. 검색은 모든 방식이 가능하고, 별도 키 설정이 필요 없습니다.

Sha256 — 키 없는 단방향 해시

평문의 SHA-256 다이제스트만 저장합니다. 검색할 때는 질의어를 똑같이 SHA-256으로 해싱해 비교하므로 exact 일치 검색은 동작합니다. 원본 평문은 복호화할 수 없습니다. 다만 키가 없으므로 디스크를 열람할 수 있는 공격자가 사전 공격(dictionary attack)을 시도할 수 있습니다 — 전화번호, 주민번호처럼 경우의 수가 유한한 값에는 위험합니다.

Hmac — 키 있는 단방향 해시

HMAC-SHA-256으로 다이제스트를 만들고, 그 다이제스트를 만드는 데 쓴 키 버전도 함께 저장합니다. 검색 시 서버는 질의어를 같은 HMAC 키로 해싱해 비교하므로 exact 일치가 가능합니다. 키 없이는 오프라인 사전 공격이 불가능합니다. SSN, 전화번호 같은 저엔트로피 값에는 Sha256 대신 반드시 Hmac을 써야 하는 이유입니다.

Rsa — 복호화 가능한 하이브리드 암호

값을 AES-256-GCM으로 암호화하고, 그 AES 키를 RSA 공개키로 감싸 저장합니다. 개인키를 가진 주체만 복호화할 수 있습니다. 기본적으로 검색이 불가능하지만, 26장 블라인드 인덱스를 추가하면 prefix/ngram 검색이 가능해집니다. 왜 이 독특한 하이브리드 구조를 택했는지는 25장에서 자세히 설명합니다.

보안 포인트

SHA-256은 "키가 없다 = 누구나 같은 해시를 만들 수 있다"는 뜻입니다. 전화번호 01012345678을 SHA-256한 값은 전 세계 어디서나 동일하므로, 공격자가 10억 개의 한국 번호를 해싱해 테이블을 만들면(레인보우 테이블) 다이제스트만으로 원본을 복구할 수 있습니다. HMAC은 서버만 아는 비밀 키가 섞이므로 그런 사전 공격이 원천 봉쇄됩니다.

보호는 레지스트리 필드에만 적용됩니다

이 지점이 가장 자주 혼동되는 부분입니다. 반드시 기억해야 합니다.

주의

FieldProtection은 레지스트리 필드(엔티티의 속성)에만 적용됩니다. 감사 로그 행의 method, url, content, 커스텀 컬럼은 항상 평문입니다. 민감한 정보(PII, 비밀번호, SSN)를 content나 URL에 넣으면 보호되지 않습니다. 민감한 값은 레지스트리 필드로 설계해 보호 수준을 붙이고, entity_id는 불투명한 ID(UUID 등)로 유지하세요.

예를 들어 환자 정보를 기록한다면 이렇게 설계합니다.

TypeSchema 설계 예시store.define_type(TypeSchema::new("patient", vec![
    FieldDef::text("ssn").protection(FieldProtection::Hmac).indexed(),
    FieldDef::text("name").protection(FieldProtection::Rsa),
    FieldDef::text("mrn").protection(FieldProtection::Sha256).indexed(),
]))?;
// entity_id는 opaque id (예: "patient-uuid-001") — SSN 직접 사용 금지
// content, url 같은 로그 컬럼에는 PII를 넣지 마세요

정리

  • 보호 수준은 필드별 선택이다 — 전역 스위치가 아니라 필드마다 용도에 맞게 골라야 한다.
  • None 평문, Sha256 키 없는 단방향, Hmac 키 있는 단방향, Rsa 복호화 가능. 검색성은 앞 셋은 exact(Rsa는 기본 없음).
  • 저엔트로피 값에는 Hmac을 쓴다 — Sha256은 오프라인 사전 공격에 취약하다.
  • 보호는 레지스트리 필드만 — 로그 컬럼(method/url/content)은 항상 평문이다.
  • 암호화하면서 검색도 하고 싶다면 → 25장 하이브리드 암호26장 블라인드 인덱스.
스스로 확인

① DB의 TDE와 컬럼 암호화는 왜 "검색 불가" 문제를 갖게 되나요? Quipu-Log의 Sha256/Hmac은 이 문제를 어떻게 회피했나요?
② SSN을 Sha256으로 보호하면 왜 위험한지, Hmac으로 바꿔야 하는 이유를 한 문장으로 설명해 보세요.
③ 환자의 이름을 로그 url에 넣는 것이 왜 문제인가요? 올바른 설계는 무엇인가요?