Một cờ quyền hạn không phải là cơ chế bảo mật. Cái bit ghi "cấm sao chép" nằm cùng trong /Encrypt dictionary với phần mật mã, điều đó khoác cho nó vẻ cưỡng chế mà thực ra nó không có, và ngay khi bạn coi hai thứ đó là một thì bản kiểm toán của bạn bắt đầu cho ra kết luận sai. Câu hỏi duy nhất đáng đặt ra với một tệp PDF không phải là "nó có được mã hóa không". Nó cụ thể hơn và khó hơn: thuật toán nào, security handler bản sửa đổi nào, trong hai mật khẩu thì cái nào đã được đặt, những bit quyền hạn nào được khai báo, và phần nào của tệp thật sự được mã hóa chạm tới. Một tệp có thể được mã hóa về mặt hình thức mà trên thực tế vẫn mở toang. Nó có thể từ chối cho đọc mà vẫn để metadata ở dạng văn bản thuần. Nó có thể khóa việc in bằng một cờ mà mọi trình xem đều được tự do phớt lờ. Kiểm toán một tệp PDF nghĩa là giải quyết tất cả những điểm đó một cách riêng rẽ, và PDF Library for Delphi, engine PDF của losLab dành cho Delphi và C++Builder, phơi bày từng điểm qua cả một API phẳng dùng handle số nguyên lẫn một tầng lớp có kiểu
/Encrypt dictionary thực sự ghi lại những gì
ISO 32000-1 §7.6 định nghĩa bảo mật tài liệu qua một nhúm mục của dictionary, và PDF Library for Delphi phản chiếu chúng một đối một trong record TPDFEncryption. Phiên bản filter V và bản sửa đổi R chọn họ thuật toán. Length mang kích thước khóa. Các bit quyền hạn nằm trong P, còn chuỗi kiểm tra mật khẩu chủ sở hữu và mật khẩu người dùng nằm trong O và U (thêm OE và UE cho AES-256), một cờ EncryptMetadata đi kèm bên cạnh, và ba trường nữa nêu tên các crypt filter áp cho chuỗi, stream và tệp nhúng theo thứ tự
Giá trị của record này là nó không diễn giải hộ bạn điều gì cả. Nó trả về nguyên dictionary thô và để bạn tự rút ra kết luận, và đó đúng là thứ một cuộc kiểm toán cần. Trường hợp văn bản thuần nằm trong tệp đã mã hóa lộ ra ở StringFilterIdentity và StreamFilterIdentity: khi một trong hai là true, dữ liệu tương ứng đi qua filter Identity mà không bị đụng tới, bất kể trạng thái mã hóa của tài liệu báo cáo thế nào. Một bộ quét dừng lại ở mức "có /Encrypt dictionary" sẽ gọi tệp như vậy là được bảo vệ trong khi chuỗi và stream của nó nằm phơi ra. Cùng sắc thái đó chi phối metadata. Khi EncryptMetadata là false thì gói XMP vẫn đọc được với mọi bộ đánh chỉ mục còn nội dung trang thì không, và điều này đáng biết ngay khi các quy tắc định tuyến của bạn dựa vào trường tiêu đề hay tác giả
Một lượt dò bảo mật ngắn bằng API phẳng
Với phần lớn pipeline, bốn lệnh gọi phẳng trả lời được những câu hỏi thường ngày. LoadFromFile trả về 1 khi thành công, và một khi tài liệu đã mở thì các bộ kiểm tra mã hóa báo cáo theo trạng thái đã giải mã của nó:
var
PDF: TPDFlib;
begin
PDF := TPDFlib.Create;
try
if PDF.LoadFromFile('contract.pdf', UserPassword) <> 1 then
raise Exception.Create('Open failed: wrong password or damaged file');
Writeln('status : ', PDF.EncryptionStatus); // đã giải mã / đã mã hóa / không rõ
Writeln('algorithm : ', PDF.EncryptionAlgorithm); // họ RC4 hay họ AES
Writeln('strength : ', PDF.EncryptionStrength); // hạng độ dài khóa
Writeln('owner pw? : ', PDF.CheckPassword(CandidatePassword));
finally
PDF.Free;
end;
end;
CheckPassword quan trọng hơn vẻ ngoài một dòng chữ ký của nó. PDF định nghĩa hai mật khẩu với quyền lực không ngang nhau. Mật khẩu người dùng là thứ bắt buộc để mở được tệp. Mật khẩu chủ sở hữu trao đầy đủ quyền và ghi đè mọi bit quyền hạn. Byte trên đĩa giống hệt nhau trong cả hai trường hợp, nhưng một phiên mở bằng mật khẩu chủ sở hữu làm được những việc mà phiên mở bằng mật khẩu người dùng không làm được, nên một bản kiểm toán không ghi lại thông tin đăng nhập nào đã được xuất trình là bản kiểm toán chỉ ghi được một nửa sự thật. Tầng lớp biến sự phân biệt đó thành thứ truy vấn được. TPDFDocument.HasUserPassword và HasOwnerPassword cho biết tệp đòi hỏi gì, còn IsUserPassword và IsOwnerPassword cho biết mật khẩu nào thực sự đã mở phiên hiện tại. Hãy ghi log sự kiện đó. Đừng bao giờ ghi log chính giá trị mật khẩu
Thang Strength, nơi "AES-256" mang hai nghĩa
Các hàm phẳng Encrypt và EncryptFile nhận một Strength kiểu số nguyên với năm giá trị có ý nghĩa: 0 cho RC4 40-bit, 1 cho RC4 128-bit, 2 cho AES 128-bit đọc được từ Acrobat 7, 3 cho AES 256-bit như Acrobat 9 giới thiệu, và 4 cho AES 256-bit như Acrobat X trở đi đòi hỏi
Điểm thú vị là 3 và 4 đều mang nhãn AES-256 mà lại không cùng một lược đồ. Strength 3 ứng với security handler bản sửa đổi 5, một thiết kế chuyển tiếp mà Acrobat 9 phát hành còn ISO thì chưa bao giờ tiếp nhận. Strength 4 ứng với bản sửa đổi 6, có hàm dẫn xuất khóa được gia cố và chuẩn hóa trong ISO 32000-2. Với một tài liệu bạn tạo ra hôm nay thì không có lý do gì chọn 3 thay vì 4. Với một cuộc kiểm toán thì khoảng cách đó mang tính quyết định: một chính sách ghi "AES-256 theo ISO 32000-2" chỉ được R6 đáp ứng, còn một tệp R5 tự xưng AES-256 sẽ trượt chính sách đó trong khi vẫn qua được một phép kiểm tra độ mạnh ngây thơ. Tầng lớp tách bạch hai thứ đó bằng tên gọi, esAES256Bit cho R5 đối lại esAES256BitAcroX cho R6, và thuộc tính EncryptionAcroX trả lời câu hỏi về bản sửa đổi bằng một giá trị boolean duy nhất
Bit quyền hạn và những dòng chữ nhỏ về độ dài khóa
EncodePermissions đóng gói tám cờ vào con số nguyên mà Encrypt và EncryptFile mong đợi. In, sao chép, sửa đổi và thêm ghi chú làm thành bộ cơ bản; điền trường, sao chép hỗ trợ tiếp cận, lắp ghép và in chất lượng đầy đủ làm thành bộ mở rộng. Dòng chữ nhỏ, mà bản demo mã hóa đi kèm thư viện nói thẳng ra, là bốn cờ mở rộng chỉ có hiệu lực từ độ mạnh 128-bit trở lên. Cờ in chất lượng đầy đủ cũng chịu chung quy tắc: xóa nó đi để ép in ở độ phân giải thấp thì một tài liệu 40-bit sẽ phớt lờ bạn, vì việc hạ cấp đó cũng đòi hỏi mã hóa 128-bit trở lên. Mã hóa một chính sách "chỉ in độ phân giải thấp" vào tệp 40-bit thì mọi trình xem vẫn cứ in ở chất lượng đầy đủ
Câu hỏi sâu hơn là ai cưỡng chế bất kỳ bit nào trong số này, và câu trả lời là chẳng có ai đáng tin cả. Quyền hạn là chỉ dẫn dành cho các trình đọc tuân thủ, không phải hạn chế mật mã. Khóa giải mã vẫn y hệt dù việc sao chép được cho phép hay bị từ chối, nên một bộ quyền hạn khóa chặt chỉ giữ cho những trình xem tử tế tiếp tục tử tế. Một trình đọc chọn phớt lờ các bit đó chẳng gặp trở ngại mật mã nào. Nếu nghĩa vụ là ngăn chặn việc trích xuất chứ không chỉ làm nản lòng, thì tệp cần một mật khẩu người dùng và quy trình cần các kiểm soát ở tầng nghiệp vụ bao quanh nó, còn báo cáo kiểm toán nên nêu rõ mỗi tệp thực sự nằm dưới chế độ nào trong hai chế độ ấy thay vì coi một cờ quyền hạn là cái khóa
Đặt chính sách và chứng minh nó đã ăn
Áp mã hóa lên các tệp sẵn có không đòi hỏi phải nạp chúng vào cây đối tượng. EncryptFile xử lý từ đầu vào ra đầu ra trong một lệnh gọi duy nhất, và vòng kiểm toán mở lại kết quả để xác nhận thứ đã đáp xuống đĩa. Bản demo mã hóa đi kèm cũng theo đúng hình mẫu ghi rồi đọc lại đó:
var
PDF: TPDFlib;
R: Integer;
begin
PDF := TPDFlib.Create;
try
R := PDF.EncryptFile('in.pdf', 'out.pdf', 'owner-secret', 'user-secret', 4,
PDF.EncodePermissions(1, 0, 0, 0, // cho in; cấm sao chép/sửa/ghi chú
0, 0, 0, 1)); // bộ mở rộng: chỉ in chất lượng đầy đủ
if (R = 1) and (PDF.LoadFromFile('out.pdf', 'user-secret') = 1) then
begin
Writeln('algorithm = ', PDF.EncryptionAlgorithm);
Writeln('strength = ', PDF.EncryptionStrength);
Writeln('owner pw accepted: ', PDF.CheckPassword('owner-secret'));
end;
finally
PDF.Free;
end;
end;
Các đội làm việc ở tầng tài liệu nhận được cùng thao tác đó với tập hợp có kiểu thay cho việc đóng gói bit, và cách viết này qua được vòng review mà người đọc đỡ phải nheo mắt hơn nhiều:
if not Doc.Encrypt('owner-secret', 'user-secret', esAES256BitAcroX,
[ppCanPrint], [ppCanPrintFull]) then
raise Exception.Create('Encryption failed');
Dù theo cách nào, bước đọc lại cũng không phải nghi thức tùy chọn. Nó tóm được những sai lầm triển khai mà nếu không thì vài tháng sau mới lộ ra trên máy khách hàng: một bản build thư viện cũ lặng lẽ hạ cấp độ mạnh được yêu cầu, một đường dẫn đầu ra chưa bao giờ được ghi vì thư mục ở chế độ chỉ đọc, một con số quyền hạn có các đối số vào sai thứ tự. Cả ba đều qua được smoke test cục bộ rồi hỏng ngoài thực địa, và việc mở lại đầu ra biến từng cái thành một ngoại lệ bạn thấy ngay trong lượt chạy đã tạo ra tệp. GetEncryptionFingerprint trả về một giá trị gọn mà bạn có thể lưu cùng bản ghi công việc, nhờ đó một lần so sánh về sau có thể cho biết hai đầu ra có cùng cấu hình mã hóa hay không mà không cần mở lại cái nào
Những dương tính giả trong kiểm toán đáng viết mã để phòng
Vài mẫu hình đều đặn đẩy các bộ quét bảo mật tới kết luận sai, và mỗi mẫu đều xuất phát từ việc thu gọn một câu hỏi nhiều phần thành một câu trả lời có hoặc không. Crypt filter Identity là ví dụ rõ nhất. Có /Encrypt dictionary, tệp báo cáo là đã mã hóa, thế mà chuỗi và stream lại chạy qua filter Identity nguyên vẹn, nên nội dung thật vẫn là văn bản thuần. Cách chữa là đọc StringFilterIdentity và StreamFilterIdentity trước khi tuyên bố bất cứ thứ gì được bảo vệ
Sự chia tách ở metadata thì tinh vi hơn. EncryptMetadata có thể lệch với phần còn lại của tài liệu theo cả hai chiều, để lại một tệp đã mã hóa mà gói XMP vẫn đọc được, hoặc ít gặp hơn là ngược lại. Câu "tệp đã được mã hóa" chẳng nói lên điều gì về metadata của nó, và điều đó thành hệ trọng ngay khi một bộ đánh chỉ mục hay một quy tắc định tuyến với tay tới tiêu đề. Tệp nhúng thêm một trục thứ ba: PDF cho phép một crypt filter riêng chỉ dành cho tệp đính kèm, nên đính kèm có thể là phần duy nhất được mã hóa trong một tài liệu vốn để mở, hoặc là phần duy nhất ở dạng thuần trong một tài liệu đã mã hóa. Hãy ghi lại ba phép gán filter thành ba trường riêng cho chuỗi, stream và tệp nhúng, thì không cái bẫy nào trong số này tóm được bạn. Lưu một giá trị boolean duy nhất thì kết luận sai chỉ còn là chuyện thời gian
Gỡ mã hóa, và chọn mã hóa cho tệp mới
Một cuộc kiểm toán thường kết thúc bằng quyết định gỡ bỏ lớp bảo vệ, và phần kỹ thuật không phải trở ngại ở đó. DecryptFile(InputFileName, OutputFileName, Password) ghi ra một bản đã giải mã mà không cần nạp toàn bộ, còn hàm Decrypt trên tài liệu đã nạp làm điều tương tự trong bộ nhớ khi tệp đã mở sẵn. Cả hai đều đòi mật khẩu hợp lệ; không cái nào đi vòng qua phần mật mã. Cửa ải thật sự là chính sách chứ không phải mã nguồn, nên hãy để các quy tắc tiếp nhận của bạn nêu thẳng khi nào được phép gỡ bỏ và ghi lại loại mật khẩu đã cho phép điều đó, bởi bản thân bước kỹ thuật chẳng để lại dấu vết nào
Lựa chọn cho đầu ra mới hẹp hơn năm giá trị Strength gợi ý. Hãy dùng Strength 4, tức AES-256 bản sửa đổi 6, trừ khi bạn buộc phải mở tệp trong các trình xem cũ hơn Acrobat X. Strength 2, tức AES-128, là mức sàn thực dụng cho một đội máy dùng trình xem đã già mà không thể nâng cấp. Các tùy chọn RC4 ở 0 và 1 có mặt để bạn đọc và kiểm toán những kho lưu trữ lịch sử, chứ không phải để bạn tạo ra thứ gì mới bằng chúng; với tay tới chúng trong một thiết kế năm 2026 là dấu hiệu rằng có một yêu cầu ở đầu nguồn đã lỗi thời
Trạng thái mã hóa dẫn thẳng vào các quyết định ký, vì một workbench vừa thẩm định vừa ký tài liệu cần đúng kỷ luật đọc lại mà cuộc kiểm toán này dựa vào. Địa hạt đó được bàn trong bài viết về workbench tuân thủ và ký. Khi một lô chạy EncryptFile trên hàng nghìn tài liệu lớn, hướng dẫn direct-access cho PDF cỡ lớn chỉ ra cách giữ bộ nhớ phẳng suốt lượt chạy. Tài liệu API mã hóa đầy đủ nằm trên trang sản phẩm PDF Library for Delphi