diff --git a/hw2/Homework 2.ipynb b/hw2/Homework 2.ipynb new file mode 100644 index 0000000..f9d2cf8 --- /dev/null +++ b/hw2/Homework 2.ipynb @@ -0,0 +1,547 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "421d5e4d-1327-449b-8aae-16cb1c99cb60", + "metadata": {}, + "source": [ + "Ondřej Hladůvka \n", + "\n", + "If you want to run code in this notebook, you can find it here: https://git.hladu.xyz/hladu357/TalTech_crypt/src/branch/master/hw2" + ] + }, + { + "cell_type": "markdown", + "id": "0ed8045c-d080-4288-909a-9190433d6a09", + "metadata": {}, + "source": [ + "## Task 1 Confusion and diffusion\n", + "From the lectures, you learned about importance of confusion and diffusion principles for the block ciphers. Let us examine them in more details. Assume you need to analyse\n", + "properties of Vigenere cipher." + ] + }, + { + "cell_type": "markdown", + "id": "c8eedd35-16bd-433d-b161-b1e9c7a26c19", + "metadata": {}, + "source": [ + "1. Encrypt message THEWANDCHOOSESTHEWIZARD with key MAGIC." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "47aba105-8f9d-40eb-a5ed-5f93bbe63b86", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ciphertext: FHKECZDIPQASKAVTECQBMRJ\n" + ] + } + ], + "source": [ + "def vigenere_enc(pt : str, key : str):\n", + " pt_int = [ord(i) - ord('A') for i in pt]\n", + " key_int = [ord(i) - ord('A') for i in key]\n", + "\n", + " ct_int = [(char + key_int[idx % len(key_int)]) % 26 for idx, char in enumerate(pt_int)]\n", + " ct = [chr(i + ord('A')) for i in ct_int]\n", + " return ct\n", + "\n", + "pt = \"THEWANDCHOOSESTHEWIZARD\"\n", + "key = \"MAGIC\"\n", + "ct = vigenere_enc(pt, key)\n", + "\n", + "print(\"ciphertext: \", \"\".join(ct))" + ] + }, + { + "cell_type": "markdown", + "id": "60d515f1-5951-4743-8286-4b41018fabc4", + "metadata": {}, + "source": [ + "2. Suppose we change one letter (W becomes L) in the plaintext to get THEWANDCHOOSESTHE L IZARD.\n", + "How may letters of the ciphertext are changed? Is the diffusion property achieved?" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "3c55e9ad-dd9f-48ee-b4fe-064dacb0706a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Witch changed plaintext at index 17\n", + "New ciphertext is: FHKECZDIPQASKAVTERQBMRJ\n", + "character changes at index 17 C -> R\n", + "diffusion is not achieved, as the plaintext's change effects the ciphertext in structured way exploitable by cryptanalysis\n" + ] + } + ], + "source": [ + "idx = 17\n", + "pt2 = pt[:idx] + \"L\" + pt[(idx + 1):]\n", + "ct2 = \"\".join(vigenere_enc(pt2, key))\n", + "\n", + "print(f\"Witch changed plaintext at index {idx}\")\n", + "print(f\"New ciphertext is: {ct2}\")\n", + "\n", + "for idx, (a, b) in enumerate(zip(ct, ct2)):\n", + " if (a != b):\n", + " print(f\"character changes at index {idx} {a} -> {b}\")\n", + "print(\"diffusion is not achieved, as the plaintext's change effects the ciphertext in structured way exploitable by cryptanalysis\")" + ] + }, + { + "cell_type": "markdown", + "id": "47b5e048-9f0a-4436-8294-fcba9fca489e", + "metadata": {}, + "source": [ + "3. Suppose we change one letter in key (G becomes N). How may letters of the ciphertext are changed?\n", + "Is confusion property achieved?" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "c89a36d4-6c24-4ca3-84c5-bafea87073c3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "it would change lower whole part n / k letters where n is length of plaintext and k is length of the key\n", + "floor(n / k) = floor(23 / 5) = 4\n", + "confusion is not achieved, as the key's effect on the ciphertext is structured and exploitable by cryptanalysis\n" + ] + } + ], + "source": [ + "import math\n", + "print(\"it would change lower whole part n / k letters where n is length of plaintext and k is length of the key\")\n", + "n = len(pt)\n", + "k = len(key)\n", + "print(f\"floor(n / k) = floor({n} / {k}) = {math.floor(n/k)}\")\n", + "print(\"confusion is not achieved, as the key's effect on the ciphertext is structured and exploitable by cryptanalysis\")" + ] + }, + { + "cell_type": "markdown", + "id": "1b4e0abb-fbcb-4019-9c4d-c8486d7c3884", + "metadata": {}, + "source": [ + "## Task 2. Pseudorandom function \n", + "Let F ∶ {0, 1}\n", + "n × {0, 1}\n", + "n → {0, 1}\n", + "n\n", + "be a secure PRF. Do the following\n", + "functions satisfy definition of pseudo-random function?" + ] + }, + { + "cell_type": "markdown", + "id": "3dc36d9b-46b1-4222-bd16-19b68356dd6a", + "metadata": {}, + "source": [ + "1. F′(k, m) = F(k, m) || 0^n, where 0^n is a zero string of length n" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "bd6cd25a-944c-4844-a82d-601c27a17a80", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Adversary with strategy that guesses its not oraculum if last n bits of the message are all zeros\n", + "can be wrong only if oraculum, was used (1/2 chance) and it generated last last n bits of the message all zeros (2^-n)\n", + "Chance of this is 2 to the power of minus (message length + 1)\n", + "With growing n, this function converges to 0 -> F' is not secure PRF\n", + "\n", + "\n", + "We can simulate this, assumming python random is close enough to random oraculum\n", + "For message 4 characters long, its only a 0.0625 chance\n", + "but only if random oracle is selected so times 0.5 -> 0.03125 chance\n", + "adverasry got response: [1, 1, 1, 1, 0, 0, 0, 0]\n", + "he guessed it is not oraculum - its False\n", + "\n", + "adverasry got response: [0, 1, 0, 0, 0, 0, 0, 0]\n", + "he guessed it is not oraculum - its True\n", + "\n", + "adverasry got response: [0, 0, 1, 0, 0, 0, 0, 0]\n", + "he guessed it is not oraculum - its True\n", + "\n", + "adverasry got response: [1, 1, 1, 0, 0, 0, 0, 0]\n", + "he guessed it is not oraculum - its True\n", + "\n", + "adverasry got response: [0, 0, 0, 1, 0, 0, 0, 0]\n", + "he guessed it is not oraculum - its True\n", + "\n", + "adverasry got response: [1, 1, 0, 0, 0, 1, 0, 0]\n", + "he guessed it is oraculum - its True\n", + "\n", + "we can meassure the prediction:\n", + "adversary passed 971 out of 1000 tests, which is 0.029 chance of failure\n" + ] + } + ], + "source": [ + "# no efficient algorithm can distinguish (with significant advantage)\n", + "# between a function chosen randomly from the PRF family and a random oracle\n", + "import random\n", + "import string\n", + "msg_len = 4\n", + "\n", + "def prf1(input):\n", + " return [random.randint(0, 1) for _ in range(len(input))] + [0] * len(input)\n", + "\n", + "def challenger1(input):\n", + " if (random.choice([True, False])): # random oracle\n", + " return [random.randint(0, 1) for _ in range(len(input) * 2)], True\n", + " else: # tested function\n", + " return prf1(input), False\n", + "\n", + "words = [\"abcd\", \"efgh\", \"ijkl\", \"mnop\", \"qrst\", \"uvwx\"]\n", + "def adversary1(w, verbose=True):\n", + " response = challenger1(w)\n", + " if verbose: print(f\"adverasry got response: {response[0]}\")\n", + " \n", + " if all(x == 0 for x in response[0][len(w):]):\n", + " if verbose: print(f\"he guessed it is not oraculum - its {response[1] == False}\\n\")\n", + " return response[1] == False\n", + " else:\n", + " if verbose: print(f\"he guessed it is oraculum - its {response[1] == True}\\n\")\n", + " return response[1] == True\n", + "\n", + "\n", + "print(\"Adversary with strategy that guesses its not oraculum if last n bits of the message are all zeros\")\n", + "print(\"can be wrong only if oraculum, was used (1/2 chance) and it generated last last n bits of the message all zeros (2^-n)\")\n", + "print(\"Chance of this is 2 to the power of minus (message length + 1)\")\n", + "print(\"With growing n, this function converges to 0 -> F' is not secure PRF\\n\\n\")\n", + "\n", + "chance = pow(2,-msg_len)\n", + "print(f\"We can simulate this, assumming python random is close enough to random oraculum\")\n", + "print(f\"For message {msg_len} characters long, its only a {chance} chance\")\n", + "print(f\"but only if random oracle is selected so times 0.5 -> {chance/2} chance\")\n", + "\n", + "for w in words:\n", + " adversary1(w)\n", + "\n", + "tests = 1000\n", + "passed = 0\n", + "print(f\"we can meassure the prediction:\")\n", + "for i in range(0, tests):\n", + " msg = ''.join(random.choices(string.ascii_lowercase, k=msg_len))\n", + " passed += adversary1(msg, False)\n", + "\n", + "print(f\"adversary passed {passed} out of {tests} tests, which is {round(1 - (passed / tests),5)} chance of failure\")" + ] + }, + { + "cell_type": "markdown", + "id": "06d4358a-0216-457a-ab08-3662f1999b89", + "metadata": {}, + "source": [ + "2. F′(k, m∣∣m′) = F(k, m)∣∣F(k, m′ ⊕ 0^n), where 0^n is a zero string of length n" + ] + }, + { + "cell_type": "markdown", + "id": "f99e71fa-a4e2-4396-8072-9621b40700fc", + "metadata": {}, + "source": [ + "Since x ⊕ 0 = x, we can simplify\n", + "F' := F(k, m∣∣m′) = F'(k, m)∣∣F'(k, m′)\n", + "\n", + "With startegy: \n", + "choose message x\n", + "x = a^n (where n is power of 2)\n", + "\n", + "get message y \n", + "if message is repeating bit => adversary guess its not oraculum \n", + "y = F'(a^(n/2) || F'(a^(n/2)) = F'(a^(n/4)) || F'(a^(n/4)) || F'(a^(n/4)) || F'(a^(n/4)) = ... = F'(a)^n\n", + "\n", + "else => guess its oraculum\n", + "\n", + "Only way adversary could be wrong if oraculum generated n repeating bits, chance of this is 2^(-n) \n", + "With growing n, this function converges to 0 -> F' is not secure PRF" + ] + }, + { + "cell_type": "markdown", + "id": "fc89bafe-990c-4a05-9bd4-1c86075f4fcd", + "metadata": {}, + "source": [ + "3. F′(k, m∣∣m′) = F(k, 0∣∣m) ⊕ F(k, m′∣∣1), where m, m′∈ {0, 1}^(n−1)" + ] + }, + { + "cell_type": "markdown", + "id": "7fccba29-9283-448a-83e5-0a9e1dca8864", + "metadata": {}, + "source": [ + "With startegy:\n", + "querying two messages x_1, x_2 \n", + "x_1 = 0^(n-1)||0^(n-1) \n", + "x_2 = 0^(n-1)||1^(n-1) \n", + " \n", + "get messages y_1, y_2 \n", + "\n", + "\n", + "if y_1 ⊕ y_2 == 0^n => adversary guess its not oraculum \n", + "y_1 ⊕ y_2 = (F(k,0||0^(n−1)) ⊕ F(k,0^(n−1)||1)) ⊕ (F(k,0||0^(n−1)) ⊕ F(k,1^(n−1||1)) \n", + "y_1 ⊕ y_2 = F(k,0||1^(n−1)) ⊕ F(k,1^(n−1||1) \n", + " \n", + "else => guess its oraculum \n", + " \n", + "chance of two random oraculum outputs being inverse (y_1 ⊕ y_2 == 0^n) is 2^(-n) \n", + "With growing n, this function converges to 0 -> F' is not secure PRF" + ] + }, + { + "cell_type": "markdown", + "id": "e62ff1ca-3ea5-4f08-9b2d-286966f45a7e", + "metadata": {}, + "source": [ + "## Task 3\n", + "Output feedback mode Consider the following permutation cipher – instead of permuting plaintext letters to get ciphertext, you are first required to convert plaintext letters to binary form and next you\n", + "permute bits according to the key. Letter H becomes encrypted to T with key (5, 1, 2, 4, 3). Let us view it as\n", + "block cipher with block length 5 bits." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "9ca5e5f2-19ae-4d1d-aad1-eecc6b9d07e3", + "metadata": {}, + "outputs": [], + "source": [ + "def perm_enc(pt: list[int], key: list[int]):\n", + " blocksize = 5\n", + " ct = [0] * blocksize\n", + " for idx in range(0, blocksize):\n", + " ct[idx] = pt[key[idx] - 1]\n", + " return ct\n", + "\n", + "def convert_str(pt: list[int]): # converts to list of binary blocks\n", + " bit_array = []\n", + " for char in pt:\n", + " bits = bin(ord(char) - ord('A'))[2:][-5:].zfill(5)\n", + " bit_array.append([int(bit) for bit in bits])\n", + " return bit_array\n", + "\n", + "def ofb(pt, key, iv, cipher):\n", + " ct = []\n", + " for block in pt:\n", + " iv = cipher(iv, key)\n", + " ct.append([x ^ y for (x,y) in zip(iv, block)]) # encrypted iv into block\n", + " return ct\n", + "\n", + "key = [5, 1, 2, 4, 3]" + ] + }, + { + "cell_type": "markdown", + "id": "95d3d5f0-26cc-4adf-ba2f-3e19aac52174", + "metadata": {}, + "source": [ + "1. Encrypt world DOG with key (4, 1, 3, 5, 2) using permutation cipher in OFB mode with iv = 01011. Leave\n", + "result as a binary string." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "63f150c4-cefc-4288-9568-6c0ed4963d7b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "plaintext: [[0, 0, 0, 1, 1], [0, 1, 1, 1, 0], [0, 0, 1, 1, 0]]\n", + "ciphertext: [[1, 0, 0, 0, 0], [1, 0, 1, 0, 0], [1, 1, 1, 1, 1]]\n" + ] + } + ], + "source": [ + "pt = convert_str(['D', 'O', 'G'])\n", + "key = [4, 1, 3, 5, 2]\n", + "iv = [0, 1, 0, 1, 1]\n", + "ct = ofb(pt, key, iv, perm_enc)\n", + "print(f\"plaintext: {pt}\")\n", + "print(f\"ciphertext: {ct}\")" + ] + }, + { + "cell_type": "markdown", + "id": "2e92ea0e-ccab-4eb7-bb0d-5f325fcd8e77", + "metadata": {}, + "source": [ + "2. Flip 5-th bit of received ciphertext (0 becomes 1 and vice versa). Now decrypt modified ciphertext. How\n", + "many bits in the plaintext get changed?" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "ae47fd93-809c-4e03-b565-3d1ebb8f4b42", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "original ciphertext: [[1, 0, 0, 0, 0], [1, 0, 1, 0, 0], [1, 1, 1, 1, 1]]\n", + "modified ciphertext: [[1, 0, 0, 0, 1], [1, 0, 1, 0, 0], [1, 1, 1, 1, 1]]\n", + "original plaintext: [[0, 0, 0, 1, 1], [0, 1, 1, 1, 0], [0, 0, 1, 1, 0]]\n", + "modified plaintext: [[0, 0, 0, 1, 0], [0, 1, 1, 1, 0], [0, 0, 1, 1, 0]]\n", + "one bit also flipped -> no diffusion\n" + ] + } + ], + "source": [ + "import copy\n", + "ct2 = copy.deepcopy(ct)\n", + "ct2[0][4] ^= 1\n", + "pt2 = ofb(ct2, key, iv, perm_enc)\n", + "\n", + "print(f\"original ciphertext: {ct}\")\n", + "print(f\"modified ciphertext: {ct2}\")\n", + "print(f\"original plaintext: {pt}\")\n", + "print(f\"modified plaintext: {pt2}\")\n", + "print(f\"one bit also flipped -> no diffusion\")" + ] + }, + { + "cell_type": "markdown", + "id": "dd8b801d-8bcc-49dc-8ad1-b0d54ce62959", + "metadata": {}, + "source": [ + "3. Flip the first bit of the IV iv′ = 11011, decrypt ciphertext from Step 1 with iv′\n", + ". How many bits in the\n", + "plaintext get changed?" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "a358aa91-7e8a-4ada-82f0-58fdb7aa30a9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "original plaintext: [[0, 0, 0, 1, 1], [0, 1, 1, 1, 0], [0, 0, 1, 1, 0]]\n", + "modified plaintext: [[0, 1, 0, 1, 1], [0, 1, 1, 1, 1], [0, 0, 1, 0, 0]]\n", + "one bit per block also flipped\n" + ] + } + ], + "source": [ + "iv[0] ^= 1\n", + "pt3 = ofb(ct, key, iv, perm_enc)\n", + "\n", + "print(f\"original plaintext: {pt}\")\n", + "print(f\"modified plaintext: {pt3}\")\n", + "print(f\"one bit per block also flipped\")" + ] + }, + { + "cell_type": "markdown", + "id": "fc3e1681-f07b-4546-bc30-688753fcbd53", + "metadata": {}, + "source": [ + "## Task 4\n", + "Consider the encryption of n−block message m = m1∣∣m2∣∣. . .∣∣m_n by some block cipher E in\n", + "CFB mode. Let use denote ciphertext produced by E as c = c1∣∣c2∣∣. . .∣∣cn. Show which information about\n", + "the plaintext can be extracted if we get a collision: ci = cj\n", + ", where i ≠ j." + ] + }, + { + "cell_type": "markdown", + "id": "7134ee9b-fcc0-4d6e-a240-208707160694", + "metadata": {}, + "source": [ + "#### In OFB mode: \n", + "Ek(iv)^n := Ek( Ek( ... Ek(iv))) n times\n", + "ct_n = pt_n ⊕ Ek(iv)^n\n", + "\n", + "ct_i == ct_j implies => \n", + "Ek(iv)^i == Ek(iv)^j => \n", + "pt_i ⊕ pt_j == Ek(iv)^(i-1) ⊕ Ek(iv)^(j-1)" + ] + }, + { + "cell_type": "markdown", + "id": "7e65fe31-62e3-4c6c-a7a8-e29ee8dc9e57", + "metadata": {}, + "source": [ + "## Task 5\n", + "Show that Pigpen cipher defined below is not IND-OT-CPA secure (where adversary is allowed to\n", + "do only one query to the challenger in the IND-CPA game).\n", + "The pigpen cipher uses graphical symbols assigned according to a key in the diagram below1\n", + "(NOTE: Positions\n", + "of the letters in the diagrams are random and not known to adversary):" + ] + }, + { + "cell_type": "markdown", + "id": "296118e7-4df4-451f-8b27-9a317c04d362", + "metadata": {}, + "source": [ + "#### Indistinguishability under One-Time Chosen Plaintext Attack definition:\n", + "1. The challenger generates a secret key K\n", + "2. The adversary submits two distinct plaintexts Pt_0, Pt_1 of equal length to the challenger\n", + "3. The challenger selects a random bit b ∈ {0,1}, resulting in ciphertext Ct = Ek(M_b) being sent to the adversary\n", + "4. Based on the Ct, the adversary guess b′ ∈ {0,1} for the value of b\n", + "5. Scheme is not secure if no adversary has a non-negligible advantage in guessing over 1/2\n", + "\n", + "#### Adversary strategy:\n", + "We can expoit Pigpen cipher being monoalphabetic\n", + "Adversary will send one message with repeating characters and another with each character unique \n", + "M_0 := \"AA\" \n", + "M_1 := \"AB\" \n", + "\n", + "If Ct is some repeating characters => choose 0 \n", + "Else => choose 1 \n", + "\n", + "This strategy will work every time thanks to monoalphabeticity" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/hw2/run.sh b/hw2/run.sh new file mode 100755 index 0000000..3e122ca --- /dev/null +++ b/hw2/run.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +docker run -p 8888:8888 \ + -v "$(pwd)":/work \ + jupyter \ No newline at end of file