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.
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.
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.
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.
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.
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.
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.
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.
| Metoda | Liczba kluczy | Jak się łamie |
|---|---|---|
| Cezar | 25 | przegląd wszystkich kluczy — ułamek sekundy |
| Podstawieniowy | 26! ≈ 4 · 10²⁶ | analiza częstości liter i par liter |
| XOR jednobajtowy | 255 | przegląd wszystkich kluczy |
| AES-256 | 2²⁵⁶ | 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ł.
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.
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.
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: szyfruj, odszyfruj, złam
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)}");
}
}
}
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.IsLetterodsiewa 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 — poprawkanowa += 26jest tu jedynym powodem, dla którego deszyfrowanie w ogóle działa. StringBuilderz 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.
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.
// 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.
Zestawienie elementów
| Element | Zwraca | Działanie i ograniczenia |
|---|---|---|
znak - 'A' | int | Pozycja litery w alfabecie, od 0. Poprawna tylko dla liter łacińskich bez znaków diakrytycznych. |
(char)(baza + n) | char | Powrót z pozycji do znaku. Rzutowanie jest konieczne — dodawanie daje int. |
a % 26 | int | Reszta z dzielenia. Zachowuje znak dzielnej, więc dla liczb ujemnych wymaga poprawki += 26. |
char.IsLetter(z) | bool | Pozwala przepuścić cyfry i znaki interpunkcyjne bez zmian. |
char.IsUpper(z) | bool | Wybór bazy 'A' albo 'a' — dzięki temu wielkość liter zostaje zachowana. |
tekst.IndexOf(z) | int | Pozycja znaku w alfabecie-kluczu albo -1. Wartość -1 trzeba obsłużyć przed użyciem jako indeksu. |
a ^ k | int | Bitowa alternatywa wykluczająca z lekcji 10. Jest własną odwrotnością: (a ^ k) ^ k == a. |
liczba.ToString("X2") | string | Zapis szesnastkowy na dwóch znakach — do pokazania bajtów, które nie są drukowalne. |
StringBuilder | — | Budowanie szyfrogramu znak po znaku bez tworzenia tysięcy napisów (lekcja 29). |
Na co uważać
| Zapis | Problem |
|---|---|
Brak poprawki dla ujemnego % | Deszyfrowanie daje znaki spoza alfabetu. W C# -3 % 26 to -3, nie 23. |
| Jedna baza dla wszystkich liter | Użycie 'a' także dla wielkich liter wyrzuca je poza zakres — zamiast D pojawiają się znaki interpunkcyjne. |
| Szyfrowanie spacji i cyfr | Szyfrogram 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 wprost | Znaki 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 zabezpieczeniem | Cezar łamie się w 25 próbach. Do prawdziwych danych służą biblioteki kryptograficzne. |
Zadania
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.
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.
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.
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.
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.
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.
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.