Obligatorisk Oppgave 1 INF-2310 - Sikkerhet i distribuerte systemer 1
Innholdsfortegnelse: Forside s. 01 Innholdsfortegnelse s. 02 Del I Selvtest s. 03 Del II CBC/PKCS#5 s. 05 Generalisert PKCS#5 Implementasjon s. 08 Cipher Block Chaining (CBC) Implementasjon s. 09 Selvtest Teste implementasjonen s. 10 Del III Testing: JCE vs. Selvlaget s. 11 Appendix A Filoversikt s. 11 2
Del I - Selvtest: Kryptere en klartekst, dekryptere den resulterende kryptoteksten, og så å sjekke at resultatet av dekrypteringen er identisk med klarteksten i utgangspunktet. Implementasjonen av del 1, er vedlagt med filnavn xxcrypt.java og viser i praksis det som blir gjennomgått i denne delen av oppgaven. import javax.crypto.cipher; import java.security.key; import javax.crypto.spec.secretkeyspec; public class xxcrypt { public static void main(string[] args) throws Exception { // Henter ut gyldig IvParameterSpec for bruk til kryptering/dekryptering. byte[] initvector = { 0, 0, 0, 0, 0, 0, 0, 0 ; IvParameterSpec iv = new IvParameterSpec(initVector); // Konverterer nøkkelstrengen til noe vi kan bruke, dvs ett bytearray. byte[] keyb = Hex.fromString( fedcba9876543210123456789abcdeff ); // Bruker denne til å lage en nøkkel til algoritmen i form av ett SecretKeySpec // objekt. Key key = new SecretKeySpec(keyB, IDEA/CBC/PKCS#5 ); // Henter ut en instans av Cipher som implementerer ønsket algoritme. Cipher _cp = Cipher.getInstance( IDEA/CBC/PKCS#5 ); // Cipher klargjøres for bruk og settes til krypteringsmodus _cp.init(cipher.encrypt_mode, key, iv); // Krypterer teksten og skriver den til skjermen byte[] encryptt = _cp.dofinal(message.getbytes( ISO8859_1 )); System.out.println( Encrypted text: + Hex.toString(encryptT)); // Klargjør Cipher på nytt men nå for å dekryptere tekst. _cp.init(cipher.decrypt_mode, key, iv); byte[] decryptt = _cp.dofinal(encryptt); System.out.println( Decrypted text: + new String(decryptT, ISO8859_1 )); 3
Det overdokumenterte utdraget ovenfor viser ikke i sin helhet den store mengden funksjonalitet som støttes via Java Cryptographic Environment (JCE). Ved kjøring av dette eksemplet vil en kunne se at teksten blir kryptert og så dekryptert for å muliggjøre sammenligning mellom den originale og den teksten en satt igjen med til slutt. Den vedlagte filen til denne delen overlater valget av initialiseringsvektor til JCE og blir dermed generert tilfeldig av denne og da er unødvendig for denne implementasjonen dette forutsetter da at kryptering og dekryptering foretas av samme objekt eller Iv-informasjon utveksles. En eksempelvis kjøring av dette programmet ga følgende output: Encrypted text: 031C0D9D98EE389B0A60C62163D34DA2E48FF4AA442A341BA783DBB9AD58EB0B Decrypted text: it loves my foo, does it not? Decrypted text matches original text... True 4
Del II CBC/PKCS#5: Implementere Cipher Block Chaining (CBC) modus og PKCS#5 padding uten bruk av JCE. En egnet implementasjon av IDEA vil bli brukt til selve kryptering / dekrypteringen. I lys av del 3 av oppgaven og for letthets skyld har jeg valgt å legge til ett ekstra lag av abstraksjon mellom programmene som benytter JCE eller vår egen implementasjon og dermed gi dem ett felles uniformt programmerings API, som dermed også som antydet angående fremtidige obligatoriske innleveringer kan være til stor hjelp. Også dermed ved å stadfeste hvilke funksjoner som skal være støttet vil det være lettere å implementere egnede implementasjoner av disse. Dette resulterte i en abstrakt klasse kalt CryptoEngine, med funksjoner og konstanter i henhold til UML diagrammet på neste side. Hver implementasjon av denne klassen må også da implementere de abstrakte funksjonene som er definert i denne sammenhengen: public abstract class CryptoEngine { abstract public byte[] encrypt(byte[] data) throws Exception; abstract public byte[] decrypt(byte[] data) throws Exception; For enkelhet er ikke unntakshåndtering gjennomført på noe revolusjonerende nivå i oppgave og blir bare kastet videre til den uheldige brukeren av systemet. Likevel etter å ha definert disse er det da en smal sak å lage en implementasjon som bruker JCE og en implementasjon laget av oss selv samt enda lettere å få dem til å samarbeide. For å lette bruken har jeg i likhet med JCE laget en getinstance funksjon som tar ett nummer som argument og returnerer en instans av den ønskede implementasjon. Dette muliggjør følgende kode: CryptoEngine ce = CryptoEngine.getInstance(CryptoEngine.JCE_Implementation, KEY, IV); // eller denne for å bruke egen implementasjon CryptoEngine ce = CryptoEngine.getInstance(CryptoEngine.Own_Implementation, KEY, IV); 5
Nå da formalitetene med det overliggende abstraksjonslaget er tilbakelagt kan en konsentrere seg om de hovedsaklige implementasjonsmessige detaljene over vår egen implementasjon av CBC og PKCS#5. UML diagram over min egen implementasjon av dette er vist her til venstre. En detaljert gjennomgang av hver funksjon er overflødig ettersom denne informasjonen er tilgjengelig i medfølgende dokumentasjon. For å lette implementasjonen av CBC er det innført en ny klasse i tillegg til denne som representerer en enkelt blokk data med andre ord en slags wrapperklasse for ett 8 byte array. 6
Generalisert PKCS#5 Implementasjon: Når en skal implementere en Cipher kan en ikke stole på at den teksten som skal krypteres passer fint inn i et visst antall ( hele ) blokker, og da vil det være nødvendig å bruke en form for padding. IDEA algoritmen krever en blokkstørrelse på 64 bit (8 byte) for å fungere, og det er her generalisert PKCS#5 padding kommer inn for å fylle ut resten av blokk. Det vil si at klarteksten M og en paddingsekvens S settes sammen til paddingsekvensen M. S består av n ( M mod n) oktetter med verdien n ( M mod n), hvor n er cipherens blokkstørrelse i oktetter. Eksempelvis kan vi forestille oss at vi deler den dataen som skal krypteres inn i 8 byte blokker, og i den siste blokken ender vi opp med kun 5 byte da. Ettersom IDEA krever at hver blokk skal være på 8 byte har vi ett 3 byte tomrom som må paddes ut. I henhold til PKCS#5 padder vi da ut disse bytene med verdien 3 (0x03 i hex). For å bedre vise hvordan dette gjøres, observer følgende eksempler (hvorav Data angir sekvenser av 8 bit data): I det siste eksemplet ser vi hva som skjer dersom all dataen tilfeldigvis fyller ut alle blokkene. Det en da gjør for å sikre at denne kodingen gir en utvetydig gjenoppretting av M fra M er at det legges til en ekstra blokk paddet helt ut. Følgene for dette er at vi nå med sikkerhet vet at den siste blokken alltid inneholder padding når denne da etter hvert skal fjernes igjen. I min implementasjon av PKCS#5 har jeg lagt inn en ekstra test på dataen som sjekker at en ikke prøver å fjerne padding fra data som ikke er paddet det vil si at siste byte i siste blokk må ha en gyldig verdi (mellom 0x01 og 0x08) i tillegg til at de bytene som er paddet må være paddet med samme verdi. Dersom disse forutsetningene ikke sammenfalles kastes en Exception med beskjeden Invalid padding detected. Fornevnte implementasjon er å finne i klassen OwnImplement og funksjonene getpaddedblocks( ) og removepadding( ). getpaddedblocks(...): Tar ett array av bytes som argument. Disse bytene konverteres til en serie datablokker og paddes ut i henhold til PKCS#5. Resultatet returneres i form av ett array datablokker. Det første som skjer er at antall tomrom som blir i den siste blokken kalkuleres. Det neste som skjer er at dersom det ikke ble noen tomrom legges det til en blokk paddet ut med 0x08 som vist i nederste eksempel i diagrammet ovenfor. Hvis dette ikke var tilfelle blir den siste blokken paddet ut. Når da dette er gjennomført returneres resultatet og det hele er klart til kryptering. 7
Implementasjon av PCKS#5: public DataBlock[] getpaddedblocks(byte[] data) { int leftover = data.length - ((data.length / 8) * 8); int blocks = (data.length / 8) + 1; DataBlock[] db; byte[] pad = {8, 8, 8, 8, 8, 8, 8, 8; if (leftover == 0) { db = new DataBlock[blocks]; for (int i = 0; i < (blocks - 1); i++) db[i] = new DataBlock(data, (i * 8)); db[blocks - 1] = new DataBlock(pad); else { db = new DataBlock[blocks]; for (int i = 0; i < (blocks - 1); i++) db[i] = new DataBlock(data, (i * 8)); for (int i = 0; i < leftover; i++) pad[i] = data[((blocks - 1) * 8) + i]; for (int i = 0; i < (8 - leftover); i++) pad[i + leftover] = (byte)(8 - leftover); db[blocks - 1] = new DataBlock(pad); return db; removepadding(...): Tar ett array av datablokker som argument. Den siste byten i den siste datablokken indikerer hvor mange bytes som skal fjernes. Hvis antallet bytes er 8, det vil si en hel blokk, fjernes denne blokken og resten av blokkene returneres. Deimplementasjon av PCKS#5: public DataBlock[] removepadding(datablock[] blocks) throws Exception { int remove = (int) blocks[blocks.length - 1].d[7]; if (remove == 8) { // Verifying that the datablock is sane for (int i = 0; i < 8; i++) if (blocks[blocks.length - 1].d[i]!= 8) throw new Exception( Invalid padding detected ); DataBlock[] temp = new DataBlock[blocks.length - 1]; for (int i = 0; i < (blocks.length - 1); i++) temp[i] = blocks[i]; return temp; else { // Verifying that the datablock is sane for (int i = 0; i < remove; i++) if (blocks[blocks.length - 1].d[7 - i]!= remove) throw new Exception( Invalid padding detected ); byte[] temp = new byte[8 - remove]; for (int i = 0; i < (8 - remove); i++) temp[i] = blocks[blocks.length - 1].d[i]; blocks[blocks.length - 1].d = temp; return blocks; 8
Cipher Block Chaining (CBC) Implementasjon: Ved kryptering av samme tekst flere ganger vil krypteringsalgoritmen alltid gi samme ciphertekst, og det er her Cipher moduser kommer inn. Den modusen vi benytter heter Cipher Block Chaining (heretter CBC). Kort sagt: Utfører en eksklusiv-eller operasjon på neste blokk klartekst med forrige genererte blokk ciphertekst. I første ledd benyttes i stedet initialiseringsvektor (IV) som forgående blokk. Den direkte implementasjonen av CBC er implementert i klassen OwnImplement i funksjonen encrypt( ). Funksjoner deler dataen som skal krypteres opp i en serie datablokker, rett og slett for å gi en mer ryddig implementasjon, som brukes under krypteringen. Se kode under for flere detaljer. encrypt( ): public byte[] encrypt(byte[] data) throws Exception { idea.initencrypt(key); DataBlock[] blocks = getpaddedblocks(data); DataBlock[] cipher = new DataBlock[blocks.length]; byte[] temp; for (int i = 0; i < blocks.length; i++) { if (i == 0) temp = xor(initvector, blocks[i].d); else temp = xor(cipher[i - 1].d, blocks[i].d); idea.blockencrypt(temp, 0, temp, 0); cipher[i] = new DataBlock(temp); return getbytes(cipher); Dekryptering foregår like enkelt fordi alle eksklusiv-eller operasjoner er sin egen invers, noe som vil si at prosessen mer eller mindre i reverseres. Se skisse og utdrag fra kildekoden under. 9
encrypt( ): public byte[] decrypt(byte[] data) throws Exception { idea.initdecrypt(key); DataBlock[] blocks = getblocks(data); DataBlock[] plain = new DataBlock[blocks.length]; byte[] temp = new byte[8]; for (int i = 0; i < (blocks.length); i++) { idea.blockencrypt(blocks[blocks.length - 1 - i].d, 0, temp, 0); if (i == (blocks.length - 1)) plain[blocks.length-1-i] = new DataBlock(xOr(initVector, temp)); else plain[blocks.length - 1 - i] = new DataBlock(xOr(blocks[blocks.length - 1 - i - 1].d, temp)); plain = removepadding(plain); return getbytes(plain); Selvtest Teste implementasjonen: Nå da både PKCS#5-padding og CBC var implementert gjenstår det bare og teste implementasjonen på de medfølgende ciphertekstene. Kildekoden til dette testprogrammet er vedlagt med filnavn Selvtest.java. En testgjennomkjøring gir følgende output: 1. Kokkos!! 2. Cogito Ergo Sum! 3. Dihydrogenmonoksid er et hvitt pulver. 4. 3.141592653589793238... 10
Del III Testing: JCE vs. Selvlaget: Nå som begge implementasjonene av CryptoEngine klassen i form av klassene JCEImplement og OwnImplement er ferdig implementert er testingen kun en liten gjenstående formalitet. Har implementert denne testen i egen fil kalt Testing.java. Appendix A Filoversikt: CryptoEngine.java: Hovedimplementasjonen av oppgaven og inneholder alle omtalte klasser og programstrukturer. Selvtest.java: Selvtest fra del II av oppgaven. Dekrypterer ciphertekstene oppgitt i oppgaven. Testing.java: Testen fra del III av oppgaven. Viser at begge implementasjonene kan snakke sammen. Bruker først min egen implementasjon til å kryptere en tekst og så JCE til å dekryptere den resulterende cipherteksten. Til sist blir det gjennomført en test om at den originale teksten og den dekrypterte teksten er 100% lik. Samme test blir også gjennomført der JCE krypterer og egen implementasjon dekrypterer. xxcrypt.java: Testfil fra del I av oppgaven. Viser bruk av JCE helt enkelt uten bruk av de mer avanserte strukturene fra filen CryptoEngine. 11