PDF Library for Delphi는 비밀번호 대신 원시 file encryption key로 암호화된 PDF를 열 수 있습니다. DAOpenFileWithEncryptionKey는 key를 hexadecimal text로 받아 encryption dictionary에 이미 저장된 verifier와 대조한 뒤 read-only Direct Access handle을 반환하며 DAOpenFromStreamWithEncryptionKey는 caller가 소유한 TStream에 대해 같은 작업을 수행합니다. 두 API는 모두 v3.496.0에 추가되었습니다
상황은 좁지만 실제로 발생합니다. 포렌식 조사에서 memory image에서 복구한 key와 비밀번호 없는 파일을 받습니다. 대량 archival 작업에는 원래 DRM system이 몇 년 전부터 비밀번호 발급을 중단해 file key가 escrow database에 들어 있는 문서가 만 개 있습니다. 폐기된 rights-management 제품에서 migration할 때 key material만 있고 나머지는 없을 수도 있습니다. 이 모든 경우 손에 든 credential은 key derivation의 input이 아니라 output이며 API의 어느 password parameter도 이를 받을 수 없습니다
파일 암호화 키가 비밀번호가 아닌 이유
비밀번호와 file encryption key는 PDF standard security handler (ISO 32000-1 §7.6.3)의 key derivation 양쪽에 놓입니다. handler는 password에 /O, /P, file ID와 revision별 hash를 섞어 file key를 만듭니다. file key를 password 자리에 넣으면 다른 nonsense에 hash된 또 다른 nonsense가 나오므로 DAOpenFile의 flag가 아니라 별도 entry point가 필요합니다
raw key를 주입할 수 있는 위치는 revision에 따라 정해져 있습니다. Revision 2부터 4는 file key, object number, generation number와 AESV2의 경우 AES salt로 별도의 per-object key를 계속 만들므로 file key를 가지고 있어도 object-level derivation을 건너뛸 수 없습니다. Revision 5부터 7은 AES-256에 32-byte file key를 직접 사용하며 per-object 단계가 없습니다. 두 계층에 공통인 것은 file key 자체뿐이므로 PDF Library for Delphi가 외부 key를 받는 위치도 여기뿐입니다. 비밀번호가 아직 있다면 일반 경로를 그대로 사용하고 잘못된 첫 시도를 처리하는 password retry lifecycle에 맡기는 편이 좋습니다. raw-key entry point는 password path가 제공하는 편의 기능 일부를 의도적으로 포기하기 때문입니다
DAOpenFileWithEncryptionKey가 받는 input
접두사가 없고 공백이 없으며 길이가 짝수인 ASCII hex만 받으며 decode된 byte 수는 encryption revision과 정확히 일치해야 합니다. Revision 2부터 4에서는 encryption dictionary의 /Length에서 expected length를 얻습니다. 8 bit의 배수이며 5에서 16 byte 사이이고 /Length가 없으면 40 bit가 기본값입니다. Revision 5부터 7은 협상 없이 정확히 32 byte입니다. 0x prefix, 홀수 digit 수, 64 hex character 초과, Options의 알 수 없는 bit, 암호화되지 않은 문서는 모두 같은 결과를 냅니다. handle은 0이고 LastErrorCode는 PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID인 425로 설정됩니다. 이 엄격함이 목적입니다. 공백을 자르고 짧은 input을 zero-pad하는 lenient parser는 잘린 clipboard paste를 credential로 받아들인 뒤 훨씬 읽기 어려운 곳에서 실패하게 만듭니다
Var
Lib: TPDFlib;
FileHandle, PageRef: Integer;
Begin
Lib:= TPDFlib.Create;
Try
// KeyHex는 AES-128 R4에서 32 hex 문자, AES-256 R6에서 64 문자입니다
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If FileHandle= 0 Then
Raise Exception.CreateFmt('raw key refused (error %d)', [Lib.LastErrorCode]);
Try
PageRef:= Lib.DAFindPage(FileHandle, 1);
WriteLn(Lib.DAExtractPageText(FileHandle, PageRef, 0));
Finally
Lib.DACloseFile(FileHandle);
End;
Finally
Lib.Free;
End;
End;
다른 failure code는 구별된 상태로 남아 있으므로 batch job은 operator error와 evidence problem을 구분할 수 있습니다. 파일이 없으면 411, 읽기 위해 열 수 없으면 401, cross-reference structure가 깨졌으면 409입니다. key와 관련된 모든 오류는 의도적으로 425로 합쳐집니다. 어떤 부분의 key가 틀렸는지 알려 주는 raw-key entry point는 oracle이 되기 때문입니다
검증이 실제로 증명하는 것
PDF Library for Delphi는 encryption dictionary가 이미 제공하는 verifier를 사용해 supplied key가 이 문서에 속한다는 것을 증명하며 검사는 revision마다 다릅니다. Revision 2는 32-byte standard padding string의 RC4 encryption을 다시 계산해 32개 byte 모두를 /U와 비교합니다. Revision 3과 4는 padding을 file ID와 함께 hash하고 RC4 pass와 XOR로 파생한 19개 round를 실행한 뒤 /U의 처음 16 byte를 비교합니다. Revision 5부터 7은 zero IV로 16-byte /Perms string을 decrypt하고 네 가지를 동시에 독립적으로 확인합니다. little-endian permission word와 /P의 일치, 5번부터 8번 위치의 네 FF byte, T 또는 F인 encrypt-metadata flag, 10번부터 12번 위치의 adb marker입니다
verifier가 존재하지만 일치하지 않으면 open은 무조건 거부됩니다. 이 기능 전체가 기대는 보장이므로 분명히 말해 둘 필요가 있습니다. 또한 verification이 의미하지 않는 것도 분명합니다. key가 이 파일을 decrypt한다는 뜻이지 누군가가 그 사용을 허가했다는 뜻은 아닙니다. /Perms에서 복구한 permission word는 key에 대한 evidence이지 grant가 아니며 문서가 실제로 무엇을 허용한다고 주장하는지 알고 싶다면 encryption과 permissions audit pass라는 별도 작업이 필요합니다. 비 ASCII AES-256 password의 SASLprep 처리 같은 password-side normalization 문제는 어떤 string도 hash에 들어가지 않으므로 여기서는 발생하지 않습니다
PDF_RAW_KEY_ALLOW_UNVERIFIED가 적용되는 경우
PDF_RAW_KEY_ALLOW_UNVERIFIED는 정확히 한 상황을 다룹니다. /Perms가 없거나 16 byte가 아니거나 /U가 비교하기에 너무 짧아 문서에 usable verifier가 없는 경우입니다. 실패한 evidence를 무시할 수는 없습니다. /Perms 내부의 hex digit 하나를 손상시키고 option을 설정한 채 올바른 key를 넘겨도 PDF Library for Delphi는 여전히 0과 425를 반환합니다. intact verifier에 32 byte의 zero key를 option과 함께 넘겨도 결과는 같습니다. 이 option은 증거의 부재를 완화할 뿐 모순된 증거를 무시하지 않습니다. recovery open과 verified open은 서로 다른 epistemic state이므로 return value에 합치지 않고 별도로 보고하며 DAGetEncryptionKeyValidation은 open handle을 받아 세 constant 중 하나를 반환합니다:
PDF_RAW_KEY_VALIDATION_VERIFIED(1)은 verifier가 있었고 일치했다는 뜻입니다PDF_RAW_KEY_VALIDATION_UNVERIFIED(2)는 verifier를 평가할 수 없었고 caller가 그 policy를 명시적으로 요청했기 때문에만 key가 accept되었다는 뜻입니다PDF_RAW_KEY_VALIDATION_NONE(0)은 일반 password-opened handle이 보고하는 값입니다
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
(Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
// 이 파일에 verifier가 없다면 명시적인 recovery policy로 재시도합니다
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex,
PDF_RAW_KEY_ALLOW_UNVERIFIED);
If FileHandle<> 0 Then
Begin
Case Lib.DAGetEncryptionKeyValidation(FileHandle) Of
PDF_RAW_KEY_VALIDATION_VERIFIED:
Chain.Note('key verified against the encryption dictionary');
PDF_RAW_KEY_VALIDATION_UNVERIFIED:
Chain.Note('no verifier available: extraction is unattested');
End;
End;
구조적으로 read-only이며 stream의 소유자는 누구인가
raw-key file entry는 항상 fmOpenRead or fmShareDenyWrite로 source를 열고 전체 Direct Access chain을 read-only로 표시하므로 DAAppendFile은 in-place write를 거부하고 incremental update를 시도하지 않은 채 2를 반환합니다. 이것은 handle을 설득해 바꿀 수 있는 policy가 아니라 파일을 parse하기도 전에 constructor에서 설정되는 속성입니다. evidence 작업에서 필요한 것은 handle을 닫은 뒤 source byte가 byte-identical하다는 점이며 regression suite는 AES-128 revision 4 fixture와 AES-256 revision 6 fixture 양쪽에서 정확히 이를 assert합니다. stream entry도 write에 대해서는 같은 동작을 하고 한 가지 규칙을 더합니다. DAOpenFromStreamWithEncryptionKey는 소유권을 절대 가져가지 않으므로 DACloseFile 뒤에도 여러분의 TStream은 살아 있고 직접 free해야 합니다
Source:= TMemoryStream.Create;
Try
Source.LoadFromFile(ArchivePath);
Source.Position:= 0;
FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
If FileHandle<> 0 Then
Try
// 2 = read-only handle: 다른 곳으로 export하고 evidence에는 append하지 않습니다
Assert(Lib.DAAppendFile(FileHandle)= 2);
Harvest(Lib, FileHandle);
Finally
Lib.DACloseFile(FileHandle);
End;
Finally
Source.Free; // handle은 이 stream을 소유한 적이 없습니다
End;
key hygiene와 DLL이 export하는 것
chain을 닫으면 file key, password cache와 derived object key를 덮어쓰고 decode된 key는 open 성공 여부와 관계없이 entry point 자체의 Finally block에서 wipe됩니다. 여기에는 실제 debugging 시간을 잡아먹은 미묘한 점이 있습니다. 살아남아야 하는 모든 복사본은 대입하지 않고 SetLength와 Move로 명시적으로 clone합니다. Delphi에서 하나의 AnsiString을 다른 것에 대입하면 copy-on-write 아래 두 이름이 하나의 buffer를 공유하므로 caller 쪽을 wipe하면 crypt handler가 아직 사용하는 key도 zero가 되고 document는 stack trace로는 설명할 수 없는 이유로 garbage로 decrypt됩니다. DLL boundary를 넘는 것은 wide와 ANSI form의 file-based entry point 및 validation status accessor뿐입니다. TStream variant는 Delphi-only로 남는데, flat C ABI로 정직하게 표현할 수 없는 Delphi object lifetime과 reference semantics에 의존하기 때문입니다. recovery tooling이 DLL client라면 key에 적용하는 것과 같은 통제 아래 temporary file에 staging한 뒤 삭제할 계획을 세워야 합니다
hex key는 document password에 적용할 handling rule을 적용하는 credential material로 취급하고 chain of custody가 만드는 log에 validation status를 남기세요. 그러면 나중에 읽는 사람이 verified extraction과 unattested extraction을 구분할 수 있습니다. 포렌식, bulk archival 또는 DRM migration을 위해 Delphi PDF component를 평가한다면 raw-key entry point, read-only guarantee와 encryption audit surface는 모두 하나의 library에 포함되어 있으며 전체 feature list는 PDF Library for Delphi 제품 페이지에서 확인할 수 있습니다