การเข้ารหัสไฟล์ PDF ขนาด 2 GB ฟังดูเหมือนปัญหาของการสตรีมมิ่ง: เปิดไฟล์ ดันข้อมูลสองกิกะไบต์ผ่าน AES-256 แล้วเขียนผลลัพธ์ โมเดลความคิดนี้ผิดในจุดที่เป็นตัวตัดสินงบประมาณด้านประสิทธิภาพทั้งหมด มาตรฐาน ISO 32000-1 §7.6 กำหนดความละเอียดของการเข้ารหัส PDF ไว้ที่ระดับอ็อบเจ็กต์แต่ละตัว — ทุกสตรีมและทุกสตริงจะถูกเข้ารหัสแยกกัน โดยแต่ละอันจะมี initialization vector และ padding เป็นของตัวเอง ไฟล์สแกนเก็บถาวรขนาด 2 GB ที่มี 500,000 อ็อบเจ็กต์ คือการทำงาน CBC ขนาดเล็ก 500,000 ครั้ง ไม่ใช่การผ่านข้อมูลยาวรวดเดียว และในระดับนั้น ต้นทุนคงที่รอบๆ การทำงานแต่ละครั้งมีความสำคัญมากกว่าการคำนวณเลขคณิตของ AES ที่อยู่ภายใน
บทความนี้เกี่ยวกับต้นทุนคงที่นั้น: เวลาถูกใช้ไปกับอะไรเมื่อโค้ด Delphi ใช้ AES-256 กับเอกสารขนาดใหญ่มาก และจะกู้คืนเวลานั้นกลับมาได้อย่างไร สำหรับด้านการตั้งค่า — รหัสผ่าน, แฟล็กสิทธิ์การใช้งาน, การพิจารณาความเข้ากันได้ระหว่างการแก้ไข 5 และ 6 — โปรดดูบทความที่เกี่ยวข้องเกี่ยวกับการ กำหนดค่าการเข้ารหัส AES-256 ใน HotPDF; ซึ่งจะไม่มีการนำมากล่าวซ้ำในที่นี้
การทำงาน CBC ครึ่งล้านครั้ง ไม่ใช่การทำงานรวดเดียว
โครงร่างของไฟล์จะยังคงเป็นข้อความธรรมดา (plaintext) ตาราง cross-reference หมายเลขอ็อบเจ็กต์ คีย์พจนานุกรม แผนผังหน้า: ไม่มีสิ่งใดในกลุ่มนี้ถูกเข้ารหัสเลย ซึ่งทำให้โปรแกรมอ่านสามารถระบุตำแหน่งของอ็อบเจ็กต์ได้ก่อนที่จะต้องตรวจสอบความถูกต้องของรหัสผ่าน สิ่งที่มาตรฐานเข้ารหัสคือเนื้อหา — ข้อมูลสตรีม เช่น คำอธิบายหน้า รูปภาพ แบบอักษร และไฟล์แนบ รวมไปถึงสตริง เช่น ค่า metadata และข้อความคำอธิบายประกอบ ภายใต้ตัวกรองการเข้ารหัส (crypt filter) แบบ AES-256 แต่ละส่วนจะถูกประมวลผลด้วยตัวเอง: โดยใช้ IV ขนาด 16 ไบต์แบบสุ่มใหม่, ใช้ CBC กับไบต์ต่างๆ, บล็อก padding ให้ลงตัวที่ขอบเขต 16 ไบต์, และเขียน IV แบบชัดเจนไว้ข้างหน้า ciphertext (ข้อความที่เข้ารหัสแล้ว)
มีผลลัพธ์สองประการตามมา ประการแรก ciphertext จะยาวกว่า plaintext เสมอ: IV เพิ่มมา 16 ไบต์และ padding เพิ่มอีก 1 ถึง 16 ไบต์ ดังนั้นสตริงขนาด 100 ไบต์จะใช้พื้นที่บนดิสก์ 128 ไบต์ และสตรีมที่ว่างเปล่าก็ยังสร้างข้อมูลขนาด 32 ไบต์ โค้ดที่กำหนดขนาดของบัฟเฟอร์เอาต์พุตเท่ากับความยาวของอินพุต หรือเขียนข้อมูลกลับเท่ากับจำนวนไบต์ที่อ่านมา จะสร้างไฟล์ที่ล้มเหลวในการถอดรหัสที่บล็อกสุดท้ายของทุกอ็อบเจ็กต์ ประการที่สอง ต้นทุนจะแปรผันตามจำนวนอ็อบเจ็กต์ ไม่ใช่แค่จำนวนไบต์ ไฟล์สแกนที่เก็บถาวรจะกระจุกตัวของไบต์ในสตรีมรูปภาพขนาดใหญ่เพียงไม่กี่สตรีม แต่มีสตรีมสั้นๆ และสตริงขนาดเล็กอีกนับแสนรายการที่ overhead ต่อการทำงาน เป็นส่วนที่สร้างภาระ ไม่ใช่ตัว AES
ความเมตตาประการหนึ่งในการออกแบบ AES-256 คือการจัดการคีย์ ตัวจัดการความปลอดภัย (Security handlers) จนถึงการแก้ไขที่ 4 (revision 4) จะสร้างคีย์ที่แตกต่างกันสำหรับแต่ละอ็อบเจ็กต์โดยการแฮชคีย์ไฟล์ร่วมกับหมายเลขอ็อบเจ็กต์และหมายเลขรุ่น บังคับให้สร้าง key schedule ใหม่ในแต่ละครั้ง โครงร่างแบบ /V 5 ได้ยกเลิกการสืบทอดคีย์แบบต่ออ็อบเจ็กต์ (per-object derivation) ไปแล้ว: คีย์ไฟล์แบบสุ่มขนาด 256 บิตหนึ่งคีย์จะเข้ารหัสทุกอ็อบเจ็กต์ในเอกสาร ความจริงข้อนี้ทำให้การปรับแต่ง (optimization) ทุกอย่างด้านล่างนี้สามารถทำได้ — สถานะการเข้ารหัสที่มีราคาแพงสามารถสร้างขึ้นได้เพียงครั้งเดียวต่อไฟล์ ไม่ใช่ครั้งเดียวต่ออ็อบเจ็กต์
พจนานุกรม R6 /Encrypt: เปิดหนึ่งครั้งอย่างช้าๆ, อ็อบเจ็กต์ราคาถูก
เอกสาร revision 6 จะประกาศโครงร่างของมันในพจนานุกรม /Encrypt ของ trailer และรายการที่สำคัญจะรวมอยู่ในไม่กี่บรรทัด:
/Filter /Standard
/V 5 /R 6 /Length 256
/CF << /StdCF << /CFM /AESV3 /Length 32 /AuthEvent /DocOpen >> >>
/StmF /StdCF /StrF /StdCF
/O ...48 bytes... /U ...48 bytes...
/OE ...32 bytes... /UE ...32 bytes...
/Perms ...16 bytes... /P -3904 /EncryptMetadata true
/V 5 เลือกสถาปัตยกรรมคีย์ 256 บิต และ /R 6 เลือกกระบวนการ handshake ที่เข้มงวดของ ISO 32000-2 /CF กำหนดตัวกรองการเข้ารหัสที่ระบุชื่อ — /AESV3 หมายถึง AES-256 ในโหมด CBC ที่มี IV อยู่ข้างหน้า — และ /StmF และ /StrF กำหนดตัวกรองนั้นให้กับสตรีมและสตริงตามลำดับ /O, /U, /OE, และ /UE เก็บข้อมูลการตรวจสอบรหัสผ่านและวัสดุในการห่อหุ้มคีย์ (key-wrapping material) และ /Perms เก็บสำเนาของแฟล็กสิทธิ์ที่ถูกเข้ารหัสด้วย AES เพื่อไม่ให้เครื่องมือแก้ไขที่เป็นอันตรายสามารถแอบสลับเปลี่ยนค่า /P ได้เงียบๆ
โครงสร้างต้นทุนซ่อนอยู่ใน /OE และ /UE การแกะคีย์ไฟล์จากมันจะต้องรัน Algorithm 2.B ซึ่งเป็นฟังก์ชัน iterated key-derivation ที่เชื่อมโยงรอบการทำงานของ SHA-256, SHA-384, และ SHA-512 — อย่างน้อย 64 รอบ โดยมีกฎการหยุดที่ขึ้นอยู่กับข้อมูล — ถูกสร้างขึ้นมาให้ช้าโดยเจตนา เพื่อให้การเดารหัสผ่านมีต้นทุนสูง ราคาที่ต้องจ่ายนั้นจะเกิดขึ้นหนึ่งครั้งเมื่อตัวเขียนสร้างไฟล์ และอีกหนึ่งครั้งเมื่อโปรแกรมอ่านเปิดมันขึ้นมา ซึ่งใช้เวลาเพียงไม่กี่มิลลิวินาทีสำหรับแต่ละครั้ง สำหรับไฟล์ที่มีอ็อบเจ็กต์ถึงครึ่งล้าน KDF แทบจะไม่มีนัยสำคัญ และหากการบันทึกทำได้ช้า Algorithm 2.B ก็ไม่ใช่ผู้ต้องสงสัย แต่ลูป per-object ต่างหากล่ะที่เป็นตัวการ
ใช้ key handle ซ้ำ, ใช้ scratch buffer ซ้ำ
การนำไปใช้งานที่เรียบง่ายเกินไป (naive implementation) คือฟังก์ชันช่วยเหลือ (utility function) ที่ดูเป็นระเบียบเรียบร้อย: เครื่องมือช่วยอย่าง EncryptAes256Cbc ที่เปิด Windows CNG provider, เลือก CBC, สร้างอ็อบเจ็กต์คีย์, เข้ารหัสหนึ่งบัฟเฟอร์, และทำลายทุกอย่างทิ้ง มันถูกต้อง สามารถทดสอบระดับหน่วยได้ และจะกลายเป็นหายนะเมื่ออยู่ภายในลูป 500,000 รอบ เอกสารของ Microsoft ระบุว่า BCryptOpenAlgorithmProvider ใช้ทรัพยากรสูงและแนะนำให้แคช handle ไว้ ส่วน BCryptGenerateSymmetricKey จะรัน AES key schedule แบบเต็มและจัดสรร provider state — ซึ่งเป็นการสูญเปล่าล้วนๆ เมื่อคีย์ไม่มีวันเปลี่ยนแปลงตลอดทั้งเอกสาร
Delphi RTL ไม่ได้มี bcrypt import unit มาให้ด้วย ดังนั้นให้ประกาศ entry point โดยตรง คลาสที่อยู่ด้านล่างจะสร้างสถานะการเข้ารหัสทั้งหมดเพียงครั้งเดียว จากนั้นเข้ารหัสอ็อบเจ็กต์จำนวนเท่าใดก็ได้โดยไม่ต้องมีการจัดสรรหน่วยความจำเพิ่มเติม (steady-state allocation):
uses
Winapi.Windows, System.SysUtils, System.Classes;
const
BCRYPT_AES_ALGORITHM = 'AES';
BCRYPT_CHAINING_MODE = 'ChainingMode';
BCRYPT_CHAIN_MODE_CBC = 'ChainingModeCBC';
BCRYPT_OBJECT_LENGTH = 'ObjectLength';
BCRYPT_BLOCK_PADDING = $00000001;
BCRYPT_USE_SYSTEM_PREFERRED_RNG = $00000002;
type
NTSTATUS = Integer;
BCRYPT_HANDLE = Pointer;
function BCryptOpenAlgorithmProvider(out hAlg: BCRYPT_HANDLE; AlgId,
Impl: PWideChar; Flags: ULONG): NTSTATUS; stdcall; external 'bcrypt.dll';
function BCryptCloseAlgorithmProvider(hAlg: BCRYPT_HANDLE;
Flags: ULONG): NTSTATUS; stdcall; external 'bcrypt.dll';
function BCryptSetProperty(hObj: BCRYPT_HANDLE; Prop: PWideChar; Input: PByte;
cbInput, Flags: ULONG): NTSTATUS; stdcall; external 'bcrypt.dll';
function BCryptGetProperty(hObj: BCRYPT_HANDLE; Prop: PWideChar; Output: PByte;
cbOutput: ULONG; out cbResult: ULONG; Flags: ULONG): NTSTATUS; stdcall;
external 'bcrypt.dll';
function BCryptGenerateSymmetricKey(hAlg: BCRYPT_HANDLE;
out hKey: BCRYPT_HANDLE; KeyObj: PByte; cbKeyObj: ULONG; Secret: PByte;
cbSecret: ULONG; Flags: ULONG): NTSTATUS; stdcall; external 'bcrypt.dll';
function BCryptDestroyKey(hKey: BCRYPT_HANDLE): NTSTATUS; stdcall;
external 'bcrypt.dll';
function BCryptEncrypt(hKey: BCRYPT_HANDLE; Input: PByte; cbInput: ULONG;
Padding: Pointer; IV: PByte; cbIV: ULONG; Output: PByte; cbOutput: ULONG;
out cbResult: ULONG; Flags: ULONG): NTSTATUS; stdcall; external 'bcrypt.dll';
function BCryptGenRandom(hAlg: BCRYPT_HANDLE; Buffer: PByte;
cbBuffer, Flags: ULONG): NTSTATUS; stdcall; external 'bcrypt.dll';
procedure CngCheck(Status: NTSTATUS; const Api: string);
begin
if Status <> 0 then
raise Exception.CreateFmt('%s failed, NTSTATUS 0x%.8x',
[Api, Cardinal(Status)]);
end;
type
TPdfObjectEncryptor = class
private
FAlg: BCRYPT_HANDLE;
FKey: BCRYPT_HANDLE;
FKeyObject: TBytes; // CNG key-object workspace, allocated once
FScratch: TBytes; // ciphertext scratch, grows and then stays
public
constructor Create(const FileKey: TBytes);
destructor Destroy; override;
procedure EncryptObject(const Plain: TBytes; Dest: TStream);
end;
constructor TPdfObjectEncryptor.Create(const FileKey: TBytes);
var
Mode: string;
ObjLen, Got: ULONG;
begin
inherited Create;
if Length(FileKey) <> 32 then
raise Exception.Create('AES-256 file key must be 32 bytes');
CngCheck(BCryptOpenAlgorithmProvider(FAlg, BCRYPT_AES_ALGORITHM, nil, 0),
'BCryptOpenAlgorithmProvider');
Mode := BCRYPT_CHAIN_MODE_CBC;
CngCheck(BCryptSetProperty(FAlg, BCRYPT_CHAINING_MODE,
PByte(PWideChar(Mode)), (Length(Mode) + 1) * SizeOf(WideChar), 0),
'BCryptSetProperty');
CngCheck(BCryptGetProperty(FAlg, BCRYPT_OBJECT_LENGTH, PByte(@ObjLen),
SizeOf(ObjLen), Got, 0), 'BCryptGetProperty');
SetLength(FKeyObject, ObjLen);
// The AES key schedule is built once here and reused for every object
CngCheck(BCryptGenerateSymmetricKey(FAlg, FKey, PByte(FKeyObject), ObjLen,
PByte(FileKey), 32, 0), 'BCryptGenerateSymmetricKey');
end;
destructor TPdfObjectEncryptor.Destroy;
begin
if FKey <> nil then
BCryptDestroyKey(FKey);
if FAlg <> nil then
BCryptCloseAlgorithmProvider(FAlg, 0);
inherited;
end;
procedure TPdfObjectEncryptor.EncryptObject(const Plain: TBytes; Dest: TStream);
var
IV, IVWork: array[0..15] of Byte;
Need, Written: ULONG;
Src: PByte;
begin
// Fresh random IV per object; it travels in the clear ahead of the data
CngCheck(BCryptGenRandom(nil, @IV[0], 16, BCRYPT_USE_SYSTEM_PREFERRED_RNG),
'BCryptGenRandom');
Src := PByte(Plain); // nil for an empty input is valid: padding-only block
// Size query: CBC padding always adds 1..16 bytes, so Need > Length(Plain)
IVWork := IV; // BCryptEncrypt advances the IV buffer while it chains
CngCheck(BCryptEncrypt(FKey, Src, Length(Plain), nil, @IVWork[0], 16,
nil, 0, Need, BCRYPT_BLOCK_PADDING), 'BCryptEncrypt(size)');
if ULONG(Length(FScratch)) < Need then
SetLength(FScratch, Need); // grows a handful of times, then stays put
IVWork := IV;
CngCheck(BCryptEncrypt(FKey, Src, Length(Plain), nil, @IVWork[0], 16,
PByte(FScratch), Need, Written, BCRYPT_BLOCK_PADDING), 'BCryptEncrypt');
// AESV3 layout: the 16-byte IV, then the padded ciphertext
Dest.WriteBuffer(IV[0], 16);
Dest.WriteBuffer(FScratch[0], Written);
end;
รายละเอียดสามส่วนที่ต้องรับภาระ การคิวรีขนาด (size query) — การเรียกใช้ BCryptEncrypt ครั้งแรก พร้อมบัฟเฟอร์เอาต์พุตที่เป็น nil — จะส่งคืนความยาวของ ciphertext ที่มี padding แล้ว ซึ่งจะไม่มีวันเท่ากับความยาวของอินพุต; padding นั้นเป็นแบบ deterministic (ทำงานอย่างมีรูปแบบตายตัว) ดังนั้นคุณจึงสามารถคำนวณ ((Len div 16) + 1) * 16 ได้ด้วยตนเองและลดจำนวนการเรียกใช้ลงครึ่งหนึ่ง แต่การคิวรีคือสัญญาระหว่างระบบที่ได้จัดทำเป็นเอกสารไว้ ประการที่สอง BCryptEncrypt จะขยับบัฟเฟอร์ IV ไปข้างหน้าแบบ in-place ขณะที่มันทำการต่อเชน ดังนั้นในแต่ละการเรียกใช้ก็จะใช้ชุดสำเนาของ IV เพื่อให้ตัวต้นฉบับดั้งเดิมสามารถนำไปใช้ในเอาต์พุตได้ ประการที่สาม FScratch จะขยายขนาดขึ้นไปจนถึงขนาดของอ็อบเจ็กต์ที่ใหญ่ที่สุดในไฟล์เท่านั้น หลังจากนั้นลูปก็จะไม่ต้องจัดสรรหน่วยความจำอะไรเพิ่มอีกเลย
การใช้ handle ซ้ำมีค่าแค่ไหน, จากการวัดผล
ไฟล์ที่ทำให้ต้องมาทบทวนเรื่องนี้คือเอกสารสแกนเงินกู้ถาวรขนาด 1.8 GB: อ็อบเจ็กต์ที่เข้ารหัสจำนวน 412,000 รายการ โดยบรรจุเพย์โหลดขนาด 1,710 MB เมื่อหักโครงสร้างแบบ plaintext ออกแล้ว บนเครื่องเดียวกัน, ไฟล์เดียวกัน, พื้นที่จัดเก็บ NVMe, เธรดเดียว:
- การตั้งค่าแบบต่อการเรียก (Per-call setup) (เปิด provider และสร้างคีย์ภายในฟังก์ชันช่วยเหลือ): ระยะการเข้ารหัส 71.3 วินาที — 1,710 MB ÷ 71.3 วินาที ≈ 24 MB/s
- การยกสถานะ (State hoisted) (คลาสที่อยู่ด้านบน): 9.6 วินาที — 1,710 MB ÷ 9.6 วินาที ≈ 178 MB/s
ความแตกต่างคือ 61.7 วินาทีจากการเรียก 412,000 ครั้ง หรือคิดเป็นเวลาประมาณ 150 ไมโครวินาทีต่อการเรียกที่ใช้ไปกับการเปิด provider, ตั้งค่าโหมด chaining และสร้าง key schedule ใหม่สำหรับคีย์ที่ไม่เคยเปลี่ยนแปลงเลย ซึ่งไม่มีส่วนใดเลยที่เป็นเรื่องเกี่ยวกับการเข้ารหัสจริง (cryptography) ด้วยชุดคำสั่ง AES-NI, การเข้ารหัส CBC บนบัฟเฟอร์ขนาดใหญ่จะสามารถรันได้ใกล้เคียง 1.4 GB/s บนคอร์ประมวลผลหนึ่งคอร์ ดังนั้นการคำนวณ AES เองจะใช้เวลาประมาณ 1.2 วินาทีจากทั้งหมด 9.6 วินาที; เวลาที่เหลือส่วนใหญ่จะเป็นการสลับเปลี่ยนโหมด BCryptEncrypt ในฝั่ง user-mode สองครั้งต่ออ็อบเจ็กต์ บวกกับการสร้าง IV ต่ออ็อบเจ็กต์ การจัดการ IV แบบกลุ่ม (batching) — โดยใช้การเรียก BCryptGenRandom หนึ่งครั้งเพื่อเติม IV ไปทีเดียว 4,096 ตัว — ช่วยลดเวลาการทำงานเหลือ 8.9 วินาที พอผ่านจุดนั้นไป คุณก็จะไปชนขีดจำกัดขั้นต่ำต่ออ็อบเจ็กต์ของ API และวิธีการเดียวที่เหลืออยู่คือการทำงานแบบคู่ขนาน (parallelism): อ็อบเจ็กต์ /V 5 จะทำงานเป็นอิสระต่อกันภายใต้คีย์ไฟล์ที่แชร์ร่วมกัน ดังนั้นการใช้ worker thread 4 เธรด โดยแต่ละเธรดมีอ็อบเจ็กต์คีย์หนึ่งตัว จะช่วยลดเวลาในระยะนี้ลงเหลือ 3.1 วินาทีก่อนที่กระบวนการเขียนเอาต์พุตจะกลายเป็นคอขวดของการทำงานแบบต่อเนื่อง (serialization point)
การเขียนใหม่ทั้งหมดเทียบกับการบันทึกส่วนเพิ่ม (incremental save)
ความละเอียดก็เป็นตัวตัดสินต้นทุนของการบันทึกเช่นกัน การเพิ่มการเข้ารหัสให้กับเอกสาร plaintext ที่มีอยู่แล้วจะเป็นการเขียนทุกอ็อบเจ็กต์ใหม่โดยนิยาม: ทุกสตรีมและสตริงจะเปลี่ยนทั้งเนื้อหาและความยาว, offset ของ cross-reference ทุกตัวจะขยับ และไม่มีเส้นทางให้ทำการอัปเดตส่วนเพิ่มได้ (incremental) ให้กำหนดงบประมาณว่ามันเป็นการเขียนใหม่ทั้งหมดแบบต่อเนื่อง (full sequential rewrite) และเขียนลงในไฟล์ชั่วคราวที่จะเปลี่ยนชื่อทับเป้าหมายในภายหลัง เพราะหากเกิดข้อขัดข้องกลางคันระหว่างการเข้ารหัส อาจทิ้งไฟล์ที่เข้ารหัสไปเพียงครึ่งเดียว ซึ่งไม่มีรหัสผ่านใดจะสามารถเปิดได้
ในทิศทางกลับกันนั้นมีต้นทุนต่ำ เมื่อไฟล์ถูกเข้ารหัสแล้ว การอัปเดตแบบ incremental จะต่อท้ายอ็อบเจ็กต์ใหม่ที่ถูกเข้ารหัสด้วยคีย์ไฟล์เดียวกัน และปล่อยให้ข้อมูลดั้งเดิมทุกไบต์คงอยู่โดยไม่มีการแตะต้อง การประทับตราคำอธิบายประกอบการอนุมัติ (approval annotation) ลงบนไฟล์เก็บถาวรขนาด 2 GB จะเสียพื้นที่เก็บเอาต์พุตเพิ่มเติมเพียงไม่กี่กิโลไบต์ ไม่ใช่การเขียนใหม่ขนาด 2 GB ข้อสรุปสำคัญในระบบไพป์ไลน์: ให้เข้ารหัสเพียงครั้งเดียว เป็นขั้นตอนสุดท้ายของงาน และให้การปรับแก้ในภายหลังอาศัยการบันทึกแบบส่วนเพิ่ม (incremental saves) การเปลี่ยนรหัสผ่านที่มีการเปลี่ยนคีย์ไฟล์ด้วย ก็จะนับเป็นการเขียนใหม่ทั้งหมดอีกครั้ง — ควรวางกำหนดการทำงานของมันให้สอดคล้องกัน
การวัดทรูพุต (throughput) โดยไม่หลอกตัวเอง
การอ้างทรูพุต (throughput) ของการเข้ารหัสมักจะผิดพลาดที่ตัวเศษ ตัวส่วน หรือทั้งสองอย่าง ตัวเศษควรจะเป็นเพย์โหลดไบต์: ผลรวมของความยาวสตรีมและสตริงที่ถูกผลักเข้าไปผ่าน AES จริงๆ หลังจากการบีบอัด ซึ่งตัวเขียนสามารถรวมผลลัพธ์ได้ในขณะที่ทำงาน ขนาดไฟล์มักจะกล่าวเกินจริง — ไฟล์เก็บถาวรข้างต้นมีขนาด 1.8 GB บนดิสก์ แต่มีเพียง 1,710 MB เท่านั้นที่สัมผัสกับตัวเข้ารหัส ตัวส่วนควรจะเป็นแค่ช่วงของการเข้ารหัสเท่านั้น โดยใช้ TStopwatch จาก System.Diagnostics ควบคุมการจับเวลาเอาไว้ โดยให้การแยกวิเคราะห์ (parsing) การบีบอัด (deflate) และ I/O ดิสก์ อยู่นอกเหนือการจับเวลานี้ หากนำทั้งหมดนั้นมารวมกัน โค้ดการเข้ารหัสที่เหมือนกันทุกประการจะวัดได้ช้ากว่าหลายเท่าบนไฟล์ที่แค่มีการบีบอัดที่แย่กว่า ตัวเลขข้างต้นสามารถนำมาเปรียบเทียบกันได้อย่างแม่นยำก็เพราะทั้งสองด้านของการหารนั้นเป็นเพียงการประมวลผลการเข้ารหัสอย่างเดียว
ไม่มีสิ่งใดเลยที่คุณจำเป็นต้องเขียนโค้ดขึ้นมาเอง HotPDF ได้รวมหลักการวิศวกรรมแบบเดียวกันไว้เบื้องหลังคุณสมบัติของคอมโพเนนต์ต่างๆ — ActivateProtection, CryptKeyLength, UseAES256R6 — ในระดับความเหมาะสมสำหรับแอปพลิเคชัน VCL เชิงโต้ตอบ โดยข้อควรระวังเกี่ยวกับลำดับการกำหนดค่า (assignment-order) มีครอบคลุมอยู่ใน บทความ HotPDF AES-256 สำหรับไพป์ไลน์ที่ทำงานโดยไม่ต้องมีคนดูแล (unattended) PDFlibPas จะใช้ AES-256 revision 6 กับไฟล์ที่มีอยู่ผ่านการเรียกใช้ EncryptFile เพียงครั้งเดียวที่ Strength 4 และทำการตรวจสอบสิ่งที่ถูกเขียนลงดิสก์ในภายหลัง ซึ่งเป็นเวิร์กโฟลว์ที่มีการอธิบายไว้ใน บทความตรวจสอบการเข้ารหัส PDFlibPas
เส้นทางการเข้ารหัสที่อธิบายไว้ในที่นี้มีให้ใช้งานใน คอมโพเนนต์ HotPDF สำหรับ Delphi และ C++Builder รวมถึงใน ไลบรารี PDFlibPas; โดยหน้าผลิตภัณฑ์ทั้งสองมีเอกสารอ้างอิงการเข้ารหัสที่ครบถ้วน