콘텐츠로 이동

(KO) CUBRID TDE — 마스터 키로 래핑된 DEK 위에서 동작하는 투명 페이지 단위 암호화

목차

페이지 단위 스토리지 엔진이 디스크에 한 바이트라도 쓰기 전에 파일 시스템 접근권을 가진 공격자로부터 데이터를 방어하려면 네 가지 질문에 먼저 답해야 한다 — 어떤 cipher 인가, 어떤 키 인가, 어떤 nonce 인가, 키는 어디에 사는가. 각 답은 다른 모 든 계층 — 버퍼 풀, WAL, 복구 코드, 백업 도구, DBA 운영 런북 — 까지 그대로 새어 나간다. 그래서 멀리서 보는 것보다 설계 공 간은 훨씬 좁다.

위협 모델 (threat model). Encryption-at-rest 는 데이터 파 일, WAL, 백업, 디스크 이미지를 복사할 수 있는 공격자를 막아 주 지만, 실행 중인 프로세스의 메모리를 읽을 수 있는 공격자는 막 지 못한다. 악의적인 DBA, 호스트에서 동작 중인 메모리 스크래핑 멀웨어, cipher 구현의 사이드 채널 같은 위협은 이 모델 밖이다. Database Internals (Petrov, 4장 “Transaction Processing and Recovery”, 그리고 마지막 장의 encryption-at-rest 단상) 는 TDE 를 파일 시스템 단위 의 방어를 페이지 단위로 다시 짠 것으로 정리한다. 엔진이 실제로 필요한 페이지만 복호화할 수 있게 하기 위해서다.

Cipher 선택. AES (FIPS 197) 가 어디서나 기본인 이유는 하 드웨어 지원이다. x86 의 AES-NI, ARMv8 의 Crypto Extensions, 그리고 OpenSSL 안의 대응 intrinsic 이 페이지당 오버헤드를 한 바이트당 몇 사이클 수준으로 떨어뜨린다. 운영 모드 (mode of operation) 선택은 cipher 선택만큼이나 중요하다. NIST SP 800-38A 가 옵션을 정리한다.

  • CBC 는 블록을 chaining 한다. 마지막 블록이 직전 블록 모 두에 의존한다. 확산 (diffusion) 은 좋지만 페이지 안에서 암 호화 경로가 순차적이고, 암호문 한 비트가 뒤집히면 복호화 시 한 블록 전체가 쓰레기 평문으로 번진다. 페이지가 자체 checksum 을 가지므로 그것 자체는 괜찮다. 다만 페이지 일부만 다시 쓰 는 동작이 있어야 한다면 좋지 않다.
  • CTR 은 nonce || counter 를 암호화한 키 스트림을 평문에 XOR 한다. 페이지 안에서 암호화는 완전히 병렬이고, 암호문 길 이가 평문과 같다 (padding 이 없다). in-place 부분 재기록도 자명하게 지원된다. 단 한 가지 절대 규칙은 (key, nonce) 쌍 이 절대 반복되어선 안 된다 는 것이다. 같은 쌍에서 만들어진 두 암호문을 XOR 하면 두 평문의 XOR 이 그대로 드러나기 때문이 다. CUBRID 는 CTR 을 골랐다.

키 계층 (key hierarchy). 모든 페이지를 사용자의 마스터 키 하나로 직접 암호화하는 평면 구조는 키 회전 (rotation) 에 적대 적이다. 키를 회전한다는 것이 데이터베이스의 모든 페이지를 다 시 암호화한다는 의미가 되기 때문이다. 교재의 해법은 Oracle TDE 가 대중화한 두 단계 계층 이다.

  1. Master Key (MK). DBA 가 시작 시 제공하거나, 키 관리 서 버가 공급한다. 서버 동작 중에는 휘발성 메모리에만 산다.
  2. 하나 이상의 Data Encryption Key (DEK). 암호화 용도별로 하나씩 둔다 (테이블 데이터, 임시 영역, WAL). 데이터베이스 를 만들 때 한 번 생성되며, 디스크에는 항상 MK 로 래핑 (wrapped) — 즉 암호화 — 된 형태로만 저장된다.

이 구조 위에서 MK 를 회전한다는 것은, 래핑된 DEK 를 옛 MK 로 풀고 새 MK 로 다시 래핑해서 그 결과 blob 을 디스크에 다시 쓰 는 일이 된다. 데이터 페이지 자체에는 손이 가지 않는 것이 핵심 이다. DEK 자체를 회전한다는 것은 그 DEK 에 의존하는 모든 페 이지를 다시 암호화한다는 뜻이 되므로, 엔진은 정말 어쩔 수 없 는 경우가 아니면 DEK 회전은 피한다.

페이지별 nonce. CTR 모드에서 nonce 유일성 invariant 는 키 의 입자성을 결정하는 단단한 제약이다. 단일 DEK 가 모든 페이지 를 암호화한다면, nonce 는 데이터베이스 전체에서 전역적으로 유 일해야 한다. 자연스러운 선택지는 다음과 같다.

  • LSN / LSA — 페이지의 가장 최근 로그 시퀀스 번호. 영구 페이지에서는 모든 수정이 페이지 flush 전에 새 로그 레코드를 쓰기 때문에 (WAL) 항상 새로운 값이 된다.
  • 페이지 식별자 — (volid, pageid) 는 공간상으로는 유일하 지만, 같은 페이지가 다시 쓰여도 변하지 않는다. 키가 고정된 상태에서 이 식별자만 nonce 로 쓰면, 다른 내용으로 다시 쓰는 순간 유일성 invariant 가 깨진다.
  • 단조 증가 카운터 — LSA 가 의미가 없는 페이지 (temp 파일 은 LSA 가 sentinel 값으로 고정되어 있다) 에서는 사용 가능하 지만, 원자 증분이 있어야 한다.
  • WAL 의 논리 페이지 ID — 데이터베이스마다 단조 증가하는 로그 페이지 번호.

CUBRID 의 선택은 영구 페이지에는 LSA, 임시 페이지에는 원자 카운터, 로그 페이지에는 논리 페이지 ID 다. 세 종류의 DEK 위 에 세 가지 정책을 얹은 셈이다.

래핑된 DEK 는 어디에 사는가. 두 진영이 있다.

  • 데이터베이스 내부. PostgreSQL 의 개발 중인 TDE, MySQL InnoDB TDE, 그리고 CUBRID 는 모두 래핑된 DEK 를 데이터베이 스 내부의 시스템 테이블이나 전용 heap 에 저장한다. trade-off 는 데이터베이스를 백업하면 래핑된 DEK 도 함께 백업된다는 점 이지만, 그것을 푸는 데에는 여전히 마스터 키가 필요하다는 점 이 안전 장치다.
  • 데이터베이스 옆. 마스터 키 파일 자체는 어딘가에 있어야 한 다. 하드웨어 모듈 (HSM / KMS) 이거나, DBA 소유의 OS 파일이 거나, 어쨌든 데이터베이스 파일과는 분리된 자리다. CUBRID 는 OS 파일 <db>_keys 를 쓴다. 기본 위치는 데이터베이스 파일 과 같은 디렉터리이고, tde_keys_file_path 로 변경할 수 있 다.

성능 예산 (performance budget). AES-NI 를 쓰는 AES-256-CTR 은 코어당 1 GB/s 정도의 처리량을 낸다. 16 KB 페이 지 한 장의 암호화 또는 복호화가 대략 16 마이크로초의 CPU 시간 을 가져간다. 버퍼 풀 hit 가 지배적인 워크로드에서는 사실상 보 이지 않고, 디스크에 매번 flush 하는 쓰기 위주 워크로드에서는 전체 쓰기 경로의 5–10 % 정도가 더 든다. AES-NI 가 없거나 페이 지가 더 작으면 비용이 빠르게 나빠진다. 비용은 전적으로 I/O 경 로에 떨어진다. 이미 복호화되어 있는 페이지에 대한 버퍼 풀 hit 는 무료다.

DBMS 공통 설계 패턴 (Common DBMS Design)

섹션 제목: “DBMS 공통 설계 패턴 (Common DBMS Design)”

다섯 개 엔진 (Oracle, SQL Server, MySQL InnoDB, PostgreSQL, CUBRID) 이 같은 이유로 폭넓게 비슷한 선택을 한다. 모두 OS 파 일 시스템과 고정 크기 페이지 추상 사이에 끼어 있어서, 암호화 경계가 자연스럽게 “버퍼 풀과 디스크의 frontier 를 가로지르는 바이트” 위에 놓인다.

Oracle TDE — 두 단계 계층을 2005 년에 처음 도입했다. 각 테이블스페이스 가 자기 DEK 를 가지며, 모든 DEK 는 Oracle Wallet (PKCS #12 파일) 또는 HSM 에 저장된 마스터 키로 래핑된 다. CTR 모드 선택은 더 나중에 들어왔고, 초기 버전은 CBC 를 썼 다. 입자성은 테이블스페이스 단위라, 표 단위 암호화는 native 하게 지원되지 않는다. 민감한 표를 별도 테이블스페이스로 분리 하는 식의 운영으로 보통 해결한다.

SQL Server TDE — 세 단계 계층이다. Service Master Key (SMK, 머신 종속) → Database Master Key (DMK, 데이터베이스별) → Database Encryption Key (DEK) 로 래핑이 이어진다. 알고리즘 은 AES-256 이다 (별도 기능인 Always Encrypted 는 클라이언트 드라이버 안에서 컬럼 단위로 암호화한다). 입자성은 데이터베이 스 단위 — DB 전체 혹은 무 (nothing) 다.

MySQL InnoDB TDE — 테이블스페이스별 DEK 를 keyring 플러그 인 (file, AWS KMS, HashiCorp Vault 등) 이 관리하는 마스터 키 가 래핑한다. 기본 알고리즘은 AES-256-CBC, 페이지별 IV 는 테이 블스페이스 ID 와 페이지 번호에서 유도된다. 래핑된 DEK 는 테이 블스페이스 헤더에 산다. 마스터 키를 회전해도 테이블스페이스별 키는 다시 래핑될 뿐 페이지 데이터는 손대지 않는다.

PostgreSQL TDE — 본 트리에서는 역사적으로 지원되지 않았 다. Cybertec / EnterpriseDB 의 TDE Patch 가 클러스터 단위 암호화를 목표로 진행 중이며, passphrase 에서 유도된 단일 DEK 와 WAL 용 별도 cluster file encryption key (CFEK) 를 둔다. 2026 년 초 시점에서도 upstream 설계는 여전히 유동적이다. Percona, Cybertec 등의 fork 가 프리릴리스 TDE 를 먼저 제공한 다.

CUBRID 는 MySQL InnoDB 나 Oracle 과 같은 공간에 자리한다. DB 별로 세 개의 DEK (perm / temp / log), 데이터베이스마다 마 스터 키 하나, AES-256-CTR (또는 한국 컴플라이언스용 ARIA-256-CTR), 그리고 마스터 키 파일은 데이터베이스 트리 에 둔다는 선택이다. 입자성 다이얼은 파일 단위 — 즉 heap 단 위, B-tree 단위 — 로 설정되며 SQL CREATE TABLE ... ENCRYPT=AES 로 노출된다. 구현은 tde.c 약 1700 줄과 페이지 버퍼, 로그 페이지 버퍼, 파일 매니저, 부트 코드의 얇은 후크들 로 이루어져 있다.

CUBRID 는 두 단계를 쓴다. 마스터 키 하나가 Data Key Set 을 래 핑하고, 두 번째 단계는 세 개의 독립적인 DEK 로 다시 갈라진다.

// TDE_DATA_KEY_SET — src/storage/tde.h
typedef struct tde_data_key_set
{
unsigned char perm_key[TDE_DATA_KEY_LENGTH]; // permanent data pages
unsigned char temp_key[TDE_DATA_KEY_LENGTH]; // temporary file pages
unsigned char log_key[TDE_DATA_KEY_LENGTH]; // WAL pages
} TDE_DATA_KEY_SET;

세 DEK 모두 256 비트 길이 (TDE_DATA_KEY_LENGTH = 32) 이며, 마스터 키도 마찬가지다 (TDE_MASTER_KEY_LENGTH = 32). 데이터 베이스 생성 시점에 tde_create_dk() 가 OpenSSL 의 RAND_bytes() 를 직접 호출해 한 번 생성한다. 유도 (derivation) 나 passphrase 는 없다. DEK 를 셋으로 쪼개는 이유 는 nonce 정책 때문이다. 페이지 종류마다 nonce 로 쓸 수 있 는 식별자가 다르고, 종류별로 별개의 DEK 를 부여하면 가상의 nonce 재사용 버그가 한 종류의 페이지로 격리된다.

이 계층을 디스크 위에 구체화하는 레코드가 TDE_KEYINFO 다.

// TDE_KEYINFO — src/storage/tde.h
typedef struct tde_keyinfo
{
int mk_index; // index in master-key file
time_t created_time; // of the master key
time_t set_time; // last MK change on this DB
unsigned char mk_hash[TDE_MASTER_KEY_LENGTH]; // SHA-256(MK), for validation
unsigned char dk_perm[TDE_DATA_KEY_LENGTH]; // wrapped: enc_MK(perm_key)
unsigned char dk_temp[TDE_DATA_KEY_LENGTH]; // wrapped: enc_MK(temp_key)
unsigned char dk_log[TDE_DATA_KEY_LENGTH]; // wrapped: enc_MK(log_key)
} TDE_KEYINFO;

이 레이아웃에서 짚을 점이 셋이다.

  1. DEK 는 래핑된 형태로만 저장된다. 푸는 데에는 MK 가 필요 하다. 평문 형태로는 절대 디스크에 닿지 않는다.
  2. MK 는 해시 로만 저장된다. 후보 MK 가 들어왔을 때 “이 MK 가 이 DEK 들을 래핑한 그 MK 와 일치하는가?” 를 검증하기에 해시면 충분하며, MK 자체를 저장할 필요가 없다. 이것이 데이 터베이스를 여는 흐름에서 잘못된 MK 를 거절하는 근거다.
  3. mk_index 는 마스터 키 파일 <db>_keys 안의 실제 MK 바이 트 스트림을 가리키는 역포인터다.

TDE_KEYINFO blob 자체는 데이터베이스 내부의 작은 CUBRID heap 에 산다. MVCC 조정을 피하려고 손으로 빚은 형태다 (tde_initialize()vacuum_rv_check_at_undo() 의 UNDO 도 중 레코드 조정을 막기 위해 일부러 더미 repid_and_flag_bits int 를 레코드 앞에 prepend 한다). 그 heap 의 HFID 는 boot_Db_parm.tde_keyinfo_hfid 에 보관되어 재시작 시 tde_cipher_initialize() 로 전달된다.

마스터 키 파일은 기본적으로 <db_full_name>_keys 위치에 (tde_keys_file_path 가 설정되어 있다면 그 경로 아래에) 자리 한다. 모드 0600 으로 열리며, 매직 헤더 다음에 최대 128 개의 고정 크기 슬롯이 이어진다.

// TDE_MK_FILE_ITEM — src/storage/tde.h
typedef struct tde_mk_file_item
{
time_t created_time; // -1 if slot is invalid
unsigned char master_key[TDE_MASTER_KEY_LENGTH];
} TDE_MK_FILE_ITEM;
#define TDE_MK_FILE_ITEM_COUNT_MAX 128

슬롯 0 은 매직을 건너뛴 CUBRID_MAGIC_MAX_LENGTH offset 에서 시작한다. created_time == -1 인 슬롯은 삭제된 것으로 취급된 다. tde_delete_mk() 는 timestamp 를 -1 로 덮어쓰지만 파일을 줄이지는 않는다. tde_add_mk() 는 첫 번째로 만나는 삭제된 슬 롯을 채우거나, 빈 슬롯이 없으면 끝에 append 한다.

graph TD
  DBA[DBA passphrase / KMS] -- 기록 --> MKF[<db>_keys 파일<br/>모드 0600<br/>최대 128 MK 슬롯]
  MKF -- 슬롯 mk_index의 32B MK --> MK[메모리 안의 Master Key<br/>32 바이트]
  MK -- AES-256-CTR / ARIA-256-CTR --> WRAP{wrap}
  PERM[perm_key 32B] -.DB 생성 시 생성.-> WRAP
  TEMP[temp_key 32B] -.DB 생성 시 생성.-> WRAP
  LOG[log_key 32B] -.DB 생성 시 생성.-> WRAP
  WRAP --> KI[TDE_KEYINFO heap 레코드<br/>데이터베이스 내부]
  KI -- mk_index --> MKF
  KI -- mk_hash --> MK
  MK -- 재시작 시 unwrap --> CIPHER[tde_Cipher.data_keys<br/>평문 DEK<br/>프로세스 메모리 한정]
  CIPHER --> ENC[페이지 암호화 / 복호화]

cipher 선택은 tde_encrypt_internal() 안의 알고리즘별 스위치 한 번으로 끝난다.

// tde_encrypt_internal — src/storage/tde.c
switch (tde_algo)
{
case TDE_ALGORITHM_AES:
cipher_type = EVP_aes_256_ctr ();
break;
case TDE_ALGORITHM_ARIA:
cipher_type = EVP_aria_256_ctr ();
break;
case TDE_ALGORITHM_NONE:
default:
assert (false);
goto cleanup;
}

두 알고리즘 모두 256 비트 키와 CTR 모드를 쓴다. ARIA 는 한국 표준 (KS X 1213) cipher 로, 한국의 금융·정부 환경에서 KISA 컴 플라이언스를 만족시키기 위해 함께 제공된다. tde.h 상단의 TDE_DK_ALGORITHM 매크로는 DEK 래핑용 알고리즘을 AES 로 못 박아 둔다 (데이터 페이지 알고리즘이 무엇이든 마스터 키는 항상 AES-256-CTR 로 DEK 를 래핑한다).

코드 전반에서 nonce 라고 부르는 IV 는 페이지 종류별로 다르 게 구성되는 16 바이트 버퍼다.

페이지 종류DEKnonce 출처길이
영구 데이터 페이지perm_keyiopage_plain->prv.lsa (8B) zero-pad16 B
임시 데이터 페이지temp_keytde_Cipher.temp_write_counter, ATOMIC_INC_64, 8B zero-pad16 B
WAL 페이지log_keylogpage_plain->hdr.logical_pageid (8B) zero-pad16 B
래핑된 DEK(master key)tde_dk_nonce(dk_type) — 종류별 0/1/2 고정 패턴16 B

영구 페이지의 경우 LSA 는 WAL 덕분에 매 수정마다 자동으로 증 가한다 — undo / redo 로그가 append 될 때마다 새 LSA 가 만들어 지고 그 값이 페이지에 stamp 된다. (perm_key, lsa) 의 결합은 (페이지, 버전) 전역에 걸쳐 유일하므로 CTR invariant 가 충족된 다. nonce 는 암호화된 페이지 안의 prv.tde_nonce 에 박혀 디 스크로 함께 가고, reader 가 그 값을 그대로 읽어 복호화에 쓴 다.

임시 페이지에서는 LSA 가 sentinel 값 (pgbuf_init_temp_page_lsa) 으로 고정되어 있어 nonce 역할을 못한다. 대신 tde_Cipher 의 64 비트 원자 카운터가 매 암호화 마다 증가하고, 그 값이 prv.tde_nonce 에 기록되었다가 복호화 시점에 읽힌다. 카운터는 서버 시작 시 0 으로 초기화된다. 임시 파일은 재시작 시 다시 만들어지므로 우려는 서버 한 라이프타임 안의 wrap-around 뿐이다. 페이지 쓰기당 1 씩 증가하는 속도로는 2⁶⁴ 가 수천 년 걸리니 사실상 무관하다.

로그 페이지는 WAL 자체의 단조 증가 페이지 번호인 logical pageid 를 그대로 쓴다. 전용 log_key 와 결합하므로, 어떤 로 그 페이지도 다른 로그 페이지와 (key, nonce) 쌍을 공유하지 않 는다.

DEK 자체에 대한 nonce 는 tde_dk_nonce() 가 정한 고정 패턴이 다.

// tde_dk_nonce — src/storage/tde.c
case TDE_DATA_KEY_TYPE_PERM: memset (dk_nonce, 0, ...); break;
case TDE_DATA_KEY_TYPE_TEMP: memset (dk_nonce, 1, ...); break;
case TDE_DATA_KEY_TYPE_LOG: memset (dk_nonce, 2, ...); break;

이것이 안전한 이유는 마스터 키가 DEK 가 다시 쓰이는 빈도보다 더 자주 회전하기 때문이다. 각 (mk, dk_type) 쌍은 DEK 의 일생 동안 정확히 두 번만 암호화된다 — DB 생성 시 한 번, 그리고 매 MK 변경 시 한 번씩. 마스터 키가 두 번 쓰기 사이에 바뀌므로 종류별로 고정 nonce 를 쓰더라도 CTR 유일성은 깨지지 않는다. 동일한 MK 가 동일한 DEK 를 두 번 래핑하면 결과 바이트가 동일 해진다는 점은 (같은 key + nonce 는 같은 키 스트림을 만든다) 허용된다.

CUBRID 의 I/O 페이지는 고정 크기 struct 인 FILEIO_PAGE 다.

// FILEIO_PAGE — src/storage/file_io.h
struct fileio_page_reserved
{
LOG_LSA lsa; // page LSN
INT32 pageid;
INT16 volid;
unsigned char ptype;
unsigned char pflag; // bit 0x1 = AES, bit 0x2 = ARIA, mask 0x3
INT32 p_reserve_1;
INT32 p_reserve_2;
INT64 tde_nonce; // counter (temp) or LSA copy (perm)
};
struct fileio_page
{
FILEIO_PAGE_RESERVED prv; // header — NEVER encrypted
char page[1]; // body — encrypted on-disk
FILEIO_PAGE_WATERMARK prv2; // tail-LSA — NEVER encrypted
};

암호화 경계는 매크로 두 개로 정의된다.

src/storage/tde.h
#define TDE_DATA_PAGE_ENC_OFFSET sizeof (FILEIO_PAGE_RESERVED)
#define TDE_DATA_PAGE_ENC_LENGTH DB_PAGESIZE

디스크 위 평문 부분은 머리의 FILEIO_PAGE_RESERVED 와 꼬리의 FILEIO_PAGE_WATERMARK 다. 암호문 부분은 그 사이의 모든 바이 트, 길이는 DB_PAGESIZE 다. 이렇게 갈라야 하는 이유가 셋이다.

  • 헤더 는 평문이어야 한다. 버퍼 매니저가 페이지를 복호화할 지 말지를 결정하기 전에 헤더를 읽기 때문이다. pflag 자 체가 이 페이지는 암호화되어 있는가 를 알려 주는 플래그이 므로, pflag 를 키 상태의 일부로 두고 pflag 를 암호화하 는 것은 순환 의존이 된다.
  • 꼬리의 watermark 도 평문이어야 한다. 이 필드 (LSA 의 복 사본) 는 애초에 torn-write 검출 을 위해 도입된 자리다. CUBRID 는 prv.lsa == prv2.lsa 비교로 페이지의 부분 쓰기를 감지하는데, 이 검사는 암호화된 형태에서도 동작해야 한다.
  • (prv 안의) tde_nonce 필드는 IV 그 자체이고, 암호학 적 doctrine 에 따라 IV 는 공개되어야 한다.

로그 페이지에도 비슷한 분할이 있다.

src/storage/tde.h
#define TDE_LOG_PAGE_ENC_OFFSET sizeof (LOG_HDRPAGE)
#define TDE_LOG_PAGE_ENC_LENGTH ((LOG_PAGESIZE) - (TDE_LOG_PAGE_ENC_OFFSET))

LOG_HDRPAGE (16 바이트: logical pageid, offset, flags, checksum) 는 평문으로 남는다. 그 안의 flags 필드가 암호화 마 커를 들고 다닌다.

src/transaction/log_storage.hpp
#define LOG_HDRPAGE_FLAG_ENCRYPTED_AES 0x1
#define LOG_HDRPAGE_FLAG_ENCRYPTED_ARIA 0x2
#define LOG_IS_PAGE_TDE_ENCRYPTED(p) \
((p)->hdr.flags & LOG_HDRPAGE_FLAG_ENCRYPTED_AES \
|| (p)->hdr.flags & LOG_HDRPAGE_FLAG_ENCRYPTED_ARIA)

이 플래그가 데이터 페이지의 pflag 와 같은 역할을 하지만, 자 리가 로그 페이지 헤더라는 점이 다를 뿐이다.

파일 단위 입자성과 테이블스페이스 다이얼

섹션 제목: “파일 단위 입자성과 테이블스페이스 다이얼”

암호화는 파일 단위 다 — 전역이 아니다. 각 FILE_HEADER 가 자기 TDE_ALGORITHM 을 들고 다니며, file_set_tde_algorithm() 으로 조작된다. 페이지가 파일에 처 음 할당될 때 file_alloc() 이 헤더의 알고리즘을 읽어 새 페이 지의 pflag 에 stamp 한다.

// file_alloc — src/storage/file_manager.c
tde_algo = file_get_tde_algorithm_internal (fhead);
// ... debug logging ...
pgbuf_set_tde_algorithm (thread_p, page_alloc, tde_algo,
FILE_IS_TEMPORARY (fhead));

이미 존재하는 파일에 암호화를 켜는 동작은 file_apply_tde_algorithm() 이다. 파일 헤더에 알고리즘을 set 한 뒤, 해당 파일의 모든 사용자 페이지를 무조건 write 모드로 latch 하면서 walk 하며 각 페이지의 pflag 를 set 한다. 모든 페이지를 다시 flag 해야 (그래서 다음 flush 에서 다시 암호화되 어야) 하므로 이 동작은 O (파일 페이지 수) 이고, 동시 접근이 시작되기 전에 끝나야 한다.

SQL 표면은 CREATE TABLE ... ENCRYPT=AES 다. 이 구문은 그 표 의 heap 파일과 모든 overflow 파일을 file_apply_tde_algorithm() 을 부르는 형태로 번역된다 (heap_create_internal 시점의 호출은 file_manager.c 의 12401, 12410 줄에 보인다). 사용자에게 노출되는 다이얼은 표 단 위지만, 내부 메커니즘은 파일 단위다.

어떤 파일에도 속하지 않는 페이지 — 가장 두드러지게 볼륨 헤더 와 파일 매니저 자체의 file-table 페이지 — 는 절대로 암호화 되지 않는다. 그 페이지들의 pflag 는 0 인 채로 남는다. 즉 데 이터 파일을 손에 넣은 공격자는 볼륨 레이아웃, sector bitmap, file allocation table 까지는 여전히 볼 수 있다. 방어가 미치는 범위는 사용자 데이터 — heap 페이지, overflow 페이지, B-tree 페이지 — 와 WAL 뿐이다.

dirty 데이터 페이지는 디스크로 가는 길에 모두 pgbuf_bcb_flush_with_wal() 을 통과한다. 암호화 후크는 “페이 지를 밖으로 복사한다” 시퀀스의 가장 위에 놓여 있다.

// pgbuf_bcb_flush_with_wal — src/storage/page_buffer.c
iopage = (FILEIO_PAGE *) PTR_ALIGN (page_buf, MAX_ALIGNMENT);
CAST_BFPTR_TO_PGPTR (pgptr, bufptr);
tde_algo = pgbuf_get_tde_algorithm (pgptr);
if (tde_algo != TDE_ALGORITHM_NONE)
{
error = tde_encrypt_data_page (&bufptr->iopage_buffer->iopage,
tde_algo, is_temp, iopage);
// ...
}
else
{
memcpy (iopage, &bufptr->iopage_buffer->iopage, IO_PAGESIZE);
}
if (uses_dwb)
{
error = dwb_set_data_on_next_slot (thread_p, iopage, ..., &dwb_slot);
// ...
}

흐름을 따라가 보면 다음과 같다.

  1. IO_MAX_PAGE_SIZE 크기의 스택 정렬 스크래치 버퍼 iopage 를 확보한다.
  2. pflag 에서 페이지별 TDE 알고리즘을 읽는다. 0 이면 메모리 상의 페이지를 스크래치 버퍼로 memcpy 하고 진행한다.
  3. 0 이 아니면 tde_encrypt_data_page() 를 부른다. 이 함수가 헤더와 watermark 를 평문으로 복사하고, nonce 를 계산하고 (영구는 LSA, 임시는 원자 카운터), nonce 를 iopage->prv.tde_nonce 에 쓰고, 본체를 CTR 로 암호화해 스 크래치 버퍼에 담는다.
  4. DWB (double-write buffer) 가 활성화되어 있으면 암호화된 페이지를 받아 자기 슬롯에 둔다. DWB 는 순수하게 torn-write 방어 장치이고, 자기가 보는 바이트는 이미 암호화된 상태다.
  5. DWB 가 제어를 돌려준 뒤 fileio_write 가 암호화된 페이지 를 데이터 볼륨에 쓴다.
sequenceDiagram
  participant T as 워커 스레드
  participant PB as 페이지 버퍼
  participant TDE as tde_encrypt_data_page
  participant DWB as Double-write 버퍼
  participant FIO as fileio_write
  participant DSK as 데이터 볼륨

  T->>PB: pgbuf_bcb_flush_with_wal
  PB->>PB: pflag 읽기 → tde_algo
  alt tde_algo != NONE
    PB->>TDE: encrypt(plain, algo, is_temp, cipher)
    TDE->>TDE: nonce = LSA (perm) 또는 atomic++ (temp)
    TDE->>TDE: 헤더 + watermark 평문 복사
    TDE->>TDE: AES-256-CTR 본체 → cipher 버퍼
    TDE-->>PB: cipher 버퍼
  else tde_algo == NONE
    PB->>PB: memcpy plain → cipher
  end
  PB->>DWB: dwb_set_data_on_next_slot(cipher)
  DWB->>FIO: fileio_write(cipher)
  FIO->>DSK: 페이지 기록

거울상은 pgbuf_claim_bcb_for_fix() 안에 있다. 페이지 miss 가 디스크 read 를 트리거하고, 그 직후 in-place 복호화가 일어 난다.

// pgbuf_claim_bcb_for_fix — src/storage/page_buffer.c
if (dwb_read_page (thread_p, vpid, &bufptr->iopage_buffer->iopage,
&success) != NO_ERROR) { ... }
else if (success == true) { /* copied from DWB */ }
else if (fileio_read (thread_p, ..., &bufptr->iopage_buffer->iopage,
vpid->pageid, IO_PAGESIZE) == NULL) { ... }
CAST_IOPGPTR_TO_PGPTR (pgptr, &bufptr->iopage_buffer->iopage);
tde_algo = pgbuf_get_tde_algorithm (pgptr);
if (tde_algo != TDE_ALGORITHM_NONE)
{
if (tde_decrypt_data_page (&bufptr->iopage_buffer->iopage, tde_algo,
pgbuf_is_temporary_volume (vpid->volid),
&bufptr->iopage_buffer->iopage) != NO_ERROR)
{
// ...
}
}

복호화는 in-place 다. tde_decrypt_data_page() 는 같은 bufptr->iopage_buffer->iopage 버퍼에서 읽고 같은 버퍼에 쓴 다. CTR 이 stream cipher 라서, 암호화의 역연산은 같은 키 스 트림과의 두 번째 XOR — 즉 정확히 같은 연산 — 이다.

DWB 는 가장 먼저 조회된다. 이 VPID 의 쓰기가 현재 DWB 에 묶여 있다면 dwb_read_page() 가 DWB 의 사본 (암호문 — DWB 는 암 호화된 페이지를 들고 있다) 을 돌려 준다. 그 후 복호화 후크가 그대로 실행된다. 이것이 DWB 를 TDE 를 투명하게 만들어 준 다.

sequenceDiagram
  participant T as 워커 스레드
  participant PB as 페이지 버퍼
  participant DWB as Double-write 버퍼
  participant FIO as fileio_read
  participant DSK as 데이터 볼륨
  participant TDE as tde_decrypt_data_page

  T->>PB: pgbuf_fix(vpid)
  PB->>DWB: dwb_read_page(vpid)
  alt DWB에 페이지 있음
    DWB-->>PB: cipher (DWB 슬롯에서)
  else DWB에 없음
    PB->>FIO: fileio_read(vpid)
    FIO->>DSK: 페이지 read
    DSK-->>FIO: cipher 바이트
    FIO-->>PB: cipher
  end
  PB->>PB: pgbuf_get_tde_algorithm(pflag)
  alt tde_algo != NONE
    PB->>TDE: decrypt(cipher, algo, is_temp, plain)
    TDE->>TDE: nonce = prv.tde_nonce
    TDE->>TDE: AES-256-CTR 본체 → 평문 (in place)
    TDE-->>PB: 평문 페이지
  end
  PB-->>T: PAGE_PTR

로그 페이지는 이미 존재하는 WAL flush 경로를 그대로 따르며, 후크가 두 군데 추가된다. append 시점 에 페이지가 사용자 데 이터를 포함하는 레코드를 한 개라도 받았다면 logpb_set_tde_algorithm 으로 페이지 자체를 암호화 대상 으 로 표시하고, flush 시점 에 디스크 쓰기 직전에 암호화한다.

append 쪽 결정은 로그 레코드 단위에서 시작해 페이지로 promote 된다. prior_set_tde_encrypted()log_prior_node 에 TDE 관련 플래그를 단다 (이 함수는 tde.hLOG_MAY_CONTAIN_USER_DATA 매크로가 열거하는 모든 heap 및 B-tree user-data 복구 인덱스에 해당하는 레코드 종류를 호 출된다). 페이지가 마무리될 때 logpb_next_append_page()logpb_start_append()log_Gl.append.appending_page_tde_encrypted 를 검사하고 PRM_ID_TDE_DEFAULT_ALGORITHM 값을 새 페이지의 flag 에 stamp 한다.

// logpb_next_append_page — src/transaction/log_page_buffer.c
if (log_Gl.append.appending_page_tde_encrypted)
{
TDE_ALGORITHM tde_algo =
(TDE_ALGORITHM) prm_get_integer_value (PRM_ID_TDE_DEFAULT_ALGORITHM);
logpb_set_tde_algorithm (thread_p, log_Gl.append.log_pgptr, tde_algo);
logpb_set_dirty (thread_p, log_Gl.append.log_pgptr);
}

flush 쪽 암호화는 logpb_writev_append_pages()logpb_write_page_to_disk() 에 산다.

// logpb_writev_append_pages — src/transaction/log_page_buffer.c
log_pgptr = to_flush[i];
if (LOG_IS_PAGE_TDE_ENCRYPTED (log_pgptr))
{
if (tde_encrypt_log_page (log_pgptr,
logpb_get_tde_algorithm (log_pgptr),
enc_pgptr) != NO_ERROR)
{
logpb_set_tde_algorithm (thread_p, log_pgptr, TDE_ALGORITHM_NONE);
// ... raise ER_TDE_ENCRYPTION_LOGPAGE_ERORR_AND_OFF_TDE ...
}
else
{
log_pgptr = enc_pgptr;
}
}

암호화 실패 시의 fallback 은 fail-open 이다 — flag 를 끄 고, 페이지를 평문으로 쓴 뒤, 에러를 raise 한다. 의도는 logpb_write_page_to_disk() 의 주석이 명시한 대로 “한 번 실 패하면 그 후로 페이지가 항상 사용자 데이터를 평문으로 흘리게 된다” 이다 [원문 그대로]. 즉 데이터베이스가 멈추는 것보다 프 라이버시 침해를 받아들이는 쪽을 택한 의도된 trade-off 다. 검 토 대상 후보다.

decrypt-on-read 거울상은 logpb_read_page_from_active_log()logpb_read_page_from_file() 에 있다. 후자에는 흥미로운 비대칭이 있다. archive 에서 읽어 오는 페이지는 이미 평문이 다 (아래 설명대로 archive 로그 파일은 복호화된 내용을 저장한 다) 라서 복호화가 필요 없고, active 로그에서 읽어 오는 페이 지는 자기 flag 를 그대로 들고 있어서 in-place 로 복호화된다.

// logpb_read_page_from_file — src/transaction/log_page_buffer.c
TDE_ALGORITHM tde_algo = logpb_get_tde_algorithm ((LOG_PAGE *) log_pgptr);
if (tde_algo != TDE_ALGORITHM_NONE)
{
if (tde_decrypt_log_page ((LOG_PAGE *) log_pgptr, tde_algo,
(LOG_PAGE *) log_pgptr) != NO_ERROR)
{ ... }
}

archive 를 평문으로 저장한다는 선택은 다소 이례적이다 — archive 된 로그가 유휴 상태에서 암호화되지 않는다 는 의미가 되며, 암호화는 active 로그에 한정된다. 소스 주석은 그 이유를, replication log 에 대해서는 TDE 가 비활성화되어 있기 때문이라 고 적는다 (UNSTABLE_TDE_FOR_REPLICATION_LOG). archive 파일 은 archival 과 replication 에 함께 쓰이는데, replication 쪽은 TDE 상태에 의존해서는 안 된다는 정책이다.

double-write buffer 는 페이지 버퍼가 암호화를 적용한 후의 페이지만을 본다. double_write_buffer.cpp 안에는 tde_* 호 출이 0 건이다. DWB 는 들어온 바이트를 그대로 자기 슬롯에 끼우 고, 그 바이트를 자기 checksum 을 계산하고, 나중 flush 때 두 번 쓴다 (DWB 볼륨에 먼저, 데이터 볼륨에 그 다음). 크래시 복구 시점에는 DWB 볼륨이 데이터 볼륨의 미완료 쓰기를 검 사되고, DWB 의 사본 (여전히 암호문이다) 이 데이터 볼륨에 replay 되며, 그 페이지는 다음 read 에서 페이지 버퍼 코드가 평 소대로 복호화한다.

깔끔한 분리다. DWB 는 torn write 에 신경 쓰고, 내용에는 신경 쓰지 않는다.

tde_initialize() 는 데이터베이스 생성 시점에 한 번 호출된다 (boot_create_volume_dirs). 다음 일을 한다.

  1. tde_make_keys_file_fullname() 으로 마스터 키 파일 경로를 만든다.
  2. tde_create_keys_file() 을 부른다. 이 함수는 O_CREAT | O_RDWR 로 모드 0600 인 파일을 열고, offset 0 에 CUBRID_MAGIC_KEYS 를 쓰고, fsync 한다. create + write 쌍을 둘러싸는 동안 (치명적인 것을 제외한) 시그널이 차단된 다.
  3. tde_create_mk() 로 임의의 MK 를 생성한다 (RAND_bytes(default_mk, 32) + 현재 time(NULL)).
  4. tde_add_mk() 가 키 파일의 슬롯 0 에 그 MK 를 쓴다.
  5. 세 개의 DEK 를 생성한다 (tde_create_dk() × 3, 각각 RAND_bytes(32)).
  6. tde_generate_keyinfo() 가 MK index, SHA-256(MK), 그리고 세 개의 래핑된 DEK 로 TDE_KEYINFO 를 빌드한다.
  7. TDE_KEYINFO blob 을 전용 TDE keyinfo heap 에 insert 한 다.

tde_cipher_initialize() 는 매 서버 재시작마다 boot_restart_servertde_cipher_initialize 경로로 호출된 다 (호출 지점은 boot_sr.c:2324, boot_sr.c:5233).

  1. <db>_keys 를 mount 한다 (사용자 지정 경로 우선, 그 다음 기본 경로).
  2. 매직 헤더를 검증한다.
  3. keyinfo heap 에서 TDE_KEYINFO 를 읽는다.
  4. 키 파일에서 keyinfo.mk_index 위치의 MK 를 읽는다.
  5. 그 MK 를 해시해서 keyinfo.mk_hash 와 비교한다. 불일치이 면 거절한다 — 이것이 잘못된 MK 상황을 잡아 내는 메커니즘이 다.
  6. tde_load_dks() 가 세 DEK 를 풀어 tde_Cipher.data_keys 에 둔다.
  7. tde_Cipher.temp_write_counter = 0 으로 리셋한다.
  8. tde_Cipher.is_loaded = true 로 설정한다.
  9. 키 파일을 dismount 한다. MK 는 자기 일을 끝냈다. 이제 프 로세스 메모리에는 평문 DEK 들만 남는다.

tde_CipherTDE_CIPHER 타입의 단일 전역이다. 세 DEK 와 temp-counter 를 들고 있고, tde.hextern 으로 선언되어 tde.c 에 정의된다. 모든 encrypt / decrypt 호출이 이 전역으로 읽고, 스레드별 사본은 없다.

tde_change_mk() (그리고 admin wrapper 인 xtde_change_mk_without_flockcubrid tde --change-key=N admin 명령으로 노출된다, util_admin.cTDE_CHANGE_KEY_S, util_cs.c:3941 참고) 가 마스터 키를 교체한다. 데이터는 단 한 페이지도 다시 암호화되지 않는다.

  1. 키 파일에서 새 MK 를 index 로 찾는다.
  2. 이미 로드되어 있는 키와 같은 키가 아닌지 확인한다.
  3. 이전 키가 파일에 여전히 존재하는지 확인한다 (이래야 DBA 가 풀 수 없는 키 경로로 데이터베이스를 좌초시키는 사태를 막는다).
  4. mk_index, 새 mk_hash, 그리고 다시 래핑된 DEK 들로 새 TDE_KEYINFO 를 만든다 (메모리 안의 평문 DEK 가 새 MK 로 다시 암호화된다).
  5. heap_update_logical 로 옛 TDE_KEYINFO 레코드를 덮어쓴 다.
  6. heap_flush() 가 필수다. 이게 빠지면 새 keyinfo 가 페 이지 버퍼에만 머문 상태에서 크래시가 일어날 수 있다. 그러 면 메모리상 keyinfo 는 새 MK 를 가리키는데 디스크상 keyinfo 는 옛 MK 를 가리키는 상태로 데이터베이스가 남게 된다. flush 가 끝난 뒤에는 DBA 가 키 파일에서 이전 MK 를 지워도 데이터 베이스가 정상적으로 열림이 보장된다.

이 명령이 회전하지 않는 것은 다음과 같다.

  • DEK 자체 — 같은 바이트 값을 유지한 채 다시 래핑될 뿐이다. 디스크 위 모든 페이지는 이전과 같은 DEK 로 계속 암호화된 상 태로 남는다.
  • 어떤 데이터 페이지도 — 그 페이지를 만든 DEK 가 변하지 않았 으므로 기존 암호문은 그대로 유효하다.

DEK 회전은 그 DEK 를 쓰는 모든 페이지를 다시 암호화해야 한 다. 현재 소스에는 그런 동작이 없다. 향후 작업의 후보다.

암호화 두 경로 모두 OpenSSL 의 EVP_* 인터페이스를 통과한다. 이 인터페이스는 x86 에서 AES-NI 를, ARM 에서는 ARM Crypto Extensions 를 자동으로 선택한다. 핫 루프는 페이지당 EVP_EncryptUpdate 한 번이다 — CTR 모드의 16 KB 페이지라면 16 KB / 16 B = 1024 AES 블록 연산이고, AES-NI 하드웨어에서는 각 블록이 약 1 사이클이다. 최신 하드웨어에서 페이지당 오버헤 드는 마이크로초 미만이다. CTR 모드는 다중 페이지 flush 를 코 어 단위로 자명하게 병렬화할 수 있게 해 주지만, CUBRID 는 이를 활용하지 않는다 — 암호화가 flush 스레드 위에서 inline 으로 동작한다.

비용은 다음에 떨어진다.

  • 페이지 버퍼 miss 로 인한 디스크 read: miss 된 페이지당 복호화 1 회.
  • 버퍼 풀 flush: flush 된 dirty 페이지당 암호화 1 회.
  • WAL flush: 사용자 데이터를 담은 로그 페이지당 암호화 1 회.

버퍼 풀 hit 는 무료다. 워킹 셋이 버퍼 풀에 들어가는 워크로드 는 TDE 비용을 log-flush 경로에서만 본다. 버퍼 풀을 thrash 하 는 OLTP 워크로드는 매 flush 와 매 miss 마다 비용을 지불한다.

graph LR
  subgraph Hot["핫 패스 — 버퍼 풀 hit"]
    Q1[Query] --> H1[pgbuf_fix]
    H1 --> M1[메모리 안의 평문]
  end
  subgraph Miss["콜드 패스 — 버퍼 풀 miss"]
    Q2[Query] --> H2[pgbuf_fix]
    H2 --> R2[fileio_read cipher]
    R2 --> D2[tde_decrypt_data_page]
    D2 --> M2[메모리 안의 평문]
  end
  subgraph Flush["flush 경로 — 암호화"]
    F1[pgbuf_bcb_flush_with_wal] --> E1[tde_encrypt_data_page]
    E1 --> W1[fileio_write cipher]
  end
  subgraph WAL["WAL 경로 — 로그 암호화"]
    L1[logpb_writev_append_pages] --> EL[tde_encrypt_log_page]
    EL --> WL[fileio_write log cipher]
  end

심볼을 서브시스템 단위로 묶는다. 각 심볼의 역할은 두 줄 이하 로 요약하고, 전체 위치 힌트는 이 섹션 끝의 표에 모은다.

  • tde_create_keys_file (tde.c) — 모드 0600 의 O_CREAT|O_RDWR open, CUBRID_MAGIC_KEYS write, fsync, close. create 동안 시그널 차단.
  • tde_validate_keys_file (tde.c) — 매직을 읽어 CUBRID_MAGIC_KEYS 와 비교한다. 재시작 시점, 그리고 키 파 일에서 read 하기 전마다 호출된다.
  • tde_make_keys_file_fullname (tde.c) — 마스터 키 파일 경 로를 결정한다. 데이터베이스 파일과 같은 위치의 <db>_keys 또는 시스템 파라미터가 설정되어 있다면 ${tde_keys_file_path}/<base>_keys.
  • tde_copy_keys_file (tde.c) — 키 파일을 블록 단위로 복사 한다 (cubrid copydb 와 backup 시 사용).
  • tde_add_mk (tde.c) — append 또는 첫 번째 삭제 슬롯 채우 기. TDE_MK_FILE_ITEM_COUNT_MAX = 128 키가 데이터베이스당 상한이다.
  • tde_find_mk (tde.c) — 슬롯으로 seek 해서 item 을 읽고 created_time != -1 임을 검증한다.
  • tde_find_first_mk (tde.c) — 슬롯 0 부터 선형 스캔으로 처음 만나는 valid 항목을 반환한다. DB 생성 시 ER_BO_VOLUME_EXISTS 상황에서 기존 키를 집어드는 데 쓰인 다.
  • tde_delete_mk (tde.c) — created_time 을 -1 로 덮어쓴 다. in-place — 파일이 줄어들지 않는다.
  • tde_dump_mks (tde.c) — admin 스캔. 모든 valid 슬롯의 index 와 생성 시각을 출력한다.
  • tde_create_mk (tde.c) — RAND_bytes(master_key, 32)time(NULL).
  • tde_print_mk (tde.c) — MK 를 hex 로 출력한다. admin 이 사용한다.

Keyinfo (DB 내부의 래핑된 DEK 레코드)

섹션 제목: “Keyinfo (DB 내부의 래핑된 DEK 레코드)”
  • tde_initialize (tde.c) — DB 생성 진입점. 키 파일, MK, 세 DEK, keyinfo 를 만들고 TDE keyinfo heap 에 insert 한다.
  • tde_cipher_initialize (tde.c) — 서버 재시작 진입점. 키 파일 mount, keyinfo fetch, MK 검증, DEK 를 tde_Cipher 로 복호화.
  • tde_get_keyinfo (tde.c) — TDE keyinfo heap 의 단일 레코 드를 heap_first 기반으로 fetch 한다.
  • tde_update_keyinfo (tde.c) — UPDATE_INPLACE_CURRENT_MVCCID 와 함께 heap_update_logical. heap_scancache_check_with_hfid 를 우회하려고 class_oid 를 약간 손본다.
  • tde_generate_keyinfo (tde.c) — TDE_KEYINFO 에 SHA-256(MK) 와 세 개의 암호화된 DEK 를 채운다.
  • tde_change_mk (tde.c) — 새 MK 로 DEK 를 다시 래핑하고 새 keyinfo 를 heap_flush 로 강제 flush 한다. flush 누락 시 후속 크래시가 데이터베이스를 좌초시킬 수 있어서 강제한 다.
  • xtde_get_mk_info (tde.c) — admin RPC. 로드된 keyinfo 의 mk_index, created_time, set_time 을 반환한다.
  • xtde_change_mk_without_flock (tde.c) — admin RPC. fileio_open 으로 키 파일을 읽고 (클라이언트 쪽이 fcntl lock 을 들고 있다), 검증한 뒤 tde_change_mk 를 부른다.
  • tde_load_mk (tde.c) — 주어진 index 로 키 파일에서 MK 를 읽고, 해시해서 keyinfo 의 mk_hash 와 비교한다.
  • tde_validate_mk (tde.c) — 후보 MK 를 SHA-256 해서 저장 된 해시와 memcmp. 상수 시간 (constant-time) 인가? memcmp 라 아니다.
  • tde_make_mk_hash (tde.c) — 32 바이트 MK 에 대한 SHA-256. SHA256_DIGEST_LENGTH == TDE_MASTER_KEY_LENGTH 라 는 정적 단언이 함께 있다는 점에 주의.
  • tde_load_dks (tde.c) — keyinfo 의 세 DEK 를 모두 풀어 tde_Cipher.data_keys 로 옮긴다.
  • tde_create_dk (tde.c) — RAND_bytes(data_key, 32).
  • tde_encrypt_dk (tde.c) — TDE_DK_ALGORITHM (= AES) 과 종류별 유도 nonce 로 DEK 를 MK 로 래핑한다.
  • tde_decrypt_dk (tde.c) — unwrap.
  • tde_dk_nonce (tde.c) — TDE_DATA_KEY_TYPE 별 0/1/2 바 이트 fill 고정 패턴.

페이지 암호화 / 복호화 — 내부

섹션 제목: “페이지 암호화 / 복호화 — 내부”
  • tde_encrypt_internal (tde.c) — 단일 OpenSSL EVP 경로: EVP_CIPHER_CTX_newEVP_EncryptInit_exEncryptUpdateEncryptFinal_exEVP_CIPHER_CTX_free. CTR 이 stream cipher 라서 cipher_len == length 를 assert 한다.
  • tde_decrypt_internal (tde.c) — 거울상.

페이지 암호화 / 복호화 — 공개

섹션 제목: “페이지 암호화 / 복호화 — 공개”
  • tde_encrypt_data_page (tde.c) — 헤더 (32 바이트 FILEIO_PAGE_RESERVED) 와 watermark 를 평문으로 복사하고, nonce (영구는 LSA, 임시는 원자 카운터) 를 유도해서 prv.tde_nonce 에 stamp 하고, 본체를 CTR 로 암호화한다.
  • tde_decrypt_data_page (tde.c) — 거울상. nonce 를 prv.tde_nonce 에서 읽는다.
  • tde_encrypt_log_page (tde.c) — 비슷하지만 offset 이 sizeof(LOG_HDRPAGE) 이고 nonce 가 hdr.logical_pageid 다.
  • tde_decrypt_log_page (tde.c) — 거울상.
  • tde_get_algorithm_name (tde.c) — 로그 라인용 문자열: NONE, AES, ARIA.
  • tde_is_loaded (tde.c) — tde_Cipher.is_loaded 의 getter.

페이지 버퍼 후크 (데이터 페이지)

섹션 제목: “페이지 버퍼 후크 (데이터 페이지)”
  • pgbuf_set_tde_algorithm (page_buffer.c) — pflag 비트 를 쓰고, skip_logging 이 false 라면 (temp 페이지는 true) RVPGBUF_SET_TDE_ALGORITHM 로그 레코드를 emit 한다.
  • pgbuf_get_tde_algorithm (page_buffer.c) — pflag 비트 를 TDE_ALGORITHM_AES / _ARIA / _NONE 으로 다시 읽는 다.
  • pgbuf_rv_set_tde_algorithm (page_buffer.c) — 복구 경로: RVPGBUF_SET_TDE_ALGORITHM 로그 레코드를 페이지에 replay 한다.
  • pgbuf_bcb_flush_with_wal (page_buffer.c) — flush 핫 패 스. DWB 이전에 encrypt-on-flush 후크를 들고 있다.
  • pgbuf_claim_bcb_for_fix (page_buffer.c) — fix 콜드 패스. DWB / fileio_read 이후에 decrypt-on-read 후크를 들고 있다.
  • pgbuf_copy_from_area (page_buffer.c) — 페이지 영역에 직 접 쓰는 특수 경로. TDE_ALGORITHM 인자를 받아서 새 페이지 에 적용한다.

파일 매니저 후크 (파일 단위 입자성)

섹션 제목: “파일 매니저 후크 (파일 단위 입자성)”
  • file_set_tde_algorithm (file_manager.c) — 알고리즘을 FILE_HEADER 에 쓰고 RVFL_FHEAD_SET_TDE_ALGORITHM 로그 레코드를 emit 한다 (temp 는 제외).
  • file_get_tde_algorithm_internal (file_manager.c) — 다시 읽기.
  • file_set_tde_algorithm_internal (file_manager.c) — 로깅 없이 쓰기 (복구가 사용한다).
  • file_get_tde_algorithm (file_manager.c) — 페이지 latch 와 함께 공개 read.
  • file_apply_tde_algorithm (file_manager.c) — 파일 헤더에 set + 모든 사용자 페이지를 walk 해서 pflag 를 stamp 한다. 호출자가 동시 접근이 없음을 보장해야 한다 (PGBUF_UNCONDITIONAL_LATCH 사용).
  • file_alloc (file_manager.c) — 할당 시점에 파일의 알고리 즘을 새 페이지의 pflag 로 복사한다. 그 줄: pgbuf_set_tde_algorithm (thread_p, page_alloc, tde_algo, FILE_IS_TEMPORARY (fhead));.
  • file_rv_set_tde_algorithm (file_manager.c) — 복구 replay.
  • logpb_set_tde_algorithm (log_page_buffer.c) — 로그 페이 지 헤더에 LOG_HDRPAGE_FLAG_ENCRYPTED_* 플래그를 set 한 다.
  • logpb_get_tde_algorithm (log_page_buffer.c) — 다시 읽 기.
  • LOG_IS_PAGE_TDE_ENCRYPTED (log_storage.hpp) — fast 매크 로 검사.
  • prior_set_tde_encrypted (log_append.cpp) — prior node 에 mark. tde_is_loaded() 가 false 면 거절한다.
  • prior_is_tde_encrypted (log_append.cpp) — 다시 읽기.
  • LOG_MAY_CONTAIN_USER_DATA (tde.h) — 페이지에 사용자 데 이터가 있음을 의미하는 복구 인덱스를 열거하는 매크로. 이것 이 prior_set_tde_encrypted 의 트리거다.
  • logpb_next_append_page (log_page_buffer.c) — 새 append 페이지 할당 시 appending_page_tde_encrypted 를 페이지 flag 로 전파.
  • logpb_start_append (log_page_buffer.c) — 페이지의 첫 레 코드에서 같은 동작.
  • logpb_writev_append_pages (log_page_buffer.c) — active 로그의 flush 핫 패스. 각 TDE 표시된 페이지를 스크래치 버퍼 로 암호화한 뒤 write 한다.
  • logpb_write_page_to_disk (log_page_buffer.c) — 단일 페 이지 flush 변형. 같은 암호화 로직.
  • logpb_read_page_from_active_log (log_page_buffer.c) — 일괄 read. 페이지를 순회하며 TDE 표시된 것들을 in-place 로 복호화한다.
  • logpb_read_page_from_file (log_page_buffer.c) — active / archive 분기와 함께 동작하는 단일 페이지 read. archive 페 이지는 이미 평문이다.
  • tde_initialize 호출 지점 — boot_sr.c:5104, boot_create_volume_dirs 안 (DB 생성 흐름).
  • tde_cipher_initialize 호출 지점 — boot_sr.c:2324 (명시 적 MK 경로와 함께 재시작, restoredb 가 사용) 와 boot_sr.c:5233 (기본 MK 경로의 일반 재시작).
  • boot_Db_parm.tde_keyinfo_hfid — TDE keyinfo heap 의 HFID. DB control block 에 영속화된다.
  • TDE_CHANGE_KEY_S (util_admin.c) — cubrid tde --change-key=N 의 짧은 옵션. 서버 측의 xtde_change_mk_without_flock 으로 전달된다.
  • TDE_CHANGE_KEY_L (util_admin.c) — 긴 옵션.
  • tde admin 명령 본체 — util_cs.c:3941 이 옵션을 읽어 서 버에 RPC 한다.
  • TDE_ALGORITHM enum (tde.h) — NONE (0), AES (1), ARIA (2).
  • TDE_DATA_KEY_TYPE enum (tde.h) — PERM, TEMP, LOG.
  • TDE_DATA_KEY_SET (tde.h) — 세 개의 32 바이트 DEK 를 묶 은 struct.
  • TDE_KEYINFO (tde.h) — 디스크 위 keyinfo blob.
  • TDE_MK_FILE_ITEM (tde.h) — 디스크 위 마스터 키 파일 슬 롯.
  • TDE_CIPHER (tde.h) — 메모리 안의 싱글톤.
  • TDE_DATA_PAGE_ENC_OFFSET / _LENGTH (tde.h) — 데이터 페이지의 암호화 영역.
  • TDE_LOG_PAGE_ENC_OFFSET / _LENGTH (tde.h) — 로그 페이 지의 암호화 영역.
  • TDE_DATA_PAGE_NONCE_LENGTH = 16, TDE_LOG_PAGE_NONCE_LENGTH = 16, TDE_DK_NONCE_LENGTH = 16 (tde.h).
  • TDE_MASTER_KEY_LENGTH = 32, TDE_DATA_KEY_LENGTH = 32 (tde.h).
  • TDE_MK_FILE_ITEM_COUNT_MAX = 128 (tde.h).
  • LOG_DBTDE_KEYS_VOLID (log_volids.hpp) — 마스터 키 파일 이 fileio_mount 로 mount 될 때 부여되는 합성 volid.
  • FILEIO_PAGE_FLAG_ENCRYPTED_AES = 0x1, _ARIA = 0x2, _MASK = 0x3 (file_io.h).
  • LOG_HDRPAGE_FLAG_ENCRYPTED_AES = 0x1, _ARIA = 0x2, _MASK = 0x3 (log_storage.hpp).
  • PRM_ID_TDE_DEFAULT_ALGORITHM (system_parameter) — tde_default_algorithm. 로그 암호화에 쓰이는 알고리즘이다 (데이터 페이지는 표 단위로 설정된 파일 헤더의 알고리즘을 따 른다).
  • PRM_ID_TDE_KEYS_FILE_PATH (system_parameter) — tde_keys_file_path. <db>_keys 위치를 선택적으로 override 한다.
  • FILEIO_SUFFIX_KEYS = _keys (file_io.h) — 기본 마스 터 키 파일 접미사.
심볼파일라인
tde_initializesrc/storage/tde.c107
tde_cipher_initializesrc/storage/tde.c233
tde_create_keys_filesrc/storage/tde.c321
tde_validate_keys_filesrc/storage/tde.c370
tde_copy_keys_filesrc/storage/tde.c410
tde_make_keys_file_fullnamesrc/storage/tde.c504
tde_generate_keyinfosrc/storage/tde.c532
tde_get_keyinfosrc/storage/tde.c569
tde_update_keyinfosrc/storage/tde.c607
tde_change_mksrc/storage/tde.c661
tde_load_mksrc/storage/tde.c705
tde_load_dkssrc/storage/tde.c742
tde_validate_mksrc/storage/tde.c774
tde_make_mk_hashsrc/storage/tde.c794
tde_create_dksrc/storage/tde.c814
tde_encrypt_dksrc/storage/tde.c838
tde_decrypt_dksrc/storage/tde.c858
tde_dk_noncesrc/storage/tde.c875
tde_encrypt_data_pagesrc/storage/tde.c908
tde_decrypt_data_pagesrc/storage/tde.c961
tde_encrypt_log_pagesrc/storage/tde.c1009
tde_decrypt_log_pagesrc/storage/tde.c1039
tde_encrypt_internalsrc/storage/tde.c1074
tde_decrypt_internalsrc/storage/tde.c1153
xtde_get_mk_infosrc/storage/tde.c1226
xtde_change_mk_without_flocksrc/storage/tde.c1258
tde_create_mksrc/storage/tde.c1323
tde_add_mksrc/storage/tde.c1363
tde_find_mksrc/storage/tde.c1449
tde_find_first_mksrc/storage/tde.c1516
tde_delete_mksrc/storage/tde.c1581
tde_dump_mkssrc/storage/tde.c1641
tde_get_algorithm_namesrc/storage/tde.c1706
TDE_CIPHER (struct)src/storage/tde.h148
TDE_KEYINFO (struct)src/storage/tde.h160
TDE_MK_FILE_ITEM (struct)src/storage/tde.h92
TDE_DATA_KEY_SET (struct)src/storage/tde.h85
LOG_MAY_CONTAIN_USER_DATA (macro)src/storage/tde.h107
FILEIO_PAGE_FLAG_ENCRYPTED_AESsrc/storage/file_io.h63
FILEIO_PAGE_RESERVED (struct)src/storage/file_io.h165
LOG_HDRPAGE_FLAG_ENCRYPTED_AESsrc/transaction/log_storage.hpp42
LOG_IS_PAGE_TDE_ENCRYPTEDsrc/transaction/log_storage.hpp47
LOG_DBTDE_KEYS_VOLIDsrc/transaction/log_volids.hpp41
pgbuf_set_tde_algorithmsrc/storage/page_buffer.c4880
pgbuf_rv_set_tde_algorithmsrc/storage/page_buffer.c4933
pgbuf_get_tde_algorithmsrc/storage/page_buffer.c4953
pgbuf_claim_bcb_for_fix (decrypt hook)src/storage/page_buffer.c8277
pgbuf_bcb_flush_with_wal (encrypt hook)src/storage/page_buffer.c10532
file_set_tde_algorithmsrc/storage/file_manager.c5823
file_get_tde_algorithmsrc/storage/file_manager.c5929
file_apply_tde_algorithmsrc/storage/file_manager.c6003
file_alloc (TDE stamp)src/storage/file_manager.c5503
prior_set_tde_encryptedsrc/transaction/log_append.cpp1564
prior_is_tde_encryptedsrc/transaction/log_append.cpp1580
logpb_get_tde_algorithmsrc/transaction/log_page_buffer.c11564
logpb_set_tde_algorithmsrc/transaction/log_page_buffer.c11592
logpb_writev_append_pages (encrypt hook)src/transaction/log_page_buffer.c2819
logpb_write_page_to_disk (encrypt hook)src/transaction/log_page_buffer.c2303
logpb_read_page_from_active_log (decrypt)src/transaction/log_page_buffer.c2201
logpb_read_page_from_file (decrypt)src/transaction/log_page_buffer.c2110
tde_initialize call sitesrc/transaction/boot_sr.c5104
tde_cipher_initialize call sitessrc/transaction/boot_sr.c2324, 5233

cubrid-page-buffer-manager.md 와 비교. 페이지 버퍼 문서 는 LRU / AOUT 교체 정책과 pgbuf_bcb_flush_with_wal 의 dirty 플래그 회계는 다루지만, flush 시퀀스 가장 위에 자리한 TDE 후 크는 언급하지 않는다. 암호화 경로는 IO_MAX_PAGE_SIZE 만큼의 스택 정렬 스크래치 버퍼를 잡고, tde_encrypt_data_page 를 통 해 메모리 안의 평문을 그 버퍼로 복사한다 (이 함수는 내부적으 로 FILEIO_PAGE_RESERVED 헤더와 watermark 를 평문으로 memcpy 한 뒤 본체를 CTR 로 암호화한다). 그 이후 DWB 와 fileio_write 가 보는 것은 그 cipher 사본뿐이다. BCB 안의 페 이지는 절대로 수정되지 않는다 — RAM 에는 평문 그대로 남 고, 디스크로 가는 스크래치 사본만 암호화된다. 그래서 버퍼 풀 hit 는 TDE 비용을 하나도 지불하지 않는다.

cubrid-double-write-buffer.md 와 비교. DWB 문서는 내용 비의존성을 주장한다. TDE 관점에서 이 주장은 정확하다. DWB 는 pgbuf_bcb_flush_with_wal 이 만들어 준 이미 암호화된 바이트 만 받고, 그 바이트로 자기 checksum 을 계산한 뒤 두 번 쓴다. 복구 시점에는 DWB 가 (cipher) 사본을 데이터 볼륨에 replay 하 고, 그 페이지의 다음 read 는 pgbuf_claim_bcb_for_fix 의 복 호화 후크를 평소대로 통과한다. 이 점이 의미를 가지는 자리가 정확히 한 군데 있다 — DWB 볼륨 자체는 단위로 암호화되지 않지 만, DWB 슬롯에 들어 있는 모든 페이지는 그 페이지의 암호화된 형태다. DWB 파일을 읽는 공격자는 평문이 아니라 암호문을 본다.

cubrid-log-manager.md 와 비교 (예상). 로그 매니저 문서 가 나오면 다음을 기록해야 한다.

  1. TDE 플래그는 페이지 단위 속성 (LOG_HDRPAGE_FLAG_ENCRYPTED_*) 이지 레코드 단위 속성이 아니다.
  2. 암호화할지의 결정은 레코드 단위에서 일어나서 (사용자 데이 터를 다루는 레코드를 prior_set_tde_encrypted) 그 레 코드를 담은 페이지로 promote 된다. 페이지에 사용자 데이 터 레코드가 하나라도 들어가면 그 페이지 전체가 flush 시에 암호화된다.
  3. archive 로그 파일은 평문 으로 저장된다. active 로그의 TDE 플래그는 페이지가 archive 로 옮겨지기 전에 logpb_archive_active_log 에 의해 클리어된다 (해당 코드 경로는 tde_decrypt_log_page 를 호출하고 archive 에는 평 문 페이지를 emit 한다). replication log 를 TDE 가 현 재 비활성화 상태이고 (UNSTABLE_TDE_FOR_REPLICATION_LOG), archive 파일이 replication 에 다시 쓰이기 때문이다.
  4. flush 경로에서 암호화 실패 시 fail-open 동작 — ER_TDE_ENCRYPTION_LOGPAGE_ERORR_AND_OFF_TDE 를 raise 하 고 플래그를 클리어 — 은 소스 주석에 명시된 알려진 trade-off 다.

cubrid-disk-manager.md 와 비교. 디스크 매니저 문서는 볼 륨 헤더, sector table, file allocation table 을 다룬다. 이 페 이지들은 어느 것도 TDE 하에서 암호화되지 않는다 — 그 페이지 들의 pflag 는 0 인 채로 남는다. TDE 경계는 file_alloc 으 로 할당되는 사용자 페이지에 적용되고, 새 페이지가 파일 헤더에 서 TDE 플래그를 물려받는다. 볼륨 레이아웃은 파일 시스템 단위 공격자에게 여전히 노출된다.

마스터 키 파일 위치. 마스터 키 파일이 데이터베이스와 같 은 호스트 에 있다는 암묵적 가정에 주의한다. 기본값이다 — 데 이터베이스와 colocate 된 <db>_keys. KMS 또는 HSM 통합은 tde_create_keys_file 과 파일 기반의 tde_find_mk / tde_add_mk 를 키 매니저 호출로 바꿔 끼워야 한다. 현재 소스 에는 그런 통합이 없다.

Replication / HA 함의. 헤더가 비활성화된 UNSTABLE_TDE_FOR_REPLICATION_LOG 심볼을 언급한다. 이 상태에 서 HA 복제기는 평문 archive 로그를 받는다 — 즉 TDE 를 켠 primary 를 미러링하는 secondary 는 데이터를 암호화해 저장 (자 기 TDE 를 가진다) 하지만, 수신 하는 WAL 스트림은 네트워크 위에서 암호화되지 않는다. 코드 주석이 명시적으로 짚어 둔 횡단 관심사다. 워크어라운드는 네트워크 단의 암호화 (TLS) 다.

  • HSM / KMS 통합. tde_load_mk, tde_find_mk, tde_add_mk, tde_delete_mk 가 모두 POSIX 파일을 read / write / lseek 로 직접 동작한다. 가장 깔끔한 확 장점은 이 네 함수를 가상화해서 플러그형 backend 로 바꾸는 것이다. 코드 경로의 다른 어느 곳도 마스터 키를 파일 시 스템 의미론을 가정하지 않는다는 점이 유리하다. 오늘은 그런 추상이 존재하지 않는다.
  • 표 단위 re-keying / 온라인 rekey. tde_change_mk 는 DEK 를 다시 래핑하기만 한다. 실제 DEK 회전은 모든 페이지 를 읽고 옛 DEK 로 복호화한 뒤 새 DEK 로 다시 써야 한다 — 알고리즘 플래그를 file_apply_tde_algorithm 이 하는 일과 비슷하지만, 본체를 완전히 다시 암호화하는 형태다. 현재 소스에는 그런 동작이 없으며, 쉽지 않은 설계 문제다. 페이지 LSA 가 nonce 역할을 하기 때문에 LSA 를 바꾸지 않고 in-place 로 다시 암호화하는 것은 CTR 유일성을 깨지 않고서는 불가능하 다.
  • 컬럼 / 행 단위 암호화. TDE 는 페이지 본체 전체를 암호화 한다. 속성 단위 암호화 (SQL Server Always Encrypted 와 비슷 한 형태) 는 클라이언트 측 지원이 필요한데, CUBRID 는 현재 그 표면을 노출하지 않는다. TDE 모듈은 순전히 페이지 단위 다.
  • 상수 시간 MK 비교. tde_validate_mk 는 후보 MK 의 SHA-256 과 저장된 해시를 raw memcmp 로 비교한다. 타이밍 사이드 채널은 작지만 (32 바이트) 0 은 아니다. OpenSSL 의 CRYPTO_memcmp 가 이를 닫아 줄 것이다.
  • archive 로그 암호화. archive 로그에서 TDE 를 끈 것은 평 문 replication 을 허용하기 위한 의식적 선택이다. HA 링크의 암호화가 네트워크 단 (TLS) 에서 이뤄질 것을 전제하면 trade- off 는 받아들일 만하다. 그렇지 않다면 archive 시점에 다시 암호화하는 경로가 필요해진다.
  • 키 파일 슬롯 회수. tde_delete_mk 는 슬롯을 invalid 로 표시하지만 파일을 compact 하지는 않는다. 키 회전이 여러 번 일어나면 파일이 128 슬롯 상한까지 선형으로 자란다. compaction 루틴은 존재하지 않는다.
  • 로그 암호화 실패 시 fail-open. ER_TDE_ENCRYPTION_LOGPAGE_ERORR_AND_OFF_TDE 경로는 암호화 가 실패하면 조용히 평문을 쓴다. 프라이버시 관점에서는 fail-closed (flush 를 거절하고 서버를 멈춘다) 가 더 안전할 수 있다. 현재 선택은 가용성을 우선한다.
  • keyinfo heap 자체에 대한 TDE. TDE_KEYINFO blob 은 CUBRID heap 파일에 저장된다. 그 heap 파일은 TDE 플래그가 붙지 않은 상태여야 한다 (붙으면 순환이 된다 — 풀려면 DEK 가 필요한데 그것이 그 안에 래핑되어 있다). keyinfo HFID 의 파일 헤더가 tde_algo == NONE 임을 점검으로 확인할 가치 가 있다. 방어적 assertion 이 추가되면 좋다.
  • production 빌드의 tde_print_mk. 이 함수는 마스터 키 소재 (material) 를 stdout 으로 출력한다. admin 디버깅용으 로 노출 (tde_dump_mks + print_value) 되지만, admin 이 아닌 컨텍스트에서 우발적으로 호출되는 일을 가드하고 있는지 확인이 필요하다.

소비된 코드 경로:

  • src/storage/tde.h
  • src/storage/tde.c
  • src/storage/page_buffer.c — encrypt-on-flush, decrypt-on-read, pflag accessor, 복구 후크.
  • src/storage/file_io.hFILEIO_PAGE 레이아웃, pflag 상수, watermark / sanity helper.
  • src/storage/file_io.c — TDE 접점이 단 하나 있다 (페이지 포맷 scrub 안의 io_page->prv.tde_nonce = 0;).
  • src/storage/file_manager.h, file_manager.c — 파일 단위 TDE 알고리즘. 페이지 할당이 이를 전파한다.
  • src/storage/double_write_buffer.cpp — 확인됨: TDE 인지 코 드가 하나도 없다. DWB 는 설계상 내용 비의존이다.
  • src/transaction/log_page_buffer.c — flush, read, archive 경계에서의 로그 페이지 encrypt / decrypt 후크.
  • src/transaction/log_storage.hppLOG_HDRPAGE_FLAG_*, LOG_IS_PAGE_TDE_ENCRYPTED.
  • src/transaction/log_append.cpp, log_append.hpp — prior node 의 TDE 플래그 (prior_set_tde_encrypted / prior_is_tde_encrypted).
  • src/transaction/log_volids.hpp — 마스터 키 파일 mount 용 합성 volid.
  • src/transaction/boot_sr.c — TDE 모듈로의 DB 생성 / 재시작 진입점.
  • src/executables/util_admin.c, util_cs.ccubrid tde admin 명령 파싱과 dispatch.

이론적 참고:

  • FIPS 197 (AES 표준) — 블록 cipher.
  • NIST SP 800-38A — 운영 모드. CUBRID 는 CTR 을 쓴다.
  • KS X 1213 (ARIA) — 한국 표준 cipher. AES 와 함께 제공되는 대안 알고리즘.
  • Database Internals (Petrov, 2019) — 스토리지 챕터의 encryption-at-rest 논의.
  • 비교 엔진: Oracle TDE (테이블스페이스별 DEK + Wallet 마스 터), SQL Server TDE (세 단계 SMK / DMK / DEK 계층), MySQL InnoDB TDE (테이블스페이스별 DEK + keyring 플러그인), PostgreSQL TDE (개발 중, 클러스터 단위 DEK 와 선택적 별도 WAL 키).