Bài viết kỹ thuật

Mở PDF mã hóa bằng raw file key trong Delphi

PDF Library for Delphi có thể mở PDF đã mã hóa bằng raw file encryption key thay vì password. DAOpenFileWithEncryptionKey nhận key dưới dạng text hexadecimal, kiểm tra nó với verifier đã lưu trong encryption dictionary và trả về Direct Access handle chỉ đọc; DAOpenFromStreamWithEncryptionKey làm điều tương tự cho TStream do caller sở hữu. Cả hai xuất hiện từ v3.496.0

Tình huống này hẹp nhưng có thật. Một cuộc điều tra số đưa cho bạn key khôi phục từ memory image nhưng không có password. Một đợt lưu trữ hàng loạt có mười nghìn document với file key nằm trong cơ sở dữ liệu escrow vì hệ thống DRM ban đầu đã ngừng cấp password từ nhiều năm trước. Một cuộc migration khỏi sản phẩm rights-management đã ngừng dùng có key material nhưng không còn gì khác. Trong mọi trường hợp đó, credential bạn có là output của key derivation chứ không phải input, và không tham số password nào trong API có thể nhận nó

Vì sao file encryption key không phải password?

Password và file encryption key nằm ở hai phía đối diện của key derivation trong standard security handler của PDF (ISO 32000-1 §7.6.3). Handler nhận password, trộn nó với /O, /P, file ID và hash theo revision, rồi tạo ra file key. Đưa file key vào vị trí password sẽ cho ra một thứ vô nghĩa rồi hash thành một thứ vô nghĩa khác, vì vậy cần entry point riêng thay vì một flag trên DAOpenFile

Nơi có thể inject raw key được quy định theo revision. Revision 2 đến 4 vẫn derive key riêng cho từng object từ file key, object number, generation number và, với AESV2, AES salt, nên có file key không giúp bạn bỏ qua bước derive cấp object. Revision 5 đến 7 dùng trực tiếp file key 32 byte cho AES-256, không có bước theo object. Lớp chung duy nhất của cả hai là file key, đó là nơi duy nhất PDF Library for Delphi nhận key do bên ngoài cung cấp. Nếu bạn vẫn có password, hãy đi theo đường thông thường và để password retry lifecycle xử lý lần thử đầu sai, vì raw-key entry cố ý bỏ qua vài tiện ích mà password path vẫn giữ

DAOpenFileWithEncryptionKey nhận input nào?

Chỉ nhận hex ASCII không có prefix, không có whitespace và có số ký tự chẵn, đồng thời số byte sau decode phải khớp chính xác với encryption revision. Với revision 2 đến 4, độ dài kỳ vọng lấy từ /Length trong encryption dictionary: bội số của 8 bit nằm trong khoảng 5 đến 16 byte, mặc định là 40 bit khi thiếu /Length. Với revision 5 đến 7, độ dài luôn chính xác 32 byte, không thương lượng. Prefix 0x, số digit lẻ, hơn 64 ký tự hex, bit chưa biết trong Options hoặc document hoàn toàn không mã hóa đều cho cùng kết quả: handle bằng 0 và LastErrorCode bằng PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, tức 425. Sự nghiêm ngặt là có chủ ý. Parser dễ dãi trim whitespace và zero-pad input ngắn sẽ vui vẻ biến một đoạn paste clipboard bị cắt thành credential rồi thất bại ở nơi khó đọc hơn nhiều

Var
  Lib: TPDFlib;
  FileHandle, PageRef: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    // KeyHex có 32 ký tự hex cho AES-128 R4, 64 cho AES-256 R6
    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;

Các failure code khác vẫn tách biệt để batch job phân biệt lỗi operator với vấn đề của evidence: 411 khi file không tồn tại, 401 khi không thể mở để đọc, 409 khi cấu trúc cross-reference bị hỏng. Mọi thứ liên quan đến key đều gộp vào 425 có chủ ý, vì raw-key entry point mà báo phần nào của key sai sẽ trở thành một oracle

Verification thực sự chứng minh điều gì?

PDF Library for Delphi chứng minh key được cung cấp thuộc về document này bằng verifier mà encryption dictionary đã chứa sẵn, và phép kiểm tra thay đổi theo revision. Revision 2 tính lại RC4 encryption của chuỗi padding chuẩn 32 byte rồi so sánh cả 32 byte với /U. Revision 3 và 4 hash padding cùng file ID, chạy pass RC4 cộng 19 vòng XOR-derived rồi so sánh 16 byte đầu của /U. Revision 5 đến 7 giải mã chuỗi /Perms 16 byte với IV bằng 0 và kiểm tra đồng thời bốn điều độc lập: permission word little-endian với /P, bốn byte FF ở vị trí 5 đến 8, cờ encrypt-metadata là T hoặc F và marker adb ở vị trí 10 đến 12

Khi verifier tồn tại nhưng không khớp, open bị từ chối tuyệt đối. Cần nói rõ điều này vì đó là guarantee mà toàn bộ tính năng dựa vào. Cũng cần phân biệt verification không phải là gì: nó nói key giải mã được file này, không nói ai đó đã cấp quyền cho bạn dùng nó. Permission word lấy từ /Perms là bằng chứng về key chứ không phải grant, và nếu muốn biết document thực sự tuyên bố cho phép gì, đó là việc riêng của một pass audit encryption và permissions. Các vấn đề normalize phía password như xử lý SASLprep cho password AES-256 không phải ASCII hoàn toàn không phát sinh ở đây vì không có string nào đi vào hash

Khi nào PDF_RAW_KEY_ALLOW_UNVERIFIED được áp dụng?

PDF_RAW_KEY_ALLOW_UNVERIFIED chỉ bao phủ một tình huống: document không có verifier dùng được vì /Perms bị thiếu hoặc không dài 16 byte, hoặc /U quá ngắn để so sánh. Nó không thể override evidence thất bại. Sửa hỏng một hex digit trong /Perms rồi đưa key đúng cùng option này, PDF Library for Delphi vẫn trả về 0 và 425. Đưa key gồm 32 byte zero cho verifier nguyên vẹn cùng option, kết quả cũng vậy. Option nới lỏng sự vắng mặt của bằng chứng, không bao giờ nới lỏng một mâu thuẫn với bằng chứng. Vì recovery open và verified open là hai trạng thái nhận thức khác nhau, chúng cũng được báo riêng thay vì gộp vào return value, còn DAGetEncryptionKeyValidation nhận open handle và trả lời bằng một trong ba constant:

  • PDF_RAW_KEY_VALIDATION_VERIFIED (1) nghĩa là verifier tồn tại và khớp
  • PDF_RAW_KEY_VALIDATION_UNVERIFIED (2) nghĩa là key chỉ được chấp nhận vì không thể đánh giá verifier và caller đã yêu cầu rõ policy đó
  • PDF_RAW_KEY_VALIDATION_NONE (0) là giá trị một handle mở bằng password thông thường báo cáo
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
   (Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
  // không có verifier trong file này? thử lại theo recovery policy tường minh
  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;

Chỉ đọc theo thiết kế, và ai sở hữu stream

Entry cho file raw-key luôn mở source với fmOpenRead or fmShareDenyWrite và đánh dấu toàn bộ Direct Access chain là chỉ đọc, nên DAAppendFile từ chối ghi tại chỗ và trả về 2 thay vì thử incremental update. Đây không phải policy có thể thuyết phục handle bỏ qua; nó được đặt trong constructor trước cả khi file được parse. Với công việc evidence, điều bạn cần là byte của source vẫn giống hệt sau khi handle đóng, và regression suite kiểm tra đúng điều đó trên cả fixture AES-128 revision 4 lẫn AES-256 revision 6. Stream entry xử lý write tương tự và thêm một quy tắc: DAOpenFromStreamWithEncryptionKey không bao giờ nhận ownership, nên DACloseFile để TStream của bạn còn sống và bạn tự free nó

Source:= TMemoryStream.Create;
Try
  Source.LoadFromFile(ArchivePath);
  Source.Position:= 0;
  FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
  If FileHandle<> 0 Then
  Try
    // 2 = handle chỉ đọc: export nơi khác, không bao giờ append vào evidence
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // handle không bao giờ sở hữu stream này
End;

Vệ sinh key và những gì DLL export

Đóng chain sẽ overwrite file key, password cache và các object key đã derive, còn key đã decode được wipe trong block Finally của entry point dù open thành công hay không. Có một điểm tinh tế khiến việc debug tốn thời gian thật sự: mọi bản copy cần tồn tại lâu hơn đều được clone tường minh bằng SetLength cộng Move thay vì assignment. Assign một AnsiString cho một AnsiString khác trong Delphi khiến hai tên dùng chung một buffer theo copy-on-write, nên wipe phía caller sẽ zero key mà crypt handler vẫn đang dùng, và document sẽ giải mã thành rác vì lý do không stack trace nào giải thích được. Chỉ các entry point theo file đi qua DLL boundary, ở dạng wide và ANSI, cùng accessor trạng thái validation; biến thể TStream chỉ có ở Delphi vì nó phụ thuộc lifetime object và reference semantics của Delphi, những thứ không có biểu diễn trung thực trong C ABI phẳng. Nếu recovery tooling của bạn là DLL client, hãy dự kiến stage ra file tạm và xóa nó theo cùng biện pháp kiểm soát áp dụng cho key

Hãy coi hex key là credential material với quy tắc xử lý như document password, đồng thời lưu validation status trong log chain of custody để người đọc sau này phân biệt extraction đã verify với extraction không có chứng thực. Nếu bạn đang đánh giá một Delphi PDF component cho forensics, lưu trữ hàng loạt hoặc migration DRM, raw-key entry point, guarantee chỉ đọc và bề mặt audit encryption đều nằm trong cùng library, còn toàn bộ feature list có trên trang sản phẩm PDF Library for Delphi