บทความเทคนิค

การ Retry รหัสผ่านของ PDF ที่เข้ารหัสไว้ใน Delphi ด้วย PDF Library for Delphi

PDF Library for Delphi retry รหัสผ่านที่ผิดของ PDF ที่เข้ารหัสไว้ ด้วยการทิ้ง TPDFDocument ที่เพิ่งล้มเหลวและสร้างตัวใหม่ทั้งหมดสำหรับความพยายามครั้งถัดไป ขับเคลื่อนด้วย callback OnPassword (TPDFlibPasswordEvent) ที่รันได้สูงสุดสิบหกครั้งก่อนที่จะยอมแพ้ นั่นเป็นการเบี่ยงเบนที่จงใจจากสัญชาตญาณที่นักพัฒนา Delphi ส่วนใหญ่มักหยิบมาใช้ก่อน คือเก็บ document object ที่นั่งอยู่ในหน่วยความจำแล้วไว้ ป้อนรหัสผ่านที่แก้ไขแล้วเข้าไป และโหลดใหม่ตรงที่ แทนที่จะเริ่มต้นใหม่จากความว่างเปล่า loop การ retry ของ PDF Library for Delphi ที่เพิ่มเข้ามาใน v3.245.0 ยืนอยู่ในตำแหน่งตรงข้าม ด้วยเหตุผลเฉพาะเจาะจงกับสิ่งที่ความพยายามรหัสผ่านที่ล้มเหลวทิ้งไว้เบื้องหลัง สถานการณ์เบื้องหลังมันเป็นเรื่องธรรมดาพอที่แอปพลิเคชัน Delphi ที่หนักด้านเอกสารส่วนใหญ่จะเจอในที่สุด หน้าจอรับเอกสารรับ PDF เข้ามา trailer ที่เข้ารหัสไว้บังคับให้เกิด dialog รหัสผ่าน ผู้ปฏิบัติงานพิมพ์ผิด และ dialog ก็ปรากฏขึ้นอีกครั้งสำหรับความพยายามครั้งที่สอง ไม่มีอะไรผิดปกติเกี่ยวกับประสบการณ์ผู้ใช้แบบนั้นเลย ดังนั้นโค้ดที่อยู่เบื้องหลังจึงต้องยอมรับรหัสผ่านผู้สมัครมากกว่าหนึ่งตัวสำหรับไฟล์เดียวกัน และมันต้องทำแบบนั้นอย่างปลอดภัย โดยไม่รั่วไหล state จากความพยายามที่ถูกปฏิเสธเข้าไปยังตัวที่ตามมา

ทำไมคุณ retry บน document object เดียวกันตรงๆ ไม่ได้

การใช้ TPDFDocument ซ้ำข้ามความพยายามรหัสผ่านใช้ไม่ได้ เพราะความพยายามที่ล้มเหลวได้รื้อ object นั้นลงไปแล้วภายใน แทนที่จะทิ้งมันไว้ใน state ที่หยุดชั่วคราวและกลับมาทำต่อได้ การเปิด PDF ที่เข้ารหัสไว้หมายถึงการ parse cross-reference table, การสร้าง reader ทับแหล่งข้อมูลข้างใต้ และการสร้าง crypt handler จากรหัสผ่านใดก็ตามที่ให้มา ทั้งหมดนี้ก่อนที่ PDF Library for Delphi จะทดสอบได้ด้วยซ้ำว่ารหัสผ่านนั้นถูกต้องหรือไม่ เมื่อรหัสผ่านกลายเป็นว่าผิด รูทีนการโหลดภายในของเอกสารจะล้าง reader, cross-reference table และ crypt handler เป็นส่วนหนึ่งของการล้มเหลวออกมา ตามที่มันควรทำ ซึ่งหมายความว่าไม่มี parser ที่สร้างไว้ครึ่งเดียวนั่งรออยู่สำหรับรหัสผ่านที่แก้ไขแล้วในการเรียกครั้งที่สอง ขับเคลื่อน object เดียวกันนั้นผ่านความพยายามโหลดอีกครั้งอยู่ดี แล้วโหมดความล้มเหลวก็เป็นประเภทพอดีที่ debug ได้ยากมาก error ปรากฏขึ้นจาก internal state ที่สร้างขึ้นสำหรับการ parse ที่ล้มเหลวไปแล้วอีกตัวหนึ่ง โดยไม่มีอะไรชัดเจนที่ชี้กลับไปยังรหัสผ่านสามการเรียกก่อนหน้า PDF Library for Delphi หลีกเลี่ยงปัญหาทั้งประเภทนี้ด้วยการไม่เคยพยายามกู้คืน document object เมื่อมันเปิดล้มเหลวไปแล้วเลย ทุกความพยายามได้ document ที่ไม่เคยเห็นรหัสผ่านที่ผิด รวมถึง reader และ cross-reference table ด้วย

callback OnPassword ขอรหัสผ่านถัดไปอย่างไร

TPDFlibPasswordEvent เป็นประเภท callback ที่ PDF Library for Delphi เรียกผ่าน TPDFlib.LoadFromFile, LoadFromStream และ LoadFromString ทุกครั้งที่รหัสผ่านที่เพิ่งลองกลายเป็นว่าผิด และมันส่งสามอย่างให้ handler คือ ความพยายามไหนกำลังจะรัน, พารามิเตอร์ Password ให้เขียนทับด้วยผู้สมัครถัดไป และ flag Retry ที่ค่าเริ่มต้นเป็น false

TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean) of object;

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

รหัสผ่านที่ส่งเข้าการเรียก LoadFromFile เดิมนับเป็นความพยายามที่หนึ่ง ดังนั้นครั้งแรกที่ OnPassword ยิงเลย AttemptNumber จะมาถึงเป็น 2 ปล่อย Retry ไม่ตั้งค่า แล้วการโหลดจะล้มเหลวอย่างสะอาดด้วย LastErrorCode 404 ตั้งมันเป็น true แล้ว PDF Library for Delphi จะลองอีกครั้งด้วยอะไรก็ตามที่ handler เพิ่งเขียนเข้า Password

ภายใน loop การ retry: TPDFDocument ใหม่สำหรับทุกความพยายาม

ภายใน PDF Library for Delphi ตอบคำถามด้านวงจรชีวิต object แบบเดียวกันสำหรับ LoadFromFile, LoadFromStream และ LoadFromString ทุกความพยายาม รวมถึงครั้งแรก สร้าง TPDFDocument ใหม่ รันมันผ่านลำดับการเปิดที่สมบูรณ์ด้วยรหัสผ่านใดก็ตามที่ความพยายามนั้นใช้อยู่ และเก็บ object ไว้ก็ต่อเมื่อรหัสผ่านยืนยันเท่านั้น TPDFDocument ของความพยายามที่ถูกปฏิเสธถูก free ทันที พาเอา reader, cross-reference table และ crypt handler ของมันลงไปด้วย และความพยายามถัดไปเริ่มต้นใหม่ด้วย object ที่ไม่มีประวัติใดๆ เลย

ผังงาน PDF Library for Delphi ของลูปลองรหัสผ่านซ้ำที่ทุกครั้งสร้าง TPDFDocument ใหม่ ความพยายามที่ถูกปฏิเสธจะคืนหน่วยความจำ reader, ตาราง cross-reference และ crypt handler ส่วน callback OnPassword ตัดสินว่าจะลองอีกครั้งหรือให้การโหลดล้มเหลวด้วย LastErrorCode 404
ผู้สมัคร (candidate) ที่ล้มเหลวไม่เคยรอดพ้นลูป: เอกสารของมันถูก free โดยที่สถานะ parser ยังสร้างค้างครึ่ง ๆ กลาง ๆ การตั้งค่า Retry จะเขียนผู้สมัครถัดไปลงใน Password และทำซ้ำกับออบเจ็กต์ที่ไม่เคยเห็นรหัสผ่านผิดมาก่อน
// ตัวอย่างที่ย่อลงจากภายใน LoadFromFile: ทุกความพยายามจะได้
// เอกสารที่ไม่เคยเห็นรหัสผ่านที่ถูกปฏิเสธมาก่อนเลย FileName,
// AttemptNumber และ AttemptPassword มาจากเมธอดที่ครอบอยู่
Var
  Doc: TPDFDocument;
  LoadResult: TPLLoadResult;
  Success: Boolean;
Begin
  Success := False;
  Repeat
    Doc := TPDFDocument.Create;
    Doc.DecodeMode := FDefaultDecodeMode;
    Try
      LoadResult := Doc.LoadFromFile(FileName, AttemptPassword);
      Success := LoadResult = lrOkay;
      if Success then
      begin
        FDocs.Add(Doc);            // ส่งมอบเอกสารที่ยืนยันแล้วให้
        Doc := nil;                 // collection ของผู้เรียก ข้าม Free ด้านล่าง
      end;
    Finally
      Doc.Free;                     // reader, xref table ของความพยายามที่ถูกปฏิเสธ
    End;                            // และ crypt handler จะถูกทำลายตรงนี้เลย
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // สำเร็จ หรือความล้มเหลวที่ไม่ใช่จากรหัสผ่าน: หยุด
    Inc(AttemptNumber);
  Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;

บรรทัด Doc := nil นั้นก่อน block Finally คือสัญญาด้านวงจรชีวิต object ทั้งหมดในหนึ่งคำสั่ง เอกสารที่ล้มเหลวพา parser state ที่สร้างไว้ครึ่งเดียวลงหลุมไปกับมันโดยการออกแบบ และเอกสารที่สำเร็จเป็นตัวเดียวที่เคยถูกเพิ่มเข้า FDocs ซึ่งเป็น collection ที่ TPDFlib เก็บไว้สำหรับทุกเอกสารที่ผู้เรียกเปิดอยู่ ไม่มีอะไรจากความพยายามที่ถูกปฏิเสธที่มองเห็นได้จากภายนอก loop การ retry เลย ไม่มี reader ที่สร้างไว้ครึ่งเดียว, ไม่มีจำนวนหน้าที่ค้าง, ไม่มี crypt handler ที่สร้างจากคีย์ที่ผิด

PDF Library for Delphi จะ retry รหัสผ่านที่ผิดกี่ครั้ง

PDF Library for Delphi อนุญาตให้ลองรวมสิบหกครั้งต่อการเรียก LoadFromFile, LoadFromStream หรือ LoadFromString หนึ่งครั้ง โดยนับรหัสผ่านที่ส่งเข้าการเรียกเองเป็นความพยายามที่หนึ่ง OnPassword ยิงแค่สำหรับความพยายามที่สองถึงสิบหกเท่านั้น ซึ่งจำกัด callback ไว้ที่สิบห้าครั้ง ขอความพยายามครั้งที่สิบเจ็ดและ PDF Library for Delphi จะปฏิเสธโดยไม่แม้แต่จะเรียก handler เลย ปล่อย Retry ไว้ที่ค่าเริ่มต้น false ณ จุดใดก็ตาม หรือใช้ครบสิบหกครั้งโดยไม่มีรหัสผ่านที่ถูกต้อง แล้ว LoadFromFile จะคืน 0 พร้อม LastErrorCode ตั้งเป็น 404 ซึ่งเป็นรหัสของ PDF Library for Delphi สำหรับรหัสผ่านที่ถูกปฏิเสธ เพดานนี้มีอยู่ด้วยเหตุผลที่มากกว่าความเป็นระเบียบเรียบร้อย loop การ retry ที่ไม่มีขอบเขตเป็นวิธีง่ายๆ ที่จะเปลี่ยนรหัสผ่านที่พิมพ์ผิดหนึ่งครั้งให้กลายเป็น denial-of-service โดยไม่ตั้งใจต่อ thread ใดก็ตามที่กำลังรันการโหลด โดยเฉพาะเมื่อ handler ถูกต่อสายเข้ากับอะไรบางอย่างที่เป็นอัตโนมัติ เช่น รายการรหัสผ่านที่เคยเห็นมาก่อน แทนที่จะเป็นมนุษย์ที่คลิกผ่าน dialog PDF Library for Delphi ยังเคารพ Abort ที่เรียกบน instance TPDFlib จากภายใน handler ด้วย เพราะ Sender มาถึงเป็น object เดียวกันนั้น มีประโยชน์อยู่หลังปุ่ม Cancel บน dialog รหัสผ่าน และหยุด loop การ retry ที่การตรวจสอบครั้งถัดไปไม่ว่า Retry จะถูกตั้งเป็นอะไรก็ตาม การโหลดที่ล้มเหลวด้วยเหตุผลอื่นที่ไม่ใช่รหัสผ่านผิด เช่น cross-reference table ที่เสียหาย จะไม่เข้า loop การ retry เลย PDF Library for Delphi รายงาน LastErrorCode 401 และหยุดหลังความพยายามครั้งแรก เพราะไม่มีจำนวนการเดารหัสผ่านใดจะแก้ไฟล์ที่เสียหายเชิงโครงสร้างได้

PDF Library for Delphi: เส้นทางความพยายามจากหนึ่งถึงสิบหก แสดง OnPassword ทำงานตั้งแต่ความพยายามครั้งที่สองเป็นต้นไป ครั้งที่สิบเจ็ดถูกปฏิเสธโดยไม่เรียก handler และผลลัพธ์แยกกันสำหรับความสำเร็จ, LastErrorCode 404 และ LastErrorCode 401
มีเพียงรหัสผ่านที่ส่งให้การเรียก load โดยตรงเท่านั้นที่นับเป็นความพยายามครั้งที่หนึ่ง และ callback จะไม่ทำงานเกินสิบห้าครั้งต่อการโหลดหนึ่งครั้ง ไฟล์ที่โครงสร้างเสียหายจะข้ามลูปนี้ทั้งหมดและรายงาน 401 หลังความพยายามเพียงครั้งเดียว

loop การ retry ทำงานเหมือนกันสำหรับไฟล์, สตรีม และ string หรือไม่

callback OnPassword และเพดานสิบหกครั้งทำงานเหมือนกันเป๊ะข้าม LoadFromFile, LoadFromStream และ LoadFromString แม้ว่าจุดเข้าทั้งสามจะถือแหล่งข้อมูลของตัวเองต่างกันระหว่างความพยายาม path ไฟล์กลับมาเยี่ยมได้ถูก เพราะแต่ละความพยายามแค่เปิดไฟล์ที่ตั้งชื่อไว้ใหม่ และแหล่งข้อมูล string นั่งอยู่ในหน่วยความจำเป็นสำเนาของผู้เรียกเองอยู่แล้ว ดังนั้นทั้งสองจึงไม่ต้องการความช่วยเหลือจากผู้เรียกระหว่างการลองเลย สตรีมที่ผู้เรียกให้มาเป็นกรณีเดียวที่ควรค่าแก่การหยุดพิจารณา LoadFromStream seek สตรีมนั้นกลับไปตำแหน่งศูนย์และ copy มันภายในก่อนความพยายาม parse ครั้งแรก ดังนั้นทุกความพยายามที่ตามมา และ TPDFDocument ที่สร้างขึ้นใหม่เบื้องหลังมัน จะเล่นซ้ำจากสำเนาภายในนั้น แทนที่จะเป็นตำแหน่งใดก็ตามที่การ parse ที่ล้มเหลวทิ้งตำแหน่งของสตรีมไว้ ส่ง TFileStream หรือ TMemoryStream ให้ PDF Library for Delphi สำหรับเอกสารที่ป้องกันด้วยรหัสผ่าน แล้วไม่ต้องย้อนกลับมันระหว่างการ retry เลย PDF Library for Delphi คำนึงถึงตำแหน่งที่ความพยายามแรกที่ล้มเหลวอาจย้ายไปแล้ว

การใส่การ retry รหัสผ่านเข้ากับหน้าจอรับเอกสาร

workflow การรับเอกสารเป็นบ้านตามธรรมชาติสำหรับ callback นี้ เพราะมันเป็นรูปร่างของปัญหาที่ OnPassword ถูกสร้างมาเพื่อแก้พอดี ไฟล์มาถึงจากภายนอกแอปพลิเคชัน รหัสผ่านของมันไม่เป็นที่รู้แน่ชัดล่วงหน้า และคนที่ให้ผู้สมัครต้องการมากกว่าหนึ่งการเดา โดยไม่ต้องให้โค้ดรอบข้างเขียน loop การ retry ของตัวเองรอบ LoadFromFile

procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean);
var
  Typed: string;
begin
  // AttemptNumber นับเริ่มจาก 2: รหัสผ่านที่ลองไปแล้วคือความพยายามที่ 1
  Typed := '';
  Retry := InputQuery('Password required',
    Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
  if Retry then
    Password := Typed;
  // Retry จะเป็น False เมื่อผู้ปฏิบัติงานกดยกเลิก ซึ่งจะทำให้
  // LastErrorCode ค้างอยู่ที่ 404 ให้ผู้เรียกรายงานต่อไป
end;
procedure TIntakeForm.LoadInboundDocument;
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.OnPassword := SupplyPassword;
    if Lib.LoadFromFile('inbound-invoice.pdf', '') = 1 then
      RegisterIntakeDocument(Lib)        // มีแค่เอกสารที่ยืนยันแล้วเท่านั้นที่มาถึงตรงนี้
    else
      LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
  finally
    Lib.Free;
  end;
end;

RegisterIntakeDocument รับ Lib ก็ต่อเมื่อ LoadFromFile คืนค่า 1 แล้วเท่านั้น หมายความว่ารหัสผ่านตัวใดตัวหนึ่งในการแลกเปลี่ยนนั้นยืนยันกับ crypt handler ของไฟล์ได้จริงๆ ความพยายามที่ถูกปฏิเสธไม่เคยไปถึงบรรทัดนั้นเลย และเอกสารที่เปิดครึ่งเดียวก็เช่นกัน สิ่งที่ตามมาต่อไป เมื่อเอกสารแบบนี้ได้รับการยืนยันว่าเปิดแล้ว ควรค่าแก่การมองครั้งที่สองที่การตั้งค่าการป้องกันของมัน แทนที่จะสมมติว่ารหัสผ่านที่ใช้ได้คือเรื่องราวความปลอดภัยทั้งหมด การตรวจสอบว่า dictionary /Encrypt ของเอกสารประกาศอะไรจริงๆ ครอบคลุมการอ่านอัลกอริทึม, revision และบิตสิทธิ์ที่ PDF Library for Delphi เปิดออกมาเมื่อไฟล์แบบนี้โหลดแล้ว

การ retry รหัสผ่านยังเป็นตัวอย่างที่แคบของวินัยที่กว้างกว่าที่ PDF Library for Delphi ใช้ทั่วทั้งชั้นการ parse ของมัน ไฟล์ที่ยังไม่ได้พิสูจน์ตัวเองไม่ได้รับประโยชน์แห่งความสงสัยเลย ไม่ว่าคำถามจะเป็นว่ารหัสผ่านตัวไหนปลดล็อคมัน หรือฟิลด์ความยาวข้างในมันโกหกเกี่ยวกับขนาด buffer ที่มันต้องการหรือไม่ การเสริมความแข็งแกร่งให้ Pascal PDF parser ต้านไฟล์ประสงค์ร้าย ครอบคลุมอีกครึ่งหนึ่งของวินัยนั้น คือ decoder ที่ปฏิบัติต่อทุกโปรแกรมฟอนต์และสตรีมภาพใน PDF ขาเข้าเป็นอินพุตที่ประสงค์ร้าย แทนที่จะเป็นเอกสารที่มีรูปแบบถูกต้องซึ่งแค่ลืมรหัสผ่านของมัน

OnPassword และ loop การ retry เบื้องหลังมันเป็นส่วนหนึ่งของไลบรารี PDF PDF Library for Delphi สำหรับ Delphi และ C++Builderรุ่นมาตรฐาน มีอยู่ทุกที่ที่ LoadFromFile, LoadFromStream หรือ LoadFromString มีอยู่แล้ว โดยไม่ต้องการโมดูลแยกหรือระดับใบอนุญาตแยกสำหรับเอกสารที่แค่ต้องการการเดารหัสผ่านครั้งที่สอง