W tym szaleństwie jest metoda

Albo rzecz o myśleniu programisty

Wstęp

Cześć,
Tym artykułem chciałbym otworzyć krótką serię artykułów pod tytułem „Teoretyczne minimum programisty”. Seria ta skierowana jest raczej dla początkujących programistów oraz tych, którzy dopiero zaczynają swoją przygodę z programowaniem. Moim celem będzie przybliżenie Wam szerokiego spektrum zagadnień związanych z podstawami informatyki. W tym artykule (jako otwierającym) skoncentruję się raczej na filozoficznej i logistycznej niż technicznej stronie inżynierii oprogramowania. Postaram się przedstawić Wam proces wytwarzania oprogramowania, dowiecie się czym jest myślenie obliczeniowe oraz poznacie podstawowe paradygmaty programiwania – trudne słowo, które wytłumaczę za dłuższy moment 😊.

Czym jest oprogramowanie

Na początku musimy sobie odpowiedzieć na pewne z pozoru błahe pytania – czym jest oprogramowanie oraz jak ono powstaje. W zasadzie komputer, od momentu naciśnięcia przycisku „Włącz”, nawet gdy nie jesteśmy jeszcze w stanie na nim nic fizycznie zrobić wykonuje całą masę programów. Najpierw jest to firmware (oprogramowanie układowe, wpalone w pamięć na płycie głównej), który ładuje do pamięci BIOS lub UEFI. One konfigurują wstępnie sprzęt aby mógł wystartować na nim system operacyjny, który dalej ładuje kolejne sterowniki, a później koordynuje pracę różnych procesów oraz zarządza sprzętem. Kiedy wystartuje system operacyjny (taki jak Windows, Linux czy Mac OS), dopiero wtedy możemy na nim uruchamiać nasze aplikacje użytkowe (takiej jak pakiet biurowy, program do edycji zdjęć czy przeglądarka internetowa) lub gry. Nie inaczej działają konsole do gier, smartphone’y, czy routery.

Warto zwrócić uwagę na drobne rozróżnienie – program jest to zbiór instrukcji dla komputera do zrealizowania, podczas gdy proces jest to aplikacja już uruchomiona tzn. system przydzielił jej zasoby (pamięć oraz czas procesora), załadował program do pamięci i zaczął go wykonywać.

Programy często wykorzystują również biblioteki funkcjonalne, dzięki którym nie trzeba pewnych podstawowych funkcji implementować od podstaw. W końcu nikt (a w szczególności korporacje liczące przede wszystkim na zysk) nie lubi odkrywać koła na nowo. Prawda jest taka, że jest wiele obszarów, które są na tyle ogólne (biblioteki do tworzenia graficznych interfejsów użytkownika czy serializacja danych, tj. zapisywanie danych w formacie umożliwiającym ich przesłanie np. przez Internet), że mogą być użyte dla praktycznie każdego scenariusza biznesowego. Między Bogiem i prawdą, gotowe, popularne biblioteki o otwartych źródłach są często solidnie przetestowane, a niektóre funkcjonalności jak kryptografia są zbyt wrażliwe na błędy by implementować je na własną rękę bez dobrego powodu.

Oprogramowanie możemy podzielić zasadniczo względem ich relacji ze sprzętem i systemem operacyjnym:

  • Niskopoziomowe – kontrolujące sprzęt lub komunikujące się z systemem operacyjnym bezpośrednio poprzez wywołania systemowe (funkcje, które system operacyjny udostępnia programistom),
  • Wysokopoziomowe – aplikacja nie wykorzystuje bezpośrednio elektroniki ani funkcji systemu operacyjnego.

Poza tym też możemy je podzielić względem zastosowań:

  • Biblioteka użytkowa – udostępnia zbiór funkcji, które zazwyczaj są na tyle ogólne, że programiści mogą je zastosować w wielu różnych obszarach lub na tyle specyficzne, że kłopotliwym byłoby pisanie ich od podstaw,
  • Aplikacja użytkowa – program, który jest po prostu użyteczny dla użytkownika,
  • Firmware – oprogramowanie dla układów elektronicznych,
  • Oprogramowanie dla systemów wbudowanych – oprogramowanie, które jest wykorzystywane w tzw. systemach wbudowanych, są to komputery, wyspecjalizowane do konkretnych zadań.
Elektronika
HAL – warstwa abstrakcji sprzętu
System operacyjny / sterowniki
 
 
Biblioteka standardowa języka  
Biblioteki użytkowe  
Aplikacje

Jak powstaje oprogramowanie

Teraz, kiedy wiemy, że cebula ma warstwy, ogry mają warstwy oraz oprogramowanie ma warstwy możemy przejść do krótkiego opisu w jaki sposób powstaje oprogramowanie. Omówię to w kilku (mam nadzieję, że krótkich) punktach

1.     Zbieranie wymagań

Podobno programista to osoba, która rozwiąże Twój problem, którego nie miałeś, w sposób, który nie rozumiesz przetwarzając w tym celu snikersy, Coca-Colę oraz kawę w kody programów. Niestety, to nie jest do końca prawda – klienci płacą tylko za sprawne programy (przynajmniej w dostatecznym stopniu), które realizują to czego się od nich oczekuje.

Aby mieć pewność, że wszystko jest jasne tworzy się listę wymagań funkcjonalnych – w świecie automotive’u nierzadko uzupełnionych o uzasadnienie oraz listę przeanalizowanych alternatyw razem z przyczyną ich odrzucenia.

Na potrzeby branży motoryzacyjnej powstał cały zbiór procesów, które należy przejść tworząc oprogramowanie aby zapewnić wysoką jakość produktów. Możecie pogooglować frazę ASPICE albo chwilę zaczekać bo planuję napisać o tym parę słów za jakiś czas). Czasem polemizowałbym z ich sensem gdyż bywają one tak sztywne, że nie wprowadza się poprawek, o ile nie są krytyczne,
gdyż wymuszałoby to tonę dodatkowej papierkowej roboty,

2.     Projektowanie architektury

W tej fazie architekci decydują o podziale aplikacji na mniejsze komponenty funkcjonalne oraz decydują o wewnętrznej strukturze aplikacji oraz planują w jakiej kolejności będą one implementowane.

Zazwyczaj projektując system staramy się zrobić to w taki sposób, by był on maksymalnie rozszerzalny i modyfikowalny.

3.     Implementacja

Chyba najprzyjemniejsza faza tworzenia oprogramowania. Programiści tworzą kody źródłowe w języku programowania, następnie kompilator tłumaczy je do formy zrozumiałej dla komputera (tzw. Języka maszynowego, ten etap kompilacji nazywamy fazą translacji) po czym linker łączy tak wygenerowane pliki z bibliotekami (nazywamy to fazą linkowania lub rzadziej, konsolidacji).

Kod źródłowy 1
Kod źródłowy 2
Biblioteka 1
 
Plik objektowy 1
Plik objektowy 1
Translacja
Translacja
 
Linker
 
 
 
Pliki obiektowe
i biblioteki są przekazywane do  linkera
 
Program / Biblioteka
Linkowanie

Często sam system budowania aplikacji (tj. przekształcania kodu w działającą aplikację) jest projektowany tak aby proces kompilacji był szybszy gdyż przekłada się to na produktywność programisty – szybszy czas budowania, mniejsze straty czasu przy testach. Odczuwalne jest to w szczególności gdy pracuje się z systemami takimi jak Yocto (na szczęście używa się go tylko i wyłącznie do budowania całych dystrybucji systemu Linux)…

Edgar Dijikstra napisał kiedyś równanie:

Programy = Algorytmy + Struktury danych

Niektórzy dopisują tam również sterowanie, gdyż rzadko programy nie wymagają żadnej interakcji ze światem zewnętrznym – choćby w postaci danych.

Wracając do meritum czym jest algorytm? Nazwa wzięła się od łacińskiego algorithmus (oznaczającego obliczenia przy użyciu liczb arabskich) będącego zlatynizowaną formą arabskiego nazwiska Al Chwarizmi, matematyka perskiego z IX wieku.

Komputer ze swojej natury jest sekwencyjny – procesor pobiera z pamięci i wykonuje kolejne instrukcje. Algorytm jest to sekwencja kroków niezbędnych do uzyskania konkretnego efektu. Przykładowo algorytm parzenia kawy mógłby wyglądać następująco.

  1. Weź kawę z półki
  2. Jeśli nie jest zmielona – zmiel ją,
  3. Zaparz wodę,
  4. Zalej wrzącą wodą,
  5. Odczekaj 5 minut żeby się nie poparzyć,
  6. Dodaj cukru dopóki nie będzie odpowiednio słodka.

Widać tutaj trzy niezbędne elementy algorytmu:

  • Sekwencja – kroki są wykonywane jeden po drugim,
  • Bloki warunkowe – niekiedy musimy rozróżnić jakie zachowania podjąć w danych okolicznościach,
  • Iteracje – niektóre kroki będziemy podejmować do momentu spełnienia pewnych warunków brzegowych, wynika z tego jedna z podstawowych zalet komputerów, nie powiedzą Ci po wykonaniu jednego kroku 1000 razy, że są zmęczone 😊.

Struktury danych definiują natomiast w jaki sposób dane są przechowywane w pamięci – co również może wpłynąć na prędkość ich przetwarzania.

W kolejnej części tego artykułu pochylimy się nad tematem wyobraźni obliczeniowej, dzięki której jesteśmy w stanie zaprojektować algorytm (tj. wyspecyfikować kroki, potrzebne do uzyskania oczekiwanego rezultatu).

4.     Testowanie

Dość wredny etap tworzenia oprogramowania… W końcu nikt nie lubi jak wskazuje mu się błędy i niedoróbki 😊.

Testy można podzielić ze względu na sposób ich przeprowadzania tj. manualne (żmudne, pracochłonne, każdy krok musi być ręcznie wykonany) czy automatyczne, albo ze względu na ich funkcję:

  • Testy jednostkowe – pisane przez programistów sprawdzające podstawowe jednostki z których zbudowany jest program,
  • Testy komponentowe – sprawdzają działanie konkretnych komponentów,
  • Testy funkcjonalne – sprawdzają działanie konkretnych funkcjonalności,
  • Testy integracyjne – testują działanie dwóch lub więcej zależnych od siebie komponentów.

Od testerów często oczekuje się certyfikatu ISTQB. Testerzy automatyczni dość często są już także praktycznie programistami choć często oczekiwania od nich są wciąż nieco mniejsze.

Według mnie,  chyba najbardziej upierdliwym elementem tego procesu jest specyfikowanie oraz dokumentacja testów. W moim obecnym projekcie dodatkowo wspieramy tzw. tracing requirement’ów. Przy każdym teście mamy wskazanie na wpis w dokumentacji use case’a testowego oraz odniesienie do wymagań funkcjonalnych – co sprawia, że czasem perspektywa poprawiania komentarzy w kodzie skutecznie zniechęca do wprowadzania poprawek w kodzie.

5.     Wdrażanie i utrzymanie

Z doświadczenia najnudniejszy etap – oprogramowanie jest wprowadzane na systemy produkcyjne, robota programisty sprowadza się zazwyczaj do łatania błędów (popularnie zwanych bug’ami).

W moim obecnym projekcie, pierwsze prototypy naszego komponentu powstawały na normalnym komputerze, dopiero później przy jednym z pierwszych release’ów (jest to moment wydawania nowej wersji w cyklu życia programu) przetestowaliśmy go na płytce, która docelowo trafiała
do samochodu.

Proces, który Wam opisałem nosi nazwę tzw. Waterfall’a, jest to tak zwany kaskadowy model tworzenia oprogramowania. Stosowany jest w sytuacji gdy wymagania są proste i przejrzyste w szczególności do prostych aplikacji.

Modnym ostatnio modelem jest SCRUM (z ang. młyn).

Oprogramowanie powstaje w sposób przyrostowy. Proces jest podzielony na sprinty, na koniec każdego dostarczony jest produkt z coraz większą liczbą funkcji. W SCRUMie każdy sprint ma swój ceremoniał składającego się z szeregu spotkań:

  1. Sprint refinement – w ramach którego wykonuje się wstępną inwestygację zadań do wykonania w najbliższym czasie,
  2. Sprint planning – zespół estymuje ile czasu zajmą zadania i wybiera te, które zostaną zrealizowane w ramach sprintu,
  3. Daily – codzienne spotkanie w czasie których dyskutowane są postępy w realizacji zadań,
  4. Sprint review – ewaluacja sprintu, dyskusje co udało się zrobić a które będą kontynuowane w kolejnym sprint’cie,
  5. Sprint retrospective – wymiana wrażeń na temat pracy w danym sprintcie, problemy, wyzwania i co można poprawić w kooperacji zespołu.

Sam osobiście nie jestem wielkim miłośnikiem tego modelu. O ile pomysł iteracyjnego tworzenia oprogramowania i by nie rozwiązywać na samym początku problemów których jeszcze nie mamy projektując od razu całą aplikację od zera uważam za dobry tak sam SCRUM może być kłopotliwy.

Zależnie od typu zadań i tego jak długo trwają scrumowe gusła (tutaj czyt. potkania 😉) może to drastycznie skracać faktyczny czas na zrealizowanie zadań. Zdarzyło mi się pracować w zespole w którym podzielono nas na dwa podzespoły:

  1. Realizujący nowe funkcjonalności niezależne od HAL’a maszyny,
  2. Dodający funkcjonalności zależne od HAL’a maszyny, które wymagały często dodatkowej analizy starego kodu, dodatkowo napisanego nie do końca zgodnie ze sztuką inżynierii oprogramowania… Strzeżcie się kodu pisanego przez naukowców 😊

Okazało się, że o ile dwutygodniowy okres sprintu dla podzespołu 1 był wystarczający tak w drugim optymalną długością były trzy tygodnie. W szczególności, że refinement’y, planningi, retrospektywy pochłaniały praktycznie około 2-3 dni roboczych (czyli de facto 20-30% czasu sprintu) bo nieraz wypadało się jeszcze do tych spotkań przygotować. W późniejszym okresie udało się to nieco skrócić 😉.

Na szczęście mimo wszystko moje wspomnienia z retrospektyw mimo wszystko są raczej pozytywne – dyskusje i spostrzeżenia dotyczące komunikacji w zespole i analizy co poszło dobrze, jakie problemy się pojawiły dość często faktycznie poprawiały sytuację i umacniały ducha zespołowego a… teraz w końcu przejdę do nieco bardziej filozoficznej części artykułu!

Myślenie obliczeniowe

W akapicie poświęconemu implementacji rozwiązań wspomniałem o pojęciu wyobraźni obliczeniowej. Chcąc zaproponować rozwiązanie algorytmiczne problemu z tzw. realnego świata, analizujemy je zazwyczaj podobnie jak w czasach szkolnych rozwiązywaliśmy zadania z matematyki.

Aby zdefiniować zbiór kroków, które komputer musi wykonać aby rozwiązać problem. Pierwsze co próbujemy zrobić to dokonać dekompozycji problemu tj. rozłożyć problem na mniejsze, które z dużym prawdopodobieństwem będzie łatwiej rozwiązać. Kolejnym krokiem jest dokonanie generalizacji, rozważenia czy istnieją pewne ogólne własności, pomocne w rozwiązaniu zadania. Następnie możemy zastosować logiczne wnioskowanie,aby wybrać użyteczne dla nas własności by koniec końców przejść przez wszystkie proste problemy, wyodrębnione w fazie dekompozycji. Ważnym elementem jest spojrzenie na problem z pewnego poziomu abstrakcji – należy uzmysłowić sobie w jaki sposób elementy prawdziwego świata można przedstawić tak. by rozwiązać zadany problem.

Zatem podstawowymi narzędziami przy projektowaniu algorytmów są:

  • Dekompozycja,
  • Generalizacja
  • Logiczne wnioskowanie
  • Abstrakcja.

Używamy ich, aby rozwiązać problem wciąż na pewnym poziomie abstrakcji a następnie używając już myślenia algorytmicznego definiujemy sekwencję kroków do wykonania dla komputera aby wykonał część obliczeniową.

Często też okazuje się, że wiele problemów sprowadza się do tego samego. Spójrzcie na tak zdefiniowany problem: jaka jest najkrótsza droga pomiędzy Wrocławiem a Warszawą?

Problem ten możemy podzielić na podproblemy:

  1. Jakich danych potrzebujemy?

Najlepiej mapę drogową z podanymi długościami odcinków drogowych.

  • Jak reprezentować mapę?

Możemy wyobrazić sobie każdą wioskę, miasto i miasteczko jako wierzchołek w grafie. Każda droga pomiędzy nimi jest krawędzią o wadze, która odpowiada długości odcinka.

  • Jak znaleźć najkrótszą drogę?

Problem znalezienia najkrótszej ścieżki pomiędzy dwoma wierzchołkami w grafie z wagami dodatnimi (ciężko przejechać -100 km 😊) jest już klasycznym problemem rozwiązujemy go dość często algorytmem Dijikstry (w tym miejscu nie jest istotne jak on działa).

Zatem program odpowiadający na nasz problem mógłby wyglądać tak:

  1. Wczytaj drogi pomiędzy miastami,
  2. Utwórz graf sieci dróg,
  3. Ustaw wierzchołek początkowy na Wrocław,
  4. Ustaw wierzchołek końcowy na Warszawę,
  5. Uruchom algorytm Dijikstry by znalazł najkrótszą drogę z Wrocławia do Warszawy.

Problemy grafowe są dość wdzięczne pod względem liczby zastosowań. Wyobraźcie sobie, że dowódcy wojskowi zajmują się logistyką i planują w jaki sposób zabezpieczyć dostawy na front tak aby zminimalizować ryzyko przerwania trasy kolejowej przez uszkodzone mosty. W tym przypadku, też będziemy wykorzystywać grafy, ale nie interesuje nas odległość a liczba krawędzi więc każda będzie miała wagę 1. Moglibyśmy również użyć algorytmu Dijikstry ale o wiele łatwiej jest to rozwiązać poprzez tzw. przejście grafu wszerz. Dzięki dokładnej analizie problemu jesteśmy w stanie wybrać odpowiednie algorytmy dla naszego problemu.

Grafy mogą być używane także zarówno w kontekście sprawdzania poprawności certyfikatów kryptograficznych, logistyce, jak i analizie sprawności sieci komputerowych i uwierzcie mi na słowo, że jest tego o wiele więcej.

Języki programowania

Kiedy już mamy algorytmy możemy się zastanowić w jaki sposób zrealizować je z wykorzystaniem komputera i tutaj pojawia się nasz ostatni element układanki czyli język programowania. Podstawowym językiem dla komputera jest kod maszynowy czyli zestaw rozkazów procesora reprezentowanych jako ciąg liczb. Prawda jest jednak taka, że w miarę rozwoju technologii używanie go dość szybko okazało się niewygodne i powstały asemblery – rodzina języków programowania,
w której kody poszczególnych operacji zostały zamienione na łatwe do zapamiętania skróty (tzw. mnemoniki). Później powstawały kolejne języki, które miały zwiększyć produktywność programistów oraz naukowców (gdyż oni pierwsi wykorzystywali komputery, najczęściej do celów wojskowych, takich np. jak projekt Manhattan czy przewidywania pogody).

Wiele eksperymentowano… pierwsze języki przypominały trochę eksperymenty małego dziecka, które nie do końca zdaje sobie z tego co potrafi. W ubiegłym wieku powstało ich pewnie co najmniej kilkaset, część z nich wprowadzały nowe pomysły dotyczące paradygmatu programowania (sposobu w jaki możemy myśleć tworząc programy, rozwinę to za chwilę), inne składniowe (niektóre się przyjęły, inne nie) albo zwiększały poziom abstrakcji i uniezależnienia od maszyny. W pewnym momencie zauważycie, że znaczna część z nich jest do siebie w pewnym stopniu podobna, przynajmniej dopóki implementują ten sam paradygmat.

Paradygmaty programowania

Pierwszy paradygmat był prostą konsekwencją tego jak działa procesor. Procesor pobiera z pamięci jedną instrukcję, dekoduje ją i wykonuje. Proces trwa dopóki komputer jest włączony. Instrukcje natomiast są różne… Mogą dodawać, odejmować, zapisywać do pamięci i odczytywać z pamięci i wiele innych ale co ważne w tym momencie dla nas umożliwiają skok do innego obszaru kodu, który może być wykonany pod pewnymi warunkami.

Dość szybko zauważono, że przy użyciu skoków i instrukcji warunkowych procesora można implementujemy pewne podstawowe struktury, którymi są pętle (blok kodu, który jest wykonywany dopóki warunek jest spełniony) oraz bloki warunkowe (fragmenty kodu wykonywane w określonych warunkach). Tak powstało programowanie strukturalne.

Kod maszynowy       AssemblerKod w języku C

Później spostrzeżono, że część kroków w kodzie jest używana na zasadzie kopiuj-wklej – kolejnym, naturalnym etapem było więc wydzielenie osobnych funkcji i łączenie ich w większe moduły, które byłyby ponownie używalne na przestrzeni całego kodu. I tak oto doszliśmy do etapu w którym wynaleziono programowanie proceduralne.

Kolejną ewolucją, która na dobre zagościła w świecie języków programowania jest programowanie obiektowe. Jest to obecnie chyba najpopularniejszy paradygmat a co zabawne zręby idei pojawiły się już… w starożytnej Grecji. Platon z Arystotelesem stworzyli teorię nazywaną dzisiaj teorią idei. Mówi ona, że:

Zgodnie z dominującą wykładnią filozofii (i powyższej alegorii) Platona ogół tego, co istnieje, dzieli się na dwie sfery – sferę przedmiotów fizycznych, wśród których aktualnie żyjemy, i sferę przedmiotów idealnych, które są poza naszym bezpośrednim zasięgiem. Pierwsze mają się do drugich tak, jak cienie do autentycznych rzeczy lub jak niedoskonałe ucieleśnienia do ich doskonałych wzorców. Owe rzeczy lub wzorce – stanowiące „prawdziwszą” lub „lepszą” rzeczywistość – Platon nazwał ideami lub formami.

Skąd wiadomo, że idee istnieją? Jeden z argumentów za ich istnieniem wygląda następująco. Przedmioty fizyczne układają się w grupy (zbiory), których elementy są do siebie podobne. Każdą z tych grup opatrujemy osobnym pojęciem i nazwą.

Źródło: https://antycznahellada.com/2020/03/15/swiat-idei-platona/

W programowaniu proceduralnym dane i procedury nie są ze sobą w żaden sposób związane podczas gdy programowanie obiektowe zmienia to drastycznie. Programiści tworzą klasy, które są swego rodzaju matrycami opisujące świat przedstawiony w programie. Klasy zawierają dane potrzebne do opisu przedmiotu oraz metody czyli zachowania specyficzne, które też mogą być dziedziczone. Ważną cechą języków obiektowych jest hermetyzacja danych – nie wszystkie są dostępne na zewnątrz.

Przykładowo, gdybyśmy pisali program do przechowywania informacji o zwierzętach w ZOO moglibyśmy stworzyć klasę Animal, która przechowywałaby informację o imieniu zwierzęcia i klasy tj. Reptiles czy Mammals, które będą miały bardziej charakterystyczne cechy tych grup gatunków jak np. pełzaj dla płazów i skacz dla ssaków 😉.

Rysunek 1 ClientFactory przy tworzeniu otrzymuje obiekt typu ConfigurationBundle, używa go aby utworzyć konkretny typ klienta

Cechą wspólną dla tych trzech paradygmatów jest to, że reprezentują one grupę paradygmatów imperatywnych – programista opisuje w nich proces wykonania jako sekwencję instrukcji zmieniających stan programu. Przeciwieństwem ich są paradygmaty deklaratywne (ostatnio modne programowanie funkcyjne czy programowanie w logice) w których programista raczej definiuje co ma zostać obliczone. Na przestrzeni ostatnich lat można zauważyć coraz silniejszy wpływ języków funkcyjnych na nowe funkcjonalności w językach imperatywnych. W językach czysto funkcyjnych nie są przechowywane żadne stany oraz funkcje nie są rozumiane jako sekwencje instrukcji do wykonania a raczej w sensie matematycznym, które przekształcają dziedzinę (argumenty) w zbiór wartości.

Programując w logice programista definiuje raczej zbiór relacji, które muszą zajść aby rozwiązać problem.

Język a ekosystem

Pamiętacie jak na początku artykułu wskazałem, że programowanie niskopoziomowe jest to tworzenie oprogramowania na styku systemu operacyjnego lub elektroniki? W przypadku języków programowania podział na języki niskiego poziomu i wysokiego poziomu jest dość subiektywny.

Co do jednego nikt się nie kłóci – na pewno językiem niskopoziomowym jest asembler. Język C? Za pewne niektórzy hardcore’owi niskopoziomowcy by się kłócili, mnie natomiast przekonuje argument, że jest to język w którym ręcznie zarządzamy pamięcią oraz są w nim pisane systemy operacyjne, sterowniki oraz oprogramowanie na mikrokontrolery.

Języki wysokopoziomowe są to języki dzięki którym tworzony kod będzie wyrażał bardziej abstrakcyjne idee. Bardzo często języki te łączą cechy wielu paradygmatów, najpopularniejsze języki łączą w sobie programowanie strukturalne, obiektowe oraz pewne cechy funkcyjne. Przykładem języków wysokopoziomowych są C++, Java, C#, Python, Ruby itd… I z tej listy tak na prawdę jedynie C++ jest kompilowany bezpośrednio do kodu maszynowego.

Języki takie jak Java oraz C# są kompilowane do swego rodzaju kodu bajtowego, który jest wykonywany przez środowisko uruchomieniowe – abstrakcyjną maszynę dzięki której program może być wykonany na dowolnym systemie na którym jest ona dostępna. Python oraz Ruby nie są kompilowane a interpretowane na bieżąco przez interpreter. Najczęściej językami interpretowymi są języki skryptowe (tj. Python, Ruby, Perl czy bash; skrypty były to proste programy wykonywane przez wiersz poleceń aby zautomatyzować pewne czynności administracyjne), chociaż istnieją wyjątki takie jak język D.

Ciekawym przykładem jest C++ – z jednej strony udostępnia wysokopoziomowe funkcje a z drugiej wciąż mamy możliwość programowania niskopoziomowego. Będąc profesjonalnie programistą C++, kiedy spotykam się z kolegami tęskniącymi z asemblerem jestem programistą pracującym na wysokim poziomie abstrakcji, podczas gdy dla kolegów Javowców jestem niskopoziomowcem… Jest to jedyny akceptowalny dla mnie konflikt w poczuciu własnej tożsamości 😊.

Wiecie, tak na dobrą sprawę, na stronie OSDev (wiki.osdev.org) można znaleźć artykuły jak napisać system operacyjny zarówno w języku C, jak i w językach wysokopoziomowych takich jak C++, D, Rust, czy Ada.

Kilka przykładów języków, które warto kojarzyć

Możecie znaleźć na internecie kursy wielu różnych języków programowania. Ja skoncentruję się na tym blogu pewnie na Rust’cie, którego się aktualnie uczę (i co by nie mówić zyskuje ostatnio coraz większą popularność), coś być może pojawi się z C/C++’a z którymi pracuję od lat i nęcącej mnie od jakiegoś czasu Adzie jednak chciałbym podać Wam krótką listę języków programowania, które warto kojarzyć. Zastawienie jest mocno subiektywne i czasem nie do końca poważne 😉. W większości języki te są imperatywne chyba, że wprost napisałem, że jest inaczej (np. jest funkcyjny albo służy do programowania w logice, jest tutaj tylko jeden przedstawiciel tej grupy).

Nazwa językaKomentarz
COBOLWspomniany wcześniej Edgar Dijikstra powiedział kiedyś „Używanie COBOL’a uwstecznia; jego nauczanie powinno być tym samym uznane za przestępstwo kryminalne”. Bardzo stary i nie powstają w nim żadne nowe projekty… O ile wiem programista COBOL’a jest poszukiwany gdy jego poprzednik zmarł.
FortranJęzyk ogólnego przeznaczenia (tzn. można w nim zrobić praktycznie wszystko) choć pierwotnie zaprojektowany do programów obliczniowych… I prawdę mówiąc kojarzy mi się jedynie jako język dla naukowców do obliczeń numerycznych i symulacji.
SchemePierwszy język z tej listy z którym miałem przyjemność obcować chwilę dłużej. Język będący dialektem Lisp’u – prawie legendarnego języka, który szybko stał się najchętniej wybieranym pod kątem badań nad sztuczną inteligencją. Jest to język funkcyjny, którego cała składnia opiera się na listach… czego konsekwencją jest masakrycznie duża liczba nawiasów.
Przykład kodu – implementacja algorytmu szybkiego sortowania
HaskellPodobnie jak poprzednik, jest to język funkcyjny… CZYSTO funkcyjny! (krzyk jest nie przypadkowy) Nie ma stanów, nie ma pętli a jest tylko wszechogarniająca rekurencja 😊 Programiści Haskell’a przez jego leniwość (obliczenia są wykonywane dopiero wtedy kiedy ich wynik jest potrzebny) potrafią stworzyć nieskończoną listę elementów w skończonym czasie. Nie znam go dobrze, ale zawsze sprawiał na mnie wrażenie dość eleganckiego języka.   Przykład kodu – implementacja algorytmu szybkiego sortowania
BasicStary język… Nie jeden programista zaczynał od niego swoją przygodę z programowania np. na komputerach Comodore. Pochodzę z tego pokolenia, których jedynym kontaktem z tym językiem były opowieści starszych kolegów po fachu. Swego czasu Microsoft stworzył na jego podstawie całkiem fajny język do nauki programowania dla młodzieży o nazwie SmallBASIC. Poza tym, chyba stosunkowo nie dawno Microsoft wyrugował aplikację Visual Basic z ich środowiska programowania Visual Studio. Obecnie pozostał on w użyciu chyba jedynie w pakiecie Microsoft Office do obsługi makr.
PascalUczono go w klasie równoległej do mojej kiedy byłem w liceum… Obecnie nie jest zbyt popularny choć miał swój okres chwały. Przez wiele lat, firma Borland (obecnie Embarcadero) tworzyła swój własny dialekt o nazwie Delphi i środowisko programistyczne, które umożliwiało w łatwy sposób tworzyć aplikacje okienkowe.
CPierwszy język, którego byłem uczony na studiach, choć nasz wykładowca nie był jego wielkim fanem. Język, który stanowi lingua franca informatyki i wywarł znaczący wpływ na składnię chyba większości współczesnych języków.   Zaimplementowano w nim jądro Linux’a, do dziś pisze się w nim sterowniki, aplikacje na mikrokontrolery i na wszystko co ma mocno ograniczone zasoby albo platforma na którą piszemy jest tak rzadka, że nic innego nie jest dostępne 😉 Jeśli jest coś co przetrwa apokalipsę zombie to pewnie będzie oprogramowane w C! Wciąż króluje także w świecie mechatroniki motoryzacyjnej za sprawą Classic Autosara.   Jest najbliższy do Torvaldsowego (twórca Linuksa) zdania: „Gdzie się podziały te czasy, gdy mężczyźni byli mężczyznami i sami pisali swoje sterowniki” – programiści C muszą sami zarządzać pamięcią, przez co większość programistów uznaje go za język trudny w obyciu.
C++Język od którego sam zaczynałem moją przygodę z programowaniem… Zanim napiszę o nim parę słów od siebie parę cytatów:
„C pozwoli Ci łatwo odstrzelić sobie stopę; C++ czyni to trudniejszym, ale jak już wybuchnie to rozwali całą nogę” – Bjarne Stroustrup, twórca języka „Ja wcale nie nienawidzę C… Po prostu moje odczucia wobec niego są ambiwalentne” – Scott Meyers, ekspert języka C++, twórca książki „Skuteczny, nowoczesny C++”   Język wysokopoziomowy i obiektowy ale wciąż może być wykorzystywany do tworzenia systemów operacyjnych i oprogramowania na mikrokontrolery… Chociaż tam wciąż spisuje się lepiej C ze względu na mniejsze rozmiary skompilowanego kodu. C++’a używamy obecnie najczęściej w systemach wbudowanych na mocniejszych platformach (np. w automotive, C++ jest językiem wiodącym w platformie Adaptive Autosar) oraz grach komputerowych ze względu na większą produktywność niż w C.   Programista wciąż musi myśleć o zarządzaniu pamięcią, chociaż dzięki tzw. inteligentnym wskaźnikom jest to o wiele przyjemniejsze.
JavaJęzyk obiektowy, który w założeniu miał działać na wszystkim o ile jest dostępna maszyna wirtualna. Obecnie najczęściej wykorzystywany do tworzenia oprogramowania na telefony komórkowe oraz do oprogramowywania backendu tzn. logiki biznesowej aplikacji webowych (np. systemów sprzedaży internetowej e-commerce).   Chyba najbardziej charakterystyczną cechą jest garbage collector – mechanizm, który odpowiada za sprzątanie nieużywanej pamięci… Problem z nim jest taki, że włącza się kiedy chce, istnieje polecenie do wywołania tego mechanizmu, ale maszyna wirtualna traktuje to wyłącznie jako sugestię. Nie jestem specjalnie fanem tego języka, też często przez mentalność programistów, która mówi – „Mam gdzieś zasoby, w razie czego zmienię maszynę na mocniejszą”.
C#Microsoftowa odpowiedź na Javę… i powiedziałbym, że ciut ładniejsza (jeśli kierować się względami estetycznymi). Najczęściej używana do tworzenia aplikacji na Windowsa i backendu aplikacji webowych. Jakiś czas temu powstał projekt o nazwie dotnet Core dzięki któremu możemy korzystać z C# także na Linuksach i Mac’ach
DMoja pierwsza miłość jeśli chodzi o mniej standardowe języki programowania. Język za którym stoi wiele mądrych głów ale niestety zbytnio się nie przyjął – mimo to używano go między innymi w Facebooku. Wspiera programowanie obiektowe, zaprojektowany przez Waltera Bright’a, twórcę pierwszego kompilatora C++ tłumaczącego bezpośrednio do kodu assemblera (wcześniej był tłumaczony najpierw do C a potem C było kompilowane do assemblera).   Cechą, która mi się podobała jest to, że posiada garbage collector, który można wyłączyć gdy potrzebujemy sami zarządzać pamięcią dla zwiększenia wydajności. Natomiast zniechęcił mnie system budowania z którym za bardzo nie potrafiłem się dogadać.   Wydaje mi się, że mógł się nie przyjąć przez fakt, że istniały dwie iteracje tego języka – D1 i D2 (współcześnie znany jako D). W języku D1 istniały dwie biblioteki standardowe Phobos (opracowywany m.in. przez Alexandrescu, eksperta od C++’a, rozwijała się powoli) oraz Tango (szybciej rozwijana bo przez środowisko, ale gorzej przetestowana), które były niekompatybilne względem siebie. W D2 (znanym dalej jako D) rozwiązano problem wprowadzając standardowy mechanizm obsługi wątków i garbage collectora.
AdaNa razie za wiele o nim nie wiem. Zaprojektowany dla wojska a używany głównie w lotnictwie. Z tego co czytałem wydaje się wręcz stworzony do systemów wymagających bezpieczeństwa.
RustNowoczesny język, który nadaje się do programowania systemów wbudowanych. Ostatnio powoli wchodzi jako jeden z języków używanych w ramach jądra Linux’a oraz Androida. Przy okazji wydaje się, że będzie coraz częściej używany także w branży automotive patrząc, że są prowadzone prace by można go było wykorzystywać w sytuacjach safety tj. kiedy muszą zostać spełnione normy ISO-26262. Zaprojektowany przez Mozillę.
GolangInny stosunkowo młody język, zaprojektowany przez Google’a, który całkiem fajnie się sprawdza w zadaniach rozproszonych. Moje główne skojarzenia z nim odnoszą się głównie z aplikacjami od Docker’ów.
PythonW mojej opinii fajny język dopóki używa się go do prototypowania. Często używany do skryptowania, małych aplikacji webowych oraz przez naukowców do symulacji i obliczeń.
RubyKolejny język skryptowy, który kojarzę głównie za sprawą swego czasu modnego frameworku Ruby on Rails, i chyba to jest jego najczęstsze zastosowanie.
PHPMartin Lechowicz śpiewał kiedyś „Programuję w dotnecie już trzecie stulecie bo kto się w PHPie połapie”… Kolejny język używany w aplikacjach internetowych o którym zbyt wiele nie wiem.
JavaScriptJęzyk skryptowy wykonywany przez przeglądarki internetowe, najczęściej służy do oprogramowania działania front-endu, czyli tzw. widocznej części aplikacji internetowych.
PrologJęzyk deklaratywny, służący do programowania w logice. Program stanowi sekwencję przesłanek, które muszą zostać spełnione, aby rozwiązać problem. :- – oznacza implikacje, tj. quicksort([X|Xs], Ys) :- … oznacza, że element X skonkatenowany z Xs jest w relacji quicksort z Ys jeśli spełnione są warunki …

Ostatnio też zauważalna jest tendencja do tworzenia języków kompatybilnych z ekosystemami związanymi z istniejącymi językami programowania:

  • Java => Scala, Clojure, Kotlin
  • JavaScript => TypeScript,
  • C++ => Carbon.

Po co tworzyć takie języki? Historia każdego jest nieco inna… Język programowania jest formą dzięki której programiści mogą wyrażać swoje myśli. Dobrym przykładem tego jest Clojure, który posiada składnię LISPu, ale wciąż działa na maszynie wirtualnej Javy dzięki czemu oba mogą koegzystować w ramach jednej aplikacji. Z drugiej strony istniejące języki nie rzadko cierpią na chorobę wieku dojrzałego. Dodajemy w nich nowe funkcje, ale nie zmieniamy części jego wcześniejszych cech dla zachowania kompatybilności przez co stają się coraz bardziej skomplikowanymi (przypadek C++’a, w którym niedługo sam diabeł nie dojdzie co się dzieje 😉). Nowy język może zachować zgodność z bibliotekami napisanymi w starym języku jednakże oferując czystszą składnię – tego przykładem jest Carbon, którego celem w teorii jest stopniowe zastępowanie części starego kodu w C++’ie. Jednak wciąć twórcy tego języka zalecają, że jeśli projekt pisany jest od podstaw, zalecane jest użycie Rust’a (pomijając nawet fakt, że nie jest on jeszcze dość dojrzały, żeby używać go na produkcji).

Zakończenie

To już koniec, mam wrażenie, że macie już pewne wyobrażenie jak wygląda praca programisty. W kolejnych artykułach przejdę na reszcie do technikaliów 😊

Miłego wieczoru!

Paweł

Sezamie otwórz się i krótko o dowodach z wiedzą zerową

Sezamie otwórz się, krótko o dowodach z wiedzą zerową

Cześć!

Pamiętacie może dziecięce przepychanki w stylu „Wiem, ale nie powiem”? Dzisiaj zastanowimy się w jaki sposób projektanci kryptosystemów radzą sobie z podobnym problemem – jak  udowodnić znajomość sekretu (np. tajnego klucza) bez ujawniania go nikomu

Wiem Ale GIF - Wiem Ale Nie - Discover & Share GIFs.

Analogia jaskini jako przykład dowodu o wiedzy zerowej

Możemy to zrobić korzystając ze swego rodzaju dowodu nie wprost, zanim jednak pokażę Wam to na przykładzie protokołu identyfikacji Schnorr’a, chciałbym żebyście przyjrzeli się pewnej dość obrazowej analogii. Spójrzcie na rysunek poniżej.

Wyobraźcie sobie, że ta grafika przedstawia plan kampusu NSA (amerykańskiej Agencji Bezpieczeństwa Narodowego). Drzwi 2 stanowią wejście do cywilnej części budynku podczas gdy drzwi 3 są dla personelu wojskowego. W bazie pod numerem 1 znajduje się pomieszczenie z klatką schodową prowadzącą do podziemnej serwerowni i jest to jedyne bezpośrednie przejście pomiędzy częścią cywilną a wojskową. Wejścia na klatkę od obu stron zabezpieczone są kartą magnetyczną z PIN’em. Część personelu wojskowego zajmująca się sprzętem komputerowym ma dostęp do serwerowni oraz obu części budynku… Chociaż tak między Bogiem i prawdą, Kevin Mitnick we wspomnianej kiedyś książce „Sztuka Infiltracji” opisywał, że największe uprawnienia dostępu fizycznego mają zazwyczaj sprzątaczki 😉.

Główną bohaterką opowiadania będzie żona jednego z pracowników. Teraz wszędzie jest moda na szukanie ruskich agentów więc założymy, że została ona podkupiona przez rosyjskie służby aby ustalić czy jej mąż posiada dostęp do serwerowni. Ponieważ nie jest ona drugim Jamesem Bondem wynajęła do pomocy prywatnego detektywa. Na szczęście mąż też nie jest na tyle rozgarnięty aby pamiętać, którymi drzwiami danego dnia wchodził 😉.

Przejścia są pilnowane przez portiera, aby nikt nie uprawiał tzw. tailgating’u, tj. wchodzenia na ogonie jednak doświadczony detektyw wie, że skoro jest on średnio rozgarnięty (agent w stylu Johnny’ego English’a) nie da rady pilnować się zawsze – przyjęto za okres obserwacji 3 miesiące. W końcu udało się i na sam koniec pracy z teleobiektywem w ręce dojrzał mężczyznę wchodzącego drzwiami 2 i wychodzącymi 3.

Zmienimy teraz nieco tę historię – przez 3 miesiące mężczyzna się pilnował a żony nie jest stać na dalszą obserwację bo męża zdziwi saldo na koncie. Żona wykazała się wręcz szatańską pomysłowością. Otóż pani, nazwijmy ją Twardowska (w końcu amerykanie nie rozróżniają polskich od rosyjskich nazwisk), wymyśliła sobie grę. Umówiła się z mężem, że nie będzie pilnować jak pan Twardowski wchodzi ale będzie mówić mężowi którymi drzwiami ma wyjść 😊. I tego dopilnuje… Jak powiedziała tak zrobiła. W końcu musi obserwować go tylko przez godzinę czy dwie a nie musi płacić pełnej dniówki detektywowi. A biorąc pod uwagę, że mąż wie, że nie ujawni informacji, którymi drzwiami wszedł mówi: „Dobra, zacznijmy zabawę!”. Czy mąż wyciągnął prawidłowe wnioski?

Odpowiedzmy sobie na pytanie – jakie jest prawdopodobieństwo, że mężczyzna zna PIN przy jednej próbie? Prawdopodobieństwo, to wynosi . Spójrzcie na tabelkę poniżej – jedynka oznacza, że wybrał on dokładnie to samo wyjście co żona.

 Wybrane przez żonę wyjście
Lewe drzwiPrawe drzwi
Losowo wybrane przez męża wejścieLewe drzwi10
Prawe drzwi01

Sprawdźmy jakie będzie prawdopodobieństwo, że w tydzień, stosując tą, grę poznamy prawdę? Prawdopodobieństwo, że przez tydzień zarówno wchodził jak i wychodził jest… pewne czyli wynosi 1. Przeprowadźmy krótkie rachunki:

Zdarzenie to jest więc niemal pewne. Zwróćcie uwagę, że poznaliśmy informację czy mąż zna PIN a nie zdradził nam samej informacji. Czy da się zrealizować coś podobnego wykorzystując matematykę? Bajeczka może i jest dość mocno naciągana ale pokażę Wam teraz protokół Schnorr’a dzięki któremu jedna osoba może udowodnić drugiej znajomość tajnego klucza nie zdradzając go.

Protokół identyfikacji Schnorr’a

Zacznijmy z kolejną historią – łatwiej będzie przedstawić mi protokół, jeśli naszym bohaterom nadamy imiona 😉 Nazwijmy ich Alicja (będzie ona posiadaczką tajnego klucza) i Bob (będzie miał informację potrzebną do weryfikacji). Alicja chce udowodnić Bobowi znajomość sekretu – aby to zrobić musi przeprowadzić z nim wymianę kilku wiadomości. Spójrzcie na diagram poniżej, jednak najpierw parę założeń:

  1. g jest parametrem znanym zarówno przez Alicję i Bob’a,
  2. Alicja jest posiadaczką klucza prywatnego  i przekazała Bob’owi wartość ,
  3. Liczby a oraz c są losowymi liczbami naturalnymi.

W jaki sposób to równanie zdradza nam, że Alicja zna hasło? Odpowiedź tkwi w matematyce jeszcze z lat szkolnych. Dowód jest następujący:

Równość jest tylko wtedy prawdziwa, gdy Alicja zna klucz prywatny. Pozostało jeszcze pytanie czy jest on bezpieczny. Dowodzimy tego z wykorzystaniem tzw. modelu z wyrocznią losową.

Dowody z wyrocznią losową

Cały pomysł dowodu inspirowany jest wyrocznią delficką. W starożytnej Grecji, w miejscowości Delfy znajdowała się znana niegdyś w całym antycznym świecie wyrocznia. Za przepowiednie odpowiadała Pytia – siedząca na trójnogu, odurzone oparami ze szczeliny w ziemi dziewczyna, wychowywana od urodzenia w świątyni. W narkotycznym widzie odpowiadała ona luźnymi słowami, które później kapłani interpretowali jako przepowiednie. Wpływały one na wszystko, od polityki do zwykłych i codziennych spraw a jako, że specjalnie wiary w takie przepowiednie nie pokładam sądzę, że były one raczej losowe. W tym też charakterze będziemy wykorzystywać wyrocznię w naszym dowodzie.

Dowód ten może Wam przypominać dowody nie wprost z lekcji matematyki jednak na razie musimy postawić parę założeń:

  1.  (od Oracle) jest to wyrocznia, która wciela się w rolę strony weryfikującej tożsamość, zakładamy, że potrafi odróżnić wymagane przez protokół wartości od liczb zupełnie losowych,
  2.  (od Adversary) – nasz oponent, który poddaje się identyfikacji, zakładamy, że nie zna klucza prywatnego jednak ma strategię na prawidłową identyfikację.
  3. Potrafimy dokonać transkrypcji wszystkich zapisów protokołu.

Od czego zaczynamy dowód? Spójrzcie na rysunek poniżej:

Przedstawia on zapis dwóch przejść protokołu. Liczby ,  służą inicjacji protokołu (nazywamy te wiadomości commitment’ami, zobowiązaniem względem weryfikatora), ,  są wyzwaniami (challenge) rzuconymi przez wyrocznię, ,  są to dowody posiadania znajomości klucza prywatnego a ,  odpowiedzi wyroczni. Warto przypomnieć, że Adwersarz nie zna klucza prywatnego jednak potrafi wyciągnąć z kapelusza (albo mówiąc bardziej technicznie wylosować lub wygenerować) liczby, które umożliwią mu identyfikację przed wyrocznią. Przypuśćmy, że nasz oponent potrafi dokonać transkrypcji wszystkich przebiegów protokołu (oznaczymy tą liczbę literą ). Spójrzmy na ten zapis transkrypcji:

                                                                                         …

Jeśli  jest dostatecznie duże (lub skrajnie – jako  zostaną wykorzystane wszystkie opcje) z pewnym prawdopodobieństwem powtórzy się któryś z wcześniej wykorzystanych commitment’ów (liczb ). Dla ułatwienia załóżmy, że  powtórzy się jako  i wyrocznia przekaże nowe wyzwanie  a adwersarz wykorzysta swoje moce by wygenerować nową wartość . Jakie są tego konsekwencje?

Jeśli liczby  i  były wcześniej wygenerowane z wykorzystaniem strategii Adwersarza i

wiemy, że

Wtedy adwersarz może obliczyć klucz prywatny z wykorzystaniem następującej formuły , gdyż zarówno wartości , , ,  są mu doskonale znane.

Czego to dowodzi? Protokół byłby możliwy do złamania, gdyby istniał adwersarz z nieskończoną pamięcią i czasem żeby przeszukać dostatecznie dużą część przestrzeni oraz potrafiący wylosować liczby zgodne z protokołem… Czego prawdopodobieństwo jest pomijalnie małe.

Czy Was ten dowód przekonuje? Mnie osobiście nie do końca… Przyjmujemy mało realne założenia, żeby dojść do oczywistych wniosków – w czasie studiów nazywałem tego typu dowody dowodami przez machanie rękami albo dowodami przez niemalże założenie tezy 😉. Przytaczam go przede wszystkim z poczucia kronikarskiego obowiązku .

Powrót do świata matematyki

O ile sam protokół działa to jest też niestety jest jeden drobny szkopuł. Matematycy lubią nieskończoność ale informatycy nie radzą sobie z nią najlepiej. O ile wyobraźnia potrafi wytrzymać nieskończenie wielką liczbę tak komputery nie mają pamięci by je przechowywać. Spójrzcie na zakresy poszczególnych typów całkowitoliczbowych w języku Rust na 64-bitowym procesorze Intel’a:

Nazwa typuDolny zakresGórny zakres
u80255
u16065535
u3204294967295
u64018446744073709551615
i8-128127
i16-3276865535
i32-21474836482147483647
i64-92233720368547758089223372036854775807

Liczby typu u64 wydają się duże ale biorąc pod uwagę poziom bezpieczeństwa i fakt, że intensywnie wykorzystujemy potęgowanie potrafię sobie wyobrazić, że będzie to wartość zbyt mała i przekroczymy zakres wartości… Czy potrafimy sobie z tym poradzić?

Odpowiedź jest na szczęście pozytywna 😊 Naszym głównym problemem jest fakt, że mnożenie w pewnym momencie może nas wyprowadzić poza zbiór osiągalnych wartości. Powinniśmy skonstruować sobie operację matematyczną, która będzie zamknięta w sensownym zbiorze wartości. Na pomoc tutaj przychodzi nam algebra abstrakcyjna i teoria struktur algebraicznych. Zanim jednak o tym, przez chwilę poopowiadam Wam o liczbach pierwszych, które będą dla nas w pewnym momencie istotne.

Liczby pierwsze

Mam nadzieję, że się nie obrazicie za drobne przypomnienie, że liczby pierwsze  są to liczby naturalne, które dzielą się tylko i wyłącznie przez 1 oraz samą siebie. Tak na marginesie jest nawet jeden dowcip z brodą jak u Albusa Dumbledora jak różni naukowcy sprawdzają czy liczby nieparzyste są pierwsze.

Studenci z różnych wydziałów dostali zadanie: udowodnij, że wszystkie liczby nieparzyste większe od 1 są pierwsze.
– Matematyk: Dla 3 – zgadza się. A dalej jakoś to pójdzie przez indukcję…
– Inżynier: Dla 3 – zgadza się, dla 5 – zgadza się, dla 7 – zgadza się…
– Chemik: Dla 3 – zgadza się, dla 5 – zgadza się, dla 7 – zgadza się, dla 9 – zgadza się…
– Fizyk: Dla 3 – zgadza się, dla 5 – zgadza się, dla 7 – zgadza się, 9 to błąd pomiarowy, dla 11 – zgadza się, dla 13 – zgadza się…
– Astronom: Dla 639547 – zgadza się, dla 979949 – zgadza się…
– Informatyk: Dla 3 – zgadza się, dla 3 – zgadza się, dla 3…

Na potrzeby protokołu będziemy potrzebowali wygenerować odpowiednio długie (tj. w zapisie wykorzystującym system dwójkowy) liczby pierwsze. Aby to zrobić możemy spróbować wykonać następujące działania:

  • Wygenerować wszystkie liczby pierwsze z wykorzystaniem sita Eratostenesa, dopóki nie osiągniemy większego zbioru liczb  z których możemy losować – rozwiązanie kiepskie bo wymaga za równo masę pamięci jak i czasu…
  • Wylosować ciąg bajtów i sprawdzić czy liczba jest podzielna przez wszystkie liczby mniejsze od niej – rozwiązanie niewiele lepsze, ale wciąż czasochłonne.

Możemy pójść o krok dalej, że nie ma sensu sprawdzać wszystkich dzielników liczby a zatrzymać się na jej pierwiastku jednak przy dużych liczbach wciąż nie jest to oczekiwane, efektywne rozwiązanie problemu.

Rozwiązanie, które jest faktycznie wykorzystywane bazuje na losowości i statystyce nazywane testem Millera-Rabina. Poniżej znajduje się opis algorytmu:

Algorytm ten będzie częścią implementacji protokołu Schnorr’a, do której link znajdziecie na końcu artykułu a teraz możemy wrócić do tematu struktur algebraicznych.

Podstawowa struktura algebraiczna – grupa

Strukturą algebraiczną nazywamy zbiór oraz działania na nim określone, które spełniają pewne konkretne własności. Warto wspomnieć, że pomysł C++’owych szablonów, gdzie funkcja do działania potrzebuje aby argumenty spełniały jedynie wąsko określone wymagania formalne. Dobrze, ale przejdźmy do rzeczy i zajmijmy się najbardziej podstawową strukturą, która wkrótce będzie dla nas istotna.

Pierwszą i najważniejszą strukturę odkrył Evariste Galois w 1832 roku jest to grupa. Definiujemy ją jako parę , gdzie  jest to zbiór na którym określone jest dowolne działanie binarne (tzn. dwuargumentowe, podobnie jak dodawanie czy mnożenie). Poniżej przedstawiona jest lista własności, które musi spełniać działanie na grupie:

  • Łączność: ,
  • Identyczność (albo istnienie elementu neutralnego :  lub  ,
  • Istnienie elementu przeciwnego :  .

W zależności czy używamy notacji addytywnej (bazującej na dodawaniu) używamy zapisu
(i nazywamy elementem przeciwnym) a w przypadku multiplikatywnej (bazującej na dodawaniu) używamy zapisu  (i nazywamy elementem odwrotnym). Co oznacza, zamkniętość działania
w grupie? Dla dowolnych dwóch elementów zbioru  wynik działania  również należy do zbioru .

Jeśli dodatkowo działanie to jest przemienne to taką grupę nazywamy abelową.

Jakie znamy przykłady grup?

  •  – addytywna grupa liczb całkowitych, elementy są liczbami całkowitymi a operacją dodawanie,
  •  – multiplikatywna grupa niezerowych reszt modulo 7, elementami są liczby od 1 do 6 a działaniem mnożenie modulo 7. Poniżej znajduje się tabela pokazująca „tabliczkę mnożenia”
123456
1123456
2246135
3362514
4415263
5531642
6654321
  •  – addytywna grupa liczb całkowitych modulo 6, elementami są liczby od 0 do 5 a działaniem dodawanie modulo 6. Można skonstruować podobną tabelkę do tej z poprzedniego przykładu, najprościej porównać dodawanie w tej grupie do liczenia godzin na zegarku 😊

Dlaczego  – zbiór liczb całkowitych z mnożeniem nie jest grupą? Nie ma ani jednej liczby całkowitej, której odwrotność względem mnożenia należy również do liczb całkowitych.

Użyteczne dla nas będą jeszcze następujące pojęcia:

  1. Rzędem grupy nazywamy moc (inaczej liczbę elementów) zbioru elementów,
  2. Rzędem elementu nazywamy najmniejszą potęgę, która da element neutralny.
  3. Generatorem grupy nazywamy element, na którym powtarzając cały czas działanie wygenerujemy wszystkie elementy zbioru grupy – żeby wygenerować całą grupę  potrzebujemy dwie wartości .
  4. Podgrupą nazywamy podzbiór zbioru  z działaniem, który też jest grupą.

Teraz wrócimy do obliczeń związanych z protokołem Schnorr’a.

Grupa Schnorr’a

Claus Schnorr zaproponował żeby wszystkie obliczenia dotyczące jego protokołu były przeprowadzane w pewnym wyspecyfikowanym matematycznym świecie, który nazywamy grupą Schnorr’a. Grupa ta jest podgrupą (rzędu dużej liczby pierwszej) multiplikatywnej grupy modulo , gdzie  jest liczbą pierwszą.

Żeby wygenerować taką grupę potrzebujemy liczby  takie, że  i  są pierwsze i . Następnie musimy wylosować

,

takie że . Liczba  z definicji musi być różna od 1 gdyż w przeciwnym wypadku  oraz  byłyby dwiema sąsiadującymi liczbami pierwszymi. Liczba  będzie generatorem naszej grupy a zarazem podstawą wszystkich potęg z protokołu. Teraz odpowiemy sobie dlaczego 😊. Warto wspomnieć, że jeśli  jest liczbą pierwszą to grupa multiplikatywna  jest cykliczna.

Po pierwsze, przy wykonywaniu protokołu wykonujemy bardzo wiele mnożeń i potęgowań (które de facto też jest złożeniem mnożeń) w związku z tym musimy mieć gwarancję, że wykonując te działania   pozostaniemy w grupie. Liczba  mówi nam w uproszczeniu o tym jak duże wartości mogą potencjalnie znaleźć się w grupie, podczas gdy liczba  będąc rzędem grupy określa maksymalną liczbę mnożeń, którą potrzebujemy wykonać aby dojść do elementu neutralnego. Dlaczego chcemy wydłużać tą ścieżkę? Żeby obliczenia pozostały w podgrupie dla każdego wykonanego mnożenia nasza podgrupa musi być cykliczna – dłuższy cykl oznacza, większy czas potrzebny na przeszukanie podgrupy wygenerowanej przez generator.

Gdyby  byłoby równe 1 oznaczałoby to, że nie nadaje się na generator gdyż podgrupa byłaby zdegenerowana – zawierałaby tylko jeden element, tj. liczbę 1.

Zakończenie

Kończąc podam Wam ostatnie założenia. Liczba  powinna być od 1024-3072 bitowa podczas gdy  160-256 bitowe. Założenie to jest konieczne aby zabezpieczyć się przed atakiem na problem dyskretnego logarytmu (czyli próbę odzyskania wykładnika potęgi, co jest dla komputera kłopotliwym zadaniem) oraz atakom bazującym na małej podgrupie.

Planuję przygotować dla Was krótki artykuł na temat ataków na matematyczną stronę protokołów w którym omówię wskazane ataki.Na stronie repozytorium (DODAJ LINK) znajdziecie uproszczoną implementację protokołu Schnorr’a – uproszczenie polega na tym, że używam mniejszych liczb niż zalecane 😉 Możecie jednak zobaczyć, że matematyka, którą Wam przed momentem opisałem faktycznie działa!

Pozdrawiam,
Paweł Jarosz

W samolot do Vegas albo parę słów o losowości

Cześć!

W poprzednim wpisie wspominałem Wam, że jeśli używamy klucza tylko i wyłącznie jednorazowo to szyfr jest na tyle bezpieczny na ile nieprzewidywalne są liczby losowe użyte do szyfrowania. Może Was to nieco dziwić… Każdy z nas, od wczesnego dzieciństwa przyzwyczajony jest do losowych wyników poprzez rzut kością w grach planszowych czy przypadkowej kolejności kart w potasowanej talii (notabene do tematu gier karcianych wrócę nieco później) więc losowość do pewnego stopnia jest dla nas naturalna.

Niestety (albo stety) nie jest tak dla komputera, który ze swej natury jest deterministyczny. Podczas gdy my, rzucając kością wyzwalamy zdarzenia losowe, ponieważ nie jesteśmy w stanie rzucić kością zawsze dokładnie tak samo (chyba, że kość jest kantowana) to komputer nie ma takiej możliwości. Prawdziwa losowość może bazować na szumie sensorów albo zjawiskach fizycznych – może to być np. rozpad promieniotwórczy nie mówiąc już o innych zjawiskach elektromagnetycznych lub kwantowych.

Sensory jako źródło entropii

Spójrzcie na nagranie poniżej – prezentuje ono zapis odczytów akcelerometru, który jak widzicie tkwi w bezruchu… A odczyty szaleją!

Odczyty akcelerometru

Szum w odczytach czujników jest sporym problemem dla robotyków, którzy radzą sobie z nimi wykorzystując różnego rodzaju filtry, takie jak np. filtr Kalmana dla nas ma on jednak wielkie znaczenie. Pokazuje, że czujniki mogą stanowić źródło losowości (albo używając mądrzejszych słów entropii) dla komputera. Jeśli się zastanowimy, to upakowana jest w nim cała masa różnych komponentów elektronicznych, które często potrzebują ze względów bezpieczeństwa np. informację o temperaturze komponentu… System operacyjny zbiera te odczyty generując entropię dla generatorów liczb losowych.

Wykres poniżej przedstawia obciążenie procesora w czasie pracy komputera. Niektóre programy tj. na przykład VeraCrypt (służący do szyfrowania danych) wykorzystują w tym celu chaotyczne trajektorie ruchów myszy użytkownika w czasie instalacj oprogramowania.

Rysunek 1 Obciążenie procesora przy włączonym Wordzie i Netflix’ie

To podejście ma jednak drobną wadę – jest powolne. Dlatego informatycy wymyślili generatory liczb pseudolosowych. Algorytmy, które zainicjowane ziarnem, podają na wyjściu liczby, sprawiające wrażenie losowych.

Generatory liczb pseudolosowych

Jednym z przykładów takich algorytmów jest liniowy generator kongruentny (ang. linear congruential pseudorandom generator, LCRNG). Wiem, nazwa brzmi mądrze, gdyż wzięła się stąd, że wykorzystuje on reszty z dzielenia (stąd kongruentny). Działa on w następujący sposób:

  1. Użytkownik inicjuje generator ziarnem, które oznaczamy jako X0
  2. Kolejne liczby są generowane wg. Następującej formuły

Zapis ten oznacza, że  ma wartość reszty z dzielenia  przez m, gdzie a, c oraz m są stałymi w czasie działania algorytmu.

Zróbmy drobny test, wykorzystajmy tę formułę w wyznaczenia przybliżonej wartości liczby π metodą Monte Carlo (tak na marginesie, została ona opisana i po raz pierwszy wykorzystana przez polskiego matematyka ze Lwowa, Stanisława Ulama). Działa ona w następujący sposób:

Losujemy punkty o współrzędnych z przedziału od -1 do 1 i zliczamy liczbę punktów, które znajdują się w kole o środku w punkcie (0, 0) i promieniu 1 – pole takiego koła wynosi… Niespodzianka, właśnie liczba π 😊. Punkty te należą do koła, jeśli spełniają nierówność .

Liczba π ma wtedy wartość  .

Rysunek 2 Rysunek poglądowy przygotowany z wykorzystaniem programu Wolfram Mathematica

Program montecarlo https://gitlab.com/sisiphus-on-code/cryptography/-/tree/main/random_number_generator/montecarlo zawiera implementację algorytmu LCRNG i wykorzystuje go do wyznaczenia wartości liczby Pi. Możecie sprawdzić, że im więcej punktów losujemy, tym bliżej otrzymana wartość bliska jest liczbie 3,14 zatem algorytm z dużym prawdopodobieństwem generuje liczby losowe. Pozostaje tylko pytanie, czy jest jakiś haczyk, tym bardziej, że jest on wykorzystywany w bibliotekach standardowych wielu różnych języków programowania – w tym glibc języka C, która jest swego rodzaju lingua franca świata komputerów. Tak na marginesie, z niej właśnie wziąłem parametry do generatora użytego w metodzie Monte Carlo.

Niestety, jak się za chwilę dowiecie algorytm ten w pewnych okolicznościach może być przewidywalny o czym boleśnie przekonały się swego czasu kasyna Las Vegas.

Historia o automatach do gry w pokera

Kilka lat temu czytałem książkę Kevina Mitnicka „Sztuka infiltracji”, obecnie szanowanego konsultanta do spraw bezpieczeństwa komputerowego, a kiedyś hakera znanego pod pseudonimem Condor (było to nawiązanie do filmu z „Trzy dni kondora” z roku 1975, w którym główną rolę grał Robert Redford).

Otóż Mitnick, przytoczył w niej historię grupki znajomych, którzy mieli przeprowadzić konsultacje w Nevadzie i pod wpływem ich piękniejszych, drugich połówek zdecydowali się by przy okazji znaleźć sposób na rozbicie puli w kasynach Vegas. Czego nie zrobią mężczyźni, gdy kobiety im wejdą na ambicje!

Grupa analizowała przede wszystkim dwie możliwości w jaki sposób można potencjalnie zaatakować kasyno albo mówiąc bardziej profesjonalnym językiem, wyznaczyli następujące wektory ataku:

  1. Fizyczne włamanie do maszyn i “podmiana firmware’u”

    Umieszczenie własnego chipu, aby zastąpić oryginalne oprogramowanie wersją zapewniającą bardziej atrakcyjne wypłaty. Rozwiązanie to jest o tyle problematyczne, że wymaga współpracy z technikiem opiekującym się automatami – można byłoby je porównać z walnięciem staruszki po głowie i odebraniem jej torebki,

  2. Atak zaczerpnięty z książki The Eudaemonic Pie Thomasa Bassa (Penguin, 1992).

    Grupa komputerowców i fizyków rozpracowywali w latach 80 system ruletki w kasynach. Opracowali komputerek, któremu jeden członek grupy podawał prędkość koła ruletki
    oraz obrotów kulki i próbował przewidzieć wynik gry. Nie wygrali tym sposobem tony złota, gdyż ich metoda była skażona tym, że dużą rolę odgrywało w niej zachowanie i stosunki międzyludzkie – nie mogli się wydać podejrzani innym uczestnikom gry.

Podejście, które zastosowała grupa bazowała na ataku 2, ale wybrali grę, która ograniczała stosunki między ludzkie do minimum – czyli automaty do gry w pokera.

Prace nad problemem rozpoczeli or pragmatycznego założenia związanego względami proceduralnymi. Maszyny zanim zostaną dopuszczone do użytku muszą przejść testy wymagane przez Komisję Gier Hazardowych Las Vegas. Testy te nie dość, że zabierają bardzo dużo czasu to do tego są jeszcze niezwykle kosztowne. Ich celem jest sprawdzenie czy z jednej strony automat zapewnia dostateczną losowość, z drugiej czy nie zapewnia nieuczciwej przewagi kasynu. Do pewnego stopnia musi być przewidywalne, jak często pada zwycięskie rozdanie, aby kontrolować maksymalną wartość puli. W związku z tym kasyna decydowały się dłużej eksploatować przestarzałe maszyny niż to było wskazane. Bezpieczeństwo poprzez wycinkę drzew i generowanie tony dokumentów jest elementem charakterystycznym dla branży automotive. Za pewne pożalę się na to w którymś z kolejnych artykułów.

Panowie koledzy pojechali więc do sąsiedniego stanu i.… zakupili w nieoficjalnym obiegu jedną ze starszych maszyn do pokera, gdyż założyli (jak się później okazało słusznie), że podobne są używane w Vegas. Stwierdzili też, że skoro planują atakować automaty o niskich wypłatach to za pewne generator nie jest zbyt silny i może być wadliwie zaimplementowany 😊. Maszyny po przewiezieniu do wynajętych przez nich apartamentów, zostały rozebrane na części pierwsze po czym zrzucili zawartość pamięci Flash z programem.

Stosując narzędzia inżynierii wstecznej (czyli odzyskiwaniu informacji jak on działa na podstawie analizy istniejącego produktu), odkryli, w jaki sposób generowane są wartości kart oraz to,
że losowość zapewniana jest przez znany nam już algorytm LCG. Dało im to zdolność do przewidzenia, za ile rozdań pojawi się zwycięskie rozdanie. W jaki sposób możemy przewidzieć kolejne liczby generatora?

Hakowanie glibc’a?

Spójrzcie na zamieszczony poniżej fragment linuksowego manual’a dotyczącego funkcji random().

Rysunek 3 Fragment linuksowego manuala dotyczącego funkcji random()

Kluczowe jest pierwsze zdanie: “The random() function uses a nonlinear additive feedback random number generator”, które jest nie do końca prawdziwe. Dlaczego?

Jeśli zerkniecie https://github.com/lattera/glibc/blob/master/stdlib/random_r.c znajdziecie tam kod, który na pierwszy rzut oka (i nawet na drugi 😉) wydaje się całkiem skomplikowany jednak Peter Selinger z Uniwersytetu Dalhousie na stronie https://www.mathstat.dal.ca/~selinger/random/ zaprezentował algorytm, który produkuje dokładnie te same wartości co funkcja random() (oczywiście dla tego samego ziarna).

Na stronie mojego repozytorium https://gitlab.com/sisiphus-on-code/cryptography/-/tree/main/random_number_generator/glibc_randomness znajdziecie trzy programy:

  • GlibcGenerator – oczekuje podania ziarna generatora, informację ile liczb losowych ominąć i ile liczb wydrukować na ekranie. Liczby są wygenerowane używając funkcji random().
  • AlternativeGenerator – oczekuje na podobne parametry wejściowe jak program powyżej jednak liczby generowane są z wykorzystaniem algorytmu Selingera, który nieco zrefaktoryzowałem,
  • GlibcNextPredictor – oczekuje na 31 liczb podanych poprzez standardowe wejście i na ich podstawie generuje jaka będzie następna liczba dostarczona poprzez generator glibc’a.

Zajmiemy się teraz przez moment analizą programu AlternativeGenerator. Co możemy zaobserwować przy pierwszym czytaniu kodu

  1. W funkcji main nie ma żadnego bezpośredniego odwołania do struktury state (typu randomness_state).

Struktura ta jest wewnętrznym stanem generatora, gdyby wyciągnąć funkcje init_randomness() oraz get_random() do osobnego pliku, programista nie miałby do niej w ogóle dostępu.

  • Przed użyciem funkcji get_random() wywołujemy funkcję init_randomness() zatem sekwencja jest podobna do użycia funkcji srand() i random().
  • Algorytm LCG jest używany TYLKO I WYŁĄCZNIE na początku inicjowania tablicy stanów, później używane są już proste sumy lub podstawienia.
  • Funkcja random() nie zwraca nam dokładnej wartości przechowywanej w wewnętrznym stanie tylko obciętą o najmniej znaczący bit.

Kluczowe dla nas będą obserwacje 3 i 4. Możemy zauważyć, że jeśli dostaliśmy 31 liczb z generatora to kolejna jest sumą liczb o indeksach 0 oraz 28. Jednak musimy sobie przypomnieć, że liczby które widzieliśmy na ekranie są obcięte o jeden bit względem wewnętrznego stanu generatora zatem kolejna liczba może być mieć jedną z następujących wartości:

  • 2 * buffer[0] + 2 * buffer[28] – brakującym bitem obu liczb jest zero
  • 2 * buffer[0] + 2 * buffer[28] + 1 – brakującym bitem jednej z liczb jest 1 (najbardziej prawdopodobna).
  • 2 * buffer[0] + 2 * buffer[28] + 2 – brakującym bitem obu liczb jest jeden

Jak widzicie udało nam się z pewnym prawdopodobieństwem określić jakie będzie kolejna liczba pochodząca z generatora.

Na ile dobry jest los?

Skoro już wiemy, że generator generatorowi nie jest równy, warto zastanowić się nad tym czy da się to w jakiś sposób sprawdzić.

Możemy tego dokonać z wykorzystaniem narzędzi takich jak:

  • Dieharder,
  • TestU01.

Próbują one znaleźć korelację pomiędzy kolejnymi liczbami losowymi implementując różne testy statystyczne (np. bazujące na paradoksie urodzinowym). Przykładowo, mogą one zliczać długość sekwencji rosnących i malejących wygenerowanych liczb i sprawdzać ich rozkład (żeby potencjalny adwersarz nie mógł określić prawdopodobieństwa czy kolejna liczba będzie większa czy mniejsza).
Do zastosowań kryptograficznych ważne jest aby rozkład generowanych liczb był jednostajny.
W rozkładzie jednostajnym, każdy bajt jest tak samo prawdopodobny, podczas gdy w rozkładzie normalnym wartości środkowe występują znacznie częściej niż skrajne.

Rysunek 4 Przykładowy rozkład zbliżony do normalnego

Rysunek 5 Przykładowy rozkład zbliżony do jednostajnego

Swego czasu, zdarzyło mi się eksperymentować ze sprzętowym generatorem liczb losowych dostępnego na płytce Nucleo L552ZE-Q. Test dieharder jest to aplikacja, która przyjmuje testowane liczby na standardowe wejście (tj. wejście na konsolę terminala) lub z pliku tekstowego. TestU01 jest natomiast dostarczany jako biblioteka, gdzie bateriom testowym przekazujemy funkcję generującą (czy w moim przypadku dostarczającą pobrane poprzez UART) liczby losowe. Wykonałem te testy na generatorze RC4, linuksowym pseudourządzeniu /dev/urandom (w systemach uniksowych każde urządzenie konceptualnie jest reprezentowane jako plik, generator liczb pseudolosowych nie jest prawdziwym urządzeniem, a sztucznie utworzonym przez jądro systemu, stąd prefiks pseudo) oraz sprzętowym z mikrokontrolera. Jakie były wyniki? Pierwsze dwa generatory okazały się bezpieczne podczas gdy STM’owy okazał się być wadliwy – prawie żaden nie przechodził. Wydało mi się to dziwne, gdyż znalazłem informację, że przechodzą na nim testy sugerowane przez NIST (bardzo ważną organizację wytyczających normy cyberbezpieczeństwa dla firm prywatnych). Myślałbym, że to wina sprzętu, jednak testy dostarczone przez producenta przechodzą właściwie (a niespodziewałbym się, że firma taka jak STMicro kłamie), stąd podejrzewam raczej, że źródło problemów znajdowało się między klawiaturą a krzesłem 😉. Później pojawiły mi się pilniejsze rzeczy do zrobienia więc nie inwestygowałem tego dalej.

Kończąc, sprzedam Wam anegdotkę, że pisząc ten test dość boleśnie przekonałem jak wiele daje buforowanie. Wczytując po jednej linii z UART’u duża bateria testowa TestU01 wykonywała się 3h i końca nie było widać podczas gdy zaimplementowałem wczytywanie bufora po 256 znaków czas skrócił się do… o ile mnie pamięć nie myli pół godziny. W tym momencie przypomniałem sobie cytat twórcy jądra systemu Linux, Linusa Torvaldsa, który słynie z dość „obrazowego” języka:

„Chciałbym również zasugerować, że należałoby wstecznie zaabortować każdego geniusza, który pomyślał, że czytanie danych przy pomocy JEDNEGO J*BANEGO BAJTU NA RAZ przez wywołania systemowe to dobry pomysł. Kto, do cholery, robi tak debilne rzeczy? Jak to się stało, że nie umarł jako dziecko – mając na uwadze, że prawdopodobnie był za głupi na znalezienie cycka, którego miał ssać.”

Całe szczęście on tego nie widział i to było tylko na chwilę 😉! Tymczasem chciałbym się z Wami pożegnać, a w kolejnym artykule pokażę Wam małe matematyczne czary mary – zajmiemy się tak zwanymi dowodami o wiedzy zerowej.

Pozdrawiam,
Paweł

UWAGA KOŃCOWA 1 – Jak tutaj się prezentuje algorytm RC4? Pewne sekwencje bajtów pojawiają się nieco częściej niż inne co zostało wykorzystane w ataku Fluhrera-Mantina-Shamira. Analizując dużą liczbę wiadomości zaszyfrowanych tym samym kluczem, można odzyskać klucz co pozwoliło złamać standard szyfrowania WiFi WEP (co przyśpieszyło rozwój standardu WPA). Istnieje jednak możliwość obrony przed tym atakiem – należy odrzucić początkową (co najmniej pierwsze 1024 bajty) porcję danych ze strumienia szyfrującego.

Szyfrowanie – podejście drugie

Cześć!

W poprzednim wpisie zajmowaliśmy się szyfrem Cezara, który okazał się być dalekim od ideału zabezpieczeniem poufności informacji. Dzisiaj, tak jak obiecałem, pokażę Wam kolejne i o wiele lepsze podejście do tego problemu😊.

W ramach krótkiego przypomnienia – transformacja stosowana na tekście jawnym musi być odwracalna i bazująca na konkretnym, tajnym kluczu. Takie algorytmy szyfrowania nazywamy symetrycznymi. Szyfr Cezara wykorzystywał podstawienia, zamiast jednej litery wstawiał inną. W jaki sposób możemy dokonać przekształcenia lepiej? Zanim podam Wam rozwiązane tego problemu musimy wrócić do pewnej sprawy podstawowej. Każda informacja jest reprezentowana jako liczba.

Istnieje wiele sposobów jak przedstawić znaki jako liczby i nazywamy je kodowaniem. Najpopularniejszymi są kodowanie ASCII (choć już chyba nieco archaiczne, notabene bazujące na kodach telegraficznych) oraz UTF-8 i UTF-16. Jeśli pójdziemy krok dalej, możemy taką liczbę reprezentować jako ciąg zer i jedynek (bitów, utożsamianych z odpowiednio wysokim i niskim napięciem) z wykorzystaniem systemu dwójkowego. Spójrzcie na przykład poniżej z wykorzystaniem kodowania ASCII:

Znak przed kodowaniemALA(spacja)MA 
Kod ASCII657665327765 
Zapis binarny010000010100110001000001001000000100110101000001 
         
Znak przed kodowaniem(spacja)KOTA
Kod ASCII3275798465
Zapis binarny0010000001001011010011110101010001000001

W tym momencie, jak przysłowiowy książę na białym koniu pojawia się nasza nowa metoda transformacji – jest nią bitowa operacja alternatywy rozłącznej, popularnie zwana operacją xor.

Operację tą możemy najprościej przedstawić za pomocą tabelki 2. Litery p i q oznaczają argumenty działania a samą operację oznaczamy znakiem .

000
011
101
110

Alternatywa rozłączna, jest to operacja bitowa więc możemy ją później wykonywać również na całych bajtach (ciągu ośmiu bitów) xor’ując ze sobą kolejne bity tj. w przykładzie poniżej. Spójrzcie na trzecią kolumnę jak na słupki przy dodawaniu pisemnym.

 Reprezentacja dziesiętnaReprezentacja binarna
790 1 0 0 1 0 1 1
2541 1 1 1 1 1 1 0
1811 0 1 1 0 1 0 1

Operacja xor ma co najmniej kilka ciekawych własności i zastosowań (polecam uwadze https://florian.github.io/xor-trick/), jednak dla nas szczególnie istotne są następujące:

  • ,
  • ,
  • , jest to tzw. łączność.

Zatem jeśli , wtedy . Voila! Odzyskaliśmy tekst jawny zatem jesteśmy technicznie w stanie użyć tej operacji do zaszyfrowania danych. Pojawia się w tym momencie pytanie, czym powinien być klucz. Przeanalizujemy teraz kilka opcji.

Rozwiązanie pierwsze:

Wykonujemy xor każdego bajtu tekstu jawnego z pewną stałą wartością liczbową. Proste? Proste! Niestety w realny sposób nie zwiększa to poziomu bezpieczeństwa. Podobnie jak w szyfrze Cezara, ten sam znak będzie miał zawsze przyporządkowywaną tą samą wartość i podobnie jak poprzednik jest podatny na analizę statystyczną. W poprzednim wpisie zajmowaliśmy się tekstem a dzisiaj pokażę Wam ciut bardziej spektakularny przykład wykorzystując obróbkę zdjęć. Spójrzcie na rysunki 1 i 2.

Rysunek 1 Lenna Sjööblom – pierwsza dama internetu wg. czasopisma Wired

Rysunek 2 Rysunek poddany szyfrowaniu poprzez xor z bajtem 0xfe

Rysunek 1 jest to fotografia, którą poddamy szyfrowaniu (łączy się z nią pewna anegdota ale o niej trochę później). Każdy format graficzny w jakiś sposób próbuje zakodować informację o ilości kolorów czerwonego, zielonego oraz niebieskiego dla każdego piksela obrazu. Format bmp, którego użyłem w przykładzie używa 1 bajtna każdy z nich.

Rysunek 2 jest nieco zniekształcony gdyż jest to kryptogram😊. Powstał on poprzez xor’owanie każdego bajtu wszystkich piksli z bajtem 0xFE (to jest zapis w systemie szesnastkowym liczby 254). Zdjęcie wygląda nieco inaczej niż w oryginale ale wciąż rozpoznajemy na nim młodą kobietę z odsłoniętymi ramionami w kapeluszu z futrem. Wniosek jest jeden – pomimo szyfrowania widzimy tutaj całkiem dużo informacji jednak przewrotnie powiem (jest to taki trochę śmiech przez łzy), że mieliśmy i tak sporo szczęścia. Rysunek 3 przedstawia wynik szyfrowania bajtem o wartości 5. Jak widzicie nie różni się on wizualnie wcale od oryginału.

Rysunek 3 Zdjęcie zaszyfrowane bajtem 0x5

W ramach chwili oddechu chciałbym Wam opowiedzieć obiecaną anegdotę. Jeśli będziecie się interesować kiedyś cyfrowym przetwarzaniem obrazu lub ich kompresją to dość często będzie ono Wam towarzyszyć (trochę jak swego czasu memy o Ryszardzie Petru wyskakującym z lodówki).

Po raz pierwszy wykorzystał ją Alexander Sawchuk, profesor Uniwersytetu Południowej Kalifornii, który razem
z kilkoma kolegami usilnie szukał zdjęć do wykorzystania jako przykład w czasie jednej z konferencji. Jak na mój gust wybrali oni dość mało konwencjonalne jak na świat nauki źródła zdjęć. Szukali ich w starych wydaniach Playboy’a, samo zdjęcie pochodzi z roku 1972 roku. Przedstawia ono Lenę Sjööblom, szwedzką modelkę, która o swojej popularności w świecie komputerowych nerdów dowiedziała się dopiero w 1988 roku i aż do 1997 roku nie korzystała z Internetu. Oryginalne zdjęcie możecie znaleźć na poprzez Google’a szukając artykułu pod tytułem Internet Icon with Lenna Sjoeoeblom 😉

W tym momencie przypomina mi się pewien dowcip z bródką:

Dwóch kolegów informatyków zaprasza trzeciego na imprezę. Nie mogą go przekonać i w końcu mówią: „Będą panie!”. Na to trzeci odpowiada – „Ile?”. Dwaj odpowiadają chórem – „Całe 5 giga”.

A teraz wracając do tematu – co możemy zrobić lepiej?

Rozwiązanie 2:

Zamiast xor’ować tekst jawny stale z tą samą wartością możemy użyć jako klucz ciąg losowych wartości i zapisać go aby umożliwić deszyfrację w przyszłości. Ten wariant został zaproponowany w 1917 roku przez Gilberta Vernama jako tzw. szyfr z kodem jednorazowym (ang. one-time pad). Gwarantuje on nam tzw. doskonałą tajność.

Szyfr jest doskonale tajny tylko i wyłącznie gdy dla każdej wiadomości m i dla każdego tekstu tajnego c, prawdopodobieństwo, że otrzymany kryptogram odpowiada wiadomości m jest takie samo jak prawdopodobieństwo nadania wiadomości m. Oznacza to de facto, że szyfrogram:

  • nie ujawnia żadnych informacji o tekście jawnym,
  • jest bezwarunkowo odporny na atak ze znanym kryptogramem (ang. known-ciphertext attack),
  • istnieje więcej kluczy niż wiadomości.

Atak ze znanym szyfrogramem polega na tym, że kryptoanalityk dysponując pewną liczbą zaszyfrowanych tym samym kluczem wiadomości, próbuje odzyskać możliwie wiele oryginalnych wiadomości lub zgadnąć klucz. Nasze zmagania z szyfrem Cezara mogą stanowić przykład takiego ataku – każdą kolejną wiadomość używamy żeby zwiększyć naszą wiedzę o prawdopodobieństwie znaków dopóki nie odzyskamy klucza.

Spójrzcie na rysunek 4. Powstał on przez zaszyfrowanie zdjęcia 1 one-time pad’em i w przeciwieństwie do rysunków 2 i 3 przypomina on raczej biały szum sygnału analogowego w telewizorach przed epoką telewizji cyfrowej.

Rysunek 4 Zdjęcie zaszyfrowane losowym ciągiem bajtów

W przypadku jednokrotnego użycia klucza prawdopodobieństwo zgadnięcia klucza wynosi , czyli jest praktycznie pomijalnie małe. Rozwiązanie na pewno jest bezpieczne, ale czy jest ono tym czego szukamy? Nie do końca… Szukamy zawsze rozwiązania optymalnego, a takie jest kompromisem pomiędzy poziomem bezpieczeństwa (lub paranoi 😊) i wygody. Aktualny pomysł nie jest zbyt wygodny: wymaga wcześniejszej wymiany bardzo długich kodów (albo ustalenia wspólnego źródła bajtów klucza), które na domiar złego muszą być jednorazowe. Mimo to, rozwiązanie było wykorzystywanie na przykład w słynnej gorącej linii pomiędzy Moskwą a Waszyngtonem… a tymczasem pokażę Wam w jaki sposób ten problem ominąć.

Rozwiązanie 3:

Naszym ostatnim krokiem jest wykorzystanie zamiast liczb losowych tzw. generatora liczb pseudolosowych. Samymi generatorami zajmiemy się bardziej w kolejnym artykule ale w telegraficznym skrócie są to komponenty, które inicjujemy tajnym kluczem i zwraca on zawsze ten sam (oczywiście przy tym samym kluczu), nieskończony ciąg liczb, które wyglądają (i zachowują się) jak losowe. Dzięki temu osoba znająca klucz może później zdekodować wiadomość.

Przykładem takiego algorytmu jest szyfr RC4 (lub Arcfour, gdyż nazwa RC4 jest zastrzeżonym znakiem handlowym). W jaki sposób generuje on kolejne liczby? Algorytm składa się z dwóch części:

  • procedury inicjowania bufora generatora,
  • algorytmu pseudolosowej generacji kolejnych bajtów.

Losowe bajty są tworzone w oparciu o bufor generatora – tablicę 256-elementową wypełnioną kolejnymi liczbami od 0 do 255 włącznie, która jest później mieszana w następujący sposób

Kolejne losowe bajty są uzyskiwane poprzez dalsze mieszanie tablicy i jest ono opisane następującym algorytmem:

Zdjęcie poniżej przedstawia wynik szyfrowania z wykorzystaniem szyfru Arcfour. Jak widzicie nie różni się ono wizualnie od wyników one-time pad’a.

Nazwa strumień jest wykorzystana w pseudokodzie nie bez powodu. Poprzez strumień rozumiemy to, że kolejne losowe bajty mogą być dostarczane na bieżąco, w miarę gdy pojawiają się kolejne bajty tekstu jawnego umożliwiając ciągłe nadawanie ciągłej zaszyfrowanej transmisji. Takie szyfry nazywamy szyframi strumieniowymi.

Tego typu szyfry są użyteczne w szczególności dla urządzeń o małej mocy obliczeniowej lub przy szyfrowanej transmisji dźwięku lub obrazu. Jeśli każdy klucz używamy jednokrotnie to szyfr jest na tyle bezpieczny na ile nieprzewidywalny jest generator.

Już zbliżamy się do końca ale, czy domyślacie się dlaczego w przypadku one-time pad’a oraz Arcfour klucz powinien być jednorazowy? Spójrzcie poniżej, jeśli  oraz  to:

W teorii wciąż nie wiemy jaka jest treść tekstów jawnych, ale czy nie mamy żadnej informacji na ich temat? Zrobimy mały eksperyment, rysunki 5 i 6 zostaną zaszyfrowane szyfrem Arcfour z wykorzystaniem tego samego klucza. Rysunek 7 będzie natomiast przedstawiał wynik xor’owania ze sobą obu szyfrogramów. Myślę, że odpowiedź nasunie się sama.

Rysunek 5 Wariacje na temat logo systemu FreeBSD, przykład do zaszyfrowania

Rysunek 6 Logo systemu FreeBSD, przykład do zaszyfrowania

Rysunek 7 Wynik xor’owania szyfrogramów rysunków 5 i 6

Kończąc, jeśli macie czas i chęci lub po prostu chcecie poeksperymentować z programami, które napisałem do tego wpisu odwiedźcie repozytorium (serdecznie zapraszam) a w następnym artykule zajmiemy się tematem komputerowej losowości, gdyż nie jest to temat taki do końca oczywisty.

Do zobaczenia!

Szybkie wejście w świat kryptografii

Cześć!

W tym artykule chciałbym przedstawić Wam świat, w którym wszyscy żyjemy w nieco innym świetle. Kryptografia, jest to pojęcie o którym wiele osób słyszało, jednak nie dostrzega go na co dzień. Mam nadzieję to zmienić i pokazać Wam jej podstawowe narzędzia.

Zazwyczaj pierwsze skojarzenia odnośnie kryptografii dotyczą świata militariów – często myślimy o wojnie, enigmie i szpiegach przesyłających tajne depesze. Prawda jest jednak zupełnie inna. Obecnie, w dobie Internetu, w świecie cywilnym ma ona o wiele szerszą liczbę zastosowań niż było to we wcześniejszych wiekach. Zanim podam Wam ich przykłady musimy ustalić jakie są jej podstawowe cele.

Często w kręgu osób zainteresowanych bezpieczeństwem w tym miejscu mówi się o modelu zwanym triadą CIA. Wiem o czym myślicie ale nie, nie ma ona nic wspólnego ze słynną amerykańską agencją wywiadowczą a z oczekiwanymi własnościami informacji:

  • Confidentialitypoufność, tylko określone osoby są w stanie zrozumieć informację,
  • Integrity – integralność, wszelkie manipulacje informacją muszą zostać wykryte przez odbiorcę,
  • Availability – dostępność, dostęp do informacji musi być uwierzytelniony.

Triada CIA opisuje cele, które uzyskujemy dzięki kryptografii. Najpowszechniejszym miejscem, w którym stykamy się z kryptografią jest Internet.

Gdzie jest kryptografia w naszym życiu?

Kilka lat temu, tuż po tym, gdy zapłaciłem za wizytę dentystyczną, mój lekarz zadał jedno pytanie – „Przepraszam, czy nie jest Pan przypadkiem informatykiem?”. Zapaliła mi się z tyłu głowy obawa przed syndromem darmowego informatyka (który zawsze i wszędzie udziela bezpłatnej pomocy) ale jednak chęć pomocy zwyciężyła, zwłaszcza, że chwilę wcześniej uratował mnie od wielkiego bólu zęba, więc odpowiedziałem zgodnie z prawdą.

Dzień wcześniej otworzył bowiem załącznik, który miał być fakturą a okazał się wirusem. Lekarz zaobserwował jeden objaw. Próba odwiedzenia poczty Google’a oraz kalendarza kończyła się komunikatem „Twoje połączenie nie jest prywatne”. Historię zwieńczyło pytanie, czy da się ominąć komunikat i przejść do usług Google’a pomimo braku szyfrowania. Niestety odpowiedź powinna brzmieć, tak ale nie należy z niej korzystać. Prawda jest taka, że większość usług, z których korzystamy w Internecie jest szyfrowana. Poniżej możecie porównać w jaki sposób wyglądają informacje wysyłane przez naszą przeglądarkę do strony internetowej z szyfrowaniem i bez.

Zrzut ekranu zawiera przykładowy zapis transmisji poprzez Wi-Fi gdzie komunikacja jest szyfrowana. Możecie zauważyć drobną wzmiankę, że wykorzystywany jest protokół HTTP2, który jest ukryty poprzez TLS’a. Nie szyfrowana komunikacja wygląda natomiast tak:

Sekcja Hypertext Transfer Protocol zawiera niezaszyfrowany ładunek informacji.

Kolejnym przykład kryptografii w naszych domach stanowią routery. Komunikacja pomiędzy urządzeniami w sieciach Wi-Fi nie zawsze przebiega bez zakłóceń. Dzięki niej jesteśmy np. w stanie wykryć przynajmniej część przekłamań w transmisji. Protokół TCP (schemat komunikacji wykorzystywany do łączności z Internetem) wymaga stosowanie dodatkowych kodów aby zapewnić integralność transmisji.

Sumy kontrolne (zakreślone żółtym podkreślaczem pole Checksum) pozwalają wykryć nieprawidłowe pakiety w sieci – może do tego dojść z wielu różnych powodów jak np. zbyt słabego sygnału czy przeszkody fizyczne takie jak ściany.

W jaki sposób zaszyfrować wiadomość?

Teraz, kiedy mamy pewne wyobrażenia dotyczące kryptografii w życiu codziennym chciałbym dać Wam pewien przedsmak tego czym będziemy zajmować się w kolejnych artykułach. Zaczniemy więc od problemu szyfrowania na przykładzie szyfru Cezara – miał być on używany przez niego do poufnej komunikacji ze swoimi przyjaciółmi. Ciekawe, czy również z Brutusem…

Opowiem Wam zaraz pewną bajkę. Musicie wiedzieć, że bajki kryptograficzne mają pewien element wspólny z komediami romantycznymi. W obu gatunkach mamy dwóch głównych bohaterów, w życiu których pojawia się trzecia, która odkąd się pojawiła to miesza
w zaistniałej wcześniej sytuacji. Bohaterowie naszej bajki nazywają się Juliusza Cezar, Marek Krassus oraz Gnejusz Pompejusz. Istnieli jak pewnie wiecie naprawdę, ale historia ta została całkowicie zmyślona.

Dawno, dawno temu, za siedmioma lasami, za siedmioma górami… No dobrze, w starożytnym Rzymie , istniał sojusz trzech wielkich mężów – wymienionego wcześniej Juliusza, Gnejusza i Marka. Trudno mówić w ich przypadku o wielkiej miłości (całe szczęście) a nawet przyjaźni. Najbliżej tej relacji do czegoś w rodzaju małżeństwa z rozsądku. Otóż Marek wybierał się na wojnę z Partami i zastanawiał się w jaki sposób informować Cezara o postępach kampanii tak żeby ani Gnejusz, ani Asteriks i Obeliks z odległej Galii przechwyciwszy wiadomość nie potrafili jej zrozumieć.

Juliusz-kryptograł pomyślał sobie: „Kurcze, przecież musi dać się jakoś tak zmodyfikować tekst żeby nie dało się go zrozumieć a później odwrócić ten proces…”. Myślał nad tym problemem kilka dni aż w końcu wymyślił! Czemu by nie przesuwać liter alfabetu o 3, tj. zamieniać litery A na D, B na E, C na F, itd ….? Przecież ten proces da się odwrócić! Jak pomyślał tak zrobił.

 Tabela przekształceń
Przed przekształceniemABCDEFGHIJKLMNOPQRSTUWVXYZ
Po przekształceniuDEFGHIJKLMNOPQRSTUWVXYZABC

Przykład:
Tekst jawny: ALA MA KOTA
Tekst poufny: DOD PD NRVD

Wiadomość, po przesunięciu liter o 3 podobna sama do siebie za bardzo nie była, natomiast po odwróceniu procesu na uzyskanej wiadomości uzyskał oryginalną. Voila! Problem rozwiązany.

Dzisiaj proces pierwszy nazywamy szyfrowaniem, gdyż służy on transformacji tekstu jawnego (plaintext) w tzw. szyfrogram (ciphertext, tekst poufny) z wykorzystaniem pewnego klucza (w przypadku szyfru Cezara jest nim wielkość przesunięcia). Proces odwrotny tj. transformacja szyfrogramu w tekst oryginalny nazywamy natomiast deszyfracją. W kolejnych artykułach przekonacie się, że klucz szyfrowania i deszyfrowania nie muszą być koniecznie takie same. Matematyk powiedział by, że szyfrowanie i deszyfrowanie są to takie funkcje Encrypt i Decrypt, że:

Wszystko ładnie i pięknie, Cezar z Krassusem czuli się w pełni komfortowo i bezpiecznie, do tego stopnia, że poruszali nawet bardziej intymne tematy. Czy słusznie?

Tutaj pojawia się trzeci bohater, gnuśny (a przynajmniej na potrzeby tej opowieści) Gnejusz, który przechwycił wiadomość. Spojrzał i na pierwszy rzut oka nic nie rozumie… Trochę tak,
jak w piosence  Voulez-vous coucher avec moi zespołu Aquarium:

Trzymam w mym ręku list, ale widzę tylko litery
Nie pamiętam nawet, jak złożyć z nich słowa.

Boris GrebensHchikov – Voulez-Vous coucher avec moi

Mimo wszystko Gnejusz okazał się być szczwanym lisem, pomyślał: „Alfabet łaciński nie jest taki długi, to tylko 26 liter…”, poza tym podsłuchał kiedyś gdy dwaj „przyjaciele” rozmawiali o jakimś przesunięciu… Wziął wszystkich swoich 25 greckich niewolników, którzy uczyli jego dzieci matematyki i kazał każdemu z nich postąpić tak: pierwszy niewolnik zamieniał B na A, drugi C na A itd…. Odkrył, że ten podstawiający z przesunięciem o 3 stworzył sensowny tekst. Sekrety Juliusza Cezara oraz Marka Krassusa nie były już dłużej bezpieczne.

W XIX wieku holenderski kryptograf Auguste Kerckhoffs ukuł kilka zasad, które można streścić w zdaniu bezpieczeństwo systemu kryptograficznego powinno być oparte na tajności klucza a nie procedur. Poza tym przestrzeń kluczy powinna być na tyle duża by nie można było ich efektywnie sprawdzić poprzez przegląd wszystkich możliwości jak to zrobił nasz Gnejusz.

Pojawia się w tym momencie kolejne pytanie, czy gdybyśmy zamiast przesunięć stworzyli losową tablicę podmian znaków byłoby to trudniejsze do złamania? W końcu takich tablic jest 26! (26 * 25 * … * 1) czyli 403 291 461 126 605 635 584 000 000, czyli chyba więcej niż dzisiaj jest mieszkańców globu. Na ówczesne czasy chyba tak, na dzisiejsze już nieco mniej.

Dla każdego języka charakterystyczną cechą jest prawdopodobieństwo z jakim występują różne litery – nazywamy to po angielsku footprint’em. Jeśli przeanalizujemy nawet takie losowe przyporządkowanie to dla stosownych znaków, proporcje zostaną zachowane i łatwo możemy odzyskać tabelę przekształceń.

Sen nocy letniejHamletTytus AndronikusJuliusz Cezar
e – 0.125070
t – 0.089611
o – 0.082563
a – 0.073165
i – 0.067040
n – 0.064873
r – 0.063607
s – 0.062375
h – 0.060174
l – 0.042804
d – 0.037934
u – 0.034204
m – 0.030064
y – 0.025354
w – 0.023495
c – 0.022468
f – 0.019651
g – 0.018613
p – 0.018237
b – 0.015545
v – 0.009261
k – 0.008850
j – 0.001688
q – 0.001437
x – 0.001380
z – 0.000160
e – 0.119177
t – 0.094475
o – 0.090922
a – 0.072433
i – 0.070513
n – 0.066361
s – 0.061381
r – 0.061328
h – 0.058782
l – 0.041492
d – 0.035600
u – 0.031561
m – 0.028901
c – 0.025002
f – 0.024669
y – 0.021922
w – 0.020429
g – 0.019023
p – 0.018363
b – 0.013724
v – 0.009891
k – 0.008652
x – 0.001613
q – 0.001586
j – 0.001520
z – 0.000387
e – 0.116557
t – 0.091163
o – 0.079234
a – 0.076153
s – 0.070158
r – 0.066631
i – 0.066056
n – 0.063976
h – 0.062430
l – 0.042079
d – 0.040791
u – 0.039810
m – 0.031577
y – 0.023036
w – 0.022481
c – 0.022115
f – 0.018478
b – 0.016279
p – 0.015238
g – 0.015040
v – 0.010047
k – 0.006777
x – 0.001496
j – 0.001120
q – 0.000941
z – 0.000337
e – 0.111776
t – 0.090989
o – 0.079855
a – 0.078732
s – 0.074840
i – 0.069186
r – 0.064229
n – 0.062292
h – 0.054276
u – 0.043762
l – 0.039957
d – 0.037449
c – 0.029907
m – 0.026944
y – 0.023323
w – 0.021900
f – 0.019925
b – 0.018240
g – 0.016004
p – 0.015713
v – 0.009188
k – 0.007784
j – 0.001249
x – 0.001046
z – 0.001026
q – 0.000397
Tabela 1 Liczby wystąpień znaków w różnych tekstach w języku angielskim
OtelloKról LearMakbetRomeo i Julia
e – 0.118021
o – 0.093300
t – 0.089555
a – 0.075691
i – 0.072920
s – 0.064400
n – 0.062901
h – 0.059833
r – 0.056231
l – 0.044050
d – 0.040107
u – 0.031610
m – 0.029905
c – 0.023494
y – 0.023396
w – 0.021660
f – 0.020160
g – 0.020030
p – 0.015265
b – 0.014678
v – 0.010811
k – 0.008557
j – 0.001355
x – 0.001226
q – 0.000624
z – 0.000206
e – 0.123370
t – 0.091010
o – 0.082386
a – 0.074466
r – 0.066092
n – 0.064729
i – 0.061943
s – 0.061511
h – 0.059232
l – 0.045270
d – 0.040576
u – 0.033572
m – 0.028211
c – 0.024047
y – 0.023805
w – 0.023063
g – 0.021867
f – 0.021526
p – 0.015605
b – 0.014197
k – 0.010820
v – 0.009252
j – 0.001355
x – 0.001287
q – 0.000606
z – 0.000197
e – 0.136079
t – 0.091050
o – 0.078711
a – 0.075283
n – 0.063903
s – 0.063343
h – 0.062361
i – 0.060133
r – 0.058979
l – 0.042537
d – 0.039887
u – 0.035202
m – 0.029912
c – 0.026279
w – 0.023548
y – 0.022760
f – 0.020612
g – 0.018212
b – 0.017812
p – 0.014785
k – 0.009038
v – 0.004879
x – 0.002342
q – 0.001497
j – 0.000514
z – 0.000343
e – 0.119085
t – 0.092119
o – 0.082889
a – 0.076255
i – 0.065469
r – 0.064054
n – 0.062439
h – 0.061715
s – 0.061532
l – 0.044045
d – 0.037029
u – 0.034058
m – 0.031761
y – 0.024470
w – 0.023845
c – 0.022922
f – 0.020649
g – 0.018860
b – 0.016862
p – 0.016563
v – 0.009755
k – 0.008240
j – 0.003129
x – 0.001332
q – 0.000633
z – 0.000291
Tabela 2: Liczby wystąpień znaków w różnych tekstach w języku angielskim

Jako próbkę danych wykorzystałem oryginalne teksty różnych sztuk Szekspira ze strony projektu Guttenberg. Możecie zauważyć, że statystyki wyglądają dość podobnie. Im dłuższy tekst próbujecie zdeszyfrować, tym dokładniej będzie działać odtworzona tablica. Na stronie repozytorium https://gitlab.com/sisiphus-on-code/cryptography, w katalogu caesar-cipher znajdziecie źródła do programów wykorzystanych do statystyk, testowe pliki oraz eksperyment z deszyfrowaniem bazując na statystykach występowania znaków.

Możecie zauważyć w tym miejscu, dwa typy problemów:

  • W jaki sposób zabezpieczyć poufność komunikacji?
  • W jaki sposób możemy przeanalizować przechwycone informacje aby odzyskać lub podmienić chociaż część danych nie znając klucza?

Na pierwsze pytanie odpowiada kryptografia a na drugie kryptoanaliza a oba zagadnienia  są przedmiotem nauki o nazwie kryptologia.

Jakie narzędzia zapewnia nam kryptografia

Teraz kiedy zobaczyliście przykład w jaki sposób możemy (chociaż może nieco nieudolnie,
w kolejnych artykułach przedstawię Wam lepsze sposoby) zapewniać poufność korespondencji, pewnie jesteście ciekawi co jeszcze będzie elementem naszego narzędziowego przybornika.

Poniżej znajdziecie krótkie zastawienie narzędzi, które zobaczycie w kolejnych wpisach:

NarzędzieOpis i zastosowanie
Algorytmy szyfrowaniaAlgorytmy, które umożliwiają odwracalne zmodyfikowanie zawartości wiadomości aby uniemożliwić jej odczytanie.
Podpisy cyfroweProtokoły wykorzystujące algorytmy szyfrowania asymetrycznego (tj. klucz szyfrowania jest inny od klucza deszyfrowania) aby umożliwić potwierdzenie np. kto jest nadawcą wiadomości.
Funkcje hashujące (skrótu)Przypisanie większemu pakietowi informacji charakterystycznej wartości liczbowej o stałym rozmiarze w sposób nieodwracalny.
Kody korekcyjneDodatkowa informacja dodawana do wiadomości dzięki której gdy pojawiły się przekłamania transmisji wiadomość można próbować odzyskać.
(H)MAC – kody uwierzytelnienia wiadomościAlgorytmy zapewniające integralność wiadomości (nie muszą zapewniać pewności, kto nadał daną wiadomość).
Podstawowe narzędzia kryptografii

Wszystkie te narzędzia są łączone w większą całość – bardziej skomplikowane protokoły
i kryptosystemy. U ich podstaw natomiast leżą pewne zagadnienia związane z matematyką tj. liczby losowe, problemy teorio-liczbowe (np. problem dyskretnego logarytmu albo rozkładu liczb na czynniki pierwsze), kombinatoryka i rachunek prawdopodobieństwa.

W kolejnych wpisach postaram się przybliżyć Wam szczegóły w jaki sposób działają powyższe narzędzia a tymczasem najwyższy czas kończyć na dziś!

Do zobaczenia 😊

Improve him believe opinion offered

It acceptance thoroughly my advantages everything as. Are projecting inquietude affronting preference saw who. Marry of am do avoid ample as. Old disposal followed she ignorant desirous two has. Called played entire roused though for one too. He into walk roof made tall cold he. Feelings way likewise addition wandered contempt bed indulged.

Still court no small think death so an wrote.

Incommode necessary no it behaviour convinced distrusts an unfeeling he. Could death since do we hoped is in. Exquisite no my attention extensive. The determine conveying moonlight age. Avoid for see marry sorry child. Sitting so totally forbade hundred to.

Can curiosity may end shameless explained

Way nor furnished sir procuring therefore but.

Warmth far manner myself active are cannot called. Set her half end girl rich met. Me allowance departure an curiosity ye. In no talking address excited it conduct. Husbands debating replying overcame blessing he it me to domestic.

  • As absolute is by amounted repeated entirely ye returned.
  • These ready timed enjoy might sir yet one since.
  • Years drift never if could forty being no.

Fat son how smiling natural

To shewing another demands sentiments. Marianne property cheerful informed at striking at. Clothes parlors however by cottage on. In views it or meant drift to. Be concern parlors settled or do shyness address. 

He always do do former he highly.

Continual so distrusts pronounce by unwilling listening

Expenses as material breeding insisted building to in. Continual so distrusts pronounce by unwilling listening. Thing do taste on we manor. Him had wound use found hoped. Of distrusts immediate enjoyment curiosity do. Marianne numerous saw thoughts the humoured.

Perfectly on furniture

Feet evil to hold long he open knew an no.

Apartments occasional boisterous as solicitude to introduced. Or fifteen covered we enjoyed demesne is in prepare. In stimulated my everything it literature. Greatly explain attempt perhaps in feeling he. House men taste bed not drawn joy. Through enquire however do equally herself at. Greatly way old may you present improve. Wishing the feeling village him musical.

Smile spoke total few great had never their too. Amongst moments do in arrived at my replied. Fat weddings servants but man believed prospect. Companions understood is as especially pianoforte connection introduced. Nay newspaper can sportsman are admitting gentleman belonging his.

Appearance guide

Yet bed any for travelling assistance indulgence unpleasing. Not thoughts all exercise blessing. Indulgence way everything joy alteration boisterous the attachment. Party we years to order allow asked of. We so opinion friends me message as delight. Whole front do of plate heard oh ought. His defective nor convinced residence own. Connection has put impossible own apartments boisterous. At jointure ladyship an insisted so humanity he. Friendly bachelor entrance to on by.

That last is no more than a foot high, and about seven paces across, a mere flat top of a grey rock which smokes like a hot cinder after a shower, and where no man would care to venture a naked sole before sunset. On the Little Isabel an old ragged palm, with a thick bulging trunk rough with spines, a very witch amongst palm trees, rustles a dismal bunch of dead leaves above the coarse sand.