Szyfrowanie RSA Liczby pierwsze Na początek przypomnijmy sobie parę użytecznych wiadomości o liczbach pierwszych. Są one znane od starożytności a ich znaczenie jest ogromne – w matematyce i tym bardziej w kryptografii. Dlaczego, o tym przekonamy się już niedługo. Liczba pierwsza jaka jest każdy widzi. Jednak dla pewności przypomnimy definicje: Liczba pierwsza jest liczbą naturalną posiadającą dokładnie dwa różne podzielniki - 1 oraz samą siebie. Zatem nie jest liczbą pierwszą liczba 0 ani liczba 1. Najmniejszą liczbą pierwszą jest liczba 2, a jak dowiódł Euklides, liczb pierwszych jest nieskończenie dużo. Powstaje pytanie, jak sprawdzić czy dana liczba jest liczbą pierwszą, bądź też jak wygenerować liczby pierwsze. Jeśli chodzi o sprawdzenie czy dana liczba jest pierwsza to można posłużyć się bezpośrednio definicją liczby pierwszej, to znaczy sprawdzić czy jakakolwiek liczba w przedziale <2, p-1> (p jest sprawdzaną liczbą) dzieli bez reszty liczbę p. Jeśli tak, to liczba p nie jest liczbą pierwszą. Metoda ta oczywiście działać będzie, natomiast łatwo zauważyć, że dla dużych liczb wykonywać się będzie wiele niepotrzebnych operacji dzielenia. Wynika to z faktu, że biorąc pod uwagę dowolną liczbę naturalną, mogą zaistnieć 4 przypadki: 1. Liczba p jest pierwsza i w całym przedziale od 2 do p-1 nie posiada dzielnika. 2. Liczba p posiada pierwiastek będący jej podwójnym dzielnikiem, np. 25 = 5 x 5 3. Pierwiastek z p nie jest liczbą pierwszą, a jeden z dzielników jest większy od pierwiastka z p. Wtedy wszystkie inne dzielniki musza być mniejsze od pierwiastka z p. np. 22 = 2 x 11 4. Pierwiastek z p nie jest liczbą pierwszą i wszystkie jego dzielniki są mniejsze od pierwiastka z p. np. 36 = 2 x 2 x 3 x 3 Z powyższych punktów wynika prosty wniosek – jeśli p jest liczbą złożoną to posiada dzielniki w przedziale <2, p >. Wystarczy wiec sprawdzić podzielność tylko w tym przedziale. Jeśli żadna liczba z tego przedziału nie dzieli p to znaczy, że p jest pierwsza. Zysk jest duży, ale na tym nie koniec. Spójrzmy na kilka początkowych liczb pierwszych: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 ... Jeśli nie weźmiemy pod uwagę 2 oraz 3, każdą z nich można zapisać w postaci 6n – 1 lub 6n +1 dla pewnej liczby naturalnej n. Nie jest to przypadek. Spójrzmy: 1. 2. 3. 4. Liczby postaci 6n nie mogą być pierwsze gdyż są podzielne przez 2 oraz 3 Liczby 6n-2, 6n+2, 6n-4, 6n+4 są podzielne przez 2, wiec również nie są pierwsze. Liczby 6n-3 oraz 6n+3 nie są pierwsze gdyż dzieli je 3. Liczby 6n-5 oraz 6n+5 również wypadają, a dlaczego - pomyślcie sami. Wynika z tego, że każda liczba pierwsza będzie postaci 6n-1 lub 6n+1. Nie znaczy to wcale ze każda liczba takiej postaci jest pierwsza. Nie są to wzory na kolejne liczby pierwsze (taki wzór nie istnieje). Wzory te ograniczają nam jedynie zbiór kandydatów do sprawdzenia poprzednią metoda. Wystarczy, że będziemy sprawdzać liczby postaci 6n-1 oraz 6n+1 dla kolejnych liczb naturalnych. Oczywiste jest, ż żadna liczba pierwsza nie jest parzysta. Dodatkowo zauważmy, że nie trzeba sprawdzać podzielności przez 4, 6, 8 ,10... jeśli sprawdziło się podzielność przez 2, nie trzeba sprawdzać podzielności przez 6,9,12,15... jeśli się sprawdziło podzielność przez 3 itd. Wynika z tego, że dla danej liczby p wystarczy sprawdzić jej podzielność przez liczby pierwsze w p >. Jest to wygodne podejście jeśli generujemy liczby pierwsze w przedziale <2, przedziale np. <2,100000>, gdy generujemy liczby pierwsze po kolei. Sito Eratostenesa Powyższe metody nie są najlepsze, jeśli chodzi o wygenerowanie wszystkich liczb pierwszych mniejszych od zadanej np. 1000000. Dużo szybszą metodę podał starożytny uczony Eratostenes. Zamiast sprawdzać podzielność kolejnych liczb naturalnych przez znalezione liczby pierwsze, możemy wyrzucać ze zbioru liczb naturalnych wielokrotności kolejnych liczb naturalnych, które nie zostały wcześniej wyrzucone. Oto przykład: 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Bierzemy 2 i wyrzucamy wszystkie jej wielokrotności. Zrealizować to można za pomocą dodawania tej samej liczby, co jest wydajniejsze niż użyte wcześniej dzielenie. Otrzymujemy: 2 3 5 7 9 11 13 15 17 19 Następnie bierzemy 3 i usuwamy jej wielokrotności. Wynik: 2 3 5 7 11 13 17 19 W realizacji komputerowej najłatwiej jest reprezentować liczby za pomocą indeksów tablicy wartości logicznych. Wartość false na miejscu o indeksie i oznacza ze liczba i została wyrzucona ze zbioru. Oczywiście podczas realizacji algorytmu trzeba uważać by nie wykonywać zbędnych operacji. Biorąc kolejną liczbę i chcąc usuwać jej wielokrotności, należy sprawdzić czy ta liczba nie została sama wcześniej usunięta, jeśli tak to usunięte zostały także wszystkie jej wielokrotności, nie ma wiec sensu powtarzać tej czynności. Są możliwe i inne ulepszenia. Zauważmy, że w pierwszym kroku usuwamy wszystkie wielokrotności liczby 2. Kiedy w następnym kroku usuwamy wielokrotności 3, widzimy ze 6 = 3x2 już zostało usunięte, gdyż 6 dzieli się przez 2. Pierwszą usuniętą wielokrotnością liczby 3 jest jej kwadrat, czyli 9 = 3x3. Dla kolejnej liczby, czyli 5, usuwamy 10, ale ona już została usunięta (dzieli się przez 2), kolejno 15, ale ona również została wcześniej usunięta (dzieli się przez 3) Podobnie dla 20=5x4. Pierwszą usuniętą wielokrotnością liczby 5 jest znów jej kwadrat 25=5x5. Jak łatwo zauważyć jest tak zawsze dla kolejnych liczb i ich wielokrotności. Dla kolejnych liczb, ich wielokrotności mniejsze od ich kwadratu posiadają dzielniki równe wcześniej znalezionym liczbom pierwszym, zatem zostały usunięte już ze zbioru wcześniej. Mamy zatem bardzo użyteczny wniosek z powyższych rozważań. Jeśli p jest górnym ograniczeniem naszego zbioru, to co się stanie jeśli dojdziemy do p ? Wtedy pierwszą liczbą, którą wypada nam wyrzucić jest dopiero p. Zatem wielokrotności liczb większych od p zostały usunięte w trakcie usuwania wielokrotności ich czynników pierwszych. Cały proces można zatem przerwać po przekroczeniu granicy równej p . Zysk jest ogromny. Przykładowo, dla p =1000000 wystarczy, że dojdziemy do p’=1000. Tysiąc razy liczb mniej do przejrzenia to już coś. Oczywiście korzystne jest odpowiednie wyznaczenie pierwszej wielokrotności, która ma być usunięta. Z powyższych rozważań wynika, że nie trzeba usuwać wielokrotności mniejszych niż kwadrat kolejnej liczby, gdyż takie wielokrotności zostały usunięte wcześniej. W praktyce liczby pierwsze używane w kryptografii są dużo większe niż zakres liczb całkowitych w tradycyjnych językach programowania, takich jak C, Pascal. Często potrzebne są liczby pierwsze o długości 2000 – 4000 bitów lub nawet większe. Jednakże małe liczby pierwsze, jak zobaczymy dalej, również są przydatne. Powstaje jednak pytanie jak generować duże liczby pierwsze. Przedstawione powyżej metody są za mało wydajne by stosować je do generacji bardzo dużych liczb pierwszych. W praktyce zatem będziemy generować dużą liczbę i następnie poddawać ją testowi na „pierwszość”. Test pozwoli określić czy dana liczba jest pierwsza z pewną dokładnością, prawdopodobieństwem. Test powtórzony kilkakrotnie pozwoli nam zmniejszyć prawdopodobieństwo pomyłki. Nie uzyskamy zatem pewności, lecz dowolnie małe prawdopodobieństwo. Poznamy zatem metodę probabilistyczną generowania dużych liczb pierwszych, jednakże w praktyce obliczenia nasze przeprowadzać będziemy na liczbach tak dużych, na jakie pozwoli nam zakres języka w którym będziemy implementować algorytm. W zastosowaniach praktycznych skorzystalibyśmy z bibliotek implementujących liczby całkowite dowolnej precyzji. Bibliotekę taką można napisać samemu, jednakże jako że zadanie takie nie jest rzeczą łatwą, jeśli chcemy stworzyć rzeczywiście niezawodne klasy i funkcje, lepiej jest skorzystać z gotowych bibliotek dostępnych w Internecie lub skorzystać z języka programowania obsługującego liczby całkowite dowolnej wielkości Zanim jednak zajmiemy się przedstawieniem metody generowania dużych liczb pierwszych potrzebne nam będą pewne wiadomości. Największy wspólny dzielnik NWD Mając dwie liczby całkowite a i b ich największym wspólnym dzielnikiem nazywamy największą liczbę d=NWD(a,b) która dzieli bez reszty obie liczby a i b. Dla przypomnienia – najmniejsza wspólna wielokrotność dwóch liczb a i b NWW(a,b) to najmniejsza liczba całkowita, która jest podzielna przez a oraz b. Związek miedzy tymi dwoma wielkościami jest następujący: NWW(a,b)=ab/NWD(a,b). Jak zatem znaleźć NWD(a,b)? Rozwiązanie tego problemu znane jest od dawna. Wymyślił je starożytny matematyk Euklides. Algorytm Euklidesa DANE: dwie liczby całkowite a i b, a>b While b!=0 do (a, b) (b, a mod b) end return a Jak widać, w każdym kroku należy zastępować odpowiednio liczby a i b obliczając resztę z dzielenia aktualnego a przez aktualne b. Reszty te w każdym kolejnym kroku są coraz mniejsze, co łatwo sprawdzić, zatem algorytm zawsze będzie miał rozwiązanie. Dla przykładu znajdźmy NWD dla liczb 48 i 36 NWD(64, 36) = NWD(36, 64 mod 36) = NWD(36, 28)= NWD(28, 36 mod 28)= NWD(28,8)= NWD(8, 28 mod 8)= NWD(8, 4)= NWD(4, 8 mod 4)= NWD(4, 0) Zatem NWD(64, 36) = 4. Żadna inna liczba większa od 4 nie dzieli jednocześnie 64 i 36. Zamiast dokonywać dzielenia modulo można wykonywać odejmowania: Algorytm Euklidesa 2 DANE: dwie liczby całkowite a i b, a>b While b!=0 do (a, b) (b, a - b) end return a Przykład: NWD(64, 36)=NWD(36,64-36)= NWD(36,28)= NWD(28,36-28)= NWD(28,8)= NWD(8,288)= NWD(8,20) =NWD(20,8)= NWD(12,8)= NWD(8,4)= NWD(4,4)= NWD(4,0) Jak widać wynik jest ten sam jednak potrzeba było (w tym przypadku) więcej kroków. Jednak trzeba wziąć pod uwagę, że operacja dodawania (zatem również odejmowania) jest szybciej wykonywana niż dzielenie modulo. Często można przyśpieszyć działanie algorytmu Euklidesa dopuszczając dzielenie z ujemnymi resztami. Przykładowo: 64 = 1 x 36 + 28 ale również 64 = 2 x 36 – 8 Jeśli wybierać będziemy mniejsze reszty zmniejszymy ilość potrzebnych kroków. Przykład: NWD(64,36)= NWD(36,8) 36=4 x 8 + 4 oraz 36 = 5 x 8 – 4 (tu akurat bez różnicy) NWD(36,8)= NWD(8,4)= NWD(4,0)=4 Algorytm Euklidesa działa, ponieważ kolejne pary mają ten sam zbiór wspólnych dzielników, zatem w szczególności maja ten sam największy wspólny dzielnik. Nie będziemy jednak przytaczać dokładnego dowodu. Jeszcze inna odmiana algorytmu Euklidesa korzysta z następujących kroków: Algorytm Euklidesa 3 DANE: dwie liczby całkowite a i b, a>b WYJSCIE: d = NWD(a,b) While a!=b do Jeśli a i b są parzyste to d=2*d’, gdzie d’=NWD(a/2, b/2) Jeśli jedna jest parzysta a druga nieparzysta (np.b jest parzysta) to d=d’, gdzie d’=NWD(a,b/2) Jeśli obie są nieparzyste i np. a>b , to d=d’, gdzie d’=NWD(a-b,b) Jeśli a jest równe b to d=a end return a Przyklad: NWD(64,36)=2*NWD(32,18)=4*NWD(16,9)=4*NWD(8,9)=4*NWD(4,9)=4* NWD(2,9)=4* NWD(1,9)= 4* NWD(1,8)= 4* NWD(1,7)=...= 4* NWD(1,1)=4 W przykładzie powyżej ilość kroków jest zdecydowanie większa od ilości kroków w sposobie pierwszym, jednak metoda ta ma swoje zalety. Jak widać mamy w tej metodzie tylko dzielenie przez 2 i odejmowanie. Jest to szczególnie użyteczne gdy operujemy na liczbach w zapisie dwójkowym, gdzie dzielenie przez 2 sprowadza się do przesunięcia bitów w prawo. Przykładowo: 12 / 2 = 6 12(10) = 1100(2) (przesuniecie bitów – usuniecie bitu najmniej znaczącego) 6(10) = 110(2) Mnożenie przed dwa sprowadza się natomiast do przesunięcia bitów w lewo i dopisaniu na miejscu powstałego najmniej znaczącego bitu zera. 3(10) = 11(2) 2*3 = 6(10) = 110(2) Operacje przesunięcia bitów mogą być zaimplementowane bardzo wydajnie. Do czego się nam przyda algorytm Euklidesa, o tym za chwile. Teraz jeszcze zdefiniujmy pojecie względnej pierwszości dwóch liczb. Def. Dwie liczby a i b są względnie pierwsze, jeśli NWD(a,b)=1 Działania modulo Dodawanie, odejmowanie i mnożenie modulo nie różnią się zbytnio od swoich zwykłych odpowiedników. Polegają one na wykonaniu działania „normalnie” a następnie skróceniu wyniku modulo dana liczba. Przykładowo załóżmy, że będziemy wykonywać działania modulo p = 5. Zatem jeśli a = 7 oraz b=4 mamy: (a+b) mod p = (7+4) mod 5 =11 mod 5 = 1 (a-b) mod p = (7-4) mod 5 =3 mod 5 = 3 (a*b) mod p = (7*4) mod 5 =28 mod 5 = 3 Tutaj mała uwaga. Resztę przyjmować będziemy zawsze jako liczbę dodatnią większą lub równą zero i mniejszą od dzielnika. Jaki jest zatem wynik działania -1 mod 5 ? Mamy dwie możliwości zapisu –1 za pomocą 5: -1 = 0*5 –1 lub -1 = -1*5 + 4 Przyjmować będziemy tę drugą odpowiedż, czyli –1 mod 5 = 4. Co jednak z dzieleniem modulo? Okazuje się ze nie jest to takie proste jak z pozostałymi działaniami modulo. Szczególnie interesuje nas przypadek znajdowania dla danej liczby b jej odwrotności modulo p czyli 1/b mod p , gdzie 1<=b<p. Znając odwrotność danej liczby modulo p możemy wykonywać dzielenie przez tą liczbę modulo p czyli obliczyć a/b mod p. Jednak nie zawsze możemy obliczyć odwrotność danej liczby modulo p. Twierdzenie: Element odwrotny do b modulo p definiujemy jako taką liczbę a, taką ze ab=1(mod p) Liczba a istnieje tylko gdy b oraz p są względnie pierwsze, czyli NWD(p,b) = 1. Zauważmy, że warunek ten jest spełniony zawsze gdy p jest liczbą pierwszą. Ponownie pominiemy dowód tego twierdzenia i zapytamy od razu co ono nam daje. Otóż mając NWD(p,b) = d można zawsze jednoznacznie zapisać go jako: d = ub + vp w naszym wypadku d=1 wiec 1=ub + vp Szukanym elementem odwrotnym a jest u. Jest tak dlatego iż z ab=1(mod p) wynika ze p | 1-ab (p dzieli ab-1) z kolei 1-ub=vp wiec p | 1-ub . Zatem a=u. Zatem, reasumując, by obliczyć liczbę odwrotną do b modulo p, należy najpierw sprawdzić czy są one względnie pierwsze, czyli czy NWD(p,a)=1. Jeśli tak, należy znaleźć takie u oraz v, że 1=ub + vp. Wtedy szukanym elementem odwrotnym jest u (UWAGA: jeśli u wyjdzie ujemne, trzeba zredukować u modulo p zanim użyje się go jako odwrotności b). Jak znaleźć u oraz v? Posłuży nam do tego rozszerzony algorytm Euklidesa: Rozszerzony algorytm Euklidesa: DANE: m>0, n>0 WYJSCIE: k =NWD(n,m) u, v – liczby całkowite, takie ze um+vn=k assert n>=0 , m>=0, n>=m (a, a’)(n,m) (u, u’, v, v’)(0,1,1,0) while a’!=0 do q=a/a’ //dzielenie bez reszty, całkowite (a,a’)(a’, a-qa’) (u, u’, v, v’)(u’,u-qu’,v’,v-qv’) end return NWD(n,m)=a, oraz u, v Przykład: Obliczmy 160-1 mod 841.d Liczby 841 oraz 160 musza być względnie pierwsze by szukany element odwrotny istniał. Obliczmy zatem rozszerzonym algorytmem Euklidesa NWD(841, 160) (a,a’)(841,160) (u, u’, v, v’)(0,1,1,0) PETLA: Krok 1; q=841/160=5 (a, a’)(160, 841-5*160)=(160, 41) (u, u’, v, v’)(1,0-5*1,0,1-5*0)=(1,-5,0,1) Krok 2: q=160/41=3 (a, a’)(41, 160-3*41)=( 41, 37) (u, u’, v, v’)(-5,1-3*(-5),1,0-3*1)=(-5,16,1,-3) Krok 3: q=41/37=1 (a, a’)(37, 41-1*37)=( 37, 4) (u, u’, v, v’)(16,-5-1*16,-3,1-1*(-3))=(16,-21,-3,4) Krok 4: q=37/4=9 (a, a’)(4, 37-9*4)=( 4, 1) (u, u’, v, v’)(-21,16-9*(-21),4,-3-9*4)=(-21,205,4,-39) Krok 5: q=4/1=4 (a, a’)(1, 4-4*1)=( 1,0 ) (u, u’, v, v’)(205,-21-4*205,-39,4-4*(-39))=(205,-841,-39,160) a’==0 wiec STOP NWD(841,160) = a = 1 = 205*160 – 39*841 Zatem element odwrotny do 160 modulo 841 istnieje gdyż NWD(841,160)=1 i wynosi on 205. Można sprawdzi poprawność obliczeń: 160*205 (mod 841) = 32800 (mod 841) = 1 Zatem zgadza się. Umiemy obliczać element odwrotny danej liczby modulo p, jeśli te liczby są względnie pierwsze. Algorytm Iteracyjnego Podnoszenia do Kwadratu Potrzebna nam będzie jeszcze umiejętność obliczania wyrażeń typu bn mod m, gdzie liczby n oraz m są dużymi liczbami, przykładowo mamy obliczyć 12335mod 133. Zakładamy, że b<m. Zamiast podnosić b do tak wysokiej potęgi możemy iteracyjnie mnożyć b przez samego siebie i za każdym razem skracać wynik modulo m. Zatem w wyniku zawsze będziemy mieć do czynienia z liczbami nie przekraczającymi m. Częściowe iloczyny oznaczamy jako a. Na końcu algorytmu szukany wynik będzie końcową wartością a. Algorytm IPdK: 1. Zapisz wykładnik n w postaci binarnej, n(2)=no + 2n1 + 4n2 + ...+ 2k-1nk-1 , gdzie k jest ilością bitów w zapisie binarnym liczby n. 2. Najmniej znaczący bit n0 służy nam jedynie do ustalenia początkowej wartości a. Jeśli n0=1 to ustaw a=b, jeśli n0=0 ustaw a=1. 3. Następnie podnosimy b do kwadratu i skracamy modulo m, czyli b1=b2mod m. Jeśli n1=1 to wykonujemy a=a*b mod m, w przeciwnym wypadku, gdy n 0=0, nie zmieniamy wartości a. 4. W każdym kroku i obliczamy wartość bi=bi-12 mod m, i jeśli ni=1 to obliczamy ai=ai-1*bi mod m. 5. Wynikiem jest końcowa wartość a. Przykład: Obliczmy 12335mod 133 Najpierw zapisujemy 35(10)=100011(2) 1. n0=1 więc a = b = 123 2. b1=b2 mod m = 123*123 mod 133=100 oraz n1=1 więc a1=a*b1 mod m = 123*100 mod 133 = 64 3. b2=b12 mod m = 100*100mod 133=25 oraz n2=0 4. b3=b22 mod m = 25*25mod 133=93 oraz n3=0 5. b4=b32 mod m = 93*93mod 133=4 oraz n4=0 6. b5=b42 mod m = 4*4mod 133=16 oraz n5=1 więc a5=64*16 mod 133=93 Zatem 12335mod 133 = 93 Umiejętność ta przyda nam się w następnym punkcie. Generowanie dużych liczb pierwszych Jak już mówiliśmy wcześniej, algorytmy przedstawione na początku tego opracowania, nie są zbyt dobrym rozwiązaniem, jeśli chcemy wygenerować naprawdę dużą liczbę pierwszą, przykładowo 4000-bitową. Dopiero takiej długości liczby, lub nawet dłuższe, gwarantują bezpieczeństwo. Metoda jaką przyjmiemy jest bardzo prosta – wybierzemy dowolną liczbę i sprawdzimy czy jest pierwsza. Liczb pierwszych jest dość dużo, w okolicy danej liczby n średnio jedna liczba na ln(n) jest liczbą pierwszą. Logarytm naturalny nie jest funkcją szybko rosnącą, przykładowo wartość logarytmu naturalnego z liczby 2k nieznacznie przekracza 0,7*k. Dla liczb 2000-bitowych, mieszczących się w zakresie 21999 – 22000, mamy mniej więcej 1386 liczb pierwszych. Dodatkowo z rozwiązań od razu możemy wyrzucić liczby w sposób oczywisty złożone, na przykład parzyste. Algorytm GenerujLiczbePierwszą: 1. Ustal zakres, z jakiego będzie generowana liczba pierwsza 2. Ustal ilość prób generowania liczby pierwszej 3. Dopóki nie przekroczono maksymalnej ilości prób, wygeneruj losowa liczbę z zadanego przedziału i sprawdź czy jest pierwsza, jeśli nie, wygeneruj następną losową liczbę. Należy zabezpieczyć się przed przypadkiem gdy dany przedział nie zawiera żadnej liczby pierwszej – program nie powinien się wtedy zawieszać. W praktyce należy uważać by dany przedział nie był również zbyt mały, gdyż, ktoś, znając przedział może spróbować wygenerować wszystkie liczby pierwsze z tego przedziału – jeśli jest on zbyt mały, może się to udać, co może prowadzić do naruszenia przyjętych zasad bezpieczeństwa. Ilość prób można określić wykorzystując wcześniej podane informacje. Przykładowo r (czyli ilość prób) może być równa r = 100*(log2u + 1) gdzie u to górne ograniczenie naszego zakresu poszukiwań. Dla liczby 2000-bitowej log2u + 1 wynosi 2000. Czynnik 100 daje dodatkową gwarancję, ze prawdopodobieństwo nie znalezienia liczby pierwszej w danej ilości prób jest znikome. Dodatkowo należy pamiętać o korzystaniu z generatora losowego liczb, który będzie generował liczby z równomiernym rozkładem.. Można również w celu uniknięcia sprawdzania liczb parzystych (które wiadomo, ze nie są pierwsze) ustawić najmniej znaczący bit wygenerowanej liczby. Sprawdzenie pierwszości danej losowej liczby przebiega następująco: Algorytm SprawdzCzyPierwsza: 1. assert n>=3 2. Sprawdź podzielność liczby n przez znane, małe liczby pierwsze np. <1000. Pozwala to szybko wykryć większość liczb złożonych podzielnych przez małe liczby. Jeśli badana liczba n jest podzielna przez którąś ze znanych, małych liczb pierwszych, zwróć false. 3. Jeśli badana liczba n przeszła zwycięsko krok 2, wykonaj na niej test Rabina-Millera. Jeśli przejdzie go zwycięsko, zwróć true, inaczej zwróć false. Pozostaje do omówienia test Rabina-Millera. Test Rabina-Millera Test ten ma charakter probabilistyczny, to znaczy, ze nie uzyskamy pewności czy dana liczba jest pierwsza. Możemy jednak dowolnie zmniejszać prawdopodobieństwo pomyłki. Test ten ma na celu sprawdzenie czy dana, nieparzysta liczba n jest pierwsza. Dojście do testu Rabina-Millera w algorytmie SprawdzCzyPierwsza daje pewność ze n jest nieparzyste (inaczej byłaby podzielna przez 2 co byłoby wykryte wcześniej). W teście Rabina-Millera dla danej liczby n wybieramy losową liczbę a, taką że a<n. Liczbę a nazywamy bazą i sprawdzamy pewną właściwość a modulo n. Właściwość ta jest spełniona jeśli n jest pierwsza. Niestety, jeśli n nie jest pierwsza, to w przypadku co najwyżej 25% rożnych liczb a (wartości bazy) właściwość ta również zachodzi. Powtarzając wiec test dla rożnych wartości bazy a, będziemy zmniejszać prawdopodobieństwo pomyłki. Im więcej różnych wartości bazy a, tym mniejsze prawdopodobieństwo pomyłki. Jeśli dla danej wartości a, liczba n nie przejdzie testu, to znaczy, że nie jest pierwsza, jeśli natomiast przejdzie, nie wiemy na pewno czy jest pierwsza, ale po coraz większej ilości różnych a, prawdopodobieństwo tego, że się mylimy, maleje. Jeśli n jest pierwsza, przejdzie przez każdą edycję testu dla jakiejkolwiek wartości a. Jeśli nie jest pierwsza, wykaże to przynajmniej 75% możliwych wartości a. Można zatem ustalić zadany poziom bezpieczeństwa, przykładowo ustalając dopuszczalne prawdopodobieństwo pomyłki na 2-128. Oto omawiany test: Funkcja Rabin-Miller Dane: n – liczba nieparzysta>=3 (nie dopuszczamy wartości 2) Wynik: wartość logiczna wskazująca czy dana liczba n jest pierwsza, czy też nie assert n>=3 oraz (n mod 2) =1 Wyznaczamy takie (s, t), że s jest nieparzyste i 2t s= n-1 (s, t) (n-1,0) while s mod 2 = 0 do (s, t)(s/2, t+1) end W zmiennej k generujemy dane o prawdopodobieństwie uzyskania fałszywego wyniku. Prawdopodobieństwo nie przekracza 2-k. Każemy pracować pętli tak długo, aż fałszywy wynik stanie się mało prawdopodobny. k0 while k<128 do //ustalony poziom bezpieczeństwa na 128 Wybieramy losową liczbę a spełniającą 2<=a<=n-1 arandom({2..n-1}) vas mod n Jeśli v=1, liczba n przeszła test dla bazy a, w przeciwnym wypadku nadal może go przejść, dlatego sprawdzamy dalej if v!=1 then Ciąg v, v2, .., vpow(2,t) musi kończyć się wartością 1 a jeśli n jest liczbą pierwszą, ostatnią wartością różną od 1 musi być n-1. Zatem i0 while v!=n-1 do if i=t-1 then return false //test zakończony niepowodzeniem else (v, i)(v2 mod n, i+1) end while end if Dojście do tego miejsca oznacza, ze n przeszła test względem bazy a. Wobec tego prawdopodobieństwo uzyskania fałszywej odpowiedzi zostało zredukowane o czynnik 22, zatem zwiększamy k o 2. kk+2 end while // pętla po różnych wartościach bazy a return true //n przeszła testy dla wszystkich wylosowanych baz a, wiec można zwrócić true Dlaczego ten test działa? Podstawą jego działania jest małe twierdzenie Fermata. Mówi ono, że dla dowolnej liczby pierwszej n i dla wszystkich liczb a, takich że 1<=a<n, zachodzi an-1mod n =1. Więc jeśli n jest pierwsza , własność ta zachodzi, jednak istnieją pewne liczby złożone, zwane liczbami Carmichaela, które mimo iż są złożone, przechodzą test dla wielu baz a. Jeszcze kilka wyjaśnień. Przedstawienie liczby n-1 w postaci 2ts, gdzie s jest nieparzysta, pozwala nam zamiast obliczania an-1 obliczyć as a następnie ten wynik podnosić do kwadratu t razy i otrzymać as*pow(2,t) = an-1. I teraz, jeśli as =1 (mod n), powtórne podniesienie do kwadratu nie zmieni wyniku, wiec będziemy mieć an-1=1(mod n). W przypadku as !=1 (mod n) sprawdzamy kolejne kwadraty tej liczby czyli ciąg as, as*pow(2, 1), as*pow(2, 2),..., as*pow(2, t) (wszystkie modulo n). Gdyby n była liczbą pierwszą, to ostatnia liczba musiałaby być równa 1. Kiedy n jest liczbą pierwszą, jedynymi liczbami x spełniającymi warunek x2=1(mod n) są 1 i n-1. Gdyby zatem n była liczbą pierwszą, w podanym ciągu musiałaby wystąpić liczba n-1 (stad warunek pętli while v!=n-1), gdyż w przeciwnym wypadku nigdy ostatni wyraz nie równałby się 1. Jeśli powyższe nie zachodzi, to zwracamy false, przerywamy test i wychodzimy z funkcji. Sprawdzamy liczbę n dla rożnych wartości a dopóki prawdopodobieństwo błędu nie spadnie poniżej (w tym przykładzie) 2-128. Poznaliśmy zatem wszystkie potrzebne nam narzędzia matematyczne, które będą nam potrzebne przy właściwym szyfrowaniu RSA. Szyfrowanie RSA W poprzednim laboratorium mieliśmy do czynienia z szyfrowaniem z kluczem symetrycznym, to znaczy taki sam klucz musiał być znany zarówno nadawcy jak i odbiorcy, aby, odpowiednio, zaszyfrować i odszyfrować wiadomość. Z tego wniosek, że musiał on być znany im obu przed wysłaniem pierwszej zaszyfrowanej wiadomości. W przeszłości zatem, przed podjęciem szyfrowanej korespondencji, musieli spotkać się i osobiście uzgodnić hasło, lub sposób szyfrowania, który w obu przypadkach (szyfrowanie-odszyfrowywanie) przebiega według tego samego mechanizmu i z wykorzystaniem tego samego hasła. Co jednak robić w sytuacji, gdy dane osoby nie mogą się spotkać ani w żaden bezpieczny sposób uzgodnić wspólnego hasła? Rozwiązaniem są systemy z kluczami niesymetrycznymi, w których podczas szyfrowania i odszyfrowywania używa się innych klucz. Zazwyczaj każdy uczestnik takiego systemu posiada dwa rodzaje kluczy: publiczny, który udostępnia wszystkim chętnym, na przykład na swojej stornie internetowej, a drugi prywatny, który zachowuje w tajemnicy. Za pomocą klucza publicznego chętni wysyłają do osoby, która udostępniła ten klucz zaszyfrowane tym kluczem wiadomości. Posiadacz klucza prywatnego jest w stanie za jego pomocą odszyfrować wiadomość. Cały sekret tkwi w tym, iż klucz publiczny nie może odszyfrować zaszyfrowanej nim wiadomości, można tego dokonać jedynie znając klucz prywatny. Przykładem takiego systemu jest RSA. RSA przedstawiony został oficjalnie w roku 1979 i jest dziełem trzech ludzi: Ronalda Rivesta, Adi Shamira oraz Leonarda Adlemana - jego nazwa pochodzi o pierwszych liter nazwisk swych twórców. Jak działa RSA? Zaczniemy od dwóch dużych, losowo wybranych, rożnych liczb pierwszych p i q. Obliczamy ich iloczyn n=pq. Liczbę n będziemy wykorzystywać do wykonywania operacji modulo. Jednak widzimy, ze nie jest ona liczbą pierwszą, wiec nie zachodzi równość, xn-1 =1 (mod n) dla 0<x<p. Aby użyć RSA musimy znaleźć taki wykładnik t by xt=1 (mod n) dla (prawie) wszystkich x. Spójrzmy: jeśli zachodzi xt=1 (mod n) to zachodzi również xt=1 (mod p) oraz xt=1 (mod q). Liczby p i q są pierwsze więc równanie x t=1 (mod n) będzie zachodzić tylko wtedy gdy p-1 będzie dzielnikiem t oraz q-1 będzie dzielnikiem t. Zatem najmniejsza liczba o takiej właściwości to NWW(p-1, q-1)=(p-1)(q-1)/NWD(p-1, q-1). Przyjmijmy zatem w dalszych rozważaniach, że t= NWW(p-1,q-1). Czasami używa się funkcji Eulera. Funkcja ta dla liczby n=pq gdzie p i q są pierwsze wynosi φ (n) = ( p − 1)(q − 1) Wartość tej funkcji jest wielokrotnością naszego t. Użycie jej zamiast t również da dobre wyniki. Wracajmy do RSA. Mając p , q, n oraz t. Potrzebujemy teraz dwóch różnych wykładników e oraz d. Muszą one spełniać warunek ed=1 (mod t). Jako publicznie znany wykładnik e wybieramy niewielką liczbę nieparzystą. Posługując się rozszerzonym algorytmem Euklidesa obliczamy d jako odwrotność e modulo t. Aby zaszyfrować wiadomość m, nadawca oblicza tekst zaszyfrowany c=me (mod n) Aby odszyfrować tekst c, odbiorca oblicza cd (mod n) co jest równe oryginalnej wiadomości m. Klucz publiczny stanowi para (n, e). Kluczem prywatnym jest zestaw (p, q, t, d). Powinien on pozostać ukryty. Ze względu na zależność xt=1(mod n), wykładniki obliczeń modulo n należy brać modulo t, gdyż wielokrotności t w wykładniku operacji modulo n nie wpływają na wynik. Jak widać umiemy przeprowadzić wszystkie potrzebne operacje matematyczne. Musimy tylko jeszcze zwrócić uwagę na parę szczegółów. Otóż jeśli e będzie miało wspólny czynnik z t = NWW(p-1,q-1), to nie będzie istniał element odwrotny do e modulo t, czyli nie obliczymy potrzebnego d. Aby można go było obliczyć, e i t muszą być względnie pierwsze. Zatem po wygenerowaniu liczb pierwszych p i q, należy sprawdzić czy dla wybranej wartości e liczby p-1 oraz q-1 nie mają wspólnych czynników z wybranym e. Wybór malej wartości e przyspiesza obliczenia i upraszcza konstrukcję systemu. Zatem jako e możemy przyjąć e=3. Po wygenerowaniu p i q sprawdzamy czy nie są one podzielne przez 3. Innymi słowy wylosowana liczba k musi spełniać k mod 3 != 1. Innym wyborem na e może być np. 5, 17 itd. Liczba e nie musi być duża, duże muszą być liczby p i q. Jak widać na podstawie jedynie klucza publicznego nie da się łatwo odgadnąć liczb w kluczu prywatnym. Można by tego dokonać rozkładając n na czynniki by poznać p i q, jednak nie jest znany wydajny algorytm rozkładu liczb na czynniki czyli faktoryzacji liczb złożonych. Dlatego mówi się, że problem rozkładu na czynniki pierwsze jest tak ważny w kryptografii. Liczba n powinna mieć kilka tysięcy bitów długości by zapewnić bezpieczeństwo na odpowiednim poziomie. Przykład: Wybieramy e=3. Dobieramy takie liczby pierwsze p i q aby p-1 oraz q-1 nie dzieliły się przez 3. Warunek taki spełniają p=53 oraz q=71. Zatem n = 53*71=3763. Za t przyjmijmy t = (53-1)*(71-1)=3640. Obliczamy teraz element odwrotny do e=3 modulo t=3640. Wynosi on d=2427. Sprawdzamy: 3*2427 mod 3640 = 7281 mod 3640 = 1 – zgadza się Załóżmy, że chcemy zaszyfrować wiadomość zapisaną jako m=1655. Klucz publiczny stanowi para (n, e) = (3763, 3). Szyfrujemy m: c = me mod n = 16553 mod 3763 = 3477 Klucz prywatny to zestaw liczb (p, q, t, d) = (53, 71, 3640, 2427). Odszyfrowujemy c: m = cd mod n = 34772427 mod 3763 = 1655 A wiec odzyskaliśmy oryginalną wiadomość. Należy zwrócić jednak uwagę na fakt, iż jeśli oryginalna wiadomość podniesiona do potęgi e (e jest małe) nie przekroczy n, to nie zostanie ona zaszyfrowana, gdyż nie nastąpi skrócenie modulo. Przykład: chcemy zaszyfrować m = 12 c = me mod n = 123 mod 3763 = 1728 mod 3763 = 1728 Jako że e jest znane, gdyż jest częścią klucza publicznego, łatwo obliczyć po prostu pierwiastek trzeciego stopnia z 1728 i odzyskać m. Należy o tym pamiętać. Powyższe podejście użycia RSA do szyfrowania wiadomości ma swoją wadę, mianowicie długość wiadomości do zaszyfrowania jest ograniczona wielkością n (musi być od niej mniejsza). Oprócz tego trzeba najpierw uzgodnić, opracować sposób przedstawiania wiadomości np. tekstowych za pomocą liczb. Można przyjąć wartości kodów ASCII, ale jest to podejście niezbyt dobre. Dłuższa wiadomość można podzielić na mniejsze części, nie przekraczające n, i kodować każdą z tych części osobno. Jednak jest to w praktyce niewydajne, gdyż szyfrowanie RSA wymaga przeprowadzenia wielu skomplikowanych i czasochłonnych operacji matematycznych, jak potęgowanie modulo. Alternatywnym i dużo lepszym w praktyce rozwiązaniem jest kodowanie za pomocą RSA nie samej wiadomości, tylko klucza, który służy do szyfrowania i odszyfrowywania wiadomości. Sama wiadomość nie ma wtedy ograniczeń co do długości. Sam klucz jest wykorzystywany do szyfrowania blokowego lub strumieniowego. Oczywiście klucz ten, zapisany jako liczba, musi spełniać odpowiednie warunki by mógł by zaszyfrowany za pomocą kluczy RSA, tzn. nie może przekraczać n. Szyfrowanie za pomocą klucza można zrealizować przy pomocy operacji logicznej XOR. Załóżmy, że nasza wiadomość to ciąg bitów m = 1001100011 a klucz to również ciąg bitów k = 1000111010 Wynik operacji XOR jest równy 1 jeśli oba bity przekazane jako argumenty są różne, a zero gdy są identyczne. Zatem: 1 XOR 1 = 0 1 XOR 0 = 1 0 XOR 0 = 0 0 XOR 1 = 1 Stosując te operacje na ciągach bitów wiadomości i klucza otrzymujemy: m = 1001100011 k = 1000111010 -------------------c = 0001011001 Co jest ważne, to to, że odszyfrowanie przebiega w identyczny sposób – bierzemy ciąg bitów szyfrogramu i ciąg bitów klucza i przeprowadzamy ponownie operacje XOR: c = 0001011001 k = 1000111010 -------------------m = 1001100011 Jeśli wiadomość, którą chcemy zaszyfrować jest dłuższa niż długość klucza, to dzielimy ją na mniejsze bloki równe długości klucza i każdy blok szyfrujemy osobno. Jest to przykład szyfrowania blokowego ( szyfrowaniem strumieniowym zajmiemy się przy okazji jednego z kolejnych ćwiczeń). Oczywiście stosując ten sposób maksymalna długość klucza jest ograniczona przez n. Jeśli zależy nam na dłuższym kluczu, możemy zwiększyć n lub też zastosować jeszcze inne podejście wykorzystujące funkcję mieszającą. Funkcja mieszająca to funkcja, która pobiera jako dane wejściowe dowolnej długości ciąg bitów i zwraca jako wynik ciąg bitów o stałym rozmiarze. Pomysł polega zatem na zaszyfrowaniu kluczami RSA danych wejściowych do funkcji mieszającej, która wygeneruje nam ciąg bitów klucza o danej, pożądanej przez nas długości. Dopiero ten klucz będzie użyty do zaszyfrowania wiadomości. Mimo iż dane wejściowe do funkcji mieszającej są ograniczone przez n (gdyż je właśnie chcemy szyfrować) to długość klucza nie zależy od tych danych wejściowych ani od parametru n RSA, możemy wiec uzyskać klucz o długości, która zależy od zaimplementowanej przez nas funkcji mieszającej. Do odbiorcy wysyła się zaszyfrowaną wiadomość i zaszyfrowane dane wejściowe dla funkcji mieszającej. Odbiorca odszyfrowywuje swym kluczem prywatnym RSA dane wejściowe dla funkcji mieszającej, generuje za pomocą tej funkcji klucz i używa go do rozkodowania wiadomości za pomocą operacji XOR. Aby jednak nie komplikować sobie tymczasowo życia, nie będziemy używać funkcji mieszającej i pozostaniemy przy szyfrowaniu RSA samego klucza, którym będziemy szyfrować wiadomość za pomocą operacji XOR. Zadania do zrealizowania: W naszych ćwiczeniach nie będziemy posługiwać się na razie żadną specjalną biblioteką umożliwiającą użycie liczb całkowitych dowolnej wielkości. Wykorzystując zakres liczb całkowitych dostępnych w danym języku, zaimplementujemy same mechanizmy i algorytmy szyfrowania RSA. Uzyskany przez nas program będzie bezpieczny na tyle na ile pozwala na to wielkość zastosowanych liczb (i rzetelność implementacji). 1. Napisz funkcję testującą czy dana liczba jest liczba pierwszą. 2. Napisz funkcję generującą liczby pierwsze nie większe od zadanego parametru. Zastosuj sito Eratostenesa. Wygenerowane liczby zapisz do pliku. 3. Napisz funkcję obliczającą NWD dla dwóch podanych liczb wykorzystując algorytm Euklidesa. 4. Napisz funkcję obliczającą NWD dla dwóch podanych liczb wykorzystując rozszerzony algorytm Euklidesa. 5. Wykorzystując funkcję z poprzedniego punktu napisz funkcję obliczającą element odwrotny do danej liczby modulo inna liczba. 6. Napisz funkcję obsługującą potęgowanie modulo. Wykorzystaj algorytm iteracyjnego podnoszenia do kwadratu. 7. Napisz funkcję generującą liczbę pierwszą z danego przedziału wykorzystującą test Rabina-Millera do sprawdzenia, czy wylosowana liczba jest pierwsza. 8. Zaimplementuj w programie szyfrowanie pliku za pomocą metody RSA wykorzystując wcześniej napisane funkcje. Szyfrowanie wiadomości odbywa się wykorzystując operacje XOR na bitach klucza danej długości i bitach pobranych ze strumienia szyfrowanego pliku. Szyfrowanie RSA wykorzystane jest do szyfrowania klucza. Wyeksportuj do plików klucze publiczny i prywatny, tak byś mógł udostępnić swój klucz publiczny wraz ze swoim oprogramowaniem by inni mogli wysyłać do Ciebie zaszyfrowane wiadomości. W zaszyfrowanej wiadomości powinien być również oczywiście zapisany zaszyfrowany klucz.