통합 문서를 작성하고 비밀번호로 암호화한 뒤 동료에게 파일을 전달하면 동료는 이를 Excel로 엽니다. Excel은 암호를 요구하고 동료가 암호를 입력하면 문제없이 암호가 입력됩니다. 여기까지는 정상적인 암호화 상태처럼 보입니다. 하지만 그 직후 Excel은 파일이 손상되어 열 수 없다는 오류 대화 상자를 띄우거나, 파일이 열리더라도 셀 내부 글씨들이 전부 깨진 유령 문자로 가득한 빈 화면만 출력됩니다. 비밀번호는 맞게 기입했으나 도면 내용은 완전히 깨진 것입니다. 이것이 Office 파일 암호화 설계 시 직면하는 가장 파악하기 어렵고 당혹스러운 결함 증상이며, 비밀번호 검증용 데이터 블록을 지칭하는 암호 영역과, 실제 워크북 데이터를 가둔 본문 데이터 영역이 격리된 두 가지 개별 방식으로 복호화되기 때문이고, 한쪽이 정상 해독되었다고 해서 본문 복호화 성공을 의미하는 것은 아니기 때문입니다.
소개하는 두 버그 모두 정확히 이러한 증상을 내포하고 있었습니다. 두 경우 모두 자격 검증용 마커는 정상 해독되었지만 정작 본문 해독이 불가능해, 개발자는 엉뚱하게 키 해독이나 비밀번호 유도 공식 부근만 탐색하며 디버깅을 허비하게 만듭니다. 진짜 오류는 데이터 복호화 이후 하위 계층에서 파일 패킷 바이트를 변환 처리하는 과정에 숨어 있었습니다. 하나는 AES 경로이고 하나는 RC4 암호화 모듈 내부에 존재하는 독립적인 결함이지만, 둘 다 절반의 성공이라는 왜곡된 현상을 유발하므로 원인 규명이 대단히 어렵습니다.
비밀번호 통과가 본문 복호화 성공을 보증하지 못하는 연유
현대식 암호화 XLSX 명세가 사용하는 표준 포맷은 ECMA-376 Standard Encryption(표준 암호화 규격)이며, 두 개의 암호화된 데이터 요소를 병렬 배치해 보관합니다. 하나는 EncryptionVerifier(암호 검증 지표)로, 임의의 무작위 값과 그 해시 정보 값을 담아 비밀번호 유도 키로 암호화해 둔 아주 컴팩트한 데이터 블록입니다. 다른 하나는 EncryptedPackage(암호화된 패키지)로, 워크북의 전체 ZIP 컨테이너를 통째로 암호화해 둔 본문 영역입니다. 검증 지표가 본문 영역과 별도로 보관되는 이유는, 대용량 본문 데이터를 전체 암호 해독하는 고부하 연산을 돌리기 전에 암호 비밀번호 매칭 여부를 가볍고 신속하게 확인하기 위함입니다. 검증 지표를 해독해 도출된 무작위 값의 해시와 기존 해시 값이 일치하면 비밀번호 판독을 성공으로 간주합니다.
함정은 검증 지표와 본문 패키지가 개별 메모리 주소 버퍼상에서 각자 다른 드로잉 루프를 거쳐 암호화된다는 사실입니다. 암호 유도 연산식 자체가 정상적이라면, 본문 데이터 가공 처리가 엉망이 되더라도 상단 검증 지표는 무조건 정상 해독됩니다. 키 공식은 맞지만 본문 가공 모듈이 오작동하면, 엑셀 프로그램은 비밀번호 입력에는 승인 반응을 보인 뒤 본문을 열어보려다 구조 손상 경고를 띄웁니다. "비밀번호는 맞는데 파일이 깨짐"이라는 현상이 나타나며, 이로 인해 개발자는 전혀 무결한 비밀번호 유도 공식 부근만 파고들게 됩니다. 레거시 RC4 방식에서도 동일한 분리 메커니즘이 가동되므로, 인덱스 정렬이 엇갈려 본문이 엉망이 되더라도 상단 암호 검증 통과는 정상적으로 수행됩니다.
버그 1: CBC가 아닌 ECB 방식의 AES 암호화 대입 규격
[MS-OFFCRYPTO] §2.3.4.15 표준 명세서에 따르면, 표준 암호화 방식은 본문 패키지를 암호화할 때 AES 대칭키 암호화 알고리즘의 ECB(Electronic Codebook) 블록 운영 방식을 활용하도록 명문화되어 있습니다. 여백 패딩이 덧대진 본문 패킷을 16바이트 블록 크기로 등분하여, 각 블록마다 독립적으로 동일 키 값을 대입해 암호화하는 규격입니다. 블록 간의 연쇄 고리(chaining)가 없고 암호화 개시 초기화 벡터(IV)도 쓰지 않습니다. 현대적 암호 아키텍처 관점에서는 보안성이 낮아 기피되는 ECB 방식을 채택하고 있어 당황스러울 수 있으나, 호환성 통합 설계 시 표준 명세 규격에 반하는 임의 설계를 대입해서는 안 됩니다. Excel이 본문 패키지를 ECB 방식으로 풀어서 해석하기 때문에, 작성자 역시 반드시 ECB 모드로 봉인해 전송해야만 호환 통신이 보장됩니다.
기존 결함은 본문 패키지를 암호화할 때 0값 데이터로 채워진 가상 초기화 벡터를 주입해 CBC 모드로 가동하고 있었습니다. 이 방식이 대단히 위협적이었던 이유는 절반은 비슷하게 동작했기 때문입니다. CBC 모드는 첫 번째 평문 블록 암호화 시 초기화 벡터와 XOR 연산을 거치는데, 초기화 벡터가 전부 0이면 XOR을 해도 데이터 원본이 변하지 않으므로, 첫 블록 암호화 결과물은 ECB 결과와 완벽히 동일하게 배출됩니다. 그러나 두 번째 블록부터는 앞선 암호문 블록 연산 결과를 체인 연쇄 연산하므로, 첫 블록 이후의 모든 데이터가 ECB 방식의 해독 사양과 완전히 어긋나 깨진 파편으로 오독됩니다.
이제 이 오동작 상태가 엑셀 데이터 파일 구조와 맞물리면 다음과 같은 결과가 나타납니다. 엑셀 본문 ZIP 파일 맨 앞부분 영역에는 파일 전체 길이를 정의하는 8바이트 크기의 리틀엔디안 정수 접두사가 주입되므로, 복호화 초기 진입점에서 이 앞머리를 해독해 정상적인 길이 범위임을 우선 판독하게 됩니다. 첫 블록이 우연히 ECB 결과와 부합해 길이 판독 유효성 검사까지 무사통과하므로 정상 파일로 판독을 시작했다가, 두 번째 블록부터 암호문이 유령 데이터로 풀려 프로그램 크래시를 유발하는 것입니다. 해결 대책은 간단합니다. 16바이트 블록들을 어떠한 연쇄 수식 대입 없이 단일 ECB 루프 내에서 가동해 암호화하는 것입니다. 복호화 엔진 내 XlsEncryptStdPackage는 16바이트 단위로 패킹 영역을 전진하며 AESEncryptECB128Block을 호출하며, 이 함수는 상단 검증 지표 암호화 시와 동일한 기본 코어 알고리즘입니다. 소스 주석에도 명시되어 있듯이 0값 IV의 CBC 방식은 첫 블록에만 ECB 해독과 부합하므로, 후속 데이터 유실 방지를 위해 반드시 단독 ECB 모드로 가동해 주어야 합니다.
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create(nil);
try
Book.Open('report.xlsx');
// SaveAsEncrypted serializes the workbook, then runs the
// ECMA-376 Standard Encryption pipeline: AES-128 ECB over the
// package per [MS-OFFCRYPTO] 2.3.4.15. Returns 1 on success.
if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
raise Exception.Create('Encryption failed');
finally
Book.Free;
end;
end;
버그 2: RC4 키 갱신 인덱스 어긋남 오류
과거의 1차원 레거시 .xls 포맷 암호화는 RC4 CryptoAPI 규격을 준수하며, 앞서 설명한 AES와는 사양이 다릅니다. [MS-OFFCRYPTO] §2.3.6 사양에 의하면, 암호화 스트림을 1024바이트 블록 크기 단위로 쪼개고 경계선을 통과할 때마다 사용 대칭키 정보를 새로 갱신(re-key)해야 합니다. 0번, 1번, 2번 등 전진 배치될 블록 일련번호를 기반으로 신규 RC4 해독키를 유도해 내며, 단일 1024바이트 블록 범위 내부에서는 문자열 포인터를 순차 전진하며 바이트 단위로 암호를 해독해 나갑니다. 여기에는 두 가지 변수가 연립해 보장되어야 합니다. 정확히 1024바이트 지점마다 갱신 알고리즘을 가동하고, 블록 내부에서는 오차 없이 순차 복호화 데이터 바이트를 배출해 소비하는 것입니다. RC4는 1차원 스트림 암호(stream cipher) 계열이므로 암호 데이터 바이트가 일렬로 나열되며, n번째 도출될 해독 문자는 이전에 소비해 치워버린 바이트 용량에 절대적으로 종속됩니다. 작성자와 해독자가 정확히 동일한 가상 눈금 주소 위치에서 한 치의 오차도 없이 동일한 데이터 인출을 완수해 내야만 올바른 데이터가 복구됩니다.
이점이 스트림 암호의 구현상 최대 난제입니다. 동기화 보정 장치(resynchronization)가 전혀 없습니다. 해독 과정에서 단 1바이트의 카운터 어긋남이 발생해도, 그 지점 이후의 모든 데이터가 한눈금씩 밀려 완전히 무관한 암호 바이트와 XOR 연산 처리되며 연쇄 오독이 작동합니다. 이 오류는 수정 복구되지 않고 끝단 블록 및 그 후속 블록 전체를 깨뜨려 나갑니다. 기존 버그가 정확히 이 증상에 기인했습니다. 블록을 계측하는 내부 변수가 오동작 예방용 센티널 값인 -1에서 루프를 개시하였고, 스킵 판단 루틴이 이 센티널 상태를 활성 블록 오프셋으로 착각해 작동했습니다. 센티널 오인으로 인해 읽어오지도 않은 1024바이트 분량의 암호문 전체가 가상 소비 처리되어 변수가 꼬이고, 남은 블록 크기가 즉시 음수 상태로 반전되었습니다. 그 결과 해독기는 진짜 데이터 번지 위치보다 한 블록씩 밀린 엉뚱한 위치를 참조하며 해독 연산을 가동해 엑셀 표 전체를 유령 문자로 채운 것입니다. 이 과정에서도 상단 독립 검증 지표는 정상 통과되므로 비밀번호는 승인되고 셀 정보만 유실되는 현상이 나타납니다.
수정된 정렬 제어 논리는 TXLSDecrypterRC4 모듈에 구현되어 있습니다. Skip(스킵) 및 Decrypt(복호화) 루틴이 공유하는 일관된 판독 루프 구조를 개설하여, 문자열 포인터가 실제 REKEY_BLOCK_SIZE(1024) 블록 경계를 통과하는 순간에만 키 갱신 연산이 작동하도록 묶고, 블록 잔여 공간 단위만큼만 안전하게 데이터를 소비시킵니다. MakeKey 키 갱신은 파손된 센티널이 아닌 유효 블록 정수 인덱스값만을 전달받아 구동되며, Skip과 Decrypt가 엑셀 인코더가 기입한 바이트 위치와 한 치의 오차도 없이 위상 정렬(phase-aligned) 상태를 유지하도록 물리 처리 바이트 크기만큼 포인터를 정확히 전진시킵니다. 교훈은 명확합니다. 스트림 암호 환경 하에서 단 1바이트의 처리 누수는 사소한 오차가 아니라 하행 패킷 전체의 완전한 유실로 귀결된다는 점입니다.
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create(nil);
try
// CanReadEncrypted checks the Compound File (OLE2) signature so
// you can branch before attempting a normal Open. OpenEncrypted
// routes plain files to Open and handles the encrypted container.
if Book.CanReadEncrypted('legacy.xls') then
Book.OpenEncrypted('legacy.xls', 'S3cret!')
else
Book.Open('legacy.xls');
// read cells here
finally
Book.Free;
end;
end;
표준 규격과 호환성을 맞추는 1바이트 단위 정밀성
두 버그 모두 공통된 아키텍처 원칙으로 귀결되며, 개발 철학의 뼈대를 바꾸는 내용이므로 곱씹어 볼 필요가 있습니다. 내보낸 파일의 수신 장치(Excel 프로그램)가 설계 변경이 불가능한 기성 하드웨어적 사양인 경우, 암호 알고리즘 모드나 키 갱신 주기 등의 요소는 개발자가 편의대로 최적화하거나 수정할 수 있는 구현 상세(implementation detail)의 범주가 아닙니다. 그것들은 프로토콜 계약 명세의 본체입니다. 엑셀 프로그램은 자신이 설계된 원칙 그대로 본문 복호화에 ECB 모드를 대입할 것이며 정확히 1024바이트 경계마다 키 갱신을 수행할 것입니다. 개발자의 유일한 책무는 이 경직된 해독 규칙 하에서 평문으로 깨끗이 번역될 바이너리 코드를 1바이트 오차 없이 공급해 주는 것입니다. 더 세련되어 보이는 암호 모드 도입, 문제없어 보이는 초기화 벡터 주입, 개발자가 선호하는 초기 오프셋 설정 등은 수신기가 정한 사양과 어긋나는 즉시 결함으로 확정됩니다. 고정 사양과의 호환 통신은 어림짐작으로 통과할 수 없으며, 1바이트 단위 정밀성을 맞추지 못하면 즉각 완전 통신 두절로 귀결됩니다.
이러한 아키텍처 특성 때문에 상단 검증 지표 통과 여부만을 확인하는 스모크 테스트는 대단히 부실합니다. 비밀번호 유도 키 연산이 동작함을 증명할 뿐, 본문 해독 여부에 대해서는 어떠한 힌트도 주지 않기 때문입니다. 암호화 파일이 비밀번호 입력 창을 통과하는 수준만 검사하면 성공 리포트를 띄우겠지만 본문은 열리지 않습니다. 안전한 통합 테스트는 본문 패키지 복호화 결과물인 압축 파일 바이트를 직접 압축 해제해 비교하거나, 저장된 암호화 파일에서 직접 셀 데이터를 역인출해 해독 여부를 바이트 대조 검사하는 방식으로 수행되어야 합니다. 검증 지표는 암호 일치만을 판별하며, 본문 복호화 데이터만이 진짜 암호화 성공을 대변합니다.
암호화된 통합 문서 조회 기록 인터페이스
공개 API 사용 환경은 대단히 단순화되어 있습니다. 암호화된 현대식 워크북을 기록하려면, TXLSXWorkbook 인스턴스에 데이터를 채우고 파일명 및 비밀번호 문자열을 전송하며 SaveAsEncrypted 메서드를 구동하면 되며, 연산이 성공하면 1을 리턴합니다. 조회 시에는 CanReadEncrypted를 호출하여 암호화된 가상 컨테이너 파일인지 감지한 뒤, OpenEncrypted 메서드로 암호를 대입해 열고, 일반 문서인 경우 Open 함수로 우회 진입하도록 구현합니다. 복잡한 ECB 모드 처리 및 1024바이트 키 갱신 알고리즘은 이 인터페이스 하위 단계에 숨겨져 자동으로 구동되며, 개발자는 비밀번호 주입만으로 호환 규격을 즉시 만족할 수 있습니다.
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create(nil);
try
Book.Open('quarterly.xlsx');
Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
// Reopen on the consumer side
Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
finally
Book.Free;
end;
end;
암호화 출력 구조의 기술 규격, EncryptionInfo 스트림 사양, 검증용 블록 레이아웃 정보 등은 AES 암호화 XLSX 구조 해독 안내 문서에서 내용을 확인할 수 있습니다. 별개 주제인 워크시트 자체 암호 보호 및 잠금 기능과 인쇄 서식 매핑 설정과의 연동은, 서식 보호 및 인쇄 레이아웃 상세 가이드를 참고하십시오. 이 모든 암호 연계 기술은 본 블로그의 다른 주제인 수식 해석, 차트 드로잉 API와 함께 Delphi 및 C++Builder용 HotXLS spreadsheet component 라이브러리에 패키징되어 제공됩니다.