Bài viết kỹ thuật

Mã hóa PDF AES-256 trong Delphi: Thiết lập HotPDF và các lỗi thường gặp

Một cờ quyền hạn PDF (permission flag) không phải là một ổ khóa. Đó là một yêu cầu mà tệp đưa ra cho bất cứ thứ gì mở nó, và một trình xem hoàn toàn có thể bỏ qua nó. Chính sự thật đơn giản đó quyết định cách bạn nên suy nghĩ về mọi lựa chọn khác trên trang này. Tính bảo mật thực sự chỉ đến từ một nơi duy nhất: mã hóa AES-256 được khóa bằng một mật khẩu mà người đọc không có. Mọi thứ khác, các ô chọn "không in" và "không sao chép", là chính sách mà phần mềm tuân thủ đồng ý tôn trọng còn phần mềm thù địch thì không. Trộn lẫn hai lớp đó và bạn sẽ phát hành thứ gì đó cảm thấy an toàn trong bản demo nhưng lại rò rỉ ngoài thực tế

HotPDF là một thành phần PDF VCL gốc cho Delphi và C++Builder, và nó phơi bày mô hình bảo vệ ISO 32000 thông qua một tập nhỏ các thuộc tính. Các thuộc tính này dễ đặt. Phần khó là biết thuộc tính nào mang lại cho bạn sự bảo vệ mật mã học và thuộc tính nào chỉ mang lại một gợi ý lịch sự, cũng như đặt đúng thứ tự gán để mã hóa mà bạn yêu cầu thực sự là mã hóa mà bạn nhận được

Hai mật khẩu thực sự hứa hẹn điều gì

Mã hóa PDF định nghĩa hai thông tin xác thực với hai nhiệm vụ khác nhau, và việc gộp chung chúng lại là lỗi thiết kế phổ biến nhất trong mã tạo ra đầu ra được bảo vệ. Mật khẩu người dùng (user password) kiểm soát việc giải mã. Không có nó, hoặc không có mật khẩu chủ sở hữu, một trình đọc tuân thủ không thể tái tạo khóa tệp và nội dung vẫn không thể đọc được về mặt mật mã học. Mật khẩu chủ sở hữu (owner password) thay vào đó kiểm soát các thiết lập quyền hạn: một trình đọc nhận được mật khẩu chủ sở hữu sẽ được cấp toàn quyền truy cập bất kể các cờ hạn chế nói gì

Các bit quyền hạn đứng trên nền tảng yếu hơn. In ấn, trích xuất nội dung, điền biểu mẫu: mỗi thứ là một cờ mà trình xem đọc và chọn tôn trọng (ISO 32000-2 §7.6.4). Mã hóa bảo vệ các byte. Các cờ quyền hạn chỉ hướng dẫn phần mềm tuân thủ, và chúng hướng dẫn sau khi sự việc đã xảy ra. Bất kỳ ai mở tài liệu bằng mật khẩu người dùng đều đã nắm giữ nội dung đã giải mã trong bộ nhớ, nên "không sao chép" và "không in" có ý nghĩa với một trình xem cư xử đúng mực và chẳng có ý nghĩa gì với một kẻ quyết tâm. Hãy xây dựng mô hình đe dọa xoay quanh ranh giới đó. Tính bảo mật nằm trong mật khẩu người dùng. Quyền hạn định hình những gì các trình xem phổ biến cung cấp, và đó là toàn bộ những gì chúng làm

Sơ đồ HotPDF về thông tin xác thực PDF mã hóa, nơi mật khẩu user suy ra khóa tệp và mở khóa giải mã, mật khẩu owner cấp toàn quyền đè lên các cờ quyền, và một dải cảnh báo ghi chú rằng các bit quyền ProtectOptions là yêu cầu chỉ được phần mềm tuân thủ tôn trọng
Mật khẩu người dùng mang tính bảo mật trong khi mật khẩu owner chỉ gỡ bỏ hạn chế; các cờ quyền dẫn dắt viewer tuân thủ và không ràng buộc được bất kỳ kẻ ác ý nào

Thứ tự cấu hình: mọi thứ trước BeginDoc

HotPDF xây dựng dictionary mã hóa và suy ra khóa tệp vào đúng thời điểm BeginDoc chạy. Bất cứ điều gì các thuộc tính bảo vệ đang giữ tại thời điểm đó chính là những gì tài liệu nhận được, và thay đổi chúng sau đó không thay đổi được gì cả. Thuộc tính quan trọng nhất ở đây là CryptKeyLength, thuộc tính chọn lược đồ từ các giá trị THPDFKeyTypek40, k128, aes128, và aes256. Gán nó sau BeginDoc và bạn sẽ không nhận được ngoại lệ nào, không cảnh báo nào, chỉ là một tệp âm thầm giữ nguyên bất cứ thứ gì nó đã bắt đầu với. Kiểu sai lệch âm thầm đó là loại tệ nhất: nó vượt qua mọi bài kiểm tra cục bộ và xuất hiện nhiều tháng sau đó như một phát hiện không tuân thủ trên bàn làm việc của khách hàng

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'statement.pdf';
    Pdf.ActivateProtection := True;
    Pdf.CryptKeyLength := aes256;        // phải được đặt trước BeginDoc
    Pdf.UserPassword := 'open-secret';
    Pdf.OwnerPassword := 'admin-secret';
    Pdf.UseAES256R6 := False;            // R=5: hỗ trợ trình xem rộng nhất
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 720, 0, 'Account statement, June 2026');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Mật khẩu là UTF-8 và bị giới hạn ở 127 byte, đây là giới hạn của ISO 32000-2 cho các lược đồ AES-256. Nếu chính sách mật khẩu của bạn đưa ra các bí mật dài hơn, hãy tự cắt bớt về phía bạn, nơi bạn kiểm soát chính xác điểm cắt rơi vào đâu. Để mặc cho may rủi và thư viện cùng một trình xem nào đó trong tương lai có thể bất đồng về điểm cắt, tạo ra một tệp mở được với bạn nhưng từ chối cùng một mật khẩu đó ở nơi khác

Revision 5 hay revision 6: một boolean, hai hệ sinh thái

UseAES256R6 chọn giữa hai kiểu bắt tay AES-256, và lựa chọn này có hệ quả lớn hơn nhiều so với kiểu boolean của nó gợi ý. Để nó là False và HotPDF ghi revision 5, lược đồ AES-256 xuất hiện như một phần mở rộng cho PDF 1.7 mà khoảng mười lăm năm trình xem có thể mở được. Đặt nó thành True và bạn có revision 6, phương pháp suy ra khóa được củng cố, được chuẩn hóa trong ISO 32000-2 cho PDF 2.0, khắc phục một điểm yếu đã biết trong cách revision 5 xác minh mật khẩu

Vậy revision 6 là câu chuyện tốt hơn về mặt mật mã học. Nó cũng là thứ làm hỏng mọi thứ. Một tệp revision 6 cần một trình xem được xây dựng cho PDF 1.7 Extension Level 3 hoặc PDF 2.0, và rất nhiều phần mềm đang triển khai không phải là cả hai: kho lưu trữ quản lý hồ sơ, trình kết xuất nhúng trong các sản phẩm khác, công cụ nghiệp vụ mà không ai đụng đến trong nhiều năm. Những thứ đó sẽ từ chối tệp hoàn toàn, và chúng sẽ làm điều đó trên máy của khách hàng, không bao giờ trên máy của bạn. Do đó mặc định thực tế là revision 5. Chỉ dùng revision 6 khi một chính sách bảo mật nêu đích danh ISO 32000-2 theo revision, và khi bạn đã thực sự xác nhận mọi bên tiêu thụ đều có thể đọc được nó. Dù chọn theo cách nào, hãy ghi lại bạn đã chọn cái nào và tại sao, vì người tiếp theo chạm vào đoạn mã này sẽ thắc mắc

Các kiểu khóa cũ hơn đáng được nhắc đến một câu để bạn biết mà bỏ qua chúng. THPDFKeyType vẫn liệt kê k40, k128, và aes128, nhưng chúng tồn tại để tái tạo các kho lưu trữ lịch sử, không phải để bảo vệ các kho lưu trữ mới. RC4 40-bit gục ngã trước phần cứng phổ thông, và các lược đồ 128-bit có trước các revision AES-256 mà bất kỳ đánh giá bảo mật hiện tại nào cũng sẽ mong đợi. Đối với một tài liệu bạn đang tạo vào năm 2026, câu hỏi thực sự chỉ là revision 5 hay revision 6; nếu bạn thấy mình đang dùng các kiểu cũ cho một thiết kế mới, có điều gì đó ở thượng nguồn đã sai

Cờ quyền hạn không kèm mật khẩu mở

Thông thường yêu cầu lại là điều ngược lại với bí mật. Bất kỳ ai cũng phải đọc được tài liệu, nhưng in ấn hoặc trích xuất thì lại cần bị hạn chế. Bạn diễn đạt điều đó bằng một mật khẩu người dùng rỗng và một mật khẩu chủ sở hữu không rỗng, cái mà PDF gọi là chế độ mật khẩu mở (open-password mode), và bạn liệt kê các thao tác muốn cho phép trong ProtectOptions

Hai luồng Delphi song song cho thấy việc gán các thuộc tính bảo vệ như CryptKeyLength aes256 trước BeginDoc cho ra tệp AES-256, trong khi cùng phép gán đó sau BeginDoc lại giữ nguyên sơ đồ gốc mà không có exception hay cảnh báo
BeginDoc đóng băng dictionary mã hóa, nên làn đúng thứ tự cho ra revision AES-256 như yêu cầu trong khi làn sai thứ tự lặng lẽ giao scheme mặc định
Pdf.ActivateProtection := True;
Pdf.CryptKeyLength := aes256;
Pdf.UserPassword := '';                      // ai cũng có thể mở tệp
Pdf.OwnerPassword := 'rotate-me-quarterly';  // bảo vệ tập quyền hạn
Pdf.ProtectOptions := [prPrint, prPrint12bit, prExtractContent];
Pdf.BeginDoc;
// ... nội dung trang ...
Pdf.EndDoc;

Tập THPDFProtectOptions ánh xạ vào các bit quyền hạn ISO: prPrintprPrint12bit cho in độ phân giải cao, prInformationCopy cho sao chép và trích xuất tổng quát, prExtractContent cho trích xuất phục vụ công nghệ hỗ trợ, cộng với prModifyStructure, prEditAnnotations, prFillAnnotations, và prAssemble. Hai trong số đó đáng được cảnh báo. Hãy để prExtractContent bật trong gần như mọi hồ sơ bạn xây dựng. Đây là bit mà một trình đọc màn hình cần để tiếp cận văn bản, và tắt nó đi sẽ âm thầm biến một quyết định về quyền hạn thành một lỗi khả năng tiếp cận mà một người khuyết tật gặp phải còn bạn thì không bao giờ thấy. Bẫy khác là prPrint đứng một mình, không có prPrint12bit: một số trình xem phản ứng bằng cách hạ chất lượng in, và người dùng của bạn sẽ báo cáo đó là lỗi hiển thị chứ không phải là thiết lập quyền hạn mà nó thực sự là

Xác minh chỉ mất năm phút và nên nằm trong danh sách kiểm tra phát hành của bạn. Mở một mẫu của từng hồ sơ trong Acrobat, mở Document Properties, và đọc tab Security, tab này nêu rõ thuật toán ("AES 256-bit") và liệt kê từng thao tác được phép một cách chi tiết. Sau đó mở cùng tệp đó trong trình xem cũ nhất mà khách hàng của bạn thực sự đang chạy, không phải trình xem mới nhất trên máy của bạn. Lần mở thứ hai đó là khoản bảo hiểm rẻ để tránh việc một tệp revision 6 trôi qua trót lọt trong quá trình phát triển rồi chết yểu tại một khách hàng chưa từng nâng cấp

Gỡ bỏ bảo vệ khỏi các tệp hiện có

Giải mã chạy ngược lại cùng một mô hình thuộc tính. Tải tài liệu bằng một thông tin xác thực hợp lệ, tắt bảo vệ, và lưu kết quả mà không có nó

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('encrypted.pdf', 'open-secret');
    if PageCount > 0 then
    begin
      Pdf.ActivateProtection := False;   // bỏ mã hóa khi lưu
      Pdf.SaveLoadedDocument('plain.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Con đường đó phân tích toàn bộ tài liệu vào bộ nhớ, điều này ổn đối với các tệp thông thường nhưng lãng phí đối với các tệp khổng lồ. Khi đầu vào lên đến hàng trăm megabyte, DecryptFile là lựa chọn rẻ hơn: nó giải mã trong khi thực hiện một bản sao ở cấp độ tệp, đi theo một đường ghi lại AES-256 trực tiếp bỏ qua việc xây dựng toàn bộ cây đối tượng bất cứ khi nào đầu vào cho phép. Đây là một phần của Direct File API được trình bày trong bài viết đồng hành về xử lý các tệp PDF lớn từ Delphi

Các ràng buộc tương tác với mã hóa

Có hai giới hạn đáng biết trước khi bạn thiết kế xoay quanh mã hóa thay vì sau đó. Giới hạn đầu tiên là sự tuân thủ lưu trữ. ISO 19005 cấm mã hóa trong PDF/A, nên bất kỳ quy trình nào vừa mã hóa một tài liệu vừa tuyên bố tuân thủ PDF/A đều mâu thuẫn ngay từ cấu trúc; HotPDF sẽ không cho phép bạn có cả hai trong một tệp. Khi bạn thực sự cần cả hai, câu trả lời là hai tài liệu riêng biệt: một bản sao được mã hóa để phân phối và một bản sao không mã hóa riêng cho việc lưu trữ

Giới hạn thứ hai khắc nghiệt hơn. Mã hóa PDF không có ký quỹ và không có khôi phục. Mất mật khẩu người dùng trên một tệp R5 hoặc R6 và các lựa chọn của bạn chỉ còn là dò brute force hoặc bỏ cuộc. Vì vậy hãy đối xử với các bí mật của chủ sở hữu và người dùng theo cách bạn đối xử với bất kỳ thông tin xác thực sản xuất nào. Tạo ra chúng, lưu trữ chúng trong một vault, xoay vòng chúng theo lịch trình. Điều duy nhất không bao giờ nên làm là mã hóa cứng chúng như các hằng số trong một unit, nơi chúng đi thẳng vào hệ thống quản lý phiên bản và nằm trong bản sao làm việc của mọi lập trình viên mãi mãi

Có một phản xạ cuối cùng đáng xây dựng. Thay đổi bảo vệ trên một tệp mà bạn không tạo ra sử dụng cùng cơ chế với giải mã, chứ không phải một tính năng riêng biệt: tải nó bằng mật khẩu của nó thông qua LoadFromFile, chỉnh sửa ProtectOptions hoặc các mật khẩu tại chỗ, và ghi nó trở lại bằng SaveLoadedDocument. Nếu bạn có thể giải mã một tệp, bạn có thể đặt lại quyền hạn cho nó, và đoạn mã trông gần như giống hệt ví dụ ở trên

Các thuộc tính bảo vệ được trình bày ở đây là một phần của HotPDF Delphi Component tiêu chuẩn cho Delphi và C++Builder; trang sản phẩm mang đầy đủ tài liệu tham khảo mã hóa, bao gồm cả enum quyền hạn đầy đủ