Technical Article

Γιατί το Excel απορρίπτει το κρυπτογραφημένο βιβλίο εργασίας σας: ECB και RC4

Γράφετε ένα βιβλίο εργασίας, το κρυπτογραφείτε με έναν κωδικό πρόσβασης, παραδίδετε το αρχείο σε έναν συνάδελφο, και ο συνάδελφος το ανοίγει στο Excel. Το Excel ζητά τον κωδικό πρόσβασης. Ο συνάδελφος τον πληκτρολογεί, και το Excel τον δέχεται. Μέχρι στιγμής η κρυπτογράφηση φαίνεται σωστή. Στη συνέχεια, το Excel εμφανίζει ένα παράθυρο διαλόγου που λέει ότι το αρχείο είναι κατεστραμμένο και δεν μπορεί να ανοίξει, ή ανοίγει σε ένα φύλλο με ακατανόητα κελιά. Ο κωδικός πρόσβασης ήταν σωστός. Το αρχείο είναι κατεστραμμένο παρ' όλα αυτά. Αυτή είναι η πιο αποπροσανατολιστική κατάσταση αποτυχίας στην κρυπτογράφηση του Office, επειδή το μέρος που επιβεβαιώνει ότι ο κωδικός πρόσβασης είναι σωστός και το μέρος που κρατά τα δεδομένα σας προστατεύονται από δύο διαφορετικές λειτουργίες, και το να κάνετε τη μία σωστά δεν εγγυάται σε τίποτα την άλλη.

Και τα δύο σφάλματα που περιγράφονται εδώ είχαν ακριβώς αυτό το σχήμα. Σε κάθε περίπτωση, ο επαληθευτής (verifier) περνούσε και το σώμα όχι, γεγονός που σας στέλνει να ψάχνετε για σφάλμα στον κωδικό πρόσβασης ή στην παραγωγή κλειδιού που δεν υπάρχει. Το πραγματικό σφάλμα ήταν στη συνέχεια, στον τρόπο με τον οποίο μετασχηματίστηκαν τα byte του πακέτου. Τα δύο σφάλματα είναι ανεξάρτητα, το ένα στη διαδρομή AES και το άλλο στη διαδρομή RC4, αλλά μοιράζονται το ίδιο πρόβλημα διάγνωσης, οπότε αξίζει να δούμε γιατί ένα ημιτελώς σωστό αποτέλεσμα είναι το πιο δύσκολο να ερμηνευτεί.

Γιατί ένας σωστός κωδικός πρόσβασης δεν αποδεικνύει τίποτα για το σώμα

Η μορφή που χρησιμοποιεί το σύγχρονο κρυπτογραφημένο XLSX είναι η ECMA-376 Standard Encryption, και αποθηκεύει δύο κρυπτογραφημένα πράγματα δίπλα-δίπλα. Το ένα είναι το EncryptionVerifier: ένα μικρό μπλοκ που κρατά μια τυχαία τιμή και τον κατακερματισμό (hash) αυτής της τιμής, κρυπτογραφημένα με το κλειδί που παράγεται από τον κωδικό πρόσβασης. Το άλλο είναι το EncryptedPackage: ολόκληρο το κοντέινερ zip του βιβλίου εργασίας, κρυπτογραφημένο με το ίδιο κλειδί. Ο επαληθευτής υπάρχει ώστε ένας αναγνώστης να μπορεί να επιβεβαιώσει έναν κωδικό πρόσβασης πριν καταβάλει προσπάθεια σε megabytes σώματος δεδομένων. Αποκρυπτογραφήστε τον επαληθευτή, κατακερματίστε την τυχαία τιμή, συγκρίνετέ την με τον αποθηκευμένο κατακερματισμό, και αν ταιριάζουν ο κωδικός πρόσβασης είναι σωστός.

Η παγίδα είναι ότι ο επαληθευτής και το πακέτο κρυπτογραφούνται με ξεχωριστές κλήσεις σε ξεχωριστά buffers. Ένα κλειδί που παράγεται σωστά θα αποκρυπτογραφήσει σωστά τον επαληθευτή ανεξάρτητα από το τι θα συμβεί στο πακέτο στη συνέχεια. Έτσι, εάν η παραγωγή του κλειδιού σας είναι σωστή αλλά ο μετασχηματισμός του πακέτου σας είναι λάθος, το Excel επιβεβαιώνει τον κωδικό πρόσβασης από τον επαληθευτή και στη συνέχεια αποτυγχάνει στο σώμα. Το σύμπτωμα εμφανίζεται ως "σωστός κωδικός, κατεστραμμένο αρχείο", γεγονός που στρέφει την έρευνα στη διαδρομή του κωδικού πρόσβασης, η οποία είναι το μόνο μέρος που δεν ήταν ποτέ σπασμένο. Ο ίδιος διαχωρισμός διέπει την παλαιά περίπτωση RC4: ο κατακερματισμός του επαληθευτή ελέγχεται πρώτος, και ένα σώμα που αποκλίνει εκτός φάσης εξακολουθεί να αφήνει αυτόν τον έλεγχο ανέπαφο.

Σφάλμα ένα: AES σε ECB, όχι CBC

Το [MS-OFFCRYPTO] §2.3.4.15 ορίζει ότι το Standard Encryption κρυπτογραφεί το πακέτο με AES σε λειτουργία Electronic Codebook (ECB). Κάθε μπλοκ 16 byte του συμπληρωμένου πακέτου κρυπτογραφείται ανεξάρτητα με το ίδιο κλειδί. Δεν υπάρχει σύνδεση (chaining) μεταξύ των μπλοκ και δεν υπάρχει διάνυσμα αρχικοποίησης (initialization vector). Αυτή είναι μια ασυνήθιστη επιλογή για τα σύγχρονα δεδομένα, όπου η ECB συνήθως αποφεύγεται, αλλά η διαλειτουργικότητα (interop) δεν είναι μέρος για να αμφισβητήσετε την προδιαγραφή. Το Excel αποκρυπτογραφεί το πακέτο ως ECB, επομένως ένας παραγωγός πρέπει να το κρυπτογραφεί ως ECB, διαφορετικά οι δύο πλευρές δεν θα συμφωνήσουν.

Το σφάλμα ήταν ότι το πακέτο κρυπτογραφούνταν με AES σε λειτουργία CBC χρησιμοποιώντας ένα διάνυσμα αρχικοποίησης με όλα μηδενικά. Να γιατί αυτό σχεδόν λειτουργεί, και γιατί το σχεδόν είναι το χειρότερο σημείο για να καταλήξετε. Στη CBC, το πρώτο μπλοκ απλού κειμένου υφίσταται XOR με το IV πριν από την κρυπτογράφηση. Όταν το IV είναι όλο μηδενικά, αυτό το XOR δεν αλλάζει τίποτα, οπότε το πρώτο μπλοκ της CBC-με-μηδενικό-IV παράγει ακριβώς το ίδιο κρυπτογραφημένο κείμενο με την ECB. Από το δεύτερο μπλοκ και μετά η CBC τροφοδοτεί το προηγούμενο κρυπτογραφημένο μπλοκ στο επόμενο, οπότε κάθε μπλοκ μετά το πρώτο αποκλίνει από την ECB.

Τώρα τοποθετήστε αυτό στη δομή. Η διάταξη του πακέτου τοποθετεί ένα πρόθεμα μήκους 8 byte little-endian στην αρχή, οπότε τα μέρη του αρχείου που ελέγχει το Excel νωρίτερα βρίσκονται στο πρώτο ή στο δεύτερο μπλοκ. Ένα πρώτο μπλοκ που τυχαίνει να ταιριάζει σημαίνει ότι η πρώτη επικύρωση περνά, ενώ κάθε επόμενο μπλοκ αποκρυπτογραφείται σε θόρυβο. Η διόρθωση δεν είναι περίπλοκη μόλις οριστεί η λειτουργία: κρυπτογραφήστε κάθε μπλοκ 16 byte με ECB και σταματήστε τη σύνδεση (chaining). Στη μηχανή, η XlsEncryptStdPackage διασχίζει το συμπληρωμένο buffer σε βήματα 16 byte και καλεί τη AESEncryptECB128Block σε καθένα από αυτά, η οποία είναι η ίδια πρωτογενής συνάρτηση που χρησιμοποιείται ήδη για τα μπλοκ του επαληθευτή. Η πηγή φέρει ένα σχόλιο στον βρόχο που αναφέρει ξεκάθαρα τον κανόνα: η CBC με μηδενικό IV ταιριάζει με την ECB μόνο για το πρώτο μπλοκ, οπότε το υπόλοιπο πακέτο θα αποκρυπτογραφούνταν σε σκουπίδια και το Excel θα το απέρριπτε.

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('report.xlsx');
    // SaveAsEncrypted serializes the workbook, then runs the
    // ECMA-376 Standard Encryption pipeline: AES-128 ECB over the
    // package per [MS-OFFCRYPTO] 2.3.4.15. Returns 1 on success.
    if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
      raise Exception.Create('Encryption failed');
  finally
    Book.Free;
  end;
end;

Σφάλμα δύο: το re-key του RC4 αποκλίνει εκτός φάσης

Η παλιά διαδρομή .xls χρησιμοποιεί το σχήμα RC4 CryptoAPI, και ο κανόνας του είναι διαφορετικού είδους. Το [MS-OFFCRYPTO] §2.3.6 ορίζει ότι ο κρυπτογράφος αλλάζει κλειδί (re-keyed) σε κάθε όριο μπλοκ 1024 byte. Η ροή χωρίζεται σε μπλοκ των 1024 byte, ένα νέο κλειδί RC4 παράγεται για το μπλοκ 0, 1, 2 και ούτω καθεξής, και μέσα σε κάθε μπλοκ η ροή κλειδιού (keystream) καταναλώνεται συνεχώς από byte σε byte. Δύο σταθερές πρέπει να ισχύουν μαζί: αλλαγή κλειδιού σε κάθε όριο, και κατανάλωση της ροής κλειδιού χωρίς κενά μέσα σε ένα μπλοκ. Ο RC4 είναι ένας κρυπτογράφος ροής (stream cipher), οπότε η ροή κλειδιού του είναι μια ενιαία διατεταγμένη αλληλουχία. Το n-οστό byte που τραβάτε καθορίζεται από το πόσα byte έχετε τραβήξει πριν από αυτό. Η αποκρυπτογράφηση είναι το ίδιο XOR έναντι της ίδιας αλληλουχίας, που σημαίνει ότι ο παραγωγός και ο καταναλωτής πρέπει να τραβούν ακριβώς τα ίδια byte στις ίδιες θέσεις.

Αυτή είναι και η όλη δυσκολία. Ένας κρυπτογράφος ροής δεν έχει επανασυγχρονισμό. Εάν σπαταλήσετε ένα byte ροής κλειδιού, κάθε byte μετά από αυτό υφίσταται XOR με λάθος byte ροής κλειδιού, και το σφάλμα δεν διορθώνεται ποτέ. Καταρρέει μέχρι το τέλος του μπλοκ και, μόλις η τρέχουσα θέση είναι λάθος, σε κάθε μπλοκ μετά από αυτό. Το σφάλμα εδώ έκανε ακριβώς αυτό. Ο μετρητής μπλοκ ξεκινούσε από μια τιμή φύλακα μείον ένα, και η ρουτίνα παράκαμψης (skip) υπέθετε ότι ο μετρητής ταίριαζε ήδη με το τρέχον μπλοκ. Ξεκινώντας από αυτόν τον φύλακα, άλλαζε κλειδί και εκτελούσε ένα πλήρες μπλοκ 1024 byte ροής κλειδιού που δεν θα έπρεπε ποτέ να είχε καταναλωθεί, και στη διαδικασία οδηγούσε το υπόλοιπο πλήθος σε αρνητικό αριθμό. Από εκείνο το σημείο ο αποκρυπτογραφητής ήταν ένα ολόκληρο μπλοκ εκτός φάσης. Ο επαληθευτής, που ελεγχόταν πριν από όλα αυτά, εξακολουθούσε να περνάει, οπότε ο κωδικός πρόσβασης φαινόταν σωστός ενώ κάθε κελί δεδομένων προέκυπτε ως σκουπίδια.

Η διορθωμένη λογική ζει στην TXLSDecrypterRC4. Και η Skip και η Decrypt μοιράζονται έναν βρόχο: αλλαγή κλειδιού μόνο όταν η τρέχουσα θέση περνά σε ένα νέο μπλοκ, όπου ο δείκτης μπλοκ είναι η θέση διαιρεμένη με το REKEY_BLOCK_SIZE (1024), και στη συνέχεια κατανάλωση μέχρι το υπόλοιπο του τρέχοντος μπλοκ και όχι περισσότερο. Η MakeKey καλείται με τον δείκτη μπλοκ, ποτέ με έναν παλιό δείκτη ή δείκτη φύλακα, και η θέση προχωρά κατά τον ακριβή αριθμό των byte που υποβλήθηκαν σε επεξεργασία, έτσι ώστε η Skip και η Decrypt να παραμένουν ευθυγραμμισμένες ως προς τη φάση με τον παραγωγό. Το μάθημα βρίσκεται στην ελάχιστη μονάδα: ένα μεμονωμένο χαμένο byte δεν είναι μικρό σφάλμα σε έναν κρυπτογράφο ροής, είναι πλήρης απώλεια όλων των δεδομένων στη συνέχεια.

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    // CanReadEncrypted checks the Compound File (OLE2) signature so
    // you can branch before attempting a normal Open. OpenEncrypted
    // routes plain files to Open and handles the encrypted container.
    if Book.CanReadEncrypted('legacy.xls') then
      Book.OpenEncrypted('legacy.xls', 'S3cret!')
    else
      Book.Open('legacy.xls');
    // read cells here
  finally
    Book.Free;
  end;
end;

Η διαλειτουργικότητα με μια παγωμένη προδιαγραφή απαιτεί αντιστοίχιση στο byte

Και τα δύο σφάλματα ανάγονται στην ίδια βασική αρχή, και αξίζει να αναφερθεί μόνη της επειδή αλλάζει τον τρόπο με τον οποίο σταθμίζετε τις επιλογές σχεδιασμού. Όταν ο καταναλωτής της εξόδου σας είναι ένα σταθερό εξωτερικό πρόγραμμα που δεν μπορείτε να αλλάξετε, η λειτουργία κρυπτογράφησης και ο ρυθμός re-key δεν είναι λεπτομέρειες υλοποίησης που μπορείτε να βελτιστοποιήσετε ή να απλουστεύσετε. Είναι μέρος του συμβολαίου. Το Excel θα αποκρυπτογραφήσει με ECB και θα αλλάξει κλειδί στα όρια των 1024 byte είτε σας αρέσουν αυτές οι επιλογές είτε όχι, και η μόνη σας δουλειά είναι να παράγετε byte που αποκρυπτογραφούνται στο αρχικό υπό αυτήν ακριβώς τη διαδικασία. Μια λειτουργία που είναι πιο σύγχρονη, ένα IV που φαίνεται ακίνδυνο, ένας μετρητής που ξεκινά από εκεί που φαίνεται φυσικό. Οποιοδήποτε από αυτά είναι ελάττωμα τη στιγμή που αποκλίνει από αυτό που περιμένει ο αναγνώστης. Η διαλειτουργικότητα έναντι μιας παγωμένης προδιαγραφής δεν είναι κατά προσέγγιση. Είναι byte-exact ή είναι χαλασμένη.

Αυτός είναι επίσης ο λόγος για τον οποίο ο επαληθευτής είναι ένας κακός έλεγχος από μόνος του. Σας λέει ότι η παραγωγή κλειδιού λειτουργεί, κάτι που είναι απαραίτητο αλλά απέχει πολύ από το να είναι επαρκές. Μια δοκιμή που ανοίγει μόνο ένα κρυπτογραφημένο αρχείο και επιβεβαιώνει ότι ο κωδικός πρόσβασης περνάει θα αναφέρει επιτυχία ενώ το σώμα είναι μη αναγνώσιμο. Μια πραγματική δοκιμή αποκρυπτογραφεί το πακέτο και συγκρίνει τα ανακτηθέντα byte με την αρχική είσοδο, ή εκτελεί round-trip σε ένα βιβλίο εργασίας μέσω κρυπτογράφησης και αποκρυπτογράφησης και διαβάζει τα κελιά πίσω. Ο επαληθευτής αποδεικνύει τον κωδικό πρόσβασης. Μόνο το σώμα αποδεικνύει την κρυπτογράφηση.

Ο υποστηριζόμενος τρόπος ανάγνωσης και εγγραφής προστατευμένων βιβλίων εργασίας

Η δημόσια επιφάνεια είναι μικρή. Για να γράψετε ένα προστατευμένο με κωδικό πρόσβασης σύγχρονο βιβλίο εργασίας, γεμίστε ή ανοίξτε ένα TXLSXWorkbook και καλέστε τη SaveAsEncrypted με ένα όνομα αρχείου και έναν κωδικό πρόσβασης. Σειριοποιεί το βιβλίο εργασίας και εκτελεί τη διοχέτευση Standard Encryption που διόρθωσε η πρώτη επισκευή, επιστρέφοντας 1 σε περίπτωση επιτυχίας. Για να διαβάσετε, καλέστε τη CanReadEncrypted για να ελέγξετε αν ένα αρχείο είναι ένα κρυπτογραφημένο κοντέινερ Compound File, και στη συνέχεια διακλαδώστε: η OpenEncrypted χειρίζεται την κρυπτογραφημένη διαδρομή και επιστρέφει στην Open για απλά αρχεία, και η Open με κωδικό πρόσβασης είναι διαθέσιμη απευθείας. Ο χειρισμός της λειτουργίας και ο βρόχος re-key που περιγράφονται παραπάνω βρίσκονται κάτω από αυτές τις κλήσεις. Εσείς παρέχετε τον κωδικό πρόσβασης και το όνομα αρχείου και η μηχανή ταιριάζει με την προδιαγραφή για λογαριασμό σας.

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('quarterly.xlsx');
    Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
    // Reopen on the consumer side
    Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
  finally
    Book.Free;
  end;
end;

Το σχήμα της προστατευμένης εξόδου, η ροή EncryptionInfo, τα μπλοκ verifier και η διάταξη του πακέτου καλύπτονται στο άρθρο μας σχετικά με την προστατευμένη με AES έξοδο XLSX. Για το ξεχωριστό ερώτημα του κλειδώματος σε επίπεδο φύλλου και πώς η προστασία αλληλεπιδρά με τη διαμόρφωση σελίδας και την εκτύπωση, δείτε το άρθρο για την προστασία, τη διαμόρφωση σελίδας και την εκτύπωση. Και τα δύο βασίζονται στη διαδρομή κρυπτογράφησης που περιγράφεται εδώ, η οποία παρέχεται ως μέρος του HotXLS spreadsheet component για Delphi και C++Builder, μαζί με τα API ανάγνωσης, συγγραφής και απόδοσης που καλύπτονται σε άλλα σημεία αυτού του ιστολογίου.