Μάθημα C // Άσκηση 2

Κρυπτογράφηση
με XOR

Πώς ένας απλός τελεστής bit-προς-bit μπορεί να κρυπτογραφεί και να αποκρυπτογραφεί δεδομένα — με το ίδιο ακριβώς κλειδί.

01000001 ⊕ 01001011
= 00001010
⊕ 01001011
= 01000001

01 — Ο κανόναςΠώς λειτουργεί το XOR

Το XOR (αποκλειστικό «ή») συγκρίνει δύο bits και επιστρέφει 1 όταν αυτά είναι διαφορετικά, αλλιώς 0. Στη γλώσσα C ο τελεστής XOR είναι το ^ (caret).

0 ⊕ 0
= 0 (ίδια)
0 ⊕ 1
= 1 (διαφορετικά)
1 ⊕ 0
= 1 (διαφορετικά)
1 ⊕ 1
= 0 (ίδια)
Ίδια bits → 0 Διαφορετικά bits → 1

02 — Δοκίμασέ τοΖωντανή κρυπτογράφηση

Άλλαξε τον χαρακτήρα και το κλειδί παρακάτω. Παρατήρησε πώς κάθε bit του αποτελέσματος είναι 1 ακριβώς όταν τα αντίστοιχα bits του χαρακτήρα και του κλειδιού διαφέρουν.

Πρόσεξε: ο τελευταίος χαρακτήρας ταυτίζεται απόλυτα με τον αρχικό. Με το ίδιο ακριβώς κλειδί, η δεύτερη εφαρμογή XOR ακυρώνει την πρώτη.

03 — Η μαγική ιδιότηταΓιατί λειτουργεί ως κρυπτογράφηση

Η ιδιότητα-κλειδί του XOR είναι ότι αν εφαρμόσεις την ίδια πράξη δύο φορές με το ίδιο κλειδί b, παίρνεις πίσω την αρχική τιμή a:

( a b ) b = a

Αυτό σημαίνει πως το ίδιο πρόγραμμα κάνει και κρυπτογράφηση και αποκρυπτογράφηση — απλώς τρέχει δύο φορές με το ίδιο κλειδί. Δεν χρειαζόμαστε ξεχωριστή λογική «για να ξεκλειδώσουμε»: το XOR είναι ο δικός του αντίστροφος.

04 — Πρώτη γνωριμίαΗ συνάρτηση 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.

Κανόνας: read first → check feof → process

05 — Ο κώδικας σε CΥλοποίηση βήμα προς βήμα

Ο πυρήνας του προγράμματος είναι ένας απλός βρόχος που διαβάζει χαρακτήρα-χαρακτήρα από το αρχείο εισόδου με τη 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;
}
01

Γιατί getchar() πριν το κλειδί;

Μετά τη scanf("%s", str), μένει στο stdin ο χαρακτήρας \n από το Enter του χρήστη. Αν δεν τον καταναλώσουμε με τη getchar(), η επόμενη scanf("%c", &key_ch) θα διαβάσει αυτόν αντί για το πραγματικό κλειδί.

02

Γιατί feof ΜΕΤΑ την ανάγνωση;

Η feof() γίνεται true μόνο αφού η fscanf αποτύχει να διαβάσει χαρακτήρα. Γι' αυτό διαβάζουμε πρώτα και ελέγχουμε μετά. Αν ελέγχαμε πριν, ο τελευταίος χαρακτήρας θα γραφόταν δύο φορές στο αρχείο εξόδου.

03

Ο τελεστής ^ (bitwise XOR)

Δουλεύει bit-προς-bit πάνω στο byte του χαρακτήρα. Επειδή ισχύει (a ^ b) ^ b = a, το ίδιο πρόγραμμα με το ίδιο κλειδί χρησιμεύει και για κρυπτογράφηση και για αποκρυπτογράφηση — δεν χρειαζόμαστε ξεχωριστό «decryption» πρόγραμμα.

04

Επανάχρηση του buffer str

Ο πίνακας str[100] χρησιμοποιείται διαδοχικά για το όνομα εισόδου και για το όνομα εξόδου. Αφού το πρώτο όνομα έχει ήδη αξιοποιηθεί στη fopen, η μνήμη του ελευθερώνεται για το δεύτερο — οικονομία RAM.