PHP krypto 2 - Symmetrisk kryptering

Tags:    php sikkerhed kryptering
Skrevet af Robert Larsen @ 17.04.2012

Introduktion


Så kom vi til det spændende...medmindre man synes at hashing er spændende.

Kryptografi handler om hemmeligholdelse af informationer, og det kan man med et hash, som vi så i sidste artikel. Problemet er bare, at man ikke kan få sine data tilbage. Heldigvis har man i årtusinder kendt til metoder til at omforme data på en måde, så man kan gå tilbage til originalen, og her bruger man typisk en algoritme og en nøgle. Hvis algoritmen er sikker, så gør det ikke noget, at hele verden ved, hvordan den virker, sålænge man kan holde nøglen hemmelig.

Der er to typer af kryptografiske algoritmer, som gør det muligt, at scramble sine data, så andre ikke kan læse dem, men som stadig gør det muligt at få sine originale data tilbage, og det er de symmetriske og asymmetriske krypterings algoritmer, også kaldet henholdsvis 'secret key' og 'public key' algoritmer. Denne artikel vil handle om den første slags.

PHP kommer med to libraries indeholdende kryptografiske funktioner: mcrypt og OpenSSL, og begge indeholder mange funktioner.

Jeg vil ikke gennemgå alle funktioner i begge libraries, men jeg vil gennemgå den nødvendige teori, så I selv burde kunne bruge de øvrige funktioner eller bruge alternative libraries, for i praksis virker de ret éns.

Definition af symmetrisk kryptografi


Ordet 'symmetri' i forbindelse med kryptografi betyder, at den samme nøgle bruges både til kryptering (at gøre sine data ulæselige) og til dekryptering (at gøre de ulæselige data læselige igen). Nøglen i denne forbindelse er et antal bits, og ikke et password, som mange tror, men det kommer vi tilbage til.

Nøglestørrelsen har meget at gøre med, hvor svært det er, at dechifrere dataene. I dag er typiske nøglestørrelser på mellem 128 og 256 bits. Det lyder ikke af mange, men det er det. Faktisk giver 256 bits rundt regnet 100.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000 (der er 77 nuller) forskellige nøgler, så det vil tage et par fantasiliarder år at prøve dem alle.

Nu er der mange krypterings algoritmer derude, og de fleste af dem, som vi bruger i dag, er åbne, så alle kan læse, hvordan de fungerer. Det er en stor fordel, for så kan alle de kloge hoveder derude også finde fejl i dem, hvis der er nogen. Af og til læser man om fejl, som gør, at den effektive nøglestørrelse bliver en faktor 100 mindre, men uanset om der er 77 eller kun 75 nuller, så er det stadig et meget stort tal, så i praksis skal der utrolig meget til, før en algoritme dømmes helt ude.

Der findes to grupper af symmetriske krypteringsalgoritmer (også kaldet ciphers), og det er stream ciphers og block ciphers.

Stream ciphers


Stream ciphers krypterer data én bit eller én byte ad gangen. Stream ciphers genererer en strøm af pseudo tilfældige bits eller bytes, som så via en XOR operation modificerer inputdataene. PHP's "rand()" funktion virker på samme måde og kan faktisk bruges til at kryptere med...meget svag kryptering dog.

Man "seeder" sin stream cipher med krypteringsnøglen, og det giver cipheren sin start tilstand. Hver gang man seeder med samme nøgle, bliver samme serie af pseudo tilfældige tal genereret og da XOR operationen virker på følgende måde:

A XOR B = C
C XOR B = A

så kan man altså kryptere og dekryptere sin byte strøm. Det prøver vi.

Fold kodeboks ind/udPHP kode 


Og igen er brugen ret simpel:

Fold kodeboks ind/udKode 


mcrypt har genereliseret mange krypterings algoritmer så det er ganske få funktioner, man skal kende, for at bruge dem. Et alternativ er OpenSSL funktionerne, men de gør tilnærmelsesvis det samme, så har man teorien bag kryptering med i baggagen, så er det ret ligetil at skifte.

Første parameter til 'mcrypt_module_open' er en identifier for den algoritme, man vil kryptere med. Vi bruger RC4, som pga. trademark problemer blev omdøbt til Arcfour men i daglig tale stadig kaldes RC4. Tredje parameter angiver krypto modus, som kun giver mening med block ciphers. De to andre argumenter angiver directories, hvor algoritmens eller modi implementering kan findes. Tomme strenge kan angives for algoritmer, som følger med mcrypt.

Nu sagde jeg, at man bruger en nøgle og ikke et password til kryptering, men vi tager alligevel et password. Det er jo lidt selvmodsigende.

En krypteringsnøgle er typisk af en fast størrelse, f.eks. 256 bits. Alle bitmønstre er lige brugbare. Et password derimod kan have alle længder og ikke alle bitmønstre er gode passwords, for nogle bitmønstre kan ikke representeres tekstuelt. For at omdanne et password (eller en passphrase) til en krypteringsnøgle af fast længde bruger vi en hashing funktion, som vi lærte om i sidste artikel. SHA256 er en fornuftig algoritme, som genererer 256 bit hashes og det er en meget stærk nøgle. Nøglens størrelse skal ofte passe til krypterings algoritmen. Nogen algoritmer kræver 128 bits, andre 256 og andre igen kan tage alle længder.

Efter at have fået fat i en krypto identifier og en nøgle bliver de to parret sammen med 'mcrypt_generic_init'. Første parameter er den identifier vi fik fra 'mcrypt_module_open'. Anden parameter er vores nøgle, og den sidste er en såkaldt initialiserings vektor, men igen giver denne kun mening i block ciphers og endda kun i visse tilfælde.

Nu er vi klar til at kryptere eller dekryptere. Stream ciphers er ret simple og der er ingen forskel på at kryptere og at dekryptere. Fodrer man 'mcrypt_generic' med plaintext, så krypterer man. Giver man krypteret data så dekrypterer man. Såre simpelt.

Til sidst befrier vi de ressourcer, som er allokeret til vores identifier. Det gør vi med 'mcrypt_generic_deinit' og 'mcrypt_module_close', og så er vi færdige.

Dette er ikke alt, hvad der er at sige om stream ciphers, men lad os tage et kig på block ciphers først og så sammenligne dem bagefter.

Block ciphers


Block ciphers krypterer data i blokke af en bestemt størrelse. Det giver en række problemer, og derfor har vi lidt ekstra parametre på funktionerne, som vi skal forholde os til. Vi modificerer vores stream cipher script til at bruge en block cipher.

Fold kodeboks ind/udPHP kode 


Igen er brugen nem, men der er forskel på, om man krypterer eller dekrypterer, så vi har et ekstra argument på scriptet:
Fold kodeboks ind/udKode 


Nemt nok, men kan I se noget mærkeligt ?

Den krypterede fil fylder 64 bytes mens den ukrypterede fylder 57 bytes. Hvordan kan det være ?

Det er fordi en blok cipher kun kan kryptere data i bestemte størrelser. Hverken mere eller mindre. Har man mere, kan man selvfølgelig bare kryptere flere gange, men har man mindre, så må man fylde ekstra data på, og det kaldes padding.

Jeg bruger en meget simpel padding, hvor jeg bare sætter ekstra nul bytes på. Det fungerer fint, fordi det er ren tekst, som jeg krypterer, og derfor bør nul bytes ikke forekomme, og derfor kan jeg let fjerne dem igen, når jeg dekrypterer. Skal jeg kryptere binære data, så er det ikke en god padding, men det kommer vi tilbage til.

Et andet problem, som ikke er helt åbenlyst illustreres bedst med et eksempel på noget krypteret data:

Fold kodeboks ind/udKode 


Prøv at kigge på ovenstående data inden du læser videre. Kan du se problemet ?

Første blok på 32 bytes bliver gentaget, derefter kommer der en ny blok. Hvordan kan det være ?

Det er fordi, hvis vi krypterer to éns blokke af data med den samme algoritme og den samme nøgle, så får vi samme cipher text. Ligesom da vi hashede to éns passwords i sidste artikel. Er det et problem ?

Tja, det giver krypto bryderne noget at arbejde med. De kan se mønstre og dermed har vi givet dem noget at arbejde med. Vi kan f.eks. se, at der her er krypteret med en blokstørrelse, som sandsynligvis er på max 32 bytes (måske er blokken kun på 16 bytes...vi ved det ikke), og det er ikke nødvendigt, som vi skal se om lidt. De fleste filformater indeholder gentagne bitmønstre, som altså kan give informationer til kodebryderne.

Problemet er, at vi krypterer i ECB (Electronic Code Book) modus.

ECB
I Electronic Code Book modus bliver hver blok krypteret isoleret som vist i de følgende diagrammer fra Wikipadia:



Et (i krypto sammenhænge) kendt eksempel er følgende billede, hvor hver pixel er blevet krypteret i ECB modus (venligst udlånt af Wikipedia):


Nu er problemet nok lidt tydeligere, men hvad kan gøre ved det ?

CBC
CBC står for Cipher Block Chaining og er en måde at variere dataene på en måde, så man kan få originalen tilbage. Diagrammet for kryptering ser nu således ud:


Kort fortalt "blander" man sin plaintext blok med den forrige krypterede blok (med XOR operationen) inden man krypterer. Dekryptering er så den omvendte operation:


Altså først dekrypterer man, og så XOR'er man med den forrige krypterede blok og får dermed den originale plaintext. Billedet af Tux krypteret med ECB giver følgende:


Meget bedre. Men hvad bruger man til den første blok ?

Det er her initialiserings vektoren kommer ind i billedet. Initialiserings vektoren er bare en "nulte" krypteret blok som vi blander ind i vores første plaintext blok, før den krypteres. Det er altså ikke rigtige data, og man behøver ikke, at holde den hemmelig.

Man kan, som jeg har gjort, vælge, at initialiserings vektoren bare skal være en blok fyldt med nul bytes, men hvis man krypterer mange filer, hvor mange har éns begyndelse (det har mange filformater), så vil det kunne ses i den krypterede form, så en tilfældig initialiserings vektor vil ofte være at foretrække. Men den er ikke hemmelig så den kan sendes med de krypterede data til modtageren.

Vi prøver at modificere vores script fra før til at bruge CBC modus:

Fold kodeboks ind/udPHP kode 


Og vi krypterer filen, som gav os et mønster tidligere:

Fold kodeboks ind/udKode 


Som I kan se, så er første blok stadig den samme, og det er fordi, jeg bruger en initialiserings vektor kun bestående af nul bytes. Til gengæld er der ikke længere gentagelser, så kodebryderne har fået lidt mindre at arbejde med.

PKCS7 padding


Nu sagde jeg tidligere, at nul padding ikke egnede sig i alle tilfælde, men hvad gør man så ?

Vi er nødt til at levere data i bestemte mængder til block cipher algoritmen, og når vi dekrypterer, så skal vi kunne regenerere de oprindelige data og altså fjerne de paddede bytes, og én af de mest brugte metoder hedder PKCS7 padding.

I PKCS7 padding vil værdien af hver tilføjet byte være lig med antallet af bytes, som vi tilføjede. Dvs. at hvis vi skal tilføje én byte, så tilføjer vi en byte med værdien 1. Tilføjer vi to bytes så tilføjer vi to bytes hver med værdien 2 og så videre. På den måde ved vi, at når vi har dekrypteret den sidste cipher blok, så skal vi læse værdien på den sidste byte. Dens værdi er lig med antallet af bytes, som skal fjernes.

Skide smart, men hvad hvis vi fylder den sidste blok ud fuldstændig ? Så har vi et problem, for så kan sidste byte ikke indeholde antallet af bytes, som skal fjernes (altså nul). Så bliver vi nødt til at tilføje en hel ekstra blok og fuldstændig udfylde den med bytes, hvis værdi er lig med blokstørrelsen.

ECB vs. CBC


Hvis CBC er så meget bedre end ECB, hvorfor så bruge ECB ?

Det kan der være noget om, men tænk over følgende eksempel. Vi har lavet en DBMS (Database Management System som f.eks. MySQL) og vi vil understøtte store krypterede tabeller. En tabel kan fylde flere terabytes.
Hvis tabellen er krypteret med en block cipher i CBC mode, hvad sker der så, hvis vi ændrer noget i den første blok ?

Så ændrer cipher teksten sig for den første blok, og så kan vi ikke længere dekryptere anden blok. Derfor skal anden blok rekrypteres, men så ændrer den sig, og hvad så med den tredje blok ? Vi bliver med andre ord nødt til at rekryptere alle data som følger den ændrede blok.

Det duer jo ikke, men vi kan heldigvis bruge et trick. Filsystemet deler store filer op i sine egne blokke, typisk 4096 bytes, så vi kan bare lade hver filsystem blok have sin egen initialiseringsvektor...måske bruge blok nummeret som initialiseringsvektor. Hvis vores krypterings algoritmes blokstørrelse er 32 bytes, så går der 128 krypto blokke på en filsystem blok. Når vi ændrer data i en blok så skal vi altså rekryptere max 128 blokke af 32 bytes. Langt bedre end at skulle rekryptere en terabyte.

Stream vs. Block


Indtil videre ser stream kryptering langt simplere ud end blok kryptering, så hvorfor overhovedet overveje blok kryptering ?

Well, de har begge deres fordele og ulemper, og dem skal man have med i overvejelserne, når man designer sine systemer. Stream ciphers fordele er block ciphers ulempe og omvendt.

Stream ciphers har bl.a. følgende fordele:
* Der er intet overhead, da man kan kryptere en enkelt byte
* De er ekstremt simple, så koden bliver tilsvarende simpel
* Grundet deres simplicitet bliver det også nemmere at arbejde sammen med forskelligartede systemer

Stream ciphers har til gengæld en meget stor ulempe:
* Du kan ikke springe rundt i de krypterede data og dekryptere tilfældige blokke

Denne ulempe gør stream ciphers næsten ubrugelige til store filer, såsom full disk kryptering, database tabeller og lignende, hvor det vil være en stor ulempe, at man skal dekryptere de første par gigabytes, af en fil, hvis man vil dekryptere et par bytes i slutningen.

Til gengæld er stream ciphers ideelle, hvis man krypterer en datastrøm, som f.eks. data mellem et trådløst tastatur og computeren eller en TCP netværks strøm.

Kompatibilitet


Vi er næsten ved vejs ende i denne artikel, men nu handler kryptografi ofte om sikker kommunikation og denne kommunikation kan give os flere problemer, for hvordan implementeres kryptografien i den modtagende ende ?
Hvis vores PHP script krypterer data, men det f.eks. er Java, som skal dekryptere, så skal de altså være enige om, hvordan data håndteres. De skal understøtte samme algoritme, samme nøglegenerering, samme krypterings modus (der findes mange andre end de to jeg gennemgik), samme padding og mulighed for at sætte samme initialiserings vektor. Det kan være et rent helvede at finde rundt i, og hvis dataene ikke dekrypteres korrekt, hvad gik så galt ? Det ved vi intet om.

Men nu kender vi nogle af ordene, så vi ved lidt, hvad vi skal kigge på, når vi vælger et library, og hvordan vi skal konfigurere algoritmerne.

Afslutning


Det var alt for denne gang. Tag et kig på mcrypt og OpenSSL funktionerne. Der er mange interessante såsom muligheden for at liste alle understøttede algoritmer så brugeren selv kan vælge. Prøv at kryptere noget data med mcrypt og dekryptere det med OpenSSL, det er en lille udfordring herfra :-)

Næste gang...asymmetrisk kryptografi.


Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.

Del også gerne artiklen med dine Facebook venner:  

Kommentarer (4)

User
Theis @ 24.04.12 21:09
He he. Syntes personligt nu ikke at stream-ciphers er så "ekstremt simple". Er hvert fald personligt ved at gå i spåner over analyser af Linear Feedback Shift Registers og collection generators. Men princippet er selvfølgelig simpelt.

Grunden til at streamciphers primært er kendte er at de er ekstrem hurtige. Og at de er total velegnet til implementering i hardware. Derfor har de været populære til eksempelvis mobiltelefoni krypteringer, wifi og lignende. Ulempen er nok at de måske ikke er nær så sikre end blok ciphers. På et tidspunkt omkring år tusind skiftet var der for eksempel en periode hvor der var blevet fundet brud på sikkerheden i næsten alle populære stream ciphers.
User
Robert Larsen @ 25.04.12 09:10
@Theis

Ja, teorien og alt der ligger bag (alle algoritmerne), er utrolig komplekst, men brugen af stream ciphers er utrolig meget simplere end brugen af block ciphers. Du skal ikke padde, der er ikke brug for IV, der er ikke brug for ECB/CBC eller de andre modes.

Jeg har ikke kunnet finde dokumentation på, at stream ciphers skulle være svagere end block ciphers, men der vil jo altid være en risiko for, at noget, vi troede var sikkert, pludselig ikke er det alligevel. Hvis nogen opdager, at S-Boxe ikke yder nogen sikkerhed, så vil stort set alle block ciphers falde fra hinanden, og hvis nogen finder ud af at faktorisere, så er RSA værdiløs.

Man kan faktisk bruge en block cipher til at generere en bytestrøm og dermed bruge den som en stream cipher:
1) Initialisér block cipheren med nøglen
2) Pad nøglen til blok størrelse
3) Krypter nøglen
4) Krypter resultatet
5) goto 4

Al den krypterede materiale er nu din "tilfældige" bytestrøm, som du kan stream kryptere med.
User
Theis @ 25.04.12 17:37
Hey, ja det du nævner er CTR mode.

Er også svært at sige om noget er sikre end andet, når der intet garanti er for deres sikkerhed. Så er jo også lidt subjektivt. Men mange af de populære stream ciphers det blev brugt tidligere har der vist sig at være sikkerhedsbrud på - eksempelvis populære RC4.
Og nogen af de nye eSTREAM kandidater har også brud på sikkerheden (her eksempelvis Grain).
User
Robert Larsen @ 26.04.12 14:59
Ikke CTR, men OFB :-)

Ved CTR krypterer man en tæller.
Du skal være logget ind for at skrive en kommentar.
t