Jesse Salo Advanced Encryption Standard Algoritmin toteutus Rust-ohjelmointikielellä Vaasa 2025 Tekniikan ja innovaatiojohtamisen akateeminen yksikkö Tietotekniikan diplomityö Automaatio- ja tietotekniikan maisteriohjelma 2 VAASAN YLIOPISTO Tekniikan ja innovaatiojohtamisen akateeminen yksikkö Tekijä: Jesse Salo Tutkielman nimi: Advanced Encryption Standard: Algoritmin toteutus Rust- ohjelmointikielellä Tutkinto: Diplomi-insinööri Oppiaine: Automaatio- ja tietotekniikan maisteriohjelma Työn ohjaaja: Jouni Lampinen Valmistumisvuosi: 2025 Sivumäärä: 77 TIIVISTELMÄ: Tässä diplomityössä käsitellään Advanced Encryption Standard -salausalgoritmia. Tämä symmetrinen algoritmi on laajalti käytössä erityisesti sulautetuissa järjestelmissä, koska se on laskennallisesti nopea ja vaatii vain vähän muistia. Advanced Encryption Standard -algoritmi tukee kolmea eri avaimen pituutta sekä useita erilaisia salausmoodeja. Diplomityön tarkoituksena on luoda ohjelmistototeutus tälle algoritmille Rust-ohjelmointikielellä, koska valmiina löytyy helposti ainoastaan valmiita kirjastoja. Laaditulla salausohjelmalla voidaan salata tekstitiedostoja sekä purkaa niiden salauksia. Toisena tavoitteena tällä työllä on suunnitella ohjelmisto siten, että sitä voidaan helposti laajentaa tukemaan muita salausmoodeja. Salausalgoritmi on toteutettu vaihe vaiheelta Rust-ohjelmointikielellä ja jokainen vaihe on testattu erikseen yksikkötasolla. Testauksessa on käytetty apuna Rust-kieleen sisäänrakennettua testausominaisuutta. Algoritmin eri vaiheet on ohjelmassa yhdistetty, jolloin ne muodostavat kierroksen, algoritmin toiminnallisen selkärangan. Algoritmin toiminta kokonaisuudessaan koostuu useasta toistuvasta kierroksesta. Salauksen toiminta on testattu syöttämällä ohjelmalle testivektori, jota vastaava salattu vektori tunnetaan, ja vertaamalla ohjelman vastetta tähän. Salausalgoritmia käytetään aina yhdessä jonkin hyväksytyn toimintamoodin kanssa, joita ohjelmaan on toteutettu kolme erilaista. Salausohjelmaan on määritelty valmiita rajapintoja, joiden avulla tuettujen toimintamoodien lisääminen helpottuu. Ohjelman käyttäjää varten algoritmin yläpuolelle on toteutettu yksinkertainen käyttöliittymä, jolla käyttäjä syöttää tekstitiedostojen kautta datan ohjelmalle, ja valitsee terminaali-ikkunassa halutun toimintamoodin. Ohjelma kirjoittaa laskennan tulokset omiin tekstitiedostoihinsa. Työssä on kehitetty yleiskäyttöinen salausohjelma Rust-ohjelmointikielellä, joka käyttää Advanced Encryption Standard -algoritmia. Ohjelma tukee kaikkia kolmea standardissa määriteltyä avaimen pituutta ja käyttää algoritmin yhteydessä hyväksyttyjä toimintamoodeja. Lisäksi lähdekieliseen ohjelmaan on sisällytetty valmiiksi rajapintoja, joiden avulla muiden toimintamoodien käyttöönottaminen helpottuu. Ohjelman oikeanlainen toiminta on varmistettu julkisilla testivektoreilla, ja huolellisesti laaditun testaussuunnitelman noudattamisen havaittiin nopeuttavan testausprosessia huomattavasti. Työn aikana Rust- ohjelmointikielen todettiin soveltuvan hyvin kryptografisen algoritmin toteuttamiseen. Sen periaatteisiin kuuluva muistiturvallisuus ehkäisee tahattomia datamuokkauksia, jotka muutoin johtaisivat salauksen epäonnistumiseen ja datan korruptoitumiseen. Samalla havaittiin, että Rust-kielen iteraattorit mahdollistavat algoritmissa vaadittavien tavutaulukoiden käsittelyn lyhyillä ja tiiviillä ohjelmointikäskyillä. Rust-kielelle ominaiset ohjelmointiperiaatteet vaativat muihin ohjelmointikieliin tottuneelta käyttäjältä ajattelutavan muutoksen. Tämä hidastaa toteuttamista aluksi, kunnes käyttäjä tottuu uusiin ohjelmointitapoihin. Varsinaista estettä valittu kieli ei tällaiseen toteutukseen kuitenkaan tuo. AVAINSANAT: AES, salausalgoritmi, kryptografia, ohjelmointi, Rust 3 Sisällys 1 Johdanto 7 2 Advanced Encryption Standard 10 2.1 Yleiset ominaisuudet 10 2.2 Matemaattinen tausta 11 2.3 Algoritmin rakenne 12 2.3.1 SubBytes-muunnos 13 2.3.2 ShiftRows-muunnos 15 2.3.3 MixColumns-muunnos 15 2.3.4 Kierrosavaimen lisäys 16 2.4 Avaimet 16 2.5 Käänteisalgoritmi 19 2.5.1 InvShiftRows-muunnos 20 2.5.2 InvSubBytes-muunnos 20 2.5.3 InvMixColumns-muunnos 21 2.5.4 Ekvivalentti käänteisalgoritmi 22 2.6 Toimintamoodit 23 2.6.1 Luottamuksellisuuden tarjoavat toimintamoodit 23 2.6.2 Todentamiseen soveltuva toimintamoodi 24 2.6.3 Todentamisen ja luottamuksellisuuden yhdistävät toimintamoodit 24 2.6.4 Muut toimintamoodit 25 2.7 Viestin täyte 26 3 Rust-ohjelmointikieli 28 3.1 Erityispiirteet 28 3.1.1 Muuttujat ja vakiot 29 3.1.2 Tietotyypit 30 3.1.3 Omistajuus 32 3.1.4 Unsafe Rust 35 3.2 Rust-kielen kehitysympäristötyökalut 37 3.2.1 Rustup 37 4 3.2.2 Cargo 38 3.3 Kryptografiset kirjastot 39 4 Suunniteltu salausohjelma 41 4.1 Tutkimussuunnitelma 41 4.2 Vaatimusten määrittely 42 4.3 Ohjelman rakenne ja arkkitehtuuri 43 4.4 Toteutussuunnitelma 45 4.5 Testaussuunnitelma 45 5 Algoritmin toteutus 47 5.1 AES-algoritmin toteutus ja testaus 48 5.2 Toimintamoodien toteutus ja testaus 52 5.3 Integraatiotestaus 57 6 Tulosten ja havaintojen analysointi 59 6.1 Ohjelman vaatimukset 59 6.2 Arkkitehtuurin suunnittelu 60 6.3 Ohjelmiston toteutus ja testaus 61 6.4 Ohjelman toiminta 66 7 Johtopäätökset 68 Lähteet 71 Liitteet 75 Liite 1. S-laatikko 75 Liite 2. S-käänteislaatikko 76 Liite 3. Testauksessa käytetyt testivektorit 77 5 Kuviot Kuvio 1. Tavujen ja bittien indeksointijärjestys. 11 Kuvio 2. Bittien järjestys algoritmin syötteessä, tilassa ja vasteessa. 11 Kuvio 3. ShiftRows-muunnoksen toiminta. 15 Kuvio 4. InvShiftRows-muunnoksen toiminta. 20 Kuvio 5. Salausohjelman sisältämät moduulit ja niiden väliset suhteet. 44 Taulukot Taulukko 1. Avaimen laajennuksen kierrosvakiot. 17 Taulukko 2. SubBytes-muunnoksen S-laatikko. 75 Taulukko 3. InvSubBytes-muunnoksen S-käänteislaatikko. 76 Algoritmit Algoritmi 1. Pseudokoodiesitys AES-algoritmin rakenteesta. 13 Algoritmi 2. Pseudokoodiesitys AES-algoritmin avaimen laajennuksesta. 18 Algoritmi 3. Pseudokoodiesitys AES-käänteisalgoritmin rakenteesta. 19 Algoritmi 4. Pseudokoodiesitys ekvivalentin AES-käänteisalgoritmin rakenteesta. 21 Algoritmi 5. Pseudokoodiesitys ekvivalentin AES-käänteisalgoritmin avaimen laajennuksesta. 22 Algoritmi 6. Funktio CMAC-toimintamoodin aliavainten johtamiseen standardin mukaisesti sekä apufunktio cmac_shift_key(). 54 Lyhenteet AAD Additional Authentication Data AES Advanced Encryption Standard CBC Cipher Block Chaining CCM Counter with Cipher Block Chaining-Message Authentication Code 6 CFB Cipher Feedback CMAC Cipher-based Message Authentication Code CTR Counter ECB Electronic Code Book FPE Format-preserving Encryption GCM Galois Counter Mode IEEE Institution of Electrical and Electronics Engineers ISO International Organization for Standardization IV Initialization Vector NIST National Institute of Standards and Technology OFB Output Feedback 7 1 Johdanto Jo muinaiset roomalaiset välittivät tietoa, jonka he halusivat pitää salassa muilta. Tiedon määrä maailmassa lisääntyy kiihtyvällä tahdilla hetki hetkeltä. Samalla kasvaa myös sellaisen tiedon määrä, joka halutaan pitää poissa asiattomien henkilöiden ulottuvilta. Kautta historian tätä tavoitetta kohti on pyritty koodaamalla tietoa siten, että vain valittu vastaanottaja voi sen purkaa ja saada tiedosta hyötyä. Nykyään kommunikoinnin ja salauksen välineet ovat sähköisiä ja menetelmät varsin monimutkaisia. Tämä muodostaa haasteen heille kuulumattomiin viesteihin käsiksi pyrkiville hyökkääjille, mutta myös salauksen toteuttaville osapuolille, koska huonosti toteutettu hyväkin salaus murtuu herkästi. Tässä diplomityössä käsitellään Advanced Encryption Standard -salausalgoritmia, joka on Yhdysvaltojen standardoimisviraston National Institute of Standards and Technologyn (NIST) hyväksymä (National Institute of Standards and Technology, 2001/2023). Tämä symmetrinen lohkosalausmenetelmä on laajalti käytössä erityisesti sulautetuissa järjestelmissä, koska se on laskennallisesti nopea sekä vaatii vain vähän muistia. Algoritmi luotiin belgialaisten tutkijoiden Joan Daemenin ja Vincent Rijmenin tekemän ehdotuksen pohjalta, jolla he osallistuivat NIST:n järjestämään kilpailuun (Daemen & Rijmen, 1999). Diplomityön tarkoituksena on luoda ohjelmistototeutus tälle algoritmille Rust- ohjelmointikielellä. Laaditulla salausohjelmalla voidaan salata tekstitiedostoja ja purkaa niiden salauksia, sekä laskea ja varmentaa viestien tunnisteita. Vuonna 2015 julkaistu Rust on tehokas ja muistiturvallinen kieli, ja se sisältää monia funktionaalisten kielien ominaisuuksia, jotka kääntäjä pystyy muuttamaan tehokkaaksi suorituskieliseksi ohjelmaksi (Sharma ja muut, 2019, s. 10). Työn aikana pyritään myös arvioimaan, kuinka valittu ohjelmointikieli soveltuu kyseessä olevan kryptografisen algoritmin toteuttamiseen. 8 Advanced Encryption Standard -algoritmi tukee kolmea eri avaimen pituutta (National Institute of Standards and Technology, 2001/2023, luku 5). Salausohjelma toteutetaan tukemaan näitä kaikkia kolmea, eli 128, 192 sekä 256 bitin pituisia avaimia. Kryptografisia algoritmeja käytettäessä avainten luomisessa ja käyttämisessä on huomioitavia turvallisuusseikkoja, mutta ne rajataan tämän työn ulkopuolelle. Huomautettakoon kuitenkin, että symmetrisissä salausalgoritmeissa käytettävä avain on pidettävä salassa, sillä sen avulla kuka tahansa pystyy väärinkäyttämään algoritmin tarjoamia mahdollisuuksia. AES-algoritmi käsittelee kerrallaan 128-bittisiä lohkoja, eikä sitä tulee käyttää sellaisenaan, vaan yhdessä jonkun kyseiselle algoritmille hyväksytyn toimintamoodin kanssa, joita on tällä hetkellä neljätoista kappaletta (National Institute of Standards and Technology, 2017/2024). Tässä työssä toteutetaan näistä kolme: Electronic Code Book, Cipher Block Chaining ja Cipher-based Message Authentication Code. Näistä kaksi ensimmäistä auttavat säilyttämään viestin luottamuksellisuuden, eli ainoastaan salausavaimen tunteva vastaanottaja pystyy selvittämään viestin sisällön. Kolmatta moodia käytetään viestien alkuperän todentamiseen. Se tarkoittaa sitä, että viestistä lasketaan tunniste, jonka avulla vastaanottaja voi varmentaa, että viesti on todellakin peräisin oikealta lähettäjältä. Toisena tavoitteena tällä työllä on suunnitella ohjelmisto siten, että sitä voidaan helposti laajentaa tukemaan muita salausmoodeja. Ohjelman arkkitehtuurissa tämä pyritään huomioimaan siten, että uutta toimintamoodia lisättäessä täytyy tehdä muutoksia mahdollisimman pieneen osaan lähdekielistä ohjelmaa. Toteutettavan salausohjelman tärkein tavoite on tarjota toimiva AES-algoritmi ja edellä mainitut kolme toimintamoodia sille. Tämä varmistetaan laajahkolla valikoimalla NIST:n julkaisemia testivektoreita, joiden avulla saadaan tähän tarkoitukseen riittävä varmuus siitä, että ohjelma toimii oikein. Ohjelmalle toteutetaan myös yksinkertainen käyttöliittymä, mutta ohjelman käytettävyys ei itsessään ole tässä työssä tarkastelun 9 kohteena. Sen tarkoituksena on vain tarjota käyttäjälle ohjelman sydän, eli algoritmin palvelut. Tällaista yleiskäyttöistä ja laajennettavaa itsenäistä ohjelmaa ei toistaiseksi ole yleisesti ja helposti saatavilla. Olemassa on jo valmiita AES-kirjastoja Rust-kielellä, mutta ne eivät ole itsenäisiä ohjelmia, vaan vain liitettävissä olemassa olevaan lähdekieliseen ohjelmaan. Sen vuoksi tämä diplomityö hyödyttää sellaisia henkilöitä, jotka ovat kiinnostuneita toteuttamaan itse kryptografisia algoritmeja Rust-kielellä tai jollain muulla ohjelmointikielellä. Työn eri vaiheissa saaduista havainnoista voi saada apua vastaavanlaisiin projekteihin. 10 2 Advanced Encryption Standard Vuonna 1997 NIST aloitti kilpailun, jossa etsittiin symmetristä salausalgoritmia liittovaltiotason informaation suojaamiseksi (Nechvatal ja muut, 2001, s. 515). Seuraavana vuonna valittiin lähetetyistä ehdotuksista 15 kandidaattia, joista yhdestä tulisi seuraava NIST:n määrittelemä salausstandardi. Erittäin pitkän ja monivaiheisen arviointiprosessin päätteeksi vuonna 2000 NIST valitsi voittajaksi algoritmin nimeltä Rijndael, jonka olivat esittäneet belgialaiset matemaatikot Joan Daemen ja Vincent Rijmen. Rijndael-algoritmia pidettiin soveltuvana valintana standardiksi, koska se tarjosi hyvän yhdistelmän turvallisuutta, suorituskykyä ja joustavuutta. Sen tehokkuutta ja toteuttamistapoja pidettiin myös arvossa (Nechvatal ja muut, 2001, s. 563). Rijndael otettiin käyttöön pienin muutoksin, ja se tunnetaan nykyään yleisesti myös nimellä Advanced Encryption Standard (AES). Daemen ja Rijmen (1999) päättivät, että heidän keksimäänsä algoritmia tai mitään sen toteutuksia ei tulla patentoimaan. Yli kaksikymmentä vuotta myöhemmin AES on edelleen NIST:n hyväksymä virallinen standardi (National Institute of Standards and Technology, 2001/2023). 2.1 Yleiset ominaisuudet AES on symmetrinen lohkosalain. Symmetrisyys tarkoittaa sitä, että salaus ja sen purkaminen tapahtuvat samalla avaimella. Sen takia avain on pidettävä ainoastaan viestin lähettäjän ja vastaanottajan tiedossa, ja tästä syystä tällaisia algoritmeja kutsutaan myös yksityisen avaimen järjestelmiksi. Tämä erottaa ne epäsymmetrisistä eli julkisen avaimen salausjärjestelmistä, joissa viestin lähettäjä salaa viestin vastaanottajan julkisella avaimella. Salaus puretaan vastaanottajan yksityisellä avaimella, jonka hän itse ainoastaan tuntee. Lohko on puolestaan tietyn mittainen bittijono, jota lohkosalain käsittelee. Lohkon pituudeksi on tässä standardissa määritelty 128 bittiä. Yli tavun mittaiset bittijonot, kuten lohkot, indeksoidaan nousevassa järjestyksessä vasemmalta oikealle sekä bittien että tavujen osalta. Yksittäisten tavujen kohdalla indeksointi 11 tapahtuu laskevassa järjestyksessä vasemmalta oikealle. Tätä havainnollistetaan alla olevassa kuviossa 1. Bitin indeksi jonossa 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Tavun indeksi jonossa 0 1 Bitin indeksi tavussa 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 Kuvio 1. Tavujen ja bittien indeksointijärjestys (National Institute of Standards and Technology, 2001/2023, luku 3.3). AES-algoritmin syöte on yksi lohko ja se tuottaa yhden lohkon. Sisäisesti algoritmi käsittelee kaksiulotteista tavumatriisia, jossa on neljä riviä ja saraketta. Tätä kutsutaan algoritmissa tilaksi. Syötteenä toimivan 128-bittisen lohkon 16 tavua sijoitetaan tähän tilaan sarakkeittain ylhäältä alas. Tilan tavut indeksoidaan kaksoisindeksillä, kuten matriiseissa on tapana. Algoritmin lopuksi tilan tavut kootaan vastaavaan järjestykseen vasteeksi. Tätä on havainnollistettu alla olevassa kuviossa 2. Syöte Tila Vaste in0 in4 in8 in12 s0,0 s0,1 s0,2 s0,3 out0 out4 out8 out12 in1 in5 in9 in13 → s1,0 s1,1 s1,2 s1,3 → out1 out5 out9 out13 in2 in6 in10 in14 s2,0 s2,1 s2,2 s2,3 out2 out6 out10 out14 in3 in7 in11 in15 s3,0 s3,1 s3,2 s3,3 out3 out7 out11 out15 Kuvio 2. Bittien järjestys algoritmin syötteessä, tilassa ja vasteessa (National Institute of Standards and Technology, 2001/2023, luku 3.4). 2.2 Matemaattinen tausta Algoritmissa tilan tavut tulkitaan kuntalaajennuksen 𝐺𝐹(28) alkioiksi (National Institute of Standards and Technology, 2001/2023, luku 4). Alkiot ovat seitsemännen asteen polynomeja, joiden kertoimet vastaavat tavun bittejä. Tavun b7b6b5b4b3b2b1b0 esitys kunnassa 𝐺𝐹(28) on siis polynomi 12 𝑏(𝑥) = b7x7 + b6x6 + b5x5 + b4x4 + b3x3 + b2x2 + b1x + b0. (1) Additiivinen operaattori tässä kunnassa määritellään eksklusiivisena disjunktiona, josta käytetään myös nimitystä XOR-operaattori. Tätä operaatiota merkitään symbolilla ⊕. Käytännössä operaattori suorittaa operandien yhteenlaskun modulo 2. Polynomien tapauksessa XOR-operaattori laskee yhteen polynomien samaa astetta olevien termien kertoimet modulo 2. Esimerkiksi (𝑥6 + 𝑥4 + 𝑥2 + 𝑥 + 1) ⊕ (𝑥7 + 𝑥 + 1) = 𝑥7 + 𝑥6 + 𝑥4 + 𝑥2. (2) Kunnan multiplikatiivinen operaattori on määritelty polynomien tulona, jossa modulona on tietty polynomi 𝑚(𝑥) = 𝑥8 + 𝑥4 + 𝑥3 + 𝑥 + 1. Operaattoria voidaan merkitä symbolilla ⋅, tai se voidaan jättää kokonaan merkitsemättä, kuten algebrassa on yleisesti tapana. Voidaan siis merkitä multiplikatiivinen operaatio esimerkiksi 𝑏(𝑥)𝑐(𝑥) 𝑚𝑜𝑑 𝑚(𝑥). Polynomi 𝑚(𝑥) vastaa heksadesimaaliesitystä 0x11B ja polynomin redusointi modulo 𝑚(𝑥) voidaan laskea helposti polynomin ja modulon eksklusiivisena disjunktiona (St Denis ja Johnson, 2006, s. 145). 2.3 Algoritmin rakenne AES-algoritmi koostuu kierroksista, joiden määrä riippuu käytetystä avaimen pituudesta. AES-128 sisältää kymmenen, AES-192 kaksitoista ja AES-256 neljätoista kierrosta (National Institute of Standards and Technology, 2001/2023, luku 5). Avaimen laajennuksessa johdetaan pääavaimesta jokaista kierrosta varten uniikki kierrosavain, jonka pituus on pääavaimen pituudesta riippumatta 128 bittiä. Kierrosavaimia tarvitaan yksi enemmän kuin algoritmissa on kierroksia, sillä ennen ensimmäistä kierrosta käytetään ensimmäinen kierrosavain. 13 Yksi kierros koostuu seuraavista neljästä muunnoksesta, tässä järjestyksessä: SubBytes, ShiftRows, MixColumns ja AddRoundKey. Viimeinen kierros poikkeaa muista siten, että MixColumns-muunnosta ei käytetä. Algoritmin sisältämä kierrosrakenne on kuvattu pseudokoodina Algoritmissa 1. AES_algoritmi(syöte, kierrokset, avain) tila = syöte kierrosavaimet[kierrokset+1]=AvaimenLaajennus(avain) tila = AddRoundKey(tila, kierrosavaimet[0]) for kierros from 1 to kierrokset - 1 tila = SubBytes(tila) tila = ShiftRows(tila) tila = MixColumns(tila) tila = AddRoundKey(tila,kierrosavaimet[kierros]) tila = SubBytes(tila) tila = ShiftRows(tila) tila = AddRoundKey(tila, kierrosavaimet[kierrokset]) return tila Algoritmi 1. Pseudokoodiesitys AES-algoritmin rakenteesta (mukaillen National Institute of Standards and Technology, 2001/2023, s. 12). Edelliseen algoritmiin 1 on lisätty myös avaimen laajennus, jotta kierrosavainten käyttö tulisi havainnollistetuksi. Algoritmin oikeassa toteutuksessa avaimen laajennus voidaan tehdä hieman eri aikaan riippuen siitä järjestelmästä, jolle algoritmi toteutetaan. Kaikki kierrosavaimet voidaan laskea kerralla etukäteen tai yksi kerrallaan sitä mukaa kuin kierrosavaimia tarvitaan. Tämä on mahdollista, koska uusi kierrosavain muodostetaan aina edellisestä kierrosavaimesta. Avaimen laajennus esitetään tarkemmin myöhemmässä luvussa. 2.3.1 SubBytes-muunnos AES-algoritmin standardin SubBytes-muunnoksessa jokainen tilan tavu vaihdetaan toiseksi käyttämällä aputaulukkoa, jota kutsutaan S-laatikoksi (National Institute of Standards and Technology, 2001/2023, luku 5.1.1). Muunnos on epälineaarinen, mutta käännettävissä oleva, jotta salauksen purku on myös mahdollista toteuttaa. S-laatikko 14 perustuu kahteen matemaattiseen muunnokseen, joista ensimmäisessä vaiheessa määritetään tavun käänteisalkio 𝑏−1 AES-algoritmin perustana olevassa äärellisessä kunnassa 𝐺𝐹(28). Nollatavulle ei ole määritetty käänteisalkiota, joten tässä kontekstissa se toimii omana käänteisalkionaan. Käänteisalkion biteistä muodostetaan muunnoksen vasteen 𝑏′ bitit yhtälön (3) mukaisella affiinilla muunnoksella 𝑏𝑖 ′ = 𝑏𝑖 −1 ⊕ 𝑏(𝑖+4)𝑚𝑜𝑑8 −1 ⊕ 𝑏(𝑖+5)𝑚𝑜𝑑8 −1 ⊕ 𝑏(𝑖+6)𝑚𝑜𝑑8 −1 ⊕ 𝑏(𝑖+7)𝑚𝑜𝑑8 −1 ⊕ 𝑐𝑖, (3) missä 𝑐 = {01100011} on vakiotavu. Muunnoksessa siis lisätään yhteen käänteisalkion bittejä sekä yksi vakiotavun bitti. S-laatikkoon on laskettu valmiiksi yhtälön (3) mukaiset vasteet kaikille 256 mahdolliselle tavulle ja se on esitetty liitteessä 1. Olkoon SubBytes-muunnoksen syötetavun heksadesimaaliesitys 0xa5. Tällöin muunnoksessa valitaan tavu riviltä a ja sarakkeesta 5, josta löytyy tavu 0x06. Vastaavaan tulokseen päästään myös suorittamalla edellä esitetyt matemaattiset muunnokset. On olemassa muitakin AES-algoritmille soveltuvia S-laatikoita, jotka täyttävät sille asetetut vaatimukset. Daemen ja Rijmen (1999, s. 26) toteavatkin alkuperäisessä ehdotuksessaan, että S-laatikko voidaan vaihtaa toiseen, jos siitä epäiltäisiin löytyvän jokin haavoittuvuus. Tämä on tärkeä ominaisuus, sillä algoritmissa S-laatikko on ainoa epälineaarinen muunnos (Daemen ja Rijmen, 1999, s. 8). Gaithuru ja Bakhtiari (2014) tekivät AES-algoritmin S-laatikolle sarjan tilastollisia testejä, joissa he havaitsivat siinä mahdollisen heikkouden. Testeissä paljastui, että S-laatikon vaste ei ole tilastollisesti tasaisesti jakautunut, vaan se sisältää poikkeamia. Tältä osin S-laatikkoa voitaisiin parantaa, mutta toisaalta he huomauttavat, että yhtään käytännöllistä hyökkäystä algoritmia vastaan ei ole löydetty, joka kykenisi murtamaan järjestelmän. Järjestelmän murtuminen tarkoittaa sitä, että on mahdollista selvittää salauksessa käytetty avain nopeammin kuin kokeilemalla kaikkia mahdollisia avaimia. Bonnetain, Naya-Plasencia ja Schrottenloher (2019) analysoivat AES-algoritmia teoreettisesti mahdollisia tulevia 15 kvanttitietokoneiden tukemia hyökkäyksiä vastaan. Tutkimuksessa kävi ilmi, että kvanttitietokoneiden tuoma laskentatehon lisäys ei nopeuta tunnettuja klassisia hyökkäyksiä riittävästi, jotta järjestelmä murtuisi. Tämä ei kuitenkaan tarkoita, etteikö sellaista olisi mahdollista löytää tulevaisuudessa. 2.3.2 ShiftRows-muunnos ShiftRows-muunnos käsittelee algoritmin tilaa riveittäin. Tilan rivillä olevia tavuja siirretään vasemmalle rivin indeksin osoittama määrä askelia. Ensimmäinen rivi, jonka indeksi on nolla, pysyy siis muuttumattomana. Toisen rivin alkioita siirretään vasemmalle yksi askel, kolmannen kaksi askelta ja neljännen rivin tavuja kolme askelta. Tätä havainnollistetaan alla olevassa kuviossa 3. Syöte Vaste s0,0 s0,1 s0,2 s0,3 s0,0 s0,1 s0,2 s0,3 s1,0 s1,1 s1,2 s1,3 → s1,1 s1,2 s1,3 s1,0 s2,0 s2,1 s2,2 s2,3 s2,2 s2,3 s2,0 s2,1 s3,0 s3,1 s3,2 s3,3 s3,3 s3,0 s3,1 s3,2 Kuvio 3. ShiftRows-muunnoksen toiminta (National Institute of Standards and Technology, 2001/2023, luku 5.1.2). 2.3.3 MixColumns-muunnos Nimensä mukaisesti MixColumns-muunnos käsittelee algoritmin tilan sarakkeita. Muunnoksen syötteenä toimivat tilan sarakevektorit, joista muodostetaan yksitellen vasteen sarakevektorit kertomalla jokainen vakiomatriisilla (National Institute of Standards and Technology, 2001/2023, luku 5.1.3). Sarakkeen muuntaminen tapahtuu yhtälön (4) mukaisesti 16 [ 𝑠′0 𝑠′1 𝑠′2 𝑠′3 ] = [ 02 03 01 01 01 02 03 01 01 01 02 03 03 01 01 02 ] [ 𝑠0 𝑠1 𝑠2 𝑠3 ] , (4) missä vakiomatriisi koostuu tavuista 01, 02 ja 03. Avaamalla yllä oleva matriisiyhtälö saadaan yksittäiset vastetavut yhtälöistä (5) 𝑠′0 = (02 ⋅ 𝑠0) ⊕ (03 ⋅ 𝑠1) ⊕ 𝑠2 ⊕ 𝑠3, 𝑠′1 = 𝑠0 ⊕ (02 ⋅ 𝑠1) ⊕ (03 ⋅ 𝑠2) ⊕ 𝑠3, 𝑠′2 = 𝑠0 ⊕ 𝑠1 ⊕ (02 ⋅ 𝑠2) ⊕ (03 ⋅ 𝑠3) ja (5) 𝑠′3 = (03 ⋅ 𝑠0) ⊕ 𝑠1 ⊕ 𝑠2 ⊕ (02 ⋅ 𝑠3). Huomaa, että tavut 01 on jätetty merkitsemättä kertolaskuihin, koska ykkösellä kertominen ei vaikuta lopputulokseen. 2.3.4 Kierrosavaimen lisäys Kierrosavaimet ovat kokonaisuudessaan 128 bitin mittaisia, mutta niiden ajatellaan koostuvan neljästä peräkkäisestä 32-bittisestä jonosta (National Institute of Standards and Technology, 2001/2023, luku 5.1.4). Ensimmäinen 32-bittinen osa lisätään XOR- operaatiolla algoritmin tilan ensimmäisen sarakkeen kanssa, toinen osa tilan toisen sarakkeen kanssa, kolmas tilan kolmannen sarakkeen ja lopulta neljäs tilan neljännen sarakkeen kanssa. Kierrosavaimet johdetaan avaimen laajennuksessa algoritmille syötetystä pääavaimesta. Avaimen laajennus kuvataan tarkemmin seuraavassa luvussa. 2.4 Avaimet Standardi määrittelee kolme algoritmin muunnosta, jotka ovat AES-128, AES-192 ja AES- 256 (National Institute of Standards and Technology, 2001/2023, luku 5). Algoritmin 17 nimen perässä oleva numero kertoo käytettävän avaimen pituuden bitteinä. Lohkon pituus näissä kaikissa on kuitenkin 128 bittiä. Avaimen luomiseen ei tässä työssä paneuduta sen tarkemmin, vaan työssä käytetään valmiita NIST:n julkaisemia testiavaimia (National Institute of Standards and Technology, 2016/2024a; 2016/2024b; 2016/2024c; 2016/2024d; 2016/2024e). Algoritmille syötetään parametrina yksi pääavain, joka määrittää algoritmin vasteen sille annetusta syötelohkosta. Toisin sanoen sama syötelohko ja sama pääavain tuottavat aina saman lopputuloksen. Algoritmin sisällä käytetään useampia pääavaimesta johdettuja kierrosavaimia. Tätä operaatiota kierrosavainten johtamiseksi kutsutaan algoritmissa avaimen laajennukseksi. Avaimen laajennuksessa luodaan 32-bittisiä sanoja, joita tarvitaan neljä kutakin algoritmin kierrosavaimen lisäysvaihetta varten. Käytettäessä 128-bittistä avainta algoritmi sisältää kymmenen kierrosta ja kierrosavaimen lisäyksiä yhteensä yksitoista. Näin ollen sanoja tarvitaan yhteensä 44. Pidempää 192-bittistä avainta käytettäessä sanoja luodaan 52 ja 256-bittistä avainta käytettäessä luodaan 60 sanaa. Laajennuksessa käytetään kymmentä kierrosvakiota, jotka on esitetty taulukossa 1. Taulukkoon ja sen alkioon indeksillä j viitataan algoritmissa merkinnällä Rcon[j]. Taulukko 1. Avaimen laajennuksen kierrosvakiot (National Institute of Standards and Technology, 2001/2023, s. 17). j Rcon[j] j Rcon[j] 1 01, 00, 00, 00 6 20, 00, 00, 00 2 02, 00, 00, 00 7 40, 00, 00, 00 3 04, 00, 00, 00 8 80, 00, 00, 00 4 08, 00, 00, 00 9 1b, 00, 00, 00 5 10, 00, 00, 00 10 36, 00, 00, 00 18 Lisäksi laajennuksessa käytetään kahta apufunktiota. Näistä ensimmäinen, RotWord(), saa syötteenä neljän tavun jonon, ja se permutoi jonoa samoin kuin ShiftRows()- muunnos permutoi riviä indeksillä 1. Siis RotWord([a0, a1, a2, a3]) = [a1, a2, a3, a0]. Toinen apufunktio, SubWord(), saa myös syötteenä neljän tavun jonon ja se vaihtaa jokaisen taulukon S-laatikon mukaiseen tavuun. Toisin sanoen SubWord([a0, a1, a2, a3]) = [SBox(a0), SBox(a1), SBox(a2), SBox(a3)]. Avaimen laajennuksen toiminta on esitetty alla olevassa algoritmissa 2, missä Nk tarkoittaa kuinka monesta sanasta pääavain koostuu, toisin sanoen avaimen pituus jaetaan luvulla 32 ja tuloksena saadaan arvo Nk. AvaimenLaajennus(avain, kierrokset) i = 0 while i ≤ Nk -1 kierrosavaimet[i] = avain[4 * i…4 * i + 3] i++ while i ≤ 4 * (kierrokset + 1) temp = kierrosavaimet[i-1] if i mod Nk = 0 temp = SubWord(RotWord(temp)) ^ Rcon[i/Nk] else if Nk > 6 && i mod Nk = 4 temp = SubWord(temp) kierrosavaimet[i] = kierrosavaimet[i-Nk] ^ temp i++ return kierrosavaimet Algoritmi 2. Pseudokoodiesitys AES-algoritmin avaimen laajennuksesta (National Institute of Standards and Technology, 2001/2023, s. 18). On hyvä huomata, että algoritmin ensimmäisessä vaiheessa pääavaimen muodostavat sanat siirtyvät kierrosavaimeksi taulukkoon sellaisinaan. Loput kierrosavaimet muodostavat sanat johdetaan näistä sanoista algoritmin seuraavassa vaiheessa. Toinen huomattava seikka liittyy ehtoon if Nk > 6 && i mod Nk = 4, mikä tarkoittaa, että seuraavaa SubWord()-kutsua käytetään ainoastaan 256-bittisen avaimen tapauksessa. Muutoin laajennus tapahtuu kullekin avaimen pituudelle samalla tavalla. 19 2.5 Käänteisalgoritmi Jotta mistään salausalgoritmista olisi hyötyä, on sen oltava käännettävissä. Toisin sanoen on oltava olemassa mahdollisuus avata tehty salaus, muutoin tieto on iäksi menetetty. AES-algoritmissa käänteisyys toteutuu hyvin intuitiivisesti. Salauksessa tehdyt vaiheet korvataan niiden käänteismuunnoksilla ja ne toteutetaan käänteisessä järjestyksessä (National Institute of Standards and Technology, 2001/2023, luku 5.3). Nämä käänteismuunnokset esitellään seuraavissa luvuissa. Käänteisalgoritmin toiminta on kuvattu alla olevassa algoritmissa 3. AES_käänteisalgoritmi(syöte, kierrokset, avain) tila = syöte kierrosavaimet[kierrokset+1]=AvaimenLaajennus(avain) tila = AddRoundKey(tila, kierrosavaimet[kierrokset]) for kierros from kierrokset - 1 down to 1 tila = InvShiftRows(tila) tila = InvSubBytes(tila) tila = AddRoundKey(tila,kierrosavaimet[kierros]) tila = InvMixColumns(tila) tila = InvShiftRows(tila) tila = InvSubBytes(tila) tila = AddRoundKey(tila, kierrosavaimet[0]) return tila Algoritmi 3. Pseudokoodiesitys AES-käänteisalgoritmin rakenteesta (mukaillen National Institute of Standards and Technology, 2001/2023, s. 22). Ensisilmäyksellä saattaa näyttää siltä, että muunnosten järjestys ei vastaa alkuperäistä algoritmia käännettynä. Kuitenkin lopusta alkuun päin lukien huomataan, että ensin lisätään kierrosavain, jota seuraavat InvSubBytes(), InvShiftRows() ja silmukan sisällä InvMixColumns() sekä seuraava kierrosavaimen lisäys silmukan puolivälissä. Tästä edelleen taaksepäin lukien muunnokset seuraavat alkuperäisen algoritmin järjestystä. Viimeisellä kierroksella kierrosavaimen lisäyksen jälkeen suoritetaan vielä kaikki muunnokset lukuun ottamatta InvMixColumns()- muunnosta, aivan kuten alkuperäisessä algoritmissa. 20 Kierrosavaimen lisäys on oma käänteisoperaationsa, joten sitä käytetään käänteisalgoritmissa sellaisenaan. Tämä johtuu XOR-operaattorin liitännäisyydestä, eli (𝐴 ⊕ 𝐵) ⊕ C = 𝐴 ⊕ (𝐵 ⊕ C). Koska kierrosavain on kummassakin algoritmissa sama, voidaan merkitä 𝐴 ⊕ (𝐵 ⊕ B) = A ⊕ 0 = 𝐴 , missä A on algoritmin tila ja B kierrosavain. 2.5.1 InvShiftRows-muunnos InvShiftRows()-muunnos toimii kuten vastinparinsa ShiftRows()-muunnos. Tavuja vain siirretään tällä kertaa vasemmalta oikealle, kullakin rivillä yhtä monta askelta kuin aiemmin, siis rivin indeksin osoittama määrä. Tätä havainnollistetaan alla olevassa kuviossa 4. Syöte Vaste s0,0 s0,1 s0,2 s0,3 s0,0 s0,1 s0,2 s0,3 s1,0 s1,1 s1,2 s1,3 → s1,3 s1,0 s1,1 s1,2 s2,0 s2,1 s2,2 s2,3 s2,2 s2,3 s2,0 s2,1 s3,0 s3,1 s3,2 s3,3 s3,1 s3,2 s3,3 s3,0 Kuvio 4. InvShiftRows-muunnoksen toiminta (mukaillen National Institute of Standards and Technology, 2001/2023, s. 23). 2.5.2 InvSubBytes-muunnos InvSubBytes()-muunnos kääntää alkuperäisen SubBytes()-muunnoksen käyttämällä käänteistä S-laatikkoa (National Institute of Standards and Technology, 2001/2023, luku 5.3.2). Syötteen jokainen tavu vaihdetaan toiseen käänteisen S-laatikon osoittamaan tavuun. Se on muodostettu vaihtamalla S-laatikon syötteiden ja vasteiden paikkoja. Käänteinen S-laatikko löytyy liitteestä 2. 21 2.5.3 InvMixColumns-muunnos InvMixColumns()-muunnos käyttää myös vakiomatriisia, jolla kerrotaan tilan sarakevektorit (National Institute of Standards and Technology, 2001/2023, luku 5.3.3). Sarakkeen muuntaminen tapahtuu yhtälön (6) mukaisesti [ 𝑠′0 𝑠′1 𝑠′2 𝑠′3 ] = [ 0𝑒 0𝑏 0𝑑 09 09 0𝑒 0𝑏 0𝑑 0𝑑 09 0𝑒 0𝑏 0𝑏 0𝑑 09 0𝑒 ] [ 𝑠0 𝑠1 𝑠2 𝑠3 ] , (6) missä vakiomatriisi koostuu tavuista 09, 0b, 0d ja 0e. Avaamalla yllä oleva matriisiyhtälö saadaan yksittäiset vastetavut yhtälöistä (7) 𝑠′0 = (0𝑒 ⋅ 𝑠0) ⊕ (0𝑏 ⋅ 𝑠1) ⊕ (0𝑑 ⋅ 𝑠2) ⊕ (09 ⋅ 𝑠3), 𝑠′1 = (09 ⋅ 𝑠0) ⊕ (0𝑒 ⋅ 𝑠1) ⊕ (0𝑏 ⋅ 𝑠2) ⊕ (0𝑑 ⋅ 𝑠3), 𝑠′2 = (0𝑑 ⋅ 𝑠0) ⊕ (09 ⋅ 𝑠1) ⊕ (0𝑒 ⋅ 𝑠2) ⊕ (0𝑏 ⋅ 𝑠3) ja (7) 𝑠′3 = (0𝑏 ⋅ 𝑠0) ⊕ (0𝑑 ⋅ 𝑠1) ⊕ (09 ⋅ 𝑠2) ⊕ (0𝑒 ⋅ 𝑠3). AES_ekvivalentti_käänteisalgoritmi(syöte, kierrokset, avain) tila = syöte kierrosavaimet[kierrokset + 1 ] =AvaimenLaajennusEkv(avain) tila = AddRoundKey(tila, kierrosavaimet[kierrokset]) for kierros from kierrokset – 1 downto 1 tila = InvSubBytes(tila) tila = InvShiftRows(tila) tila = InvMixColumns(tila) tila = AddRoundKey(tila,kierrosavaimet[kierros]) tila = InvSubBytes(tila) tila = InvShiftRows(tila) tila = AddRoundKey(tila, kierrosavaimet[0]) return tila Algoritmi 4. Pseudokoodiesitys ekvivalentin AES-käänteisalgoritmin rakenteesta (mukaillen National Institute of Standards and Technology, 2001/2023, s. 25). 22 AvaimenLaajennusEkv(avain, kierrokset) i = 0 while i ≤ Nk -1 kierrosavaimet[i] = avain[4*i…4*i+3] i++ while i ≤ 4 * kierrokset + 3 temp = kierrosavaimet[i-1] if i mod Nk = 0 temp = SubWord(RotWord(temp)) ^ Rcon[i/Nk] else if Nk > 6 && i mod Nk = 4 temp = SubWord(temp) kierrosavaimet[i] = kierrosavaimet[i-Nk] ^ temp i++ for kierros from 1 to kierrokset -1 i = 4 * kierros kierrosavaimet[i…i +3] = InvMixColumns( kierrosavaimet[i…i+3] ) return kierrosavaimet Algoritmi 5. Pseudokoodiesitys ekvivalentin AES-käänteisalgoritmin avaimen laajennuksesta (mukaillen National Institute of Standards and Technology, 2001/2023, s. 25). 2.5.4 Ekvivalentti käänteisalgoritmi AES-algoritmi on mahdollista toteuttaa hakutaulujen avulla, jossa jokaisen kierroksen tulos haetaan muistissa sijaitsevasta hakutaulusta. Tällöin on tärkeää, että algoritmin epälineaarinen muunnos SubBytes() aloittaa kierroksen ja rivejä siirretään ennen MixColumns()-muunnosta (Daemen ja Rijmen, 1999, s. 19). Siksi edellä esitetty käänteisalgoritmi ei toimi hakutaulujen avulla tehdyn toteutuksen kanssa. Daemen ja Rijmen (1999, s. 19) suunnittelivat algoritminsa siten, että käänteisalgoritmi voidaan toteuttaa myös pitämällä käänteismuunnokset samassa järjestyksessä kuin niiden vastinparit esiintyvät algoritmissa. Tässä ekvivalentissa käänteisalgoritmissa on käytettävä sille suunniteltua avaimen laajennusta ja InvMixColumns()-muunnosta kaikille kierrosavaimille paitsi ensimmäiselle ja viimeiselle. Tätä ekvivalenttia 23 käänteisalgoritmia voidaan käyttää myös hakutaulutoteutuksen yhteydessä. Algoritmissa 4 on esitetty ekvivalentti käänteisalgoritmi ja sen käyttämä avaimen laajennus algoritmissa 5. 2.6 Toimintamoodit Edellä esitetty algoritmi kuvaa ainoastaan yhden lohkon matkan syötteestä vasteeksi. Lohkon 128 bittiä eivät riitä kuvaamaan kuin aivan lyhyitä informaation pätkiä ja usein on tarpeen salata paljon suurempia määriä tietoa. Tällöin AES-algoritmia tulee käyttää jossain toimintamoodissa, joka määrittelee kuinka salattavat lohkot linkittyvät toisiinsa. Toimintamoodi valitaan sen perusteella, mitä ominaisuuksia siltä halutaan, kuten luottamuksellisuutta, todentamista tai molempia. National Institute of Standards and Technology ylläpitää listausta hyväksytyistä toimintamoodeista. Osa niistä on tarkoitettu käytettäväksi ainoastaan AES-algoritmin kanssa. Joitakin puolestaan voi käyttää minkä tahansa hyväksytyn lohkosalaimen kanssa. 2.6.1 Luottamuksellisuuden tarjoavat toimintamoodit Lohkosalaimille on hyväksytty viisi erilaista toimintamoodia, jotka varmistavat tiedon luottamuksellisuuden (engl. confidentiality): Electronic Codebook (ECB), Cipher Block Chaining (CBC), Cipher Feedback (CFB), Output Feedback (OFB) sekä Counter (CTR) (National Institute of Standards and Technology, 2001). NIST (2006, s. 6) määrittelee luottamuksellisuuden informaatioon käsiksi pääsemiseen sekä paljastamiseen liittyvien valtuutettujen rajoitusten säilyttämiseksi. Tähän sisältyvät esimerkiksi keinot suojata henkilökohtaista yksityisyyttä sekä omistettuja tietoja. 24 2.6.2 Todentamiseen soveltuva toimintamoodi Todentaminen (engl. authentication) tarkoittaa käyttäjän, prosessin tai laitteen identiteetin varmistamista, ja usein se on tehtävä ennen pääsyn myöntämistä informaatiojärjestelmän resursseihin (National Institute of Standards and Technology, 2006, s. 6). Tätä tarkoitusta varten on määritelty symmetrisille lohkosalaimille toimintamoodi nimeltään CMAC (cipher-based message authentication code) (National Institute of Standards and Technology, 2005). Tämän toimintamoodin avulla voidaan varmistaa, että vastaanotettu tieto on peräisin siitä lähteestä, josta se väittää tulevansa. Samalla varmistetaan tiedon eheys (engl. integrity), joka tarkoittaa, ettei sitä ole sopimattomasti muunnettu lähettämisen jälkeen (National Institute of Standards and Technology, 2006, s. 7). 2.6.3 Todentamisen ja luottamuksellisuuden yhdistävät toimintamoodit Molemmat edellä mainitut ominaisuudet yhdistyvät CCM-toimintamoodissa (Counter with Cipher Block Chaining-Message Authentication Code), joka yhdistää tekniikoita CTR- ja CBC-MAC-moodeista (National Institute of Standards and Technology, 2004/2007). Sitä voidaan käyttää sellaisten symmetristen lohkosalainten kanssa, joiden lohkon pituus on 128 bittiä. Edellä mainitut ominaisuudet pätevät myös moodiin nimeltä Galois/Counter Mode (GCM) (National Institute of Standards and Technology, 2007). Tässä moodissa salauksessa käytetään salattavan tekstin lisäksi ylimääräistä todennettua dataa (AAD, additional authenticated data), jota ei salata. Algoritmi tuottaa salatun tekstin lisäksi tunnisteen, jonka avulla vastaanottaja voi varmistaa viestin eheyden. Jos viestin osia ei tarvitse salata vaan pelkkä tunnisteen luominen riittää, toimintamoodia kutsutaan nimellä GMAC. 25 2.6.4 Muut toimintamoodit Laitteiden muistiin pitkäaikaisesti tallennettavaan tietoon kohdistuu erilaisia hyökkäyksen uhkia kuin tiedonsiirtoon liittyvään dataan. Tietoa pitkäksi aikaa tallentaville laitteille on määritelty oma luottamuksellisuuden takaava toimintamoodi XTS-AES (Institute of Electrical and Electronics Engineers, 2007/2018), jonka NIST hyväksyi käyttöön lisäten yhden vaatimuksen salattavan tiedon pituuden rajoittamiseksi (National Institute of Standards and Technology, 2010). Tässä moodissa yhden datayksikön salausavain koostuu kahdesta 128- tai 256-bittisestä avaimesta. Sama lohko salataan AES-algoritmilla kertaalleen kummallakin avaimella. Lisäksi moodissa käytetään jokaiselle datayksikölle nollasta eroavaa kokonaislukuarvoa, joka voi liittyä esimerkiksi datan fyysiseen sijaintiin tallennuslaitteella. Salausavaimien luottamuksellisuuden ja eheyden suojaamiseksi on kehitetty toimintamoodeja, joita kutsutaan avaimen käärimiseksi (National Institute of Standards and Technology, 2012). Näillä moodeilla voidaan salata salausavaimia esimerkiksi niiden siirtoa varten. Luonnollisesti salaamiseen käytetään muita avaimia. Avaimen käärimismoodit laajentavat syötteen siten, että salattu vaste on pitempi kuin syöte, mikä vaikeuttaa salatun avaimen selvittämistä pelkän vasteen avulla. Nämä toimintamoodit ovat nimeltään AES Key Wrap (WP), AES Key Wrap With Padding (KWP) sekä Triple DEA Key Wrap (TKW), jota käytetään Triple Data Encryption Algorithm -nimisen algoritmin kanssa. Kaikki edelliset toimintamoodit käsittelevät dataa binäärisinä bittijonoina. Joissain tapauksissa data halutaan säilyttää jossain muussa muodossa eikä sen rakennetta haluta muuttaa, kuten lohkosalain saattaisi tehdä. Esimerkiksi yhdysvaltalainen sosiaaliturvanumero (Social Security Number, SSN) on yhdeksän numeron jono. Se voidaan käsitellä bittijonona ja salata lohkosalaimella, mutta vasteena voidaan saada bittijono, jonka esittämiseen tarvitaan enemmän kuin yhdeksän numeroa. Tähän tarkoitukseen soveltuu muotoilun säilyttävä salaus (Format-preserving encryption, FPE), 26 joka perustuu Feistel-rakenteelle (National Institute of Standards and Technology, 2016). Näitä toimintamoodeja ovat FF1 ja FF3. 2.7 Viestin täyte AES-algoritmin lohkon pituus on aina 128 bittiä. Ideaalitilanteessa salattavan viestin pituus on lohkon pituuden monikerta, mutta näin ei aina ole. Viimeinen lohko voi jäädä vajaaksi, kun viesti jaetaan 128-bittisiin lohkoihin. Joillain toimintamoodeilla tämä ei ole ongelma, vaan niitä voidaan käyttää vajaillakin lohkoilla, koska ne täyttävät lohkot sisäisen rakenteensa avulla. Toisista moodeista tämä ominaisuus puuttuu, kuten esimerkiksi ECB-, CBC- ja CFB-moodeista. Näihin tilanteisiin sovelletaan syötteen täyttämistä (engl. padding), jossa viimeinen lohko täydennetään 128-bitin mittaiseksi jollain ennalta sovitulla tavalla (National Institute of Standards and Technology, 2001, s. 17). Yleinen tapa täyttää viesti on lisätä viestin viimeisen bitin jälkeen yksi 1b ja sen jälkeen niin monta 0b, että vaadittu lohkon pituus täyttyy (National Institute of Standards and Technology, 2001, s. 17). Paterson ja Yau (2004, s. 309–310) tutkivat ISO-standardissa määriteltyjä täytemetodeja CBC-moodissa käytettynä, ja edellä mainittu metodi ei ole haavoittuvainen täyteoraakkelihyökkäyksille (engl. padding oracle attack), koska lohko on väärin täytetty ainoastaan silloin, kun se sisältää pelkästään nollabittejä. Hyökkäyksessä hyökkääjällä on käytössään täyteoraakkeli, joka kertoo, onko sille syötetty lohko oikealla tavalla täytetty. Reaalitilanteessa esimerkiksi serverin vastaus sille esitettyyn pyyntöön voi toimia hyökkääjälle täyteoraakkelina, jolla käytännöllinen hyökkäys on toteutettavissa. Toinen tapa täyttää viesti on täyttää viimeinen lohko pelkästään nollabiteillä. Tätä täytemetodia vastaan ei voida hyökätä täyteoraakkelin avulla, koska mikä tahansa lohko on sen mukainen (Paterson ja Yau, 2004, s. 309). Sen ongelma on siinä, että useampi erilainen vajaa lohko voidaan täyttää samanlaiseksi, joten voi olla haastavaa valita näistä 27 oikea salauksen purkamisen jälkeen. Tämä koskee erityisesti lohkoja, joiden viimeiset bitit ovat nollia, sillä täyttäessä ei lisätä bittiä 1b merkitsemään täytteen alkamista. Kolmannessa metodissa viesti täytetään nollabiteillä vaaditun lohkon kokoon saakka. Tämän lisäksi viestin alkuun lisätään uusi lohko, jonka viimeiset bitit kertovat seuraavissa lohkoissa sijaitsevan viestin pituuden täyttämättömänä. Lohkon ensimmäinen nollasta eroava bitti ja sitä seuraavat bitit lohkon päättymiseen saakka ovat siis alkuperäisen täyttämättömän viestin pituuden binääriesitys. Paterson ja Yau (2004, s. 310–315) esittävät kuitenkin hyökkäyksen tätä metodia vastaan, jolla täyteoraakkelin avulla minkä tahansa viestin salaus saadaan avattua 𝑛 + 𝑂(𝑙𝑜𝑔2𝑛) oraakkelikutsulla lohkoa kohti, missä 𝑛 on lohkon pituus. 28 3 Rust-ohjelmointikieli Rust alkoi ohjelmistokehittäjä Graydon Hoaren henkilökohtaisena projektina vuonna 2006, jota hänen työnantajansa Mozilla lähti myöhemmin tukemaan. Hänen motivaationsa kielen kehittämiseen alkoi hissistä, jota ohjaava ohjelma oli kaatunut. Hoare tiesi kyseisten ongelmien johtuvan usein ongelmista muistinhallinnassa, mikä on yleinen ongelma C- tai C++-ohjelmointikielissä, joita useissa sulautetuissa laitteissa käytetään. Kielen ensimmäinen julkaisuversio Rust 1.0 näki lopulta päivänvalon toukokuussa 2015. Helmikuusta 2021 alkaen kieltä ja sen ekosysteemiä on hallinnoinut Rust Foundation, jonka perustivat yhdessä AWS, Huawei, Google, Microsoft ja Mozilla (Rust Foundation, 2021). Rust-kielen kehitystä ovat innoittaneet mm. C-kielen turvallinen muunnos Cyclone, C++ sekä Haskell (Sharma ja muut, 2019, s. 8). 3.1 Erityispiirteet Rust on monipuolinen ohjelmointikieli ja se sisältää vaikutteita funktionaalisista, imperatiivisista sekä oliopohjaisista kielistä. Moni Rustin ominaisuus on olemassa muussakin ohjelmointikielessä, mutta näiden kaikkien ominaisuuksien summa tekee siitä ainutlaatuisen. Rust on suunniteltu estämään tyyppi- ja muistivirheitä ja sen suunnitteluperiaatteiden avulla kielen muistinhallinta tapahtuu ilman roskienkeruuta (Balasubramanian ja muut, 2017, s. 156). Näiden ominaisuuksien vuoksi kielessä on tiukkoja sääntöjä, jotka eroavat suuresti muista ohjelmointikielistä, ja ne tekevät Rust- kielen oppimisesta aluksi vaikeaa (Fulton ja muut, 2021, s. 603). Rust on avoimen lähdekoodin kieli, jonka kehitykseen kuka tahansa voi osallistua ehdottamalla uusia ominaisuuksia toteutettavaksi kieleen (Sharma ja muut, 2019, s. 9). Rust-kielessä funktio voi palauttaa arvon ilman return-avainsanaa (Sharma ja muut, 2019, s. 24). Kielen syntaksissa lausekkeet päättyvät puolipisteeseen, kuten esimerksi C- ja Java-kielissä. Funktion viimeisestä lausekkeesta puolipiste voidaan kuitenkin jättää pois, jolloin funktio palauttaa kutsujalleen kyseisen lausekkeen arvon, esimerkiksi 29 kokonaislukujen summan a + b. Return sanaa voi silti käyttää samoin kuin C- tai Java- kielessä, mutta se ei ole pakollista. Jos funktiolle ei ole määritelty paluuarvon tyyppiä, ts. se ei palauta mitään erillistä arvoa, se palauttaa niin sanotun yksikkötyypin, jota merkitään symbolilla (). Sen lähin vastine on C- ja C++-kielissä käytetty void-tyyppi (Sharma ja muut, 2019, s. 24). 3.1.1 Muuttujat ja vakiot Klabnik & Nichols (2023, luku 3.1) esittelevät kirjassaan kattavasti Rust-kielen muuttujien ominaisuuksia ja periaatteita. Yksi näistä keskeisistä periaatteista on se, että muuttujat ovat oletusarvoisesti muuttumattomia. Tämä koskee sekä tavallisia muuttujia että viittauksia niihin. Käyttäjän on muuttujaa määritellessään erikseen tehtävä muuttujasta muokattava käyttämällä avainsanaa mut tai muuten kääntäjä antaa virheilmoituksen havaitessaan muuttujan muokkausyrityksen ohjelmassa. Tämä pakottaa käyttäjän miettimään milloin on tarkoituksenmukaista käyttää muokattavaa muuttujaa ja toisaalta ohjelma ei voi vahingossa muokata muuttumatonta muuttujaa. Vakiot ovat kuin muuttumattomat muuttujat, mutta niiden välillä on muutama ero (Klabnik & Nichols, 2023, luku 3.1). Vakion määrittelyssä käytetään avainsanaa const ja vakion tyyppi on aina määriteltävä erikseen. Muuttujan tapauksessa tyyppimäärittely voidaan usein jättää tekemättä, sillä kääntäjä osaa joitain poikkeuksia lukuun ottamatta päätellä muuttujan tyypin kontekstista. Toinen erottava tekijä on se, että vakion määrittelevän lausekkeen arvo tulee aina olla tiedossa jo kääntämisvaiheessa. Se ei siis voi riippua ohjelman ajon aikana laskettavasta arvosta, mikä on mahdollista muuttujan tapauksessa. 30 3.1.2 Tietotyypit Kirjassaan Klabnik ja Nichols (2023, luku 3.2) toteavat, että Rust-kielessä jokainen arvo edustaa jotain tiettyä tietotyyppiä. Koska Rust on staattisesti tyypitetty kieli, jokaisen arvon tyyppi on oltava tiedossa jo kääntämisvaiheessa. Kuten mainittua, kääntäjä osaa usein päätellä tietotyypin kontekstin perusteella. Rust-kielessä on neljä erilaista skalaariarvoa, joita ovat kokonaisluvut, liukuluvut, boolean-arvot sekä merkit (Klabnik & Nichols, 2023, luku 3.2). Kukin niistä esittää tasan yhtä arvoa. Kokonaisluku voi olla etumerkillinen tai etumerkitön ja sen pituus bitteinä voidaan määritellä olevan 8, 16, 32, 64 tai 128 bittiä. Etumerkillistä kokonaislukutyyppiä merkitään i-kirjaimella, jota seuraa luvun pituus bitteinä. Vastaavasti etumerkittömän luvun pituutta edeltää u-kirjain. Rust-kieli tarjoaa myös käytettävän tietokoneen arkkitehtuurista riippuvan kokonaislukutyypin etumerkillisenä tai etumerkittömänä, isize ja usize. Näiden pituudet ovat joko 32 tai 64 bittiä riippuen siitä, millaista arkkitehtuuria se tietokone käyttää, jolla Rust-ohjelma ajetaan. Liukulukuja puolestaan on kahta tyyppiä, 32-bittinen f32 sekä 64-bittinen f64. Kuten useissa muissakin kielissä, boolean-arvot ovat Rust-kielessä true ja false. Tyyppi määritellään kielessä sanalla bool ja se vie muistia yhden tavun verran. Merkki puolestaan vie muistitilaa neljä tavua ja Rust tulkitsee sen Unicode-merkistön mukaisesti. Merkeillä voidaan siis ilmaista esimerkiksi kiinalaisia erikoismerkkejä sekä emojeja (Klabnik & Nichols, 2023, luku 3.2). Rust sisältää myös kaksi primitiivistä yhdistelmätyyppiä, monikon ja taulukon (Klabnik & Nichols, 2023, luku 3.2). Monikko on tyyppi, joka voi sisältää nolla tai enemmän erilaisten tyyppien arvoja. Samassa monikossa voi siis aivan hyvin olla sekaisin erilaisia lukuja, boolean-arvoja sekä merkkejä. Monikolla, joka ei sisällä yhtään arvoa, on Rust-kielessä erityismerkitys. Sitä kutsutaan yksikkötyypiksi, ja se on lausekkeiden implisiittinen palautusarvo, mikäli muuta arvoa ei ole määritelty. 31 Taulukko on kokoelma yhdentyyppisiä arvoja ja Rust-kielessä taulukon pituus on muuttumaton. Sen arvoihin viitataan indeksillä, kuten monissa muissa kielissä. Jos käyttäjä yrittää viitata taulukon alkioon viallisella indeksillä, Rust keskeyttää ohjelman suorittamisen välittömästi (Klabnik & Nichols, 2023, luku 3.2). Rust-kielessä ei siis ole mahdollista päästä käsiksi taulukon avulla sen ulkopuolella olevaan tietoon viittaamalla taulukon ulkopuolelle menevään indeksiin. Käyttäjä voi määritellä omia tietueita (engl. struct), jotka voivat sisältää useita erityyppisiä arvoja (Klabnik & Nichols, 2023, luku 5). Tietue koostuu nimetyistä kentistä, joiden tyypit käyttäjä määrittelee. Sen kenttiin viitataan määritetyllä nimellä eikä indeksillä, kuten monikossa. Myös koko tietue nimetään, toisin kuin monikko, jolla ei ole erillistä nimeä. On myös mahdollista käyttää näiden tietorakenteiden välimuotoa, monikkotietuetta, jolle annetaan nimi, mutta sen kenttiä ei nimetä. Sen alkioihin viitataan indeksillä, kuten tavallisen monikon tapauksessa. Sharma ja muut (2019, s. 34) suosittelevat, että monikkotietuetta käytetään korkeintaan neljä tai viiden kentän tapauksissa, koska sitä useampi kenttä haittaa lähdekielisen ohjelman luettavuutta. Rust-kielen tietue muistuttaa esimerkiksi C-kielen tietuetta. Sille voidaan lisäksi määritellä omia metodeja, jollaisia käytetään esimerkiksi muissa olio-ohjelmointiin soveltuvissa kielissä. Metodien parametreina voidaan käyttää tietuetta tai viittausta siihen. Käyttäjän ei kuitenkaan tarvitse välittää kumpaa metodi käyttää, sillä tässä tapauksessa kääntäjä osaa päätellä oikean tyypin ja suorittaa automaattisen viittauksen tai viittauksen poistamisen (Klabnik & Nichols, 2023, luku 5.3). Arvojoukko (engl. enumeration) on toinen käyttäjän määriteltävissä oleva tietorakenne (Klabnik & Nichols, 2023, luku 6). Rust-kielessä arvojoukko määrittelee kaikki ne tyypit, joita arvojoukon alkio voi edustaa. Toisin sanoen arvojoukko on listaus mitä tahansa tietotyyppejä ja kyseistä arvojoukkoa oleva muuttuja voi olla mitä tahansa näistä tyypeistä. Arvojoukko voi sisältää esimerkiksi erilaisia lukutyyppejä, merkkijonoja tai kokoelmatyyppejä, jotka puolestaan sisältävät muita tyyppejä. 32 Yksi yleisesti muissa kielissä esiintyvä ominaisuus on jätetty Rust-kielestä kokonaan pois jo suunnitteluvaiheessa. Se ei nimittäin sisällä niin sanottua null-arvoa, eli arvoa, joka tarkoittaa, ettei kyseisessä muistiosoitteessa ole mitään mielekästä tai merkityksellistä arvoa (Klabnik & Nichols, 2023, luku 6.1). Null-arvon käyttäminen oikean ja mielekkään arvon sijasta johtaa helposti muistivirheisiin tai haavoittuvuuksiin, koska usein on käyttäjän vastuulla varmistaa, että muistiviittaukset ovat oikein. Rust-kielessä null-arvoa ei ole, jolloin olemattomia viittauksia ei pääse tapahtumaan. Sen sijaan kielen standardikirjastosta löytyy Option-arvojoukko, johon kuuluu kaksi tyyppiä: Some(T) ja None (Klabnik & Nichols, 2023, luku 6.1). Some(T) on kääretyyppi, joka sisältää minkä tahansa geneerisen tyypin muuttujan, ja siihen on käärittynä olemassa oleva arvo. None puolestaan ilmaisee, että haluttua arvoa ei ole olemassa. Useat standardikirjaston funktiot käyttävät Option-arvojoukkoa palatusarvotyyppinä, mikä pakottaa käyttäjän käsittelemään palautusarvon mielekkäällä tavalla. Some(T) ja None ovat omia tietotyyppejään, joten niitä ei voi suoraan käyttää esimerkiksi laskennassa. Näin ollen vahingossa tapahtuvia muistiviittauksia olemattomilla arvoilla ei pääse tapahtumaan. 3.1.3 Omistajuus Ominaisin Rust-kielen piirteistä on sen omistajuusmalli, joka luo pohjan kielen väitteille sen muistiturvallisuudesta (Klabnik & Nichols, 2023, luku 4). Tämä omistajuusmalli koostuu säännöistä, jotka määrittävät Rust-ohjelman muistinhallintaa. Kääntäjä valvoo näiden sääntöjen noudattamista, eikä ohjelma käänny, mikäli niitä rikotaan. Kuitenkaan ohjelman suorittaminen ei hidastu sääntöjen takia. Klabnik ja Nichols (2023, luku 4.1) toteavat, että omistajuusmalliin tottuminen kestää aikansa, koska se on uusi monille ohjelmoijille. Tämä ajatusmallin muutos on joidenkin käyttäjien mielestä koko kielen vaikein piirre omaksua (Fulton ja muut, 2021, s. 603–604). Ohjelman sisältämiä muuttujia voidaan tallentaa muistissa joko pinoon tai kekoon. Muuttuja voidaan tallentaa pinoon ainoastaan silloin, kun sen koko on tiedossa käännösvaiheessa, eikä se tule muuttumaan (Klabnik & Nichols, 2023, luku 4.1). Tällaisia 33 ovat esimerkiksi kaikki lukutyypit. Jos muuttujan koko voi muuttua, tai se ei ole tiedossa käännösvaiheessa, se tallennetaan kekoon. Muuttujan tallennuspaikalla on vaikutusta ohjelman muistinhallintaan. Omistajuusmallin kolme pääsääntöä ovat seuraavat (Klabnik & Nichols, 2023, luku 4.1): • jokaisella arvolla on omistaja, • kullakin arvolla voi olla vain yksi omistaja kerrallaan, • arvo pudotetaan, kun sen omistaja lakkaa olemasta voimassa. Kun uusi muuttuja luodaan ohjelman ajon aikana, sille varataan tietty määrä muistia keosta, joka pitää myöhemmin vapauttaa. Rust-ohjelmassa tämä tapahtuu automaattisesti siinä vaiheessa, kun muistin omistavan muuttujan voimassaolo päättyy. Tällöin ohjelma kutsuu implisiittisesti drop-funktiota, joka vapauttaa kyseisen muistialueen takaisin ohjelman käytettäväksi. Jos muuttuja on tallennettu kekoon, pinossa on ainoastaan muistiviittaus siihen sekä tiedot muuttujan pituudesta ja varatun muistialueen koosta. Oletetaan nyt, että Rust- ohjelmassa on luotu jokin muuttuja v, joka on tallennettu kekoon. Jos tämä muuttuja sidotaan uuteen muuttujaan w, Rust-ohjelma siirtää muuttujan v tiedot muuttujalle w, mikä liittyy edellä mainittuun toiseen sääntöön. Tämä tarkoittaa sitä, että w omistaa tästä eteenpäin muistiviittauksen kekoon, muuttujan pituuden ja muistialueen koon, jotka oli aiemmin liitetty muuttujaan v, joka puolestaan mitätöidään. Tällä estetään se, että molemmat muuttujat viittaisivat samaan muistialueeseen keossa yhtä aikaa, jolloin ne voisivat vapauttaa saman muistialueen kahdesti lakatessaan olemasta voimassa ja aiheuttaa ohjelmassa ajonaikaisen virheen (Klabnik & Nichols, 2023, luku 4.1). Rust-ohjelma voi helposti kopioida pinoon tallennettuja muuttujia implisiittisesti, silloin kun sitä tarvitaan (Klabnik & Nichols, 2023, luku 4.1). Keossa oleva muuttuja voidaan kloonata, kun käyttäjä sitä eksplisiittisesti pyytää kutsumalla clone-metodia. Tällöin 34 kekoon tallennetaan kopio halutun muuttujan tiedoista, ja kaksi muuttujaa voivat viitata samansisältöiseen tietoon, jotka kuitenkin sijaitsevat eri muistialueilla eikä toinen sääntö päde tähän tilanteeseen. Jos muuttuja annetaan parametrina funktiolle, se kopioidaan tai siirretään edellä mainittujen periaatteiden mukaisesti (Klabnik & Nichols, 2023, luku 4.1). Pinossa olevista muuttujista välitetään funktiolle kopio, mutta keossa olevien muuttujien omistajuus siirretään funktiolle itselleen. Funktion parametrit ja sisäiset ja muuttujat lakkaavat olemasta voimassa funktion jälkeen, jolloin ne pudotetaan. Muuttuja voidaan palauttaa tarvittaessa takaisin funktion paluuarvon kautta. Muuttuja voidaan myös lainata funktion käytettäväksi antamatta kuitenkaan sille omistajuutta muuttujaan (Klabnik & Nichols, 2023, luku 4.2). Tämä tapahtuu antamalla funktiolle viittaus muuttujaan, jota merkitään Rust-kielessä asettamalla ampersandi (&) muuttujan nimen eteen. Viittaus voidaan tehdä ainoastaan olemassa olevaan muuttujaan, mikä osaltaan myös ehkäisee mahdollisia muistinhallinnan virheitä. Viittaukset ovat muuttujien tapaan oletusarvoisesti muuttumattomia eli funktio saa ainoastaan lukuoikeuden muuttujan sisältämään tietoon. Käyttäjän on itse määriteltävä viittaus muuttuvaksi avainsanalla mut, jotta funktio voisi myös muokata viittauksen takana olevan muuttujan arvoa. Muistinhallinnan ongelmat syntyvät siitä, kun useampi kuin yksi muuttuja haluaa käsitellä samaa muistissa olevaa tietoa samanaikaisesti. Rust estää tällaisten kilpailutilainteiden syntyä valvomalla kahta viittauksiin liittyvää sääntöä (Klabnik & Nichols, 2023, luku 4.2): • samaan muuttujaan voi samanaikaisesti viitata joko tasan yksi muuttuva viittaus tai mikä tahansa määrä muuttumattomia viittauksia, • viittausten on aina osoitettava voimassa oleviin muuttujiin. 35 Mikäli nämä säännöt eivät ohjelmassa täyty, kääntäjä antaa siitä virheilmoituksen. Esimerkkitapaus on sellainen, jossa muuttuja lakkaa olemasta voimassa, mutta ohjelmassa on myöhemmin viittaus siihen. Kääntäjä ilmoittaa tällaisesta virheestä ja pysäyttää käännöksen saman tien. 3.1.4 Unsafe Rust Kääntäjän tiukka muistiturvallisuuteen liittyvien sääntöjen valvonta estää tiettyjen ohjelmointitapojen ja -ominaisuuksien käytön. Joidenkin tehtävien suorittamiseksi tällaisia on pakko käyttää ja Rust-kieli tarjoaa mahdollisuuden kiertää osaa säännöistä. Tämä tapahtuu unsafe-lohkojen avulla, joiden sisällä kääntäjä jättää tarkastamatta nämä säännöt, ja vastuu muistiturvallisuudesta on tällöin käyttäjällä (Klabnik & Nichols, 2023, luku 19.1). Itse asiassa jopa osa Rust-kielen standardikirjastosta on kirjoitettu tarkastetuilla unsafe-lohkoilla ja ne on vain kääritty turvallisiin rajapintoihin. Unsafe-lohkon sisällä voi käyttää viittä toimintoa, joita kääntäjä ei tavallisesti hyväksyisi. Nämä toiminnot ovat (Klabnik & Nichols, 2023, luku 19.1): • osoitinviittaukset, • unsafe-funktio- tai -metodikutsu, • muokattavan staattisen muuttujan käsittely ja muokkaus, • unsafe trait -ominaisuuden toteuttaminen, • unioni-tietotyypin kenttien käsittely. Kaikki muut kielen turvaominaisuudet pysyvät edelleen voimassa, joten unsafe- lohkossakaan ei voi tehdä aivan mitä tahansa. Kääntäjä tarkastaa edelleen esimerkiksi kaikki viittaukset. Osoitinviittauksen luominen ei vaadi unsafe-lohkon käyttämistä. Osoittimen viittaamassa muistiosoitteessa olevan arvon käyttäminen sen sijaan ei onnistu ilman sitä 36 (Klabnik ja Nichols, 2023, luku 19.1). Osoittimet voivat olla muokattavia tai muuttumattomia, kuten muutkin muuttujat. Niiden ei tarvitse noudattaa Rust-kielen viittaussääntöjä, vaan samaan arvoon voidaan viitata usealla muokattavalla viittauksella tai muokattavalla ja muuttumattomalla viittauksella samaan aikaan. Osoittimien ei voida taata viittaavan voimassa oleviin arvoihin ja ne voivat olla null-arvoisia. Osoittimien avulla käyttäjä voi luoda tehokkaampia algoritmeja ollen samalla itse vastuussa muistiturvallisuudesta. Rust-kielen funktion tai metodin määrittelyssä voidaan aloittaa käyttämällä unsafe- avainsanaa (Klabnik ja Nichols, 2023, luku 19.1). Tällöin kyseistä funktiota tai metodia voidaan kutsua ainoastaan unsafe-lohkon sisällä. Ilman tätä kääntäjä ilmoittaa, ettei se voi taata kyseisen funktion tai metodin toimintaa. Kutsumalla funktiota tai metodia unsafe-lohkossa, käyttäjä ottaa sen toiminnan omalle vastuulleen. Unsafe-lohkon sisältävää funktiota voidaan kuitenkin kutsua ilman unsafe-lohkoa, jolloin kyseinen funktio toimii turvallisena abstraktiona tälle osalle ohjelmaa. Globaaleja muuttujia kutsutaan Rust-kielessä staattisiksi muuttujiksi. Muuttumaton staattinen muuttuja toimii lähes kuten vakio. Erona on vain se, että muuttuja käyttää aina samaa arvoa tietyssä muistiosoitteessa, kun taas vakion arvo voidaan tarvittaessa kopioida muualle (Klabnik ja Nichols, 2023, luku 19.1). Muokattava staattinen muuttuja voi aiheuttaa kilpailutilanteita, jonka takia sen käyttäminen vaatii unsafe-lohkon käyttämistä. Klabnik ja Nichols (2023, luku 19.1) suosittelevat käyttämään niiden sijasta esimerkiksi rinnakkaisten säikeiden käsittelyyn soveltuvia Rust-kielen älykkäitä osoittimia, joihin ei syvennytä tässä työssä. Piirre tai ominaisuus (engl. trait) on Rust-kielessä muiden kielten rajapintoja lähes vastaava ominaisuus (Klabnik ja Nichols, 2023, luku 19.1). Piirre määrittelee metodeja, joilla saadaan aikaan tietty toiminnallisuus. Erilaiset tietotyypit, kuten tietueet, voivat toteuttaa näitä piirteitä, jotka voivat olla unsafe-avainsanalla määriteltyjä. Tällöin niiden 37 toteutuksessa on myös käytettävä unsafe-avainsanaa ja samalla ilmaistava kääntäjälle, että vastuu siirretään käyttäjälle. Unioni näyttää ulkoisesti tietueelta, mutta sen kentistä ainoastaan yksi on kerrallaan voimassa. Koska kääntäjä ei voi taata käännösvaiheessa mikä kenttä on kulloinkin voimassa, unionin kenttien käsittely on tehtävä unsafe-lohkossa (Klabnik ja Nichols, 2023, luku 19.1). 3.2 Rust-kielen kehitysympäristötyökalut Rust tarjoaa käyttäjälleen helppokäyttöisiä työkaluja, jotka helpottavat kehitystyötä. Käyttäjiltä kiittävää palautetta saa eritoten kääntäjä, jonka informatiiviset virheviestit helpottavat vianetsintää (Fulton ja muut, 2021, s. 604). Monissa tapauksissa kääntäjä osaa kertoa tarkasti missä ongelma on ja jopa tarjota ratkaisuehdotuksen siihen. Käyttäjät kehuvat lisäksi Rustin virallista dokumentaatiota. Kehitysympäristön puutteina käyttäjät kokevat muita kieliä suuremman kääntämiseen kuluvan ajan sekä kielen nuoresta iästä johtuvan kirjastojen puutteen. 3.2.1 Rustup Rust-kehitysympäristön asentaminen tapahtuu rustup-työkalun avulla, jolla hallinnoidaan Rustin versioita ja siihen liittyviä työkaluja (Klabnik & Nichols, 2023, luku 1.1). Se on komentorivityökalu, joka on ladattavissa ilmaiseksi Windows- käyttöjärjestelmälle Rust-kielen kotisivuilta ja Linux- sekä macOS-käyttöjärjestelmille suoraan terminaalissa curl-komennolla. Linux- ja macOS-asennukset tarvitsevat lisäksi erillisen linkkeriohjelman. Rustup-työkalulla asennetaan kääntäjä (Sharma ja muut, 2019, s.15). Sen lisäksi sillä asennetaan valmiiksi käännetty Rust-kielen standardikirjasto sekä paketinhallintatyökalu 38 Cargo ja tarvittaessa muita työkaluja, kuten kielen formatointityökalu rustfmt, Rust Language server, ynnä muita työkaluja. Asennukseen kuuluu myös standardikirjaston dokumentaatio. Asennuksen jälkeen rustup-työkalua voi käyttää asennettujen työkalujen päivittämiseen, mukaan lukien sen itsensä. 3.2.2 Cargo Cargo on Rustin monitoimityökalu. Se hallinnoi käännösympäristöä sekä toimii kielen paketinhallintaohjelmana (Klabnik & Nichols, 2023, luku 1.3). Cargolla luodaan uusia Rust-projekteja ja oletuksena se luo esimerkkiprojektin, joka tulostaa terminaaliin ”Hello, world!” (Sharma ja muut, 2019, s. 60). Tämän tai minkä tahansa muun Rust-kielellä kirjoitetun ohjelman voi kääntää joko suoraan kutsumalla kääntäjää, tai antamalla Cargon hoitaa kaiken. Sen avulla käyttäjä voi valita kääntää ohjelman, kääntää ja suorittaa sen tai vain tarkistaa, että ohjelman kääntäminen onnistuu, mutta suoritettavaa ohjelmaa ei muodosteta. Jos käyttäjä valitsee kääntää ja suorittaa ohjelman, mutta ohjelman tiedostoissa ei ole tapahtunut muutoksia edellisen kääntämisen jälkeen, Cargo osaa huomioida tämän ja pelkästään suorittaa ohjelman kääntämättä sitä uudestaan. Cargon konfigurointi tapahtuu Cargo.toml-tiedostossa, joka on TOML-formaatissa (Tom’s Obvious, Minimal Language). Tässä tiedostossa määritellään kyseisen ohjelman nimi, tai paketin, kuten sitä Rust-ympäristössä kutsutaan (Klabnik & Nichols, 2023, luku 1.3). Myös paketin versionumero sekä painos määritellään siinä. Toinen tärkeä konfiguraatiotiedoston osa on listaus ulkopuolisista paketeista, joita kyseinen ohjelma tarvitsee toimiakseen. Näitä paketteja kutsutaan laatikoiksi (engl. crate), ja Cargo lataa ne automaattisesti heti kun niitä ensimmäisen kerran tarvitaan. Muissa ohjelmointikielissä Rustin laatikko vastaa ulkopuolista kirjastoa. Cargoa on kuitenkin kritisoitu siitä, että se ohjaa paisuttamaan ohjelmia liiallisesti näiden laatikkoriippuvuuksien kautta (Fulton ja muut, 2021, s. 604). Tämä voi olla ongelmallista 39 etenkin ohjelman turvallisuuden kannalta, sillä jokainen laatikko lisää mahdollista hyökkäyspintaa ohjelmaan. Cargoon on sisäänrakennettu mahdollisuus ajaa ohjelmaa testiympäristössä (Sharma ja muut, 2019, s. 63–65). Lähdekielisen ohjelman kanssa samaan tiedostoon voi kirjoittaa yksikkötestejä, jolla testataan samassa tiedostossa olevia funktioita. Ne tulee merkitä #[test]-attribuutilla, jolloin kääntäjä ottaa ne osaksi ohjelmaa ainoastaan testimoodissa (Sharma ja muut, 2019, s. 87–88). Integraatiotestit eroavat yksikkötesteistä siinä, että kääntäjä odottaa niiden sijaitsevan tests-hakemistossa (Sharma ja muut, 2019, s. 92). Integraatiotestien tarkoituksena on testata ohjelman toimintaa käyttäjän näkökulmasta, kun taas yksikkötestit testaavat yksittäisiä osia ohjelmasta. 3.3 Kryptografiset kirjastot Rust-kielen laatikot ovat saatavilla crates.io -palvelussa, jonne kuka tahansa voi lähettää kehittämänsä laatikon cargo-työkalun avulla tai vastaavasti ladata minkä tahansa laatikon ohjelmaansa. Tällä hetkellä palveluun on tallennettu yli 140 000 laatikkoa, joita on ladattu yli 68 miljardia kertaa. Hakusana ”cryptography” tuottaa hieman yli 1500 hakutulosta, joiden aiheet vaihtelevat laajasti kryptografian suuntauksesta toiseen. Edustettuina ovat niin symmetriset kuin epäsymmetriset algoritmit, satunnaislukugeneraattorit sekä lohkoketjut. Tulosten määrä rajautuu viiteensataan hakusanalla ”aes”. Osa näistä on erilaisia AES-algoritmin toimintamoodien toteutuksia, esimerkiksi AES-KWP, AES-GCM ja AES-CCM moodilaatikot löytyvät palvelusta. Julkisten laatikoiden käyttöön kannattaa suhtautua varauksella, sillä niiden toimivuudesta sekä yhteensopivuudesta ei yleensä ole mitään takeita. Mindermann ja muut (2018) tutkivat kryptografisten Rust-laatikoiden käytettävyyttä. Tutkimuksen aikana he identifioivat kaikkien laatikoiden joukosta 81, jotka täyttivät heidän määritelmänsä kryptografisesta sisällöstä. Näistä seitsemän täytti heidän 40 mielestään tärkeän (engl. major) laatikon kriteerit, ja ne ovat rust-openssl, rust-crypto, sodiumoxide, octavo, ring, rust_sodium ja RustCrypto. Näistä käytettävyyskokeeseen valittiin ring ja rust-crypto, jotka kumpikin osoittautuivat käytettävyydeltään heikoiksi. Tutkimuksen kohteena olivat yliopisto-opiskelijat, joilla oli ohjelmointikokemusta, mutta ei merkittävää kokemusta kryptografiasta. Vaikka kryptoprimitiivi olisi teknisesti virheettömästi toteutettu, kokematon käyttäjä voi käyttää sitä virheellisellä tavalla ja vaarantaa järjestelmän turvallisuuden. Sulautetuissa järjestelmissä käytetään tyypillisesti C- ja C++-ohjelmointikieliä. Rust mahdollistaa myös ohjelmoinnin rekisteritasolla, esimerkiksi ajurien kirjoittamiseen, ja tarjoaa täten vaihtoehdon edellä mainituille kielille. Nosedan ja muiden (2022) tutkimuksessa vertailtiin C- ja Rust-kielen suoritusaikoja sulautetussa järjestelmässä eri kryptoalgoritmeille. He havaitsivat, että käytetyllä kielellä ei ollut korrelaatiota suoritusajan kanssa. Suurin vaikutus on ilmeisesti algoritmin toteutuksella, joka toisaalta saattaa kasvattaa ohjelmalta vaadittavan muistitilan kokoa. Ohjelman suorituksen aikana pinomuistiin säilötään väliaikaisesti kaikenlaista tietoa. Kryptografisen algoritmin suorituksen jälkeen pinoon saattaa jäädä sensitiivistä materiaalia, joka saattaa hyökkääjän käsiin joutuessaan saattaa paljastaa kryptografisia avaimia ja selkotekstejä tai välituloksia, joiden avulla ne voidaan selvittää. Siksi on erittäin tärkeää nollata tai kirjoittaa tällaisten tietojen päälle algoritmin suorituksen päätyttyä. Arranz Olmos ja muut (2023) tutkivat kuinka useat C-, C++- ja Rust-kielellä kirjoitetut kryptografiset kirjastot käsittelevät sensitiivisen datan nollaamista algoritmien suorituksen jälkeen. Monet niistä yrittävät suorittaa tämän vaativan tehtävän, mutta yksikään niistä ei ollut turvallinen tutkimuksessa esitettyä hyökkääjämallia vastaan. Toisaalta he toteavat, että lähdekielen tasolla ohjelmassa kaiken tiedon nollaaminen on lähes mahdotonta ilman erillisiä suojausmekanismeja, eikä mikään tutkituista kirjastoista edes esitä minkäänlaisia väitteitä niiden turvallisuudesta tässä kontekstissa. Rust-laatikoista tutkimuksessa olivat mukana versio 0.17 ring-laatikosta ja 2.0-versio Dalek-laatikosta. 41 4 Suunniteltu salausohjelma Salausohjelma toteutetaan konstruktiivista tutkimusotetta hyödyntäen. Lukka (2001) toteaa, että sillä pyritään ratkaisemaan käytännöllisiä reaalimaailman ongelmia. Menetelmässä pyritään tuottamaan jokin uusi konstruktio, ja toteuttamisyrityksellä kokeillaan, onko se soveltuva käytäntöön. Lisäksi Lukka mainitsee, että konstruktiivisen tutkimusotteen on oltava huolellisesti kytketty jo tunnettuun teoriaan. Tässä työssä tuotetaan uutena konstruktiona salausohjelma, jonka teoreettinen viitekehys on esitetty edeltävissä luvuissa 2 ja 3. 4.1 Tutkimussuunnitelma Tässä työssä tutkitaan, kuinka toteutetaan valitulla kohdekielellä yleiskäyttöinen salausohjelma, joka sisältää standardoidun salausalgoritmin. Näkökulma on hyvin käytännönläheinen, kuten ohjelmistokehityksessä yleisesti on. Työn aikana tutkimus ja toteutus vastannevat toisiaan suurissa määrin, eikä niiden välille ole siten tarpeen tehdä erottelua. Koska lopputuloksena tulisi olla ohjelmisto, kyseessä on Sommervillen (2011, s. 28) mukaan ohjelmistoprosessi. Sommerville lisää, että ohjelmistoprosessissa on oltava neljä vaihetta: ohjelmiston määrittely, suunnittelu ja toteutus, ohjelmiston toiminnan tarkoituksenmukaisuuden tarkastaminen sekä ohjelmiston evoluutio, jotka kaikki voivat jakautua pienempiin aliprosesseihin. Ohjelmistoprosessia voidaan kuvata erilaisilla malleilla, jotka ovat yksinkertaistettuja esityksiä siitä, mitä ohjelmistokehityksessä oikeasti tapahtuu (Sommerville, 2001, s. 29). Sommerville toteaa, että ohjelmistoprosessin mallit ovat vain reaaliprosessin abstraktioita, joita voidaan käyttää työkaluna, kun halutaan kuvata prosessia muille. Hänen mukaansa malleja voi ajatella prosessin viitekehyksinä, jotka ovat muokattavissa ja laajennettavissa tarkkarajaisempien ohjelmistokehitysprosessien luomiseksi. Tähän työhön on valittu vesiputousmalli kuvaamaan ohjelmiston toteutusta. Sommervillen määritelmässä vesiputousmalli käsittelee ohjelmistoprosessin neljää päävaihetta 42 erillisinä prosessin vaiheina. Mallin periaate on se, että yksi vaihe suoritetaan kokonaan ennen seuraavan aloittamista. Kyseessä on siis hyvin suoraviivainen kuvaus prosessista. Käytännössä tilanne voi olla hieman monimutkaisempi, koska toteutuksen aikana voi paljastua virheitä tai parannusehdotuksia edellisen vaiheen sisältöön. Sommervillen (2011, s. 31) kuvauksessa vesiputousmallissa on viisi vaihetta: (1) Vaatimusten analysointi ja määrittely. (2) Järjestelmän ja ohjelmiston suunnittelu. (3) Toteutus ja yksikkötestaus. (4) Integraatio- ja järjestelmätestaus. (5) Toiminta ja ylläpito. Näistä vaiheista viimeinen on rajattu tämän työn ulkopuolelle, koska kehitettävää ohjelmaa ei ole tarkoitus ylläpitää aktiivisesti. Ohjelma suunnitellaan kuitenkin siten, että käyttäjä voi halutessaan laajentaa sen toiminnallisuutta itse. Vesiputousmallia tulee käyttää vain sellaiseen ohjelmistokehitykseen, jossa ohjelmiston vaatimukset tunnetaan hyvin eivätkä ne tule muuttumaan kehityksen aikana (Sommerville, 2011, s. 32). Koska työssä toteutetaan olemassa olevan standardin mukainen algoritmi, nämä molemmat ehdot täyttyvät selvästi. 4.2 Vaatimusten määrittely Vesiputousmallin ensimmäinen vaihe on vaatimusten analysointi ja määrittely (Sommerville, 2011, s. 31). Toteutettava salausohjelma sisältää standardoidun AES- algoritmin, joten kyseinen standardi itsessään toimii ulkoisena vaatimuksena. Sama pätee algoritmin eri toimintamoodeihin, jotka on myös standardoitu. Näistä voidaan johtaa seuraavat kaksi ulkoista korkean tason vaatimusta: 1. standardin mukaisesti oikein toteutettu algoritmi, 2. tuetut toimintamoodit toteutettu oikein standardin mukaisesti. 43 Loput vaatimukset ovat sisäisiä, jotka määritellään itse. Ensimmäiseksi, ohjelman on tuettava kaikkia kolmea standardissa määriteltyä avaimen pituutta. Tällöin algoritmi toteuttaa kaiken mahdollisen avainten näkökulmasta eikä siihen ole mahdollista lisätä mitään. Ohjelman toiminnallisuuden laajentaminen tapahtuu ainoastaan lisäämällä tuettuja toimintamoodeja. Aluksi salausohjelma tulee tukemaan kolmea toimintamoodia, jotka ovat ECB, CBC ja CMAC. Lisäksi ohjelma suunnitellaan siten, että uusien toimintamoodien lisääminen on mahdollisimman helppoa. Toiminnallisuuden ja turvallisuuden takaamiseksi ohjelmassa ei käytetä Rust-kielen unsafe-lohkoja, eikä ulkoisia laatikoita. Käytössä on ainoastaan kielen standardikirjasto. Tiivistettynä, sisäiset vaatimukset ovat: 1. AES-algoritmi tukee kolmea avaimen pituutta, 2. ohjelma tukee toimintamoodeja ECB, CBC ja CMAC, 3. ohjelma on helposti laajennettavissa tukemaan muita toimintamoodeja, 4. lähdekielisessä ohjelmassa ei käytetä unsafe-lohkoja, 5. ohjelmassa käytetään ainoastaan standardikirjaston laatikoita. 4.3 Ohjelman rakenne ja arkkitehtuuri Vesiputousmallin toinen vaihe koostuu ohjelman vaatimusten muokkaamisesta ohjelmistoarkkitehtuurin muotoon. Siinä tunnistetaan ja kuvataan järjestelmän perustan abstraktiot ja niiden väliset suhteet (Sommerville, 2011, s. 31). Salausohjelman logiikka toteutetaan pääohjelmaan, joka tarjoaa käyttöliittymäpalvelut terminaalin kautta käyttäjälle. Pääohjelma toteuttaa kaikki kommunikointikanavat käyttäjän kanssa ja tarkastaa saadut syötteet ja parametrit sekä palauttaa lopputuloksen käyttäjälle. Tämä toiminnallisuus löytyy ylimmän tason moduulista nimeltä main.rs. Salauksen toteuttamista varten pääohjelma kutsuu toimintamoodikirjastoa modes.rs, joka on ylätason koontikirjasto kaikille tuetuille toimintamoodeille. Se puolestaan sisältää jokaisen tuetun toimintamoodin erillisenä kirjastona, joiden nimet ovat muotoa 44 aes_[moodin lyhenne].rs. Tällä suunnitteluvalinnalla pyritään minimoimaan pääohjelmaan tarvittavat muutokset uusien toimintamoodien lisäämisen yhteydessä. Varsinainen AES-algoritmi toteutetaan myös erillisenä kirjastona aes_rs, jota toimintamoodit kutsuvat itsenäisesti tarpeensa mukaan. Kuvaus ohjelman arkkitehtuurista on esitetty alla olevassa kuviossa 5. Kuvio 5. Salausohjelman sisältämät moduulit ja niiden väliset suhteet. Käyttäjä kommunikoi ohjelman kanssa main.rs-moduulin kautta, joka välittää tarvittavat tiedot modes.rs- moduulille. Se puolestaan kutsuu haluttua toimintamoodikirjastoa, kuten aes_ecb.rs tai aes_cbc.rs, jotka kaikki käyttävät yhteistä aes.rs-moduulia, jossa varsinainen algoritmi on toteutettu. 45 4.4 Toteutussuunnitelma Seuraavassa ohjelmistoprosessin vaiheessa toteutetaan ohjelman osat vaatimusten ja arkkitehtuurin mukaisesti. Sommervillen (2011, s. 31) vesiputousmallissa tämä tarkoittaa osaa vaiheesta kolme. Se aloitetaan toteuttamalla AES-algoritmi aloittaen yksittäisistä kierrosfunktioista ja avaimen laajennuksesta. Nämä kootaan yhteen, jolloin ne toteuttavat koko algoritmin, alkaen 128-bittisistä avaimista. Seuraavana toteutetaan tuki myös 192- ja 256-bittisille avaimille. Kun varsinainen algoritmi on valmis, aloitetaan toimintamoodien toteuttaminen. Ensimmäisenä algoritmi yhdistetään ECB-toimintamoodiin, jota seuraavat CBC ja CMAC. Koontikirjasto toimintamoodeille toteutetaan näiden jälkeen. Siihen toteutetaan rajapinta, jonka kautta valitaan haluttu toimintamoodi ja sen jälkeen asetetaan oikeat parametrit. Koontikirjastoon toteutetaan lisäksi valmius mahdollisimman helposti lisätä uusia tuettuja toimintamoodeja. Viimeisenä toteutetaan pääohjelma, joka toimii käyttöliittymänä ja ohjelman logiikan pyörittäjänä. Pääohjelmatoteutus sisältää vuorovaikutuksen käyttäjän kanssa terminaalin kautta sekä toimintamoodikirjastokutsut, joiden kautta algoritmi pääsee tekemään työtään. 4.5 Testaussuunnitelma Ohjelmiston testaaminen kuuluu vesiputousmallin vaiheisiin kolme ja neljä (Sommerville, 2011, s. 31). Testaus ja toteutus kulkevat käsi kädessä, sillä jokainen ohjelman osa testataan ja sen toiminta varmistetaan ennen sen integrointia muuhun ohjelmaan. AES- algoritmin kierrosfunktioille ja avaimen laajennukselle toteutetaan yksikkötestit käyttämällä Rust-kieleen sisäänrakennettua testausominaisuutta. Sen avulla saadaan nopeasti tietoa funktioiden toiminnan oikeellisuudesta. Testauksessa käytetään NIST:n 46 julkaisemia esimerkkejä välivaiheineen AES-algoritmin funktioille (National Institute of Technology and Standards, 2016/2024c). Seuraavaksi testataan koko algoritmin toimintaa varmennettujen osien integroituna järjestelmänä. Tässäkin vaiheessa käytetään Rust-kielen testausominaisuutta. Testeillä pyritään varmistamaan, että algoritmi ja käänteisalgoritmi toimivat virheettömästi. Tämä on edellytys sille, että toimintamoodien testaus voidaan aloittaa, koska ne nojaavat oikein toimivaan algoritmiin. Testeissä käytetään samoja esimerkkejä kuin yksittäisten funktioiden testauksessa. Kun algoritmin virheetön toiminta on todennettu, testataan toteutettujen toimintamoodien oikeellisuutta. Alustava testaus toteutetaan NIST:n toimintamoodeille julkaisemilla esimerkeillä välivaiheineen (National Institute of Technology and Standards, 2016/2024d; 2016/2024e). Lopullinen algoritmin testaus toteutetaan luomalla ECB-, CBC- ja CMAC-toimintamoodeille testit, joissa syötteenä käytetään NIST:n julkaisemia ja varmentamia testivektoreita (National Institute of Technology and Standards, 2016/2024a; 2016/2024b; 2016/2024c). Toimintamoodifunktioiden tuottamaa vastetta verrataan syötteeltä odotettuun testivasteeseen ja mikäli vasteet täsmäävät, algoritmi on toteutettu oikein. Testauksessa käytetyt testivektorit on yksilöity liitteessä 3. Näitä testivektoreita käyttämällä kuka tahansa voi epävirallisesti varmentaa toteuttamansa AES-algoritmin toimintamoodien toiminnan. Viimeinen testausvaihe sisältää järjestelmätason testejä koko ohjelmalle. Niillä varmistetaan, että ohjelma kykenee käsittelemään käyttäjän antamia syötteitä oikein ja että ohjelman logiikka toimii oikein. Näiden hyväksyntätestien tarkoitus on varmistaa järjestelmän vaatimustenmukaisuus (Sommerville, 2011, s. 31). Onnistuneiden hyväksyntätestien jälkeen ohjelma olisi valmis toimitettavaksi asiakkaalle, jos sellainen olisi. 47 5 Algoritmin toteutus Advanced Encryption Standard -algoritmin sisältävä salausohjelma kirjoitetaan Rust- kielen versiolla 1.79.0, joka on julkaistu 10. kesäkuuta 2024. Ohjelma kehitetään tietokoneella, jossa on 64-bittinen prosessori ja käyttöjärjestelmänä Microsoft Windows 10:n versio 22H2. Ohjelmointiympäristöksi on valittu Microsoft Visual Studio 2022, josta käytetään versiota v17.10.2. Tässä ohjelmassa on integroitu terminaali helpottamaan ohjelman kääntämistä sekä ajamista. Visual Studioon on lisäksi asennettu rust- analyzer.vs-laajennuksen versio 2.0.187. Sen tarkoitus on edelleen helpottaa kehitystyötä, sillä se lisää ohjelman lähdekieleen syntaksivärityksen ja toisaalta analysoi lähdekielistä ohjelmaa antaen ohjeita ja huomauttaen virheistä. Luvussa 4 esitetyt vaatimukset ja arkkitehtuurikuvaus määrittävät ohjelman rakennetta laajassa kuvassa. Ne jättävät kuitenkin paljon yksityiskohtia määrittelemättä, joiden toteutustapa määrittyy tarkemmin toteutusvaiheessa. AES-algoritmin tilaa käsitellään teoriassa vaihtelevasti joko tavuina tai 32-bittisinä sanoina. Työssä käytettävä tietokone mahdollistaisi myös 64-bittisen käsittelyn. Näistä toteutustavaksi valitaan 32-bittinen versio, joka on nopeampi kuin käsittely tavuittain, mutta toisaalta algoritmin rakenteen takia luontevampi vaihtoehto kuin 64-bittisten lukujen käsittely. Algoritmi toteutetaan puhtaasti ohjelmistototeutuksena, koska salausohjelma on suunniteltu käytettäväksi yleiskäyttöisillä tietokoneilla. Laitteistokiihdytystä tai prosessorikohtaista optimointia ei käytetä. Ohjelmalle ei ole asetettu suorituskykyvaatimuksia, minkä takia toteutus tehdään ilman etukäteen laskettuja hakutauluja. Tämä ratkaisu on hitaampi kuin hakutaulutoteutus, mutta säästää muistia, koska hakutaulujen koko on neljä kilotavua (Daemen ja Rijmen, 1999, s. 18). SubBytes- muunnos ja sen käänteismuunnos toteutetaan kuitenkin S-laatikon ja S-käänteislaatikon avulla. 48 5.1 AES-algoritmin toteutus ja testaus Ensimmäisenä toteutetaan AES-algoritmi, joka toimii koko ohjelman sydämenä, aloittaen tuesta 128-bittisille avaimille. Kierrosfunktioista ensimmäisenä toteutetaan SubBytes-muunnos, sekä sen käänteismuunnos InvSubBytes. Näitä varten luodaan kaksi 256 tavun hakutaulua liitteiden 1 ja 2 mukaisesti. Funktiot ottavat parametrina muokattavan viittauksen algoritmin tilaan, joka koostuu neljästä 32-bittisestä sanasta. Sanoista erotellaan tavut, joille noudetaan vastinpari hakutaulusta apufunktiolla. Noudetut tavut kootaan lopuksi yhteen 32-bittisiksi sanoksi ja sijoitetaan takaisin algoritmin tilaan. SubBytes- ja InvSubBytes-muunnosten apufunktioita, joilla yksittäiset tavut vaihdetaan hakutaulun mukaisesti, testataan antamalla niille yksi tavu ja vertaamalla palautetun tavun arvoa odotettuun arvoon. Hakutaulujen oikeellisuus varmistetaan kokeilemalla apufunktioita kaikille tavuille. Tätä varten jokainen hakutaulun tavu käsitellään järjestyksessä apufunktiolla ja käänteisellä apufunktiolla ja tulokset kerätään taulukkoon. Lopuksi tulostaulukkoa verrataan hakutaulukkoon, jolloin niiden sisältöjen tulisi olla identtiset. Testi toteutetaan molemmille hakutaulukoille. Varsinaisia muunnoksia testataan antamalla niille testisyötteenä standardin esimerkkivektorien välituloksia (National Institute of Standards and Technology, 2016/2024c) kuvaavia algoritmin tilaviittauksia. Kutakin testisyötettä vastaavaa ohjelman antamaa vastetta verrataan tunnettuun kyseistä testisyötettä vastaavaan odotettuun vasteeseen, jotta saadaan varmuus muunnosten toiminnan oikeellisuudesta. ShiftRows-muunnos ottaa myös parametrina vastaan muokattavan viittauksen algoritmin tilaan. Tilan muodostavat sanat kuvaavat tilan matriisiesityksen sarakevektoreita. Muunnoksen toteutuksessa sanat tallennetaan väliaikaisiin muuttujiin. Näistä sopivia bittimaskeja käyttämällä poimitaan oikeat tavut ja ne yhdistetään oikeaan järjestykseen suoraan takaisin tilan sarakevektoreiksi. InvShiftRows-muunnoksen toteutus on täysin vastaava, mutta tavujen järjestys vain on käänteinen. 49 Muunnosten testaus tapahtuu NIST:n julkaisemilla välituloksia sisältävillä esimerkkivektoreilla (National Institute of Standards and Technolody, 2016/2024c). Testeissä muunnoksille annetaan esimerkkien mukaiset viittaukset algoritmin tilaan ja operaation jälkeen tilaa verrataan odotettuun välitulokseen. Lisäksi testataan muunnosten toimintaa yhdessä SubBytes- ja InvSubBytes-muunnosten kanssa. Näissä testeissä tilaa käsitellään ensin SubBytes-muunnoksella ja heti sen jälkeen ShiftRows- muunnoksella. Tilaa verrataan näiden jälkeen odotettuun välitulokseen. Vastaava testi toteutetaan myös käänteismuunnoksille. MixColumns-muunnos käsittelee algoritmin tilaa sarakkeittain, joten sille on luonnollista toteuttaa apufunktio, joka käsittelee erikseen kutakin tilan sarakevektoria kuvaavaa sanaa. Valittu toteutus on yksinkertainen ja se käyttää apufunktiota nimeltään xtimes, joka kertoo monomilla 𝑥 sille annetun polynomin kunnassa 𝐺𝐹(28) (St Denis ja Johnson, 2006, s. 152–153). Sarakevektorin jokainen tavu kerrotaan erikseen monomilla 𝑥 ja tulokset tallennetaan. Tämän jälkeen sarakevektorin tavuja ja ensin laskettuja tuloksia yhdistellään oikeassa järjestyksessä vastetavuiksi, joista lopulta kootaan lopullinen sarakevektori. Käänteismuunnos InvMixColumns toimii hieman vastaavalla tavalla, mutta vaatii toteutukseen apufunktion gf_mul, joka kertoo keskenään sille annetut polynomit kunnassa 𝐺𝐹(28) (St Denis ja Johnson, 2006, s. 145). Tämä johtuu siitä, että käänteismuunnoksessa käytettävässä kerroinmatriisissa alkioiden bittiesityksissä on enemmän bittejä kuin MixColumns-muunnoksessa. Vastetavut saadaan kertomalla sarakevektorin tavuja kerroinmatriisin alkioilla ja laskemalla niitä yhteen. Lopuksi ne kootaan yhteen uudeksi sarakevektoriksi. Muunnosta testataan, kuten aiempia kierrosfunktioita, antamalla sille muokattava viittaus algoritmin tilaan ja vertaamalla välitulosta odotettuun arvoon. Lisäksi toteutetaan testit, joissa kolme kierrosfunktiota, SubBytes, ShiftRows ja MixColumns 50 käänteismuunnoksineen käsittelevät tilaa peräkkäin, ja tulosta verrataan testivektorin esimerkkitulokseen. Kierrosavaimen lisäys toteutetaan avaimen laajennuksen yhteydessä, jotta siihen voidaan liittää mahdollisuus määrittää minkä kierroksen avain on lisättävä. Avaimen laajennuksessa tarvitaan kahta apufunktiota, jotka käsittelevät 32-bittistä sanaa. Toinen on SubBytes-muunnos, jota kierrosfunktiona käytetään koko algoritmin tilaan. Tämä on toteutuksessa huomioitu siten, että SubBytes-muunnos pystyy käsittelemään mielivaltaisen pituista 32-bittisiä sanoja sisältävää taulukkoviittausta. Toinen apufunktio, rot_bytes, siirtää sanan neljää tavua yhden tavun verran vasemmalle siten, että ensimmäisestä tavusta tulee neljäs, toisesta ensimmäinen ja niin edelleen. Se toimii siis samoin kuin ShiftRows-muunnoksessa tapahtuu algoritmin tilan riveille. Algoritmin pääavain toimii itsessään nollantena kierrosavaimena ja loput kierrosavaimet johdetaan siitä luvussa 2.4 esitetyn algoritmin 2 mukaisesti. Jokainen 128-bittinen kierrosavain koostuu neljästä sanasta ja jokaisen pääavainta seuraavan kierrosavaimen ensimmäinen sana käsitellään myös apufunktioilla ja siihen lisätään kierrosta vastaava kierrosvakio. Avaimen laajennus testataan kevyesti vertaamalla annetusta pääavaimesta johdettua ensimmäistä kierrosavainta odotettuun tulokseen. Tähän ratkaisuun päädyttiin, koska NIST ei tarjoa testivektoreita avaimen laajennukselle, vaan jokainen avain olisi laskettava erikseen algoritmin testivektoreista vertaamalla algoritmin tilaa ennen ja jälkeen kierrosavaimen lisäyksen. Oikeellisuus varmistuu myöhemmin, kun testataan algoritmia yhden lohkon salaamiseen. Kun kaikki kierrosfunktiot algoritmille ja käänteisalgoritmille on toteutettu, ne integroidaan yhteen, jolloin ne muodostavat täydellisen AES-algoritmin sekä sen käänteisalgoritmin. Salaamiselle ja salauksen purkamiselle toteutetaan omat funktiot, joille annetaan samat parametrit: viittaus salattavaan tai purettavaan lohkoon, 51 muokattava viittaus taulukkoon, johon algoritmin tulos kirjoitetaan, sekä käytettävä pääavain. Funktioita testataan kokonaisilla NIST:n esimerkkivektorilohkoilla ja avaimilla (National Institute of Standards and Technology, 2016/2024c). Saatuja lopputuloksia verrataan jälleen odotettuihin lopputuloksiin. Tuloksen ollessa oikea tiedetään, että toteutettu algoritmi ja sen käänteisalgoritmi toimivat oikein. Seuraavaksi algoritmia laajennetaan tukemaan 192- ja 256-bittisiä avaimia. Itse algoritmissa tämä näkyy ainoastaan kierrosten määrän kasvamisena kymmenestä kahteentoista 192 bitin avaimilla ja neljääntoista 256 bitin avaimilla. Oikea kierrosmäärä saadaan avaimen pituudesta, joka saadaan helposti Rust-kielen taulukoihin liitetyllä metodilla len(). Taulukon pituutta vertaamalla voidaan asettaa kierrosmuuttujalle sopiva arvo. Avaimen laajennuksessa kierrosten määrästä voidaan päätellä, kuinka monesta 32- bittisestä sanasta pääavain koostuu ja tallentaa se muuttujaan Nk. Kierrosavaimet muodostavan sanataulukon ensimmäiset Nk sanaa kopioidaan suoraan pääavaimesta. Muuttujaa voidaan käyttää myös ehdossa, jolla määritetään joka Nk. sana, jota käsitellään edellä mainituilla apufunktioilla. Lisäksi tarvitaan algoritmissa 2 esitetty ehto 256-bittisille avaimille, jolla tarkistetaan, milloin algoritmin määrittämä toinen käsittely tarvitaan. Muutosten jälkeen voidaan testata yhden lohkon salaaminen ja salauksen purkaminen vastaavalla tavalla kuin 128-bittisten avainten tapauksessa. Apuna käytetään NIST:n määrittelemiä esimerkkivektoreita (National Institute of Standards and Technology 2016/2024c). Mikäli lopputulos vastaa määritettyä odotettua lopputulosta, on algoritmin oikea toiminta varmistettu kaikille kolmelle avaimen pituudelle. 52 5.2 Toimintamoodien toteutus ja testaus Ensimmäisenä toimintamoodina toteutetaan ECB-moodi, koska se on yksinkertaisin valituista moodeista. Salattava viesti jaetaan 128-bittisiksi lohkoiksi, jotka salataan erikseen AES-algoritmilla käyttäen kaikille samaa avainta. Salauksen purkaminen tapahtuu vastaavalla tavalla. Tässä moodissa tietty syöte ja tietty avain tuottavat aina saman vasteen, mistä moodin nimi Electronic Codebook on peräisin (National Institute of Standards and Technology, 2001, s. 9). Salattavan viestin lohkot eivät ole mitenkään riippuvaisia toisistaan. Tätä toimintamoodia käytetään luottamuksellisuuden saavuttamiseksi, toisin sanoen vain oikean salausavaimen haltijat kykenevät selvittämään salatun viestin sisällön. Ohjelmointikielellä ECB-moodi toteutetaan luontevasti silmukalla, jossa syötetään syöteviestin lohkoja yksi kerrallaan ja niistä tuotettuja vasteita kirjoitetaan vastetaulukkoon. Toteutus vaatii hieman indeksien laskentaa taulukoihin, mutta on pääpiirteissään yksinkertainen vähänkään ohjelmointia harrastaneelle. Salauksen tai sen purkamisen tuloksia voidaan tallentaa suoraan kohdetaulukkoon, kunhan indeksien laskenta toimii oikein. Myös CBC-moodi tuottaa viestin luottamuksellisuuden. Englanninkielisen nimensä Cipher Block Chaining mukaisesti se ketjuttaa yhteen salattavia lohkoja, jolloin edellisen lohkon laskennan tulos vaikuttaa tuleviin lohkoihin (National Institute of Standards and Technology, 2001, s. 10). Tässä moodissa tarvitaan lisäksi alustusvektori (engl. initialization vector, IV). Salattava viesti jaetaan 128-bittisiksi lohkoiksi ja ensimmäinen lohko yhdistetään alustusvektorin kanssa XOR-operaattorilla. Näin saatu lohko salataan AES-algoritmilla ja saatu vastelohko yhdistetään XOR-operaattorilla seuraavan salaamattoman lohkon kanssa. Tämä lohko salataan jälleen AES-algoritmilla ja yhdistetään jälleen seuraavan salaamattoman lohkon kanssa. Näin jatketaan, kunnes viimeinenkin lohko on salattu. 53 Salauksen purkaminen tapahtuu käänteisessä järjestyksessä. Aluksi puretaan ensimmäisen lohkon salaus ja tulokseen yhdistetään alustusvektori XOR-operaattorilla viestilohkon paljastamiseksi. Toisen lohkon salaus puretaan, ja siihen yhdistetään ensimmäinen lohko salattuna, jolloin toinen viestilohko paljastuu. Näin jatketaan, kunnes viimeisen salatun lohkon salaus on purettu ja siihen yhdistetty sitä edeltävä salattu lohko. CBC-moodissa on tärkeää huolehtia alustusvektorin säilyttämisestä, sillä ilman sitä salauksen purkaminen on mahdotonta. CBC on vain hieman ECB-moodia monimutkaisempi toteuttaa. Salauksessa mukaan otetaan yksi välitaulukko, johon muodostetaan algoritmin syötelohko. Ensimmäisenä siinä yhdistetään alustusvektori ja viestilohko ja seuraavilla kerroilla viimeisin salattu lohko ja seuraava viestilohko. Tulokset tallennetaan kohdetaulukkoon, josta salauslohkoja voidaan kopioida välitaulukkoon seuraavaa silmukan kierrosta varten. Salauksen purkamisessa välitaulukkoa ei edes tarvita, sillä alustusvektori ja salattu lohko ovat alusta alkaen valmiina käytettäväksi. Lohkon salaus puretaan suoraan kohdetaulukkoon ja XOR-operaatio edellisen salatun lohkon kanssa suoritetaan suoraan kohdetaulukkoon. CMAC-toimintamoodi eroaa käyttötarkoitukseltaan ECB- ja CBC-moodeista, sillä sitä käytetään todentamisessa (National Institute of Standards and Technology, 2005). Tällä toimintamoodilla voidaan joko laskea annetulle syötteelle ominainen tunniste tai varmentaa vastaako tiettyyn syötteeseen liitetty tunniste todella kyseistä syötettä. Ensimmäiseksi toteutetaan moodin aliavainten johtaminen pääavaimesta. Toteutettu funktio avainten johtamiseen on alla olevassa algoritmissa 6. 54 fn cmac_derive_subkeys(key: &[u32], k1: &mut[u32], k2: &mut[u32]){ let temp: [u32;4] = [0; 4]; aes::aes_encrypt_block(&temp, k1, &key); if k1[0] & 0x80000000 == 0 { cmac_shift_key(k1); } else { cmac_shift_key(k1); k1[3] = k1[3] ^ Rb ; } for i in 0..4 { k2[i] = k1[i]; } if k1[0] & 0x80000000 == 0 { cmac_shift_key(k2); } else { cmac_shift_key(k2); k2[3] = k2[3] ^ Rb; } } fn cmac_shift_key(key: &mut[u32]) { for i in 0..3 { key[i] = key[i] << 1; if key[i + 1] >= 0x80000000 { key[i] = key[i] + 1; } } key[3] = key[3] << 1; } Algoritmi 6. Funktio CMAC-toimintamoodin aliavainten johtamiseen standardin mukaisesti sekä apufunktio cmac_shift_key(). 55 Apufunktio cmac_shift_key() saa syötteenä 128-bittistä lukua kuvaavan 32-bittisten lukujen taulukon ja se siirtää tämän kuvattavan luvun bittejä yhden askeleen vasemmalle. Aliavainten johtaminen alkaa salaamalla pelkästään nollia sisältävä lohko AES- algoritmilla. Jos vasteen merkitsevin bitti on yksi, ensimmäinen aliavain k1 saadaan siirtämällä saadun vasteen bittejä askel vasemmalle. Merkitsevimmän bitin ollessa nolla aliavain saadaan siirtämällä bittejä askel vasemmalle sekä lisäämällä viimeiseen sanaan vakio Rb = 0x00000087. Toinen aliavain k2 muodostetaan samalla tavalla aliavaimesta k1 kuin se muodostettiin nollalohkon salaamisen vasteesta. Aliavainten johtamisen jälkeen toteutetaan varsinainen tunnisteen laskenta. CMAC- toimintamoodin vaiheet ovat edeltäneitä ECB- ja CBC-moodeja monimutkaisemmat, koska syötteenä annettavan viestin ei tarvitse olla AES-lohkon monikerta. Viimeisen lohkon käsittelytapa riippuu siitä, onko syöteviestin pituus bitteinä 128:n monikerta vai ei. Viestin pituus voi olla jopa nolla, jolloin viesti käsitellään yhtenä vajaana lohkona. Viesti jaetaan lohkoiksi, joista viimeinen voi viestin pituuden mukaan olla täysi tai vajaa. Jos viimeinen lohko on täysi, siihen lisätään XOR-operaattorilla aliavain k1. Jos viimeinen lohko jää vajaaksi, se täytetään sopivan mittaisella bittijonolla, jonka merkitsevin bitti on 1 ja muut bitit nollia. Tämän jälkeen lohkoon lisätään XOR-operaattorilla aliavain k2. Tunniste lasketaan kohdetaulukkoon siten, että jokainen viestilohko salataan AES- algoritmilla pääavainta käyttäen ja saatu vaste lisätään aluksi pelkkiä nollia sisältävään kohdetaulukkoon XOR-operaattorilla. Saatu 128-bittinen lohko riippuu siten jokaisesta viestilohkosta, pääavaimesta ja siitä johdetuista aliavaimista. Standardin mukaan tunnisteen pituus voi olla mitä tahansa 1 ja 128 bitin väliltä, mutta suositeltava pituus on vähintään 64 bittiä (National Institute of Standards and Technology, 2005, s. 11). Tällä pyritään tekemään tunnisteen arvaamisesta riittävän vaikeaa, jotta tunnisteen käyttäminen on turvallista. Luonnollisesti tunnisteen turvallisuus on suoraan verrannollinen tunnisteen pituuteen. Lopullisen tunnisteen muodostavat valitun pituuden verran merkitsevimpiä bittejä lasketusta tunnisteesta. 56 Toteutuksessa käytetään apuna välitaulukkoa, johon kopioidaan ensin viestilohko, ja sen jälkeen lisätään XOR-operaattorilla jo laskettu välitulos kohdetaulukosta. Ensimmäisellä kerralla kohdetaulukko on alustettu nollilla, joten se ei vaikuta lopputulokseen. Välitaulukko annetaan syötteenä AES-algoritmille ja sen vaste kirjoitetaan kohdetaulukkoon. Tätä jatketaan silmukassa, kunnes jäljellä on enää yksi täysi tai vajaa käsittelemätön lohko. Jos viimeinen lohko on täysi, laskenta suoritetaan samoin kuin edellä, mutta välitaulukkoon lisätään lisäksi aliavain k1. Mikäli lohko on vajaa, se täytetään kuten edellä on kuvattu, ja siihen lisätään aliavain k2. Haastavinta tässä vaiheessa on viestin täyttäminen, koska bitti 1 on saatava oikeaan kohtaan oikeaa 32- bittistä sanaa, ja kaikkien sitä seuraavien bittien on oltava nollia. Tunnisteen todentamisfunktio laskee annetusta viestistä tunnisteen ja vertaa sitä annettuun tunnisteeseen aina syötettyyn tunnisteen pituuteen saakka. Funktio palauttaa totuusarvon siitä vastaavatko tunnisteet toisiaan. Vaikka standardi mahdollistaisi tunnisteen pituuden olevan mitä tahansa yhden ja 128 bitin väliltä, tässä toteutuksessa hyväksyttyjä tunnisteen pituuksia ovat kahdeksan bitin monikerrat eli tavut. Kutakin toteutettua AES-algoritmin toimintamoodia testataan kattavammin kuin varsinaista algoritmia, jonka testaus toteutusvaiheessa on melko suppeaa. Tämä on kuitenkin perusteltua, koska toimintamoodit käyttävät varsinaista algoritmia hyväkseen, ja nojaavat sen virheettömään toimintaan. Tämän takia AES-algoritmi tulee testatuksi myös toimintamoodien testien yhteydessä. ECB-toimintamoodi testataan kokoelmalla testejä, joissa toimintamoodin funktioita salaukseen ja salauksen purkuun kutsutaan tarjoamalla niille syötteenä NIST:n testivektori ja sitä vastaava avain (National Institute of Standards and Technology 2016/2024a). Funktion palauttamaa vastetta verrataan kussakin tapauksessa testivektorin määrittelemään odotettuun vasteeseen ja testi on suoritettu onnistuneesti, jos ne ovat identtiset. Näissä testeissä salausavain on jokaisella kerralla erilainen. Myös 57 salattavan tai purettavan tekstin pituus vaihtelee, ollen kuitenkin aina algoritmin lohkon pituuden monikerta. Sekä salausta että salauksen purkua testataan kuudella testivektorilla kullekin kolmesta tuetusta avaimen pituudesta. Seuraavaksi testataan CBC-toimintamoodia vastaavasti. Syötteenä on näissä testivektoreissa lisäksi toimintamoodin tarvitsema alustusvektori. Kullekin avaimen pituudelle testataan salausta jälleen kuudella testivektorilla ja salauksen purkua samoin. Funktioiden vasteiden ollessa samat kuin testivektorissa määriteltyjen odotettujen vasteiden, tulkitaan testien onnistuneen. CMAC-toimintamoodin testeissä käytetään ainoastaan varmennustoimintoa, sillä sekin laskee annetulle syötteelle tunnisteen ja vertaa sitä annettuun tunnisteeseen. Testeissä varmistetaan toimintamoodin toiminta kaikille tuetuille avaimen pituuksille. Kullekin laaditaan testejä, joissa syötteen pituus ja lasketun tunnisteen pituus vaihtelevat, eikä kummankaan tarvitse olla algoritmin lohkon pituuden monikerta. Testeissä käytetään NIST:n testivektoreita (National Institute of Standards and Technology 2016/2024b). Käytetyissä testivektoreissa on sekä oikeita että vääriä syötteisiin liittyviä tunnisteita. Tällä pyritään varmistamaan, että väärä tunniste ei läpäise todentamista eikä to