Πώς ένας απλός τελεστής bit-προς-bit μπορεί να κρυπτογραφεί και να αποκρυπτογραφεί δεδομένα — με το ίδιο ακριβώς κλειδί.
Το XOR (αποκλειστικό «ή») συγκρίνει δύο bits και
επιστρέφει 1 όταν αυτά είναι διαφορετικά,
αλλιώς 0. Στη γλώσσα C ο τελεστής XOR είναι το
^ (caret).
Άλλαξε τον χαρακτήρα και το κλειδί
παρακάτω. Παρατήρησε πώς κάθε bit του αποτελέσματος είναι 1
ακριβώς όταν τα αντίστοιχα bits του χαρακτήρα και του κλειδιού διαφέρουν.
Η ιδιότητα-κλειδί του XOR είναι ότι αν εφαρμόσεις την ίδια πράξη δύο φορές με το ίδιο κλειδί b, παίρνεις πίσω την αρχική τιμή a:
Αυτό σημαίνει πως το ίδιο πρόγραμμα κάνει και κρυπτογράφηση και αποκρυπτογράφηση — απλώς τρέχει δύο φορές με το ίδιο κλειδί. Δεν χρειαζόμαστε ξεχωριστή λογική «για να ξεκλειδώσουμε»: το XOR είναι ο δικός του αντίστροφος.
feof()
Η feof() μας λέει αν φτάσαμε στο τέλος ενός αρχείου.
Δεν «προβλέπει» όμως το τέλος — γίνεται true μόνο
αφού μια προσπάθεια ανάγνωσης (fscanf,
fgetc) αποτύχει επειδή δεν υπάρχουν άλλα δεδομένα.
Γι' αυτό η σωστή σειρά είναι: πρώτα διαβάζω, μετά ελέγχω.
while (!feof(fp)) { fscanf(fp, "%c", &ch); fprintf(fp_out, "%c", ch); }
Ο έλεγχος γίνεται πριν την ανάγνωση. Όταν η
τελευταία fscanf αποτύχει, η ch κρατάει
ακόμα την παλιά της τιμή — και γράφεται ξανά
ως φάντασμα στο αρχείο εξόδου.
while (1) { fscanf(fp, "%c", &ch); if (feof(fp)) break; fprintf(fp_out, "%c", ch); }
Διαβάζουμε πρώτα, ελέγχουμε μετά.
Αν η ανάγνωση χτύπησε στο τέλος, βγαίνουμε με break
χωρίς να γράψουμε ψευδή χαρακτήρα. Αυτό είναι το standard C idiom.
Ο πυρήνας του προγράμματος είναι ένας απλός βρόχος που διαβάζει
χαρακτήρα-χαρακτήρα από το αρχείο εισόδου με τη
fscanf, εφαρμόζει XOR με το κλειδί, και γράφει το
αποτέλεσμα στο αρχείο εξόδου με τη fprintf.
Ο τερματισμός του βρόχου ελέγχεται με τη feof.
#include <stdio.h> int main() { // Δηλώσεις δεικτών αρχείων FILE *fp_in; // δείκτης για το αρχείο εισόδου (αρχικό κείμενο) FILE *fp_out; // δείκτης για το αρχείο εξόδου (κρυπτογραφημένο) // Κοινός πίνακας χαρακτήρων που θα χρησιμοποιηθεί διαδοχικά // για το όνομα του αρχείου εισόδου και του αρχείου εξόδου. // Αφού το πρώτο όνομα αξιοποιηθεί στη fopen, ο πίνακας // ξαναχρησιμοποιείται για το δεύτερο - οικονομία μνήμης. char str[100]; char ch; // ο τρέχων χαρακτήρας που διαβάζουμε από το αρχείο char key_ch; // ο χαρακτήρας-κλειδί για την κρυπτογράφηση // ----- Άνοιγμα αρχείου εισόδου ----- printf("Enter input file: "); scanf("%s", str); // διαβάζουμε το όνομα αρχείου ως string // Άνοιγμα του αρχείου εισόδου σε λειτουργία ανάγνωσης ("r" = read) fp_in = fopen(str, "r"); if (fp_in == NULL) { // Αν η fopen απέτυχε (π.χ. δεν υπάρχει το αρχείο), // τυπώνουμε μήνυμα σφάλματος και τερματίζουμε με return 1 // (μη μηδενική τιμή = σήμα σφάλματος προς το λειτουργικό). printf("Error: Input file can't be loaded\n"); return 1; } // ----- Άνοιγμα αρχείου εξόδου ----- printf("Enter output file: "); scanf("%s", str); // ξαναχρησιμοποιούμε τον ίδιο πίνακα str // Άνοιγμα του αρχείου εξόδου σε λειτουργία εγγραφής ("w" = write). // Αν το αρχείο δεν υπάρχει, δημιουργείται· αν υπάρχει, σβήνεται. fp_out = fopen(str, "w"); if (fp_out == NULL) { // Πρώτα κλείνουμε το ήδη ανοιχτό fp_in για να μη μείνει // ορφανός δείκτης αρχείου, και μετά τερματίζουμε με return 1. fclose(fp_in); printf("Error: Output file can't be created\n"); return 1; } // ----- Είσοδος του χαρακτήρα-κλειδιού ----- // Η getchar() εδώ είναι ΠΟΛΥ σημαντική! // Όταν ο χρήστης πάτησε Enter στο προηγούμενο scanf("%s", ...), // ο χαρακτήρας '\n' έμεινε αδιάβαστος στο stdin. // Αν τώρα κάναμε scanf("%c", &key_ch) χωρίς αυτή τη getchar(), // η scanf θα "άρπαζε" αυτό το '\n' ως κλειδί - κρίσιμο bug! getchar(); printf("Enter key char: "); scanf("%c", &key_ch); // διαβάζουμε τον πραγματικό χαρακτήρα-κλειδί // ----- Κύριος βρόχος κρυπτογράφησης ----- // // Ο βρόχος while(1) είναι ΑΤΕΡΜΟΝΟΣ (infinite loop). // Η έξοδος γίνεται μέσα από το if/break όταν εντοπίσουμε EOF. // // Λογική: // 1) Διαβάζουμε έναν χαρακτήρα από το αρχείο εισόδου με fscanf. // 2) Ελέγχουμε αν φτάσαμε στο τέλος του αρχείου με feof. // ΠΡΟΣΟΧΗ: Ο έλεγχος γίνεται ΜΕΤΑ την ανάγνωση! Η feof() // γίνεται true μόνο αφού η fscanf αποτύχει να διαβάσει. // Αν ελέγχαμε ΠΡΙΝ, ο τελευταίος χαρακτήρας θα γραφόταν // δύο φορές στο αρχείο εξόδου. // 3) Αν δεν είμαστε στο τέλος, εφαρμόζουμε XOR με το κλειδί // και γράφουμε το αποτέλεσμα στο αρχείο εξόδου. // // Η πράξη ch ^ key_ch είναι το bitwise XOR στη C: // - Λειτουργεί bit-προς-bit στο byte του χαρακτήρα. // - Ισχύει η ιδιότητα: (a ^ b) ^ b = a // - Άρα το ΙΔΙΟ πρόγραμμα με το ΙΔΙΟ κλειδί χρησιμεύει // και για κρυπτογράφηση και για αποκρυπτογράφηση. while (1) { fscanf(fp_in, "%c", &ch); // διαβάζουμε έναν χαρακτήρα if (feof(fp_in)) // ελέγχουμε ΜΕΤΑ την ανάγνωση break; // αν τέλος αρχείου, βγαίνουμε // Γράφουμε στο αρχείο εξόδου τον χαρακτήρα κρυπτογραφημένο fprintf(fp_out, "%c", ch ^ key_ch); } // ----- Ολοκλήρωση ----- printf("\n\nThe encryption was completed!\n\n"); // Κλείσιμο αρχείων - απαραίτητο για να αποθηκευτούν σωστά // τα δεδομένα στον δίσκο (flush του buffer εγγραφής). fclose(fp_in); fclose(fp_out); return 0; }
getchar() πριν το κλειδί;Μετά τη scanf("%s", str), μένει στο stdin ο
χαρακτήρας \n από το Enter του χρήστη. Αν δεν τον
καταναλώσουμε με τη getchar(), η επόμενη
scanf("%c", &key_ch) θα διαβάσει αυτόν αντί για
το πραγματικό κλειδί.
feof ΜΕΤΑ την ανάγνωση;Η feof() γίνεται true μόνο αφού η fscanf
αποτύχει να διαβάσει χαρακτήρα. Γι' αυτό διαβάζουμε πρώτα και
ελέγχουμε μετά. Αν ελέγχαμε πριν, ο τελευταίος χαρακτήρας θα
γραφόταν δύο φορές στο αρχείο εξόδου.
^ (bitwise XOR)Δουλεύει bit-προς-bit πάνω στο byte του χαρακτήρα. Επειδή
ισχύει (a ^ b) ^ b = a, το ίδιο πρόγραμμα με το ίδιο
κλειδί χρησιμεύει και για κρυπτογράφηση και για αποκρυπτογράφηση
— δεν χρειαζόμαστε ξεχωριστό «decryption» πρόγραμμα.
strΟ πίνακας str[100] χρησιμοποιείται διαδοχικά για
το όνομα εισόδου και για το όνομα εξόδου. Αφού το πρώτο όνομα
έχει ήδη αξιοποιηθεί στη fopen, η μνήμη του
ελευθερώνεται για το δεύτερο — οικονομία RAM.