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

SASLprep AES-256 Κωδικοί PDF σε Delphi με PDF Library for Delphi

Ένα AES-256 PDF κρυπτογραφημένο με μη ASCII κωδικό πρόσβασης ανοίγει στο πρόγραμμα που το έγραψε και πουθενά αλλού. Η αιτία είναι σχεδόν πάντα ένα βήμα προετοιμασίας που λείπει: το ISO 32000-2 §7.6.4.3.3 απαιτεί τον κωδικό να επεξεργαστεί με το προφίλ SASLprep του stringprep πριν κωδικοποιηθεί σε UTF-8 και κερματιστεί (hashed). Το PDF Library for Delphi, η βιβλιοθήκη PDF για Delphi και C++Builder, εκτελεί αυτή την προετοιμασία μέσα στις Encrypt, EncryptFile και DecryptFile

Αυτό δεν είναι η ιστορία του λάθος κωδικού ούτε η ιστορία των bits αδειών. Αν οι χρήστες σας πληκτρολογούν έναν κωδικό που δεν εκδώσατε ποτέ, ο μηχανισμός επανάληψης στο άρθρο για την επανάληψη κωδικών κρυπτογραφημένων PDF είναι αυτό που θέλετε, και αν προσπαθείτε να καταλάβετε τι πραγματικά επιβάλλει ένα υπάρχον αρχείο, ο έλεγχος κρυπτογράφησης και αδειών καλύπτει αυτό το έδαφος. Αυτό εδώ είναι πιο στενό και πιο περίεργο: ο κωδικός είναι σωστός, ο χρήστης τον πληκτρολόγησε σωστά, και το αρχείο εξακολουθεί να αρνείται να ανοίξει κάπου αλλού

Γιατί ένας μη ASCII κωδικός ανοίγει σε έναν reader αλλά όχι σε άλλον;

Επειδή τα δύο προγράμματα κερματίζουν διαφορετικές ακολουθίες byte από τα ίδια πληκτρολογήματα. Η παραγωγή κλειδιού αναθεώρησης 6 στο ISO 32000-2 §7.6.4.3.3 παίρνει τον κωδικό ως bytes UTF-8, τον περικόπτει σε 127 bytes, προσαρτά ένα salt, και τρέχει τον ενισχυμένο (hardened) hash· το αποτέλεσμα ελέγχεται έναντι των καταχωρίσεων /U και /O στο λεξικό κρυπτογράφησης. Τίποτα σε αυτή την αλυσίδα δεν είναι ασαφές. Ένα διαφορετικό byte οπουδήποτε στην είσοδο παράγει ένα εντελώς διαφορετικό digest, η επικύρωση αποτυγχάνει, και ο reader έχει ακριβώς ένα πράγμα να πει: λάθος κωδικός

Τα bytes αποκλίνουν επειδή το Unicode προσφέρει πολλούς τρόπους να πληκτρολογήσετε αυτό που φαίνεται ο ίδιος κωδικός. Ένας κινεζικός κωδικός μπορεί να φτάσει ως προσυντεθειμένοι χαρακτήρες από μια μέθοδο εισαγωγής και ως μορφές συμβατότητας από άλλη. Ένας γερμανικός ή γαλλικός κωδικός που αντιγράφηκε από επεξεργαστή κειμένου μπορεί να φέρει ένα NO-BREAK SPACE (U+00A0) εκεί όπου ο χρήστης πιστεύει ότι υπάρχει κανονικό διάστημα, ή ένα SOFT HYPHEN (U+00AD) που αποδίδεται ως τίποτα απολύτως. Το SASLprep υπάρχει για να συμπτύξει όλα αυτά σε μία κανονική μορφή πριν κάποιος κερματίσει οτιδήποτε, ώστε κάθε συμμορφούμενη υλοποίηση να παράγει το ίδιο κλειδί από την ίδια πρόθεση

Διάγραμμα του PDF Library for Delphi για τον ίδιο μη-ASCII κωδικό PDF που κατακερματίζεται σε δύο διαφορετικές ακολουθίες bytes UTF-8, όπου ο αναγνώστης που παραλείπει την προετοιμασία αποτυγχάνει στον έλεγχο /U και ο αναγνώστης με προετοιμασία SASLprep παράγει το λειτουργικό κλειδί
Ο αναγνώστης που παραλείπει την προετοιμασία κατακερματίζει διαφορετικά byte, οπότε η επικύρωση AES-256 καταρρέει σε ψευδή αναφορά λάθους κωδικού

Τι αλλάζει στην πραγματικότητα το SASLprep σε έναν κωδικό;

Το RFC 4013 ορίζει το SASLprep ως προφίλ του πλαισίου stringprep στο RFC 3454, και είναι τέσσερα διατεταγμένα βήματα αντί για έναν μετασχηματισμό. Η αντιστοίχιση έρχεται πρώτη: ο πίνακας C.1.2 του RFC 3454 (μη ASCII κενά) αντιστοιχίζεται στο U+0020, και ο πίνακας B.1 (χαρακτήρες που συνήθως αντιστοιχίζονται σε τίποτα) διαγράφεται εντελώς. Ακολουθεί κανονικοποίηση σε Unicode NFKC, που είναι το βήμα που συμπτύσσει χαρακτήρες συμβατότητας και ακολουθίες συνδυασμού. Μετά ο έλεγχος απαγορευμένης εξόδου απορρίπτει οτιδήποτε βρίσκεται στους πίνακες C.2.1 έως C.9. Τέλος ο κανόνας διεύθυνσης (bidirectional) από το RFC 3454 τμήμα 6 εφαρμόζεται στο κανονικοποιημένο string

Το PDF Library for Delphi υλοποιεί ολόκληρο το προφίλ στη μονάδα PDFlibSASLprep, η οποία εκθέτει ένα μόνο σημείο εισόδου. Η PLSASLprepPassword παίρνει τον ακατέργαστο κωδικό, γράφει την προετοιμασμένη μορφή σε παράμετρο var, και επιστρέφει False όταν ο κωδικός πρέπει να απορριφθεί. Η συνάρτηση είναι σκόπιμα ολοκληρωμένη στη διαδρομή επιτυχίας: ένας αμιγώς ASCII κωδικός επιστρέφει byte-identical, οπότε τίποτα δεν αλλάζει σε υπάρχουσες αναπτύξεις

uses
  PDFlibSASLprep;

var
  Prepared: WideString;
begin
  // RFC 4013: αντιστοίχιση, μετά NFKC, μετά απαγορευμένη έξοδος, μετά ο κανόνας bidi
  PLSASLprepPassword('I' + WideChar($00AD) + 'X', Prepared);  // -> 'IX'   το B.1 διαγράφει το SOFT HYPHEN
  PLSASLprepPassword('a' + WideChar($00A0) + 'b', Prepared);  // -> 'a b'  το C.1.2 αντιστοιχίζει το NBSP σε U+0020
  PLSASLprepPassword(WideString(WideChar($00AA)), Prepared);  // -> 'a'    το NFKC συμπτύσσει το ORDINAL INDICATOR
  PLSASLprepPassword(WideString(WideChar($2168)), Prepared);  // -> 'IX'   το NFKC συμπτύσσει το ROMAN NUMERAL NINE
  PLSASLprepPassword('user', Prepared);                       // -> 'user' το ASCII δεν αγγίζεται ποτέ
end;

Η ασάφεια του U+200B που οι πίνακες δεν επιλύουν

Ένα code point καταλήγει σε δύο πίνακες RFC 3454 ταυτόχρονα, και οι δύο πίνακες διαφωνούν. Το ZERO WIDTH SPACE (U+200B) πέφτει μέσα στο εύρος C.1.2 U+2000 έως U+200B, όπου ο κανόνας λέει να αντιστοιχιστεί στο U+0020, και πέφτει επίσης μέσα στο εύρος B.1 U+200B έως U+200D, όπου ο κανόνας λέει να διαγραφεί. Διαβάστε το βήμα αντιστοίχισης με οποιαδήποτε σειρά και παίρνετε διαφορετικά bytes από τον ίδιο κωδικό: το a+U+200B+b προετοιμάζεται σε a b κάτω από το C.1.2 και σε ab κάτω από το B.1. Το RFC 4013 ονομάζει και τους δύο πίνακες και δεν λέει ποιος υπερισχύει, οπότε αυτή είναι μια γνήσια ασάφεια στην προδιαγραφή και όχι σφάλμα ανάγνωσης. Το PDF Library for Delphi ελέγχει πρώτα τη συμμετοχή στο C.1.2 και επομένως αντιστοιχίζει το U+200B σε διάστημα, που είναι η συμπεριφορά στην οποία κατέληξαν άλλες ευρέως αναπτυγμένες υλοποιήσεις stringprep· η αντιστοίχιση με αυτές είναι το μόνο που έχει σημασία εδώ, επειδή ο στόχος είναι συμφωνία byte με όποιον reader τυχαίνει να χρησιμοποιεί ο πελάτης

Ανάγνωση παλιών αρχείων: προετοιμασμένος πρώτα, ακατέργαστος δεύτερος

Η διόρθωση δημιουργεί το δικό της πρόβλημα συμβατότητας. Κάθε αρχείο AES-256 που γράφτηκε πριν την αλλαγή κερμάτισε τον ακατέργαστο κωδικό UTF-8, οπότε το να κάνει τον reader αυστηρά συμμορφούμενο θα κλείδωνε τους πελάτες έξω από τα δικά τους αρχεία. Το PDF Library for Delphi το λύνει αυτό στην πλευρά ανάγνωσης δοκιμάζοντας δύο υποψηφίους με σειρά. Η TPDFDocument.SetPassword χτίζει μια λίστα υποψηφίων που ξεκινά με την προετοιμασμένη μορφή και επιστρέφει στην ακατέργαστη μορφή, και προσθέτει την προετοιμασμένη καταχώριση μόνο όταν το έγγραφο είναι πραγματικά AES-256 και οι δύο μορφές διαφέρουν. Για έναν κωδικό ASCII οι μορφές είναι πανομοιότυπες, η λίστα κρατά μία καταχώριση, και το κόστος όλου του μηχανισμού είναι μία μόνο σύγκριση string. Η DecryptFile κάνει το ίδιο πράγμα στη δική της απευθείας διαδρομή επανεγγραφής AES-256, καλώντας την PLDirectDecryptFileAES256 με τον προετοιμασμένο κωδικό πρώτα

var
  Lib: TPDFlib;
  Bytes: AnsiString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.DrawText(100, 100, 'saslprep roundtrip');
    // Οι τιμές Strength 3 και 4 είναι οι δύο τιμές AES-256· και οι δύο προετοιμάζονται πριν το hashing
    Lib.Encrypt('ow' + WideChar($00AD) + 'ner', 'pa' + WideChar($00AD) + 'ss', 4,
      Lib.EncodePermissions(1, 0, 0, 0, 0, 0, 0, 1));
    Bytes := Lib.SaveToString;
  finally
    Lib.Free;
  end;

  Lib := TPDFlib.Create;
  try
    // Το 'pass' είναι αυτό που παρήγαγε το SASLprep και αυτό που υπολογίζει κάθε συμμορφούμενος αναγνώστης,
    // οπότε η απλή μορφή ASCII ανοίγει ένα αρχείο που δημιουργήθηκε με τη μορφή soft-hyphen
    if Lib.LoadFromString(Bytes, 'pass') = 1 then
      Caption := IntToStr(Lib.PageCount);
  finally
    Lib.Free;
  end;
end;

Η εναλλακτική λύση φέρει μία φρουρά που αξίζει να αντιγραφεί. Η δεύτερη προσπάθεια στην DecryptFile τρέχει μόνο όταν η προετοιμασμένη και η ακατέργαστη μορφή διαφέρουν και η πρώτη προσπάθεια δεν ανέφερε κωδικό σκληρού σφάλματος. Μια δομική αποτυχία σημαίνει ότι η είσοδος είναι κατεστραμμένη ή δεν είναι η αναθεώρηση κρυπτογράφησης που υποθέσατε, και η επανάληψη ενός κατεστραμμένου αρχείου με άλλον κωδικό απλώς καίει μια δεύτερη πλήρη ανάλυση πάνω σε εχθρική είσοδο· η λογική πίσω από αυτό το αντανακλαστικό εκτίθεται στη σημείωση για την ασφαλή ανάλυση μη αξιόπιστων PDF. Σημειώστε επίσης ότι δεν υπάρχει εναλλακτική στην πλευρά εγγραφής, και αυτή η ασυμμετρία είναι σκόπιμη. Η ανάγνωση ανέχεται το ιστορικό, η εγγραφή όχι: κάθε νέο αρχείο AES-256 παίρνει τα συμμορφούμενα bytes

Ποιοι κωδικοί απορρίπτονται εντελώς, και τι είναι το σφάλμα 604;

Το SASLprep μπορεί να αρνηθεί έναν κωδικό εντελώς, και όταν το κάνει, η κρυπτογράφηση πρέπει να αποτύχει με θόρυβο αντί να αντικαταστήσει κάτι σιωπηλά. Οι Encrypt και EncryptFile προετοιμάζουν και τον κωδικό ιδιοκτήτη και τον κωδικό χρήστη όποτε το Strength είναι 3 ή 4, επιστρέφουν 0 σε άρνηση, και ορίζουν το LastErrorCode σε PDFLIB_ERROR_PASSWORD_SASLPREP, που είναι 604. Δύο οικογένειες εισόδου το ενεργοποιούν. Οι πίνακες απαγορευμένης εξόδου απορρίπτουν χαρακτήρες ελέγχου (C.2.1 και C.2.2), code points ιδιωτικής χρήσης (C.3), μη χαρακτήρες (C.4), μεμονωμένα surrogates (C.5), U+FFFD (C.6), χαρακτήρες περιγραφής ιδεογραμμάτων (C.7), και τα εύρη ελέγχου εμφάνισης και σήμανσης (C.8 και C.9). Ξεχωριστά, ο κανόνας bidi του RFC 3454 τμήμα 6 απορρίπτει κάθε string που περιέχει έναν χαρακτήρα RandALCat από τον πίνακα D.1 εκτός αν το string ξεκινά και τελειώνει με έναν τέτοιο χαρακτήρα και δεν περιέχει καθόλου γράμματα από αριστερά προς τα δεξιά

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // Το U+0007 είναι χαρακτήρας ελέγχου C.2.1, οπότε η προετοιμασία απορρίπτει τον κωδικό
    if Lib.Encrypt('owner', 'bad' + WideChar($0007), 4,
         Lib.EncodePermissions(1, 0, 0, 0, 0, 0, 0, 1)) = 0 then
    begin
      if Lib.LastErrorCode = PDFLIB_ERROR_PASSWORD_SASLPREP then  // 604
        ShowMessage('The password contains characters that PDF encryption does not permit.');
    end;
  finally
    Lib.Free;
  end;
end;

Αυτός ο κανόνας bidi είναι αυτός που θα εκπλήξει το γραφείο υποστήριξής σας. Ένας αραβικός ή εβραϊκός κωδικός που τελειώνει σε δυτικό ψηφίο, ή ένας με έναν αδέσποτο λατινικό χαρακτήρα στη μέση, απορρίπτεται από την προδιαγραφή ακόμη κι αν φαίνεται απόλυτα λογικός στο πεδίο εισαγωγής. Παρουσιάστε το 604 ως μήνυμα για τους χαρακτήρες του κωδικού, όχι ως γενική αποτυχία κρυπτογράφησης, αλλιώς κάποιος θα ξοδέψει ένα απόγευμα ψάχνοντας για bug στην παραγωγή κλειδιού σας

Διάγραμμα του PDF Library for Delphi για τα τέσσερα διατεταγμένα βήματα SASLprep του RFC 4013 που αντιστοιχίζει μη-ASCII κενά, διαγράφει χαρακτήρες που αντιστοιχίζονται στο τίποτα, διπλώνει φόρμες συμβατότητας με NFKC και εκτελεί τους ελέγχους prohibited-output και αμφίδρομης κατεύθυνσης για να παράγει έναν σταθερό προετοιμασμένο κωδικό
Η αντιστοίχιση, η κανονικοποίηση NFKC, η σάρωση απαγορευμένων εξόδων και ο κανόνας bidi εκτελούνται με τη σειρά και αφήνουν ανέπαφους τους κωδικούς μόνο-ASCII

Ειλικρινή όρια: NFKC, ένα προσεγγιστικό LCat, και μία παγίδα Delphi

Δύο μέρη της υλοποίησης είναι προσεγγίσεις, και τα δύο αξίζει να δηλωθούν καθαρά αντί να θαφτούν. Η κανονικοποίηση NFKC εκτελείται από το Windows API NormalizeString, φορτωμένο δυναμικά από το Normaliz.dll. Όταν αυτή η βιβλιοθήκη δεν είναι διαθέσιμη το αντιστοιχισμένο string χρησιμοποιείται μη κανονικοποιημένο, που σημαίνει ότι τα βήματα αντιστοίχισης και απαγόρευσης εξακολουθούν να τρέχουν αλλά η σύμπτυξη συμβατότητας όχι. Στην πράξη το DLL διατίθεται με κάθε έκδοση Windows από το Vista, οπότε η υποβαθμισμένη διαδρομή είναι ζήτημα προ-Vista και μη-Windows και όχι ζωντανό, αλλά ένας κωδικός που βασίζεται σε σύμπτυξη NFKC θα παρήγαγε διαφορετικά bytes εκεί και αυτή είναι μια πραγματική, αν και απομακρυσμένη, απόκλιση. Ο έλεγχος bidi είναι η δεύτερη προσέγγιση: η ανίχνευση χαρακτήρων LCat χρησιμοποιεί τα κοινά εύρη γραμμάτων αντί για τον πλήρη πίνακα D.2 του RFC 3454, και η κατεύθυνση αυτού του σφάλματος είναι αυτό που το κάνει αποδεκτό. Ένας χαμένος χαρακτήρας LCat μπορεί μόνο να κάνει τον κανόνα bidi να περάσει εκεί όπου η προδιαγραφή θα είχε απορρίψει, ποτέ το αντίστροφο, και ποτέ δεν αγγίζει τα βήματα αντιστοίχισης ή κανονικοποίησης, οπότε η προετοιμασμένη ακολουθία byte ενός αποδεκτού κωδικού παραμένει αμετάβλητη. Ο υπολειπόμενος κίνδυνος είναι επομένως απόκλιση πολιτικής και όχι απόκλιση byte: ένας κωδικός με εξωτικό σύστημα γραφής που μια αυστηρότερη υλοποίηση θα αρνούνταν εντελώς να δεχτεί. Κάθε κωδικός που αποδέχονται και οι δύο πλευρές κερματίζεται πανομοιότυπα, που είναι η ιδιότητα από την οποία πραγματικά εξαρτάται η διαλειτουργικότητα

Τέλος, μια παγίδα σύνταξης Delphi που κοστίζει μία ώρα αν δεν την έχετε συναντήσει πριν. Όταν μια συνάρτηση επιστρέφει διαδικαστικό τύπο, η ανάθεσή της χωρίς παρενθέσεις δεν την καλεί. Ο μεταγλωττιστής διαβάζει το Proc := GetNormalizeProc; ως λήψη της διεύθυνσης της ίδιας της GetNormalizeProc, και μετά αναφέρει E2009 με το μη χρήσιμο παράπονο ότι οι συμβάσεις κλήσης διαφέρουν, επειδή ο accessor χρησιμοποιεί την προεπιλεγμένη σύμβαση ενώ ο εισαγόμενος τύπος API είναι stdcall. Οι κενές παρενθέσεις είναι υποχρεωτικές

Διάγραμμα του PDF Library for Delphi για κρυπτογράφηση AES-256 που απορρίπτει κωδικούς των οποίων οι χαρακτήρες χτυπούν τους πίνακες prohibited-output C.2.1 έως C.9 ή παραβιάζουν τον αμφίδρομο κανόνα του RFC 3454, επιστρέφοντας μηδέν με LastErrorCode 604
Δύο οικογένειες εισόδου ενεργοποιούν τη διαδρομή άρνησης: οι απαγορευμένοι πίνακες σημείων κώδικα και ο αμφίδρομος κανόνας του RFC 3454
type
  TNormalizeString = function(NormForm: Integer; SrcString: PWideChar; SrcLength: Integer;
    DstString: PWideChar; DstLength: Integer): Integer; stdcall;

function GetNormalizeProc: TNormalizeString;   // φορτώνει το Normaliz.dll κατά την πρώτη χρήση
...
var
  Proc: TNormalizeString;
begin
  // Proc := GetNormalizeProc;   // E2009: διαβάζεται ως @GetNormalizeProc, οι συμβάσεις διαφέρουν
  Proc := GetNormalizeProc();    // σωστό: καλεί τον accessor και αναθέτει το αποτέλεσμά του
  if not Assigned(Proc) then
    Exit;                        // δεν υπάρχει διαθέσιμο NFKC, το αντιστοιχισμένο string χρησιμοποιείται ως έχει
end;

Η προετοιμασία κωδικού είναι μία από εκείνες τις λεπτομέρειες που ποτέ δεν εμφανίζεται σε λίστα χαρακτηριστικών και αποφασίζει αν ένα κρυπτογραφημένο έγγραφο επιβιώνει από την επαφή με έναν πελάτη σε άλλο locale. Τα σημεία εισόδου Encrypt, EncryptFile, DecryptFile και SetPassword που περιγράφονται εδώ είναι μέρος της losLab PDF Developer Library Pascal Edition για Delphi και C++Builder, της οποίας η σελίδα προϊόντος φέρει την πλήρη αναφορά κρυπτογράφησης και τον πλήρη πίνακα κωδικών σφάλματος