기술 문서

Delphi에서 HotXLS를 사용하여 Agile 암호화된 Excel 파일 읽기

HotXLS는 Excel 2010 및 그 이후의 모든 버전이 기본적으로 적용하는 비밀번호 보호 체계인 Agile 암호화된 Excel 파일을 TXLSXWorkbook.OpenEncrypted 호출 한 번으로 읽을 수 있습니다. 이 컴포넌트는 XML 암호화 디스크립터를 파싱하고, SHA-512 스핀 카운트(spin-count) 해시 체인을 통해 비밀번호로부터 키를 유도하며, 암호화된 검증기(verifier)와 비밀번호를 비교 검증한 다음, 4096바이트 크기의 AES-CBC 세그먼트 단위로 패키지를 복호화합니다. 이 과정에서 Excel 프로그램 설치, COM 호출, 혹은 별도의 외부 암호화 DLL 등은 전혀 필요하지 않습니다

본 문서는 구체적으로 Agile 암호화 체계의 읽기 작업만을 중점적으로 다룹니다. 관련하여 발생하는 두 가지 다른 문제들은 각각 별도의 문서로 제공됩니다. 오래된 BIFF .xls 파일 내부의 레거시 RC4 및 XOR 보호 방식과의 상호 운용성은 ECB 및 RC4 상호 운용성 가이드에서 설명하며, ECMA-376 표준 암호화 방식의 비밀번호 보호 통합 문서를 직접 생성하는 방법은 AES 보호 XLSX 출력 가이드에서 안내합니다. 본 문서에서는 누군가가 비밀번호를 걸어 이미 생성해 둔 파일을 안전하게 여는 과정을 중점적으로 다룹니다

이 문제가 발생하는 시나리오는 문서 자동화 파이프라인을 구축해 본 개발자라면 매우 친숙할 것입니다. 서버 측 업로드 서비스가 통합 문서를 수신하는데, 해당 서버에는 당연히 Excel 프로그램이 설치되어 있지 않으며 앞으로도 설치할 계획이 없습니다. 그러던 어느 날 아침, 한 고객이 지극히 일반적인 .xlsx 파일을 업로드했는데 ZIP 리더 라이브러리가 파손된 파일이라며 거부하는 현상이 발생합니다. 분석해 보니 파일 내용이 압축 파일 규격이 아니었던 것입니다. 고객이 파일에 비밀번호를 설정해 저장했던 것입니다. 이 시점부터 개발자가 구축한 로더 시스템이 오피스 암호화 표준 규격인 [MS-OFFCRYPTO]를 올바르게 인식하지 못하면, 고객 입장에서는 아무런 잘못도 하지 않았는데 파일 로드 오류가 발생하는 사태가 초래됩니다

Excel 파일 내의 Agile 암호화란 무엇인가요?

Agile encryption is the password-protection scheme defined in [MS-OFFCRYPTO] §2.3.4.10 through §2.3.4.15, and it is what Excel 2010 and later write whenever a workbook is saved with a password. The encrypted file is no longer a ZIP package. It is an OLE Compound File Binary (CFB) container holding two streams: EncryptionInfo, which describes how the encryption was performed, and EncryptedPackage, which is the real .xlsx ZIP encrypted as an opaque blob. The CFB signature (D0 CF 11 E0 A1 B1 1A E1) is the same magic that legacy BIFF .xls files carry, which is why a renamed or encrypted file cannot be classified by extension alone

Agile 암호화 규격이 구형 방식들과 구별되는 가장 큰 강점은 EncryptionInfo 스트림이 자가 기술적(self-describing)이라는 것입니다. 주 버전과 부 버전이 모두 4임을 선언하는 8바이트 버전 헤더 뒤에 UTF-8 인코딩의 XML 설명서 본문이 기술되어 있습니다. 이 안에 수록된 keyData 요소는 암호화 알고리즘(AES), 블록 암호 방식(ChainingModeCBC), 해시 방식(SHA512), 키 비트 길이, 블록 크기 및 Base64 형식의 솔트(salt) 데이터를 명시합니다. 또한 비밀번호를 처리하는 keyEncryptor 요소는 개별적인 솔트값과 키 반복 횟수(spinCount), 그리고 Base64로 인코딩된 세 가지 핵심 페이로드(encryptedVerifierHashInput, encryptedVerifierHashValue, encryptedKeyValue)를 포함합니다. 일반적으로 엑셀 프로그램은 10만 회의 스핀 카운트를 지정한 AES-256 방식을 쓰지만 규격상 AES-128이나 AES-192 형식도 사용 가능하도록 설계되어 있으며, HotXLS는 특정 사양을 고정해 가정하지 않고 XML에 선언된 keyBits 수치를 기준으로 유연하게 가동합니다

평문, 표준 및 Agile 규격을 단번에 해독하는 단일 진입점

TXLSXWorkbook.OpenEncrypted는 사용자가 접하게 되는 세 가지 파일 상태(암호화 없는 순수 ZIP, 표준 암호화 방식, Agile 암호화 방식)를 모두 내부적으로 일괄 소화하므로, 업로드 수신 라이브러리 단계에서 파일을 개별 분류하는 번거로운 분기 로직을 둘 필요가 없습니다. 이 메서드는 파일을 접수하면 가장 먼저 포맷 시그니처를 검사합니다. 만일 OLE CFB 매직 넘버가 누락된 평문 파일이라면 일반적인 Open 경로로 처리를 이관하고 전달받은 비밀번호 매개변수는 무시합니다. OLE 컨테이너 형태라면 ECMA-376 표준 암호화 복호화를 시도한 뒤, EncryptionInfo의 버전 정보가 Agile 4.4 형식과 일치하면 Agile 전용 디코드 파이프라인으로 분기 처리합니다. 성공 시 Open 메서드와 동일하게 성공 계약 값인 1을 리턴합니다

var
  Wb: TXLSXWorkbook;
begin
  Wb := TXLSXWorkbook.Create;
  try
    // Works for plain .xlsx, Standard-encrypted and
    // Agile-encrypted files alike
    if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
      Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
  finally
    Wb.Free;
  end;
end;

암호화되지 않은 원본 입력에 대해 일반 처리 경로로 유연하게 복구 이행(fallback)되는 설계는 사용해 보면 생각보다 아주 편리합니다. 일괄 가져오기 배치 작업을 구현할 때 파일의 암호화 여부와 관계없이 무조건 OpenEncrypted를 호출하도록 설계하면 호출 영역의 코드가 단순해집니다. 암호가 없는 일반 파일은 이전과 완전히 동일하게 로드되고, 암호화된 파일은 스트림 복호화 단계를 거친 뒤 메모리 상에서 해제된 가상 스트림 정보 상태로 기존 ZIP 분석 엔진에 무사히 전달됩니다. 테스트하고 유지보수해야 할 코드 경로가 3개에서 1개로 크게 줄어드는 이점이 있습니다

비밀번호를 사용하여 AES 키를 어떻게 파생하나요?

Agile 암호화 방식은 입력된 비밀번호를 가공 없이 그대로 키로 사용하지 않습니다. HotXLS는 먼저 반복 방식의 키 파생 해시 연산을 수행합니다. 1단계로 비밀번호 솔트와 UTF-16LE 인코딩 방식의 입력 비밀번호 문자열을 결합하여 1차 SHA-512 해시 값을 얻습니다. 이후 해당 결과 데이터 앞에 32비트 리틀엔디안 형식의 반복 카운터 수치를 결합하며 총 spinCount 횟수만큼 SHA-512 해시 연산을 직렬로 무한 반복합니다. 엑셀의 기본 설계 값인 10만 회를 기준으로 하면 비밀번호 1회 대조 시마다 총 10만 번의 SHA-512 해시 함수가 연속해서 호출되며, 이것이 바로 이 규격의 핵심 노하우입니다. 이러한 횟수 제한(spin count)은 브루트 포스(무차별 대입) 해킹 시도를 차단하는 필터 역할을 합니다. 올바른 사용자에게는 최초 1회 몇 밀리초의 대기 시간이지만, 수천만 번의 비밀번호를 대조해 내는 무차별 무작위 해킹 공격자에게는 대조 1회당 매번 동일한 지연 시간을 발생시켜 사실상 해킹을 원천 봉쇄합니다

// [MS-OFFCRYPTO] iterated password hash:
//   H(0) = SHA-512(salt + UTF-16LE(password))
//   H(n) = SHA-512(LE32(n - 1) + H(n - 1)), repeated spinCount times
function AgilePasswordHash(const Password: WideString;
  const Salt: TBytes; SpinCount: Integer): TBytes;
var
  buf: TBytes;
  i: Integer;
begin
  Result := XlsSHA512(Concat(Salt, Utf16LEBytes(Password)));
  SetLength(buf, 4 + 64);
  for i := 0 to SpinCount - 1 do
  begin
    PutLE32(buf, 0, i);            // iteration counter, little-endian
    Move(Result[0], buf[4], 64);   // previous digest
    Result := XlsSHA512(buf);
  end;
end;

반복 해시가 완료된 데이터 역시 그 자체로 암호화 키로 쓰이지는 않습니다. 용도별로 고정 정의된 8바이트 블록 키 상수 값을 해당 결과에 덧붙여 다시 1회 추가 SHA-512 해싱 연산을 수행함으로써 3가지 독립적인 파생 키를 도출해 냅니다. 검증기 입력값 복호화용 상수(FE A7 D2 76 3B 4B 9E 79), 검증기 해시 데이터 비교용 상수(D7 AA 0F 6D 30 61 34 4E), 그리고 실제 패키지 비밀 키 복호화용 상수(14 6E 0B E7 AB AC D0 D6)가 사용됩니다. 각각 계산된 SHA-512 결과 데이터를 타겟 글꼴 암호 키 비트 길이에 맞게 잘라내며, 혹시 해시 결과가 필요한 키 크기보다 짧은 극단적인 경우에는 규격 가이드라인[MS-OFFCRYPTO]에 따라 남는 공간을 0x36 값으로 채워 넣습니다. 이 0x36 패딩 규칙은 비밀번호 솔트 데이터를 기반으로 CBC 복호화의 초기화 벡터(IV)를 생성할 때도 동일하게 응용됩니다

비밀번호 검증 및 saltSize 자르기 함정

HotXLS는 실제 데이터인 패키지 본문을 처리하기 전에 XML 명세에 기록된 검증 데이터 쌍을 사용해 비밀번호의 유효성을 먼저 검증합니다. 1차 유도된 키로 encryptedVerifierHashInput을 복호화하고 그 해독 값을 SHA-512로 해싱한 다음, 2차 유도된 키로 encryptedVerifierHashValue를 복호화해 두 결과 데이터를 바이트 수준에서 엄격하게 대조합니다. 이 비교 결과가 일치하지 않으면 잘못된 비밀번호로 판정하여 에러 상태를 조기에 보고합니다. 덕분에 불필요하게 본문 데이터를 잘못된 키로 복호화하느라 리소스를 낭비하지 않으며, 비밀번호 오입력 시 일부 손상된 데이터가 마치 정상인 것처럼 불완전하게 표시되는 원천적인 오류를 철저히 차단합니다

구현할 때 실수하기 쉬운 미묘한 상세 규격이 있습니다. [MS-OFFCRYPTO] §2.3.4.13에서는 검증기 데이터의 정밀 비교 단위를 블록 암호 크기(block size)가 아닌, 키 엔크립터 솔트 길이인 saltSize 바이트로 제한해 규정합니다. AES-CBC 복호화 결과물은 블록 크기에 맞게 패딩 정렬되므로 복호화된 결과물이 16바이트의 배수로 수집되는데, 이를 해싱하기 전에 반드시 saltSize 크기만큼 데이터 뒷부분을 잘라내야 합니다. 통상 엑셀 프로그램은 이 두 값을 모두 16바이트로 일치시켜 생성하므로, 이 자르기 단계를 건너뛰더라도 평범한 테스트 파일들은 신기하게 다 통과하게 됩니다. 그러나 솔트 길이를 다르게 지정해 생성하는 다른 엑셀 라이브러리 저작 파일들을 서버에서 수신하는 순간 파서가 충돌을 일으키며 실패하게 됩니다. HotXLS는 규격서의 문구 그대로 솔트 길이를 대조하여 잘라내도록 정교하게 설계되었으며, 두 수치가 실무에서 같게 매핑되는 것은 단순한 우연의 일치일 뿐 규격 상의 의무 조건이 아님을 정확히 파악해 대처하고 있습니다

EncryptedPackage 스트림은 어떻게 복호화되나요?

실제 데이터 스트림인 EncryptedPackage는 리틀엔디안 방식의 8바이트 파일 평문 크기 정보 헤더로 시작하며, 그 뒤로 4096바이트 크기의 세그먼트 단위 암호화 블록들이 연속해 배치됩니다. HotXLS는 매 세그먼트마다 고유의 초기화 벡터(IV)를 새로 계산해 가며 복호화를 차근차근 진행합니다. 본문을 암호화한 패키지 비밀 키 자체는 비밀번호에서 곧장 도출되지 않습니다. 이는 생성 프로그램이 랜덤 생성한 후 encryptedKeyValue 영역에 암호화해 숨겨둔 것으로, HotXLS는 3차 유도된 키를 사용해 이 키를 복호화해 낸 뒤 keyData에 선언된 키 크기 규격에 맞춰서 확보합니다. 개별 세그먼트의 초기화 벡터(IV)는 keyData 솔트 데이터와 32비트 리틀엔디안 형식의 세그먼트 순서 인덱스를 결합하여 SHA-512 해싱 연산을 수행한 후 블록 크기만큼 획득합니다. 이러한 설계 덕분에 4096바이트 블록을 다른 영역에 구애받지 않고 무작위로 선택해 개별 복호화(random access)할 수 있으며, HotXLS는 메모리 상에서 패키지 전체를 안전하게 복호화한 후 완성된 ZIP 압축 바이트 구조를 일반 XLSX 로더 엔진에 전달해 엑셀 데이터를 로드합니다

선행 선언된 원본 평문 크기 정보 헤더는 마지막 후처리 단계를 조율해 줍니다. AES-CBC 암호화 결과물은 항상 블록 경계 크기에 맞춰지므로 최종 블록 뒷부분에 최대 15바이트의 불필요한 패딩 가비지 데이터가 덧붙여지는데, 복호화 완료 후 맨 앞 헤더의 크기 정보 바이트 크기만큼만 정확히 잘라냄으로써 순수한 .xlsx ZIP 패키지 원본을 복원해 냅니다. HotXLS는 복호화 착수 전에 파일 전체 길이와 헤더에 선언된 크기 정보의 무결성을 먼저 대조하므로, 네트워크 전송 중 파일이 잘렸거나 크기 정보 헤더가 비정상 변조된 파손 문서의 유입을 메모리 초과 충돌 없이 조기에 차단합니다

오류 예외 상세 구분 및 지원 경계

다양한 유형의 실패 오류 상태를 대충 뭉뚱그리지 않고 명확히 분류해 줍니다. 비밀번호 오입력 시에는 내부 검증기 비교 불일치 판정 결과에 근거해 명시적으로 'Wrong Password' 예외를 발생시키므로, 상위 UI 화면에서 사용자에게 비밀번호 재입력 입력을 매끄럽게 유도할 수 있습니다. 반면 Agile XML 설명서 내에 지원 불가능한 암호화 알고리즘이 표기되어 있거나(AES-CBC 외의 타 알고리즘이나 SHA-512 외의 해시 방식 등), 표준 암호화 및 Agile 규격 둘 다 충족하지 않는 예외적인 손상 OLE 복합 문서 구조인 경우에는 'Unsupported Scheme' 전용 예외를 호출합니다. 이 두 오류는 철저히 분리되어 보고되어야 합니다. 암호화 포맷 자체를 분석하지 못하는 호환성 결함 상황에서 사용자에게 비밀번호 재시도를 계속 요구하는 것은 시간 낭비이며, 비밀번호 오류를 포맷 규격 파손 에러로 오진해 기술 지원 리포트를 발행하면 기술 지원 부서가 엉뚱한 원인 분석을 시작해 업무 효율을 크게 떨어뜨리기 때문입니다

function LoadUploadedWorkbook(const FileName: WideString;
  const Password: WideString; Wb: TXLSXWorkbook): Boolean;
begin
  Result := False;
  try
    Result := Wb.OpenEncrypted(FileName, Password) = 1;
  except
    on E: EXlsxEncryptionNotImplemented do
      // Raised for both a wrong password and an unsupported
      // scheme; E.Message states which, so log it verbatim and
      // only offer a password retry for the wrong-password case
      RejectUpload(FileName, E.Message);
  end;
end;

성능 및 지원 범위를 명확히 짚어 둡니다. HotXLS는 Excel 2010부터 최신 Excel 365 버전이 실제로 출력하는 SHA-512 및 AES-CBC 조합 기반의 Agile 암호화 통합 문서를 모든 키 크기에 대해 정상 해독합니다. 그 외의 타 암호나 특수 해시 알고리즘이 지정된 파일은 처리를 기각하며, 인증서 공개 키 기반의 특수 키 보호 방식은 지원하지 않고 일반 비밀번호 입력 보호 방식만을 해독합니다. 참고로 파일 작성(write) 시점에 HotXLS는 현재 Agile 방식이 아닌 표준(Standard) 암호화 형태로 문서를 인코딩하여 출력하므로, 출력 결과물 암호 규격을 민감하게 대조하는 외부 사후 처리 도구를 연계해 운영할 때는 유의할 필요가 있습니다. 관련 세부 사항은 AES 보호 XLSX 출력 파일 작성 기사를 참고하십시오

암호 파생 해시 연산 단계를 단순 오류 예외가 아닌, 파일 포맷 파싱 규칙의 정규 하위 모듈로 정교하게 편입해 구축하면, 비밀번호 걸린 파일 업로드 처리도 더 이상 번거로운 예외 대상이 되지 않습니다. 본 문서에 안내해 드린 단일 복호화 진입점인 OpenEncrypted API와 SHA-512 반복 해시 엔진 및 세그먼트 단위 AES-CBC 복호화 처리 모듈은 모두 Delphi 및 C++Builder용 고유 XLS/XLSX 읽기 및 쓰기 코어인 HotXLS Component에 일체형으로 탑재되어 함께 배포됩니다