BLOG

Uusimmat kuulumiset ohjelmistotalosta

Tietoturvalliset verkkosovellukset - osa 3

Ohjelmointi, Turvallisuus, Antti Sand

Tämä kirjoitus on suoraa jatkoa edellisille kahdelle kirjoitukselle turvallisista verkkosovelluksista. Tällä kertaa palaamme ohjelmoinnin pariin, mutta lisukkeena on jälleen hieman palvelinpuolen asioita. Aiheena on kuitenkin salasanat, niiden luominen ja niiden säilyttäminen.

Tämä kirjoitus on osa tietoturvalliset verkkosovellukset -sarjaa. Löydät kaikki sarjan kirjoitukset tästä linkistä.

Huonoa historiaa

Tarina kertoo, että ainakin joitain vuosia sitten Marvel Comicsilla oli verkkopalvelu. Tähän verkkopalveluun, kuten moniin muihinkin, kirjauduttiin käyttäjätunnuksella ja salasanalla. Tällä verkkopalvelulla oli kuitenkin poikkeuksellinen ominaisuus, jota ei tänä päivänä enää useinkaan näe. Palvelusta pystyi nimittäin tilaamaan oman salasanansa sähköpostiviestinä, siltä varalta, että se sattui unohtamaan. Kuinka kätevää!

Jotkut kehittäjät päättävät tallentaa käyttäjien salasanat tietokantaan sellaisenaan. Miksikös ei, tietokantahan on salasanan takana? Kuten tämän sarjan ensimmäisessä osassa mainittiin, joskus käy niin, että hyökkääjä pääsee käsiksi tietokannan sisältöön ja joskus salasanat pääsevät vuotamaan.

Jotta tähän voitaisiin varautua, jotkut kehittäjät käyttivät PHP:n funktioita mcrypt_encrypt() salatakseen salasanat tietokantaan. Sitten, jos salasana piti palauttaa käyttäjälle, voitiin käyttää mcrypt_decrypt() -funktiota. Eipähän olleet salasanat suoraan luettavissa tietokannassa. Mutta jos tietokanta pääsi vuotamaan, hyökkääjän tarvitsi vain ajaa tiedot jälkimmäisen funktion läpi.

Lyhyesti sanottuna, jos saat jostain palvelusta salasanasi palautettua sähköpostiisi, niin kannattaa käyttää kyseisessä palvelussa sellaista salasanaa, jota ei käytä missään muualla. Eikä kannata laittaa kyseiseen palveluun mitään sellaista tietoa, jota ei halua jakaa avoimesti.

Vaikka unohtuneen salasanan palauttaminen voi kuulostaa hienolta ominaisuudelta, se käytännössä tarkoittaa sitä, että tietosi eivät ole turvassa.

Salasanojen tallentaminen tietokantaan

Sovitaan siis heti aluksi, että tietokantaan ei tallenneta salasanoja missään sellaisessa muodossa, josta ne voidaan palauttaa selkokieliseksi tekstiksi. Toisin sanoen, salasana ei saa olla selkokielinen, eikä kaksisuuntaisella matemaattisella funktiolla saavutettu. Kaksisuuntaisella matemaattisella funktiolla tarkoitan sellaista funktiota, joka voidaan ajaa kumpaankin suuntaan. Hyvin pelkistäen vaikkapa niin, että:

1 + 2 = 3
3 - 2 = 1

Tuossa funktiossa meillä on lähtöarvona 1, jota mutatoidaan operandilla 2. Tulosta uudelleen mutatoimalla samalla operandilla, mutta käänteisellä operaattorilla, saadaan takaisin lähtöarvo 1. Tämä siis toimi molempiin suuntiin. Salasanojen kanssa tarvitaan funktio, joka voidaan laskea vain yhteen suuntaan.

Mitä ovat tiivisteet?

Tiivisteet (Hash), ovat funktioita, jotka mutatoivat lähtöarvon tuloarvoksi siten, että tuloarvoa ei voida palauttaa takaisin lähtöarvoksi. Se on siis yhdensuuntainen funktio. Tiivisteet eivät ole salakirjoitusta. Salakirjoitus voidaan purkaa. Kuten aiemmin käytiin läpi, koskaan ei ole tilannetta, että salasana pitäisi voida palauttaa selkokielisenä. Tallennamme siis vain tiivisteen, jota ei voida laskea takaisin lähtöarvokseen.

Tässä on tietenkin tärkeää se, että tiivistefunktio palauttaa samalla lähtöarvolla aina saman tuloarvon. Muutenhan se ei toimisi. Tiivistefunktiot palauttavat toteutuksesta riippuen kiinteän määrän merkkejä. Koska tiivistefunktiolle voidaan antaa rajaton määrä merkkejä, on väistämätöntä, että joskus tiivistefunktio palauttaa saman tuloarvon eri lähtöarvoilla. Tällöin oikean salasanan voi arvata useammalla eri vaihtoehdolla. Kuitenkin, kunhan eri vaihtoehtojen määrä on riittävän suuri, ei tästä tule kauhean isoa ongelmaa ja palaamme myöhemmin siihen, miten arvailujen määrää voidaan rajoittaa.

Miten tiivisteitä vastaan hyökätään?

Koska tiedämme, miten yhdensuuntainen tiiviste lasketaan ja tiedämme, että samalla lähtöarvolla saadaan aina sama tuloarvo, voimme laskea valmiiksi taulukon tuloarvoista ja lähtöarvoista.

$aaaa = '74b87337454200d4d33f80c4663dc5e5';
$aaab = '4c189b020ceb022e0ecc42482802e2b8';
$aaac = '3963a2ba65ac8eb1c6e2140460031925';
...

Kun laskemme kaikki mahdolliset merkkijonot vaikkapa kuuden merkin pituuteen asti, meillä menee ikä ja terveys, mutta saamme valmiin taulukon, josta voimme palauttaa tiivisteen salasanaksi. Tietysti netistä löytyy tällaisia taulukoita valmiiksi.

Valmiin taulukon lataaminen siis mahdollistaa tiivisteen palauttamisen selkokieliseksi. Miten sitä vastaan voidaan suojautua?

Tiivistefunktiolle voidaan antaa parametri, jota hyödynnetään tiivisteen laskemisessa. Jos parametri on sattumanvarainen, ei valmiiksi laskettua taulukkoa voida enää hyödyntää. Taulukko pitäisi laskea uudelleen kyseisen parametrin kanssa. Tätä kutsutaan termillä salt. Kaksi samaa merkkijonoa tiivistettynä eri saltilla tuottavat kaksi erilaista tiivistettä.

Onko satunnainen sama, kuin satunnainen?

Ei ole. Periaatteessa tiivistefunktion saltin ei tarvitse olla täysin satunnainen, mutta perus rand() -funktiokaan ei riitä. PHP:n rand() ja mt_rand() -funktioissa on se onglema, että ne käyttävät satunnaisuuden muodostamiseen lähtödataa, jota voidaan manipuloida ja ennustaa. Ne eivät siis muodosta todella sattumanvaraisia merkkijonoja. Hienommin sanottuna, ne eivät sisällä tarpeeksi entropiaa. Jos rand() ajetaan tarpeeksi monta kertaa, voidaan lopulta ennustaa sen seuraava tulos. Itse asiassa PHP:n rand() -funktio pitäisi ajaa vain 624 kertaa, jotta tiedettäisiin kaikki tulevat arvot.

Miten sitten saadaan lisää entropiaa? Yksi vaihtoehto on käyttää palvelimen /dev/random:ia. Tämä kerää entropiaa palvelimen raudalta, näppäimistöstä, hiiren liikkeistä, levyjärjestelmän tapahtumista, ym. Tässä on vain se ongelma, että tuon entropian keräämiseen saattaa kulua paljon aikaa. Kirjautumissivu voi jäädä minuutiksi odottamaan tarpeeksi suuren entropiamäärän kerääntymistä. Toinen vaihtoehto on lukea palvelimen /dev/urandom:sta. Tämä ei ole satunnaista, mutta jokseenkin tarpeeksi satunnaista tähän tarkoitukseen. /dev/urandom palauttaa heti pseudo-satunnaisen luvun, joka riittänee normaalissa käytössä.

Satunnaisen salt:in luominen perus PHP ohjelmassa voisi tapahtua seuraavasti:

// Binäärinen salt /dev/urandom:sta
$saltLength = 22;
$binarySalt = mcrypt_create_iv(
	$saltLength,
	MCRYPT_DEV_URANDOM
);

// Salt merkkijonoksi
$salt = substr(
	strrt(
		base64_encode($binarySalt),
		'+',
		'.'
	),
	0,
	$saltLength
);

Tiivistefunktiot

Tiivistefunktioita on useita ja ne ovat kehittyneet aikojen saatossa turvellisemmiksi versio versiolta.

MD5

Yleisin ja aikanaan todella paljon käytetty on md5. md5 on nopea ja hyvin tuettu tiivistefunktio. Se ei kuitenkaan ole turvallinen. Törmäytystilanteet, joissa siis useampi erilainen merkkijono tuottaa saman tiivisteen, ovat md5:llä melko triviaaleja toteuttaa.

Joukko tutkijoita demonstroi törmäytysten laskemista tavallisella kannettavalla tietokoneella jo vuonna 2005. Kannettavat vuonna 2005 olivat kivikautisia nykyiseen verrattuna. Tämä kuitenkin osoitti, että hyökkäykseen ei tarvita kallista supertietokonetta tai paljoa aikaa. Tässä ei kuitenkaan otettu huomioon satunnaista salt:ia. Sen kanssa tiivistefunktio on edelleen jokseenkin turvallinen, mutta parempiakin on olemassa.

MD5 Wikipediassa

SHA-1

SHA-1:tä pidettiin turvallisena vuosikausia. Itsekin tyytyväisenä päivitin aikanaan tiivistefunktion md5:stä SHA-1:een ja tunsin oloni turvalliseksi. Kehitys kuitenkin kehittyy ja jo 2005 (joo, huono vuosi) Shandongin Yliopiston tutkijat julkaisivat paperin, jossa laskettiin, että törmäytys SHA-1:llä vaatisi "vain" 2^69 operaatiota. Törmäytys 2^80:llä operaatiolla on jonkinlainen turvallisen rajapyykki. Mutta Mooren laki on johtanut siihen, että SHA-1:kin on auttamatta liian heikko tiiviste. Siis ilman salt:ia.

SHA-1 Wikipediassa

SHA-256 / SHA-512

Vuonna 2001 SHA-1:lle julkaistiin korvaaja, SHA-2. SHA-256 ja SHA-512 toimivat samalla tavalla, mutta siinä missä SHA-256 käyttää 32-bittisiä sanoja, SHA-512 käyttää 64-bittisä sanoja. Lisäksi niiden laskentakerroissa on eroja. SHA-2 on tällä hetkellä turvallinen, mutta sitä ei ole taisteluissa testattu yhtä kattavasti, kuin Blowfish -salausta, jota BCrypt käyttää. Blowfish on ollut julkisuudessa jo vuodesta 1991 ja sitä pidetään edelleen kryptografisesti turvallisena, kunhan salausavain on riittävän vahva. Koska BCrypt käyttää salausavainta, se on hitaampi laskea, mikä on siis hyvä asia, kun ajatellaan tiivisteiden valmiiksi laskemista.

SHA-2 Wikipediassa

BCrypt

BCrypt julkaistiin vuonna 1999 ja sitä on tutkittu kattavasti. Koska sen laskenta on hitaampaa, se tarjoaa pienen lisäsuojan tiivistetaulukoiden valmiiksilaskemista vastaan. Tällä hetkellä sitä pidetään kryptografisesti turvallisena ja sitä minä käyttäisin. BCrypt:llä voi tiivistää vain 72 merkkiä, joten se asettaa ylärajan salasanoille, mutta periaatteessa ei haittaa, vaikka käyttäjä kirjoittaisi pidemmän salasanan, mutta siitä tarkistettaisiin vain ensimmäiset 72 merkkiä.

BCrypt:n turvallisuuden puolesta puhuu sekin, että se on BSD -käyttöjärjestelmän ja joidenkin suosittujen Linux distrojen oletustiivistefunktio salasanoille.

BCrypt Wikipediassa

SCrypt

SCrypt on uusi kisaaja, se julkaistiin vuonne 2012. SCrypt on muisti-intensiivinen derivaattafunktio. Teoriassa SCrypt:n pitäisi olla turvallisempi kuin BCrypt:n, koska sen algoritmi on hitaampi. Haittapuolena on taas uutuus. Yleensä kryptografiassa uusi funktio on "huonompi", koska sitä ei ole vielä taistelutestattu niin kauaa. Tästä syystä BCrypt saattaa olla vielä parempi valinta. Tämä tilanne saattaa kuitenkin muuttua tavallista nopeammalla aikataululla, sillä SCrypt:iä käytetään muutamissa kryptovaluutoissa, kuten Litecoin ja Dogecoin, joka puolestaan tarkoittanee sitä, että se tulee kohtaamaan huomattavan määrän murtoyrityksiä nopeammin, kuin edeltäjänsä.

SCrypt Wikipediassa

Tiivisteiden tallentaminen

Jokainen tiivistefunktio palauttaa aina määrätynmittaisen merkkijonon. Näin ollen tehokkuuden nimiin voi tulla kiusaus rajoittaa tietokannan salasanakentät juuri tuohon merkkimäärään. Esimerkiksi BCrypt tuottaa aina tasan 60 merkkiä piskän tiivisteen. Tietokantakenttä voisi siis olla char(60). Tällä säästettäisiin muutama tavu. Tämä ei kuitenkaan ole hyvä lähestymistapa asiaan. Kun tiivistefunktiot kehittyvät ja niiden turvallisuustilanne muuttuu, kehittäjälle voi tulla tarve vaihtaa käyttämäänsä tiivistefunktiota ja samalla tietokannan kentän mittaa. Jos salasanakenttä on jo valmiiksi vaikkapa varchar(255), käytämme sitä nyt tehottomasti, mutta jatkossa voimme vaihtaa käyttämäämme tiivistefunktiota muuttamatta tietokantaskeemaa.

Jos ohjelmaa halutaan optimoida, löytyy varmasti useita parempiakin kohtia, joista voi saada myös huomattavasti suuremman hyödyn.

Tiivistefunktio päivittäminen

Koska salasanoja ei tallenneta sellaisessa muodossa, josta niitä voitaisiin palauttaa, ei järjestelmän tiivistefunktiota voida vaihtaa "lennosta". Käytännössä pitäisi sallia samanaikaisesti kirjautumiset vanhalla ja uudella tiivistefunktiolla ja pitää kirjaa siitä, onko kyseinen käyttäjä kirjautunut sen jälkeen, kun uusi tiivistefunktio on otettu käyttöön. Jos ei ole kirjautunut, niin kirjautumisen yhteydessä autentikoidaan vanhalla tiivistefunktiolla, tallennetaan salasana uudelleen uudella tiivistefunktiolla ja merkataan käyttäjän salasana päivitetyksi, jolloin jatkossa yritetään autentikointia uudella tiivistefunktiolla.

Tällä tavalla jokaisen käyttäjän salasana muuttuu turvalliseksi seuraavan kirjautumisen yhteydessä. Mutta kaikki käyttäjät eivät kirjaudu kovin usein. Silloin vaihtoehtoina on lähettää käyttäjille viesti, jossa voidaan samalla kehottaa vaihtamaan salasanansa uuteen, tai sitten poistaa tietyn siirtymäajan jälkeen vanhat käyttäjätilit kylmästi. Tällöinkin voisi olla paikallaan laittaa viestiä, että kiitos käynnistä ja luo uusi tili, jos joskus haluat jatkaa palvelun käyttöä.

Ensimmäinen vaihtoehto olisi siis siinä mielessä mukavampi, että se ei aiheuttaisi aktiivisille käyttäjille mitään huomattavaa haittaa. Mutta toisaalta, jos tiivistefunktio on syytä vaihtaa turvallisuustilanteen muuttumisen vuoksi, niin ehkä sitten olisi syytä vaatia käyttäjiä vaihtamaan salasanansakin.

Yhteenveto

Tässä kirjoituksessa kävimme läpi lähinnä vain tiivistefunktioita salasanojen turvallisempaan tallentamiseen. Jos tästä jotain haluaa ottaa mukaansa, niin ehkä sen, että turvallisuus usein koostuu kerroksista. Vaikka tietokanta on salasanasuojattu, niin voimme varautua siihen, että tiedot vuotavat. Ja vaikka salasanat eivät ole selkokielisiä, voimme varautua siihen, että joku käyttää huomattavan määrän energiaa niiden murtamiseen. Jos käytät asiallista ohjelmistokehystä, niin tiivisteet todennäköisesti lasketaan jo melko turvallisesti. Taustat on silti hyvä tietää, ettei itse tee mitään tyhmää, mikä avaisi tietoturva-aukkoja.

Se, mitä emme tässä vielä käyneet ja joka jää seuraavaan kirjoitukseen, on salasanan murtaminen brute force -tekniikalla, eli yksinkertaisesti vain kokeilemalla eri vaihtoehtoja. Tähänkin löytyy vastakeinonsa.

Lähteet ja lisää luettavaa

Tiedon salaaminen, Antti Sand

Salausmenetelmät, Petteri Järvinen

Building secure PHP apps - a practical guide, Ben Edmunds

Antti Sand

Ikuinen oppija, kiinnostunut miltei kaikesta, paitsi jääkiekosta. Siltä osalta siis aivan väärässä seurassa. Kirjoittelee joskus pohditojaan oppimisesta tähän blogiin.

Takaisin blogisivulle