Τεχνικό Άρθρο

Επανάληψη Κωδικών Κρυπτογραφημένου PDF σε Delphi με το PDFlibPas

Το PDFlibPas ξαναδοκιμάζει έναν λανθασμένο κωδικό σε ένα κρυπτογραφημένο PDF απορρίπτοντας το TPDFDocument που μόλις απέτυχε και δημιουργώντας ένα εντελώς νέο για την επόμενη προσπάθεια, οδηγούμενο από μια επιστροφή κλήσης OnPassword (TPDFlibPasswordEvent) που τρέχει έως δεκαέξι φορές πριν παραιτηθεί. Αυτή είναι μια σκόπιμη απόκλιση από το ένστικτο που έχουν οι περισσότεροι προγραμματιστές Delphi: κρατήστε το αντικείμενο εγγράφου που ήδη κάθεται στη μνήμη, τροφοδοτήστε του έναν διορθωμένο κωδικό, και φορτώστε ξανά επί τόπου αντί να ξεκινήσετε από το μηδέν. Ο βρόχος επανάληψης του PDFlibPas, που προστέθηκε στην v3.245.0, παίρνει την αντίθετη θέση, για λόγους ειδικούς σχετικά με το τι αφήνει πίσω μια αποτυχημένη προσπάθεια κωδικού. Το σενάριο πίσω από αυτό είναι αρκετά συνηθισμένο ώστε οι περισσότερες εφαρμογές Delphi με πολλά έγγραφα να το συναντούν τελικά: μια οθόνη εισαγωγής δέχεται ένα PDF, ένα κρυπτογραφημένο trailer αναγκάζει έναν διάλογο κωδικού, ο χειριστής πληκτρολογεί λάθος τη συμβολοσειρά, και ο διάλογος εμφανίζεται ξανά για δεύτερη προσπάθεια. Τίποτα σε αυτή την εμπειρία χρήστη δεν είναι ασυνήθιστο, οπότε ο κώδικας πίσω της πρέπει να δέχεται περισσότερους από έναν υποψήφιους κωδικούς για το ίδιο αρχείο, και πρέπει να το κάνει με ασφάλεια, χωρίς να διαρρέει κατάσταση από την απορριφθείσα προσπάθεια σε αυτή που ακολουθεί

Γιατί δεν μπορείτε απλώς να ξαναδοκιμάσετε στο ίδιο αντικείμενο εγγράφου;

Η επαναχρησιμοποίηση ενός TPDFDocument σε πολλές προσπάθειες κωδικού δεν λειτουργεί, γιατί μια αποτυχημένη προσπάθεια έχει ήδη κατεδαφίσει εσωτερικά εκείνο το αντικείμενο αντί να το αφήσει σε κάποια παυμένη, επαναλήψιμη κατάσταση. Το άνοιγμα ενός κρυπτογραφημένου PDF σημαίνει ανάλυση του πίνακα διαγώνιων αναφορών, χτίσιμο ενός reader πάνω από την υποκείμενη πηγή, και κατασκευή ενός χειριστή κρυπτογράφησης από ό,τι κωδικό δόθηκε, όλα πριν το PDFlibPas μπορέσει έστω να ελέγξει αν εκείνος ο κωδικός είναι σωστός. Όταν ο κωδικός αποδειχθεί λάθος, η εσωτερική ρουτίνα φόρτωσης του εγγράφου καθαρίζει τον reader, τον πίνακα διαγώνιων αναφορών, και τον χειριστή κρυπτογράφησης ως μέρος της αποτυχίας, ακριβώς όπως πρέπει, κάτι που σημαίνει ότι δεν υπάρχει κανένας μισοχτισμένος parser να περιμένει έναν διορθωμένο κωδικό σε δεύτερη κλήση. Οδηγήστε ούτως ή άλλως το ίδιο αντικείμενο μέσα από άλλη προσπάθεια φόρτωσης και ο τρόπος αποτυχίας είναι ακριβώς το είδος που είναι άθλιο να γίνει debug: ένα σφάλμα εμφανίζεται από εσωτερική κατάσταση χτισμένη για μια διαφορετική, ήδη αποτυχημένη ανάλυση, χωρίς τίποτα σε αυτό να δείχνει προφανώς πίσω στον κωδικό τρεις κλήσεις πιο πάνω. Το PDFlibPas αποφεύγει ολόκληρη αυτή την κατηγορία προβλήματος μη προσπαθώντας ποτέ να ανακτήσει ένα αντικείμενο εγγράφου μόλις αποτύχει να ανοίξει· κάθε προσπάθεια αποκτά ένα έγγραφο που ποτέ δεν έχει δει λάθος κωδικό, reader και πίνακα διαγώνιων αναφορών συμπεριλαμβανομένων

Πώς ζητά η επιστροφή κλήσης OnPassword τον επόμενο κωδικό;

Το TPDFlibPasswordEvent είναι ο τύπος επιστροφής κλήσης που καλεί το PDFlibPas μέσω των TPDFlib.LoadFromFile, LoadFromStream και LoadFromString όποτε ο κωδικός που μόλις δοκιμάστηκε αποδεικνύεται λάθος, και παραδίδει στον χειριστή τρία πράγματα: ποια προσπάθεια πρόκειται να τρέξει, μια παράμετρο Password προς αντικατάσταση με τον επόμενο υποψήφιο, και μια σημαία 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 και το PDFlibPas ξαναδοκιμάζει με ό,τι μόλις έγραψε ο χειριστής στο Password

Μέσα στον Βρόχο Επανάληψης: Ένα Νέο TPDFDocument για Κάθε Προσπάθεια

Εσωτερικά, το PDFlibPas απαντά στο ερώτημα κύκλου ζωής αντικειμένου με τον ίδιο τρόπο για τις LoadFromFile, LoadFromStream και LoadFromString: κάθε προσπάθεια, συμπεριλαμβανομένης της πρώτης, κατασκευάζει ένα φρέσκο TPDFDocument, το τρέχει μέσα από την πλήρη ακολουθία ανοίγματος με όποιον κωδικό χρησιμοποιεί εκείνη η προσπάθεια, και κρατά το αντικείμενο μόνο αν ο κωδικός επαληθευτεί. Το TPDFDocument μιας απορριφθείσας προσπάθειας απελευθερώνεται αμέσως, παρασύροντας μαζί του τον reader, τον πίνακα διαγώνιων αναφορών, και τον χειριστή κρυπτογράφησης, και η επόμενη προσπάθεια ξεκινά από την αρχή με ένα αντικείμενο που δεν έχει καθόλου ιστορικό

// 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;

Εκείνη η γραμμή Doc := nil ακριβώς πριν το μπλοκ Finally είναι ολόκληρο το συμβόλαιο κύκλου ζωής αντικειμένου σε μία εντολή. Ένα έγγραφο που αποτυγχάνει παίρνει την μισοχτισμένη κατάσταση του parser του στον τάφο μαζί του, εκ σχεδιασμού, και ένα έγγραφο που πετυχαίνει είναι το μόνο που προστίθεται ποτέ στο FDocs, τη συλλογή που κρατά η TPDFlib για κάθε έγγραφο που έχει ανοιχτό ο καλών. Τίποτα από μια απορριφθείσα προσπάθεια δεν είναι ορατό από έξω από τον βρόχο επανάληψης: ούτε ένας μισό-αρχικοποιημένος reader, ούτε ένας παλιωμένος μετρητής σελίδων, ούτε ένας χειριστής κρυπτογράφησης χτισμένος από λάθος κλειδί

Πόσες Φορές θα Ξαναδοκιμάσει το PDFlibPas έναν Λάθος Κωδικό;

Το PDFlibPas επιτρέπει δεκαέξι συνολικές προσπάθειες ενάντια σε μία μόνη κλήση LoadFromFile, LoadFromStream, ή LoadFromString, μετρώντας τον κωδικό που περάστηκε στην ίδια την κλήση ως πρώτη προσπάθεια. Το OnPassword πυροδοτείται μόνο για τις προσπάθειες δύο έως δεκαέξι, κάτι που περιορίζει την επιστροφή κλήσης σε δεκαπέντε επικλήσεις· ζητήστε δέκατη έβδομη προσπάθεια και το PDFlibPas αρνείται χωρίς να καλέσει καν τον χειριστή. Αφήστε το Retry στην προεπιλογή του false οποιαδήποτε στιγμή, ή εξαντλήστε και τις δεκαέξι προσπάθειες χωρίς σωστό κωδικό, και η LoadFromFile επιστρέφει 0 με το LastErrorCode να τίθεται σε 404, τον κωδικό του PDFlibPas για απορριφθέντα κωδικό. Το ανώτατο όριο υπάρχει για λόγους πέρα από την τακτοποίηση: ένας απεριόριστος βρόχος επανάληψης είναι ένας εύκολος τρόπος να μετατραπεί ένας λάθος πληκτρολογημένος κωδικός σε τυχαία άρνηση εξυπηρέτησης ενάντια σε όποιο νήμα τρέχει τη φόρτωση, ειδικά μόλις ένας χειριστής συνδεθεί με κάτι αυτοματοποιημένο, όπως μια λίστα προηγουμένως δοκιμασμένων κωδικών, αντί για έναν άνθρωπο να κάνει κλικ σε έναν διάλογο. Το PDFlibPas τιμά επίσης την Abort που καλείται στην παρουσία TPDFlib από μέσα από τον χειριστή, αφού το Sender φτάνει ως το ίδιο αντικείμενο, χρήσιμο πίσω από ένα κουμπί Ακύρωση σε έναν διάλογο κωδικού, και σταματά τον βρόχο επανάληψης στον επόμενο έλεγχο ανεξάρτητα από το τι είχε τεθεί το Retry. Μια φόρτωση που αποτυγχάνει για λόγο διαφορετικό από λάθος κωδικό, έναν κατεστραμμένο πίνακα διαγώνιων αναφορών για παράδειγμα, ποτέ δεν εισέρχεται καν στον βρόχο επανάληψης: το PDFlibPas αναφέρει LastErrorCode 401 και σταματά μετά την πρώτη προσπάθεια, γιατί κανένας αριθμός μαντεψιών κωδικού δεν διορθώνει ένα δομικά κατεστραμμένο αρχείο

Δουλεύει ο Βρόχος Επανάληψης το Ίδιο για Αρχεία, Streams και Συμβολοσειρές;

Η επιστροφή κλήσης OnPassword και το ανώτατο όριο δεκαέξι προσπαθειών συμπεριφέρονται πανομοιότυπα στις LoadFromFile, LoadFromStream και LoadFromString, αν και τα τρία σημεία εισόδου κρατούν την πηγή τους διαφορετικά μεταξύ προσπαθειών. Μια διαδρομή αρχείου είναι φθηνή να ξαναεπισκεφθεί, αφού κάθε προσπάθεια απλώς ξανανοίγει το ονομασμένο αρχείο, και μια πηγή συμβολοσειράς ήδη κάθεται στη μνήμη ως δικό του αντίγραφο του καλούντος, οπότε καμία από τις δύο δεν χρειάζεται καμία βοήθεια από τον καλούντα μεταξύ προσπαθειών. Ένα stream παρεχόμενο από τον καλούντα είναι η μία περίπτωση που αξίζει να σταθούμε: η LoadFromStream επαναφέρει εκείνο το stream στη θέση μηδέν και το αντιγράφει εσωτερικά πριν την πρώτη προσπάθεια ανάλυσης, οπότε κάθε επόμενη προσπάθεια, και το φρεσκοκατασκευασμένο TPDFDocument πίσω της, αναπαράγεται από εκείνο το εσωτερικό αντίγραφο αντί από όπου μια αποτυχημένη ανάλυση άφησε τη θέση του stream. Δώστε στο PDFlibPas ένα TFileStream ή TMemoryStream για ένα έγγραφο προστατευμένο με κωδικό και δεν χρειάζεται να το επαναφέρετε μεταξύ επαναλήψεων· το PDFlibPas ήδη λογαριάζει μια θέση που μια πρώτη, αποτυχημένη προσπάθεια μπορεί να έχει μετακινήσει

Ενσωματώνοντας την Επανάληψη Κωδικού σε μια Οθόνη Εισαγωγής Εγγράφων

Μια ροή εργασίας εισαγωγής εγγράφων είναι το φυσικό σπίτι για αυτή την επιστροφή κλήσης, γιατί είναι ακριβώς το σχήμα προβλήματος για το οποίο χτίστηκε το OnPassword να λύσει: ένα αρχείο φτάνει από έξω από την εφαρμογή, ο κωδικός του δεν είναι γνωστός με βεβαιότητα εκ των προτέρων, και το άτομο που παρέχει υποψηφίους χρειάζεται περισσότερες από μία μαντεψιές χωρίς ο περιβάλλων κώδικας να γράφει τον δικό του βρόχο επανάληψης γύρω από την 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 λαμβάνει ποτέ το Lib μόνο αφού η LoadFromFile έχει επιστρέψει 1, που σημαίνει ότι κάποιος κωδικός σε εκείνη την ανταλλαγή πράγματι επαληθεύτηκε ενάντια στον χειριστή κρυπτογράφησης του αρχείου· μια απορριφθείσα προσπάθεια ποτέ δεν φτάνει σε εκείνη τη γραμμή, ούτε φτάνει ένα μισό-ανοιχτό έγγραφο. Αυτό που ακολουθεί στη συνέχεια, μόλις ένα τέτοιο έγγραφο επιβεβαιωθεί ανοιχτό, αξίζει μια δεύτερη ματιά στις ρυθμίσεις προστασίας του παρά μια υπόθεση ότι ο κωδικός που δούλεψε είναι ολόκληρη η ιστορία ασφάλειας: ο έλεγχος του τι πράγματι δηλώνει το λεξικό /Encrypt ενός εγγράφου καλύπτει την ανάγνωση του αλγορίθμου, της αναθεώρησης, και των bits δικαιωμάτων που εκθέτει το PDFlibPas μόλις φορτωθεί ένα τέτοιο αρχείο

Η επανάληψη κωδικού είναι επίσης μια στενή περίπτωση μιας ευρύτερης πειθαρχίας που εφαρμόζει το PDFlibPas σε όλο το επίπεδο ανάλυσής του: ένα αρχείο που δεν έχει ακόμα αποδείξει τον εαυτό του δεν παίρνει το ευεργέτημα της αμφιβολίας, είτε το ερώτημα είναι ποιος κωδικός το ξεκλειδώνει είτε αν ένα πεδίο μήκους μέσα του ψεύδεται για το μέγεθος buffer που χρειάζεται. Το σκλήρυνση ενός parser PDF σε Pascal ενάντια σε κακόβουλα αρχεία καλύπτει το άλλο μισό αυτής της πειθαρχίας, τους αποκωδικοποιητές που αντιμετωπίζουν κάθε πρόγραμμα γραμματοσειράς και ροή εικόνας σε ένα εισερχόμενο PDF ως εχθρική είσοδο αντί για ένα καλοδιατυπωμένο έγγραφο που απλώς ξέχασε τον κωδικό του

Το OnPassword και ο βρόχος επανάληψης πίσω του είναι μέρος της τυπικής βιβλιοθήκης PDF PDFlibPas για Delphi και C++Builder, διαθέσιμος οπουδήποτε είναι ήδη διαθέσιμες οι LoadFromFile, LoadFromStream, ή LoadFromString, χωρίς να απαιτείται ξεχωριστή μονάδα ή επίπεδο άδειας για ένα έγγραφο που απλώς χρειάζεται μια δεύτερη μαντεψιά στον κωδικό του