Laboratorul 03 - PRGs

Prezentarea PowerPoint pentru acest laborator poate fi găsită aici.

Puteți lucra acest laborator folosind și platforma Google Colab, accesând acest link.

utils.py
import base64
 
# CONVERSION FUNCTIONS
 
def _chunks(string, chunk_size):
    for i in range(0, len(string), chunk_size):
        yield string[i:i+chunk_size]
 
def _hex(x):
    return format(x, '02x')
 
def hex_2_bin(data):
    return ''.join(f'{int(x, 16):08b}' for x in _chunks(data, 2))
 
def str_2_bin(data):
    return ''.join(f'{ord(c):08b}' for c in data)
 
def bin_2_hex(data):
    return ''.join(f'{int(b, 2):02x}' for b in _chunks(data, 8))
 
def str_2_hex(data):
    return ''.join(f'{ord(c):02x}' for c in data)
 
def bin_2_str(data):
    return ''.join(chr(int(b, 2)) for b in _chunks(data, 8))
 
def hex_2_str(data):
    return ''.join(chr(int(x, 16)) for x in _chunks(data, 2))
 
# XOR FUNCTIONS
 
def strxor(a, b):  # xor two strings, trims the longer input
    return ''.join(chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b))
 
def bitxor(a, b):  # xor two bit-strings, trims the longer input
    return ''.join(str(int(x) ^ int(y)) for (x, y) in zip(a, b))
 
def hexxor(a, b):  # xor two hex-strings, trims the longer input
    return ''.join(_hex(int(x, 16) ^ int(y, 16)) for (x, y) in zip(_chunks(a, 2), _chunks(b, 2)))
 
# BASE64 FUNCTIONS
 
def b64decode(data):
    return bytes_to_string(base64.b64decode(string_to_bytes(data)))
 
def b64encode(data):
    return bytes_to_string(base64.b64encode(string_to_bytes(data)))
 
# PYTHON3 'BYTES' FUNCTIONS
 
def bytes_to_string(bytes_data):
    return bytes_data.decode()  # default utf-8
 
def string_to_bytes(string_data):
    return string_data.encode()  # default utf-8

Exercițiul 1 (3p)

În acest exercițiu vom încerca să spargem un LCG (Linear Congruential Generator, care se poate folosi pentru a genera numere “slab” aleatoare. Am folosit un astfel de generator pentru a obține o secvență de octeți cu care a fost criptat un mesaj (plaintext). Textul cifrat (ciphertext) în sistemul hexazecimal este următorul:

a432109f58ff6a0f2e6cb280526708baece6680acc1f5fcdb9523129434ae9f6ae9edc2f224b73a8

După cum știți, LCG folosește următoarea formula pentru a genera octeți:

s_next = (a * s_prev + b) mod p

unde s_next și s_prev sunt octeți (0-255) generați, p = 257, iar a și b au valori între 0 și 256.

Mai știți și că primele 16 litere din plaintext sunt “Let all creation”. Criptarea a fost obținută realizând XOR între plaintext și secvența de octeți consecutivi generați de LCG.

Spargeți LCG astfel încât să puteți prezice șirul de numere generate “aleatoriu”. Care este mesajul inițial?

Puteți folosi următorul schelet de cod:

'ex1_weak_rng.py'
from utils import *
 
 
class WeakRNG:
    """ Simple class for weak RNG """
 
    def __init__(self):
        self.rstate = 0
        self.maxn = 255
        self.a = 0  # Set this to correct value
        self.b = 0  # Set this to correct value
        self.p = 257
 
    def init_state(self, rstate):
        """ Initialise rstate """
        self.rstate = rstate  # Set this to some value
        self.update_state()
 
    def update_state(self):
        """ Update state """
        self.rstate = (self.a * self.rstate + self.b) % self.p
 
    def get_prg_byte(self):
        """ Return a new PRG byte and update PRG state """
        s = self.rstate & 0xFF
        self.update_state()
        return s
 
 
def main():
    # Initialise weak rng
    wr = WeakRNG()
    wr.init_state(0)
 
    # Print ciphertext
    CH = 'a432109f58ff6a0f2e6cb280526708baece6680acc1f5fcdb9523129434ae9f6ae9edc2f224b73a8'
    print("Full ciphertext in hexa:", CH)
 
    # Print known plaintext
    pknown = 'Let all creation'
    nb = len(pknown)
    print("Known plaintext:", pknown)
    pkh = str_2_hex(pknown)
    print("Plaintext in hexa:", pkh)
 
    # Obtain first nb bytes of RNG
    gh = hexxor(pkh, CH[0:nb*2])
    print(gh)
    gbytes = []
    for i in range(nb):
        gbytes.append(ord(hex_2_str(gh[2*i:2*i+2])))
    print("Bytes of RNG: ")
    print(gbytes)
 
    # Break the LCG here:
    # TODO 1: Find a and b, and set them in the RNG
 
    # TODO 2: Predict/generate rest of RNG bytes
 
    # TODO 3: Decrypt plaintext
 
    # TODO 4: Print the full plaintext
    p = ''
    print("Full plaintext is:", p)
 
 
if __name__ == "__main__":
    main()

Exercițiul 2 (2p)

Avantaje. Scopul acestui exercițiu este de a clarifica conceptul de avantaje prezentat la curs. Vom considera două experimente $\mathsf{EXP}(0)$ și $\mathsf{EXP}(1)$, astfel:

  • În $\mathsf{EXP}(0)$ verificatorul dă cu banul (probabilitate de $1/2$ pentru CAP și $1/2$ pentru PAJURĂ) și transmite rezultatul adversarului $\mathsf{A}$.
  • În $\mathsf{EXP}(1)$ verificatorul întotdeauna va transmite PAJURĂ către adversar.

Considerăm $r = 0$ pentru CAP și $r = 1$ pentru PAJURĂ. Jocul va arăta astfel:

Scopul adversarului este să diferențieze între cele două experimente: la finalul fiecarui experiment, adversarul returnează un bit $\mathsf{b'}$, $0$ sau $1$, în speranța de a ghici experimentul în care se află (adică, $\mathsf{EXP}(b) = \mathsf{EXP}(b')$). Fie $W_{b}$ evenimentul când în experimentul $\mathsf{EXP}(b)$ adversarul returnează $b' = 1$. Adversarul vrea să își maximizeze avantajul de a distinge experimentele. Mai exact, el vrea să maximizeze valoarea $\mathsf{Adv} = \left| \mathsf{Pr}\left[W_{0}\right] − \mathsf{Pr}\left[W_{1}\right] \right| \in \left[0, 1\right]$.

Avantajul $\mathsf{Adv}$ redă capacitatea adversarului de a diferenția experimentele. Dacă avantajul este $0$, atunci advesarul se comportă identic în ambele cazuri și nu poate să le distingă. Dacă avantajul este $1$, atunci adversarul știe concret care este valoarea lui $b$. Dacă avantajul este neglijabil pentru orice adversar eficient atunci putem spune că nu se poate distinge între experimente.

a. Calculați avantajul pentru fiecare dintre următorii adversari:

  • A1: Returnează mereu $b'=1$.
  • A2: Ignoră valoarea primită de la verificator și returnează aleatoriu $0$ sau $1$, cu aceeași probabilitate.
  • A3: Returnează $1$ dacă a primit CAP ($r=0$) de la verificator, altfel returnează $0$.
  • A4: Returnează $0$ dacă a primit CAP de la verificator, altfel returnează $1$.
  • A5: Dacă a primit CAP, returnează $1$. Dacă a primit PAJURĂ, returnează aleatoriu $0$ sau $1$ cu aceeași probabilitate.
  • A6: Dacă a primit CAP, returnează aleatoriu $0$ sau $1$, cu aceeași probabilitate. Dacă a primit PAJURĂ, returnează $0$.

b. Care este avantajul maxim pe care îl poate obține un adversar? De ce?

Încercați să obtineți mai întai o formulă generală ce nu depinde de adversar.

Exercițiul 3 (2p)

O aplicație a avantajului în schemele criptografice este de a arăta că un $PRG$ este sigur sau nu. Amintiți-vă de la curs că $G : K \rightarrow \{0, 1\}^n$ este un $PRG$ sigur dacă pentru orice test statistic $\mathsf{A}$ eficient (care rulează în timp polinomial), avantajul $\mathsf{Adv_{PRG}[A, G]}$ este neglijabil. Un test statistic este de fapt un algoritm care încearcă să determine dacă intrarea pare aleatoare sau nu. Acest algoritm poate fi definit astfel:

\begin{equation*} A(x) = \begin{cases} 1 ,& \text{dacă x e aleatoriu}\\ 0 ,& \text{altfel} \end{cases} \end{equation*}

Putem asocia pentru fiecare test statistic $A$ câte un adversar care încearcă să spargă $\mathsf{PRG}$-ul (adică poate distinge între rezultatul $\mathsf{PRG}$-ului și un generator de numere complet aleatoare). În acest caz, vom defini experimentele după cum urmează:

  • În $\mathsf{EXP}(0)$ verificatorul trimite către adversarul $\mathsf{A}$ valoarea $G(k)$ generată de $\mathsf{PRG}$;
  • În $\mathsf{EXP}(1)$ verificatorul trimite către adversarul $\mathsf{A}$ o valoare $r$ generată de un generator de numere complet aleatoare.

Ca mai devreme, $W_{b}$ este evenimentul în care adversarul spune că numărul este aleatoriu și a avut loc experimentul $b$. Dacă adversarul poate să distingă între $\mathsf{PRG}$ și un generator de numere complet aleatoare, atunci $\mathsf{PRG}$-ul nu e sigur. Așadar, avantajul devine:

$$\mathsf{Adv_{PRG}[A, G]} = \left|\underset{k \leftarrow K}{Pr}[A(G(k)) = 1] - \underset{r \leftarrow \{0, 1\}^n}{Pr}[A(r) = 1] \right|$$

Deși problema determinării dacă există $\mathsf{PRG}$-uri care să poată fi demonstrate ca fiind sigure este echivalentă cu a rezolva bine-cunoscuta problemă $\mathsf{P}$ vs $\mathsf{NP}$, în practică există $\mathsf{PRG}$-uri considerate sigure prin folosirea unor euristici.

În acest exercițiu, se dă un $\mathsf{PRG}$ $G : \{0, 1\}^s \rightarrow \{0, 1\}^n$ sigur. Cu ajutorul acestuia, spuneți dacă următoarele $\mathsf{PRG}$-uri sunt sigure.

  • $G_{1}(k) = G(k) \oplus 1^n$
  • $G_{2}(k) = G(k) \| 0$
  • $G_{3}(k) = G(0)$
  • $G_{4}(k=(k_1, k_2)) = G(k_1) \| G(k_2)$
  • $G_{5}(k)=G(k) \| G(k)$

Pentru două șiruri de biți $y$ și $z$ folosim $y \| z$ pentru a indica concatenarea între $y$ și $z$.

Pentru unele dintre ele nu este nevoie să calculați avantajul.

Exercițiul 4 (3p)

Vom folosi experimentul definit mai devreme pentru a construi un generator pseudoaleatoriu ($\mathsf{PRG}$):

  1. Se alege lungimea $n$ a șirului ce va fi generat
  2. Se obține un șir de biți $R$ de lungime $n$ (de exemplu, puteți folosi chiar și LCG-ul din Exercițiul 1)
  3. Pentru fiecare bit $r$ din șirul $R$ generat anterior, $\mathsf{PRG}$-ul va întoarce un bit $b$ astfel:
    • dacă bitul $r$ este $0$, atunci întoarce un bit aleatoriu $b \in \{0, 1\}$
    • dacă bitul $r$ este $1$, atunci întoarce $1$

a. Implementați frequency (monobit) test așa cum este descris de către NIST (vezi secțiunea 2.1). Verificați dacă secvența generată de $\mathsf{PRG}$-ul de mai sus este aleatoare sau nu (să presupunem pentru $n=100$).

b. Executați testul pe un șir aleatoriu de biți (de exemplu, șirul R folosit în $\mathsf{PRG}$) și comparați rezultatele.

Dacă rezultatele celor două experimente diferă în mod regulat, acest test vă oferă un atacator care sparge $\mathsf{PRG}$-ul descris mai sus.

Pentru a genera un șir aleatoriu de biți, puteți folosi o funcție de forma:

import random
 
def get_random_string(n):
    """ Generate random bit string """
    return bin(random.getrandbits(n)).lstrip('0b').zfill(n)

În Python puteți folosi funcții precum sqrt, fabs sau erfc din modulul math.

import math

Exercițiul 5 - Bonus (2p)

În acest exercițiu, veți încerca să verificați dacă într-adevar Salsa20 stream cipher este mai rapid decât RC4. Descărcați această arhivă. Rulați programul folosind același fișier de intrare atât pentru Salsa20, cât și pentru RC4. Comparați timpul de execuție în ambele cazuri.

Pentru sistemele UNIX (și WSL), folosiți scriptul prepare.sh pentru a complila sursele și a genera fișierele necesare. Dacă nu folosiți WSL pe Windows, scriptul prepare.ps1 generează fișierele necesare, dar nu și compilează sursele.

Folosiți opțiunea ”-h” pentru a afișa lista tuturor argumentelor folosite de program.

Uitați-vă în fișierul salsa20.h. Unde se execută rundele de criptare?

Încercați să implementați aceeași funcționalitate folosind OpenSSL. OpenSSL suportă RC4, dar nu și Salsa20. Folosiți ChaCha20 in loc de Salsa20, aceasta fiind o variantă îmbunătățită a algoritmului.

ic/labs/03.txt · Last modified: 2022/10/18 09:10 by mihai.bogatu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0