Artikel Teknis

Mencoba Ulang Password PDF Terenkripsi di Delphi dengan PDFlibPas

PDFlibPas mencoba ulang sebuah password yang salah pada sebuah PDF terenkripsi dengan membuang TPDFDocument yang baru saja gagal dan membuat yang sama sekali baru untuk percobaan berikutnya, digerakkan oleh sebuah callback OnPassword (TPDFlibPasswordEvent) yang berjalan hingga enam belas kali percobaan sebelum menyerah. Itu adalah sebuah penyimpangan yang disengaja dari insting yang biasanya digunakan lebih dulu kebanyakan developer Delphi: pertahankan objek dokumen yang sudah duduk di memori, beri makan password yang dikoreksi, dan muat lagi di tempat alih-alih memulai lagi dari nol. Loop retry PDFlibPas, ditambahkan di v3.245.0, mengambil posisi berlawanan, karena alasan spesifik untuk apa yang ditinggalkan sebuah percobaan password yang gagal. Skenario di baliknya cukup biasa sehingga kebanyakan aplikasi Delphi berat-dokumen akhirnya menabraknya: sebuah layar intake menerima sebuah PDF, sebuah trailer terenkripsi memaksa sebuah dialog password, operator salah ketik string tersebut, dan dialog itu muncul kembali untuk percobaan kedua. Tidak ada apa pun yang tidak biasa tentang pengalaman pengguna itu, sehingga kode di baliknya harus menerima lebih dari satu password kandidat untuk file yang sama, dan itu harus dilakukan dengan aman, tanpa membocorkan state dari percobaan yang ditolak ke yang mengikutinya

Mengapa Anda tidak bisa sekadar mencoba ulang pada objek dokumen yang sama?

Menggunakan ulang sebuah TPDFDocument di seluruh percobaan password tidak bekerja, karena sebuah percobaan yang gagal sudah merobohkan objek itu secara internal alih-alih membiarkannya dalam sebuah state jeda yang bisa dilanjutkan. Membuka sebuah PDF terenkripsi berarti mem-parse tabel cross-reference, membangun sebuah reader di atas sumber yang mendasarinya, dan membangun sebuah crypt handler dari password apa pun yang diserahkan, semuanya sebelum PDFlibPas bahkan bisa menguji apakah password itu benar. Ketika password itu ternyata salah, rutin muat internal dokumen membersihkan reader, tabel cross-reference, dan crypt handler sebagai bagian dari gagal keluar, persis seperti seharusnya, yang berarti tidak ada parser setengah-dibangun yang duduk di sana menunggu sebuah password yang dikoreksi pada pemanggilan kedua. Jalankan objek yang sama itu lewat percobaan muat lain bagaimanapun juga dan mode kegagalannya persis jenis yang menyedihkan untuk di-debug: sebuah error muncul dari state internal yang dibangun untuk sebuah parse yang berbeda dan sudah gagal, tanpa apa pun yang secara jelas menunjuk kembali ke password tiga pemanggilan di hulu. PDFlibPas menghindari seluruh kelas masalah ini dengan tidak pernah mencoba memulihkan sebuah objek dokumen begitu ia gagal terbuka; setiap percobaan mendapatkan sebuah dokumen yang tidak pernah melihat sebuah password yang salah, reader dan tabel cross-reference termasuk

Bagaimana callback OnPassword meminta password berikutnya?

TPDFlibPasswordEvent adalah tipe callback yang dipanggil PDFlibPas lewat TPDFlib.LoadFromFile, LoadFromStream, dan LoadFromString kapan pun password yang baru saja dicoba ternyata salah, dan menyerahkan tiga hal ke handler tersebut: percobaan mana yang akan berjalan, sebuah parameter Password untuk ditimpa dengan kandidat berikutnya, dan sebuah flag Retry yang secara default false

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

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

Password yang diserahkan ke pemanggilan LoadFromFile asli dihitung sebagai percobaan satu, sehingga kali pertama OnPassword terpicu sama sekali, AttemptNumber tiba sebagai 2. Biarkan Retry tidak diatur dan muat itu gagal dengan bersih dengan LastErrorCode 404; atur menjadi true dan PDFlibPas mencoba lagi dengan apa pun yang baru saja ditulis handler ke dalam Password

Di dalam loop retry: sebuah TPDFDocument baru untuk setiap percobaan

Secara internal, PDFlibPas menjawab pertanyaan siklus-hidup-objek dengan cara yang sama untuk LoadFromFile, LoadFromStream, dan LoadFromString: setiap percobaan, termasuk yang pertama, membangun sebuah TPDFDocument baru, menjalankannya lewat urutan pembukaan lengkap dengan password apa pun yang digunakan percobaan itu, dan hanya mempertahankan objek itu jika password-nya terverifikasi. TPDFDocument sebuah percobaan yang ditolak langsung dibebaskan, membawa turun reader, tabel cross-reference, dan crypt handler-nya bersamanya, dan percobaan berikutnya mulai lagi dengan sebuah objek yang tidak memiliki sejarah sama sekali

// Simplified excerpt from inside LoadFromFile: every attempt gets a
// document that has never seen a previously rejected password. FileName,
// AttemptNumber and AttemptPassword come from the enclosing method.
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);            // hand the verified document to the
        Doc := nil;                 // caller's collection; skip the Free below
      end;
    Finally
      Doc.Free;                     // a rejected attempt's reader, xref table
    End;                            // and crypt handler are torn down right here
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // success, or a non-password failure: stop
    Inc(AttemptNumber);
  Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;

Baris Doc := nil tepat sebelum blok Finally itu adalah seluruh kontrak siklus-hidup-objek dalam satu statement. Sebuah dokumen yang gagal membawa state parser setengah-dibangunnya ke liang kubur bersamanya, sesuai desain, dan sebuah dokumen yang berhasil adalah satu-satunya yang pernah ditambahkan ke FDocs, koleksi yang dipertahankan TPDFlib untuk setiap dokumen yang dibuka pemanggil. Tidak ada apa pun tentang sebuah percobaan yang ditolak yang terlihat dari luar loop retry: bukan sebuah reader setengah-terinisialisasi, bukan sebuah hitungan halaman basi, bukan sebuah crypt handler yang dibangun dari key yang salah

Berapa kali PDFlibPas akan mencoba ulang sebuah password yang salah?

PDFlibPas mengizinkan enam belas total percobaan terhadap satu pemanggilan LoadFromFile, LoadFromStream, atau LoadFromString tunggal, menghitung password yang diserahkan ke pemanggilan itu sendiri sebagai percobaan satu. OnPassword hanya pernah terpicu untuk percobaan dua hingga enam belas, yang membatasi callback itu pada lima belas invokasi; minta sebuah percobaan ketujuh belas dan PDFlibPas menolak tanpa bahkan memanggil handler tersebut. Biarkan Retry pada default-nya false pada titik mana pun, atau habiskan seluruh enam belas percobaan tanpa sebuah password yang benar, dan LoadFromFile mengembalikan 0 dengan LastErrorCode diatur ke 404, kode PDFlibPas untuk sebuah password yang ditolak. Batas itu ada karena alasan di luar kerapian: sebuah loop retry tak terbatas adalah cara mudah mengubah satu password salah ketik menjadi sebuah denial-of-service tidak sengaja terhadap thread apa pun yang menjalankan muat tersebut, terutama begitu sebuah handler dikabel ke sesuatu yang otomatis, seperti sebuah daftar password yang pernah terlihat sebelumnya, alih-alih seorang manusia mengklik lewat sebuah dialog. PDFlibPas juga menghormati Abort yang dipanggil pada instance TPDFlib dari dalam handler, karena Sender tiba sebagai objek yang sama itu, berguna di balik sebuah tombol Cancel pada sebuah dialog password, dan menghentikan loop retry pada pemeriksaan berikutnya terlepas dari apa yang diatur Retry. Sebuah muat yang gagal karena alasan selain password yang salah, sebuah tabel cross-reference yang rusak misalnya, tidak pernah memasuki loop retry sama sekali: PDFlibPas melaporkan LastErrorCode 401 dan berhenti setelah percobaan pertama, karena tidak ada jumlah tebakan password yang memperbaiki sebuah file yang rusak secara struktural

Apakah loop retry bekerja sama untuk file, stream, dan string?

Callback OnPassword dan batas enam belas-percobaan berperilaku identik di seluruh LoadFromFile, LoadFromStream, dan LoadFromString, meski ketiga titik masuk itu memegang sumbernya secara berbeda di antara percobaan. Sebuah path file murah untuk dikunjungi ulang, karena setiap percobaan sekadar membuka kembali file yang dinamai, dan sebuah sumber string sudah duduk di memori sebagai salinan pemanggil sendiri, sehingga tak satu pun membutuhkan bantuan apa pun dari pemanggil di antara percobaan. Sebuah stream yang diserahkan pemanggil adalah satu kasus yang layak dijeda: LoadFromStream mencari stream itu kembali ke posisi nol dan menyalinnya secara internal sebelum percobaan parse pertama, sehingga setiap percobaan berikutnya, dan TPDFDocument yang baru dibangun di baliknya, memutar ulang dari salinan internal itu alih-alih dari mana pun percobaan parse yang gagal meninggalkan posisi stream tersebut. Serahkan PDFiumPas sebuah TFileStream atau TMemoryStream untuk sebuah dokumen terlindungi-password dan tidak perlu me-rewind-nya di antara percobaan ulang; PDFlibPas sudah memperhitungkan sebuah posisi yang mungkin dipindahkan sebuah percobaan pertama yang gagal

Memasukkan retry password ke dalam sebuah layar intake dokumen

Sebuah workflow intake dokumen adalah rumah alami untuk callback ini, karena itu persis merupakan bentuk masalah yang dibangun untuk diselesaikan OnPassword: sebuah file tiba dari luar aplikasi, password-nya tidak diketahui dengan pasti lebih dulu, dan orang yang menyediakan kandidat membutuhkan lebih dari satu tebakan tanpa kode di sekitarnya menulis loop retry-nya sendiri di sekitar LoadFromFile

procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean);
var
  Typed: string;
begin
  // AttemptNumber counts from 2: the password already tried was attempt 1.
  Typed := '';
  Retry := InputQuery('Password required',
    Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
  if Retry then
    Password := Typed;
  // Retry is False when the operator cancels, which leaves
  // LastErrorCode at 404 for the caller to report.
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)        // only a verified document reaches here
    else
      LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
  finally
    Lib.Free;
  end;
end;

RegisterIntakeDocument hanya pernah menerima Lib begitu LoadFromFile sudah mengembalikan 1, berarti sebuah password dalam pertukaran itu benar-benar terverifikasi terhadap crypt handler file tersebut; sebuah percobaan yang ditolak tidak pernah mencapai baris itu, begitu pula sebuah dokumen setengah-terbuka. Apa yang datang selanjutnya, begitu sebuah dokumen seperti ini dikonfirmasi terbuka, layak sebuah tinjauan kedua atas pengaturan proteksinya alih-alih sebuah asumsi bahwa password yang berhasil adalah seluruh cerita keamanannya: mengaudit apa yang sebenarnya dideklarasikan dictionary /Encrypt sebuah dokumen membahas membaca algoritma, revisi, dan bit izin yang diekspos PDFlibPas begitu sebuah file seperti ini dimuat

Retry password juga sebuah instance sempit dari sebuah disiplin lebih luas yang diterapkan PDFlibPas di seluruh lapisan parsing-nya: sebuah file yang belum membuktikan dirinya tidak mendapat keuntungan dari keraguan, baik pertanyaannya password mana yang membukanya atau apakah sebuah field panjang di dalamnya berbohong tentang ukuran buffer yang dibutuhkannya. Mengeraskan sebuah parser PDF Pascal terhadap file berbahaya membahas separuh lain dari disiplin itu, decoder yang memperlakukan setiap program font dan stream gambar dalam sebuah PDF masuk sebagai input adversarial alih-alih sebuah dokumen well-formed yang sekadar lupa password-nya

OnPassword dan loop retry di baliknya adalah bagian dari PDFlibPas PDF library standar untuk Delphi dan C++Builder, tersedia di mana pun LoadFromFile, LoadFromStream, atau LoadFromString sudah tersedia, tanpa modul terpisah atau tingkat lisensi yang dibutuhkan untuk sebuah dokumen yang sekadar membutuhkan tebakan kedua atas password-nya