Krüptograafia
Krüptograafia on matemaatika ja arvutiteaduse haru, mis tegeleb informatsiooni salastamise ning kehtivuse ja omaniku kontrollimisega. Krüptograafias väljatöötatud meetodid võimaldavad selliseid laialdaselt kasutavaid funktsioone nagu autentimine, andmete krüpteerimine ja digitaalne allkirjastamine. Eelnimetatud funktsioonid on kriitilise tähtsusega kaasaegses infoühiskonnas toimivate andme- ja internetipõhiste teenuste toimimisel, nagu nt mobiilside ja e-post, krediitkaartid, pangaautomaadid ja internetipank, turvalised võrguühendused jne. Eesti kodanike e-identiteet põhineb talle väljastatud ID-kaartil peituva avaliku sertifikaadi ja salajase võtme toimemehhhanismile. Eesti ID-kaartide sertifikaate haldab skidsolutions: https://www.skidsolutions.eu/ . Eesti kodaniku avaliku sertifikaadi nägemiseks piisab veebilehitseja aadressiribale kopeerida ldap://esteid.ldap.sk.ee:636/c=EE??sub?(cn=perenimi,eesnimi,isikukood) , kus perenimi, eesnimi ja isikukood tuleb asendada enda või huvipakkuva inimese vastavate andmetega. Seejärel avaneb sertifikaadi omaniku kontaktinfo. Navigeerides avanenud aknas isiku nimi --> Properties --> IDs --> [email protected] --> Properties, avaneb vastava inimese avalik sertifikaat.
Vajutades tabalukule ning seejärel sõnale "Sertifikaat", avaneb sisuliselt sama ülesehitusega sertifikaat nagu ka inimese puhul:
Suurte algarvude genereerimine
Et moodustada turvalisi krüptograafilisi võtmeid, on vaja esmalt leida suuri algarve. Antud naturaalarvu korral on erinevaid meetodeid kontrollimaks, kas ta on algarv. Üks praktilisemaid viise algarvulisust testida on kasutades Fermat' väikest teoreemi: Naturaalarv $p$ on algarv, kui iga naturaalarvu $a \in [2, p-1]$ korral kehtib implikatsioon \[ (a,p) = 1 \Rightarrow a^{p-1} \equiv 1 \; (\text{mod} \; p), \] kus $(a,p)$ on täisarvu $a$ ja algarvu $p$ suurim ühistegur. Jäeldus: Olgu $p \gt 2$ algarv ja $a$ täisarv ning $p \nmid a$. Esitame paarisarvu $p-1$ kujul $p-1 = 2^s \cdot t$, kus $s \ge 0$ ja $t$ on paaritu arv. Siis kas $a^t \equiv 1 \; (\text{mod} \; p)$ või leidub $0 \leq r \lt s$ nii, et $a^{2^r \cdot t} \equiv -1 \; (\text{mod} \; p)$. Fermat' väikest teoreemi kasutatakse Miller-Rabini algarvulisuse testis. Kui naturaalarvu $n$ korral leidub täisarv $a$ nii, et $(a,n) = 1$ ja $n-1 = 2^s \cdot t$, kus $t$ on paaritu arv ning $a^t \neq 1 \; (\text{mod} \; p)$ ja $a^{2^i \cdot t} \neq -1 \; (\text{mod} \; p)$ iga $i = 0,1,...,s-1$ korral, siis $n$ on kordarv. Seega algarvulisuse testimiseks anname arvule $a$ erinevaid täisarvulisi väärtusi lõigust $[2,n-1]$. Kui $n$ on paarisarv või $(a,n) \gt 1$, siis $n$ on ilmselt kordarv. Vastasel korral avaldame $n$ kujul $n-1 = 2^s \cdot t$ ja leiame järjest $a^t, \; (a^t)^2 = a^{2t}, a^{2^2t}, \; ... \; a^{2^{s-1} \cdot t} \; (\text{mod} \; n)$. Kui $a^t$ ei ole kongruentne arvuga $1$ mooduli $p$ järgi ja $a^{2^i \cdot t}$ ei ole kongruentne arvuga $-1$ iga $i = 0,1,...,s-1$ korral, siis $a$ on Miller-Rabini tunnistaja ja $n$ on kordarv. Vastasel korral, testides mitmete erinevate $a$ väärtustega, on $n$ tõenäoliselt algarv. Olgu $n$ paaritu kordarv. Siis vähemalt 75% täisarvude $1 \lt a \lt n$ korral on $n$ kordarv.Autentimine
RSA on asümmeetriline avaliku võtme krüptosüsteem. Asümmeetriline tähendab siin seda, et kodeerimine ja dekodeerimine toimuvad erinevate võtmetega. Muuhulgas kasutab RSA krüptosüsteemi Eesti ID-kaart, millele on talletatud digitaalallkirjastamisel kasutatavad avalik ja salajane võti. Asümmeetria tõttu saab viimaseid kasutada ka tekstide kodeerimiseks, aga see ei ole eriti otstarbekas ega levinud. RSA algoritm töötab järgmise skeemi aluse. Kodeerimiseks ja dekodeerimiseks on vajalikud avalik ja salajane võti. Kuna iga digitaalset suurust saab esitada naturaalarvuna, siis need ongi lihtsalt kaks naturaalarvu. Võtmete leidmiseks kasutatakse järgmist skeemi: 1. valime kaks algarvu $p$ ja $q$ (siin on kasulikud algarvulisuse testid), mis praktilistel kaalutlustel peaksid olema umbkaudu samas suurusjärgus 2. arvutame mooduli $n = p \cdot q$ 3. leiame $\varphi(n) = (p-1) \cdot (q-1)$ 4. valime sellise avaliku astendaja $1 \lt e \lt \varphi(n)$, et $(e,\varphi(n)) = 1$ 5. leiame salajase astendaja $1 \lt d \lt \varphi(n)$ nii, et $ed \equiv 1 \; (\text{mod} \; \varphi(n))$ Avalik võti on arvupaar $(n,e)$ ja salajane võti on paar $(n,d)$. Kuna $d$ arvutati $\varphi(n), \; p$ ja $q$ abil, siis tuleb ka viimased salajas hoida. Avalik võti tehakse järgmistes protsessides osalejatele teatavaks, salajane võti peab jääma ainult kodeerija enda teada.Salajase võtme edastamine
Diffie-Hellmani võtmevahetus on meetod salajase informatsiooni (üldiselt on selleks mingi krüptosüsteemi salajased võtmed või nende osad) jagamiseks üle ebaturvalise kanali (näiteks avalik internet). Skeem on iseenesest järgmine: 1. valime lõpliku tsüklilise rühma $G$ ja selle moodustaja $g$ (sageli $G = \mathbb{Z}_{p}^{\ast}$ ja $g$ on algjuur mooduli $p$ järgi) 2. osapool $A$ valib juhusliku naturaalarvu $k$ ja saadab teisele osapoolele $B$ elemendi $g^k$ 3. osapool $B$ valib juhusliku naturaalarvu $l$ ja saadab $A$-le tagasi $g^l$ 4. $A$ arvutab $(g^l)^k$ ja $B$ arvutab $(g^k)^l$ 5. kuna mistahes rühmas $(g^l)^k = g^{kl} = (g^k)^l$, siis on mõlemal osapoolel nüüd jagatud saladus $g^{kl}$.Digitaalne allkirjastamine
Algoritmi parameetrid: 1. Vali krüptograafiline räsifunktsioon $H$, mille väljund on $H$-bitine. 2. Vali võtme pikkus, harilikult suurusega $512 \le 64*n \le 1024$, kus $n$ on naturaalarv. 3. Vali mooduli pikkus $N$ nii, et $N < L$ ja $N \le |H|$. Harilikult on $N$ ja $L$ paarid (1024,160), (2048,224), (2048, 256) või (3072, 256). 4. Vali $N$-bitine algarv $q$. 5. Vali $L$-bitine algarv $p$ nii, $q$ jagab $p-1$. 6. Vali juhuslik arv $h \in \{ 2,...,p-2 \}$. 7. Arvuta $g:= h^{(p-1)/q} \mod p$. Algoritmi parameetrid on $(p,q,g)$. Kasutajaspetsiifilised võtmed: 1. Vali juhuslik täisarv $x$ hulgast $\{ 1,...,q-1 \}$; 2. Arvuta $y:= g^x \mod p$; $x$ on privaatne võti ja $y$ on avalik võti. Digitaalne allkirjastamine: 1. Vali juhuslik arv $k$ hulgast $\{1,...,q-1\}$; 2. Arvuta $r := (g^k \mod p) \mod q$. Juhul kui $r=0$, vali uus $k$ ja korda $r$ arvutamist; 3. Arvuta $s := (k^{-1} (H(m) + xr)) \mod q$, kus $m$ on allkirjastatav sõnum ja $H$ on räsifunktsioon. Juhul, kui $s = 0$, vali uus $k$ ja alusta 2. sammust uuesti. Saadud allkirjaks on $(r,s)$. Allkirja $(r,s)$ kontrollimine sõnumi $m$ jaoks toimib järgnevalt: 1. Veendu, et $0 < r < q$ ja $0 < s < q$; 2. Arvuta $w := s^{-1} \mod q$; 3. Arvuta $u_1 := H(m) \cdot w \mod q$; 4. Arvuta $u_2 := r \cdot w \mod q$; 5. Arvuta $v := (g^{u_1} y^{u_2} \mod p) \mod q$; Kui $v = r$, siis digitaalallkiri on kehtiv, vastasel juhul mitte.Krüpteerimine
package com.prometheus.crypto.impl;
import java.util.HashMap;
import java.util.Map;
/**
* Advanced Encryption Standard (AES) encryption algorithm, implemented by the specification:
* https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf
*/
public class EncrypterImpl {
public static final int reductionPolynomial = 0b100011011;
public byte[] encrypt(byte[] message, byte[] key) {
int keySizeBits = key.length*8;
int rounds;
if(keySizeBits == 128) rounds = 10;
else if (keySizeBits == 192) rounds = 12;
else if(keySizeBits == 256)rounds = 14;
else throw new RuntimeException("UnsupportedKeySizeException");
byte[][] state = new byte[4][4];
byte[] bytes = message;
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 4; j++) {
state[j][i] = bytes[4*i+j];
}
}
byte[][] keyExpansion = keyExpansion(key);
state = addRoundKey(state, getRoundKey(keyExpansion,0));
for(int r = 1; r <= rounds; r++) {
byte[][] roundKey = getRoundKey(keyExpansion,r);
state = subBytes(state, false);
state = shiftRows(state, false);
if(r < rounds) state = mixColumns(state, false);
state = addRoundKey(state, roundKey);
}
byte[] resultBytes = new byte[16];
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 4; j++) {
resultBytes[4*i+j] = state[j][i];
}
}
return resultBytes;
}
public byte[] decrypt(byte[] message, byte[] key) {
int keySizeBits = key.length*8;
int rounds;
if(keySizeBits == 128) rounds = 10;
else if (keySizeBits == 192) rounds = 12;
else if(keySizeBits == 256)rounds = 14;
else throw new RuntimeException("UnsupportedKeySizeException");
byte[][] state = new byte[4][4];
byte[] bytes = message;
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 4; j++) {
state[j][i] = bytes[4*i+j];
}
}
byte[][] keyExpansion = keyExpansion(key);
state = addRoundKey(state, getRoundKey(keyExpansion,rounds));
for(int r = 1; r <= rounds; r++) {
byte[][] roundKey = getRoundKey(keyExpansion,rounds-r);
state = shiftRows(state, true);
state = subBytes(state, true);
state = addRoundKey(state, roundKey);
if(r < rounds) state = mixColumns(state, true);
}
byte[] resultBytes = new byte[16];
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 4; j++) {
resultBytes[4*i+j] = state[j][i];
}
}
return resultBytes;
}
public byte[][] getRoundKey(byte[][] keyExpansion, int round) {
byte[][] roundKey = new byte[4][4];
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 4; j++) {
roundKey[i][j] = keyExpansion[4*round+j][i];
}
}
return roundKey;
}
/**
* Applies Rijandel S-box to every element of the 4x4 state matrix.
*/
public byte[][] subBytes(byte[][] state, boolean inverse) {
byte[][] result = new byte[4][4];
for(int i = 0; i < state.length; i++) {
for(int j = 0; j < state[0].length; j++) {
result[i][j] = applySBox(state[i][j], inverse);
}
}
return result;
}
/**
* Circular left-shift n-th row vector of the state matrix by n places
*/
public byte[][] shiftRows(byte[][] state, boolean inverse) {
byte[][] shiftedState = new byte[4][4];
for(int i = 0; i < state.length; i++) {
byte[] row = state[i];
byte[] shiftedRow = new byte[4];
for(int j = 0; j < row.length; j++) {
if(inverse) shiftedRow[j] = row[(4+j-i)%4];
else shiftedRow[j] = row[(j+i)%4];
}
shiftedState[i] = shiftedRow;
}
return shiftedState;
}
/**
* Multiplies each column of a state with a specific multiplier matrix.
*/
public byte[][] mixColumns(byte[][] state, boolean inverse) {
byte[][] multiplier = null;
if(inverse) {
multiplier = new byte[][]{
{0x0e,0x0b,0x0d,0x09},
{0x09,0x0e,0x0b,0x0d},
{0x0d,0x09,0x0e,0x0b},
{0x0b,0x0d,0x09,0x0e}
};
} else {
multiplier = new byte[][]{
{0x02,0x03,0x01,0x01},
{0x01,0x02,0x03,0x01},
{0x01,0x01,0x02,0x03},
{0x03,0x01,0x01,0x02}
};
}
byte[][] resultState = new byte[4][4];
for(int i = 0; i < 4; i++) {
byte[][] column = {{state[0][i]}, {state[1][i]}, {state[2][i]}, {state[3][i]}};
byte[][] product = multiplyMatricesFiniteField(multiplier, column);
for(int j = 0; j < 4; j++) {
resultState[j][i] = product[j][0];
}
}
return resultState;
}
/**
* Multiplying matrices in GF(2^8), where each entry of a matrix is
* represented as a polynomial of rank 7.
*/
public byte[][] multiplyMatricesFiniteField(byte[][] m1, byte[][] m2) {
byte[][] result = new byte[m1.length][m2[0].length];
int rows = m1.length;
int cols = m2[0].length;
for(int i = 0; i < rows; i++) {
int sum = 0;
for(int j = 0; j < cols; j++) {
for(int k = 0; k < 4; k++) {
sum ^= multiplyPolynomialsMod(m1[i][k],m2[k][j], reductionPolynomial);
}
result[i][j] = (byte)sum;
}
}
return result;
}
public int polynomialMod(int poly, int modParam) {
int a = poly;
int mod = modParam;
while (Integer.toBinaryString(a).length() >= Integer.toBinaryString(mod).length()) {
String bin1 = Integer.toBinaryString(a);
String binMod = Integer.toBinaryString(mod);
String binModNew = Integer.toBinaryString(mod << (bin1.length() - binMod.length()));
//System.out.println(bin1);
//System.out.println("mod = " + binModNew);
int newMod = Integer.parseInt(binModNew, 2);
a ^= newMod;
//System.out.println("a = " + Integer.toBinaryString(a));
}
return a;
}
public int multiplyPolynomials(byte b1, byte b2) {
int product = 0;
int b11 = Byte.toUnsignedInt(b1);
int b22 = Byte.toUnsignedInt(b2);
for(int i = 0; i < 8; i++) {
if(((b22 >> i) & 1) == 1) {
product ^= (b11 << i);
}
}
return product;
}
public int multiplyPolynomialsMod(byte b1, byte b2, int mod) {
int product = multiplyPolynomials(b1,b2);
int quotient = polynomialMod(product, mod);
return quotient;
}
/**
* Combines the state matrix with the given round key by xor-ing corresponding entries.
*/
public byte[][] addRoundKey(byte[][] state, byte[][] roundKey) {
byte[][] result = new byte[4][4];
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 4; j++) {
result[i][j] = (byte)(state[i][j] ^ roundKey[i][j]);
}
}
return result;
}
/**
* Expands the encryption key into 4,6 or 8 round keys depending on the key size.
*/
public byte[][] keyExpansion(byte[] key) {
int keySizeBits = key.length*8;
//Define N as the length of the key in 32-bit words:
//4 words for AES-128, 6 words for AES-192, and 8 words for AES-256
int N = 0;
if(keySizeBits == 128) N = 4;
else if (keySizeBits == 192) N = 6;
else if(keySizeBits == 256)N = 8;
else throw new RuntimeException("UnsupportedKeySizeException");
//Define K_0, K_1, ... K_(N-1) as the 32-bit words of the original key
byte[][] K = new byte[N][4];
byte[] keyBytes = key;
for(int i = 0; i < N; i++) {
byte[] subBytes = new byte[]{keyBytes[4*i], keyBytes[4*i+1], keyBytes[4*i+2], keyBytes[4*i+3]};
K[i] = subBytes;
}
//Define W_0, W_1, ... W_(4R-1) as the 32-bit words of the expanded key
int R = 0;
if(keySizeBits == 128) R = 11;
else if (keySizeBits == 192) R = 13;
else if(keySizeBits == 256)R = 15;
else throw new RuntimeException("UnsupportedKeySizeException");
byte[][] W = new byte[4*R][4];
for(int i = 0; i < 4*R; i++) {
if(i < N)
W[i] = K[i];
else if(i >= N && i%N == 0)
W[i] = applyXor(applyXor(W[i-N],subWord(rotWord(W[i-1]),false)),
new byte[]{(byte)rConst(i/N), 0, 0, 0});
else if(i >= N && N > 6 && i%4 == 0)
W[i] = applyXor(W[i-N], rotWord(W[i-1]));
else W[i] = applyXor(W[i-N], W[i-1]);
/*
StringBuilder builder = new StringBuilder();
builder.append("W["+i+"]=");
builder.append(Integer.toHexString(W[i][0])+" ");
builder.append(Integer.toHexString(W[i][1])+" ");
builder.append(Integer.toHexString(W[i][2])+" ");
builder.append(Integer.toHexString(W[i][3])+" ");
System.out.println(new String(builder));
*/
}
return W;
}
public byte[] applyXor(byte[] arr1, byte[] arr2) {
int len = arr1.length;
byte[] result = new byte[len];
for(int i = 0; i < len; i++) {
result[i] = (byte)(arr1[i] ^ arr2[i]);
}
return result;
}
/**
* Performs a cyclic permutation on a given 4-byte word
*/
public byte[] rotWord(byte[] word) {
byte[] rotated = new byte[word.length];
for(int i = 0; i < word.length; i++) {
rotated[i] = word[(i+1)%word.length];
}
return rotated;
}
public byte[] subWord(byte[] word, boolean inverse) {
byte[] subWord = new byte[word.length];
for(int i = 0; i < subWord.length; i++) {
subWord[i] = applySBox(word[i], inverse);
}
return subWord;
}
/**
* Apply Rijandel S-box to a given byte element
*/
public byte applySBox(byte b, boolean inverse) {
String hexStr = Integer.toHexString(b);
if(hexStr.length() == 1) hexStr = "0"+hexStr;
String firstIndexStr = hexStr.substring(hexStr.length()-2,hexStr.length()-1);
String secondIndexStr = hexStr.substring(hexStr.length()-1,hexStr.length());
String[] hex = {"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"};
Map hexToDec = new HashMap<>();
for(int i = 0; i < hex.length; i++) {
hexToDec.put(hex[i], Integer.toString(i));
}
int firstIndex = Integer.parseInt(hexToDec.get(firstIndexStr));
int secondIndex = Integer.parseInt(hexToDec.get(secondIndexStr));
//byte result = (byte)sBox[firstIndex][secondIndex];
byte inv = inverse(b);
int intInv = Byte.toUnsignedInt(inv);
int intByte = Byte.toUnsignedInt(b);
byte result = 0;
if(inverse) {
result = (byte)(
leftCircShift(intByte,1) ^
leftCircShift(intByte,3) ^
leftCircShift(intByte,6) ^ 0x5);
} else {
result = (byte)(inv ^ leftCircShift(intInv,1) ^
leftCircShift(intInv,2) ^
leftCircShift(intInv,3) ^
leftCircShift(intInv,4) ^ 0x63);
}
return inverse ? inverse(result) : result;
}
public byte inverse(byte A) {
if(A == 0) return 0;
for(byte i = -128; i < 128; i++) {
int product = multiplyPolynomialsMod(i, A,reductionPolynomial);
if(product == 1) {
return i;
}
}
return 0;
}
public byte rightCircShift(byte b, int k) {
return (byte)((b >>> k) | (b << (Byte.SIZE - k)));
}
public int leftCircShift(int b, int k) {
return ((b << k) | (b >>> (Byte.SIZE - k)));
}
public String byteToBinaryStr(byte b) {
return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0');
}
/**
* Derives a round constant used in key expansion
*/
public int rConst(int round) {
int rci = 0;
if(round == 1) rci = 1;
else if(round > 1 && rConst(round-1) < 0x80) rci = ((2 * rConst(round-1)));
else if(round > 1 && rConst(round-1) >= 0x80) rci = (byte)((2 * rConst(round-1)) ^ 0x1b);
else rci = 0;
return rci;
}
}