Algorytmy tekstowe i szyfrowanie

Znak jest liczbą, więc tekst da się przeliczyć tak samo jak liczby. Napiszesz szyfr Cezara, złamiesz go w dwudziestu pięciu próbach i zobaczysz, dlaczego bezpieczeństwo bierze się z liczby kluczy, a nie z pomysłowości.

C# szyfr Cezara XOR modulo 55 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zaszyfrujesz i odszyfrujesz tekst szyfrem Cezara, zachowując wielkość liter
  • Wyjaśnisz, dlaczego reszta z dzielenia liczby ujemnej wymaga w C# poprawki
  • Zaimplementujesz szyfr podstawieniowy z kluczem będącym przestawionym alfabetem
  • Użyjesz operatora XOR i wyjaśnisz, dlaczego jest własną odwrotnością
  • Ocenisz, ile kluczy ma dany szyfr i co z tego wynika dla jego bezpieczeństwa

Przygotowanie: lekcje 01–29. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.

TEORIA

Szyfr Cezara — przesunięcie w alfabecie

Najstarszy opisany szyfr podstawieniowy: każdą literę zastępujemy literą oddaloną o ustaloną liczbę pozycji. Przy przesunięciu 3 litera A staje się D, a Z wraca na początek i staje się C.

zasada
jawny:      A B C D E F G ... X Y Z
zaszyfrowany: D E F G H I J ... A B C
              └──── przesuniecie o 3 ────┘

Z lekcji 07 wiesz, że znak jest liczbą. To wszystko, czego potrzeba: odejmij kod litery 'A', dodaj przesunięcie, weź resztę z dzielenia przez 26 i wróć do litery.

definicja metody
static char PrzesunLitere(char znak, int przesuniecie)
{
    if (!char.IsLetter(znak))
    {
        return znak;                       // cyfry, spacje i kropki zostawiamy
    }

    char baza = char.IsUpper(znak) ? 'A' : 'a';
    int pozycja = znak - baza;             // 0 dla A, 25 dla Z
    int nowa = (pozycja + przesuniecie) % 26;

    if (nowa < 0)
    {
        nowa += 26;                        // dla ujemnego przesuniecia
    }

    return (char)(baza + nowa);
}

Reszta z dzielenia liczby ujemnej

W C# -3 % 26 daje −3, a nie 23 — operator % zachowuje znak dzielnej. To nie jest matematyczne modulo. Dlatego przy deszyfrowaniu, gdzie przesunięcie jest ujemne, bez poprawki if (nowa < 0) nowa += 26; wyjdą znaki spoza alfabetu. To jedna z najczęstszych pułapek w zadaniach z szyfrowaniem.

Deszyfrowanie to ta sama metoda

Skoro szyfrowanie przesuwa o k, to deszyfrowanie przesuwa o -k. Nie potrzeba drugiej metody — wystarczy zmienić znak argumentu.

Program.cs
string tajne = Zaszyfruj("Atak o swicie", 3);
string jawne  = Zaszyfruj(tajne, -3);

ROT13

Przesunięcie o 13 ma ciekawą własność: ponieważ 13 + 13 = 26, ta sama operacja szyfruje i odszyfrowuje. Stąd nazwa ROT13 i jego popularność na dawnych forach do ukrywania puent i spoilerów. To nie jest zabezpieczenie — to tylko sposób, żeby tekst nie rzucał się w oczy przypadkiem.

TEORIA

Dlaczego to nie jest zabezpieczenie

Szyfr Cezara ma dokładnie 25 sensownych kluczy. Program, który wypróbuje wszystkie i wypisze wyniki, łamie go w ułamku sekundy — a człowiek rozpoznaje właściwy wariant na pierwszy rzut oka.

Program.cs
for (int klucz = 1; klucz <= 25; klucz++)
{
    Console.WriteLine($"{klucz,2}: {Zaszyfruj(tajne, -klucz)}");
}

To atak siłowy (ang. brute force): sprawdzamy wszystkie możliwości. Działa zawsze, gdy kluczy jest mało.

Szyfr podstawieniowy z dowolnym przestawieniem alfabetu ma już 26! ≈ 4 · 10²⁶ kluczy i atak siłowy przestaje mieć sens. Mimo to też jest łamany — analizą częstości. W polskim tekście litera a występuje najczęściej, w angielskim e. Wystarczy policzyć znaki w szyfrogramie i przypisać najczęstsze do najczęstszych.

MetodaLiczba kluczyJak się łamie
Cezar25przegląd wszystkich kluczy — ułamek sekundy
Podstawieniowy26! ≈ 4 · 10²⁶analiza częstości liter i par liter
XOR jednobajtowy255przegląd wszystkich kluczy
AES-2562²⁵⁶nie znamy metody szybszej niż przegląd — i ten jest niewykonalny

Czego z tej lekcji NIE wynika

Żaden z opisanych tu szyfrów nie nadaje się do ochrony prawdziwych danych. Uczymy się ich, bo są doskonałymi ćwiczeniami na pracę ze znakami, tablicami i arytmetyką modulo — a przy okazji pokazują, dlaczego bezpieczeństwo bierze się z liczby możliwych kluczy, a nie z pomysłowości. W prawdziwych programach używa się gotowych bibliotek (System.Security.Cryptography), a haseł nigdy się nie szyfruje — zapisuje się ich skróty funkcją przeznaczoną do haseł.

TEORIA

Szyfr podstawieniowy i XOR

Dowolne przestawienie alfabetu

Zamiast przesuwać o stałą liczbę, możemy podać cały przestawiony alfabet jako klucz. Litera o pozycji i w alfabecie jawnym zamienia się na znak o pozycji i w kluczu.

definicja metody
const string ALFABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const string KLUCZ   = "QWERTYUIOPASDFGHJKLZXCVBNM";

static char Podstaw(char znak, string z, string na)
{
    int i = z.IndexOf(char.ToUpper(znak));

    if (i < 0)
    {
        return znak;               // znak spoza alfabetu zostawiamy
    }

    char wynik = na[i];
    return char.IsLower(znak) ? char.ToLower(wynik) : wynik;
}

Szyfrowanie to Podstaw(znak, ALFABET, KLUCZ), a deszyfrowanie — te same argumenty w odwrotnej kolejności: Podstaw(znak, KLUCZ, ALFABET). Jedna metoda w obie strony.

Klucz musi być poprawny

Klucz jest sensowny tylko wtedy, gdy zawiera każdą literę dokładnie raz. Powtórzona litera sprawia, że deszyfrowanie przestaje być jednoznaczne. Sprawdzenie tego warunku to dobre ćwiczenie na tablice: policz wystąpienia każdej litery i upewnij się, że wszystkie wynoszą 1.

XOR — szyfr, który jest własną odwrotnością

Operator bitowy ^ z lekcji 10 ma własność, która czyni z niego najprostszy szyfr: (a ^ k) ^ k = a. Ta sama operacja z tym samym kluczem szyfruje i odszyfrowuje.

definicja metody
static string XorHex(string tekst, char klucz)
{
    StringBuilder wynik = new StringBuilder();

    foreach (char znak in tekst)
    {
        int zaszyfrowany = znak ^ klucz;
        wynik.Append(zaszyfrowany.ToString("X2")).Append(' ');
    }

    return wynik.ToString().TrimEnd();
}

Dlaczego wynik pokazujemy szesnastkowo

Po operacji XOR często powstają znaki niedrukowalne — dzwonek, znak końca wiersza, znak zerowy. Wypisanie ich wprost psuje wygląd konsoli, a zapis do pliku tekstowego może je uszkodzić. Dlatego wynik pokazujemy jako kody szesnastkowe (ToString("X2")) z lekcji 13. To ta sama zasada, dla której prawdziwe systemy kodują szyfrogramy Base64.

PRZYKŁAD

Przykład: szyfruj, odszyfruj, złam

Program.cs
using System.Text;

class Program
{
    static char PrzesunLitere(char znak, int przesuniecie)
    {
        if (!char.IsLetter(znak)) return znak;

        char baza = char.IsUpper(znak) ? 'A' : 'a';
        int nowa = (znak - baza + przesuniecie) % 26;
        if (nowa < 0) nowa += 26;

        return (char)(baza + nowa);
    }

    static string Cezar(string tekst, int przesuniecie)
    {
        StringBuilder wynik = new StringBuilder(tekst.Length);

        foreach (char znak in tekst)
        {
            wynik.Append(PrzesunLitere(znak, przesuniecie));
        }

        return wynik.ToString();
    }

    static void Main()
    {
        string jawny = "Spotkanie o 7 rano!";
        string tajny = Cezar(jawny, 3);

        Console.WriteLine($"jawny : {jawny}");
        Console.WriteLine($"tajny : {tajny}");
        Console.WriteLine($"wrocil: {Cezar(tajny, -3)}");
        Console.WriteLine();

        Console.WriteLine("Atak silowy na szyfrogram:");
        for (int klucz = 1; klucz <= 5; klucz++)
        {
            Console.WriteLine($"  klucz {klucz,2}: {Cezar(tajny, -klucz)}");
        }
    }
}
wynik w konsoli
jawny : Spotkanie o 7 rano!
tajny : Vsrwndqlh r 7 udqr!
wrocil: Spotkanie o 7 rano!

Atak silowy na szyfrogram:
  klucz  1: Urqvmcpkg q 7 tcpq!
  klucz  2: Tqpulbojf p 7 sbop!
  klucz  3: Spotkanie o 7 rano!
  klucz  4: Rontjzmhd n 7 qzmn!
  klucz  5: Qnmsiylgc m 7 pylm!

Co dzieje się po kolei

  • Cyfra 7, spacje i wykrzyknik przechodzą bez zmian — warunek char.IsLetter odsiewa je na wejściu.
  • Wielkość liter jest zachowana, bo bazą jest 'A' albo 'a' zależnie od znaku. Bez tego wielkie litery wypadłyby poza alfabet.
  • Przy kluczu ujemnym (znak - baza + przesuniecie) bywa ujemne — poprawka nowa += 26 jest tu jedynym powodem, dla którego deszyfrowanie w ogóle działa.
  • StringBuilder z lekcji 29 zamiast sklejania: dla długiego tekstu różnica jest wyraźna, a kod nie jest dłuższy.
  • Atak siłowy pokazuje właściwy tekst już przy trzeciej próbie. Tyle jest wart ten szyfr.

Polskie znaki

Ten algorytm obsługuje wyłącznie alfabet łaciński bez ogonków. Litera ł przejdzie przez char.IsLetter jako litera, ale 'ł' - 'a' da liczbę spoza zakresu 0–25 i wynik będzie przypadkowy. W zadaniach albo usuwamy polskie znaki przed szyfrowaniem, albo rozszerzamy alfabet o własną tablicę znaków — to drugie jest dobrym zadaniem na koniec lekcji.

TEORIA

Suma kontrolna — wykrywanie błędów, nie ukrywanie

Szyfrowanie ukrywa treść. Suma kontrolna robi coś innego: pozwala wykryć, że dane zostały przekłamane. Spotykasz ją codziennie — w numerze PESEL, NIP, ISBN i numerze konta.

definicja metody
// cyfra kontrolna PESEL: wagi 1,3,7,9,1,3,7,9,1,3
static bool CzyPoprawnyPesel(string pesel)
{
    if (pesel.Length != 11) return false;

    int[] wagi = { 1, 3, 7, 9, 1, 3, 7, 9, 1, 3 };
    int suma = 0;

    for (int i = 0; i < 10; i++)
    {
        if (!char.IsDigit(pesel[i])) return false;

        suma += (pesel[i] - '0') * wagi[i];
    }

    int kontrolna = (10 - suma % 10) % 10;

    return kontrolna == pesel[10] - '0';
}

Po co to działa

Pomyłka w jednej cyfrze prawie zawsze zmienia sumę ważoną, więc cyfra kontrolna przestaje pasować i formularz odrzuca wpis, zanim dane trafią do bazy. To nie ma nic wspólnego z bezpieczeństwem — każdy może policzyć cyfrę kontrolną. Chodzi o wyłapanie literówki, nie oszusta.

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZwracaDziałanie i ograniczenia
znak - 'A'intPozycja litery w alfabecie, od 0. Poprawna tylko dla liter łacińskich bez znaków diakrytycznych.
(char)(baza + n)charPowrót z pozycji do znaku. Rzutowanie jest konieczne — dodawanie daje int.
a % 26intReszta z dzielenia. Zachowuje znak dzielnej, więc dla liczb ujemnych wymaga poprawki += 26.
char.IsLetter(z)boolPozwala przepuścić cyfry i znaki interpunkcyjne bez zmian.
char.IsUpper(z)boolWybór bazy 'A' albo 'a' — dzięki temu wielkość liter zostaje zachowana.
tekst.IndexOf(z)intPozycja znaku w alfabecie-kluczu albo -1. Wartość -1 trzeba obsłużyć przed użyciem jako indeksu.
a ^ kintBitowa alternatywa wykluczająca z lekcji 10. Jest własną odwrotnością: (a ^ k) ^ k == a.
liczba.ToString("X2")stringZapis szesnastkowy na dwóch znakach — do pokazania bajtów, które nie są drukowalne.
StringBuilderBudowanie szyfrogramu znak po znaku bez tworzenia tysięcy napisów (lekcja 29).
CZĘSTE BŁĘDY

Na co uważać

ZapisProblem
Brak poprawki dla ujemnego %Deszyfrowanie daje znaki spoza alfabetu. W C# -3 % 26 to -3, nie 23.
Jedna baza dla wszystkich literUżycie 'a' także dla wielkich liter wyrzuca je poza zakres — zamiast D pojawiają się znaki interpunkcyjne.
Szyfrowanie spacji i cyfrSzyfrogram staje się nieczytelny, a zachowanie spacji i tak nie zmniejsza bezpieczeństwa Cezara — on go po prostu nie ma.
Polskie znaki w tekście jawnym'ą' - 'a' daje liczbę spoza 0–25. Usuń ogonki albo zbuduj własny alfabet.
Wypisywanie wyniku XOR wprostZnaki niedrukowalne psują konsolę i plik. Pokazuj kody szesnastkowe.
Klucz podstawieniowy z powtórzoną literąDeszyfrowanie przestaje być jednoznaczne — dwie litery jawne dają ten sam znak.
Nazywanie tego zabezpieczeniemCezar łamie się w 25 próbach. Do prawdziwych danych służą biblioteki kryptograficzne.
ZADANIA

Zadania

ZAD 1Cezar w obie strony★☆☆

Napisz metody Zaszyfruj(string, int) i Odszyfruj(string, int), przy czym druga ma wywoływać pierwszą z ujemnym przesunięciem. Sprawdź dla przesunięcia 3, 13 i 25 oraz dla tekstu zawierającego cyfry i znaki interpunkcyjne.

ZAD 2ROT13★☆☆

Sprawdź doświadczalnie, że przesunięcie o 13 zastosowane dwa razy zwraca tekst pierwotny. Wyjaśnij w komentarzu, dlaczego tak się dzieje, odwołując się do działania modulo.

ZAD 3Atak siłowy★★☆

Dla szyfrogramu "Mrgybm xifjsbua" wypisz wszystkie 25 możliwych odszyfrowań wraz z numerem klucza i wskaż właściwy. Następnie dopisz proste kryterium automatyczne: wariant, w którym występuje najwięcej trzyliterowych ciągów ze spacjami wokół, jest najbardziej prawdopodobny.

ZAD 4Szyfr podstawieniowy★★☆

Zaimplementuj szyfrowanie i deszyfrowanie kluczem będącym przestawionym alfabetem. Dopisz metodę bool CzyKluczPoprawny(string klucz), sprawdzającą, że klucz ma 26 znaków i każdą literę dokładnie raz.

ZAD 5XOR i podgląd bajtów★★☆

Zaszyfruj tekst operatorem XOR z kluczem będącym jednym znakiem i wypisz wynik szesnastkowo. Odszyfruj go tą samą metodą i porównaj z oryginałem. Sprawdź, co się stanie, gdy kluczem będzie znak '\0', i wyjaśnij wynik.

ZAD 6Alfabet z polskimi znakami★★★

Rozszerz szyfr Cezara o polskie znaki. Zbuduj własny napis-alfabet "aąbcćdeęfghijklłmnńoópqrsśtuvwxyzżź", a pozycję litery ustalaj metodą IndexOf zamiast odejmowania kodów. Zachowaj wielkość liter i sprawdź program na zdaniu zawierającym wszystkie polskie znaki. Zastanów się, ile kluczy ma teraz twój szyfr i czy zmienia to cokolwiek w jego bezpieczeństwie.

PODSUMOWANIE

Co trzeba zapamiętać

  • Szyfr Cezara przesuwa litery o stałą wartość; deszyfrowanie to przesunięcie o tę samą wartość ze znakiem minus.
  • W C# % zachowuje znak dzielnej — dla wyniku ujemnego trzeba dodać rozmiar alfabetu.
  • Zachowanie wielkości liter wymaga wyboru bazy 'A' albo 'a' dla każdego znaku osobno.
  • Cezar ma 25 kluczy i łamie się atakiem siłowym; szyfr podstawieniowy — analizą częstości liter.
  • XOR jest własną odwrotnością, dlatego ta sama metoda szyfruje i odszyfrowuje; wynik pokazujemy szesnastkowo.
  • Suma kontrolna nie ukrywa danych — pozwala wykryć przekłamanie, jak cyfra kontrolna w numerze PESEL.
  • To są ćwiczenia algorytmiczne, nie zabezpieczenia. Prawdziwe dane chroni się gotowymi bibliotekami.

Dokumentacja: Microsoft Learn — temat tej lekcji.