diff --git a/.DS_Store b/.DS_Store index 4f80154..3dd59b4 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/encrypted.bin b/encrypted.bin new file mode 100644 index 0000000..87ec1fd --- /dev/null +++ b/encrypted.bin @@ -0,0 +1 @@ +U2FsdGVkX1+SsKEug91LbdSIWzu7KyjBo9/7v4btDVY= diff --git a/myfile.txt b/myfile.txt new file mode 100644 index 0000000..980a0d5 --- /dev/null +++ b/myfile.txt @@ -0,0 +1 @@ +Hello World! diff --git a/z_jupyter/01_fundamentals.ipynb b/z_jupyter/01_fundamentals.ipynb new file mode 100644 index 0000000..791990c --- /dev/null +++ b/z_jupyter/01_fundamentals.ipynb @@ -0,0 +1,978 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "509b908c", + "metadata": {}, + "source": [ + "# Fundamentals\n", + "Within cryptography our aim is often to secure a message between Bob and Alice, and so that Eve - the adversory - cannot view or change a message that is passed between them. Along with Bob and Alice, we have other cybersecurity actors, such as Peggy (the prover) and Victor (the verifier):\n", + "\n", + "![Alt text](graphics/g_fun01.png \"Bob and Alice, and others\")\n", + "\n", + "This workbook outlines of the key fundamentals that are used in cryptography, including the usage of binary operators, ciphertext representation, Boolean operators and prime numbers. " + ] + }, + { + "cell_type": "markdown", + "id": "e8a9af38", + "metadata": {}, + "source": [ + "## Introduction\n", + "\n", + "In order to implement security, we often take plaintext and convert it into ciphertext:\n", + "\n", + "![Alt text](graphics/g_fun04.png \"Plaintext to ciphertext\")\n", + "\n", + "With a program, we often have numerical values (integers or floating point) or characters and strings. So, while the plaintext can contain characters that can be viewable, with binary we often have to represent the byte values with Base64 or in a hexadecimal form.\n" + ] + }, + { + "cell_type": "markdown", + "id": "37c0d953", + "metadata": {}, + "source": [ + "## Hex, binary, decimal and Base64\n", + "The conversion of byte values into a hexademical form involves us breaking the byte vakues into 4 bit nibbles, and then representing each of these values with a hexadecimal value. For example, if we have a byte pattern of\n", + "\n", + "```\n", + "01111111 01010001\n", + "```\n", + "We can then split up the byte values to give:\n", + "\n", + "```\n", + "0111 1111 0101 0001\n", + "7 F 5 1\n", + "```\n", + "An example of this is:\n", + "\n", + "![Alt text](graphics/g_fun05.png \"Hexademical\")\n", + "\n", + "Another typical format is Base64, and where we take 6 bits at a time and represent it with a Base64 character:\n", + "\n", + "![Alt text](graphics/g_fun06.png \"Base64\")\n", + "\n", + "For a pattern of:\n", + "\n", + "```\n", + "01000001 01000010 \n", + "```\n", + "We can then split up the byte values to give:\n", + "\n", + "```\n", + "010000 010100 0010 [00] \n", + "Q U I =\n", + "```\n", + "This gives us \"QUI=\", and where we pad with zeros to fill up slots of six bits, and then pad with \"=\" characters to give a multiple of four characters within the Base64 string. In Python, we can represent a hex, octal and binary value with 0x, 0o and 0b at the start of the value. We can then convert into decimal, hex, octal and binary with the int(), hex(), oct() and bin() methods:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "8c8fae3f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "21 Int=21, Hex=0x15, Oct=0o25\n", + "34 Bin=0b100010, Hex=0x22, Oct=0o42, Dec=34\n" + ] + } + ], + "source": [ + "val1=0b10101\n", + "val2=int(val1)\n", + "val3=hex(val1)\n", + "val4=oct(val1)\n", + "\n", + "print(f\"{val1} Int={val2}, Hex={val3}, Oct={val4}\")\n", + "\n", + "val1=0o42\n", + "val2=bin(val1)\n", + "val3=hex(val1)\n", + "val4=oct(val1)\n", + "val5=int(val1)\n", + "\n", + "print(f\"{val1} Bin={val2}, Hex={val3}, Oct={val4}, Dec={val5}\")" + ] + }, + { + "cell_type": "markdown", + "id": "5f33519c", + "metadata": {}, + "source": [ + "> Modify the program so that you can determine the hex, octal and binary value for the decimal value of 42.\n", + "\n", + "> Modify the program so that you can determine the decimal, octal and binary value for the hex value of 0x42.\n", + "\n", + "> Modify the program so that you can determine the hex, decimal, and binary value for the octal value of 0o42.\n", + "\n", + "> Modify the program so that is shows the following table (note: use for \"i in range()\"):\n", + "\n", + "```\n", + "Val Hex Oct Binary\n", + "0 0x0 0o0 0b0\n", + "1 0x1 0o1 0b1\n", + "2 0x2 0o2 0b10\n", + "3 0x3 0o3 0b11\n", + "4 0x4 0o4 0b100\n", + "5 0x5 0o5 0b101\n", + "6 0x6 0o6 0b110\n", + "7 0x7 0o7 0b111\n", + "8 0x8 0o10 0b1000\n", + "9 0x9 0o11 0b1001\n", + "10 0xa 0o12 0b1010\n", + "11 0xb 0o13 0b1011\n", + "12 0xc 0o14 0b1100\n", + "13 0xd 0o15 0b1101\n", + "14 0xe 0o16 0b1110\n", + "15 0xf 0o17 0b1111 \n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "73426b52", + "metadata": {}, + "source": [ + "## Byte conversions\n", + "In cryptography, we often operate on byte arrays and need to convert from byte array formats into other formats, such as for ASCII string values. For this, we can use the binascii library, such as using hexlify() to convert a hex string into a into a byte array, and with b2a_hex() to perform the reverse operation: " + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "9129b9e6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "String abcdefg is b'61626364656667' in bytes\n", + "Bytes b'3631363236333634363536363637' is 3631363236333634363536363637 as a string\n" + ] + } + ], + "source": [ + "import binascii\n", + "\n", + "a=\"abcdefg\"\n", + "b=binascii.hexlify(a.encode())\n", + "\n", + "print(f\"String {a} is {b} in bytes\")\n", + "\n", + "\n", + "c=binascii.b2a_hex(b)\n", + "\n", + "print(f\"Bytes {c} is {c.decode()} as a string\")" + ] + }, + { + "cell_type": "markdown", + "id": "b72f269f", + "metadata": {}, + "source": [ + "## Bit operations\n", + "While we mostly deal with bytes in cryptography, we might also modify bit values. For this we can use the bitwise AND, OR and XOR operations. For this we get:\n", + "```\n", + "a b AND(a,b) OR(a,b) XOR(a,b)\n", + "0 0 0 0 0\n", + "0 1 0 1 1\n", + "1 0 0 1 1\n", + "1 1 1 1 0\n", + "```\n", + "\n", + "We can then use the bitwise operations of & (AND), | (OR) and ^ (XOR). For example we can specify in binary, and then perform bitwise operations. In this case we will display the results in a binary format:" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "8eaccf0e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0x32 AND 0b1000011 = 0b10\n", + "0b110010 OR 0b1000011 = 0b1110011\n", + "0b110010 XOR 0b1000011 = 0b1110001\n", + "0 0b0 0b10101010 0b10101010\n", + "1 0b0 0b10101011 0b10101011\n", + "2 0b10 0b10101010 0b10101000\n", + "3 0b10 0b10101011 0b10101001\n", + "4 0b0 0b10101110 0b10101110\n", + "5 0b0 0b10101111 0b10101111\n", + "6 0b10 0b10101110 0b10101100\n", + "7 0b10 0b10101111 0b10101101\n", + "8 0b1000 0b10101010 0b10100010\n", + "9 0b1000 0b10101011 0b10100011\n", + "10 0b1010 0b10101010 0b10100000\n", + "11 0b1010 0b10101011 0b10100001\n", + "12 0b1000 0b10101110 0b10100110\n", + "13 0b1000 0b10101111 0b10100111\n", + "14 0b1010 0b10101110 0b10100100\n", + "15 0b1010 0b10101111 0b10100101\n", + "16 0b0 0b10111010 0b10111010\n", + "17 0b0 0b10111011 0b10111011\n", + "18 0b10 0b10111010 0b10111000\n", + "19 0b10 0b10111011 0b10111001\n", + "20 0b0 0b10111110 0b10111110\n", + "21 0b0 0b10111111 0b10111111\n", + "22 0b10 0b10111110 0b10111100\n", + "23 0b10 0b10111111 0b10111101\n", + "24 0b1000 0b10111010 0b10110010\n", + "25 0b1000 0b10111011 0b10110011\n", + "26 0b1010 0b10111010 0b10110000\n", + "27 0b1010 0b10111011 0b10110001\n", + "28 0b1000 0b10111110 0b10110110\n", + "29 0b1000 0b10111111 0b10110111\n", + "30 0b1010 0b10111110 0b10110100\n", + "31 0b1010 0b10111111 0b10110101\n", + "32 0b100000 0b10101010 0b10001010\n", + "33 0b100000 0b10101011 0b10001011\n", + "34 0b100010 0b10101010 0b10001000\n", + "35 0b100010 0b10101011 0b10001001\n", + "36 0b100000 0b10101110 0b10001110\n", + "37 0b100000 0b10101111 0b10001111\n", + "38 0b100010 0b10101110 0b10001100\n", + "39 0b100010 0b10101111 0b10001101\n", + "40 0b101000 0b10101010 0b10000010\n", + "41 0b101000 0b10101011 0b10000011\n", + "42 0b101010 0b10101010 0b10000000\n", + "43 0b101010 0b10101011 0b10000001\n", + "44 0b101000 0b10101110 0b10000110\n", + "45 0b101000 0b10101111 0b10000111\n", + "46 0b101010 0b10101110 0b10000100\n", + "47 0b101010 0b10101111 0b10000101\n", + "48 0b100000 0b10111010 0b10011010\n", + "49 0b100000 0b10111011 0b10011011\n", + "50 0b100010 0b10111010 0b10011000\n", + "51 0b100010 0b10111011 0b10011001\n", + "52 0b100000 0b10111110 0b10011110\n", + "53 0b100000 0b10111111 0b10011111\n", + "54 0b100010 0b10111110 0b10011100\n", + "55 0b100010 0b10111111 0b10011101\n", + "56 0b101000 0b10111010 0b10010010\n", + "57 0b101000 0b10111011 0b10010011\n", + "58 0b101010 0b10111010 0b10010000\n", + "59 0b101010 0b10111011 0b10010001\n", + "60 0b101000 0b10111110 0b10010110\n", + "61 0b101000 0b10111111 0b10010111\n", + "62 0b101010 0b10111110 0b10010100\n", + "63 0b101010 0b10111111 0b10010101\n", + "64 0b0 0b11101010 0b11101010\n", + "65 0b0 0b11101011 0b11101011\n", + "66 0b10 0b11101010 0b11101000\n", + "67 0b10 0b11101011 0b11101001\n", + "68 0b0 0b11101110 0b11101110\n", + "69 0b0 0b11101111 0b11101111\n", + "70 0b10 0b11101110 0b11101100\n", + "71 0b10 0b11101111 0b11101101\n", + "72 0b1000 0b11101010 0b11100010\n", + "73 0b1000 0b11101011 0b11100011\n", + "74 0b1010 0b11101010 0b11100000\n", + "75 0b1010 0b11101011 0b11100001\n", + "76 0b1000 0b11101110 0b11100110\n", + "77 0b1000 0b11101111 0b11100111\n", + "78 0b1010 0b11101110 0b11100100\n", + "79 0b1010 0b11101111 0b11100101\n", + "80 0b0 0b11111010 0b11111010\n", + "81 0b0 0b11111011 0b11111011\n", + "82 0b10 0b11111010 0b11111000\n", + "83 0b10 0b11111011 0b11111001\n", + "84 0b0 0b11111110 0b11111110\n", + "85 0b0 0b11111111 0b11111111\n", + "86 0b10 0b11111110 0b11111100\n", + "87 0b10 0b11111111 0b11111101\n", + "88 0b1000 0b11111010 0b11110010\n", + "89 0b1000 0b11111011 0b11110011\n", + "90 0b1010 0b11111010 0b11110000\n", + "91 0b1010 0b11111011 0b11110001\n", + "92 0b1000 0b11111110 0b11110110\n", + "93 0b1000 0b11111111 0b11110111\n", + "94 0b1010 0b11111110 0b11110100\n", + "95 0b1010 0b11111111 0b11110101\n", + "96 0b100000 0b11101010 0b11001010\n", + "97 0b100000 0b11101011 0b11001011\n", + "98 0b100010 0b11101010 0b11001000\n", + "99 0b100010 0b11101011 0b11001001\n", + "100 0b100000 0b11101110 0b11001110\n", + "101 0b100000 0b11101111 0b11001111\n", + "102 0b100010 0b11101110 0b11001100\n", + "103 0b100010 0b11101111 0b11001101\n", + "104 0b101000 0b11101010 0b11000010\n", + "105 0b101000 0b11101011 0b11000011\n", + "106 0b101010 0b11101010 0b11000000\n", + "107 0b101010 0b11101011 0b11000001\n", + "108 0b101000 0b11101110 0b11000110\n", + "109 0b101000 0b11101111 0b11000111\n", + "110 0b101010 0b11101110 0b11000100\n", + "111 0b101010 0b11101111 0b11000101\n", + "112 0b100000 0b11111010 0b11011010\n", + "113 0b100000 0b11111011 0b11011011\n", + "114 0b100010 0b11111010 0b11011000\n", + "115 0b100010 0b11111011 0b11011001\n", + "116 0b100000 0b11111110 0b11011110\n", + "117 0b100000 0b11111111 0b11011111\n", + "118 0b100010 0b11111110 0b11011100\n", + "119 0b100010 0b11111111 0b11011101\n", + "120 0b101000 0b11111010 0b11010010\n", + "121 0b101000 0b11111011 0b11010011\n", + "122 0b101010 0b11111010 0b11010000\n", + "123 0b101010 0b11111011 0b11010001\n", + "124 0b101000 0b11111110 0b11010110\n", + "125 0b101000 0b11111111 0b11010111\n", + "126 0b101010 0b11111110 0b11010100\n", + "127 0b101010 0b11111111 0b11010101\n", + "128 0b10000000 0b10101010 0b101010\n", + "129 0b10000000 0b10101011 0b101011\n", + "130 0b10000010 0b10101010 0b101000\n", + "131 0b10000010 0b10101011 0b101001\n", + "132 0b10000000 0b10101110 0b101110\n", + "133 0b10000000 0b10101111 0b101111\n", + "134 0b10000010 0b10101110 0b101100\n", + "135 0b10000010 0b10101111 0b101101\n", + "136 0b10001000 0b10101010 0b100010\n", + "137 0b10001000 0b10101011 0b100011\n", + "138 0b10001010 0b10101010 0b100000\n", + "139 0b10001010 0b10101011 0b100001\n", + "140 0b10001000 0b10101110 0b100110\n", + "141 0b10001000 0b10101111 0b100111\n", + "142 0b10001010 0b10101110 0b100100\n", + "143 0b10001010 0b10101111 0b100101\n", + "144 0b10000000 0b10111010 0b111010\n", + "145 0b10000000 0b10111011 0b111011\n", + "146 0b10000010 0b10111010 0b111000\n", + "147 0b10000010 0b10111011 0b111001\n", + "148 0b10000000 0b10111110 0b111110\n", + "149 0b10000000 0b10111111 0b111111\n", + "150 0b10000010 0b10111110 0b111100\n", + "151 0b10000010 0b10111111 0b111101\n", + "152 0b10001000 0b10111010 0b110010\n", + "153 0b10001000 0b10111011 0b110011\n", + "154 0b10001010 0b10111010 0b110000\n", + "155 0b10001010 0b10111011 0b110001\n", + "156 0b10001000 0b10111110 0b110110\n", + "157 0b10001000 0b10111111 0b110111\n", + "158 0b10001010 0b10111110 0b110100\n", + "159 0b10001010 0b10111111 0b110101\n", + "160 0b10100000 0b10101010 0b1010\n", + "161 0b10100000 0b10101011 0b1011\n", + "162 0b10100010 0b10101010 0b1000\n", + "163 0b10100010 0b10101011 0b1001\n", + "164 0b10100000 0b10101110 0b1110\n", + "165 0b10100000 0b10101111 0b1111\n", + "166 0b10100010 0b10101110 0b1100\n", + "167 0b10100010 0b10101111 0b1101\n", + "168 0b10101000 0b10101010 0b10\n", + "169 0b10101000 0b10101011 0b11\n", + "170 0b10101010 0b10101010 0b0\n", + "171 0b10101010 0b10101011 0b1\n", + "172 0b10101000 0b10101110 0b110\n", + "173 0b10101000 0b10101111 0b111\n", + "174 0b10101010 0b10101110 0b100\n", + "175 0b10101010 0b10101111 0b101\n", + "176 0b10100000 0b10111010 0b11010\n", + "177 0b10100000 0b10111011 0b11011\n", + "178 0b10100010 0b10111010 0b11000\n", + "179 0b10100010 0b10111011 0b11001\n", + "180 0b10100000 0b10111110 0b11110\n", + "181 0b10100000 0b10111111 0b11111\n", + "182 0b10100010 0b10111110 0b11100\n", + "183 0b10100010 0b10111111 0b11101\n", + "184 0b10101000 0b10111010 0b10010\n", + "185 0b10101000 0b10111011 0b10011\n", + "186 0b10101010 0b10111010 0b10000\n", + "187 0b10101010 0b10111011 0b10001\n", + "188 0b10101000 0b10111110 0b10110\n", + "189 0b10101000 0b10111111 0b10111\n", + "190 0b10101010 0b10111110 0b10100\n", + "191 0b10101010 0b10111111 0b10101\n", + "192 0b10000000 0b11101010 0b1101010\n", + "193 0b10000000 0b11101011 0b1101011\n", + "194 0b10000010 0b11101010 0b1101000\n", + "195 0b10000010 0b11101011 0b1101001\n", + "196 0b10000000 0b11101110 0b1101110\n", + "197 0b10000000 0b11101111 0b1101111\n", + "198 0b10000010 0b11101110 0b1101100\n", + "199 0b10000010 0b11101111 0b1101101\n", + "200 0b10001000 0b11101010 0b1100010\n", + "201 0b10001000 0b11101011 0b1100011\n", + "202 0b10001010 0b11101010 0b1100000\n", + "203 0b10001010 0b11101011 0b1100001\n", + "204 0b10001000 0b11101110 0b1100110\n", + "205 0b10001000 0b11101111 0b1100111\n", + "206 0b10001010 0b11101110 0b1100100\n", + "207 0b10001010 0b11101111 0b1100101\n", + "208 0b10000000 0b11111010 0b1111010\n", + "209 0b10000000 0b11111011 0b1111011\n", + "210 0b10000010 0b11111010 0b1111000\n", + "211 0b10000010 0b11111011 0b1111001\n", + "212 0b10000000 0b11111110 0b1111110\n", + "213 0b10000000 0b11111111 0b1111111\n", + "214 0b10000010 0b11111110 0b1111100\n", + "215 0b10000010 0b11111111 0b1111101\n", + "216 0b10001000 0b11111010 0b1110010\n", + "217 0b10001000 0b11111011 0b1110011\n", + "218 0b10001010 0b11111010 0b1110000\n", + "219 0b10001010 0b11111011 0b1110001\n", + "220 0b10001000 0b11111110 0b1110110\n", + "221 0b10001000 0b11111111 0b1110111\n", + "222 0b10001010 0b11111110 0b1110100\n", + "223 0b10001010 0b11111111 0b1110101\n", + "224 0b10100000 0b11101010 0b1001010\n", + "225 0b10100000 0b11101011 0b1001011\n", + "226 0b10100010 0b11101010 0b1001000\n", + "227 0b10100010 0b11101011 0b1001001\n", + "228 0b10100000 0b11101110 0b1001110\n", + "229 0b10100000 0b11101111 0b1001111\n", + "230 0b10100010 0b11101110 0b1001100\n", + "231 0b10100010 0b11101111 0b1001101\n", + "232 0b10101000 0b11101010 0b1000010\n", + "233 0b10101000 0b11101011 0b1000011\n", + "234 0b10101010 0b11101010 0b1000000\n", + "235 0b10101010 0b11101011 0b1000001\n", + "236 0b10101000 0b11101110 0b1000110\n", + "237 0b10101000 0b11101111 0b1000111\n", + "238 0b10101010 0b11101110 0b1000100\n", + "239 0b10101010 0b11101111 0b1000101\n", + "240 0b10100000 0b11111010 0b1011010\n", + "241 0b10100000 0b11111011 0b1011011\n", + "242 0b10100010 0b11111010 0b1011000\n", + "243 0b10100010 0b11111011 0b1011001\n", + "244 0b10100000 0b11111110 0b1011110\n", + "245 0b10100000 0b11111111 0b1011111\n", + "246 0b10100010 0b11111110 0b1011100\n", + "247 0b10100010 0b11111111 0b1011101\n", + "248 0b10101000 0b11111010 0b1010010\n", + "249 0b10101000 0b11111011 0b1010011\n", + "250 0b10101010 0b11111010 0b1010000\n", + "251 0b10101010 0b11111011 0b1010001\n", + "252 0b10101000 0b11111110 0b1010110\n", + "253 0b10101000 0b11111111 0b1010111\n", + "254 0b10101010 0b11111110 0b1010100\n", + "255 0b10101010 0b11111111 0b1010101\n" + ] + } + ], + "source": [ + "\n", + "a=0x32\n", + "b=0x43\n", + "\n", + "c=a & b\n", + "d=a | b\n", + "e=a ^ b\n", + "\n", + "print(f\"{hex(a)} AND {bin(b)} = {bin(c)}\")\n", + "print(f\"{bin(a)} OR {bin(b)} = {bin(d)}\")\n", + "print(f\"{bin(a)} XOR {bin(b)} = {bin(e)}\")\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "fb34d492", + "metadata": {}, + "source": [ + "> Check the Boolean operations to see that the program works.\n", + "\n", + "> Using the program, determine the result of 0b10101010 XOR 0b10101010.\n", + "\n", + "> Modify the program so that you can specify hexademical values, and then display in a hexademical format.\n", + "\n", + "> Modify the program so that we have a loop from 0 to 255 (note: use for \"i in range()\"), and AND, OR and XOR is a value of 0b10101010. A sample run for the first few outputs is:\n", + "\n", + "```\n", + "i& AND\tOR\t XOR\n", + "0b0 0b0\t0b10101010\t0b10101010\n", + "0b1 0b0\t0b10101011\t0b10101011\n", + "0b10 0b10\t0b10101010\t0b10101000\n", + "0b11 0b10\t0b10101011\t0b10101001\n", + "0b100 0b0\t0b10101110\t0b10101110\n", + "0b101 0b0\t0b10101111\t0b10101111\n", + "0b110 0b10\t0b10101110\t0b10101100\n", + "0b111 0b10\t0b10101111\t0b10101101\n", + "0b1000 0b1000\t0b10101010\t0b10100010\n", + "0b1001 0b1000\t0b10101011\t0b10100011\n", + "0b1010 0b1010\t0b10101010\t0b10100000\n", + "0b1011 0b1010\t0b10101011\t0b10100001\n", + "0b1100 0b1000\t0b10101110\t0b10100110\n", + "0b1101 0b1000\t0b10101111\t0b10100111\n", + "0b1110 0b1010\t0b10101110\t0b10100100\n", + "0b1111 0b1010\t0b10101111\t0b10100101\n", + "0b10000 0b0\t0b10111010\t0b10111010\n", + "0b10001 0b0\t0b10111011\t0b10111011\n", + "0b10010 0b10\t0b10111010\t0b10111000\n", + "0b10011 0b10\t0b10111011\t0b10111001\n", + "0b10100 0b0\t0b10111110\t0b10111110\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "3634d7ce", + "metadata": {}, + "source": [ + "## Modulo operation\n", + "\n", + "One of the most basic operations in cryptography is to use the modulo operation, and which gives the remainder of an integer division. For 17 modulo 5, we get 2. In most cases, with cryptography, we use the modulo of a prime number (p), and use (mod p) operation, such as:\n", + "\n", + "```\n", + "A = B (mod p)\n", + "```\n", + "\n", + "The operator for modulo operations is defined with \"%\". " + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "e870306b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "15434 (mod 127) = 67\n" + ] + } + ], + "source": [ + "a=15434\n", + "p=127\n", + "\n", + "b = a%p\n", + "\n", + "print(f\"{a} (mod {p}) = {b}\")" + ] + }, + { + "cell_type": "markdown", + "id": "29be2ce3", + "metadata": {}, + "source": [ + "> Verify that the program is working correctly. Then determine the following:\n", + "```\n", + "15434 (mod 127) = ?\n", + "546324 (mod 997) = ?\n", + "905463124 (mod 524287) = ?\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "b0d38eae", + "metadata": {}, + "source": [ + "## Prime numbers\n", + "\n", + "Many areas of cryptography use prime numbers, and which are integer values which can only be divisable by itself and 1. Every other integer - known as composite numbers - can be made up of a multiplication of prime numbers. Examples of prime numbers are 13, 97 and 997. The following composite numbers can be factorized into the multiplication of prime numbers:\n", + "\n", + "```\n", + "33 = 3 x 11\n", + "532 = 2 x 2 x 7 x 19\n", + "2,542 = 2 x 31 x 41\n", + "743,243 = 193 x 3,851\n", + "1,234,567,890 = 2 x 3 x 3 x 5 x 3,607 x 3,803\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "0b9e3720", + "metadata": {}, + "source": [ + "In the following, we will use the libnum library to generate a random prime number with a given number of bits:" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "id": "fd09fcb6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Random: 812768180957 Length: 40\n", + "\n", + "Prime (p): 560263049029. Length: 40 bits, Digits: 12\n" + ] + } + ], + "source": [ + "import libnum\n", + "\n", + "bitsize=40\n", + "\n", + "r=libnum.randint_bits(bitsize)\n", + "\n", + "print (\"Random: %d Length: %d\" % (r,libnum.len_in_bits(r)))\n", + "\n", + "\n", + "p=libnum.generate_prime(bitsize)\n", + "\n", + "print (\"\\nPrime (p): %d. Length: %d bits, Digits: %d\" % (p,libnum.len_in_bits(p), len(str(p)) )) " + ] + }, + { + "cell_type": "markdown", + "id": "5a1a3c85", + "metadata": {}, + "source": [ + "another library is Crypto:" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "id": "6ec3d9cb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "No of bits in prime is 64\n", + "\n", + "Random n-bit Prime (p): 11916865567959269669\n" + ] + } + ], + "source": [ + "import Crypto.Util.number\n", + "\n", + "import sys\n", + "\n", + "bits=64\n", + "\n", + "print (\"No of bits in prime is \",bits)\n", + "\n", + "p=Crypto.Util.number.getPrime(bits, randfunc=Crypto.Random.get_random_bytes)\n", + "print (\"\\nRandom n-bit Prime (p): \",p)\n" + ] + }, + { + "cell_type": "markdown", + "id": "dc11d9d0", + "metadata": {}, + "source": [ + "> Generate a 64-bit random prime number. And use this page to determine if it is prime [here](https://asecuritysite.com/primes/testprime).\n", + "\n", + "> Generate a 256-bit random prime number. And use this page to determine if it is prime [here](https://asecuritysite.com/primes/testprime).\n", + "\n", + "> By observing the last few digits of a number, which numbers are obviously not prime numbers?" + ] + }, + { + "cell_type": "markdown", + "id": "d2d3bd5d", + "metadata": {}, + "source": [ + "## Finite Fields\n", + "Within cryptography, we typically constrain our operations within a finite field. This is normally defined by a prime number (p), and where we have values between 0 and (p-1). This can be defined with the (mod p) operator. For this, we need to define a mapping from A to B, and which can be reversed back from B to A. For the add operation with a (mod p) operation, we can reverse with a subtraction:\n", + "\n", + "![Alt text](graphics/g_fun03.png \"Finite field\")\n", + "\n", + "For a logarithmic operation, we should be able to map all the values in A to B:\n", + "\n", + "![Alt text](graphics/g_fun02.png \"Finite field\")\n", + "\n", + "In the following, we will determine the mapping of values when we add and subtract 5 using the (mod p) operation:" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "d4b145d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0+5 (mod 7)=5\n", + "0-5 (mod 7)=2\n", + "1+5 (mod 7)=6\n", + "1-5 (mod 7)=3\n", + "2+5 (mod 7)=0\n", + "2-5 (mod 7)=4\n", + "3+5 (mod 7)=1\n", + "3-5 (mod 7)=5\n", + "4+5 (mod 7)=2\n", + "4-5 (mod 7)=6\n", + "5+5 (mod 7)=3\n", + "5-5 (mod 7)=0\n", + "6+5 (mod 7)=4\n", + "6-5 (mod 7)=1\n" + ] + } + ], + "source": [ + "\n", + "b=5\n", + "p=7\n", + "\n", + "for a in range(p):\n", + " c=(a+b) % p\n", + " d=(a-b) % p\n", + " print(f\"{a}+{b} (mod {p})={c}\")\n", + " print(f\"{a}-{b} (mod {p})={d}\")\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "6867a166", + "metadata": {}, + "source": [ + "\n", + "> Verify that for each value of a, that we get a unique mapping to a value in the range of 0 to p-1.\n", + "> Verify that we can add a given value to a number, and then subtract the same value to get the original value back. Note the value should be between 0 and p-1." + ] + }, + { + "cell_type": "markdown", + "id": "528f0427", + "metadata": {}, + "source": [ + "## Inverse mod\n", + "As we are using integer values, the perform a division by finding the inverse mod of a value, and then multiplying it with another value. For a divided by b, we get:\n", + "\n", + "```\n", + "Inv_b = pow(b,-1,p)\n", + "a_b = a * Inv_b\n", + "```\n", + "\n", + "In the following, we will multiply a by b, and then reverse by dividing by b:" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "id": "dcb77c31", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "a=13, b=6\n", + "13 times 6 (mod p)=2\n", + "Inverse 6 (mod 19)=16\n", + "2 divided by 6 (mod 19)=13\n" + ] + } + ], + "source": [ + "a = 13\n", + "b= 6\n", + "p=19\n", + "\n", + "c=a*b % p\n", + "\n", + "print(f\"a={a}, b={b}\")\n", + "\n", + "print(f\"{a} times {b} (mod p)={c}\")\n", + "\n", + "Inv_b=pow(b,-1,p)\n", + "\n", + "print(f\"Inverse {b} (mod {p})={Inv_b}\")\n", + "\n", + "d = c*Inv_b %p\n", + "print(f\"{c} divided by {b} (mod {p})={d}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "4127f768", + "metadata": {}, + "source": [ + "> For a prime number of 19, determine the inverse mod of 6?\n", + "\n", + "> For a prime number of 97, determine the inverse mod of 6?\n", + "\n", + "> For a prime number of 997, a=101 and b=97. Compute c = a times b (mod p), and show that c divided by b (mod p) will give a." + ] + }, + { + "cell_type": "markdown", + "id": "d4676b59", + "metadata": {}, + "source": [ + "We can also use the libnum library to compute the inverse mod value, such as:" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "349b3ff1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Result=54\n" + ] + } + ], + "source": [ + "import libnum\n", + "\n", + "p=997\n", + "a=54\n", + "b=62\n", + "\n", + "c= a*b %p\n", + "\n", + "Inv_b = libnum.invmod(b,p)\n", + "\n", + "res = (c* Inv_b) % p\n", + "\n", + "print(f\"Result={res}\")" + ] + }, + { + "cell_type": "markdown", + "id": "b24407b8", + "metadata": {}, + "source": [ + "## GCD (Greatest Common Divisor)\n", + "Another basic operator in cryptography is GCD, and which returns the largest value that can be divided into a defined value. For example, the GCD(60, 12) is 12, as 12 is the largest divider of 60 and 12. In the following we will use the libnum library to determine the GCD of two numbers:" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "id": "3565fb67", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "GCD(802,72)=2\n" + ] + } + ], + "source": [ + "from libnum import gcd\n", + "a=56\n", + "b=72\n", + "\n", + "print(f\"GCD({a},{b})={gcd(a,b)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "9c647834", + "metadata": {}, + "source": [ + "> Run the program, and verify its operation with a few examples.\n", + "\n", + "> What is the result of GCD(802,72)?\n", + "\n", + "> What is the result of GCD(997,81)?" + ] + }, + { + "cell_type": "markdown", + "id": "ee22a1f0", + "metadata": {}, + "source": [ + "Overall, we tend to use this operation to determine when two values do not share the same factor, and thus often look for:\n", + "\n", + "GCD(a,b)==1" + ] + }, + { + "cell_type": "markdown", + "id": "f5e2f3fc", + "metadata": { + "vscode": { + "languageId": "html" + } + }, + "source": [ + "> Determine if 91 and 27 share any factors" + ] + }, + { + "cell_type": "markdown", + "id": "84b8a11d", + "metadata": {}, + "source": [ + "# Exponentiation cipher\n", + "Exponentiation ciphers use a form of C=M^e (mod p) to encrypt and decrypt a message (M) using an encryption key of e and a prime number p. It is used in Pohlig-Hellman and RSA. In the following, we use two methods of computing the exponentiation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8e266cc", + "metadata": {}, + "outputs": [], + "source": [ + "# Cipher = M^e (mod p)\n", + "\n", + "M=1234567890\n", + "e=65537\n", + "p=2**255-1\n", + "\n", + "C=M**e % p # Slow for large values\n", + "print (f\"Cipher = {C}\")\n", + "\n", + "C=pow(M,e,p) # Much faster\n", + "print (f\"Cipher = {C}\")" + ] + }, + { + "cell_type": "markdown", + "id": "4c49a2c7", + "metadata": {}, + "source": [ + "> Which of the two methods is the fastest?\n", + "> Make the message value much larger, what effect does it have on the computation?\n", + "> Why is the pow() method faster?\n", + "> If you create d=pow(e,-1,p), and then try M=pow(C,d,p), what is the result?" + ] + }, + { + "cell_type": "markdown", + "id": "4c083835", + "metadata": {}, + "source": [] + } + ], + "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.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/01_introduction.ipynb b/z_jupyter/01_introduction.ipynb deleted file mode 100644 index 6c4bbf4..0000000 --- a/z_jupyter/01_introduction.ipynb +++ /dev/null @@ -1,309 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "ddb45b14", - "metadata": {}, - "source": [ - "# Lab 1: Cryptography Fundamentals" - ] - }, - { - "cell_type": "markdown", - "id": "27a217f9", - "metadata": {}, - "source": [ - "Objective: The key objective of this lab is to be introduced to some of the fundamental principles involved in cryptography, including the usage of Base-64, hexadecimal, the modulus operator some basic operators (such as AND, OR, X-OR, Rotate Right and Rotate Left), and prime numbers. This lab also involves cracking puzzles, and which have been added to get you to think about the methods involved in cipher cracking. You can undertake the additional challenges if you want to further develop your cryptography skills.\n", - "\n", - "Go to vsoc2.napier.ac.uk and find your folder. Run your Ubuntu instance demo. The virtual machine's password is napier123. Lab demo: (Note that you will be using Ubuntu, while the demo shows Kali). A demo of the lab is here." - ] - }, - { - "cell_type": "markdown", - "id": "c3d1c955", - "metadata": {}, - "source": [ - "## A.1 Is prime?\n", - "\n", - "Using: here\n", - "\n", - "Test for the following prime numbers: 91: [Yes] [No]\n", - "\n", - "421: [Yes] [No]\n", - "\n", - "1449: [Yes] [No]" - ] - }, - { - "cell_type": "markdown", - "id": "7c70d292", - "metadata": {}, - "source": [ - "## A.2 gcd\n", - "\n", - "Using: here\n", - "\n", - "Determine the GCD for the following: 88, 46:\n", - "\n", - "105, 35:" - ] - }, - { - "cell_type": "markdown", - "id": "22cfed86", - "metadata": {}, - "source": [ - "## A.3 Base-64 convertor\n", - "\n", - "Using: here\n", - "\n", - "Determine the Base 64 and Hex values for the following strings:\n", - "\n", - "Hello:\n", - "\n", - "hello:\n", - "\n", - "HELLO:" - ] - }, - { - "cell_type": "markdown", - "id": "41348372", - "metadata": {}, - "source": [ - "## A.4 Base-64 convertor\n", - "\n", - "Using: here\n", - "\n", - "Determine the following ASCII strings for these encoded formats:\n", - "\n", - "bGxveWRz\n", - "6E6170696572\n", - "01000001 01101110 01101011 01101100 01100101 00110001 00110010 00110011\n" - ] - }, - { - "cell_type": "markdown", - "id": "cb873433", - "metadata": {}, - "source": [ - "## A.5 The mod operator\n", - "\n", - "Using Python, what is the result of 53,431 (mod 453)?\n", - "\n", - "In Python, this is:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2d9dc064", - "metadata": {}, - "outputs": [], - "source": [ - "print (53431 % 453)" - ] - }, - { - "cell_type": "markdown", - "id": "e2bf6a52", - "metadata": {}, - "source": [ - "## A.6 Bitwise operations\n", - "Using Python, what is the results of the following:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9f6d382a", - "metadata": {}, - "outputs": [], - "source": [ - "print (0x43 | 0x21)\n", - "print (0x43 & 0x21)\n", - "print (0x43 ^ 0x21)" - ] - }, - { - "cell_type": "markdown", - "id": "a7c4bcb4", - "metadata": {}, - "source": [ - "In this case, \"|\" does a bitwise OR, \"&\" does a bitwise AND, and \"^\" does a bitwise X-OR. Using a pen and paper, prove that these results are correct. Results:" - ] - }, - { - "cell_type": "markdown", - "id": "23c4d2f7", - "metadata": {}, - "source": [ - "## A.7 Hex, Oct, Char and Binary\n", - "\n", - "Using Python, what is the hex, octal, character, and binary equivalents of the value of 93:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4ca29ad0", - "metadata": {}, - "outputs": [], - "source": [ - "val1=93\n", - "print (\"Dec:\\t\",val1)\n", - "print (\"Bin:\\t\",bin(val1))\n", - "print (\"Hex:\\t\",hex(val1))\n", - "print (\"Oct:\\t\",oct(val1))\n", - "print (\"Char:\\t\",chr(val1))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3193073d", - "metadata": {}, - "outputs": [], - "source": [ - "## A.8 Node.js\n", - "\n", - "JavaScript is often used in cryptography. Using node.js, repeat A.7.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "eea0739b", - "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'console' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mval\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m93\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mconsole\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlog\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mval\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtoString\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3\u001b[0m \u001b[0mconsole\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlog\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mval\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtoString\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m16\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mconsole\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlog\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mval\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtoString\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m8\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mconsole\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlog\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mString\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfromCharCode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mval\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mNameError\u001b[0m: name 'console' is not defined" - ] - } - ], - "source": [ - "val=93\n", - "console.log(val.toString(2))\n", - "console.log(val.toString(16))\n", - "console.log(val.toString(8))\n", - "console.log(String.fromCharCode(val))" - ] - }, - { - "cell_type": "markdown", - "id": "f56f2f5d", - "metadata": {}, - "source": [ - "A.9 Base64\n", - "\n", - "Using Python, what is the Base-64 conversion for the string of “crypto”?\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e348a9ae", - "metadata": {}, - "outputs": [], - "source": [ - "import base64\n", - "str='crypto'\n", - "print (base64.b64encode(str.encode()))" - ] - }, - { - "cell_type": "markdown", - "id": "49065b48", - "metadata": {}, - "source": [ - "\n", - "## Base64\n", - "If we use a string of \"crypto1\", what do you observe from the Base64 conversion compared to the result in the previous question (A.9)? Observation:" - ] - }, - { - "cell_type": "markdown", - "id": "86ef2106", - "metadata": {}, - "source": [ - "## A.11 Bit shift\n", - "\n", - "Using Python, using a decimal value of 41, determine the result of a shift left by one bit, a shift left by two bits, a right shift by one bit, and a right shift by two bits:\n", - "\n", - "Web link (Bit shift): here\n", - "\n", - "Decimal form: 41\n", - "\n", - "Shift left (1):\n", - "\n", - "Shift left (2):\n", - "\n", - "Shift right(1):\n", - "\n", - "Shift right(2):\n", - "\n", - "Why would a shift left or shift right operator not be used on its own in cryptography?" - ] - }, - { - "cell_type": "markdown", - "id": "a87e860c", - "metadata": {}, - "source": [ - "## A.12 Factors\n", - "\n", - "In several cases in cryptography, we try and factorize a value into its factors. An example is 15, and which has factors of 5 and 3. Using the Python program defined in the following link, determine the factors of 432:\n", - "\n", - "Web link (Factorization): here\n", - "\n", - "Think of two extremely large values and determine their factors." - ] - }, - { - "cell_type": "markdown", - "id": "db2c5f72", - "metadata": {}, - "source": [ - "## A.13 Compression\n", - "\n", - "Another format we can use for our data is compression, and we can do the compression before or after the encryption process. One of the most popular methods is gzip compress, and which uses the LZ method to reduce the number of bits used. For this we will use node.js. Create a file named a_13.js and determine what the following Base64 conversions are when they are uncompressed (Hint: they are cities of the World):\n", - "\n", - "Web link (Compression): here\n", - "\n", - "Take a string of “abc” and compress it, and now keep building up the string with the same sequence (such as “abcabc…”). What do you observe from the length of the compression string if you use a random characters of the same length as an input:\n", - "\n", - "eJzzyc9Lyc8DAAgpAms=\n", - "eJxzSi3KycwDAAfXAl0=\n", - "eJzzSy1XiMwvygYADKUC8A==" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "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.8.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/z_jupyter/02_symmetric_key.ipynb b/z_jupyter/02_symmetric_key.ipynb new file mode 100644 index 0000000..2660ce1 --- /dev/null +++ b/z_jupyter/02_symmetric_key.ipynb @@ -0,0 +1,996 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "509b908c", + "metadata": {}, + "source": [ + "# Symmetric Key" + ] + }, + { + "cell_type": "markdown", + "id": "e8a9af38", + "metadata": {}, + "source": [ + "## Introduction\n", + "\n", + "Symmetric key encryption involves the same key being used to encrypt and decrypt:\n", + "\n", + "![Alt text](graphics/g_sym_04.png \"Symmetric Key\")\n", + "\n", + " Apart from ECB mode, we also normally apply a salt value known as an IV (Initialization Vector). The main modes are ECB (Electronic Code Book), CBC (Cipher Block Chaining), CTR (Counter), OFB (Output Feedback) and GCM (Galois/Counter Mode). Overall, Bob and Alice will share the same encryption key, and covert plaintext into ciphertext for the encryption key, and then from ciphertext to plaintext when we decrypt. Overall, Bob and Alice share the same key, and where Eve will find it difficult to find the key.\n", + " \n", + " ## Block cipher\n", + "\n", + " With a block cipher, we take the plaintext data, and split it into blocks. This block size can vary between different ciphers, but will typically be 128 bits (16 bytes). Some older symmetric key methods, such as DES and 3DES, use a 64-bit block size (8 bytes). \n", + " \n", + " ![Alt text](graphics/g_sym_08.png \"Symmetric Key\")\n", + " \n", + " In the following, we define a message, and then the number of bytes per block (n). We can then check the value of n to change the size of the block. For the last block, we will pad with the space character:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "9a64a147", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Message: Hello how are you?\n", + "Bytes per block: 8\n", + "\n", + "Blocks ...\n", + "Block [ 0 ] Hello ho\n", + "Block [ 1 ] w are yo\n", + "Block [ 2 ] u? \n" + ] + } + ], + "source": [ + "# https://asecuritysite.com/encryption/blk\n", + "import sys\n", + "\n", + "BLOCKSIZE = 8\n", + "n=BLOCKSIZE\n", + "message=\"Hello how are you?\"\n", + "\n", + "print (\"Message:\",message)\n", + "print (\"Bytes per block:\",n)\n", + "print (\"\\nBlocks ...\")\n", + "\n", + "message = [message[i: i + n] for i in range(0, len(message), n)]\n", + "\n", + "lengthOfLastBlock = len(message[len(message)-1])\n", + "\n", + "if ( lengthOfLastBlock < BLOCKSIZE):\n", + "\tfor i in range(lengthOfLastBlock, BLOCKSIZE):\n", + " \t\tmessage[len(message)-1] += \" \"\n", + "\n", + "i=0\n", + "for b in message:\n", + "\tprint ('Block [',i,']',b)\n", + "\ti=i+1\n" + ] + }, + { + "cell_type": "markdown", + "id": "1dbd2658", + "metadata": {}, + "source": [ + "> For a message of \"This is a test message.\". How many blocks will be used for a 64-bit block size?\n", + "> For a message of \"This is a test message.\". How many blocks will be used for a 128-bit block size?" + ] + }, + { + "cell_type": "markdown", + "id": "ec4619c5", + "metadata": {}, + "source": [ + " \n", + " ### Padding with PKCS7\n", + "In the last block, we may not have enough bytes to fill it up. We thus need to add padding bytes before the encryption, and then remove after we decrypt. The most typical way that we pad is with PKCS7, and where we find out the number of bytes that we need to pad for the last block, and then take that value and repeat it for the number of bytes we need to pad. in the following, we have size as the number of bits in a block, and a message. For \"abc\" and for a block size of 128 bits (16 bytes), we will have 13 filling bytes. The value of 13 is represented as 0xd:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "30efb5d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Message: abcde\n", + "Block size: 128\n", + "Padded: 61626364650b0b0b0b0b0b0b0b0b0b0b\n", + "Unpadded: abcde\n" + ] + } + ], + "source": [ + "# https://asecuritysite.com/hazmat/hashnew28\n", + "\n", + "from cryptography.hazmat.primitives import padding\n", + "import sys\n", + "import binascii\n", + "\n", + "size=128\n", + "message=\"abcde\"\n", + "\n", + "\n", + "print (\"Message: \",message)\n", + "print (\"Block size: \",size)\n", + "\n", + "message=message.encode()\n", + "padder = padding.PKCS7(size).padder()\n", + "padded_data = padder.update(message)\n", + "\n", + "padded_data += padder.finalize()\n", + "print (\"Padded: \",binascii.hexlify(padded_data).decode())\n", + "\n", + "unpadder = padding.PKCS7(size).unpadder()\n", + "data = unpadder.update(padded_data)\n", + "\n", + "unpadded = data + unpadder.finalize()\n", + "\n", + "print (\"Unpadded: \",unpadded.decode())" + ] + }, + { + "cell_type": "markdown", + "id": "b909868d", + "metadata": {}, + "source": [ + "> For a message of \"hello\" and a block size of 128 bits. What is the padding value?\"\n", + "> For a message of \"The boy stood on the burning deck!\" and a block size of 128 bits. What is the padding value?\"\n", + "> What will happen when we have 18 characters in the plaintext. Will we have one block or two, and what will the padding value be?" + ] + }, + { + "cell_type": "markdown", + "id": "10722ff1", + "metadata": {}, + "source": [ + " ## AES encryption\n", + " \n", + " The most popular symmetric key method is AES (Advanced Encryption Standard), and which was defined as a standard by NIST. It has a 128-bit block (16 bytes) and can use a 128-bit, 192-bit or 256-bit encryption key. We then have a number of modes that can be used. The most basic mode is ECB (Electronic Code Book), and which basically just applies the encryption key to each block:\n", + "\n", + " ![Alt text](graphics/g_sym_13.png \"Symmetric Key\")\n", + "\n", + "In the following, we apply create AES encryption with ECB mode:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "11f31dd9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Message:\t Hello\n", + "Key:\t b'dd87fcbf7b6d0a43b339e4cab145e8c3520649a6fcbf7cebdc1af053999cd20c'\n", + "\n", + "\n", + "=== AES ECB === \n", + "Cipher: b'f52c9d5089181d25bf57221d49a2d256'\n", + "Decrypted: Hello\n" + ] + } + ], + "source": [ + "# 02_01.py\n", + "# https://asecuritysite.com/hazmat/hashnew4\n", + "import os\n", + "from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes \n", + "from cryptography.hazmat.primitives import padding\n", + "import sys\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "import binascii\n", + "\n", + "def go_encrypt(msg,method,mode):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " encryptor = cipher.encryptor()\n", + " ct = encryptor.update(msg) + encryptor.finalize()\n", + " return (ct)\n", + "\n", + "\n", + "def go_decrypt(ct,method,mode):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " decryptor = cipher.decryptor()\n", + " return (decryptor.update(ct) + decryptor.finalize())\n", + "\n", + "\n", + "def pad(data,size=128):\n", + " padder = padding.PKCS7(size).padder()\n", + " padded_data = padder.update(data)\n", + " padded_data += padder.finalize()\n", + " return(padded_data)\n", + "\n", + "def unpad(data,size=128):\n", + " padder = padding.PKCS7(size).unpadder()\n", + " unpadded_data = padder.update(data)\n", + " unpadded_data += padder.finalize()\n", + " return(unpadded_data)\n", + "\n", + "\n", + "key = os.urandom(32)\n", + "\n", + "msg=b\"Hello\"\n", + "\n", + "print (\"Message:\\t\",msg.decode())\n", + "print (\"Key:\\t\",binascii.b2a_hex(key))\n", + "\n", + "padded_data=pad(msg)\n", + "\n", + "\n", + "print (\"\\n\\n=== AES ECB === \")\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.ECB())\n", + "\n", + "plain=go_decrypt(cipher,algorithms.AES(key), modes.ECB())\n", + "data=unpad(plain)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "96dae988", + "metadata": {}, + "source": [ + "> With a message of \"Hello123\". What is the ciphertext in hexadecimal?\n", + "> If you encrypt the same message, do the message change?" + ] + }, + { + "cell_type": "markdown", + "id": "69c8303b", + "metadata": {}, + "source": [ + "## Additional Modes\n", + "The problem with ECB, is that the output cipher is always the same for the same plaintext input. To overcome this, we can add an Initialisation Vector (IV) or salt. This is normally a random value that is greater than 64 bits. The cipher is then stored with the IV, and when we decrypt, we need both the encryption key and the salt value. With CBC (Cipher Block Chaining), we add salt into the first block encryption, and then chain the output of this into the next block, and so on:\n", + "\n", + "![Alt text](graphics/g_sym_12.png \"Symmetric Key\")\n", + "\n", + "In the following, we use the cipher modes of CBC, OBF, CFB and CTR, and where we add a random salt value:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "704df22b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Message:\t Hello\n", + "Key:\t b'4cf24c4b7435aeb48e92f93f614dccc4d4e2b712867b1cabd0fafb80f1ab7d6b'\n", + "IV:\t b'e476433e82a1bf574af60eff91832e52'\n", + "=== AES CBC === \n", + "Cipher: b'bdf3405006a681a750ed2f692e58278c'\n", + "Decrypted: Hello\n", + "=== AES OFB === \n", + "Cipher: b'a59ba9e04357114e348ea440317e149b'\n", + "Decrypted: Hello\n", + "=== AES CFB === \n", + "Cipher: b'a59ba9e04357114e348ea440317e149b'\n", + "Decrypted: Hello\n", + "=== AES CTR === \n", + "Cipher: b'a59ba9e04357114e348ea440317e149b'\n", + "Decrypted: Hello\n" + ] + } + ], + "source": [ + "# 02_02.py\n", + "# https://asecuritysite.com/hazmat/hashnew4\n", + "import os\n", + "from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes \n", + "from cryptography.hazmat.primitives import padding\n", + "import sys\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "import binascii\n", + "\n", + "def go_encrypt(msg,method,mode):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " encryptor = cipher.encryptor()\n", + " ct = encryptor.update(msg) + encryptor.finalize()\n", + " return (ct)\n", + "\n", + "\n", + "def go_decrypt(ct,method,mode):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " decryptor = cipher.decryptor()\n", + " return (decryptor.update(ct) + decryptor.finalize())\n", + "\n", + "\n", + "def pad(data,size=128):\n", + " padder = padding.PKCS7(size).padder()\n", + " padded_data = padder.update(data)\n", + " padded_data += padder.finalize()\n", + " return(padded_data)\n", + "\n", + "def unpad(data,size=128):\n", + " padder = padding.PKCS7(size).unpadder()\n", + " unpadded_data = padder.update(data)\n", + " unpadded_data += padder.finalize()\n", + " return(unpadded_data)\n", + "\n", + "\n", + "key = os.urandom(32)\n", + "iv = os.urandom(16)\n", + "msg=b\"Hello\"\n", + "\n", + "print (\"Message:\\t\",msg.decode())\n", + "print (\"Key:\\t\",binascii.b2a_hex(key))\n", + "print (\"IV:\\t\",binascii.b2a_hex(iv))\n", + "\n", + "padded_data=pad(msg)\n", + "\n", + "\n", + "\n", + "print (\"=== AES CBC === \")\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CBC(iv))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.AES(key), modes.CBC(iv))\n", + "data=unpad(plain)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "\n", + "print (\"=== AES OFB === \")\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.OFB(iv))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.AES(key), modes.OFB(iv))\n", + "data=unpad(plain)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "print (\"=== AES CFB === \")\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CFB(iv))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.AES(key), modes.CFB(iv))\n", + "data=unpad(plain)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "print (\"=== AES CTR === \")\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CTR(iv))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.AES(key), modes.CTR(iv))\n", + "data=unpad(plain)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "110beed2", + "metadata": {}, + "source": [ + "> What is the size of the IV?\n", + "> What is the size of the key used?\n", + "> For each run, do you get a different cipher for the same input?" + ] + }, + { + "cell_type": "markdown", + "id": "23a803f5", + "metadata": {}, + "source": [ + "## Stream ciphers\n", + "With a stream cipher, we take the salt value and some randomization, and create an infinitely long key stream. With this key, we then XOR each bit of the plaintext stream with the key stream. This makes the cipher much faster than a block cipher, and where the main overhead is the key stream generation:\n", + "\n", + "\n", + " ![Alt text](graphics/g_sym_05.png \"Symmetric Key\")\n", + "\n", + "One of the main methods that we use for a stream cipher is ChaCha20, and which provides an alterative to AES. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "718d27a4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Message:\t Hello\n", + "Key:\t b'ca472445b83c22523cb4f25ce14c1f21c74294b1672f89a9a570f865794c004a'\n", + "IV:\t b'064c1792484091dd65e70ec5a81ea6d9'\n", + "\n", + "=== ChaCha20 === \n", + "Cipher: b'f36e696aa3'\n", + "Decrypted: Hello\n" + ] + } + ], + "source": [ + "# 02_03.py\n", + "# https://asecuritysite.com/symmetric/hashnew4\n", + "import os\n", + "from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes \n", + "from cryptography.hazmat.primitives import padding\n", + "import sys\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "import binascii\n", + "\n", + "def go_encrypt(msg,method,mode):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " encryptor = cipher.encryptor()\n", + " ct = encryptor.update(msg) + encryptor.finalize()\n", + " return (ct)\n", + "\n", + "\n", + "def go_decrypt(ct,method,mode):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " decryptor = cipher.decryptor()\n", + " return (decryptor.update(ct) + decryptor.finalize())\n", + "\n", + "def go_encrypt_with_auth(msg,method,mode,add):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " encryptor = cipher.encryptor()\n", + " encryptor.authenticate_additional_data(add)\n", + " ct = encryptor.update(msg) + encryptor.finalize()\n", + " return (ct,encryptor.tag)\n", + "\n", + "\n", + "def go_decrypt_with_auth(ct,method,mode,add):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " decryptor = cipher.decryptor()\n", + " decryptor.authenticate_additional_data(add)\n", + " pl=decryptor.update(ct) + decryptor.finalize()\n", + " return (pl)\n", + "\n", + "def pad(data,size=128):\n", + " padder = padding.PKCS7(size).padder()\n", + " padded_data = padder.update(data)\n", + " padded_data += padder.finalize()\n", + " return(padded_data)\n", + "\n", + "def unpad(data,size=128):\n", + " padder = padding.PKCS7(size).unpadder()\n", + " unpadded_data = padder.update(data)\n", + " unpadded_data += padder.finalize()\n", + " return(unpadded_data)\n", + "\n", + "\n", + "key = os.urandom(32)\n", + "iv = os.urandom(16)\n", + "msg=b\"Hello\"\n", + "\n", + "\n", + "print (\"Message:\\t\",msg.decode())\n", + "print (\"Key:\\t\",binascii.b2a_hex(key))\n", + "print (\"IV:\\t\",binascii.b2a_hex(iv))\n", + "\n", + "padded_data=pad(msg)\n", + "\n", + "print (\"\\n=== ChaCha20 === \")\n", + "\n", + "cipher=go_encrypt(msg,algorithms.ChaCha20(key,iv), None)\n", + "\n", + "plain=go_decrypt(cipher,algorithms.ChaCha20(key,iv), None)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {plain.decode()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "082cd7f3", + "metadata": {}, + "source": [ + "> What is the size of the salt value used?\n", + "\n", + "> What is the size of the key used?\n", + "\n", + "> Run the ChaCha20 code, and determine the number of bytes in the cipher for \"Hello\", \"Hello1\" and \"Hello2\". What can you say about the relationship between the plaintext size and the ciphertext size?" + ] + }, + { + "cell_type": "markdown", + "id": "4c17ac2c", + "metadata": {}, + "source": [ + "## AES GCM \n", + "The most popular symmetric key method is AES GCM, and which is a stream cipher which uses AES encryption. It also adds an authentication tag, so that the cipher can be checked that is have not changed.\n", + "\n", + " ![Alt text](graphics/g_sym_07.png \"Symmetric Key\")\n", + "\n", + "The following provides the sample sample code, and where we add in some additional data (AE). This method is known as AEAD (Authenticated Encryption with Additional Data):" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "db724b07", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Message:\t Hello\n", + "Key:\t b'3459f49cb7b8ae9b1dcbb390525ff6b9add7fa0f377ea28f3fff8d28b7b097e5'\n", + "IV:\t b'e41b966e55930acd56d5e3dea466d09c'\n", + "=== AES GCM === \n", + "Cipher: b'b4c55b4769'\n", + "Decrypted: Hello\n" + ] + } + ], + "source": [ + "# 02_04.py\n", + "# https://asecuritysite.com/hazmat/hashnew4\n", + "import os\n", + "from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes \n", + "from cryptography.hazmat.primitives import padding\n", + "import sys\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "import binascii\n", + "\n", + "\n", + "def go_encrypt_with_auth(msg,method,mode,add):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " encryptor = cipher.encryptor()\n", + " encryptor.authenticate_additional_data(add)\n", + " ct = encryptor.update(msg) + encryptor.finalize()\n", + " return (ct,encryptor.tag)\n", + "\n", + "\n", + "def go_decrypt_with_auth(ct,method,mode,add):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " decryptor = cipher.decryptor()\n", + " decryptor.authenticate_additional_data(add)\n", + " pl=decryptor.update(ct) + decryptor.finalize()\n", + " return (pl)\n", + "\n", + "\n", + "\n", + "key = os.urandom(32)\n", + "iv = os.urandom(16)\n", + "msg=b\"Hello\"\n", + "tag= b\"Some data for the authentication tag\"\n", + "\n", + "\n", + "print (\"Message:\\t\",msg.decode())\n", + "print (\"Key:\\t\",binascii.b2a_hex(key))\n", + "print (\"IV:\\t\",binascii.b2a_hex(iv))\n", + "\n", + "padded_data=pad(msg)\n", + "\n", + "\n", + "\n", + "print (\"=== AES GCM === \")\n", + "# In GCM mode we convert to a stream cipher, so there is no need for padding\n", + "cipher,auth_tag=go_encrypt_with_auth(msg,algorithms.AES(key), modes.GCM(iv),tag)\n", + "\n", + "plain=go_decrypt_with_auth(cipher,algorithms.AES(key), modes.GCM(iv,auth_tag),tag)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {plain.decode()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "6c29af0e", + "metadata": {}, + "source": [ + "> Verify that the cipher changes for each run.\n", + "\n", + "> What do we not need to pad in this code?\n", + "\n", + "> If you change the tag before the decryption, does the cipher decrypt?" + ] + }, + { + "cell_type": "markdown", + "id": "4c190297", + "metadata": {}, + "source": [ + " \n", + " ## Additional Ciphers\n", + " There are many other symmetric key methods supported in the cryptography library. This includes implement AES, Chacha20, Camellia, 3DES, IDEA, CAST5 and Blowfish. The following Python program will use a random 256-bit encryption key, and a random 128-bit IV value. In most of the modes, we need to pad the plaintext to the size of the block (typically this is 128 bits or 16 bytes), but AES GCM mode we do not need to pad the cipher as it is a stream cipher." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "1fed6488", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Message:\t Hello\n", + "Key:\t b'394f66d40b7ff08e054f155d581299feeee5dd4c346f455432cf280003b65cc9'\n", + "IV:\t b'783992db351505f6e4455ece244288b1'\n", + "\n", + "\n", + "=== AES ECB === \n", + "Cipher: b'8d18d2608555aafa6c49a867a3df7da3'\n", + "Decrypted: Hello\n", + "=== AES CBC === \n", + "Cipher: b'e883fc6c0b568e984f37b5be0660990d'\n", + "Decrypted: Hello\n", + "=== AES OFB === \n", + "Cipher: b'39dccf1ec235d002ca472abe03e9cc98'\n", + "Decrypted: Hello\n", + "=== AES CFB === \n", + "Cipher: b'39dccf1ec235d002ca472abe03e9cc98'\n", + "Decrypted: Hello\n", + "=== AES CTR === \n", + "Cipher: b'39dccf1ec235d002ca472abe03e9cc98'\n", + "Decrypted: Hello\n", + "=== AES GCM === \n", + "Cipher: b'38426fbe32'\n", + "Decrypted: Hello\n", + "=== AES XTS === \n", + "Cipher: b'dbc50e1f568ac761307228c06a11c6a2'\n", + "Decrypted: Hello\n", + "\n", + "=== Blowfish ECB === \n", + "Cipher: b'0d3166526e87a287e20dbbca5068a253'\n", + "Decrypted: Hello\n", + "=== Blowfish CBC === \n", + "Cipher: b'bdb3d83379e84e42de2c68cfc6100cd8'\n", + "Decrypted: Hello\n", + "\n", + "=== ChaCha20 === \n", + "Cipher: b'3aa44314ab'\n", + "Decrypted: Hello\n", + "\n", + "=== 3DES ECB === \n", + "Cipher: b'ee9fbc2a9e2ab17d9294e35946806f88'\n", + "Decrypted: Hello\n", + "=== 3DES CBC === \n", + "Cipher: b'41521ecf924f5aac9a3043e9de7f4c98'\n", + "Decrypted: Hello\n", + "\n", + "=== Camellia ECB === \n", + "Cipher: b'115c858dbb2b404788f4aca3be95ecbe'\n", + "Decrypted: Hello\n", + "=== Camellia CBC === \n", + "Cipher: b'b87a6aa181ccc4f0ff16af4662d5a500'\n", + "Decrypted: Hello\n", + "=== IDEA ECB === \n", + "Cipher: b'880a57ed61680708eb9bd39e0d3e6942'\n", + "Decrypted: Hello\n", + "=== IDEA CBC === \n", + "Cipher: b'1479e18fd3dc95bb899d1eda3476f271'\n", + "Decrypted: Hello\n", + "\n", + "=== CAST5 ECB === \n", + "Cipher: b'72c565e0207e7afbbbf9e0f53fbd29a0'\n", + "Decrypted: Hello\n", + "=== CAST5 CBC === \n", + "Cipher: b'3c21106fb5e60aeb8421a974cbe98674'\n", + "Decrypted: Hello\n" + ] + } + ], + "source": [ + "# 02_05.py\n", + "# https://asecuritysite.com/hazmat/hashnew4\n", + "import os\n", + "from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes \n", + "from cryptography.hazmat.primitives import padding\n", + "import sys\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "import binascii\n", + "\n", + "def go_encrypt(msg,method,mode):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " encryptor = cipher.encryptor()\n", + " ct = encryptor.update(msg) + encryptor.finalize()\n", + " return (ct)\n", + "\n", + "\n", + "def go_decrypt(ct,method,mode):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " decryptor = cipher.decryptor()\n", + " return (decryptor.update(ct) + decryptor.finalize())\n", + "\n", + "def go_encrypt_with_auth(msg,method,mode,add):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " encryptor = cipher.encryptor()\n", + " encryptor.authenticate_additional_data(add)\n", + " ct = encryptor.update(msg) + encryptor.finalize()\n", + " return (ct,encryptor.tag)\n", + "\n", + "\n", + "def go_decrypt_with_auth(ct,method,mode,add):\n", + " cipher = Cipher(method, mode,backend=default_backend())\n", + " decryptor = cipher.decryptor()\n", + " decryptor.authenticate_additional_data(add)\n", + " pl=decryptor.update(ct) + decryptor.finalize()\n", + " return (pl)\n", + "\n", + "def pad(data,size=128):\n", + " padder = padding.PKCS7(size).padder()\n", + " padded_data = padder.update(data)\n", + " padded_data += padder.finalize()\n", + " return(padded_data)\n", + "\n", + "def unpad(data,size=128):\n", + " padder = padding.PKCS7(size).unpadder()\n", + " unpadded_data = padder.update(data)\n", + " unpadded_data += padder.finalize()\n", + " return(unpadded_data)\n", + "\n", + "\n", + "key = os.urandom(32)\n", + "iv = os.urandom(16)\n", + "msg=b\"Hello\"\n", + "tag= b\"Some data for the authentication tag\"\n", + "\n", + "\n", + "print (\"Message:\\t\",msg.decode())\n", + "print (\"Key:\\t\",binascii.b2a_hex(key))\n", + "print (\"IV:\\t\",binascii.b2a_hex(iv))\n", + "\n", + "padded_data=pad(msg)\n", + "\n", + "\n", + "print (\"\\n\\n=== AES ECB === \")\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.ECB())\n", + "\n", + "plain=go_decrypt(cipher,algorithms.AES(key), modes.ECB())\n", + "data=unpad(plain)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CBC(iv))\n", + "\n", + "print (\"=== AES CBC === \")\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CBC(iv))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.AES(key), modes.CBC(iv))\n", + "data=unpad(plain)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CBC(iv))\n", + "\n", + "print (\"=== AES OFB === \")\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.OFB(iv))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.AES(key), modes.OFB(iv))\n", + "data=unpad(plain)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "print (\"=== AES CFB === \")\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CFB(iv))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.AES(key), modes.CFB(iv))\n", + "data=unpad(plain)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "print (\"=== AES CTR === \")\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CTR(iv))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.AES(key), modes.CTR(iv))\n", + "data=unpad(plain)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "print (\"=== AES GCM === \")\n", + "# In GCM mode we convert to a stream cipher, so there is no need for padding\n", + "cipher,auth_tag=go_encrypt_with_auth(msg,algorithms.AES(key), modes.GCM(iv),tag)\n", + "\n", + "plain=go_decrypt_with_auth(cipher,algorithms.AES(key), modes.GCM(iv,auth_tag),tag)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {plain.decode()}\")\n", + "\n", + "\n", + "print (\"=== AES XTS === \")\n", + "# In XTS the iv value is known as a tweak - and relates to the sector number\n", + "# The keys are double length, so that a 32 byte key (256 bits) is actually AES-128\n", + "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.XTS(iv))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.AES(key), modes.XTS(iv))\n", + "data=unpad(plain)\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "\n", + "print (\"\\n=== Blowfish ECB === \")\n", + "\n", + "cipher=go_encrypt(padded_data,algorithms.Blowfish(key), modes.ECB())\n", + "\n", + "plain=go_decrypt(cipher,algorithms.Blowfish(key), modes.ECB())\n", + "\n", + "data=unpad(plain)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "print (\"=== Blowfish CBC === \")\n", + "\n", + "cipher=go_encrypt(padded_data,algorithms.Blowfish(key), modes.CBC(iv[:8]))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.Blowfish(key), modes.CBC(iv[:8]))\n", + "\n", + "data=unpad(plain)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "\n", + "print (\"\\n=== ChaCha20 === \")\n", + "\n", + "cipher=go_encrypt(msg,algorithms.ChaCha20(key,iv), None)\n", + "\n", + "plain=go_decrypt(cipher,algorithms.ChaCha20(key,iv), None)\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "print (\"\\n=== 3DES ECB === \")\n", + "cipher=go_encrypt(padded_data,algorithms.TripleDES(key[:16]), modes.ECB())\n", + "\n", + "plain=go_decrypt(cipher,algorithms.TripleDES(key[:16]), modes.ECB())\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "print (\"=== 3DES CBC === \")\n", + "cipher=go_encrypt(padded_data,algorithms.TripleDES(key[:16]), modes.CBC(iv[:8]))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.TripleDES(key[:16]), modes.CBC(iv[:8]))\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "\n", + "print (\"\\n=== Camellia ECB === \")\n", + "cipher=go_encrypt(padded_data,algorithms.Camellia(key), modes.ECB())\n", + "\n", + "plain=go_decrypt(cipher,algorithms.Camellia(key), modes.ECB())\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "\n", + "print (\"=== Camellia CBC === \")\n", + "cipher=go_encrypt(padded_data,algorithms.Camellia(key), modes.CBC(iv))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.Camellia(key), modes.CBC(iv))\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "print (\"=== IDEA ECB === \")\n", + "cipher=go_encrypt(padded_data,algorithms.IDEA(key[:16]), modes.ECB())\n", + "\n", + "plain=go_decrypt(cipher,algorithms.IDEA(key[:16]), modes.ECB())\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "print (\"=== IDEA CBC === \")\n", + "cipher=go_encrypt(padded_data,algorithms.IDEA(key[:16]), modes.CBC(iv[:8]))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.IDEA(key[:16]), modes.CBC(iv[:8]))\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "print (\"\\n=== CAST5 ECB === \")\n", + "cipher=go_encrypt(padded_data,algorithms.CAST5(key[:16]), modes.ECB())\n", + "\n", + "plain=go_decrypt(cipher,algorithms.CAST5(key[:16]), modes.ECB())\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")\n", + "\n", + "print (\"=== CAST5 CBC === \")\n", + "cipher=go_encrypt(padded_data,algorithms.CAST5(key[:16]), modes.CBC(iv[:8]))\n", + "\n", + "plain=go_decrypt(cipher,algorithms.CAST5(key[:16]), modes.CBC(iv[:8]))\n", + "\n", + "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", + "print (f\"Decrypted: {data.decode()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "02727c7e", + "metadata": {}, + "source": [ + "# Computing time to crack symmetric key with brute force\n", + "If we have x bits for a symmetric key, we will then have 2^x different keys. If we assume that it takes t seconds to try a single key, then the total time to crack will be:\n", + "\n", + "T = 2^x * t\n", + "\n", + "Overall, the average time will be half of this value. For example, if we have a 32-bit key and can crack for 1 nanosecond per key, we can compute the average time with:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "bb4410df", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time to crack = 18446744073.709553 seconds\n", + "Time to crack = 213503.9823346013 days\n" + ] + } + ], + "source": [ + "x=32\n", + "keys=2**x\n", + "t=1e-9\n", + "\n", + "time=keys*t\n", + "print(f\"Time to crack = {time/2} seconds\")\n", + "print(f\"Time to crack = {time/2/60/60} hours\")" + ] + }, + { + "cell_type": "markdown", + "id": "6f21c025", + "metadata": {}, + "source": [ + "> Modify the program to show the number of days to crack.\n", + "> For a 64 bit key, and 1 pico second crack per key, what is the average cracking time in days?" + ] + } + ], + "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.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/03_hashing.ipynb b/z_jupyter/03_hashing.ipynb new file mode 100644 index 0000000..c337f53 --- /dev/null +++ b/z_jupyter/03_hashing.ipynb @@ -0,0 +1,480 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7782a35", + "metadata": {}, + "source": [ + "\n", + "# Hashing\n", + "Within hashing methods, we take data in the form of a byte array, and then create a fixed length hash value. For MD5, the length of the hash is 128 bits, for SHA-1 it is 160 bits, and for SHA-256, it is 256 bits. \n", + "\n", + "\n", + "\n", + "These hashes include MD5, SHA-1 and SHA-256. With MD5 we get a 128-bit output, and which is 32 hex characters:\n", + "\n", + "\n", + "\n", + "SHA-1 has an output of 160 bits, and SHA-256 has an output of 256 bits. MD5 should not be used in production environments as the method has weaknesses, along with the output hash begin too short. SHA-1, too, has been shown to have weaknesses, and thus we should use SHA-2 methods. These include SHA224, SHA-256, SHA-384 and SHA-512. A newer standard is known as SHA-3. \n", + "\n", + "\n", + "\n", + "\n", + "## OpenSSL hashing\n", + "OpenSSL can be used to create hash values for SHA1, SHA-256, and other methods. An example for Linux and Windows is [here]:\n", + "\n", + "```\n", + "Linux command: echo -n \"Hello\" | openssl dgst -md5\n", + "Windows command: echo | set /p = \"Hello\" | openssl dgst -md5\n", + "\n", + "Message: Hello\n", + "Mode: md5\n", + "========\n", + "MD5d1a7fb5eab1c16cb4f7cf341cf188c3d\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "cd0b8fe6", + "metadata": {}, + "source": [ + "> Using OpenSSL in the command prompt, or using this site [here], determine the hash values SHA-1 and SHA-256 hash values for: \"Edinburgh\" and \"Glasgow\".\n", + "> Do the hash values change when we use \"edinburgh\" and \"glasgow\"?" + ] + }, + { + "cell_type": "markdown", + "id": "09119c37", + "metadata": {}, + "source": [ + "## MD5 and SHA-1\n", + "In the following we will use the hashing methods supported by the Hazmat primitive. " + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "e88c246d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data: Hello\n", + " Hex: 48656c6c6f\n", + "MD5: 8b1a9953c4611296a827abf8c47804d7 ixqZU8RhEpaoJ6v4xHgE1w==\n", + "\n" + ] + } + ], + "source": [ + "# 03_01.py\n", + "from cryptography.hazmat.primitives import hashes\n", + "import binascii\n", + "import sys\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "st = \"Hello\"\n", + "\n", + "try:\n", + " data=st.encode() # Convert to a byte array\n", + "\n", + " digest = hashes.Hash(hashes.MD5(),backend=default_backend())\n", + " digest.update(data)\n", + " res=digest.finalize()\n", + " hexval=binascii.b2a_hex(res).decode() # hex format\n", + " b64val=binascii.b2a_base64(res).decode() # Base64 format\n", + "\n", + "\n", + " print (\"Data: \",st)\n", + " print (\" Hex: \",binascii.b2a_hex(data).decode())\n", + " print (f\"MD5: {hexval} {b64val}\")\n", + "\n", + "except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "f916c2c0", + "metadata": {}, + "source": [ + "\n", + "\n", + "> Can you determine the hash value for \"Hello\"?\n", + "\n", + "> Now modify Line 11 in the program below to give SHA1() and also SHA256(). What are the values (list the first two hex characters)?\n", + "\n", + "> What is the length of the hash (in bits) for SHA-1?\n", + "\n", + "> What is the lenguth of the hash (in bits) for SHA-256?" + ] + }, + { + "cell_type": "markdown", + "id": "ee4c4c0e", + "metadata": {}, + "source": [ + "Two of the main formats for hashing are hexademical and Base64. In this example, we will use the binascii library to convert our data into a hash value:\n", + "\n", + "\n", + "\n", + "In this case, we will use other hashing methods such as Blake2, SHA-3, SHA-224, SHA-384 and SHA-512:" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "5b576b95", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data: hello\n", + " Hex: 68656c6c6f\n", + "\n", + "MD5: 5d41402abc4b2a76b9719d911017c592 XUFAKrxLKna5cZ2REBfFkg==\n", + "\n", + "SHA1: aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d qvTGHdzF6KLavt4PO0gs2a6pQ00=\n", + "\n", + "SHA224: ea09ae9cc6768c50fcee903ed054556e5bfc8347907f12598aa24193 6gmunMZ2jFD87pA+0FRVblv8g0eQfxJZiqJBkw==\n", + "\n", + "SHA256: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=\n", + "\n", + "SHA384: 59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f WeF0h3dEjGnea4ANejO7+5/xtGPkQ1TDVTvNucZm+pASWjx5+QOXvfX2oT3oKGhP\n", + "\n", + "SHA3_224: b87f88c72702fff1748e58b87e9141a42c0dbedc29a78cb0d4a5cd81 uH+IxycC//F0jli4fpFBpCwNvtwpp4yw1KXNgQ==\n", + "\n", + "SHA3_256: 3338be694f50c5f338814986cdf0686453a888b84f424d792af4b9202398f392 Mzi+aU9QxfM4gUmGzfBoZFOoiLhPQk15KvS5ICOY85I=\n", + "\n", + "SHA3_384: 720aea11019ef06440fbf05d87aa24680a2153df3907b23631e7177ce620fa1330ff07c0fddee54699a4c3ee0ee9d887 cgrqEQGe8GRA+/Bdh6okaAohU985B7I2MecXfOYg+hMw/wfA/d7lRpmkw+4O6diH\n", + "\n", + "SHA3_512: 75d527c368f2efe848ecf6b073a36767800805e9eef2b1857d5f984f036eb6df891d75f72d9b154518c1cd58835286d1da9a38deba3de98b5a53e5ed78a84976 ddUnw2jy7+hI7Pawc6NnZ4AIBenu8rGFfV+YTwNutt+JHXX3LZsVRRjBzViDUobR2po43ro96YtaU+XteKhJdg==\n", + "\n", + "SHA512: 9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043 m3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQw==\n", + "\n", + "SHA512_224: fe8509ed1fb7dcefc27e6ac1a80eddbec4cb3d2c6fe565244374061c /oUJ7R+33O/CfmrBqA7dvsTLPSxv5WUkQ3QGHA==\n", + "\n", + "SHA512_256: e30d87cfa2a75db545eac4d61baf970366a8357c7f72fa95b52d0accb698f13a 4w2Hz6KnXbVF6sTWG6+XA2aoNXx/cvqVtS0KzLaY8To=\n", + "\n" + ] + } + ], + "source": [ + "# 03_02.py\n", + "# https://asecuritysite.com/hazmat/hashnew\n", + "from cryptography.hazmat.primitives import hashes\n", + "import binascii\n", + "import sys\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "st = \"hello\"\n", + "hex=False\n", + "showhex=\"No\"\n", + "\n", + "def show_hash(name,type,data):\n", + " digest = hashes.Hash(type,backend=default_backend())\n", + " digest.update(data)\n", + " res=digest.finalize()\n", + " hex=binascii.b2a_hex(res).decode()\n", + " b64=binascii.b2a_base64(res).decode()\n", + " print (f\"{name}: {hex} {b64}\")\n", + "\n", + "if (showhex==\"yes\"): hex=True\n", + "\n", + "try:\n", + "\tif (hex==True): data = binascii.a2b_hex(st)\n", + "\telse: data=st.encode()\n", + "\n", + "\n", + "\tprint (\"Data: \",st)\n", + "\tprint (\" Hex: \",binascii.b2a_hex(data).decode())\n", + "\tprint()\n", + "\n", + "\tshow_hash(\"MD5\",hashes.MD5(),data)\n", + "\tshow_hash(\"SHA1\",hashes.SHA1(),data)\t\n", + "\tshow_hash(\"SHA224\",hashes.SHA224(),data)\n", + "\tshow_hash(\"SHA256\",hashes.SHA256(),data)\n", + "\tshow_hash(\"SHA384\",hashes.SHA384(),data)\n", + "\tshow_hash(\"SHA3_224\",hashes.SHA3_224(),data)\n", + "\tshow_hash(\"SHA3_256\",hashes.SHA3_256(),data)\n", + "\tshow_hash(\"SHA3_384\",hashes.SHA3_384(),data)\n", + "\tshow_hash(\"SHA3_512\",hashes.SHA3_512(),data)\n", + "\tshow_hash(\"SHA512\",hashes.SHA512(),data)\n", + "\tshow_hash(\"SHA512_224\",hashes.SHA512_224(),data)\n", + "\tshow_hash(\"SHA512_256\",hashes.SHA512_256(),data)\n", + "\n", + "except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "36c0aa30", + "metadata": {}, + "source": [ + "\n", + "> In this case the input data is \"00\". Can you run the program again, and this time use the data input of \"The quick brown fox jumps over the lazy dog\". Prove that:\n", + "\n", + "* MD5 hash value is \"9e107d9d372bb6826bd81d3542a419d6\"\n", + "* SHA-1 hash value is \"2fd4e1c67a2d28fced849ee1bb76e7391b93eb12\"\n", + "* SHA-256 hash value is \"d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592\"\n", + "\n", + "\n", + "> How many hex characters does MD5, SHA-1 and SHA-256, and how would you determine number of characters used?" + ] + }, + { + "cell_type": "markdown", + "id": "5d0ef125", + "metadata": {}, + "source": [ + "## Adding salt\n", + "One problem with hashing methods, is that we get the same hash output for the same input. This can allow an intruder to match the hash to the input string. To overcome this, we can add a salt value to the hashing process. This can be to append or prepend the data onto the input data. We obviously need to store the salt value with the hash value, in order to check a hash. \n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "82ee9a0e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data: hello\n", + " Hex: 68656c6c6f\n", + "\n", + "MD5: bfd9929c0794146bae6dcccb4317e99c v9mSnAeUFGuubczLQxfpnA==\n", + "\n", + "SHA1: 710fd92d6fa82d1851f9691ba48f29124890761d cQ/ZLW+oLRhR+WkbpI8pEkiQdh0=\n", + "\n", + "SHA224: 079bb5febb6d8f6d8cdfdd8e63360aa1803170325ed6738bbf62b189 B5u1/rttj22M392OYzYKoYAxcDJe1nOLv2KxiQ==\n", + "\n", + "SHA256: 5bf84899a6394b4d1ceaf8a28ffe61425a40ddef1a1563321437f8dd388ba0d2 W/hImaY5S00c6viij/5hQlpA3e8aFWMyFDf43TiLoNI=\n", + "\n", + "SHA384: f5b0fd54d84d07531b579030015b699073efec74491cd62d0e78b217465d77a4cdd9ea5974622ceaecb4acd49da2cf0d 9bD9VNhNB1MbV5AwAVtpkHPv7HRJHNYtDniyF0Zdd6TN2epZdGIs6uy0rNSdos8N\n", + "\n", + "SHA3_224: b9473057d8131ff11cf1cffeb5f4bdebe972baa131344a3b86c22030 uUcwV9gTH/Ec8c/+tfS96+lyuqExNEo7hsIgMA==\n", + "\n", + "SHA3_256: 071cdb333657cce85c765a287dbfa3388e430fce8b48b398a08444674965edde BxzbMzZXzOhcdloofb+jOI5DD86LSLOYoIREZ0ll7d4=\n", + "\n", + "SHA3_384: a1f631cb30bdebc5020cfdc0f7fe59dba5702f3c6f0f418bc756c759ae5e645d213ce81810fed5fc3473793d37beff3d ofYxyzC968UCDP3A9/5Z26VwLzxvD0GLx1bHWa5eZF0hPOgYEP7V/DRzeT03vv89\n", + "\n", + "SHA3_512: bf033772d578b5b5f27e7025bf04c57ff752d71517175f20fb48785a1e2b3f9f40e5b1d30a0fee24daeb6f1d61240ab938b772faed649feb9db457387e204c7c vwM3ctV4tbXyfnAlvwTFf/dS1xUXF18g+0h4Wh4rP59A5bHTCg/uJNrrbx1hJAq5OLdy+u1kn+udtFc4fiBMfA==\n", + "\n", + "SHA512: 261e05408898a3afc8d81a8f118a3aee30f04cf110c77087a4fa82a35dcd25fe8cad9e3448f38eb0fcd538ccc91403e61dd72fd57db8ab35b304a3ded7d2ac0b Jh4FQIiYo6/I2BqPEYo67jDwTPEQx3CHpPqCo13NJf6MrZ40SPOOsPzVOMzJFAPmHdcv1X24qzWzBKPe19KsCw==\n", + "\n", + "SHA512_224: 00b5af9dec28d8d3d69cedea1c093eb0568882b62a81f52265e99089 ALWvnewo2NPWnO3qHAk+sFaIgrYqgfUiZemQiQ==\n", + "\n", + "SHA512_256: ba41801fb5b9c05853c4628a0816eacabbe55f767fa040790fcba7d5e60aeec3 ukGAH7W5wFhTxGKKCBbqyrvlX3Z/oEB5D8un1eYK7sM=\n", + "\n" + ] + } + ], + "source": [ + "# 03_03.py\n", + "from cryptography.hazmat.primitives import hashes\n", + "import binascii\n", + "import sys\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "st = \"hello\"\n", + "salt = \"N20\"\n", + "hex=False\n", + "showhex=\"No\"\n", + "\n", + "def show_hash(name,type,data,salt):\n", + " digest = hashes.Hash(type,backend=default_backend())\n", + " digest.update(salt)\n", + " digest.update(data)\n", + " res=digest.finalize()\n", + " hex=binascii.b2a_hex(res).decode()\n", + " b64=binascii.b2a_base64(res).decode()\n", + " print (f\"{name}: {hex} {b64}\")\n", + "\n", + "if (showhex==\"yes\"): hex=True\n", + "\n", + "try:\n", + "\tif (hex==True): data = binascii.a2b_hex(st)\n", + "\telse: data=st.encode()\n", + "\n", + "\n", + "\tprint (\"Data: \",st)\n", + "\tprint (\" Hex: \",binascii.b2a_hex(data).decode())\n", + "\tprint()\n", + "\n", + "\tshow_hash(\"MD5\",hashes.MD5(),data,salt.encode())\n", + "\tshow_hash(\"SHA1\",hashes.SHA1(),data,salt.encode())\t\n", + "\tshow_hash(\"SHA224\",hashes.SHA224(),data,salt.encode())\n", + "\tshow_hash(\"SHA256\",hashes.SHA256(),data,salt.encode())\n", + "\tshow_hash(\"SHA384\",hashes.SHA384(),data,salt.encode())\n", + "\tshow_hash(\"SHA3_224\",hashes.SHA3_224(),data,salt.encode())\n", + "\tshow_hash(\"SHA3_256\",hashes.SHA3_256(),data,salt.encode())\n", + "\tshow_hash(\"SHA3_384\",hashes.SHA3_384(),data,salt.encode())\n", + "\tshow_hash(\"SHA3_512\",hashes.SHA3_512(),data,salt.encode())\n", + "\tshow_hash(\"SHA512\",hashes.SHA512(),data,salt.encode())\n", + "\tshow_hash(\"SHA512_224\",hashes.SHA512_224(),data,salt.encode())\n", + "\tshow_hash(\"SHA512_256\",hashes.SHA512_256(),data,salt.encode())\n", + "\n", + "except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "c4eac92e", + "metadata": {}, + "source": [ + "> Verify that the hash value changes for different salt values.\n", + "\n", + "> Rather than a string for the salt value. Can you modify the program, so that it has a random salt value with 16 bytes?" + ] + }, + { + "cell_type": "markdown", + "id": "9154359d", + "metadata": {}, + "source": [ + "## Variable length hashes (XOF)\n", + "There are some hashing methods that support a variable number of bytes in the output hash. These include Blake2b, Blake2s, SHAKE128 and SHAKE256:\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "b04c8f47", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data: hello\n", + " Hex: 68656c6c6f\n", + "\n", + "Blake2p (64 bytes): e4cfa39a3d37be31c59609e807970799caa68a19bfaa15135f165085e01d41a65ba1e1b146aeb6bd0092b49eac214c103ccfa3a365954bbbe52f74a2b3620c94 5M+jmj03vjHFlgnoB5cHmcqmihm/qhUTXxZQheAdQaZboeGxRq62vQCStJ6sIUwQPM+jo2WVS7vlL3Sis2IMlA==\n", + "\n", + "Blake2s (32 bytes): 19213bacc58dee6dbde3ceb9a47cbb330b3d86f8cca8997eb00be456f140ca25 GSE7rMWN7m294865pHy7Mws9hvjMqJl+sAvkVvFAyiU=\n", + "\n", + "SHAKE128 (64 bytes): 8eb4b6a932f280335ee1a279f8c208a349e7bc65daf831d3021c213825292463c59e22d0fe2c767cd7cacc4df42dd5f6147f0c5c512ecb9b933d14b9cc1b2974 jrS2qTLygDNe4aJ5+MIIo0nnvGXa+DHTAhwhOCUpJGPFniLQ/ix2fNfKzE30LdX2FH8MXFEuy5uTPRS5zBspdA==\n", + "\n", + "SHAKE256 (64 bytes): 1234075ae4a1e77316cf2d8000974581a343b9ebbca7e3d1db83394c30f221626f594e4f0de63902349a5ea5781213215813919f92a4d86d127466e3d07e8be3 EjQHWuSh53MWzy2AAJdFgaNDueu8p+PR24M5TDDyIWJvWU5PDeY5AjSaXqV4EhMhWBORn5Kk2G0SdGbj0H6L4w==\n", + "\n" + ] + } + ], + "source": [ + "# 03_04.py\n", + "from cryptography.hazmat.primitives import hashes\n", + "import binascii\n", + "import sys\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "st = \"hello\"\n", + "hex=False\n", + "showhex=\"No\"\n", + "\n", + "def show_hash(name,type,data):\n", + " digest = hashes.Hash(type,backend=default_backend())\n", + " digest.update(data)\n", + " res=digest.finalize()\n", + " hex=binascii.b2a_hex(res).decode()\n", + " b64=binascii.b2a_base64(res).decode()\n", + " print (f\"{name}: {hex} {b64}\")\n", + "\n", + "if (showhex==\"yes\"): hex=True\n", + "\n", + "try:\n", + "\tif (hex==True): data = binascii.a2b_hex(st)\n", + "\telse: data=st.encode()\n", + "\n", + "\n", + "\tprint (\"Data: \",st)\n", + "\tprint (\" Hex: \",binascii.b2a_hex(data).decode())\n", + "\tprint()\n", + "\n", + "\tshow_hash(\"Blake2p (64 bytes)\",hashes.BLAKE2b(64),data)\n", + "\tshow_hash(\"Blake2s (32 bytes)\",hashes.BLAKE2s(32),data)\n", + "\tshow_hash(\"SHAKE128 (64 bytes)\",hashes.SHAKE128(64),data)\n", + "\tshow_hash(\"SHAKE256 (64 bytes)\",hashes.SHAKE256(64),data)\n", + "\n", + "except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "fc4ff412", + "metadata": {}, + "source": [ + "> Run the program and verify the hashes produced.\n", + "\n", + "> Modify the program so that that we get a hash with 16 bytes, and verify that the length is correct.\n", + "\n", + "> Modify the program so that that we get a hash with 512 bytes, and verify that the length is correct." + ] + }, + { + "cell_type": "markdown", + "id": "24740fab", + "metadata": {}, + "source": [ + "## LM and NTLM Hash\n", + "Previous Microsoft Windows systems have used the LM and NTLM hash to store user passwords. The method is supported in the passlib library:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5ad92fc", + "metadata": {}, + "outputs": [], + "source": [ + "import passlib.hash;\n", + "string=\"hello\"\n", + "print \"LM Hash:\"+passlib.hash.lmhash.encrypt(string)\n", + "print \"NT Hash:\"+passlib.hash.nthash.encrypt(string)" + ] + }, + { + "cell_type": "markdown", + "id": "680f89f0", + "metadata": {}, + "source": [ + "> Compute the LM and NTLM hash for \"edinburgh\" and \"glasgow\". How many bytes are in the hash?\n", + "> Observe what happens to the hash when we use an input of \"aaaaaa\", \"aaaaaaa\", \"aaaaaaaa\", and \"aaaaaaaaaaaa\"?" + ] + } + ], + "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.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/04_key_exchange.ipynb b/z_jupyter/04_key_exchange.ipynb new file mode 100644 index 0000000..33c1a61 --- /dev/null +++ b/z_jupyter/04_key_exchange.ipynb @@ -0,0 +1,394 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7782a35", + "metadata": {}, + "source": [ + "\n", + "# Diffie Hellman\n", + "With key exchange, Bob and Alice end up with the same secret key, even though Eve is listening to their communications:\n", + "\n", + "\n", + "\n", + "The Diffie-Hellman method allows Bob and Alice to pass public values to each other and then derive the same key:\n", + "\n", + "\n", + "\n", + "For this Bob generates a secret value (b) and Alice generates a secret value (a). Bob passes g^b mod p, and Alice passes g^a mod p.\n" + ] + }, + { + "cell_type": "markdown", + "id": "1265ed12", + "metadata": {}, + "source": [ + "> Bob and Alice have agreed on the values:\n", + "\n", + "g=2879, N= 9929\n", + "\n", + "Bob Select x=6, Alice selects y=9\n", + "\n", + "Now calculate (using a calculator or Python:\n", + "\n", + "Alice’s A value (g^x mod N):\n", + "\n", + "Bob’s B value (g^y mod N):\n", + "\n", + "Now they exchange the values. Next calculate the shared key:\n", + "\n", + "Alice’s value (B^x mod N):\n", + "\n", + "Bobs’s value (A^y mod N):\n", + "\n", + "> Do they match? Yes/No" + ] + }, + { + "cell_type": "markdown", + "id": "3ed130a0", + "metadata": {}, + "source": [ + "We can use Python to implement a simple example with a given base generator (g) and prime number (p):" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "80911155", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "g: 5 (a shared value), n: 1001 (a prime number)\n", + "\n", + "Alice calculates:\n", + "a (Alice random): 410\n", + "Alice value (A): 298 (g^a) mod p\n", + "\n", + "Bob calculates:\n", + "b (Bob random): 105\n", + "Bob value (B): 265 (g^b) mod p\n", + "\n", + "Alice calculates:\n", + "Key: 155 (B^a) mod p\n", + "Key: 210e3b160c355818509425b9d9e9fd3ea2e287f2c43a13e5be8817140db0b9e6\n", + "\n", + "Bob calculates:\n", + "Key: 155 (A^b) mod p\n", + "Key: 210e3b160c355818509425b9d9e9fd3ea2e287f2c43a13e5be8817140db0b9e6\n" + ] + } + ], + "source": [ + "# 04_01.py\n", + "# https://asecuritysite.com/keyexchange/diffie_py\n", + "import random\n", + "import hashlib\n", + "\n", + "g=5\n", + "p=1001\n", + "\n", + "a=random.randint(1, p)\n", + "\n", + "b=random.randint(1,p)\n", + "\n", + "A = (g**a) % p\n", + "B = (g**b) % p\n", + "\n", + "print('g: ',g,' (a shared value), n: ',p, ' (a prime number)')\n", + "\n", + "print('\\nAlice calculates:')\n", + "print('a (Alice random): ',a)\n", + "print('Alice value (A): ',A,' (g^a) mod p')\n", + "\n", + "print('\\nBob calculates:')\n", + "print('b (Bob random): ',b)\n", + "print('Bob value (B): ',B,' (g^b) mod p')\n", + "\n", + "print('\\nAlice calculates:')\n", + "keyA=(B**a) % p\n", + "print('Key: ',keyA,' (B^a) mod p')\n", + "print('Key: ',hashlib.sha256(str(keyA).encode()).hexdigest())\n", + "\n", + "print('\\nBob calculates:')\n", + "keyB=(A**b) % p\n", + "print('Key: ',keyB,' (A^b) mod p')\n", + "print('Key: ',hashlib.sha256(str(keyB).encode()).hexdigest())" + ] + }, + { + "cell_type": "markdown", + "id": "6d22b5c8", + "metadata": {}, + "source": [ + "> Run the code and verify that Bob and Alice compute the same shared secret.\n", + "\n", + "> The (g**b) % p operation can be computationally intensive. Replace this operation with pow(g,b,p), and verify that the program still works.\n", + "\n", + "> Use a prime number of 997, and verify the operation of the code.\n", + "\n", + "> Use a prime number of 2^19-1, and verify the operation of the code." + ] + }, + { + "cell_type": "markdown", + "id": "b8e14951", + "metadata": {}, + "source": [ + "## Selecting generator for discrete logs\n", + "With a discrete log method, we have the form of:\n", + "\n", + "Y=g^x (mod p)\n", + "\n", + "For the base, we must select a base generator that produces all the possible values of Y for x, with a given prime number of p.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "548c6405", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "p: 11\n", + "Values for g^x mod p: \n", + "2 6 7 8 None\n" + ] + } + ], + "source": [ + "# 04_02.py\n", + "# https://asecuritysite.com/dh/pickg2\n", + "p=11\n", + "\n", + "def getG(p):\n", + "\n", + " for x in range (1,p):\n", + "\t rand = x\n", + "\t exp=1\n", + "\t next = rand % p\n", + "\n", + "\t while (next != 1 ):\n", + "\t\t next = (next*rand) % p\n", + "\t\t exp = exp+1\n", + "\n", + "\n", + "\t if (exp==p-1):\n", + "\t\t print (rand,end=' ')\n", + "\n", + "print (\"p: \",p)\n", + "print (\"Values for g^x mod p: \")\n", + "print (getG(p))" + ] + }, + { + "cell_type": "markdown", + "id": "222bec32", + "metadata": {}, + "source": [ + "> For a prime number of p=11, what are the possible base generator values?\n", + "> For a prime number of p=97, what are the possible base generator values?\n", + "> If we pick a value of p=11, and take a value of g=5, show that we do not output all the possible values for Y=g^x (mod p).\n", + "> If we pick a value of p=11, and take a value of g=6, show that we do output all the possible values for Y=g^x (mod p)." + ] + }, + { + "cell_type": "markdown", + "id": "8c457f5e", + "metadata": {}, + "source": [ + "# Elliptic Curve Diffie Hellman (ECDH)\n", + "The most popular key exchange method is ECDH (Elliptic Curve Diffie Hellman). This uses an elliptic curve method, of which the most popular curve is P256 (SECP256R1). \n", + "\n", + "\n", + "\n", + "The following is the some code:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "73037ef4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Name of curve: secp256r1\n", + "Generated key size: 32 bytes (256 bits)\n", + "\n", + "Bob private key value: 115386775143760985994606199743124500698563324842662591622007624940178885352350\n", + "Bob's public key: 3059301306072a8648ce3d020106082a8648ce3d0301070342000453c9db700d76051a285ac6ecf2a27bc82056faca157b52d9744d1f781577e2a8bd0011523ad95fc5012b2c1d5bbae06a51f4a64e34e5397e4c282fe595bf8037\n", + "\n", + "Alice private key value: 73556695653589803082300318169887127685187875212044508782243551530526730975658\n", + "Alice's public key: 3059301306072a8648ce3d020106082a8648ce3d03010703420004aa8ac00f7a33ab033eb047d9fe2892a9da3ad8381b8bd1f4e0007f89b13d5b6a243a3ec52af2c48903cdf662c6cf79acc05292bce510f4fcb11071cb6be338f2\n", + "\n", + "Bob's derived key: c1c76e98cb3ef84986fa5395b8da34bff86c180005ff3de1ea088807542ad517\n", + "Alice's derived key: c1c76e98cb3ef84986fa5395b8da34bff86c180005ff3de1ea088807542ad517\n" + ] + } + ], + "source": [ + "# 04_03.py\n", + "# https://asecuritysite.com/hazmat/hashnew13\n", + "from cryptography.hazmat.primitives import hashes\n", + "from cryptography.hazmat.primitives.asymmetric import ec\n", + "from cryptography.hazmat.primitives.kdf.hkdf import HKDF\n", + "from cryptography.hazmat.primitives import serialization\n", + "from cryptography.hazmat.backends import default_backend\n", + "import binascii\n", + "import sys\n", + "\n", + "Bob_private_key = ec.generate_private_key(ec.SECP384R1(),default_backend())\n", + "\n", + "Alice_private_key = ec.generate_private_key(ec.SECP384R1(),default_backend())\n", + "size=32 # 256 bit key\n", + "\n", + "\n", + "\n", + "Bob_private_key = ec.generate_private_key(ec.SECP256R1(),default_backend())\n", + "Alice_private_key = ec.generate_private_key(ec.SECP256R1(),default_backend()) \n", + "\n", + "Bob_shared_key = Bob_private_key.exchange(ec.ECDH(), Alice_private_key.public_key())\n", + "\n", + "Bob_derived_key = HKDF(algorithm=hashes.SHA256(),length=size,salt=None,info=b'',backend=default_backend()).derive(Bob_shared_key)\n", + "\n", + "Alice_shared_key = Alice_private_key.exchange(ec.ECDH(), Bob_private_key.public_key())\n", + "\n", + "Alice_derived_key = HKDF(algorithm=hashes.SHA256(),length=size,salt=None,info=b'',backend=default_backend()).derive(Alice_shared_key)\n", + "\n", + "print (\"Name of curve: \",Bob_private_key.public_key().curve.name)\n", + "print (f\"Generated key size: {size} bytes ({size*8} bits)\")\n", + "\n", + "vals = Bob_private_key.private_numbers()\n", + "print (f\"\\nBob private key value: {vals.private_value}\")\n", + "vals=Bob_private_key.public_key()\n", + "enc_point=binascii.b2a_hex(vals.public_bytes(encoding=serialization.Encoding.DER,format=serialization.PublicFormat.SubjectPublicKeyInfo)).decode()\n", + "print(\"Bob's public key: \",enc_point)\n", + "\n", + "vals = Alice_private_key.private_numbers()\n", + "print (f\"\\nAlice private key value: {vals.private_value}\")\n", + "vals=Alice_private_key.public_key()\n", + "enc_point=binascii.b2a_hex(vals.public_bytes(encoding=serialization.Encoding.DER,format=serialization.PublicFormat.SubjectPublicKeyInfo)).decode()\n", + "print(\"Alice's public key: \",enc_point)\n", + "\n", + "print (\"\\nBob's derived key: \",binascii.b2a_hex(Bob_derived_key).decode())\n", + "print(\"Alice's derived key: \",binascii.b2a_hex(Alice_derived_key).decode())" + ] + }, + { + "cell_type": "markdown", + "id": "4a3e413f", + "metadata": {}, + "source": [ + "> The program uses SECP256R1, and which is also known as the NIST P-256 curve. The Bitcoin curve is SECP256k1. Modify the program so that it uses this curve. Verify that it still works of the shared key.\n", + "\n", + "> An enhanced curve is SECP521R1. Modify the program so that it uses this curve. Verify that it still works of the shared key.\n", + "\n", + "> The Brainpool curves have some advantages over SECP2561. Modify the program so that it uses the P256R1 curve, and verify that it still works for the shared key.\n", + "\n", + "> The program creates a 256-bit key. Modify the program so that it generated a 128-bit key.\n", + "\n", + "> Modify the program so that is displays the shared key in a Base64 format.\n" + ] + }, + { + "cell_type": "markdown", + "id": "1c0e6351", + "metadata": {}, + "source": [ + "## DH Parameters \n", + "In the DH key generation we have two base parameters: g and p. We can use OpenSSL to generate these parameters. Such as:" + ] + }, + { + "cell_type": "markdown", + "id": "ead31dbd", + "metadata": {}, + "source": [ + ">> Generate 768-bit Diffie-Hellman parameters:\n", + "\n", + "openssl dhparam -out dhparams.pem -text 768 \n", + "\n", + ">> View your key exchange parameters with:\n", + "\n", + "cat dhparams.pem\t\n", + "\n", + ">> What is the value of g:\n", + "\n", + ">> How many bits does the prime number have?\n", + "\n", + ">> How long does it take to produce the parameters for 1,024 bits (Group 2)?\n", + "\n", + ">> How long does it take to produce the parameters for 1536 bits (Group 5)?\n", + "\n", + ">> How would we change the g value?" + ] + }, + { + "cell_type": "markdown", + "id": "a4266944", + "metadata": {}, + "source": [ + "## EC Paramters\n", + "With elliptic curves, we can use OpenSSL to generate a curve:" + ] + }, + { + "cell_type": "markdown", + "id": "f844cbaf", + "metadata": {}, + "source": [ + ">> Let’s look at the Elliptic curves we can create:\n", + "\n", + "openssl ecparam -list_curves\n", + "\n", + ">> We can create our elliptic parameter file with:\n", + "\n", + "openssl ecparam -name secp256k1 -out secp256k1.pem\n", + "\n", + ">> Now view the details with:\n", + "\n", + "openssl ecparam -in secp256k1.pem -text -param_enc explicit -noout\n", + "\n", + ">> What are the details of the key?\n", + "\n", + ">> Now we can create our key pair:\n", + "\n", + "openssl ecparam -in secp256k1.pem -genkey -noout -out mykey.pem\n", + "\n", + ">> Name three 160-bit curves:" + ] + } + ], + "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.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/05_mac.ipynb b/z_jupyter/05_mac.ipynb new file mode 100644 index 0000000..8ef7123 --- /dev/null +++ b/z_jupyter/05_mac.ipynb @@ -0,0 +1,337 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7782a35", + "metadata": {}, + "source": [ + "\n", + "# Message Authentication Code (MAC)\n", + "A MAC involves a hashing being signed by a shared secret key between Bob and Alice. For this, we define a given hashing method, such as SHA-256, and a given MAC type.\n", + "\n", + "## HMAC\n", + "HMAC (hash-based message authentication code) supports the usage of a key to hash data. This key is kept secret between Bob and Alice, and can be used to authentication both the data and that the sender still knows the secret. Overall HMAC can be used with a range of different hashing methods, such as MD5, SHA-1, SHA-256 (SHA-2) and SHA-3. \n", + "\n", + "\n", + "\n", + "For HMAC-BLAKE2, we can have:\n", + "\n", + "\n", + "\n", + "\n", + "The code to implement this is:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "52ead7e5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data: The quick brown fox jumps over the lazy dog\n", + " Hex: 54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67\n", + "Key: key\n", + " Hex: 6b6579\n", + "\n", + "HMAC-SHA256: f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8 97yD9DBThCSxMpjmqm+xQ+9NWaFJRhdZl0edvC0aPNg=\n", + "\n", + "HMAC Verified\n" + ] + } + ], + "source": [ + "# 05_01.py\n", + "# https://asecuritysite.com/hazmat/hashnew2\n", + "\n", + "from cryptography.hazmat.primitives import hashes, hmac\n", + "import binascii\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "st = \"The quick brown fox jumps over the lazy dog\"\n", + "hex=False\n", + "showhex=\"No\"\n", + "k=\"key\"\n", + "\n", + "def show_hmac(name,type,data,key):\n", + " digest = hmac.HMAC(key, type,backend=default_backend())\n", + " digest.update(data)\n", + " res=digest.finalize()\n", + " hex=binascii.b2a_hex(res).decode()\n", + " b64=binascii.b2a_base64(res).decode()\n", + " print (f\"HMAC-{name}: {hex} {b64}\")\n", + " \n", + " digest2 = hmac.HMAC(key, type,backend=default_backend())\n", + " digest2.update(data)\n", + " rtn=digest2.verify(res)\n", + " if (rtn==None): print(\"HMAC Verified\")\n", + "\n", + "try:\n", + "\tif (hex==True): data = binascii.a2b_hex(st)\n", + "\telse: data=st.encode()\n", + "\n", + "\tif (hex==True): key = binascii.a2b_hex(k)\n", + "\telse: key=k.encode()\n", + "\n", + "\tprint (\"Data: \",st)\n", + "\tprint (\" Hex: \",binascii.b2a_hex(data).decode())\n", + "\tprint (\"Key: \",k)\n", + "\tprint (\" Hex: \",binascii.b2a_hex(key).decode())\n", + "\tprint()\n", + "\n", + "\tshow_hmac(\"SHA256\",hashes.SHA256(),data,key)\n", + "\n", + "except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "7b15458c", + "metadata": {}, + "source": [ + "> Verify that the HMAC signature works with the given message.\n", + "\n", + "> Modify the program for HMAC-MD5 and verify that it works.\n", + "\n", + "> Modify the program for HMAC-SHA1 and verify that it works.\n", + "\n", + "> Modify the program for HMAC-SHA512 and verify that it works.\n", + "\n", + "> Observe how the signature varies for different hashing methods." + ] + }, + { + "cell_type": "markdown", + "id": "e4358ba5", + "metadata": {}, + "source": [ + "## Poly1305\n", + "For a stream cipher, it is important that we have a MAC, so that we can detect any changes in the bits in the cipher. ChaCha20 typically uses Poly1305 for its MAC. \n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "3e3b1107", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Message: --f=/Users/billbuchanan/Library/Jupyter/runtime/kernel-v2-71602TT5d9wYK6Zod.json\n", + "Key: a84edba3b95371406aa09d7f3c0e36b8fb1ac7808dd204078f0660cf9dfe3834\n", + "\n", + "Poly1305 tag (16 bytes): cf7fd40e03581a380911434fc41d40c2\n", + "Signature matches\n", + "\n", + "\n", + "--- Generating with alternative method ---\n", + "cf7fd40e03581a380911434fc41d40c2\n" + ] + } + ], + "source": [ + "# 05_02.py\n", + "# https://asecuritysite.com/hazmat/hashnew27\n", + "import os\n", + "import sys\n", + "import binascii\n", + "\n", + "from cryptography.hazmat.primitives import poly1305\n", + "\n", + "\n", + "message = \"message\"\n", + "\n", + "if (len(sys.argv)>1):\n", + "\tmessage=str(sys.argv[1])\n", + "\n", + "\n", + "message=message.encode()\n", + "key = os.urandom(32)\n", + "\n", + "c = poly1305.Poly1305(key)\n", + "\n", + "c.update(message)\n", + "\n", + "\n", + "signature = c.finalize()\n", + "\n", + "print (f\"Message: {message.decode()}\" )\n", + "\n", + "print (f\"Key: {binascii.b2a_hex(key).decode()}\")\n", + "\n", + "print (f\"\\nPoly1305 tag (16 bytes): {binascii.b2a_hex(signature).decode()}\")\n", + "\n", + "c = poly1305.Poly1305(key)\n", + "c.update(message)\n", + "\n", + "try:\n", + " rtn=c.verify(signature)\n", + " print (\"Signature matches\")\n", + "except:\n", + " print (\"Signature does not match\")\n", + "\n", + "\n", + "\n", + "print (\"\\n\\n--- Generating with alternative method ---\")\n", + "tag=poly1305.Poly1305.generate_tag(key,message)\n", + "print (binascii.b2a_hex(tag).decode())\n" + ] + }, + { + "cell_type": "markdown", + "id": "7a469b7b", + "metadata": {}, + "source": [ + "> Verify the the operation of the Poly1305 in this program.\n", + "\n", + "> What is the size of the key used in this example?\n", + "\n", + "> How many bits does the Poly1305 tag have?" + ] + }, + { + "cell_type": "markdown", + "id": "5eafe837", + "metadata": {}, + "source": [ + "## CMAC\n", + "CMACs (Cipher-based message authentication codes) create a message authentication codes (MACs) using a block cipher and a secret key. They differ from HMACs in that they use a symmetric key method for the MACs rather than a hashing method. In the following, we use the AES method to create the MAC:\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c613e949", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Message: --f=/Users/billbuchanan/Library/Jupyter/runtime/kernel-v2-71602TT5d9wYK6Zod.json\n", + "Type: TripleDES\n", + "Key: 56837cb6ddfc06db9f55553e653628cc3cdfaf738f45deee83698fdf60bd1c87\n", + "CMAC signature: 58118c98fa654fb7a42a392b5df31c26\n", + "Signature matches\n" + ] + } + ], + "source": [ + "# 05_03.py\n", + "# https://asecuritysite.com/hazmat/hashnew26\n", + "import os\n", + "import sys\n", + "import binascii\n", + "\n", + "from cryptography.hazmat.primitives import cmac\n", + "\n", + "from cryptography.hazmat.primitives.ciphers import algorithms\n", + "\n", + "message = \"message\"\n", + "ctype=\"TripleDES\"\n", + "\n", + "if (len(sys.argv)>1):\n", + "\tmessage=str(sys.argv[1])\n", + "if (len(sys.argv)>2):\n", + "\tctype=str(sys.argv[2])\n", + "\n", + "message=message.encode()\n", + "key = os.urandom(32)\n", + "\n", + "c = cmac.CMAC(algorithms.AES(key),backend=default_backend())\n", + "\n", + "\n", + "\n", + "c.update(message)\n", + "\n", + "c_copy = c.copy() \n", + "\n", + "signature = c.finalize()\n", + "\n", + "print (f\"Message: {message.decode()}\" )\n", + "print (f\"Type: {ctype}\" )\n", + "print (f\"Key: {binascii.b2a_hex(key).decode()}\")\n", + "\n", + "print (f\"CMAC signature: {binascii.b2a_hex(signature).decode()}\")\n", + "\n", + "try:\n", + " rtn=c_copy.verify(signature)\n", + " print (\"Signature matches\")\n", + "except:\n", + " print (\"Signature does not match\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "928069a0", + "metadata": {}, + "source": [ + "> Verify the operation of the code.\n", + "\n", + "> Rather than using AES, implement the code using 3DES.\n", + "\n", + "> Rather than using AES, implement the code using Blowfish." + ] + }, + { + "cell_type": "markdown", + "id": "472d880e", + "metadata": {}, + "source": [ + "## Generate HMAC with OpenSSL\n", + "We can use OpenSSL to generate MACs. For HMAC, we can generate:\n", + "\n", + "```\n", + "Message: Hello\n", + "Password: test\n", + "Hash: sha256\n", + "========================\n", + "HMAC:\n", + "Windows command: \"echo set /p=\"Hello\" | openssl dgst -sha256 -hmac test\"\n", + "Linux command: \"echo -n \"Hello\" | openssl dgst -sha256 -hmac test\"\n", + "Result:\n", + "e2bb88dfaff4fea88d7dc7a00d18b79bb971653de0fe54c7f3985e4daa1e6a25\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "b74c84f4", + "metadata": {}, + "source": [ + ">> With OpenSSL, generate the HMAC signature for a message of \"Goodbye\" and a key of \"qwerty123\". Use a hash of SHA1, SHA256, and SHA3-256. Check your answer [here](https://asecuritysite.com/mac/openssl_hmac)." + ] + } + ], + "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.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/06_kdf.ipynb b/z_jupyter/06_kdf.ipynb new file mode 100644 index 0000000..1cdb718 --- /dev/null +++ b/z_jupyter/06_kdf.ipynb @@ -0,0 +1,548 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7782a35", + "metadata": {}, + "source": [ + "\n", + "# Key Derivation Function (KDF)\n", + "\n", + "## HKDF\n", + "HKDF (HMAC Key Derivation Function) is used to generate an encryption key based on a password. We can use a range of hashing methods to dervie the encryption key. In this case we will use a range of hashing methods, and derive a key of a given size.\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4eb825c2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Key: 00\n", + " Hex: 3030\n", + "Salt: \n", + " Hex: \n", + "\n", + "HKDF SHA256: c9f9ff58d94c9d8901f5ed32e36f30af yfn/WNlMnYkB9e0y428wrw==\n", + "\n" + ] + } + ], + "source": [ + "# 06_01.py\n", + "# https://asecuritysite.com/hazmat/hashnew3\n", + "from cryptography.hazmat.primitives import hashes\n", + "from cryptography.hazmat.primitives.kdf.hkdf import HKDF\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "\n", + "import binascii\n", + "import sys\n", + "\n", + "st = \"00\"\n", + "hex=False\n", + "showhex=\"No\"\n", + "k=\"00\"\n", + "length=16\n", + "slt=\"\"\n", + "\n", + "def show_hash(name,type,data,length,salt):\n", + "\n", + " hkdf = HKDF(algorithm=type, length=length,salt=salt, info=b\"\",backend=default_backend())\n", + " mykey=hkdf.derive(data)\n", + " hex=binascii.b2a_hex(mykey).decode()\n", + " b64=binascii.b2a_base64(mykey).decode()\n", + " print (f\"HKDF {name}: {hex} {b64}\")\n", + " \n", + "\n", + "\n", + "\n", + "try:\n", + "\tif (hex==True): data = binascii.a2b_hex(st)\n", + "\telse: data=st.encode()\n", + "\tif (hex==True): salt = binascii.a2b_hex(slt)\n", + "\telse: salt=slt.encode()\n", + "\n", + "\n", + "\tprint (\"Key: \",st)\n", + "\tprint (\" Hex: \",binascii.b2a_hex(data).decode())\n", + "\n", + "\tprint (\"Salt: \",slt)\n", + "\tprint (\" Hex: \",binascii.b2a_hex(salt).decode())\n", + "\n", + "\tprint()\n", + "\n", + "\n", + "\tshow_hash(\"SHA256\",hashes.SHA256(),data,length,salt)\n", + "\n", + "\n", + "\n", + "except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "e39a85a4", + "metadata": {}, + "source": [ + "> Verify the operaton of the program.\n", + "\n", + "> Change the SHA-256 method to MD5. Verify the operation.\n", + "\n", + "> Change the SHA-256 method to SHA-1. Verify the operation.\n", + "\n", + "> Change the SHA-256 method to Blake2p, and use 64 bytes of hash. Verify the operation." + ] + }, + { + "cell_type": "markdown", + "id": "fa6da6d0", + "metadata": {}, + "source": [ + "## PBKDF2\n", + "PBKDF2 (Password-Based Key Derivation Function 2) is defined in RFC 2898 and generates a salted hash. Often this is used to create an encryption key from a defined password, and where it is not possible to reverse the password from the hashed value. It is used in TrueCrypt to generate the key required to read the header information of the encrypted drive, and which stores the encryption keys. Also, it is used in WPA-2 in order to create a hashed version of the password. With this, WPA-2 uses 4,096 interations. We can also specify the length of the generated hashed. \n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "23a3df3f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Message:\tHello\n", + "Salt:\t\te9ed6ffc41f8350c15ac0b67611ae723\n", + "Salt:\t\t6e1v/EH4NQwVrAtnYRrnIw==\n", + "Rounds:\t\t1000\n", + "Hash:\t\tsha1\n", + "\n", + "Key:\t\tdf9f33fc4e8d7ec3655d2369539caa0228e7946bc629076d3381e3616dac1f73\n", + "Key:\t\t358z/E6NfsNlXSNpU5yqAijnlGvGKQdtM4HjYW2sH3M=\n", + "KDF Verified\n" + ] + } + ], + "source": [ + "\n", + "# 06_02.py\n", + "# https://asecuritysite.com/pbkdf2/hazkdf\n", + "import os\n", + "import sys\n", + "import base64\n", + "from cryptography.hazmat.primitives import hashes\n", + "from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "round=1000\n", + "hashtype=0\n", + "length=32\n", + "message=\"Hello\"\n", + "\n", + "\n", + "salt = os.urandom(16)\n", + "h=None\n", + "if (hashtype==0): h=hashes.SHA1()\n", + "elif (hashtype==1): h=hashes.SHA512_224()\n", + "elif (hashtype==2): h=hashes.SHA512_256()\n", + "elif (hashtype==3): h=hashes.SHA224()\n", + "elif (hashtype==4): h=hashes.SHA256()\n", + "elif (hashtype==5): h=hashes.SHA384()\n", + "elif (hashtype==6): h=hashes.SHA512()\n", + "elif (hashtype==7): h=hashes.SHA3_224()\n", + "elif (hashtype==8): h=hashes.SHA3_256()\n", + "elif (hashtype==9): h=hashes.SHA3_384()\n", + "elif (hashtype==10): h=hashes.SHA3_512()\n", + "elif (hashtype==11): h=hashes.MD5()\n", + "elif (hashtype==12): h=hashes.SM3()\n", + "elif (hashtype==13): h=hashes.BLAKE2b(64)\n", + "elif (hashtype==14): h=hashes.BLAKE2s(32)\n", + "\n", + "\n", + "kdf = PBKDF2HMAC(algorithm=h,length=length,salt=salt, iterations=round,backend=default_backend())\n", + "\n", + "key = kdf.derive(message.encode())\n", + "\n", + "\n", + "# verify\n", + "\n", + "kdf = PBKDF2HMAC(algorithm=h, length=length,salt=salt, iterations=round,backend=default_backend())\n", + "\n", + "rtn=kdf.verify(message.encode(), key)\n", + "\n", + "print(f\"Message:\\t{message}\")\n", + "print(f\"Salt:\\t\\t{salt.hex()}\")\n", + "print(f\"Salt:\\t\\t{base64.b64encode(salt).decode()}\")\n", + "print(f\"Rounds:\\t\\t{round}\")\n", + "print(f\"Hash:\\t\\t{kdf._algorithm.name}\")\n", + "print(f\"\\nKey:\\t\\t{key.hex()}\")\n", + "print(f\"Key:\\t\\t{base64.b64encode(key).decode()}\")\n", + "if (rtn==None): print(\"KDF Verified\")" + ] + }, + { + "cell_type": "markdown", + "id": "1b11596f", + "metadata": {}, + "source": [ + "We can also use a pure PBKDF2 method with:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "906b48e7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using PBKDF2\n", + "Password: qwerty, Salt: \n", + "\n", + "Hash: b'b1444b43fe945ff29561e772401db5a0'\n" + ] + } + ], + "source": [ + "# 06_03.py\n", + "import binascii\n", + "from Crypto.Protocol.KDF import PBKDF2, HKDF\n", + "from Crypto.Hash import SHA256\n", + "from Crypto.Random import get_random_bytes\n", + "import sys\n", + "\n", + "password=\"qwerty\"\n", + "salt = get_random_bytes(16)\n", + "s=\"\"\n", + "type=1\n", + "bytes=16\n", + "\n", + "\n", + "salt=binascii.unhexlify(s)\n", + "\n", + "\n", + "KEK = PBKDF2(password, salt, bytes, count=1000, hmac_hash_module=SHA256)\n", + "print (\"Using PBKDF2\")\n", + "\n", + "\n", + "print (f\"Password: {password}, Salt: {s}\")\n", + "print (\"\\nHash: \",binascii.hexlify(KEK))" + ] + }, + { + "cell_type": "markdown", + "id": "19cdb854", + "metadata": {}, + "source": [ + "## scrypt\n", + "scrypt is a password-based key derivation function which produces a hash with a salt and iterations. The iteration count slows down the cracking and the salt makes pre-computation difficult. The main parameters are: passphrase (P); salt (S); Blocksize (r) and CPU/Memory cost parameter (N - a power of 2). \n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9f5ff8f", + "metadata": {}, + "outputs": [], + "source": [ + "# 06_04.py\n", + "import os\n", + "import sys\n", + "import base64\n", + "from cryptography.hazmat.primitives import hashes\n", + "from cryptography.hazmat.primitives.kdf.scrypt import Scrypt\n", + "\n", + "\n", + "length=32\n", + "message=\"Hello\"\n", + "N=14\n", + "r=8\n", + "p=1\n", + "\n", + "if (len(sys.argv)>1):\n", + "\tmessage=str(sys.argv[1])\n", + "if (len(sys.argv)>2):\n", + "\tN=int(sys.argv[2])\n", + "if (len(sys.argv)>3):\n", + "\tr=int(sys.argv[3])\n", + "if (len(sys.argv)>4):\n", + "\tlength=int(sys.argv[4])\n", + "if (len(sys.argv)>5):\n", + "\tp=int(sys.argv[5])\n", + "\n", + "\n", + "salt = os.urandom(16)\n", + "\n", + "\n", + "kdf = Scrypt(salt=salt,length=length,n=2**N,r=r,p=p)\n", + "\n", + "key = kdf.derive(message.encode())\n", + "\n", + "\n", + "# verify\n", + "\n", + "kdf = Scrypt(salt=salt,length=length,n=2**N,r=r,p=p)\n", + "\n", + "rtn=kdf.verify(message.encode(), key)\n", + "\n", + "print(\"== scrypt === \")\n", + "print(f\"Message:\\t{message}\")\n", + "print(f\"Salt:\\t\\t{salt.hex()}\")\n", + "print(f\"Salt:\\t\\t{base64.b64encode(salt).decode()}\")\n", + "print(f\"scrypt param:\\tn=2**{N}, r={r}, p={p}\")\n", + "print(f\"\\nKey:\\t\\t{key.hex()}\")\n", + "print(f\"Key:\\t\\t{base64.b64encode(key).decode()}\")\n", + "if (rtn==None): print(\"KDF Verified\")" + ] + }, + { + "cell_type": "markdown", + "id": "ed20a77c", + "metadata": {}, + "source": [ + "> From the Cryptography libary, which other KDFs are available?" + ] + }, + { + "cell_type": "markdown", + "id": "31a8309e", + "metadata": {}, + "source": [ + "\n", + "# bcrypt\n", + "MD5 and SHA-1 produce a hash signature, but this can be attacked by rainbow tables. Bcrypt is a more powerful hash generator for passwords and uses salt to create a non-recurrent hash. It was designed by Niels Provos and David Mazières, and is based on the Blowfish cipher. It is used as the default password hashing method for BSD and other systems. \n", + "\n", + "Overall it uses a 128-bit salt value, which requires 22 Radix-64 characters. It can use a number of iterations, which will slow down any brute-force cracking of the hashed value.\n", + "\n", + "For example, “Hello” with a salt value of “$2a$06$NkYh0RCM8pNWPaYvRLgN9.” gives:\n", + "\n", + "$2a$06$NkYh0RCM8pNWPaYvRLgN9.LbJw4gcnWCOQYIom0P08UEZRQQjbfpy\n", + "\n", + "As illustrated below, the first part is \"$2a$\" (or \"$2b$\"), and then followed by the number of iterations used (in this case is it 6 iterations (where each additional iternation doubles the hash time). The 128-bit (22 character) salt values comes after this, and then finally there is a 184-bit hash code (which is 31 characters).\n", + "\n", + "\n", + "\n", + "The slowness of Bcrypt is highlighted with a recent AWS EC2 server benchmark using hashcat [here]:\n", + "\n", + "Hash type: MD5 Speed/sec: 380.02M words\n", + "Hash type: SHA1 Speed/sec: 218.86M words\n", + "Hash type: SHA256 Speed/sec: 110.37M words\n", + "Hash type: bcrypt, Blowfish(OpenBSD) Speed/sec: 25.86k words\n", + "Hash type: NTLM. Speed/sec: 370.22M words\n", + "\n", + "You can see that Blowfish is almost four times slower than MD5 (380,000,000 words/sec down to only 25,860 words/sec). With John The Ripper:\n", + "\n", + "md5crypt [MD5 32/64 X2] 318237 c/s real, 8881 c/s virtual\n", + "bcrypt (\"$2a$05\", 32 iterations) 25488 c/s real, 708 c/s virtual\n", + "LM [DES 128/128 SSE2-16] 88090K c/s real, 2462K c/s virtual\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1badea25", + "metadata": {}, + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'bcrypt'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mbinascii\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mCrypto\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mProtocol\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mKDF\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mPBKDF2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mscrypt\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mHKDF\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 4\u001b[0;31m \u001b[0;32mimport\u001b[0m \u001b[0mbcrypt\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 5\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mCrypto\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mHash\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mSHA256\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mCrypto\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mRandom\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mget_random_bytes\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'bcrypt'" + ] + } + ], + "source": [ + "# 06_05.py\n", + "# https://asecuritysite.com/bcrypt/kdf\n", + "import binascii\n", + "from Crypto.Protocol.KDF import PBKDF2, scrypt,HKDF\n", + "import bcrypt\n", + "from Crypto.Hash import SHA256\n", + "from Crypto.Random import get_random_bytes\n", + "import sys\n", + "\n", + "password=\"qwerty\"\n", + "salt = get_random_bytes(16)\n", + "s=\"\"\n", + "type=1\n", + "bytes=16\n", + "\n", + "if (len(sys.argv)>1):\n", + "\tpassword=str(sys.argv[1])\n", + "if (len(sys.argv)>2):\n", + "\ts=str(sys.argv[2])\n", + "if (len(sys.argv)>3):\n", + "\ttype=int(sys.argv[3])\n", + "if (len(sys.argv)>4):\n", + "\tbytes=int(sys.argv[4])\n", + "\n", + "\n", + "salt=binascii.unhexlify(s)\n", + "\n", + "\n", + "KEK = bcrypt.kdf(password=password.encode(),salt=b'salt',desired_key_bytes=bytes,rounds=100)\n", + "print (\"Using bcrypt\")\n", + "\n", + "\n", + "\n", + "print (f\"Password: {password}, Salt: {s}\")\n", + "print (\"\\nHash: \",binascii.hexlify(KEK))" + ] + }, + { + "cell_type": "markdown", + "id": "5d4cdaac", + "metadata": {}, + "source": [ + "> Change the program so that the number of rounds is 500, 1000, 5000 and 10000. What happens to the creation of the hash?" + ] + }, + { + "cell_type": "markdown", + "id": "fe1a15b0", + "metadata": {}, + "source": [ + "## Argon 2.0\n", + "This case we will use PyNaCl (Networking and Cryptography) library, and which is a Python binding to libsodium. We will hash a password using SHA-256 and SHA-512, and also create a KDF (Key Derivation Function) using scrypt and Argon2. With Argon2, we have a memory robust key derivation function from a password and a salt value. Argon2 was designed Alex Biryukov, Daniel Dinu, and Dmitry Khovratovich is a key derivation function (KDF), where were we can create hashed values of passwords, or create encryption keys based on a password. It was a winner of the Password Hashing Competition in July 2015, and is robust against GPU and side channel attacks [paper]. We will also use the salt value as a key for the key-hash method of SIPHash24. " + ] + }, + { + "cell_type": "markdown", + "id": "2aa950e4", + "metadata": {}, + "source": [ + "With many fast hashing methods, such as MD5, we can now get billions or even trillions or hashes per second, where even 9 or 10 character passwords can be cracked for a reasonable financial cost. This includes salting of the password, as the salt is contained with the hashed password, and can be easily cracked with brute force (or dictionaries). The alternative is to use a hashing method which has a cost in memory, and for CPU processing. We also want to create a method which makes it difficult to apply parallel threads (and thus run it on GPUs). So a step forward is Argon2 which was designed Alex Biryukov, Daniel Dinu, and Dmitry Khovratovich as a key derivation function. It was a winner of the Password Hashing Competition in July 2015. It is resistant to GPU attacks, and also has a memory cost. The costs include: execution time (CPU cost); memory required (memory cost); and degree of parallelism.\n", + "\n", + "The parameters include:\n", + "\n", + "* Password (P): Defines the password bytes to be hashed\n", + "* Salt (S): Defines the bytes to be used for salting.\n", + "* Parallelism (p): Defines the number of thread that are required for the parallelism.\n", + "* TagLength (T): Define the number of bytes to return for the hash.\n", + "* MemorySizeKB (m): Amount of memory (in KB) to use.\n", + "\n", + "$argon2id$v=19$m=8,t=3,p=1$yh3bPPtc2tSnm8xpK1RZbw$CBE1uk3HK23zkotoY9280NWemhMj6FnljoDMSZ8PZ58\n", + "\n", + "Note: You will need to install the libary with \"pip install PyNaCl\".\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "634aa8a1", + "metadata": {}, + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'nacl'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# https://asecuritysite.com/nacl/nacl02\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;32mimport\u001b[0m \u001b[0mnacl\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mhash\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mnacl\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mhashlib\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mnacl\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpwhash\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mnacl\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mencoding\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'nacl'" + ] + } + ], + "source": [ + "# https://asecuritysite.com/argon2/nacl02\n", + "# 06_06.py\n", + "import nacl.hash\n", + "import nacl.hashlib\n", + "import nacl.pwhash\n", + "import nacl.encoding\n", + "import binascii\n", + "import sys\n", + "\n", + "from os import urandom\n", + "\n", + "password='hello'\n", + "salt=''\n", + "\n", + "\n", + "salt=salt.zfill(16)\n", + "\n", + "print(\"Password: \",password)\n", + "print(\"Salt: \",salt)\n", + "\n", + "salt=salt.encode()\n", + "password=password.encode()\n", + "\n", + "mlevel=nacl.pwhash.MEMLIMIT_INTERACTIVE\n", + "olevel=nacl.pwhash.OPSLIMIT_INTERACTIVE\n", + "\n", + "print (\"\\nSHA256: \",nacl.hash.sha256(password))\n", + "print (\"SHA512: \",nacl.hash.sha512(password))\n", + "print (\"\\nBlake2b: \",nacl.hashlib.blake2b(password,salt=salt).hexdigest())\n", + "print (\"Scrypt: \",binascii.hexlify(nacl.hashlib.scrypt(password,salt, n=2, r=8, p=1, maxmem=2**25, dklen=64)))\n", + "\n", + "\n", + "print(\"\\nArgon2id\")\n", + "print(binascii.hexlify(nacl.pwhash.argon2id.kdf(size=32,password=password,salt=salt,opslimit=3,memlimit=8192)))\n", + "print(nacl.pwhash.argon2id.str(password=password,opslimit=3,memlimit=8192))\n", + "\n", + "print(\"\\nArgon2i\")\n", + "\n", + "print(binascii.hexlify(nacl.pwhash.argon2i.kdf(size=32,password=password,salt=salt,opslimit=3,memlimit=8192)))\n", + "print(nacl.pwhash.argon2i.str(password=password,opslimit=3,memlimit=8192))\n", + "\n", + "salt=salt.zfill(32)\n", + "print(\"\\nScrypt\")\n", + "print(binascii.hexlify(nacl.pwhash.scrypt.kdf(size=32,password=password,salt=salt,opslimit=olevel,memlimit=mlevel)))\n", + "print(nacl.pwhash.scrypt.str(password=password,opslimit=3,memlimit=mlevel))" + ] + }, + { + "cell_type": "markdown", + "id": "358fadf4", + "metadata": {}, + "source": [ + "> What is the value of the salt we have used?\n", + "> The program for different values of opslimits, and observe the result." + ] + } + ], + "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.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/07_public_key.ipynb b/z_jupyter/07_public_key.ipynb new file mode 100644 index 0000000..29b3ca8 --- /dev/null +++ b/z_jupyter/07_public_key.ipynb @@ -0,0 +1,452 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7782a35", + "metadata": {}, + "source": [ + "\n", + "# Public Key Encryption\n", + "The two main method used for public key methods are RSA and ECC (Elliptic Curve Cryptography). With encryption Bob can use Alice's public key to encrypt data, and then she will use her private key to decrypt it:\n", + "\n", + "\n", + "\n", + "\n", + "## RSA\n", + "Overall, Bob generates two random prime numbers (p and q), and create a public modulus of:\n", + "\n", + "N=p.q\n", + "\n", + "Next Bob computes φ:\n", + "\n", + "φ=(p−1).(q−1)\n", + "\n", + "Bob then picks a public exponent of e and which does not share a factor with φ, and computes the private exponent as:\n", + "\n", + "d=e^{−1}(modφ)\n", + "\n", + "Bob will then use a public exponent (e) to cipher a message (M) with:\n", + "\n", + "C=M^e (mod N)\n", + "\n", + "To decrypt we use the private exponent (d) to decipher the ciphertext:\n", + "\n", + "M=C^d (mod N)\n", + "\n", + "Bob's public key is [e,N] and his private key is [d,N].\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "c6b56278", + "metadata": {}, + "source": [ + "In the following, we can create a PEM file which contains the details of the RSA keys:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4eb825c2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RSA key size: 512\n", + "M=5\n", + "\n", + "=== RSA Private key ===\n", + "p=98710391635011658445043479723965207117107833632529428791312969625241457542591 q=91574907843657592470899807478818283529012640132059371827885349647328299106257 d=2885732068138574425866818792280420045060058024166826343777663006218892797603507245051130842680362766169475457680538759083143987436171153523033398613515713 N=9039395017187541924674204339436090645879983879639675848014372451895974442048802001144807129089503774586739781388049268071126576402394788054433636812091887\n", + "\n", + "Bit length of p and q is 256\n", + "Bit length of N is 512\n", + "\n", + "=== RSA Public key ===\n", + "\n", + "N=9039395017187541924674204339436090645879983879639675848014372451895974442048802001144807129089503774586739781388049268071126576402394788054433636812091887 e=65537\n", + "\n", + "Message=5\n", + "Cipher=3544860157153023115291949857759384949327036235359598261186917451178297711294191011096096138343964468460279246190156272920524720409418704879718038288800283\n", + "Decrypt=5\n", + "\n", + "=== Private Key PEM format ===\n", + "Private key: -----BEGIN PRIVATE KEY-----\n", + "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEArJekeLwmsS1Gif9t\n", + "W85cd5FXHJV6HCN3oaIbeEkXoJIZAzQwguv/F2I/uy+A5bJGRk8Ls1oILZSRh6kk\n", + "5hRx7wIDAQABAkA3GSoeiSJpCaUtROfqmRSX70Q6EdYryX7VkOToH511WuhnZ6nZ\n", + "lq4f44Kl0GSwjyRKN8Y51iaGhQZf77YFqh3BAiEA2jwaDeS8C1/L3UDNHCOGTkeI\n", + "lE+9tJXpt/MXiIiURb8CIQDKdY+iOSk5zCw4ICTXOePdQVzEW5qPZLj1kHsfJoK/\n", + "0QIgbxLf/+pHe1KKpFXzDztVhEoMTdA4Mc2LY0Cq3acGMGkCIGUIj9ozzlfyhOqp\n", + "V9k0IXtrv+CZmiMO76JULK0Z6cvRAiEAqBdXqjbpaMEscG+U2pSvp7wJaNL+wTml\n", + "2Ex3GP+obQQ=\n", + "-----END PRIVATE KEY-----\n", + "\n", + "\n", + "=== Public Key PEM format ===\n", + "Public key: -----BEGIN PUBLIC KEY-----\n", + "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKyXpHi8JrEtRon/bVvOXHeRVxyVehwj\n", + "d6GiG3hJF6CSGQM0MILr/xdiP7svgOWyRkZPC7NaCC2UkYepJOYUce8CAwEAAQ==\n", + "-----END PUBLIC KEY-----\n", + "\n" + ] + } + ], + "source": [ + "# https://asecuritysite.com/hazmat/hashnew5\n", + "from cryptography.hazmat.primitives.asymmetric import rsa\n", + "from cryptography.hazmat.primitives import serialization\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "\n", + "size=512\n", + "M=5\n", + "\n", + "\n", + "try:\n", + "\tprint(f\"RSA key size: {size}\\nM={M}\\n\")\n", + "\n", + "\tprivate_key = rsa.generate_private_key(public_exponent=65537,key_size=size,backend=default_backend())\n", + "\n", + "\tpriv= private_key.private_numbers()\n", + "\tp=priv.p\n", + "\tq=priv.q \n", + "\td=priv.d\n", + "\tn=p*q\n", + "\tprint(\"=== RSA Private key ===\")\n", + "\tprint (f\"p={p} q={q} d={d} N={n}\")\n", + "\tprint (f\"\\nBit length of p and q is {p.bit_length()}\")\n", + "\tprint (f\"Bit length of N is {n.bit_length()}\")\n", + "\n", + "\tprint(\"\\n=== RSA Public key ===\")\n", + "\tpub = private_key.public_key()\n", + "\te=pub.public_numbers().e\n", + "\tn=pub.public_numbers().n\n", + "\tprint (f\"\\nN={n} e={e}\")\n", + "\n", + "\n", + "\n", + "\tC = pow(M,e,n)\n", + "\tPlain = pow(C,d,n)\n", + "\tprint (f\"\\nMessage={M}\")\n", + "\tprint (f\"Cipher={C}\")\n", + "\tprint (f\"Decrypt={Plain}\")\n", + "\n", + "\n", + "\n", + "\tprint(\"\\n=== Private Key PEM format ===\")\n", + "\tpem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", + "\tprint (\"Private key: \",pem.decode())\n", + "\n", + "\n", + "\tpem = pub.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)\n", + "\n", + "\tprint(\"\\n=== Public Key PEM format ===\")\n", + "\tprint (\"Public key: \",pem.decode())\n", + "\n", + "except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "fc3f0b7a", + "metadata": {}, + "source": [ + "### DER and PEM format\n", + "We need ways to distribute our public keys, private keys and digital certificates in a portable format. One of the most common forms is Distinguished Encoding Rules (DER) encoding of ASN.1. Overall it is truly binary representation of the encoded data. The other common format is PEM, and which converts the binary encoding into a text readable format. With PEM we can encode cryptographic infromation in a Base64 ASCII format and with plain-text headers and footers of \"-----BEGIN RSA PRIVATE KEY-----\" and \"-----END RSA PRIVATE KEY-----\", whereas with DER we have binary format. \n", + "\n", + "We need ways to distribute our public keys, private keys and digital certificates in a portable format. One of the most common forms is Distinguished Encoding Rules (DER) encoding of ASN.1 (Abstract Syntax Notation One). Overall it is a truly binary representation of the encoded data. The other common format is PEM, and which converts the binary encoding into a text readable format. With PEM we can encode cryptographic information in a Base64 ASCII format and with plain-text headers and footers of “ — — -BEGIN RSA PRIVATE KEY — — -” and “ — — -END RSA PRIVATE KEY — — -”, whereas with DER we have binary format.\n", + "\n", + "This page will look at the DER format and has code to decode a hex string and into its contents. Overall ASN.1 is used to define abstract types and values. One of the most basic types is SEQUENCE and is an ordered collection of one or more types. In DER, SEQUENCE is identified with a tag of “30”, and followed by a byte value for the length of the object defined. The other common types are OBJECT IDENTIFIER (and which has a tag of “06”) and BIT STRING (and which has a tag of “03”).\n", + "\n", + "The object identifier tag is used to define the cryptography methods used. An example identifier for ECC encryption is “1.2.840.10045.2.1”, and where 1 is OSI, 2 is member body, 840 is US (ANSI), and 10045 is “ansi-X9–62”, and “2” is key type [1]. Other common algorithms are: “1.2.840.113549.1.1.1” (X509 RSA), “1.2.840.10040.4.1” (X509 Digital Signature Standard -DSS), and “1.2.840.10046.2.1” (Diffie-Hellman — DH). The following is an example of the hex sequence for an object ID, and where we have the “06” tag, followed by an identifier for seven bytes (“07”), and then the Object ID of seven bytes (“2a8648ce3d0201”):\n", + "\n", + "06 07 2a8648ce3d0201 # Object ID - 7 bytes long - 1.2.840.10045.2.1 (ECC)\n", + "\n", + "We can also define the curve type in the object identifier, and where we have the form of iso(1), member-body(2), us(840), ansi-X9–62(10045), curves(3), prime(1). For example, 1.2.840.10045.3.1.7 defines ECDSA P-256. Other examples are SECP192R1 (“1.2.840.10045.3.1.1”), SECP224R1 (“1.3.132.0.33”), SECP256K1 (“1.3.132.0.10”), SECP256R1 (“1.2.840.10045.3.1.7”), SECP384R1 (“1.3.132.0.34”), SECP521R1 (“1.3.132.0.35”), and BRAINPOOLP256R1 (“1.3.36.3.3.2.8.1.1.7”). An example where we have an identifier (“06”), followed by the number of bytes identifier (“08”) and Object ID of eight bytes (“2a8648ce3d030107”):\n", + "\n", + "06 08 2a8648ce3d030107 # Object ID - 8 bytes long - 1.2.840.10045.3.1.7 (ECDSA P256)\n", + "\n", + "For the “03” tag, we define a bitstream for keys. In the following, we have “03”, followed by the number of bytes (66 bytes) for the keys, and then the keys are defined after this (64 bytes):\n", + "\n", + "```\n", + "03 42 # Bit stream - 0x42 (66 bytes long)\n", + "\n", + "0004 # Identifies public key\n", + "2927b10512bae3eddcfe467828128bad2903269919f7086069c8c4df6c732838 # Identifies public key x co-ordinate\n", + "c7787964eaac00e5921fb1498a60f4606766b3d9685001558d1a974e7341513e # Identifies public key y co-ordinate\n", + "```\n", + "\n", + "An example hex string for a DER format for ECC public keys is:\n", + "\n", + "```\n", + "3059301306072a8648ce3d020106082a8648ce3d030107034200042927b10512bae3eddcfe467828128bad2903269919f7086069c8c4df6c732838c7787964eaac00e5921fb1498a60f4606766b3d9685001558d1a974e7341513\n", + "```\n", + "\n", + "We can then break down with:\n", + "\n", + "```\n", + "30 59 # Sequence length 0x59 - 91 bytes long\n", + "30 13 # Sequence length 0x13 - 21 bytes long\n", + "06 07 2a8648ce3d0201 # Object ID - 7 bytes long - 1.2.840.10045.2.1 (ECC)\n", + "06 08 2a8648ce3d030107 # Object ID - 8 bytes long - 1.2.840.10045.3.1.7 (ECDSA P256)\n", + "03 42 # Bit stream - 0x42 (66 bytes long)\n", + "0004 # Identifies public key\n", + "2927b10512bae3eddcfe467828128bad2903269919f7086069c8c4df6c732838 # Identifies public key x co-ordinate\n", + "c7787964eaac00e5921fb1498a60f4606766b3d9685001558d1a974e7341513e # Identifies public key y co-ordinate\n", + "```\n", + "\n", + "If we need to convert a DER into a hex format, we can just use \"xxd\" and \"tr\":\n", + "\n", + "```\n", + "% xxd -plain 512b-rsa-example-keypair.der | tr -d '\\n'\n", + "3082013b0201000241009bfc6690798442bbab13fd2b7bf8de1512e5f193e3068a7bb8b1e19e26bb9501bfe730ed648502dd1569a834b006ec3f353c1e1b2b8ffa8f001bdf07c6ac5307020301000102401b4af77b31f7e56146d6d1866943ab400eb5732688239dd9760091d4853c6f1ec051ebe6905b417fe6aa316bac59539626f1aedabe55a47540225f2717a0d291022100c8c4277cd561adbf328e1ecbe894f49f5577e5a8c970d00f8104b709b21b53e9022100c6e665be10c86db71eee8e41bce867099d8dee461bd590b9ee0dc5f9c6c9c96f0221009bb0318706da36a89c85c5b00eeee43c6345151dad0904efe0f74d1201c25b71022046d1258c84a1381f290e3aec40fc6623504b8678c3d448514ae6f0843c3900550221008d036e290ed74e1f2770b52079fb316a14b7e6559a6540cfe0e646f8b28ef4630a\n", + "```\n", + "\n", + "In OpenSSL, we can convert from DER to PEM with:\n", + "\n", + "```\n", + "openssl x509 -inform der -in mycert.der -out mycert.pem\n", + "```\n", + "and:\n", + "\n", + "```\n", + "openssl x509 -outform der -in mycert.pem -out mycert.der\n", + "```\n", + "\n", + "An example of a public key is:\n", + "```\n", + "-----BEGIN PUBLIC KEY-----\n", + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw51PMBm2psyIjHPU1efH\n", + "Ulyh22zy3hEhlsNPH6/Cqg0HJorX1WbNKLfiU2aAt24jn4CC+y8PusrmMMCIca5x\n", + "0L4XZxm14QvKKImIOMOMblS1Te29n64HuuQ9owKLHuSMww4wiLiY/nAvjK/5/kKT\n", + "HL6x7nK/Pq72eoQ/etFBkaX5nYGUD/+G+5BgAPx1mBgU5/y9+/+QZ9xbYU6zogOW\n", + "Tfa6rDMSAbmJOtkk1ghnuaq4dSoHWbW+zpHMVtjtHgzDGhX9KjOmvSDQIGn4wevD\n", + "p2yDLULUbsdO4ylacTkxyIc92ZHdZeP6Hh+KhNC04Z65zwXLEA3M4bucX+u6nszW\n", + "xwIDAQAB\n", + "-----END PUBLIC KEY-----\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "3ca1eac1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "=== Private Key PEM format ===\n", + "\n", + "Private key (PEM):\n", + " -----BEGIN PRIVATE KEY-----\n", + "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA15v1euL6NUSl9Mcx\n", + "+/u9x+K9/6/Ird29f37QXQTvWc7BBRaRLDOY4LGA3DclBAWP6zrsMjrgC9nvdOJV\n", + "5QaXnwIDAQABAkEAq4ZWFyoFFWWEhSQG/sj55M/ciVGl27PA8nHHf4jShWVAF1bO\n", + "/4XrU+L64kAHrx3oK2QyU3zTisziZo1dV0VFgQIhAP76XMiECELFvm0hVH1uJw7I\n", + "jwfZBVTdLXXQXX78bebfAiEA2HkzEAiz+zv3/QO7jjWHgBBFd8RlkxRkfxX6in5t\n", + "J0ECIQCF5hQzeKKd7hpCVO55sA7yxH/YLy+NkVj+NZ3jwcw1IQIgNJW8gbibf/wh\n", + "UaUGepUmRUWumllrEz9w6i48nkf33AECIHS/+veFHnZSYgG5FP3Cbs8IIhGxXzVg\n", + "IOdwpeFnzOrq\n", + "-----END PRIVATE KEY-----\n", + "\n", + "\n", + "Private key (DER):\n", + " 30820155020100300d06092a864886f70d01010105000482013f3082013b020100024100d79bf57ae2fa3544a5f4c731fbfbbdc7e2bdffafc8adddbd7f7ed05d04ef59cec10516912c3398e0b180dc372504058feb3aec323ae00bd9ef74e255e506979f0203010001024100ab8656172a05156584852406fec8f9e4cfdc8951a5dbb3c0f271c77f88d28565401756ceff85eb53e2fae24007af1de82b6432537cd38acce2668d5d57454581022100fefa5cc8840842c5be6d21547d6e270ec88f07d90554dd2d75d05d7efc6de6df022100d879331008b3fb3bf7fd03bb8e358780104577c4659314647f15fa8a7e6d274102210085e6143378a29dee1a4254ee79b00ef2c47fd82f2f8d9158fe359de3c1cc352102203495bc81b89b7ffc2151a5067a95264545ae9a596b133f70ea2e3c9e47f7dc01022074bffaf7851e76526201b914fdc26ecf082211b15f356020e770a5e167cceaea\n", + "\n", + "=== Public Key format ===\n", + "Public key (PEM):\n", + " -----BEGIN PUBLIC KEY-----\n", + "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANeb9Xri+jVEpfTHMfv7vcfivf+vyK3d\n", + "vX9+0F0E71nOwQUWkSwzmOCxgNw3JQQFj+s67DI64AvZ73TiVeUGl58CAwEAAQ==\n", + "-----END PUBLIC KEY-----\n", + "\n", + "\n", + "Public key (DER):\n", + " 305c300d06092a864886f70d0101010500034b003048024100d79bf57ae2fa3544a5f4c731fbfbbdc7e2bdffafc8adddbd7f7ed05d04ef59cec10516912c3398e0b180dc372504058feb3aec323ae00bd9ef74e255e506979f0203010001\n", + "\n", + "Public key (OpenSSL):\n", + " ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDXm/V64vo1RKX0xzH7+73H4r3/r8it3b1/ftBdBO9ZzsEFFpEsM5jgsYDcNyUEBY/rOuwyOuAL2e904lXlBpef\n" + ] + } + ], + "source": [ + "# https://asecuritysite.com/hazmat/hashnew6\n", + "from cryptography.hazmat.primitives.asymmetric import rsa\n", + "from cryptography.hazmat.primitives import serialization\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "import binascii\n", + "\n", + "size=512\n", + "\n", + "try:\n", + " private_key = rsa.generate_private_key(public_exponent=65537,key_size=size,backend=default_backend())\n", + "\n", + "\n", + " pub = private_key.public_key()\n", + "\n", + " print(\"\\n=== Private Key PEM format ===\")\n", + " pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", + "\n", + " der= private_key.private_bytes(encoding=serialization.Encoding.DER,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", + "\n", + "\n", + " print (\"\\nPrivate key (PEM):\\n\",pem.decode())\n", + " print (\"\\nPrivate key (DER):\\n\",binascii.b2a_hex(der).decode())\n", + "\n", + "\n", + " pem = pub.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo).decode(\"utf-8\")\n", + "\n", + " openssh = pub.public_bytes(encoding=serialization.Encoding.OpenSSH,format=serialization.PublicFormat.OpenSSH)\n", + "\n", + "\n", + " print(\"\\n=== Public Key format ===\")\n", + "\n", + " der=''.join(pem.split('\\n')[1:-2])\n", + " print (\"Public key (PEM):\\n\",pem)\n", + " print (\"\\nPublic key (DER):\\n\",binascii.b2a_hex(binascii.a2b_base64(der)).decode())\n", + " print (\"\\nPublic key (OpenSSL):\\n\",openssh.decode())\n", + "\n", + "except Exception as e:\n", + " print(e)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "5375d045", + "metadata": {}, + "source": [ + "## Elliptic Curve\n", + "With Elliptic Curve Cryptography (ECC) we can use a Weierstrass curve form of the form of y^2=x^3+ax+b(modp). Bitcoin and Ethereum use secp256k1 and which has the form of y^2=x^3+7(modp). In most cases, though, we use the NIST defined curves. These are SECP256R1, SECP384R1, and SECP521R1, but an also use SECP224R1 and SECP192R1. SECP256R1 has 256-bit (x,y) points, and where the private key is a 256-bit scalar value (a) and which gives a public key point of a.G. In this case, G is the base point of the curve. This page generates a range of ECC key pair, and displays the private and public key in various formats. The encoding formats are PEM, DER and Raw, and the formating is undertaken with OpenSSH, PublicSubjectInfo and Raw. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "477d486c", + "metadata": {}, + "outputs": [], + "source": [ + "from cryptography.hazmat.primitives import serialization as crypto_serialization\n", + "from cryptography.hazmat.primitives.asymmetric import ec\n", + "from cryptography.hazmat.backends import default_backend as crypto_default_backend\n", + "import binascii\n", + "import sys\n", + "\n", + "\n", + "private_key_encoding= crypto_serialization.Encoding.PEM\n", + "private_key_format= crypto_serialization.PrivateFormat.OpenSSH\n", + "public_key_format= crypto_serialization.PublicFormat.OpenSSH\n", + "# PEM or DER\n", + "public_key_encode=0\n", + "public_key_form=0\n", + "private_key_encode=0\n", + "private_key_form=0\n", + "curve =0\n", + "\n", + "\n", + "\n", + "if (private_key_encode==0): \n", + "\tprivate_key_encoding= crypto_serialization.Encoding.PEM\n", + "elif (private_key_encode==1): \n", + "\tprivate_key_encoding= crypto_serialization.Encoding.DER\n", + "else:\n", + "\tprivate_key_encoding= crypto_serialization.Encoding.Raw\n", + "\n", + "if (private_key_form==0): \n", + "\tprivate_key_format= crypto_serialization.PrivateFormat.PKCS8\n", + "elif (private_key_form==1): \n", + "\tprivate_key_format= crypto_serialization.PrivateFormat.OpenSSH\n", + "else:\n", + "\tprivate_key_format= crypto_serialization.PrivateFormat.Raw\n", + "\n", + "if (public_key_encode==0): \n", + "\tpublic_key_encoding= crypto_serialization.Encoding.PEM\n", + "elif (public_key_encode==1): \n", + "\tpublic_key_encoding= crypto_serialization.Encoding.DER\n", + "elif (public_key_encode==2): \n", + "\tpublic_key_encoding= crypto_serialization.Encoding.OpenSSH\n", + "else:\n", + "\tpublic_key_encoding= crypto_serialization.Encoding.Raw\n", + "\n", + "if (public_key_form==0): \n", + "\tpublic_key_format= crypto_serialization.PublicFormat.SubjectPublicKeyInfo\n", + "elif (public_key_form==1): \n", + "\tpublic_key_format= crypto_serialization.PublicFormat.OpenSSH\n", + "else:\n", + "\tpublic_key_format= crypto_serialization.PublicFormat.Raw\n", + "\n", + "\n", + "key=ec.generate_private_key(ec.SECP256K1())\n", + "\n", + "\n", + "\n", + "try:\n", + "\tprint(\"Private key encoding:\\t\",private_key_encoding)\n", + "\tprint(\"Private key format:\\t\",private_key_format)\n", + "\tprint(\"Public key encoding:\\t\",public_key_encoding)\n", + "\tprint(\"Public key format:\\t\",public_key_format)\n", + "\tprint(\"Curve:\\t\\t\\t\",key.curve.name)\n", + "\tprint(\"Key size:\\t\\t\",key.curve.key_size)\n", + "\n", + "\tprivate_key = key.private_bytes(private_key_encoding,private_key_format,crypto_serialization.NoEncryption())\n", + "\n", + "\tif (private_key_encoding== crypto_serialization.Encoding.DER or private_key_encoding== crypto_serialization.Encoding.Raw):\n", + "\t\tprint(f\"\\nPrivate key:\\n{binascii.b2a_hex(private_key).decode()}\")\n", + "\telse:\n", + "\t\tprint(f\"\\nPrivate key:\\n{private_key.decode()}\")\n", + "except Exception as e:\n", + "\tprint(\"Private key error: \",e) \n", + "try:\n", + "\tpublic_key = key.public_key().public_bytes(public_key_encoding,public_key_format)\n", + "\n", + "\tif (public_key_encoding== crypto_serialization.Encoding.DER or public_key_encoding== crypto_serialization.Encoding.Raw):\n", + "\t\tprint(f\"\\nPublic key:\\n{binascii.b2a_hex(public_key).decode()}\")\n", + "\telse:\n", + "\t\tprint(f\"\\nPublic key:\\n{public_key.decode()}\")\n", + "except Exception as e:\n", + "\tprint(\"Public key error: \",e) " + ] + }, + { + "cell_type": "markdown", + "id": "806aa071", + "metadata": {}, + "source": [ + "> Modify the program so that it shows the keys as PEM, DER and Raw. How do the key differ?\n", + "> Update the program so that is uses the SECP256R1 curve, and verify its operation.\n", + "> Update the program so that is uses the SECP521R1 curve, and verify its operation." + ] + } + ], + "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.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/08_public_key_enc.ipynb b/z_jupyter/08_public_key_enc.ipynb new file mode 100644 index 0000000..83f5ab7 --- /dev/null +++ b/z_jupyter/08_public_key_enc.ipynb @@ -0,0 +1,483 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7782a35", + "metadata": {}, + "source": [ + "\n", + "# Public Key Encryption\n", + "The two main method used for public key methods are RSA and ECC (Elliptic Curve Cryptography). With encryption Bob can use Alice's public key to encrypt data, and then she will use her private key to decrypt it:\n", + "\n", + "\n", + "\n", + "\n", + "## RSA\n", + "Overall, Bob generates two random prime numbers (p and q), and create a public modulus of:\n", + "\n", + "N=p.q\n", + "\n", + "Next Bob computes φ:\n", + "\n", + "φ=(p−1).(q−1)\n", + "\n", + "Bob then picks a public exponent of e and which does not share a factor with φ, and computes the private exponent as:\n", + "\n", + "d=e^{−1}(modφ)\n", + "\n", + "Bob will then use a public exponent (e) to cipher a message (M) with:\n", + "\n", + "C=M^e (mod N)\n", + "\n", + "To decrypt we use the private exponent (d) to decipher the ciphertext:\n", + "\n", + "M=C^d (mod N)\n", + "\n", + "Bob's public key is [e,N] and his private key is [d,N].\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "8fcac21d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Message=Hello\n", + "p=1015811308611355393\n", + "q=755523213775410319\n", + "\n", + "d=589399628769593821059839430475148801\n", + "e=65537\n", + "N=767469024471456365658376241308500367\n", + "\n", + "Private key (d,n)\n", + "Public key (e,n)\n", + "\n", + "cipher=302667991570245321224673969207643936\n", + "decipher=b'Hello'\n" + ] + } + ], + "source": [ + "# https://asecuritysite.com/rsa/rsa12\n", + "from Crypto.Util.number import *\n", + "from Crypto import Random\n", + "import Crypto\n", + "import libnum\n", + "\n", + "bits=60\n", + "msg=\"Hello\"\n", + "\n", + "p = Crypto.Util.number.getPrime(bits, randfunc=Crypto.Random.get_random_bytes)\n", + "q = Crypto.Util.number.getPrime(bits, randfunc=Crypto.Random.get_random_bytes)\n", + "\n", + "n = p*q\n", + "PHI=(p-1)*(q-1)\n", + "\n", + "e=65537\n", + "d=libnum.invmod(e,PHI)\n", + "\n", + "m= bytes_to_long(msg.encode('utf-8'))\n", + "\n", + "c=pow(m,e, n)\n", + "res=pow(c,d ,n)\n", + "\n", + "print (\"Message=%s\\np=%s\\nq=%s\\n\\nd=%d\\ne=%d\\nN=%s\\n\\nPrivate key (d,n)\\nPublic key (e,n)\\n\\ncipher=%s\\ndecipher=%s\" % (msg,p,q,d,e,n,c,(long_to_bytes(res))))" + ] + }, + { + "cell_type": "markdown", + "id": "788ce969", + "metadata": {}, + "source": [ + "\n", + "> Change the size of the prime numbers to 100 bits, and verify that the program works.\n", + "\n", + "> Change the \"d=libnum.invmod(e,PHI)\" to \"pow(e,-1,PHI)\" and show that the code still works correctly." + ] + }, + { + "cell_type": "markdown", + "id": "0a96db97", + "metadata": {}, + "source": [ + "A more complex example is:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d7657bf", + "metadata": {}, + "outputs": [], + "source": [ + "# https://asecuritysite.com/hazmat/hashnew8\n", + "from cryptography.hazmat.primitives.asymmetric import rsa\n", + "from cryptography.hazmat.primitives import serialization\n", + "from cryptography.hazmat.primitives.asymmetric import padding\n", + "from cryptography.hazmat.primitives import hashes\n", + "import binascii\n", + "import sys\n", + "\n", + "\n", + "size=1024\n", + "message = b\"Hello world\"\n", + "\n", + "if (len(sys.argv)>1):\n", + "\tmessage=str(sys.argv[1]).encode()\n", + "if (len(sys.argv)>2):\n", + "\tsize=int(sys.argv[2])\n", + "\n", + "print(\"Message: \",message)\n", + "print(\"Key size: \",size)\n", + "try:\n", + " private_key = rsa.generate_private_key(public_exponent=65537,key_size=size)\n", + "\n", + "\n", + " pub = private_key.public_key()\n", + "\n", + " ciphertext = pub.encrypt(message,padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),algorithm=hashes.SHA256(),label=None))\n", + "\n", + " print (\"\\nCiphertext:\\n\",binascii.b2a_hex(ciphertext).decode())\n", + "\n", + " plaintext = private_key.decrypt(ciphertext,padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),algorithm=hashes.SHA256(),label=None))\n", + " print(\"\\nDecrypted Message: \",plaintext.decode())\n", + "\n", + " print(\"\\n=== Private Key PEM format ===\")\n", + " pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", + "\n", + "\n", + " print (\"\\nPrivate key (PEM):\\n\",pem.decode())\n", + "\n", + "\n", + " pem = pub.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo).decode(\"utf-8\")\n", + "\n", + "\n", + "\n", + " print(\"\\n=== Public Key format ===\")\n", + " print (\"Public key (PEM):\\n\",pem)\n", + "\n", + "\n", + "except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "3d288d40", + "metadata": {}, + "source": [ + "> Implement the code with 2K RSA key, and verify its operation.\n", + "\n", + "> Implement the program using a DER format for the keys. When you view the keys, what do you observe from the first two hex characters of the hex value of the keys?\n", + "\n", + "> The code uses SHA-256 for the padding method. Change the implementation of this to SHA-1, and verify the operation." + ] + }, + { + "cell_type": "markdown", + "id": "6d4d695a", + "metadata": {}, + "source": [ + "## ECIES\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ada57ad3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Private key: 0x8429f9e6136fcfa883130853defd319d931e25207749de92b5e12a80c8e62619\n", + "Public key: (0xa4581b11e2aa0249274374216d8fb965517e45e6bcc337f3fc679c2bfd84dce7, 0x4716b0eaf5f748af3921aac8455e623b96b590453f313b4d200419a801c11267)\n", + "\n", + "\n", + "=========================\n", + "Random value: 23353093005829895849666617279986636640\n", + "rG: (38430190155405327152400342621384391216588051326127513706497722898874838651319, 32551409443475257236910193737311399537798158570670164100113247023590451377060)\n", + "\n", + "\n", + "======Symmetric key========\n", + "Encryption key: 34425016396310312658614745921144437662843062008454472115656949568073393878924 34425016396310312658614745921144437662843062008454472115656949568073393878924\n", + "Encrypted:\t b'02c9b6ed4ffcf2bdf4f3118ecc232326'\n", + "Decrypted:\t Hello\n" + ] + } + ], + "source": [ + "# https://asecuritysite.com/ecc/ecc3\n", + "import collections\n", + "import hashlib\n", + "import random\n", + "import binascii\n", + "import sys\n", + "from Crypto.Cipher import AES\n", + "import Padding\n", + "\n", + "\n", + "def enc_long(n):\n", + " '''Encodes arbitrarily large number n to a sequence of bytes.\n", + " Big endian byte order is used.'''\n", + " s = \"\"\n", + " while n > 0:\n", + " s = chr(n & 0xFF) + s\n", + " n >>= 8\n", + " return s\n", + "\n", + "\n", + "\n", + "\n", + "# Padding for the input string --not\n", + "# related to encryption itself.\n", + "BLOCK_SIZE = 16 # Bytes\n", + "pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \\\n", + " chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)\n", + "unpad = lambda s: s[:-ord(s[len(s) - 1:])]\n", + "\n", + "\n", + "def encrypt(plaintext,key, mode):\n", + "\tencobj = AES.new(key,mode)\n", + "\treturn(encobj.encrypt(plaintext))\n", + "\n", + "def decrypt(ciphertext,key, mode):\n", + "\tencobj = AES.new(key,mode)\n", + "\treturn(encobj.decrypt(ciphertext))\n", + "\n", + "\n", + "EllipticCurve = collections.namedtuple('EllipticCurve', 'name p a b g n h')\n", + "\n", + "curve = EllipticCurve(\n", + " 'secp256k1',\n", + " # Field characteristic.\n", + " p=0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f,\n", + " # Curve coefficients.\n", + " a=0,\n", + " b=7,\n", + " # Base point.\n", + " g=(0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,\n", + " 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8),\n", + " # Subgroup order.\n", + " n=0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141,\n", + " # Subgroup cofactor.\n", + " h=1,\n", + ")\n", + "\n", + "\n", + "# Modular arithmetic ##########################################################\n", + "\n", + "def inverse_mod(k, p):\n", + " \"\"\"Returns the inverse of k modulo p.\n", + " This function returns the only integer x such that (x * k) % p == 1.\n", + " k must be non-zero and p must be a prime.\n", + " \"\"\"\n", + " if k == 0:\n", + " raise ZeroDivisionError('division by zero')\n", + "\n", + " if k < 0:\n", + " # k ** -1 = p - (-k) ** -1 (mod p)\n", + " return p - inverse_mod(-k, p)\n", + "\n", + " # Extended Euclidean algorithm.\n", + " s, old_s = 0, 1\n", + " t, old_t = 1, 0\n", + " r, old_r = p, k\n", + "\n", + " while r != 0:\n", + " quotient = old_r // r\n", + " old_r, r = r, old_r - quotient * r\n", + " old_s, s = s, old_s - quotient * s\n", + " old_t, t = t, old_t - quotient * t\n", + "\n", + " gcd, x, y = old_r, old_s, old_t\n", + "\n", + " assert gcd == 1\n", + " assert (k * x) % p == 1\n", + "\n", + " return x % p\n", + "\n", + "\n", + "# Functions that work on curve points #########################################\n", + "\n", + "def is_on_curve(point):\n", + " \"\"\"Returns True if the given point lies on the elliptic curve.\"\"\"\n", + " if point is None:\n", + " # None represents the point at infinity.\n", + " return True\n", + "\n", + " x, y = point\n", + "\n", + " return (y * y - x * x * x - curve.a * x - curve.b) % curve.p == 0\n", + "\n", + "\n", + "def point_neg(point):\n", + " \"\"\"Returns -point.\"\"\"\n", + " assert is_on_curve(point)\n", + "\n", + " if point is None:\n", + " # -0 = 0\n", + " return None\n", + "\n", + " x, y = point\n", + " result = (x, -y % curve.p)\n", + "\n", + " assert is_on_curve(result)\n", + "\n", + " return result\n", + "\n", + "\n", + "def point_add(point1, point2):\n", + " \"\"\"Returns the result of point1 + point2 according to the group law.\"\"\"\n", + " assert is_on_curve(point1)\n", + " assert is_on_curve(point2)\n", + "\n", + " if point1 is None:\n", + " # 0 + point2 = point2\n", + " return point2\n", + " if point2 is None:\n", + " # point1 + 0 = point1\n", + " return point1\n", + "\n", + " x1, y1 = point1\n", + " x2, y2 = point2\n", + "\n", + " if x1 == x2 and y1 != y2:\n", + " # point1 + (-point1) = 0\n", + " return None\n", + "\n", + " if x1 == x2:\n", + " # This is the case point1 == point2.\n", + " m = (3 * x1 * x1 + curve.a) * inverse_mod(2 * y1, curve.p)\n", + " else:\n", + " # This is the case point1 != point2.\n", + " m = (y1 - y2) * inverse_mod(x1 - x2, curve.p)\n", + "\n", + " x3 = m * m - x1 - x2\n", + " y3 = y1 + m * (x3 - x1)\n", + " result = (x3 % curve.p,\n", + " -y3 % curve.p)\n", + "\n", + " assert is_on_curve(result)\n", + "\n", + " return result\n", + "\n", + "\n", + "def scalar_mult(k, point):\n", + " \"\"\"Returns k * point computed using the double and point_add algorithm.\"\"\"\n", + " assert is_on_curve(point)\n", + "\n", + " if k % curve.n == 0 or point is None:\n", + " return None\n", + "\n", + " if k < 0:\n", + " # k * point = -k * (-point)\n", + " return scalar_mult(-k, point_neg(point))\n", + "\n", + " result = None\n", + " addend = point\n", + "\n", + " while k:\n", + " if k & 1:\n", + " # Add.\n", + " result = point_add(result, addend)\n", + "\n", + " # Double.\n", + " addend = point_add(addend, addend)\n", + "\n", + " k >>= 1\n", + "\n", + " assert is_on_curve(result)\n", + "\n", + " return result\n", + "\n", + "\n", + "# Keypair generation and ECDSA ################################################\n", + "\n", + "def make_keypair():\n", + " \"\"\"Generates a random private-public key pair.\"\"\"\n", + " private_key = random.randrange(1, curve.n)\n", + " public_key = scalar_mult(private_key, curve.g)\n", + "\n", + " return private_key, public_key\n", + "\n", + "\n", + "\n", + "message=\"Hello\"\n", + "\n", + "\n", + "dA, Qa = make_keypair()\n", + "print(\"Private key:\", hex(dA))\n", + "print((\"Public key: (0x{:x}, 0x{:x})\".format(*Qa)))\n", + "\n", + "\n", + "print(\"\\n\\n=========================\")\n", + "\n", + "r = random.randint(0, 2**128)\n", + "\n", + "rG = scalar_mult(r,curve.g)\n", + "S = scalar_mult(r,Qa)\n", + "\n", + "print(\"Random value: \" , r)\n", + "print(\"rG: \" , rG)\n", + "\n", + "print(\"\\n\\n======Symmetric key========\")\n", + "\n", + "print(\"Encryption key:\",S[0],str(S[0]))\n", + "password='hello'\n", + "\n", + "key = hashlib.sha256(str(S[0]).encode()).digest()\n", + "\n", + "message = Padding.appendPadding(message,blocksize=Padding.AES_blocksize,mode=0)\n", + "\n", + "ciphertext = encrypt(message.encode(),key,AES.MODE_ECB)\n", + "\n", + "\n", + "\n", + "\n", + "print(\"Encrypted:\\t\",binascii.hexlify(ciphertext))\n", + "\n", + "\n", + "Snew = scalar_mult(dA,rG)\n", + "key = hashlib.sha256(str(Snew[0]).encode()).digest()\n", + "\n", + "text = decrypt(ciphertext,key,AES.MODE_ECB)\n", + "\n", + "\n", + "print(\"Decrypted:\\t\",Padding.removePadding(text.decode(),mode=0))" + ] + } + ], + "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.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/09_public_key_sign.ipynb b/z_jupyter/09_public_key_sign.ipynb new file mode 100644 index 0000000..ad7dd25 --- /dev/null +++ b/z_jupyter/09_public_key_sign.ipynb @@ -0,0 +1,347 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7782a35", + "metadata": {}, + "source": [ + "\n", + "# Public Key Signatures\n", + "With a public key signature, we use the private key to sign a hash of the message, and then prove the signature with the public key. In this case, Bob signs a hash of the message with his private key, and then Alice will check this with his public key:\n", + "\n", + "\n", + "\n", + "## RSA Signatures\n", + "In the following, we create an RSA signature, using the private key, and then check with the public key:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "8fcac21d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Message=Hello\n", + "p=1015811308611355393\n", + "q=755523213775410319\n", + "\n", + "d=589399628769593821059839430475148801\n", + "e=65537\n", + "N=767469024471456365658376241308500367\n", + "\n", + "Private key (d,n)\n", + "Public key (e,n)\n", + "\n", + "cipher=302667991570245321224673969207643936\n", + "decipher=b'Hello'\n" + ] + } + ], + "source": [ + "# https://asecuritysite.com/hazmat/hashnew7\n", + "from cryptography.hazmat.primitives.asymmetric import rsa\n", + "from cryptography.hazmat.primitives import serialization\n", + "from cryptography.hazmat.primitives.asymmetric import padding\n", + "from cryptography.hazmat.primitives import hashes\n", + "from cryptography.hazmat.primitives import exceptions\n", + "\n", + "import sys\n", + "import binascii\n", + "\n", + "size=512\n", + "message = b\"Hello world\"\n", + "\n", + "if (len(sys.argv)>1):\n", + "\tmessage=str(sys.argv[1]).encode()\n", + "if (len(sys.argv)>2):\n", + "\tsize=int(sys.argv[2])\n", + "\n", + "print(\"Message: \",message)\n", + "print(\"Key size: \",size)\n", + "try:\n", + " private_key = rsa.generate_private_key(public_exponent=65537,key_size=size)\n", + "\n", + "\n", + " pub = private_key.public_key()\n", + "\n", + "\n", + "\n", + " signature = private_key.sign(message,padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),hashes.SHA256())\n", + "\n", + " print (\"\\nSignature: \",binascii.b2a_hex(signature).decode())\n", + "\n", + " try:\n", + " rtn=pub.verify(signature,message,padding.PSS(mgf=padding.MGF1(hashes.SHA256()),salt_length=padding.PSS.MAX_LENGTH),hashes.SHA256())\n", + " except exceptions.InvalidSignature:\n", + " print(\"A bad signature failed\")\n", + " else:\n", + " print(\"Good signature verified\")\n", + "\n", + "\n", + " try:\n", + " pub.verify(signature,b\"test\",padding.PSS(mgf=padding.MGF1(hashes.SHA256()),salt_length=padding.PSS.MAX_LENGTH),hashes.SHA256())\n", + " except exceptions.InvalidSignature:\n", + " print(\"A bad signature failed\")\n", + " else:\n", + " print(\"Bad signature verified\")\n", + "\n", + "\n", + "\n", + " print (\"\\nVerified: \",rtn)\n", + " print(\"\\n=== Private Key PEM format ===\")\n", + " pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", + "\n", + "\n", + " print (\"\\nPrivate key (PEM):\\n\",pem.decode())\n", + "\n", + "\n", + " pem = pub.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo).decode(\"utf-8\")\n", + "\n", + "\n", + "\n", + " print(\"\\n=== Public Key format ===\")\n", + " print (\"Public key (PEM):\\n\",pem)\n", + "\n", + "\n", + "except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "788ce969", + "metadata": {}, + "source": [ + "\n", + "> Implement the signature with SHA-1 hashing, and verify the operation/\n", + "\n", + "> Change the key size of 2,048 bits, and verify the operation." + ] + }, + { + "cell_type": "markdown", + "id": "0a96db97", + "metadata": {}, + "source": [ + "## ECDSA\n", + "ECDSA has been around for over two decades and was first proposed in [1]. The ECDSA method significantly improved the performance of signing messages than the RSA-based DSA method. Its usage of elliptic curve methods speeded up the whole process and supported much smaller key sizes. Its crown and glory were being selected by Satoshi Nakamoto for his Bitcoin protocol, and then its adoption into Ethereum. \n", + "\n", + "[1] Johnson, D., Menezes, A., & Vanstone, S. (2001). The elliptic curve digital signature algorithm (ECDSA). International journal of information security, 1(1), 36–63 [here].\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d7657bf", + "metadata": {}, + "outputs": [], + "source": [ + "# https://asecuritysite.com/hazmat/hashnew10\n", + "from cryptography.hazmat.primitives.asymmetric import ec\n", + "from cryptography.hazmat.primitives import serialization\n", + "from cryptography.hazmat.primitives import hashes\n", + "from cryptography import exceptions\n", + "import binascii\n", + "import sys\n", + "\n", + "private_key = ec.generate_private_key(ec.SECP384R1())\n", + "\n", + "data = b\"test\"\n", + "\n", + "if (len(sys.argv)>2):\n", + "\ttype=int(sys.argv[2])\n", + "if (len(sys.argv)>1):\n", + "\tdata=str(sys.argv[1]).encode()\n", + "\n", + "\n", + "private_key = ec.generate_private_key(ec.SECP256R1())\n", + "\n", + "\n", + "private_vals = private_key.private_numbers()\n", + "no_bits=private_vals.private_value.bit_length()\n", + "\n", + "print (f\"Private key value: {private_vals.private_value}. Number of bits {no_bits}\")\n", + "\n", + "public_key = private_key.public_key()\n", + "pub=public_key.public_numbers()\n", + "print (\"Name of curve: \",pub.curve.name)\n", + "\n", + "print (\"Message: \",data.decode())\n", + "try:\n", + "\n", + " signature = private_key.sign(data,ec.ECDSA(hashes.SHA256()))\n", + " print (\"Good Signature: \",binascii.b2a_hex(signature).decode())\n", + " public_key.verify(signature, data, ec.ECDSA(hashes.SHA256()))\n", + "except exceptions.InvalidSignature:\n", + " print(\"A bad signature failed\")\n", + "else:\n", + " print(\"Good signature verified\")\n", + "\n", + "try:\n", + "\n", + " signature = private_key.sign(b\"bad message\",ec.ECDSA(hashes.SHA256()))\n", + " print (\"Bad Signature: \",binascii.b2a_hex(signature).decode())\n", + " public_key.verify(signature, data, ec.ECDSA(hashes.SHA256()))\n", + "except exceptions.InvalidSignature:\n", + " print(\"A bad signature failed\")\n", + "else:\n", + " print(\"Good signature verified\")\n", + "\n", + "\n", + "\n", + "pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", + "\n", + "der = private_key.private_bytes(encoding=serialization.Encoding.DER,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", + "\n", + "\n", + "\n", + "print (\"\\nPrivate key (PEM):\\n\",pem.decode())\n", + "print (\"Private key (DER):\\n\",binascii.b2a_hex(der))\n", + "\n", + "pem = public_key.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)\n", + "\n", + "der = public_key.public_bytes(encoding=serialization.Encoding.DER,format=serialization.PublicFormat.SubjectPublicKeyInfo)\n", + "\n", + "print (\"\\nPublic key (PEM):\\n\",pem.decode())\n", + "print (\"Public key (DER):\\n\",binascii.b2a_hex(der))" + ] + }, + { + "cell_type": "markdown", + "id": "3d288d40", + "metadata": {}, + "source": [ + "> Implement with the SECP521R1, and observe the differences in the keys sizes used.\n", + "\n", + "> Implement the code with the BrainpoolP256R1 curve, and verify its operation.\n", + "\n", + "> The bitcoin curve is secp256k1. Modify the program so that it uses secp256k1." + ] + }, + { + "cell_type": "markdown", + "id": "6d4d695a", + "metadata": {}, + "source": [ + "## EdDSA\n", + "\n", + "With Ed25519 we use a private key to sign data, and then the public key can prove it. We use Curve 25519 for the generation of the public key and for the signing process. Curve 25519 uses a Montgomery curve of the form of y2=x3+ax2+x(modp). If we have a point P, we then find a point nP - where n is the number of times we add P. Curve 25519 takes the form of y^2=x^3+486662x^2+x(modp) and a base point at x=9. This gives a base point (G), and where we find the point nG for a given n value, and where n is the private key value, and nG is the public key point. Normally in Curve 25519 we only focus on the x-point, and do not need the (x,y) value.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ada57ad3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Private key: 0x8429f9e6136fcfa883130853defd319d931e25207749de92b5e12a80c8e62619\n", + "Public key: (0xa4581b11e2aa0249274374216d8fb965517e45e6bcc337f3fc679c2bfd84dce7, 0x4716b0eaf5f748af3921aac8455e623b96b590453f313b4d200419a801c11267)\n", + "\n", + "\n", + "=========================\n", + "Random value: 23353093005829895849666617279986636640\n", + "rG: (38430190155405327152400342621384391216588051326127513706497722898874838651319, 32551409443475257236910193737311399537798158570670164100113247023590451377060)\n", + "\n", + "\n", + "======Symmetric key========\n", + "Encryption key: 34425016396310312658614745921144437662843062008454472115656949568073393878924 34425016396310312658614745921144437662843062008454472115656949568073393878924\n", + "Encrypted:\t b'02c9b6ed4ffcf2bdf4f3118ecc232326'\n", + "Decrypted:\t Hello\n" + ] + } + ], + "source": [ + "# https://asecuritysite.com/hazmat/hashnew11\n", + "from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey\n", + "from cryptography import exceptions\n", + "from cryptography.hazmat.primitives import serialization\n", + "import binascii\n", + "import sys\n", + "\n", + "data=b\"Test\"\n", + "\n", + "if (len(sys.argv)>1):\n", + "\tdata=str(sys.argv[1]).encode()\n", + "\n", + "private_key = Ed25519PrivateKey.generate()\n", + "public_key = private_key.public_key()\n", + "\n", + "\n", + "public_key = private_key.public_key()\n", + "\n", + "print (\"Message: \",data.decode())\n", + "try:\n", + "\n", + " signature = private_key.sign(data)\n", + " print (\"Good Signature: \",binascii.b2a_hex(signature).decode())\n", + " public_key.verify(signature, data)\n", + "except exceptions.InvalidSignature:\n", + " print(\"A bad signature failed\")\n", + "else:\n", + " print(\"Good signature verified\")\n", + "\n", + "try:\n", + "\n", + " signature = private_key.sign(b\"Bad data\")\n", + " print (\"Bad Signature: \",binascii.b2a_hex(signature).decode())\n", + " public_key.verify(signature, data)\n", + "except exceptions.InvalidSignature:\n", + " print(\"A bad signature failed\")\n", + "else:\n", + " print(\"Good signature verified\")\n", + "\n", + "\n", + "\n", + "pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", + "\n", + "der = private_key.private_bytes(encoding=serialization.Encoding.DER,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", + "\n", + "\n", + "\n", + "print (\"\\nPrivate key (PEM):\\n\",pem.decode())\n", + "print (\"Private key (DER):\\n\",binascii.b2a_hex(der))\n", + "\n", + "pem = public_key.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)\n", + "\n", + "der = public_key.public_bytes(encoding=serialization.Encoding.DER,format=serialization.PublicFormat.SubjectPublicKeyInfo)\n", + "\n", + "print (\"\\nPublic key (PEM):\\n\",pem.decode())\n", + "print (\"Public key (DER):\\n\",binascii.b2a_hex(der))" + ] + } + ], + "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.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/10_digital_certs.ipynb b/z_jupyter/10_digital_certs.ipynb new file mode 100644 index 0000000..7ac0f66 --- /dev/null +++ b/z_jupyter/10_digital_certs.ipynb @@ -0,0 +1,532 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7782a35", + "metadata": {}, + "source": [ + "# Trust and Digital Certificates\n", + "\n", + "Objective: Digital certificates are used to define a trust infrastructure within PKI (Public Key Infrastructure). A certificate can hold a key pair, while a distributable certificate will only contain the public key. In this lab we will read-in digital certificates and analyse them. A lab demo is [here](https://youtu.be/-uNQFv0GTZc).\n", + "\n", + "\n", + "## A\tIntroduction\n", + "\n", + "### A.1\t\n", + "From this web link (Digital Certificate): \n", + "\n", + "```\n", + "http://asecuritysite.com/digitalcert/digitalcert\n", + "```\n", + "\n", + "Open up Certificate 1 and identify the following:\t\n", + "\n", + "* Serial number:\n", + "* Effective date:\n", + "* Name:\n", + "* Issuer:\n", + "* What is CN used for:\n", + "* What is OU used for:\n", + "* What is O used for:\n", + "* What is L used for:\n", + "\n", + "The public key should be \"30818902 ... 203010001\". We can see this starts with \"30\", and which represents a DER format. We can examine by pasting into this DER analyser [here](https://asecuritysite.com/digitalcert/sigs3). Using this, what do the two values represent, and what type of public key is it?\n", + "\n", + "\n", + "### A.2\t\n", + "Now open-up the ZIP file for the certificates , and view the CER file.\n", + "\n", + "* What other information can you gain from the certificate:\n", + "* What is the size of the public key:\n", + "* Which hashing method has been used:\n", + "* Is the certificate trusted on your system: [Yes][No]\n", + "\n", + "### A.3\t\n", + "Make a connection to the www.live.com Web site:\n", + "\n", + "```\n", + "openssl s_client -connect www.live.com:443\n", + "```\n", + "\n", + "Can you identity the certificate chain?\n", + "\n", + "What is the subject on the certificate?\n", + "\n", + "Who is the issuer on the certificate?\n", + "\n", + "### A.4\t\n", + "\n", + "Google moved in July 2018 to mark sites as being insecure if they did not have a match between their digital certificate and the site. First open a browser and see if you can access testfire.net (you can try both https and http for the connection). \n", + "\n", + "* Run a scan from [SSLLabs](https://www.ssllabs.com/) on testfire.net. What do you observe from the result?\n", + "* What is the SSLLabs rating on this site? Is it \"A\", \"B\", \"C\", \"D\", \"E\" or \"F\"?\n", + "* What does a “T” rating identify?\n", + "* Can you locate another \"T\" rated site?\n", + "\n", + "### A.5\t\n", + "Which the certificates in A.2, for Example 2 to Example 6. Complete the following table:\n", + "\n", + "|Cert|\tOrganisation (Issued to)|\tDate range when valid|\tSize of public key|\tIssuer|\tRoot CA\tHash method|\tIs it trusted?|\n", + "| ----| --- | ---| ---| ---| ---| ---| \n", + "| 2 | ||||||\t\t\t\t\t\t\t\n", + "|3 | ||||||\t\t\t\t\t\t\t\n", + "|4 | ||||||\t\t\t\t\t\t\t\n", + "|5 | ||||||\t\t\t\t\t\t\t\n", + "|6 | ||||||\t\t\t\t\t\t\t\n", + "\n", + "### A.6\t\n", + "Now download the DER files from:\n", + "\n", + "Web link (Digital Certificate): http://asecuritysite.com/der.zip\n", + "\n", + "Now use openssl to read the certificates:\n", + "\n", + "```\n", + "openssl x509 -inform der -in [certname] -noout -text\n", + "```\n", + "\n", + "### A.7\n", + "Examine the following certificate, and identify its weakness.\n", + "\n", + "![alt text](http://asecuritysite.com/public/certp.png)\n", + "\n", + "There are two OID numbers related to the public key encryption method and the siganture method. Use this page [here](https://oidref.com/) to identify them.\n", + "\n", + "## B\tCreating certificates\n", + "\n", + "Now we will create our own self-signed certificates.\n", + "\n", + "\n", + "### B.1\t\n", + "Create your own certificate from:\n", + "\n", + "Web link (Create Certificate): [here](http://asecuritysite.com/digitalcert/createcert)\n", + "\n", + "Add in your own details.\t\n", + "\n", + "* View the certificate, and verify some of the details on the certificate.\n", + "* Can you view the DER file?\n", + "\n", + "We have a root certificate authority of My Global Corp, which is based in Washington, US, and the administrator is admin@myglobalcorp.com and we are going to issue a certificate to My Little Corp, which is based in Glasgow, UK, and the administrator is admin@mylittlecorp.com.\n", + "\n", + "\n", + "### B.2\t\n", + "Create your RSA key pair with:\n", + "\n", + "```\n", + "openssl genrsa -out ca.key 2048\n", + "```\n", + "\n", + "Next create a self-signed root CA certificate ca.crt for My Global Corp:\n", + "\n", + "```\n", + "openssl req -new -x509 -days 1826 -key ca.key -out ca.crt\n", + "```\n", + "\n", + "\n", + "\t\n", + "* How many years will the certificate be valid for?\n", + "* Which details have you entered:\n", + "\n", + "### B.3\t\n", + "Next go to Places, and from your Home folder, open up ca.crt and view the details of the certificate.\n", + "\n", + "* Which Key Algorithm has been used:\n", + "* Which hashing methods have been used:\n", + "* When does the certificate expire:\n", + "* Who is it verified by:\n", + "* Who has it been issued to:\n", + "\n", + "### B.4\t\n", + "Next we will create a subordinate CA (My Little Corp), and which will be used for the signing of the certificate. First, generate the key:\n", + "\n", + "```\n", + "openssl genrsa -out ia.key 2048\n", + "```\n", + "\n", + "Next we will request a certificate for our newly created subordinate CA:\n", + "\n", + "```\n", + "openssl req -new -key ia.key -out ia.csr\n", + "```\n", + "\n", + "We can then create a certificate from the subordinate CA certificate and signed by the root CA.\n", + "\n", + "```\n", + "openssl x509 -req -days 730 -in ia.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out ia.crt\n", + "```\n", + "\n", + "* View the newly created certificate.\n", + "* When does it expire:\n", + "* Who is the subject of the certificate:\n", + "* Which is their country:\n", + "* Who signed the certificate:\n", + "* Which is their country:\n", + "* What is the serial number of the certificate:\n", + "* Check the serial number for the root certificate. What is its serial number:\n", + "\n", + "### B.5\t\n", + "If we want to use this certificate to digitally sign files and verify the signatures, we need to convert it to a PKCS12 file:\n", + "\n", + "```\n", + "openssl pkcs12 -export -out ia.p12 -inkey ia.key -in ia.crt -chain -CAfile ca.crt\n", + "```\n", + "\n", + "* Can you view ia.p12 in a text edit?\n", + "\n", + "\n", + "\n", + "### B.6\t\n", + "The crt format is in encoded in binary. If we want to export to a Base64 format, we can use DER:\n", + "\n", + "```\n", + "openssl x509 -inform pem -outform pem -in ca.crt -out ca.cer\n", + "```\n", + "\n", + "and for My Little Corp:\n", + "```\n", + "openssl x509 -inform pem -outform pem -in ia.crt -out ia.cer\n", + "```\n", + "\n", + "* View each of the output files in a text editor (ca.cer and then ia.cer). What can you observe from the format:\n", + "* Which are the standard headers and footers on the file used:\n", + "\n", + "\n", + "\n", + "### B.7\t\n", + "Enter and run the following program, and verify its operation:\n", + "\n", + "```python\n", + "import OpenSSL.crypto\n", + "from OpenSSL.crypto import load_certificate_request, FILETYPE_PEM\n", + "\n", + "csr = '''-----BEGIN NEW CERTIFICATE REQUEST-----\n", + "MIICyTCCAbECAQAwajELMAkGA1UEBhMCVUsxDTALBgNVBAgTBE5vbmUxEjAQBgNV\n", + "BAcTCUVkaW5idXJnaDEXMBUGA1UEChMOTXkgTGl0dGxlIENvcnAxDDAKBgNVBAsT\n", + "A01MQzERMA8GA1UEAxMITUxDLm5vbmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\n", + "ggEKAoIBAQCuQE68qgssJ210wGxfKjCX3PG/RgSb5VpAp2rzavx71M9Bhg9kUORE\n", + "OP7BQC3E6DGu+xba3NdnhrHAFNa+hH9dnTZrlxb98aM5q9+TUm76V1toIseOMDdU\n", + "UE9IpxXoFvD6b0inbFZnbrjFj3XUUzIIqvvizw4rIOxzgbWqZ5+F7YpP8d59eWW0\n", + "6iXzJKoeE/+Gw7Slsdr1+QQAUaX05MHTweMYbZEHir2M8f1RA4o81zEd2tWCK85F\n", + "6VS/EkCzUG1cqDBQQ7D2S9MWN8Zk2P7CS8/yZx7uRTmT1t3UWKLUyIN0TU3IjCeY\n", + "t53P6C+9DT6UD0fDFZRBCmPOH+qb6/YBAgMBAAGgGjAYBgkqhkiG9w0BCQcxCxMJ\n", + "UXdlcnR5MTIzMA0GCSqGSIb3DQEBBQUAA4IBAQCqpXjmaQf2/o/xbNZG5ggAV8yV\n", + "d6rSabnov5zIkcit9NQXsPJEi84u7CbcriYqY5h7XlMWjv476mAGbgAVZB2ZhIlp\n", + "qLal+lx9xwhFbuLHNRxZcUMM0g9KQZaZTkAQdlDVU/vPzRjq+EHGoPfG7R9QKGD0\n", + "k1b4DqOvInWLOs+yuWT7YYtWdr2TNKPpcBqbzCYzrWL6UaUN7LYFpNn4BbqXRgVw\n", + "iMAnUh9fvLMe7oreYfTaevXT/506Sj9WvQFXTcLtRhs+M30q22/wUK0ZZ8APjpwf\n", + "rQMegvzXXEIO3xEGrBi5/wXJxsawRLcM3ZSGPu/Ws950oM5Ahn8K8HBdKubQ\n", + "-----END NEW CERTIFICATE REQUEST-----'''\n", + "\n", + "req = load_certificate_request(FILETYPE_PEM, csr)\n", + "key = req.get_pubkey()\n", + "key_type = 'RSA' if key.type() == OpenSSL.crypto.TYPE_RSA else 'DSA'\n", + "subject = req.get_subject()\n", + "components = dict(subject.get_components())\n", + "print (\"Key algorithm:\", key_type)\n", + "print (\"Key size:\", key.bits())\n", + "print (\"Common name:\", components['CN'])\n", + "print (\"Organisation:\", components['O'])\n", + "print (\"Organisational unit\", components['OU'])\n", + "print (\"City/locality:\", components['L'])\n", + "print (\"State/province:\", components['ST'])\n", + "print (\"Country:\", components['C'])\n", + "```\n", + "\n", + "Web link (CSR): [here](https://asecuritysite.com/digitalcert/csr)\n", + "\n", + "### D.8\t\n", + "Now check the signing on these certificate requests:\n", + "```\n", + "-----BEGIN NEW CERTIFICATE REQUEST-----\n", + "MIICyTCCAbECAQAwajELMAkGA1UEBhMCVUsxDTALBgNVBAgTBE5vbmUxEjAQBgNV\n", + "BAcTCUVkaW5idXJnaDEXMBUGA1UEChMOTXkgTGl0dGxlIENvcnAxDDAKBgNVBAsT\n", + "A01MQzERMA8GA1UEAxMITUxDLm5vbmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\n", + "ggEKAoIBAQCuQE68qgssJ210wGxfKjCX3PG/RgSb5VpAp2rzavx71M9Bhg9kUORE\n", + "OP7BQC3E6DGu+xba3NdnhrHAFNa+hH9dnTZrlxb98aM5q9+TUm76V1toIseOMDdU\n", + "UE9IpxXoFvD6b0inbFZnbrjFj3XUUzIIqvvizw4rIOxzgbWqZ5+F7YpP8d59eWW0\n", + "6iXzJKoeE/+Gw7Slsdr1+QQAUaX05MHTweMYbZEHir2M8f1RA4o81zEd2tWCK85F\n", + "6VS/EkCzUG1cqDBQQ7D2S9MWN8Zk2P7CS8/yZx7uRTmT1t3UWKLUyIN0TU3IjCeY\n", + "t53P6C+9DT6UD0fDFZRBCmPOH+qb6/YBAgMBAAGgGjAYBgkqhkiG9w0BCQcxCxMJ\n", + "UXdlcnR5MTIzMA0GCSqGSIb3DQEBBQUAA4IBAQCqpXjmaQf2/o/xbNZG5ggAV8yV\n", + "d6rSabnov5zIkcit9NQXsPJEi84u7CbcriYqY5h7XlMWjv476mAGbgAVZB2ZhIlp\n", + "qLal+lx9xwhFbuLHNRxZcUMM0g9KQZaZTkAQdlDVU/vPzRjq+EHGoPfG7R9QKGD0\n", + "k1b4DqOvInWLOs+yuWT7YYtWdr2TNKPpcBqbzCYzrWL6UaUN7LYFpNn4BbqXRgVw\n", + "iMAnUh9fvLMe7oreYfTaevXT/506Sj9WvQFXTcLtRhs+M30q22/wUK0ZZ8APjpwf\n", + "rQMegvzXXEIO3xEGrBi5/wXJxsawRLcM3ZSGPu/Ws950oM5Ahn8K8HBdKubQ\n", + "-----END NEW CERTIFICATE REQUEST-----\n", + "```\n", + "and:\n", + "\n", + "```\n", + "-----BEGIN NEW CERTIFICATE REQUEST-----\n", + "MIIDPzCCAqgCAQAwZDELMAkGA1UEBhMCQ04xCzAJBgNVBAgTAmJqMQswCQYDVQQH\n", + "EwJiajERMA8GA1UEChMIbXhjei5uZXQxETAPBgNVBAsTCG14Y3oubmV0MRUwEwYD\n", + "VQQDEwx3d3cubXhjei5uZXQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMQ7\n", + "an4v6pHRusBA0prMWXMWJCXY1AO1H0X8pvZj96T5GWg++JPCQE9guPgGwlD02U0B\n", + "NDoEABeD1fwyKZ+JV5UFiOeSjO5sWrzIupdMI7hf34UaPNxHo6r4bLYEykw/Rnmb\n", + "GKnNcD4QlPkypE+mLR4p0bnHZhe3lOlNtgd6NpXbAgMBAAGgggGZMBoGCisGAQQB\n", + "gjcNAgMxDBYKNS4yLjM3OTAuMjB7BgorBgEEAYI3AgEOMW0wazAOBgNVHQ8BAf8E\n", + "BAMCBPAwRAYJKoZIhvcNAQkPBDcwNTAOBggqhkiG9w0DAgICAIAwDgYIKoZIhvcN\n", + "AwQCAgCAMAcGBSsOAwIHMAoGCCqGSIb3DQMHMBMGA1UdJQQMMAoGCCsGAQUFBwMB\n", + "MIH9BgorBgEEAYI3DQICMYHuMIHrAgEBHloATQBpAGMAcgBvAHMAbwBmAHQAIABS\n", + "AFMAQQAgAFMAQwBoAGEAbgBuAGUAbAAgAEMAcgB5AHAAdABvAGcAcgBhAHAAaABp\n", + "AGMAIABQAHIAbwB2AGkAZABlAHIDgYkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n", + "AAAAAAAAAAAAAAAAAAAAADANBgkqhkiG9w0BAQUFAAOBgQBIKHVhHb9FZdVLV4VZ\n", + "9DK4aBSuYY//jlIpvsfMIdHXfAsuan7w7PH87asp1wdb6lD9snvLZix1UGK7VQg6\n", + "wUFYNlMqJh1m7ITVvzhjdnx7EzCKkBXSxEom4mwbvSNvzqOKAWsDE0gvHQ9aCSby\n", + "NFBQQMoW94LqrG/kuIQtjwVdZA==\n", + "-----END NEW CERTIFICATE REQUEST-----\n", + "```\n", + "and:\n", + "\n", + "```\n", + "-----BEGIN CERTIFICATE REQUEST-----\n", + "MIIByjCCATMCAQAwgYkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh\n", + "MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgSW5jMR8w\n", + "HQYDVQQLExZJbmZvcm1hdGlvbiBUZWNobm9sb2d5MRcwFQYDVQQDEw53d3cuZ29v\n", + "Z2xlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEApZtYJCHJ4VpVXHfV\n", + "IlstQTlO4qC03hjX+ZkPyvdYd1Q4+qbAeTwXmCUKYHThVRd5aXSqlPzyIBwieMZr\n", + "WFlRQddZ1IzXAlVRDWwAo60KecqeAXnnUK+5fXoTI/UgWshre8tJ+x/TMHaQKR/J\n", + "cIWPhqaQhsJuzZbvAdGA80BLxdMCAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4GBAIhl\n", + "4PvFq+e7ipARgI5ZM+GZx6mpCz44DTo0JkwfRDf+BtrsaC0q68eTf2XhYOsq4fkH\n", + "Q0uA0aVog3f5iJxCa3Hp5gxbJQ6zV6kJ0TEsuaaOhEko9sdpCoPOnRBm2i/XRD2D\n", + "6iNh8f8z0ShGsFqjDgFHyF3o+lUyj+UC6H1QW7bn\n", + "-----END CERTIFICATE REQUEST-----\n", + "```\n", + "\n", + "* What are the details on the requests?\n", + "\n", + "## C\tElliptic Curve Key Creation\n", + "Elliptic curve key pairs are increasing used within corporate Web sites. \n", + "\n", + "In Openssl we can view the curves with the ecparam option:\n", + "\n", + "```\n", + "openssl ecparam -list_curves\n", + "```\n", + "\n", + "* Outline some of the curve names:\n", + "* By performing an Internet search, which are the most popular curves (and where are they used)?\n", + "\n", + "\n", + "We can create our elliptic parameter file with:\n", + "```\n", + "openssl ecparam -name secp256k1 -out secp256k1.pem\n", + "```\n", + "\n", + "Now view the details with:\n", + "\n", + "```\n", + "openssl ecparam -in secp256k1.pem -text -param_enc explicit -noout\n", + "```\n", + "\n", + "* What are the details of the key?\n", + "\n", + "\n", + "Now we can create our key pair:\n", + "\n", + "```\n", + "openssl ecparam -in secp256k1.pem -genkey -noout -out mykey.pem\n", + "```\n", + "\n", + "Now we will encrypt your key pair (and add a password), and convert it into a format which is ready to be converted into a digital certificate:\n", + "\n", + "```\n", + "openssl ec -aes-128-cbc -in mykey.pem -out enckey.pem\n", + "```\n", + "\n", + "Finally we will convert into a DER format, so that we can import the keys into a system:\n", + "\n", + "```\n", + "openssl ec -in enckey.pem -outform DER -out enckey.der\n", + "```\n", + "\n", + "* Examine each of the files created and outline what they contain:\n", + "* Now pick another elliptic curve type and perform the same operations as above. Which type did you use?\n", + "* Outline the commands used:\n", + "* If you want to create a non-encrypted version (PFX), which command would you use:\n", + "\n", + "\n", + "\n", + "Go to www.cloudflare.com and examine the digital certificate on the site.\n", + "\n", + "* What is the public key method used?\n", + "* What is the size of the public key?\n", + "* What is the curve type used?\n", + "\n", + "## E\tPFX files\n", + "We have a root certificate authority of My Global Corp, which is based in Washington, US, and the administrator is admin@myglobalcorp.com and we are going to issue a certificate to My Little Corp, which is based in Glasgow, UK, and the administrator is admin@mylittlecorp.com.\n", + "\n", + "\n", + "### E.1\tWe will now view some PFX certificate files, and which are protected with a password:\n", + "\n", + "Web link (Digital Certificates): [here](http://asecuritysite.com/digitalcert/digitalcert2)\n", + "\n", + "* For Certificate 1, can you open it in the Web browser with an incorrect password:\n", + "* Now enter “apples” as a password, and record some of the key details of the certificate:\n", + "* Now repeat for Certificate 2:\n", + "\n", + "\n", + "### E.2\t\n", + "Now with the PFX files (contained in the ZIP files from the Web site), try and import them onto your computer. Try to enter an incorrect password first and observe the message.\n", + "\n", + "\n", + "* Was the import successful?\n", + "* If successful, outline some of the details of the certificates:\n", + "\n", + "\n", + "\n", + "\n", + "## F\tCracking Certificates\n", + "Digital certificates are often protected with a simple password. With this we can use a Python program to try various passwords on the certificate, and if it does not create an exception, then we have found the required password. First download the following pfx files:\n", + "\n", + "[here](https://asecuritysite.com/cert_crack.zip)\n", + "\n", + "Now for bill01.pfx and fred.pfx, crack the password with the following code:\n", + "\n", + "```python\n", + "import OpenSSL \n", + "from cryptography import x509\n", + "from cryptography.hazmat.backends import default_backend\n", + "\n", + "str=\"fred.pfx\"\n", + "\n", + "passwords=[\"ankle\",\"battery\",\"password\",\"bill\",\"apple\",\"apples\",\"orange\"]\n", + "\n", + "for password in passwords:\n", + "\ttry:\n", + "\t\tpfx = open(str, 'rb').read()\n", + "\t\t\n", + "\t\tp12 = OpenSSL.crypto.load_pkcs12(pfx, password.encode())\n", + "\t\tprint (\"Found: \",password)\n", + "\n", + "\n", + "\t\tprivkey=OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, p12.get_privatekey())\n", + "\n", + "\t\tcert=OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, p12.get_certificate())\n", + "\n", + "\t\tcert = x509.load_pem_x509_certificate(cert, default_backend())\n", + "\n", + "\n", + "\t\tprint (\" Issuer: \",cert.issuer)\n", + "\t\tprint (\" Subect: \",cert.subject)\n", + "\t\tprint (\" Serial number: \",cert.serial_number)\n", + "\t\tprint (\" Hash: \",cert.signature_hash_algorithm.name)\n", + "\t\tprint (privkey)\n", + "\n", + "\n", + "\n", + "\texcept:\n", + "\n", + "\t\tprint (\"Not working: \",password)\n", + "```\n", + "\n", + "* What is the password?\n", + "* The files bill01.pfx, bill02.pfx ... bill18.pfx have a password which are **fruits**. Can you determine the fruits used?\n", + "* The files country01.pfx, country02.pfx ... country06.pfx have a password which are **countries** of the world. Can you determine the countries used?\n", + "\n", + "## G\tSetting up a certificate on a Web site\n", + "### G.1\t\n", + "Now we will enable HTTPs on an Apache Web Server, and install a digital certificate. Execute the following commands:\n", + "```\n", + "sudo a2enmod ssl\n", + "service apache2 restart\n", + "openssl genrsa -out ca.key 2048\n", + "sudo openssl req -nodes -new -key ca.key -out ca.csr\n", + "sudo openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt\n", + "sudo mkdir /etc/apache2/ssl\n", + "sudo cp ca.crt ca.key ca.csr /etc/apache2/ssl/\n", + "sudo nano /etc/apache2/sites-enabled/000-default.conf\n", + "sudo /etc/init.d/apache2 restart\n", + "```\n", + "\n", + "HTTPs should now be enabled with a self-signed certificate. If you try https://localhost, you will have to add an exception to view the page, as we are using a self-signed certificate:\n", + "\n", + " \n", + "## Additional lab question\n", + "The ECDSA signature is used in Bitcoin and Ethereum. Using the code [here](https://asecuritysite.com/ecdsa/ecdsa3):\n", + "\n", + "```python\n", + "import sys\n", + "import random\n", + "import hashlib\n", + "import libnum\n", + "\n", + "from secp256k1 import curve,scalar_mult,point_add\n", + "\n", + "msg=\"Hello\"\n", + "\n", + "if (len(sys.argv)>1):\n", + " msg=(sys.argv[1])\n", + "\n", + "# Alice's key pair (dA,QA)\n", + "dA = random.randint(0, curve.n-1)\n", + "QA = scalar_mult(dA,curve.g)\n", + "\n", + "h=int(hashlib.sha256(msg.encode()).hexdigest(),16)\n", + "\n", + "k = random.randint(0, curve.n-1)\n", + "\n", + "rpoint = scalar_mult(k,curve.g)\n", + "\n", + "r = rpoint[0] % curve.n\n", + "\n", + "# Bob takes m and (r,s) and checks\n", + "inv_k = libnum.invmod(k,curve.n)\n", + "\n", + "s = (inv_k*(h+r*dA)) % curve.n\n", + "\n", + "print (f\"Msg: {msg}\\n\\nAlice's private key={dA}\\nAlice's public key={QA}\\nk= {k}\\n\\nr={r}\\ns={s}\")\n", + "\n", + "# To check signature\n", + "\n", + "inv_s = libnum.invmod(s,curve.n)\n", + "c = inv_s\n", + "u1=(h*c) % curve.n\n", + "u2=(r*c) % curve.n\n", + "P = point_add(scalar_mult(u1,curve.g), scalar_mult(u2,QA))\n", + "\n", + "res = P[0] % curve.n\n", + "print (f\"\\nResult r={res}\")\n", + "\n", + "if (res==r):\n", + "\tprint(\"Signature matches!\")\n", + "```\n", + "\n", + "Run the code and answer the following questions:\n", + "\n", + "* How is the private key created?\n", + "* How is the public key created?\n", + "* Can you identify the nonce value used in the signature?\n", + "* What are the two output values of the signature?\n", + "* Which key (public or private key) is used to verify the signature?\n", + "* Which key (public or private key) is used to verify the signature?\n", + "\n" + ] + } + ], + "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.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/11_tunnelling.ipynb b/z_jupyter/11_tunnelling.ipynb new file mode 100644 index 0000000..5a12e1d --- /dev/null +++ b/z_jupyter/11_tunnelling.ipynb @@ -0,0 +1,485 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7782a35", + "metadata": {}, + "source": [ + "\n", + "# Tunnelling and Web Security\n", + "Objective: In this lab we will investigate the usage of SSL/TLS and VPN tunnftels.\n", + "\n", + "\n", + "YouTube Demo: https://youtu.be/ASCDJq4Wy9Y \n", + "\n", + "## A\tWeb cryptography assessment\n", + "The Ssllabs tool (https://ssllabs.com) can be used to assess the security of the cryptography used on a Web site. Pick three of your favouriate sites to scan. Now perform a test on them, and determine:\n", + "\n", + "\n", + "* What grade does the site get?\n", + "* The digital certificate key size and type?\n", + "* Does the name of the site match the name on the server?\t\t\t\n", + "* Who is the signer of the digital certificate?\n", + "* The expiry date on the digital certificate?\n", + "* What is the hashing method on the certificate?\n", + "* If it uses RSA keys, what is the e value that is used in the encryption (Me mod N)?\t\t\t\n", + "* Determine a weak cipher suite used and example why it might be weak?\n", + "* Is SSL v2 supported?\n", + "* If SSL v2 was supported, what problems might there be with the site (this will require some research)?\t\t\t\n", + "* Outline the usage of TLS 1.0/1.1 and 1.2, and identify a problem if one of these TLS versions were not supported?\t\n", + "* Is the site vulnerable to Heartbleed?\n", + "* Is the site vulnerable to DROWN?\n", + "* Is the site vulnerable to BEAST?\n", + "* Is the site vulnerable to POODLE?\t\t\t\n", + "\n", + "Research questions:\n", + "\n", + "* What does TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 identify?\n", + "* If a site gets a ‘T’ grade, what is the problem?\n", + "* If the site was susceptible to Poodle, what is the vulnerability?\n", + "* Can you find a site which gets an \"A+\"? What features does a site need to get an \"A+\" grade?\n", + "\n", + "\n", + "## A.2\t\n", + "We will now create a Python program which calls up the SSLlabs assessment. First create a CSV file (sites.csv) with your sites in it. The format is Name of site, URL:\n", + "\n", + "```\n", + "web,site\n", + "Cloudflare,www.cloudflare.com\n", + "BBC,bbc.co.uk\n", + "```\n", + "\n", + "Next enter the following code and run it: \n", + "\n", + "```python\n", + "import requests\n", + "import time\n", + "import sys\n", + "import logging\n", + "\n", + "API = 'https://api.ssllabs.com/api/v2/'\n", + "\n", + "\n", + "def requestAPI(path, payload={}):\n", + " '''This is a helper method that takes the path to the relevant\n", + " API call and the user-defined payload and requests the\n", + " data/server test from Qualys SSL Labs.\n", + " Returns JSON formatted data'''\n", + "\n", + " url = API + path\n", + "\n", + " try:\n", + " response = requests.get(url, params=payload)\n", + " except requests.exception.RequestException:\n", + " logging.exception('Request failed.')\n", + " sys.exit(1)\n", + "\n", + " data = response.json()\n", + " return data\n", + "\n", + "\n", + "def resultsFromCache(host, publish='off', startNew='off', fromCache='on', all='done'):\n", + " path = 'analyze'\n", + " payload = {\n", + " 'host': host,\n", + " 'publish': publish,\n", + " 'startNew': startNew,\n", + " 'fromCache': fromCache,\n", + " 'all': all\n", + " }\n", + " data = requestAPI(path, payload)\n", + " return data\n", + "\n", + "\n", + "def newScan(host, publish='off', startNew='on', all='done', ignoreMismatch='on'):\n", + " path = 'analyze'\n", + " payload = {\n", + " 'host': host,\n", + " 'publish': publish,\n", + " 'startNew': startNew,\n", + " 'all': all,\n", + " 'ignoreMismatch': ignoreMismatch\n", + " }\n", + " results = requestAPI(path, payload)\n", + "\n", + " payload.pop('startNew')\n", + "\n", + " while results['status'] != 'READY' and results['status'] != 'ERROR':\n", + " time.sleep(30)\n", + " results = requestAPI(path, payload)\n", + "\n", + " return results\n", + "\n", + "\n", + "import csv\n", + "print (\"Scanning\")\n", + "with open('sites.csv') as csvfile:\n", + " reader = csv.DictReader(csvfile)\n", + " for row in reader:\n", + " url = row['site'].strip()\n", + " print (\"Scanning...\",url)\n", + " a = newScan(url)\n", + " with open(\"out3.txt\", \"a\") as myfile:\n", + " myfile.write(str(row['web'])+\"\\n\"+str(a)+\"\\n\\n\\n\")\n", + " print (row['web'])\n", + "``` \n", + "\n", + "Note that it will can take a few minutes to perform a single scan. By reading the out3.txt file, outline your findings.\n", + "\n", + "Here is the [Replit](https://replit.com/@billbuchanan/ssllab#main.py) site.\n", + "\n", + "\n", + "Site name:\t\t\t\t\n", + "Site rating:\n", + "Other significant details:\n", + "\n", + "\n", + "\n", + "\n", + "Site name:\t\t\t\t\n", + "Site rating:\n", + "Other significant details:\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "## B\tViewing details\n", + "\n", + "### B.1\t\n", + "On your VM instance (or your desktop), run Wireshark and capture traffic from your main network connection. Start a Web browser and go to:\n", + "```\n", + "https://google.com\n", + "```\n", + "\n", + "Stop Wireshark and identify some of your connection details:\n", + "\t\n", + "* Your IP address and TCP port:\n", + "* Google’s Web server IP address and TCP port:\n", + "* Which SSL/TLS version is used:\n", + "* By examining the Wireshark trace, which encryption method is used for the tunnel (hint: look in the ‘Server Hello’ response):\n", + "* By examining the Wireshark trace, which hashing method is used for the tunnel (hint: look in the ‘Server Hello’ response):\n", + "* By examining the Wireshark trace, what is the length of the encryption key (hint: look in the ‘Server Hello’ response):\n", + "* Using Firefox, and examining the connection details from the site (click on green padlock), can you verify the TLS version, the symmetric key encryption method, the handshaking method and the hashing method used within the tunnel? A sample is shown below.\n", + "\n", + "\n", + "### B.2\tRun Wireshark and capture traffic from your main network connection. Start a Web browser and go to:\n", + "\n", + "```\n", + "https://twitter.com\n", + "```\n", + "\n", + "Stop Wireshark and identify some of your connection details:\n", + "\n", + "* Your IP address and TCP port:\n", + "* Twitter’s Web server IP address and TCP port:\n", + "* Which SSL/TLS version is used:\n", + "* By examining the Wireshark trace, which encryption method is used for the tunnel (hint: look in the ‘Server Hello’ response):\n", + "* By examining the Wireshark trace, which hashing method is used for the tunnel (hint: look in the ‘Server Hello’ response):\n", + "* By examining the Wireshark trace, what is the length of the encryption key (hint: look in the ‘Server Hello’ response):\n", + "* Using Firefox, and examining the connection details from the site (click on green padlock), can you verify the TLS version, the symmetric key encryption method, the handshaking method and the hashing method used within the tunnel? A sample is shown below.\n", + "\n", + "\n", + "## C\tOpenSSL\n", + "\n", + "### C.1\t\n", + "On your VM instance (or your desktop), make a connection to the www.live.com Web site:\n", + "```\n", + "openssl s_client -connect www.live.com:443\n", + "```\n", + "\n", + "* Which SSL/TLS method has been used:\n", + "* Which method is used on the encryption key on the certificate, and what is the size of the public key?\n", + "* Which is the handshaking method that has been used to create the encryption key?\n", + "* Which TLS version is used for the tunnel?\n", + "* Which symmetric encryption method is used for the tunnel:\n", + "* Which hashing method is used for the tunnel:\n", + "* What is the length of the symmetric encryption key:\n", + "* Who has signed the certificate:\n", + "\n", + "\n", + "\n", + "## D\tExamining traces\n", + "\n", + "### D.1\tDownload the following file, and examine the trace with Wireshark:\n", + "```\n", + "http://asecuritysite.com/log/ssl.zip\n", + "```\n", + "\n", + "* Client IP address and TCP port:\n", + "* Web server IP address and TCP port:\n", + "* Determine one of the symmetric key encryption methods, the key exchange, and the hashing methods that the client wants to use (Hint: look at the ‘Client Hello’ packet):\n", + "* Which SSL/TLS method has been used:\n", + "* Which encryption method is used for the tunnel:\n", + "* Which hashing method is used for the tunnel:\n", + "* What is the length of the encryption key:\n", + "\n", + "### D.2\tDownload the following file, and examine the trace with Wireshark:\n", + "\n", + "```\n", + "http://asecuritysite.com/log/https.zip\n", + "```\n", + "\n", + "* Client IP address and TCP port:\n", + "* Web server IP address and TCP port:\n", + "* Determine one of the symmetric key encryption methods, the key exchange, and the hashing methods that the client wants to use (Hint: look at the ‘Client Hello’ packet):\n", + "* Which SSL/TLS method has been used:\n", + "* Which encryption method is used for the tunnel:\n", + "* Which hashing method is used for the tunnel:\n", + "* What is the length of the encryption key:\n", + "\n", + "### D.3\tDownload the following file, and examine the trace with Wireshark:\n", + "\n", + "```\n", + "http://asecuritysite.com/log/heart.zip\n", + "```\n", + "\n", + "* Client IP address and TCP port:\n", + "* Web server IP address and TCP port:\n", + "* Determine one of the symmetric key encryption methods, the key exchange, and the hashing methods that the client wants to use (Hint: look at the ‘Client Hello’ packet):\n", + "* Which SSL/TLS method has been used:\n", + "* Which encryption method is used for the tunnel:\n", + "* Which hashing method is used for the tunnel:\n", + "* What is the length of the encryption key:\n", + "\n", + "\n", + "### D.4\tDownload the following file, and examine the trace with Wireshark:\n", + "```\n", + "http://asecuritysite.com/log/ipsec.zip \n", + "```\n", + "\n", + "* Which is the IP address of the client and of the server:\n", + "* Which packet number identifies the start of the VPN connection (Hint: look for UDP Port 500):\n", + "* Determine one of the encryption and the hashing methods that the client wants to use:\n", + "* Now determine the encryption and hashing methods that are agreed in the ISAKMP:\n", + "\n", + "\n", + "### D.5\tDownload the following file, and examine the trace with Wireshark:\n", + "\n", + "```\n", + "http://asecuritysite.com/log/tor.zip \n", + "```\n", + "\n", + "* Which TCP port does the client use to send to?\n", + "* What is the IP address of the Tor node that the client connects to?\n", + "* What is strange about the packet size?\n", + "* Is SSL/TLS used for the connection?\n", + "* Can you trace any content in the conversation?\n", + "* Can you determine the Web site that is being connected to?\n", + "\n", + "\n", + "## E\tTLS Connection\n", + "### E.1\t\n", + "We will now create our own SSL/TLS server and client in Python. First, we need to generate a certificate for our server:\n", + "```\n", + "openssl req -new -x509 -days 365 -nodes -out mycert.pem -keyout mycert.pem\n", + "```\n", + "\n", + "Next we will create a server which will listen on Port 444 (as 443 is likely to be used already for HTTPs), and support two cipher suites ('AES256+ECDH:AES256+EDH'):\n", + "\n", + "\n", + "```python\n", + "import socket, ssl\n", + "\n", + "context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)\n", + "context.load_cert_chain(certfile=\"mycert.pem\") \n", + "\n", + "def handle(conn):\n", + " conn.write(b'GET / HTTP/1.1\\n')\n", + " print(conn.recv().decode())\n", + "\n", + "while True:\n", + " sock = socket.socket()\n", + " sock.bind(('', 444))\n", + " sock.listen(5)\n", + " context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)\n", + " context.load_cert_chain(certfile=\"mycert.pem\") \n", + " context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 # optional\n", + " context.set_ciphers('AES256+ECDH:AES256+EDH')\n", + " while True:\n", + " conn = None\n", + " ssock, addr = sock.accept()\n", + " try:\n", + " conn = context.wrap_socket(ssock, server_side=True)\n", + " handle(conn)\n", + " except ssl.SSLError as e:\n", + " print(e)\n", + " finally:\n", + " if conn:\n", + " conn.close()\n", + "```\n", + "\n", + "Now we will create the client to connect on Port 444. As we have a self-signed certificate, we will disable the checking of the host and certificate (remember to change the IP address to the address of your local host):\n", + "\n", + "```python\n", + "import socket, ssl\n", + "\n", + "HOST, PORT = '10.10.10.10', 444\n", + "\n", + "def handle(conn):\n", + " conn.write(b'GET / HTTP/1.1\\n')\n", + " print(conn.recv().decode())\n", + "\n", + "def main():\n", + "\n", + " sock = socket.socket(socket.AF_INET)\n", + "\n", + " context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)\n", + " context.check_hostname = False\n", + " context.verify_mode=ssl.CERT_NONE\n", + "\n", + "\n", + " context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 \n", + "\n", + " conn = context.wrap_socket(sock, server_hostname=HOST)\n", + "\n", + " try:\n", + " conn.connect((HOST, PORT))\n", + " handle(conn)\n", + " finally:\n", + " conn.close()\n", + "\n", + "if __name__ == '__main__':\n", + " main()\n", + "```\n", + " \n", + "\n", + "Now run Wireshark (sudo wireshark &), and capture from the Ethernet port (a sample run is show in in Figure 1). Now run the server, and then run the client. Stop Wireshark and determine:\n", + "\t\n", + "* The cipher suites sent from client to the server (‘Client Hello’): \n", + "* The cipher suite selected by the server (‘Server Hello’):\n", + "\n", + "\n", + "If we change the code to:\n", + "\n", + "```python\n", + "context.set_ciphers(‘HIGH’)\n", + "```\n", + "\n", + "What are the cipher suites sent from server, and which cipher suite is selected by the client:\n", + "\n", + "\n", + "\n", + "\n", + " \n", + "Figure 1: Sample capture\n", + "\n", + "Now select your own cipher suits to accept. The possible settings are given next. You can use the “+” (to add), “-“ (to take away), and “!” (for not).\n", + "\n", + "Key exchange:\n", + "\n", + "* kRSA, aRSA, RSA. RSA Key exchange.\n", + "* kDHE, kEDH, DH. Ephemeral DH key agreement.\n", + "* DHE, EDH. Cipher suites using authenticated ephemeral DH key agreement.\n", + "* kEECDH, kECDHE, ECDH. Cipher suites using ephemeral ECDH key agreement.\n", + "* ECDHE, EECDH. Cipher suites using authenticated ephemeral ECDH key agreement.\n", + "* aECDSA, ECDSA. Cipher suites with ECDSA authentication.\n", + "\n", + "Encryption:\n", + "\n", + "* AES128, AES256, AES, AESGCM, AESCCM, AESCCM8.\n", + "* ARIA128, ARIA256, ARIA.\n", + "* CAMELLIA128, CAMELLIA256, CAMELLIA.\n", + "* CHACHA20.\n", + "* 3DES, DES, RC4, RC2, IDEA.\n", + "\n", + "Hashing methods:\n", + "\n", + "•\tMD5, SHA1, SHA. SHA256, SHA384\n", + "•\taGOST, kGOST, GOST94, GOST89MAC.\n", + "\n", + "We can also use: HIGH (256-bit); MEDIUM (128-bit); LOW (56-bit or 64-bit).\n", + "\n", + "## G\tSecure services\n", + "### G.1\t\n", + "On your VM, determine your IP address with ipconfig, and then using nmap, show the running servers on the server:\n", + "\n", + "
\n",
+    "ifconfig\n",
+    "nmap [ip]\n",
+    "
\n", + "\n", + "What are the servers that are running:\n", + "\n", + "\n", + "\n", + "Open a Web browser on your server, and open up the home page with:\n", + "
\n",
+    "https://[ip]\n",
+    "
\n", + "\n", + "What is contained on the home page:\n", + "\n", + "\n", + "### G.2\t\n", + "Now to the /var/www/html folder and show that there is a file named index.html. Connect to the sftp service by determining your IP address () and use the command:\n", + "\n", + "
\n",
+    "sftp sftpuser@[ip]\n",
+    "
\n", + "\n", + "With this we run the normal FTP service, but integrate with the SSH service (and which runs on Port 22). Now run the following commands, and determine the output:\n", + "\n", + "
\n",
+    "pwd\n",
+    "ls\n",
+    "cd napier\n",
+    "put index.html\n",
+    "
\n", + "\n", + "### G.3\t\n", + "Now exit from sftp and try and locate the file you have copied. Go back to sftp, and now see if you can copy a file to the /home/napier folder.\n", + "\n", + "Now start wireshark (with sudo wireshark &), and capture your session. Now login into your local host with the ssh server:\n", + "
\n",
+    "ssh napier@localhost\n",
+    "
\n", + "\n", + "\n", + "What observations can you make on the creation of the secure connection:\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "### G.4\t\n", + "Now, let’s repeat the lab question from last week. Let’s enable HTTPs:\n", + "
\n",
+    "sudo a2enmod ssl\n",
+    "service apache2 restart\n",
+    "openssl genrsa -out ca.key 2048\n",
+    "sudo openssl req -nodes -new -key ca.key -out ca.csr\n",
+    "sudo openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt\n",
+    "sudo mkdir /etc/apache2/ssl\n",
+    "sudo cp ca.crt ca.key ca.csr /etc/apache2/ssl/\n",
+    "sudo nano /etc/apache2/sites-enabled/000-default.conf\n",
+    "sudo /etc/init.d/apache2 restart\n",
+    "
\n", + "\n", + "HTTPs should now be enabled with a self-signed certificate. If you try https://localhost, you will have to add an exception to view the page, as we are using a self-signed certificate:" + ] + } + ], + "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.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/crypto.ipynb b/z_jupyter/crypto.ipynb deleted file mode 100644 index 8e55763..0000000 --- a/z_jupyter/crypto.ipynb +++ /dev/null @@ -1,1497 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "ddb45b14", - "metadata": {}, - "source": [ - "# Hazmat Primitives in Cryptography" - ] - }, - { - "cell_type": "markdown", - "id": "27a217f9", - "metadata": {}, - "source": [ - "One of the useful library for crypto primitives in Python is called cryptography. It contain both \"secure\" primitives and a \"hazmat\" layer. Overall the \"hazmat\" layer should be treated with care, as it could contain security vulnerabilities. In this case we will implement a number of hashing primitives with the \"hazmat\" layer." - ] - }, - { - "cell_type": "markdown", - "id": "bd1df656", - "metadata": {}, - "source": [ - "## Hashing" - ] - }, - { - "cell_type": "markdown", - "id": "a71b189d", - "metadata": {}, - "source": [ - "Within hashing methods, we take data in the form of a byte array, and then create a fixed length hash value. For MD5, the length of the hash is 128 bits, for SHA-1 it is 160 bits, and for SHA-256, it is 256 bits. In the following we will use the hashing methods supported by the Hazmat primitive. This includes MD5, SHA-1 and SHA-256:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5b576b95", - "metadata": {}, - "outputs": [], - "source": [ - "from cryptography.hazmat.primitives import hashes\n", - "import binascii\n", - "import sys\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "st = \"00\"\n", - "hex=False\n", - "showhex=\"No\"\n", - "\n", - "def show_hash(name,type,data):\n", - " digest = hashes.Hash(type,backend=default_backend())\n", - " digest.update(data)\n", - " res=digest.finalize()\n", - " hex=binascii.b2a_hex(res).decode()\n", - " b64=binascii.b2a_base64(res).decode()\n", - " print (f\"{name}: {hex} {b64}\")\n", - "\n", - "if (showhex==\"yes\"): hex=True\n", - "\n", - "try:\n", - "\tif (hex==True): data = binascii.a2b_hex(st)\n", - "\telse: data=st.encode()\n", - "\n", - "\n", - "\tprint (\"Data: \",st)\n", - "\tprint (\" Hex: \",binascii.b2a_hex(data).decode())\n", - "\tprint()\n", - "\n", - "\tshow_hash(\"Blake2p (64 bytes)\",hashes.BLAKE2b(64),data)\n", - "\tshow_hash(\"Blake2s (32 bytes)\",hashes.BLAKE2s(32),data)\n", - "\tshow_hash(\"MD5\",hashes.MD5(),data)\n", - "\tshow_hash(\"SHA1\",hashes.SHA1(),data)\t\n", - "\tshow_hash(\"SHA224\",hashes.SHA224(),data)\n", - "\tshow_hash(\"SHA256\",hashes.SHA256(),data)\n", - "\tshow_hash(\"SHA384\",hashes.SHA384(),data)\n", - "\tshow_hash(\"SHA3_224\",hashes.SHA3_224(),data)\n", - "\tshow_hash(\"SHA3_256\",hashes.SHA3_256(),data)\n", - "\tshow_hash(\"SHA3_384\",hashes.SHA3_384(),data)\n", - "\tshow_hash(\"SHA3_512\",hashes.SHA3_512(),data)\n", - "\tshow_hash(\"SHA512\",hashes.SHA512(),data)\n", - "\tshow_hash(\"SHA512_224\",hashes.SHA512_224(),data)\n", - "\tshow_hash(\"SHA512_256\",hashes.SHA512_256(),data)\n", - "\tshow_hash(\"SHAKE128 (64 bytes)\",hashes.SHAKE128(64),data)\n", - "\tshow_hash(\"SHAKE256 (64 bytes)\",hashes.SHAKE256(64),data)\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "15aa36f1", - "metadata": {}, - "source": [ - "In this case the input data is \"00\". Can you run the program again, and this time use the data input of \"The quick brown fox jumps over the lazy dog\". Prove that:\n", - "\n", - "- MD5 hash value is \"9e107d9d372bb6826bd81d3542a419d6\"\n", - "- SHA-1 hash value is \"2fd4e1c67a2d28fced849ee1bb76e7391b93eb12\"\n", - "- SHA-256 hash value is \"d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592\"" - ] - }, - { - "cell_type": "markdown", - "id": "baf56309", - "metadata": {}, - "source": [ - "## HMAC" - ] - }, - { - "cell_type": "markdown", - "id": "0d2ca3fe", - "metadata": {}, - "source": [ - "HMAC (hash-based message authentication code) supports the usage of a key to hash data. This key is kept secret between Bob and Alice, and can be used to authentication both the data and that the sender still knows the secret. Overall HMAC can be used with a range of different hashing methods, such as MD5, SHA-1, SHA-256 (SHA-2) and SHA-3. This page outlines the main HMAC methods used in the Hazmat cryptography layer. You can either enter as a hexademical string or an ASCII string, and the output is displayed in a hexademical and a Base-64 format." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "7e78337d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Data: The quick brown fox jumps over the lazy dog\n", - " Hex: 54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67\n", - "Key: key\n", - " Hex: 6b6579\n", - "\n", - "HMAC-Blake2p (64 bytes): 92294f92c0dfb9b00ec9ae8bd94d7e7d8a036b885a499f149dfe2fd2199394aaaf6b8894a1730cccb2cd050f9bcf5062a38b51b0dab33207f8ef35ae2c9df51b kilPksDfubAOya6L2U1+fYoDa4haSZ8Unf4v0hmTlKqva4iUoXMMzLLNBQ+bz1Bio4tRsNqzMgf47zWuLJ31Gw==\n", - "\n", - "HMAC-Blake2s (32 bytes): f93215bb90d4af4c3061cd932fb169fb8bb8a91d0b4022baea1271e1323cd9a0 +TIVu5DUr0wwYc2TL7Fp+4u4qR0LQCK66hJx4TI82aA=\n", - "\n", - "HMAC-MD5: 80070713463e7749b90c2dc24911e275 gAcHE0Y+d0m5DC3CSRHidQ==\n", - "\n", - "HMAC-SHA1: de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9 3nybhbi3iqa8ino29wqQcBydtNk=\n", - "\n", - "HMAC-SHA224: 88ff8b54675d39b8f72322e65ff945c52d96379988ada25639747e69 iP+LVGddObj3IyLmX/lFxS2WN5mIraJWOXR+aQ==\n", - "\n", - "HMAC-SHA256: f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8 97yD9DBThCSxMpjmqm+xQ+9NWaFJRhdZl0edvC0aPNg=\n", - "\n", - "HMAC-SHA384: d7f4727e2c0b39ae0f1e40cc96f60242d5b7801841cea6fc592c5d3e1ae50700582a96cf35e1e554995fe4e03381c237 1/RyfiwLOa4PHkDMlvYCQtW3gBhBzqb8WSxdPhrlBwBYKpbPNeHlVJlf5OAzgcI3\n", - "\n", - "HMAC-SHA3_224: ff6fa8447ce10fb1efdccfe62caf8b640fe46c4fb1007912bf85100f /2+oRHzhD7Hv3M/mLK+LZA/kbE+xAHkSv4UQDw==\n", - "\n", - "HMAC-SHA3_256: 8c6e0683409427f8931711b10ca92a506eb1fafa48fadd66d76126f47ac2c333 jG4Gg0CUJ/iTFxGxDKkqUG6x+vpI+t1m12Em9HrCwzM=\n", - "\n", - "HMAC-SHA3_384: aa739ad9fcdf9be4a04f06680ade7a1bd1e01a0af64accb04366234cf9f6934a0f8589772f857681fcde8acc256091a2 qnOa2fzfm+SgTwZoCt56G9HgGgr2SsywQ2YjTPn2k0oPhYl3L4V2gfzeiswlYJGi\n", - "\n", - "HMAC-SHA3_512: 237a35049c40b3ef5ddd960b3dc893d8284953b9a4756611b1b61bffcf53edd979f93547db714b06ef0a692062c609b70208ab8d4a280ceee40ed8100f293063 I3o1BJxAs+9d3ZYLPciT2ChJU7mkdWYRsbYb/89T7dl5+TVH23FLBu8KaSBixgm3AgirjUooDO7kDtgQDykwYw==\n", - "\n", - "HMAC-SHA512: b42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a tCrwkFe6weLUFwjkipAuCbX/fxKrQopP6GZTxz3SSPuC+UilSfe3kaW0GRXuTR7Dk1NX5OIxclDQNyr6Lr7rOg==\n", - "\n", - "HMAC-SHA512_224: a1afb4f708cb63570639195121785ada3dc615989cc3c73f38e306a3 oa+09wjLY1cGORlRIXha2j3GFZicw8c/OOMGow==\n", - "\n", - "HMAC-SHA512_256: 7fb65e03577da9151a1016e9c2e514d4d48842857f13927f348588173dca6d89 f7ZeA1d9qRUaEBbpwuUU1NSIQoV/E5J/NIWIFz3KbYk=\n", - "\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives import hashes, hmac\n", - "import binascii\n", - "import sys\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "st = \"The quick brown fox jumps over the lazy dog\"\n", - "hex=False\n", - "showhex=\"No\"\n", - "k=\"key\"\n", - "\n", - "def show_hmac(name,type,data,key):\n", - " digest = hmac.HMAC(key, type,backend=default_backend())\n", - " digest.update(data)\n", - " res=digest.finalize()\n", - " hex=binascii.b2a_hex(res).decode()\n", - " b64=binascii.b2a_base64(res).decode()\n", - " print (f\"HMAC-{name}: {hex} {b64}\")\n", - "\n", - "if (showhex==\"yes\"): hex=True\n", - "\n", - "\n", - "try:\n", - "\tif (hex==True): data = binascii.a2b_hex(st)\n", - "\telse: data=st.encode()\n", - "\n", - "\tif (hex==True): key = binascii.a2b_hex(k)\n", - "\telse: key=k.encode()\n", - "\n", - "\tprint (\"Data: \",st)\n", - "\tprint (\" Hex: \",binascii.b2a_hex(data).decode())\n", - "\tprint (\"Key: \",k)\n", - "\tprint (\" Hex: \",binascii.b2a_hex(key).decode())\n", - "\tprint()\n", - "\n", - "\tshow_hmac(\"Blake2p (64 bytes)\",hashes.BLAKE2b(64),data,key)\n", - "\tshow_hmac(\"Blake2s (32 bytes)\",hashes.BLAKE2s(32),data,key)\n", - "\tshow_hmac(\"MD5\",hashes.MD5(),data,key)\n", - "\tshow_hmac(\"SHA1\",hashes.SHA1(),data,key)\t\n", - "\tshow_hmac(\"SHA224\",hashes.SHA224(),data,key)\n", - "\tshow_hmac(\"SHA256\",hashes.SHA256(),data,key)\n", - "\tshow_hmac(\"SHA384\",hashes.SHA384(),data,key)\n", - "\tshow_hmac(\"SHA3_224\",hashes.SHA3_224(),data,key)\n", - "\tshow_hmac(\"SHA3_256\",hashes.SHA3_256(),data,key)\n", - "\tshow_hmac(\"SHA3_384\",hashes.SHA3_384(),data,key)\n", - "\tshow_hmac(\"SHA3_512\",hashes.SHA3_512(),data,key)\n", - "\tshow_hmac(\"SHA512\",hashes.SHA512(),data,key)\n", - "\tshow_hmac(\"SHA512_224\",hashes.SHA512_224(),data,key)\n", - "\tshow_hmac(\"SHA512_256\",hashes.SHA512_256(),data,key)\n", - "\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "4d58782c", - "metadata": {}, - "source": [ - "## HKDF" - ] - }, - { - "cell_type": "markdown", - "id": "8b676e51", - "metadata": {}, - "source": [ - "HKDF (HMAC Key Derivation Function) is used to generate an encryption key based on a password. We can use a range of hashing methods to dervie the encryption key. In this case we will use a range of hashing methods, and derive a key of a given size." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "144316a0", - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Key: 00\n", - " Hex: 3030\n", - "Salt: \n", - " Hex: \n", - "\n", - "name 'default_backend' is not defined\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives import hashes\n", - "from cryptography.hazmat.primitives.kdf.hkdf import HKDF\n", - "from cryptography.hazmat.primitives.kdf.scrypt import Scrypt\n", - "from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC\n", - "from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash,ConcatKDFHMAC\n", - "from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "\n", - "import binascii\n", - "import sys\n", - "\n", - "st = \"00\"\n", - "hex=False\n", - "showhex=\"No\"\n", - "k=\"00\"\n", - "length=16\n", - "slt=\"\"\n", - "\n", - "def show_hash(name,type,data,length,salt):\n", - "\n", - " hkdf = HKDF(algorithm=type, length=length,salt=salt, info=b\"\",backend=default_backend())\n", - " mykey=hkdf.derive(data)\n", - " hex=binascii.b2a_hex(mykey).decode()\n", - " b64=binascii.b2a_base64(mykey).decode()\n", - " print (f\"HKDF {name}: {hex} {b64}\")\n", - "\n", - "def show_hash_pbkdf2(name,type,data,length,salt):\n", - "\n", - " hkdf = PBKDF2HMAC(algorithm=type, length=length,salt=salt, iterations=1000,backend=default_backend())\n", - " mykey=hkdf.derive(data)\n", - " hex=binascii.b2a_hex(mykey).decode()\n", - " b64=binascii.b2a_base64(mykey).decode()\n", - " print (f\"HKDF {name}: {hex} {b64}\")\n", - "\n", - "def show_hash_scrypt(name,data,length,salt):\n", - "\n", - " hkdf = Scrypt(length=length,salt=salt,n=2**14,r=8, p=1,backend=default_backend())\n", - " mykey=hkdf.derive(data)\n", - " hex=binascii.b2a_hex(mykey).decode()\n", - " b64=binascii.b2a_base64(mykey).decode()\n", - " print (f\"HKDF {name}: {hex} {b64}\")\n", - "\n", - "def show_hash_concat(name,type,data,length,salt):\n", - "\n", - " hkdf = ConcatKDFHash(algorithm=type, length=length,otherinfo=b\"\",backend=default_backend())\n", - " mykey=hkdf.derive(data)\n", - " hex=binascii.b2a_hex(mykey).decode()\n", - " b64=binascii.b2a_base64(mykey).decode()\n", - " print (f\"HKDF {name}: {hex} {b64}\")\n", - "\n", - "\n", - "if (showhex==\"yes\"): hex=True\n", - "\n", - "\n", - "\n", - "try:\n", - "\tif (hex==True): data = binascii.a2b_hex(st)\n", - "\telse: data=st.encode()\n", - "\tif (hex==True): salt = binascii.a2b_hex(slt)\n", - "\telse: salt=slt.encode()\n", - "\n", - "\n", - "\tprint (\"Key: \",st)\n", - "\tprint (\" Hex: \",binascii.b2a_hex(data).decode())\n", - "\n", - "\tprint (\"Salt: \",slt)\n", - "\tprint (\" Hex: \",binascii.b2a_hex(salt).decode())\n", - "\n", - "\tprint()\n", - "\n", - "\n", - "\tshow_hash(\"Blake2p (64 bytes)\",hashes.BLAKE2b(64),data,length,salt)\n", - "\tshow_hash(\"Blake2s (32 bytes)\",hashes.BLAKE2s(32),data,length,salt)\n", - "\tshow_hash(\"MD5\",hashes.MD5(),data,length,salt)\n", - "\tshow_hash(\"SHA1\",hashes.SHA1(),data,length,salt)\t\n", - "\tshow_hash(\"SHA224\",hashes.SHA224(),data,length,salt)\n", - "\tshow_hash(\"SHA256\",hashes.SHA256(),data,length,salt)\n", - "\tshow_hash(\"SHA384\",hashes.SHA384(),data,length,salt)\n", - "\tshow_hash(\"SHA3_224\",hashes.SHA3_224(),data,length,salt)\n", - "\tshow_hash(\"SHA3_256\",hashes.SHA3_256(),data,length,salt)\n", - "\tshow_hash(\"SHA3_384\",hashes.SHA3_384(),data,length,salt)\n", - "\tshow_hash(\"SHA3_512\",hashes.SHA3_512(),data,length,salt)\n", - "\tshow_hash(\"SHA512\",hashes.SHA512(),data,length,salt)\n", - "\tshow_hash(\"SHA512_224\",hashes.SHA512_224(),data,length,salt)\n", - "\tshow_hash(\"SHA512_256\",hashes.SHA512_256(),data,length,salt)\n", - "\tshow_hash_pbkdf2(\"PBKDF2\",hashes.SHA256(),data,length,salt)\n", - "\tshow_hash_scrypt(\"Scrypt SHA256\",data,length,salt)\n", - "\tshow_hash_concat(\"Concat SHA256\",hashes.SHA256(),data,length,salt)\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "9154359d", - "metadata": {}, - "source": [] - }, - { - "cell_type": "markdown", - "id": "509b908c", - "metadata": {}, - "source": [ - "## Symmetric Key" - ] - }, - { - "cell_type": "markdown", - "id": "e8a9af38", - "metadata": {}, - "source": [ - "Symmetric key encryption involves the same key being used to encrypt and decrypt. Apart from ECB mode, we also normally apply a salt value known as an IV (Initialization Vector). The main modes are ECB (Electronic Code Book), CBC (Cipher Block Chaining), CTR (Counter), OFB (Output Feedback) and GCM (Galois/Counter Mode). In this case we will use the cryptography library in Python, and implement AES, Chacha20, Camellia, 3DES, IDEA, CAST5 and Blowfish. The Python program will use a random 256-bit encryption key, and a random 128-bit IV value. In most of the modes, we need to pad the plaintext to the size of the block (typically this is 128 bits or 16 bytes), but AES GCM mode we do not need to pad the cipher as it is a stream cipher." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "1fed6488", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Message:\t Hello\n", - "Key:\t b'75108231d759b576e9e26d74a69fac01de5412c427b01779fefe805640ee7756'\n", - "IV:\t b'b3c3f903d81e2f47a5bb9d6d7102c46a'\n", - "\n", - "\n", - "=== AES ECB === \n", - "Cipher: b'52710346abd34fdd302a33a103009310'\n", - "Decrypted: Hello\n", - "=== AES CBC === \n", - "Cipher: b'0b3e0cc6c8f580f8a658b69e9982e3a3'\n", - "Decrypted: Hello\n", - "=== AES OFB === \n", - "Cipher: b'3d2139534946484050cc14b3129cf67e'\n", - "Decrypted: Hello\n", - "=== AES CFB === \n", - "Cipher: b'3d2139534946484050cc14b3129cf67e'\n", - "Decrypted: Hello\n", - "=== AES CTR === \n", - "Cipher: b'3d2139534946484050cc14b3129cf67e'\n", - "Decrypted: Hello\n", - "=== AES GCM === \n", - "Cipher: b'5de7889741'\n", - "Decrypted: Hello\n", - "=== AES XTS === \n", - "Cipher: b'84b89b9f920d781443e6e959a278c800'\n", - "Decrypted: Hello\n", - "\n", - "=== Blowfish ECB === \n", - "Cipher: b'53a677a4b06b48fc764539ff9b357a5a'\n", - "Decrypted: Hello\n", - "=== Blowfish CBC === \n", - "Cipher: b'e414ca26835d193aa1095c4b99656d87'\n", - "Decrypted: Hello\n", - "\n", - "=== ChaCha20 === \n", - "Cipher: b'43d3ff9fd6'\n", - "Decrypted: Hello\n", - "\n", - "=== 3DES ECB === \n", - "Cipher: b'1a6a67bbeb161e73b8df6e840a944b04'\n", - "Decrypted: Hello\n", - "=== 3DES CBC === \n", - "Cipher: b'd1590a192c68beced42474dbe595f114'\n", - "Decrypted: Hello\n", - "\n", - "=== Camellia ECB === \n", - "Cipher: b'eb1d01d74a0699e1aa5e338d8ee1992b'\n", - "Decrypted: Hello\n", - "=== Camellia CBC === \n", - "Cipher: b'f9c418b9b73680015c0c19944a7a836f'\n", - "Decrypted: Hello\n", - "=== IDEA ECB === \n", - "Cipher: b'0e1e83b4b6cf9c73fa96477e41c095fd'\n", - "Decrypted: Hello\n", - "=== IDEA CBC === \n", - "Cipher: b'eb04b675cc1c20cfa09828fe3a8f4975'\n", - "Decrypted: Hello\n", - "\n", - "=== CAST5 ECB === \n", - "Cipher: b'9a35089a74674806069fa63712384c1b'\n", - "Decrypted: Hello\n", - "=== CAST5 CBC === \n", - "Cipher: b'a31fccf6db4a119bf8f04507c1927c6d'\n", - "Decrypted: Hello\n" - ] - } - ], - "source": [ - "import os\n", - "from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes \n", - "from cryptography.hazmat.primitives import padding\n", - "import sys\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "import binascii\n", - "\n", - "def go_encrypt(msg,method,mode):\n", - " cipher = Cipher(method, mode,backend=default_backend())\n", - " encryptor = cipher.encryptor()\n", - " ct = encryptor.update(msg) + encryptor.finalize()\n", - " return (ct)\n", - "\n", - "\n", - "def go_decrypt(ct,method,mode):\n", - " cipher = Cipher(method, mode,backend=default_backend())\n", - " decryptor = cipher.decryptor()\n", - " return (decryptor.update(ct) + decryptor.finalize())\n", - "\n", - "def go_encrypt_with_auth(msg,method,mode,add):\n", - " cipher = Cipher(method, mode,backend=default_backend())\n", - " encryptor = cipher.encryptor()\n", - " encryptor.authenticate_additional_data(add)\n", - " ct = encryptor.update(msg) + encryptor.finalize()\n", - " return (ct,encryptor.tag)\n", - "\n", - "\n", - "def go_decrypt_with_auth(ct,method,mode,add):\n", - " cipher = Cipher(method, mode,backend=default_backend())\n", - " decryptor = cipher.decryptor()\n", - " decryptor.authenticate_additional_data(add)\n", - " pl=decryptor.update(ct) + decryptor.finalize()\n", - " return (pl)\n", - "\n", - "def pad(data,size=128):\n", - " padder = padding.PKCS7(size).padder()\n", - " padded_data = padder.update(data)\n", - " padded_data += padder.finalize()\n", - " return(padded_data)\n", - "\n", - "def unpad(data,size=128):\n", - " padder = padding.PKCS7(size).unpadder()\n", - " unpadded_data = padder.update(data)\n", - " unpadded_data += padder.finalize()\n", - " return(unpadded_data)\n", - "\n", - "\n", - "key = os.urandom(32)\n", - "iv = os.urandom(16)\n", - "msg=b\"Hello\"\n", - "tag= b\"Some data for the authentication tag\"\n", - "\n", - "\n", - "print (\"Message:\\t\",msg.decode())\n", - "print (\"Key:\\t\",binascii.b2a_hex(key))\n", - "print (\"IV:\\t\",binascii.b2a_hex(iv))\n", - "\n", - "padded_data=pad(msg)\n", - "\n", - "\n", - "print (\"\\n\\n=== AES ECB === \")\n", - "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.ECB())\n", - "\n", - "plain=go_decrypt(cipher,algorithms.AES(key), modes.ECB())\n", - "data=unpad(plain)\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CBC(iv))\n", - "\n", - "print (\"=== AES CBC === \")\n", - "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CBC(iv))\n", - "\n", - "plain=go_decrypt(cipher,algorithms.AES(key), modes.CBC(iv))\n", - "data=unpad(plain)\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CBC(iv))\n", - "\n", - "print (\"=== AES OFB === \")\n", - "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.OFB(iv))\n", - "\n", - "plain=go_decrypt(cipher,algorithms.AES(key), modes.OFB(iv))\n", - "data=unpad(plain)\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "print (\"=== AES CFB === \")\n", - "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CFB(iv))\n", - "\n", - "plain=go_decrypt(cipher,algorithms.AES(key), modes.CFB(iv))\n", - "data=unpad(plain)\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "print (\"=== AES CTR === \")\n", - "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.CTR(iv))\n", - "\n", - "plain=go_decrypt(cipher,algorithms.AES(key), modes.CTR(iv))\n", - "data=unpad(plain)\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "print (\"=== AES GCM === \")\n", - "# In GCM mode we convert to a stream cipher, so there is no need for padding\n", - "cipher,auth_tag=go_encrypt_with_auth(msg,algorithms.AES(key), modes.GCM(iv),tag)\n", - "\n", - "plain=go_decrypt_with_auth(cipher,algorithms.AES(key), modes.GCM(iv,auth_tag),tag)\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {plain.decode()}\")\n", - "\n", - "\n", - "print (\"=== AES XTS === \")\n", - "# In XTS the iv value is known as a tweak - and relates to the sector number\n", - "# The keys are double length, so that a 32 byte key (256 bits) is actually AES-128\n", - "cipher=go_encrypt(padded_data,algorithms.AES(key), modes.XTS(iv))\n", - "\n", - "plain=go_decrypt(cipher,algorithms.AES(key), modes.XTS(iv))\n", - "data=unpad(plain)\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "\n", - "print (\"\\n=== Blowfish ECB === \")\n", - "\n", - "cipher=go_encrypt(padded_data,algorithms.Blowfish(key), modes.ECB())\n", - "\n", - "plain=go_decrypt(cipher,algorithms.Blowfish(key), modes.ECB())\n", - "\n", - "data=unpad(plain)\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "print (\"=== Blowfish CBC === \")\n", - "\n", - "cipher=go_encrypt(padded_data,algorithms.Blowfish(key), modes.CBC(iv[:8]))\n", - "\n", - "plain=go_decrypt(cipher,algorithms.Blowfish(key), modes.CBC(iv[:8]))\n", - "\n", - "data=unpad(plain)\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "\n", - "print (\"\\n=== ChaCha20 === \")\n", - "\n", - "cipher=go_encrypt(msg,algorithms.ChaCha20(key,iv), None)\n", - "\n", - "plain=go_decrypt(cipher,algorithms.ChaCha20(key,iv), None)\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "print (\"\\n=== 3DES ECB === \")\n", - "cipher=go_encrypt(padded_data,algorithms.TripleDES(key[:16]), modes.ECB())\n", - "\n", - "plain=go_decrypt(cipher,algorithms.TripleDES(key[:16]), modes.ECB())\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "print (\"=== 3DES CBC === \")\n", - "cipher=go_encrypt(padded_data,algorithms.TripleDES(key[:16]), modes.CBC(iv[:8]))\n", - "\n", - "plain=go_decrypt(cipher,algorithms.TripleDES(key[:16]), modes.CBC(iv[:8]))\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "\n", - "print (\"\\n=== Camellia ECB === \")\n", - "cipher=go_encrypt(padded_data,algorithms.Camellia(key), modes.ECB())\n", - "\n", - "plain=go_decrypt(cipher,algorithms.Camellia(key), modes.ECB())\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "\n", - "print (\"=== Camellia CBC === \")\n", - "cipher=go_encrypt(padded_data,algorithms.Camellia(key), modes.CBC(iv))\n", - "\n", - "plain=go_decrypt(cipher,algorithms.Camellia(key), modes.CBC(iv))\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "print (\"=== IDEA ECB === \")\n", - "cipher=go_encrypt(padded_data,algorithms.IDEA(key[:16]), modes.ECB())\n", - "\n", - "plain=go_decrypt(cipher,algorithms.IDEA(key[:16]), modes.ECB())\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "print (\"=== IDEA CBC === \")\n", - "cipher=go_encrypt(padded_data,algorithms.IDEA(key[:16]), modes.CBC(iv[:8]))\n", - "\n", - "plain=go_decrypt(cipher,algorithms.IDEA(key[:16]), modes.CBC(iv[:8]))\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "print (\"\\n=== CAST5 ECB === \")\n", - "cipher=go_encrypt(padded_data,algorithms.CAST5(key[:16]), modes.ECB())\n", - "\n", - "plain=go_decrypt(cipher,algorithms.CAST5(key[:16]), modes.ECB())\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")\n", - "\n", - "print (\"=== CAST5 CBC === \")\n", - "cipher=go_encrypt(padded_data,algorithms.CAST5(key[:16]), modes.CBC(iv[:8]))\n", - "\n", - "plain=go_decrypt(cipher,algorithms.CAST5(key[:16]), modes.CBC(iv[:8]))\n", - "\n", - "print (\"Cipher: \",binascii.b2a_hex(cipher))\n", - "print (f\"Decrypted: {data.decode()}\")" - ] - }, - { - "cell_type": "markdown", - "id": "9172ce8d", - "metadata": {}, - "source": [ - "## RSA Keys " - ] - }, - { - "cell_type": "markdown", - "id": "3526088c", - "metadata": {}, - "source": [ - "With RSA, we have the magic of public key encryption, and where Bob can generate a key pair: a public key and a private key, and then send Alice his public key. If she wants to encrypt something for him, she encrypts it with his public key, and then the only key which can decrypt it is his private key. A truly wonderful concept, and it was Rivest, Shamir and Adleman who made it come alive with the RSA method. In RSA, we start by generating two prime numbers (p,q) and then calculate the modulus (N) of N=pq. It is this modulus (N) that provides the core of the security of RSA, and it must be difficult to determine the prime numbers for a given modulus value. Our prime numbers must thus be of a size that makes it difficult to factorize, and they must be randomly generated." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "b0559a57", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "RSA key size: 512\n", - "M=5\n", - "\n", - "=== RSA Private key ===\n", - "p=114240027289133679916853601188430818342505970058336223690127546253419301921291 q=106927850855783140995262884657855361840603034156020906226195283705417869958079 d=12056263550552130122923905739070666874031192187927860369377606874056401919958137436002088193238240131651226273674194799808471728408204851623607010221110053 N=12215440599733082136976701922011568649001781649277680148229058975388191837550991125104446323995201242037017634104455399946932098494548782067414314327559989\n", - "\n", - "Bit length of p and q is 256\n", - "Bit length of N is 512\n", - "\n", - "=== RSA Public key ===\n", - "\n", - "N=12215440599733082136976701922011568649001781649277680148229058975388191837550991125104446323995201242037017634104455399946932098494548782067414314327559989 e=65537\n", - "\n", - "Message=5\n", - "Cipher=8681388997006403186260644889804391620158460251057105492554241431796114500559026556435367120212960187799018356532032536157753321962365210361500584359919374\n", - "Decrypt=5\n", - "\n", - "=== Private Key PEM format ===\n", - "Private key: -----BEGIN PRIVATE KEY-----\n", - "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEA6TvUc/1/kZ9IKfWE\n", - "GcSYGaNKqDpJ4kwZVrMvZIe+3sGq0w4ZusEvPitTX9ahWiZqoyRwfC9Lhuxowjpi\n", - "kj1PNQIDAQABAkEA5jHJ500cmnJ1/Au4/OG/KRa0ecNF5tcq7xpdPUCJTzo9r046\n", - "MQh0TB9yHoFAYeTYQx9MOL6lNTpfy5GQhJgzJQIhAPyRkGbBar/VjIoNvFNgYk7J\n", - "a2cq0xksTi4o/H+LsWILAiEA7GcE1L2WtDlWfzU0pd14mhpvGJo/MMBnzjw6mwFq\n", - "G78CIH54oZdiyQKQe5RxtlkFzZRoNdjuT9CuJ+PjcDSqDcPrAiAYb1WKf8Yx/rM4\n", - "FtHISyoKE61kB518dobERGlyWCUVlQIgJh6O3xe8W6fQCwY3y1StRWo4pQmsJ/VY\n", - "R953mvvhQ3E=\n", - "-----END PRIVATE KEY-----\n", - "\n", - "\n", - "=== Public Key PEM format ===\n", - "Public key: -----BEGIN PUBLIC KEY-----\n", - "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAOk71HP9f5GfSCn1hBnEmBmjSqg6SeJM\n", - "GVazL2SHvt7BqtMOGbrBLz4rU1/WoVomaqMkcHwvS4bsaMI6YpI9TzUCAwEAAQ==\n", - "-----END PUBLIC KEY-----\n", - "\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives.asymmetric import rsa\n", - "from cryptography.hazmat.primitives import serialization\n", - "import sys\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "size=512\n", - "M=5\n", - "\n", - "\n", - "try:\n", - "\tprint(f\"RSA key size: {size}\\nM={M}\\n\")\n", - "\n", - "\tprivate_key = rsa.generate_private_key(public_exponent=65537,key_size=size,backend=default_backend())\n", - "\n", - "\tpriv= private_key.private_numbers()\n", - "\tp=priv.p\n", - "\tq=priv.q \n", - "\td=priv.d\n", - "\tn=p*q\n", - "\tprint(\"=== RSA Private key ===\")\n", - "\tprint (f\"p={p} q={q} d={d} N={n}\")\n", - "\tprint (f\"\\nBit length of p and q is {p.bit_length()}\")\n", - "\tprint (f\"Bit length of N is {n.bit_length()}\")\n", - "\n", - "\tprint(\"\\n=== RSA Public key ===\")\n", - "\tpub = private_key.public_key()\n", - "\te=pub.public_numbers().e\n", - "\tn=pub.public_numbers().n\n", - "\tprint (f\"\\nN={n} e={e}\")\n", - "\n", - "\n", - "\n", - "\tC = pow(M,e,n)\n", - "\tPlain = pow(C,d,n)\n", - "\tprint (f\"\\nMessage={M}\")\n", - "\tprint (f\"Cipher={C}\")\n", - "\tprint (f\"Decrypt={Plain}\")\n", - "\n", - "\n", - "\n", - "\tprint(\"\\n=== Private Key PEM format ===\")\n", - "\tpem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", - "\tprint (\"Private key: \",pem.decode())\n", - "\n", - "\n", - "\tpem = pub.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)\n", - "\n", - "\tprint(\"\\n=== Public Key PEM format ===\")\n", - "\tprint (\"Public key: \",pem.decode())\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "bca147bd", - "metadata": {}, - "source": [ - "## RSA Key Formats" - ] - }, - { - "cell_type": "markdown", - "id": "888b3824", - "metadata": {}, - "source": [ - "We need ways to distribute our public keys, private keys and digital certificates in a portable format. One of the most common forms is Distinguished Encoding Rules (DER) encoding of ASN.1. Overall it is truly binary representation of the encoded data. The other common format is PEM, and which converts the binary encoding into a text readable format. With PEM we can encode cryptographic infromation in a Base64 ASCII format and with plain-text headers and footers of \"-----BEGIN RSA PRIVATE KEY-----\" and \"-----END RSA PRIVATE KEY-----\", whereas with DER we have binary format. In this page, we will read in a DER encoded hex string and determine its contents. " - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "3455e9c4", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "name 'default_backend' is not defined\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives.asymmetric import rsa\n", - "from cryptography.hazmat.primitives import serialization\n", - "import sys\n", - "import binascii\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "size=512\n", - "\n", - "try:\n", - " private_key = rsa.generate_private_key(public_exponent=65537,key_size=size,backend=default_backend())\n", - "\n", - "\n", - " pub = private_key.public_key()\n", - "\n", - " print(\"\\n=== Private Key PEM format ===\")\n", - " pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", - "\n", - " der= private_key.private_bytes(encoding=serialization.Encoding.DER,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", - "\n", - "\n", - " print (\"\\nPrivate key (PEM):\\n\",pem.decode())\n", - " print (\"\\nPrivate key (DER):\\n\",binascii.b2a_hex(der).decode())\n", - "\n", - "\n", - " pem = pub.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo).decode(\"utf-8\")\n", - "\n", - " openssh = pub.public_bytes(encoding=serialization.Encoding.OpenSSH,format=serialization.PublicFormat.OpenSSH)\n", - "\n", - "\n", - " print(\"\\n=== Public Key format ===\")\n", - "\n", - " der=''.join(pem.split('\\n')[1:-2])\n", - " print (\"Public key (PEM):\\n\",pem)\n", - " print (\"\\nPublic key (DER):\\n\",binascii.b2a_hex(binascii.a2b_base64(der)).decode())\n", - " print (\"\\nPublic key (OpenSSL):\\n\",openssh.decode())\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "b715d4bd", - "metadata": {}, - "source": [ - "## RSA Signatures" - ] - }, - { - "cell_type": "markdown", - "id": "0b7f433a", - "metadata": {}, - "source": [ - "At the core of digital trust is the usage of digital signatures. With this we can verify the creator of the data, and also that it has not been modified. We do this using public key encryption, and in this article we will look at how we can use the hazmat primitives in the Python cryptograpy library. With public key encryption we create a key pair: a public key and a private key. If Alice is sending data to Bob, she can add her digital signature, and which will prove that she is the sender and also verify that the data has not been changed. She does this by signing the data with her private key, and then Bob can prove the signature with Alices's public key. In this example we will use RSA keys to sign a message, and then verify the correct signature, but verify that an incorrect signature will fail. " - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "7d14ac0a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Message: b'Hello world'\n", - "Key size: 512\n", - "\n", - "Signature: a01c26461569ecc39e6b66527789510d8a311f642e85c3bd7635a7974d1d58ac3d571439ed8fef4682df3c38759192f3bc255162be81e6c1c2b08fecbf7772da\n", - "Good signature verified\n", - "A bad signature failed\n", - "\n", - "Verified: None\n", - "\n", - "=== Private Key PEM format ===\n", - "\n", - "Private key (PEM):\n", - " -----BEGIN PRIVATE KEY-----\n", - "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2Ywlcqf634Jsqt9Y\n", - "GJFKTDiXoYWuZ8zI4LqT9191ZpOqkUK4vaPwJONDx1rng4YDsclSMU7RFXesDRbi\n", - "a5ksuwIDAQABAkBm+RRcsjBojYa8Vt2PO25sp7j2Rp2oGmHXJdmWFQQwGcQGDBJv\n", - "Ct1se31q3aXK50/ATMdVkKuENL98G1YvSaLZAiEA8KGLf27nltaRpLt3/TiYVg7g\n", - "opp677t37V2w1MendiUCIQDncSvm0JxXh15wLX1rvIf4KM8n39hzlAL/t0GoT7Zx\n", - "XwIhAL8ajP8XxTU2E1UQzap9V/6AskZxCVF7QTNyRQ23wndBAiEAkEVu3XoLC1D6\n", - "IKlqtn8Wry4ZPD0Ae8O3PtpollfiXbcCIBGFaDpLQxDacjdJlQFQxO4EEtk4icLx\n", - "6uPLf87z2l7c\n", - "-----END PRIVATE KEY-----\n", - "\n", - "\n", - "=== Public Key format ===\n", - "Public key (PEM):\n", - " -----BEGIN PUBLIC KEY-----\n", - "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmMJXKn+t+CbKrfWBiRSkw4l6GFrmfM\n", - "yOC6k/dfdWaTqpFCuL2j8CTjQ8da54OGA7HJUjFO0RV3rA0W4muZLLsCAwEAAQ==\n", - "-----END PUBLIC KEY-----\n", - "\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives.asymmetric import rsa\n", - "from cryptography.hazmat.primitives import serialization\n", - "from cryptography.hazmat.primitives.asymmetric import padding\n", - "from cryptography.hazmat.primitives import hashes\n", - "from cryptography import exceptions\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "import sys\n", - "import binascii\n", - "\n", - "size=512\n", - "message = b\"Hello world\"\n", - "\n", - "\n", - "print(\"Message: \",message)\n", - "print(\"Key size: \",size)\n", - "try:\n", - " private_key = rsa.generate_private_key(public_exponent=65537,key_size=size,backend=default_backend())\n", - "\n", - "\n", - " pub = private_key.public_key()\n", - "\n", - "\n", - "\n", - " signature = private_key.sign(message,padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),hashes.SHA256())\n", - "\n", - " print (\"\\nSignature: \",binascii.b2a_hex(signature).decode())\n", - "\n", - " try:\n", - " rtn=pub.verify(signature,message,padding.PSS(mgf=padding.MGF1(hashes.SHA256()),salt_length=padding.PSS.MAX_LENGTH),hashes.SHA256())\n", - " except exceptions.InvalidSignature:\n", - " print(\"A bad signature failed\")\n", - " else:\n", - " print(\"Good signature verified\")\n", - "\n", - "\n", - " try:\n", - " pub.verify(signature,b\"test\",padding.PSS(mgf=padding.MGF1(hashes.SHA256()),salt_length=padding.PSS.MAX_LENGTH),hashes.SHA256())\n", - " except exceptions.InvalidSignature:\n", - " print(\"A bad signature failed\")\n", - " else:\n", - " print(\"Bad signature verified\")\n", - "\n", - "\n", - "\n", - " print (\"\\nVerified: \",rtn)\n", - " print(\"\\n=== Private Key PEM format ===\")\n", - " pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", - "\n", - "\n", - " print (\"\\nPrivate key (PEM):\\n\",pem.decode())\n", - "\n", - "\n", - " pem = pub.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo).decode(\"utf-8\")\n", - "\n", - "\n", - "\n", - " print(\"\\n=== Public Key format ===\")\n", - " print (\"Public key (PEM):\\n\",pem)\n", - "\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "67f3eb96", - "metadata": {}, - "source": [ - "## RSA Encryption" - ] - }, - { - "cell_type": "markdown", - "id": "00f1f9d7", - "metadata": {}, - "source": [ - "With public-key encryption, we create a key pair: a public key and a private key. If Alice is sending data to Bob, she can add her digital signature, and which will prove that she is the sender and also verify that the data has not been changed. She does this by signing the data with her private key, and then Bob can prove the signature with Alice's public key. In this example, we will use RSA keys to sign a message, and then verify the correct signature, but verify that an incorrect signature will fail." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "750c45e4", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Message: b'Hello world'\n", - "Key size: 1024\n", - "\n", - "Ciphertext:\n", - " a03bc31a7e3fca1abaecff8dd7a252def14ae50faeeb28989f69978acc71395059b4bdee36028dd3707418b21d08c2aec311165725ead1374551807c574ad425fa946475396f1abe4acba69da0ab1c71e417df74648f077f2d58e70eb88fa5123d96638e2ace9e6b9a3b46245d8f97bdba67c1c89a5c8b21ea75c2775a1d74e3\n", - "\n", - "Decrypted Message: Hello world\n", - "\n", - "=== Private Key PEM format ===\n", - "\n", - "Private key (PEM):\n", - " -----BEGIN PRIVATE KEY-----\n", - "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAM9h+pN2k/Vyrb24\n", - "uLh89E8QzoOXszBpkQYVqMTnZHcLpJarAcNmlP15Hx6YtDfQWL47FapqCWBbUr2E\n", - "gwFhzFZbIz4t8DnUROwQLAJcsz/aQ0zxEJUS8taOg9oFUvHYg7MdD2FnG30tEBSx\n", - "roXMT1Mt4RhxK9YA2+27gF0IHSA3AgMBAAECgYEAvSPjYbL4ZxfclfhvoPN17tap\n", - "QyOMbk8Z0a7Xah6QZnLb/f6hcRGSOUvKsiEb3PvMtFM3A2IKgzNHV0oQIaaMsWHC\n", - "XL8yeRLFpkTkWKWvfzaIdYj0VO5KTtsPtiJN4iaToZV/DVQjtADd2GdNRm26FCO4\n", - "8NLt8Z2LUojf/Bv3oQECQQDpPHLCV6CvzKBaFO2XmiN3y3TcGjpBfQ0hp8EnZX0P\n", - "T320exTt5KIoHqTc5tnYyZ6htBsAROjHq0tGn/jz2gjXAkEA45+RLsgae2KETcN9\n", - "F/sgXh46PajO7RRsDR3POdbkRwvYclvaxf6Tu0klY4PBAq6Fs5vC/OzeeaK7WSEg\n", - "rtfXoQJABBFY0wjiNY61ET0iM62thq5WuGwArhm3O+oIFV7Qwo6ZW6d14vxE07xN\n", - "Fck07XH+wUU531RUVv8mzfGGTwJzewJBAJXGYi6JJru3WF0e28oSyPqFI0d9MyNW\n", - "w75htgBWAQEqrzKYdDzBxY1/lOWFRVQxZ/KF9Giep8kbpfEA1lOV3OECQALE07c7\n", - "ZnVyA6D6WQP/gR9KewkQroiksnMYHe3g8j7NStvDaNSDzHtovs37mL08kcpmuxxc\n", - "B6UvxXlwrSOQdEc=\n", - "-----END PRIVATE KEY-----\n", - "\n", - "\n", - "=== Public Key format ===\n", - "Public key (PEM):\n", - " -----BEGIN PUBLIC KEY-----\n", - "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDPYfqTdpP1cq29uLi4fPRPEM6D\n", - "l7MwaZEGFajE52R3C6SWqwHDZpT9eR8emLQ30Fi+OxWqaglgW1K9hIMBYcxWWyM+\n", - "LfA51ETsECwCXLM/2kNM8RCVEvLWjoPaBVLx2IOzHQ9hZxt9LRAUsa6FzE9TLeEY\n", - "cSvWANvtu4BdCB0gNwIDAQAB\n", - "-----END PUBLIC KEY-----\n", - "\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives.asymmetric import rsa\n", - "from cryptography.hazmat.primitives import serialization\n", - "from cryptography.hazmat.primitives.asymmetric import padding\n", - "from cryptography.hazmat.primitives import hashes\n", - "import binascii\n", - "import sys\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "\n", - "size=1024\n", - "message = b\"Hello world\"\n", - "\n", - "\n", - "print(\"Message: \",message)\n", - "print(\"Key size: \",size)\n", - "try:\n", - " private_key = rsa.generate_private_key(public_exponent=65537,key_size=size,backend=default_backend())\n", - "\n", - "\n", - " pub = private_key.public_key()\n", - "\n", - " ciphertext = pub.encrypt(message,padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),algorithm=hashes.SHA256(),label=None))\n", - "\n", - " print (\"\\nCiphertext:\\n\",binascii.b2a_hex(ciphertext).decode())\n", - "\n", - " plaintext = private_key.decrypt(ciphertext,padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),algorithm=hashes.SHA256(),label=None))\n", - " print(\"\\nDecrypted Message: \",plaintext.decode())\n", - "\n", - " print(\"\\n=== Private Key PEM format ===\")\n", - " pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", - "\n", - "\n", - " print (\"\\nPrivate key (PEM):\\n\",pem.decode())\n", - "\n", - "\n", - " pem = pub.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo).decode(\"utf-8\")\n", - "\n", - "\n", - "\n", - " print(\"\\n=== Public Key format ===\")\n", - " print (\"Public key (PEM):\\n\",pem)\n", - "\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "4ecdcc50", - "metadata": {}, - "source": [ - "## EC Key Generation" - ] - }, - { - "cell_type": "markdown", - "id": "29e3176f", - "metadata": {}, - "source": [ - " With Elliptic Curve Cryptography we have a private key value, and where we generate a public key on the point on the curve." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "873a9bb2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Private key value: 2043558419001729619814474899588981766865054577966515345132. Number of bits 191\n", - "\n", - "Public key encoded point: 049fe6e8fe419a6b3f815955073a759f9fc0e28e932a568f04cc8e3ce05e188159211fecef2567922028016dedaf8afc37 \n", - "x=9fe6e8fe419a6b3f815955073a759f9fc0e28e932a568f04 \n", - "y=cc8e3ce05e188159211fecef2567922028016dedaf8afc37\n", - "\n", - "Private key (PEM):\n", - " -----BEGIN PRIVATE KEY-----\n", - "MG8CAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQEEVTBTAgEBBBhTV77TSCWX4CwtVKtr\n", - "7i8MjmzmeWNZ1uyhNAMyAASf5uj+QZprP4FZVQc6dZ+fwOKOkypWjwTMjjzgXhiB\n", - "WSEf7O8lZ5IgKAFt7a+K/Dc=\n", - "-----END PRIVATE KEY-----\n", - "\n", - "Private key (DER):\n", - " b'306f020100301306072a8648ce3d020106082a8648ce3d0301010455305302010104185357bed3482597e02c2d54ab6bee2f0c8e6ce6796359d6eca134033200049fe6e8fe419a6b3f815955073a759f9fc0e28e932a568f04cc8e3ce05e188159211fecef2567922028016dedaf8afc37'\n", - "\n", - "Public key (PEM):\n", - " -----BEGIN PUBLIC KEY-----\n", - "MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAEn+bo/kGaaz+BWVUHOnWfn8DijpMq\n", - "Vo8EzI484F4YgVkhH+zvJWeSICgBbe2vivw3\n", - "-----END PUBLIC KEY-----\n", - "\n", - "Public key (DER):\n", - " b'3049301306072a8648ce3d020106082a8648ce3d030101033200049fe6e8fe419a6b3f815955073a759f9fc0e28e932a568f04cc8e3ce05e188159211fecef2567922028016dedaf8afc37'\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/opt/tljh/user/lib/python3.7/site-packages/ipykernel_launcher.py:28: CryptographyDeprecationWarning: encode_point has been deprecated on EllipticCurvePublicNumbers and will be removed in a future version. Please use EllipticCurvePublicKey.public_bytes to obtain both compressed and uncompressed point encoding.\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives.asymmetric import ec\n", - "from cryptography.hazmat.primitives import serialization\n", - "import binascii\n", - "import sys\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "private_key = ec.generate_private_key(ec.SECP384R1(),backend=default_backend())\n", - "type =1\n", - "\n", - "\n", - "if (type==1): private_key = ec.generate_private_key(ec.SECP192R1(),backend=default_backend())\n", - "elif (type==2): private_key = ec.generate_private_key(ec.SECP224R1(),backend=default_backend())\n", - "elif (type==3): private_key = ec.generate_private_key(ec.SECP256K1(),backend=default_backend())\n", - "elif (type==4): private_key = ec.generate_private_key(ec.SECP256R1(),backend=default_backend())\n", - "elif (type==5): private_key = ec.generate_private_key(ec.SECP384R1(),backend=default_backend())\n", - "elif (type==6): private_key = ec.generate_private_key(ec.SECP521R1(),backend=default_backend())\n", - "elif (type==7): private_key = ec.generate_private_key(ec.BrainpoolP256R1(),backend=default_backend())\n", - "elif (type==8): private_key = ec.generate_private_key(ec.BrainpoolP384R1(),backend=default_backend())\n", - "elif (type==9): private_key = ec.generate_private_key(ec.BrainpoolP512R1(),backend=default_backend())\n", - "\n", - "vals = private_key.private_numbers()\n", - "no_bits=vals.private_value.bit_length()\n", - "print (f\"Private key value: {vals.private_value}. Number of bits {no_bits}\")\n", - "\n", - "public_key = private_key.public_key()\n", - "vals=public_key.public_numbers()\n", - "\n", - "enc_point=binascii.b2a_hex(vals.encode_point()).decode()\n", - "\n", - "print (f\"\\nPublic key encoded point: {enc_point} \\nx={enc_point[2:(len(enc_point)-2)//2+2]} \\ny={enc_point[(len(enc_point)-2)//2+2:]}\")\n", - "\n", - "\n", - "pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", - "\n", - "der = private_key.private_bytes(encoding=serialization.Encoding.DER,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", - "\n", - "\n", - "\n", - "print (\"\\nPrivate key (PEM):\\n\",pem.decode())\n", - "print (\"Private key (DER):\\n\",binascii.b2a_hex(der))\n", - "\n", - "pem = public_key.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)\n", - "\n", - "der = public_key.public_bytes(encoding=serialization.Encoding.DER,format=serialization.PublicFormat.SubjectPublicKeyInfo)\n", - "\n", - "print (\"\\nPublic key (PEM):\\n\",pem.decode())\n", - "print (\"Public key (DER):\\n\",binascii.b2a_hex(der))" - ] - }, - { - "cell_type": "markdown", - "id": "d1f06bfd", - "metadata": {}, - "source": [ - "## ECDSA" - ] - }, - { - "cell_type": "markdown", - "id": "37eb6676", - "metadata": {}, - "source": [ - "With ECDSA (Elliptic Curve Digital Signature Algorithm) we use a private key to sign data, and then the public key can prove it. " - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "b24a1dca", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Private key value: 1520095257287423441745410468085985957586179605404446742156. Number of bits 190\n", - "Name of curve: secp192r1\n", - "Message: test\n", - "Good Signature: 3035021879910e82df6a069d6ab007172d6918daa880e3f218a42079021900b83ff8e14ec7b38c8895897da92f173eecc74f4691efbe39\n", - "Good signature verified\n", - "Bad Signature: 303402183b7c44b272e9c1a2774d6d3f58fa6ea59b12471af019c51702185f142f21ababddfec36d7fcecd131926fac644439ef21596\n", - "A bad signature failed\n", - "\n", - "Private key (PEM):\n", - " -----BEGIN PRIVATE KEY-----\n", - "MG8CAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQEEVTBTAgEBBBg9/oj1rAm7twxSu7fI\n", - "KoQDLwC+8tPR1oyhNAMyAASWY2aP00uyPh0Hu2nx82wwQUnxKbfKIvlJDwb6yAdc\n", - "k/BQmOyP55WPPZZ0tgqMXvk=\n", - "-----END PRIVATE KEY-----\n", - "\n", - "Private key (DER):\n", - " b'306f020100301306072a8648ce3d020106082a8648ce3d0301010455305302010104183dfe88f5ac09bbb70c52bbb7c82a84032f00bef2d3d1d68ca134033200049663668fd34bb23e1d07bb69f1f36c304149f129b7ca22f9490f06fac8075c93f05098ec8fe7958f3d9674b60a8c5ef9'\n", - "\n", - "Public key (PEM):\n", - " -----BEGIN PUBLIC KEY-----\n", - "MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAElmNmj9NLsj4dB7tp8fNsMEFJ8Sm3\n", - "yiL5SQ8G+sgHXJPwUJjsj+eVjz2WdLYKjF75\n", - "-----END PUBLIC KEY-----\n", - "\n", - "Public key (DER):\n", - " b'3049301306072a8648ce3d020106082a8648ce3d030101033200049663668fd34bb23e1d07bb69f1f36c304149f129b7ca22f9490f06fac8075c93f05098ec8fe7958f3d9674b60a8c5ef9'\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives.asymmetric import ec\n", - "from cryptography.hazmat.primitives import serialization\n", - "from cryptography.hazmat.primitives import hashes\n", - "from cryptography import exceptions\n", - "import binascii\n", - "import sys\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "private_key = ec.generate_private_key(ec.SECP384R1(),backend=default_backend())\n", - "\n", - "data = b\"test\"\n", - "\n", - "\n", - "\n", - "\n", - "if (type==1): private_key = ec.generate_private_key(ec.SECP192R1(),backend=default_backend())\n", - "elif (type==2): private_key = ec.generate_private_key(ec.SECP224R1(),backend=default_backend())\n", - "elif (type==3): private_key = ec.generate_private_key(ec.SECP256K1(),backend=default_backend())\n", - "elif (type==4): private_key = ec.generate_private_key(ec.SECP256R1(),backend=default_backend())\n", - "elif (type==5): private_key = ec.generate_private_key(ec.SECP384R1(),backend=default_backend())\n", - "elif (type==6): private_key = ec.generate_private_key(ec.SECP521R1(),backend=default_backend())\n", - "elif (type==7): private_key = ec.generate_private_key(ec.BrainpoolP256R1(),backend=default_backend())\n", - "elif (type==8): private_key = ec.generate_private_key(ec.BrainpoolP384R1(),backend=default_backend())\n", - "elif (type==9): private_key = ec.generate_private_key(ec.BrainpoolP512R1(),backend=default_backend())\n", - "elif (type==10): private_key = ec.generate_private_key(ec.SECT163K1(),backend=default_backend())\n", - "elif (type==11): private_key = ec.generate_private_key(ec.SECT163R2(),backend=default_backend())\n", - "elif (type==12): private_key = ec.generate_private_key(ec.SECT233K1(),backend=default_backend())\n", - "elif (type==13): private_key = ec.generate_private_key(ec.SECT233R1(),backend=default_backend())\n", - "elif (type==14): private_key = ec.generate_private_key(ec.SECT283K1(),backend=default_backend())\n", - "elif (type==15): private_key = ec.generate_private_key(ec.SECT233R1(),backend=default_backend())\n", - "\n", - "private_vals = private_key.private_numbers()\n", - "no_bits=private_vals.private_value.bit_length()\n", - "\n", - "print (f\"Private key value: {private_vals.private_value}. Number of bits {no_bits}\")\n", - "\n", - "public_key = private_key.public_key()\n", - "pub=public_key.public_numbers()\n", - "print (\"Name of curve: \",pub.curve.name)\n", - "\n", - "print (\"Message: \",data.decode())\n", - "try:\n", - "\n", - " signature = private_key.sign(data,ec.ECDSA(hashes.SHA256()))\n", - " print (\"Good Signature: \",binascii.b2a_hex(signature).decode())\n", - " public_key.verify(signature, data, ec.ECDSA(hashes.SHA256()))\n", - "except exceptions.InvalidSignature:\n", - " print(\"A bad signature failed\")\n", - "else:\n", - " print(\"Good signature verified\")\n", - "\n", - "try:\n", - "\n", - " signature = private_key.sign(b\"bad message\",ec.ECDSA(hashes.SHA256()))\n", - " print (\"Bad Signature: \",binascii.b2a_hex(signature).decode())\n", - " public_key.verify(signature, data, ec.ECDSA(hashes.SHA256()))\n", - "except exceptions.InvalidSignature:\n", - " print(\"A bad signature failed\")\n", - "else:\n", - " print(\"Good signature verified\")\n", - "\n", - "\n", - "\n", - "pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", - "\n", - "der = private_key.private_bytes(encoding=serialization.Encoding.DER,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", - "\n", - "\n", - "\n", - "print (\"\\nPrivate key (PEM):\\n\",pem.decode())\n", - "print (\"Private key (DER):\\n\",binascii.b2a_hex(der))\n", - "\n", - "pem = public_key.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)\n", - "\n", - "der = public_key.public_bytes(encoding=serialization.Encoding.DER,format=serialization.PublicFormat.SubjectPublicKeyInfo)\n", - "\n", - "print (\"\\nPublic key (PEM):\\n\",pem.decode())\n", - "print (\"Public key (DER):\\n\",binascii.b2a_hex(der))" - ] - }, - { - "cell_type": "markdown", - "id": "993187c9", - "metadata": {}, - "source": [ - "## EdDSA" - ] - }, - { - "cell_type": "markdown", - "id": "e98ebf84", - "metadata": {}, - "source": [ - "With Ed25519 we use a private key to sign data, and then the public key can prove it. We use Curve 25519 for the generation of the public key and for the signing process. " - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "90b4f801", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Message: -f\n", - "Good Signature: a3ad2f0a47b055120e6b07f1da5c46fcc29290f5eae89d45821a501ceeb7a509db2bfe881e156080511c24d1fdfa6cf58d2caab7eee2764a3502ce9004a0690a\n", - "Good signature verified\n", - "Bad Signature: 54d68b1e1dbcfb26af2b47fbbdecbe7e928c8af26544a3aacde3207cacd4ef0a2a454b3c87e3332c4ff0f352d2d10cfe534f72a4805f1e93476ca1abf9bd710a\n", - "A bad signature failed\n", - "\n", - "Private key (PEM):\n", - " -----BEGIN PRIVATE KEY-----\n", - "MC4CAQAwBQYDK2VwBCIEINmf90gJ2jkVT5AxJtOTLbaXkxe+9IifEjAdzCiEjJ7D\n", - "-----END PRIVATE KEY-----\n", - "\n", - "Private key (DER):\n", - " b'302e020100300506032b657004220420d99ff74809da39154f903126d3932db6979317bef4889f12301dcc28848c9ec3'\n", - "\n", - "Public key (PEM):\n", - " -----BEGIN PUBLIC KEY-----\n", - "MCowBQYDK2VwAyEAf1sbNhi9c3jtiF5dshrMDNEHExVcUC19ikVMofdXpEE=\n", - "-----END PUBLIC KEY-----\n", - "\n", - "Public key (DER):\n", - " b'302a300506032b65700321007f5b1b3618bd7378ed885e5db21acc0cd10713155c502d7d8a454ca1f757a441'\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey\n", - "from cryptography import exceptions\n", - "from cryptography.hazmat.primitives import serialization\n", - "import binascii\n", - "import sys\n", - "\n", - "data=b\"Test\"\n", - "\n", - "\n", - "private_key = Ed25519PrivateKey.generate()\n", - "public_key = private_key.public_key()\n", - "\n", - "\n", - "public_key = private_key.public_key()\n", - "\n", - "print (\"Message: \",data.decode())\n", - "try:\n", - "\n", - " signature = private_key.sign(data)\n", - " print (\"Good Signature: \",binascii.b2a_hex(signature).decode())\n", - " public_key.verify(signature, data)\n", - "except exceptions.InvalidSignature:\n", - " print(\"A bad signature failed\")\n", - "else:\n", - " print(\"Good signature verified\")\n", - "\n", - "try:\n", - "\n", - " signature = private_key.sign(b\"Bad data\")\n", - " print (\"Bad Signature: \",binascii.b2a_hex(signature).decode())\n", - " public_key.verify(signature, data)\n", - "except exceptions.InvalidSignature:\n", - " print(\"A bad signature failed\")\n", - "else:\n", - " print(\"Good signature verified\")\n", - "\n", - "\n", - "\n", - "pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", - "\n", - "der = private_key.private_bytes(encoding=serialization.Encoding.DER,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())\n", - "\n", - "\n", - "\n", - "print (\"\\nPrivate key (PEM):\\n\",pem.decode())\n", - "print (\"Private key (DER):\\n\",binascii.b2a_hex(der))\n", - "\n", - "pem = public_key.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)\n", - "\n", - "der = public_key.public_bytes(encoding=serialization.Encoding.DER,format=serialization.PublicFormat.SubjectPublicKeyInfo)\n", - "\n", - "print (\"\\nPublic key (PEM):\\n\",pem.decode())\n", - "print (\"Public key (DER):\\n\",binascii.b2a_hex(der))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "363e239f", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "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.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} - diff --git a/z_jupyter/graphics/Thumbs.db b/z_jupyter/graphics/Thumbs.db new file mode 100644 index 0000000..2deaccf Binary files /dev/null and b/z_jupyter/graphics/Thumbs.db differ diff --git a/z_jupyter/graphics/bc.png b/z_jupyter/graphics/bc.png new file mode 100644 index 0000000..7a232d2 Binary files /dev/null and b/z_jupyter/graphics/bc.png differ diff --git a/z_jupyter/graphics/hmac.png b/z_jupyter/graphics/blake2.png similarity index 99% rename from z_jupyter/graphics/hmac.png rename to z_jupyter/graphics/blake2.png index 20ab595..0d1877b 100644 Binary files a/z_jupyter/graphics/hmac.png and b/z_jupyter/graphics/blake2.png differ diff --git a/z_jupyter/graphics/ecc3.png b/z_jupyter/graphics/ecc3.png new file mode 100644 index 0000000..2cf45ac Binary files /dev/null and b/z_jupyter/graphics/ecc3.png differ diff --git a/z_jupyter/graphics/ecdh.png b/z_jupyter/graphics/ecdh.png new file mode 100644 index 0000000..ba59f10 Binary files /dev/null and b/z_jupyter/graphics/ecdh.png differ diff --git a/z_jupyter/graphics/g_aws_01.png b/z_jupyter/graphics/g_aws_01.png new file mode 100644 index 0000000..04c12bd Binary files /dev/null and b/z_jupyter/graphics/g_aws_01.png differ diff --git a/z_jupyter/graphics/g_aws_02.png b/z_jupyter/graphics/g_aws_02.png new file mode 100644 index 0000000..c3ec548 Binary files /dev/null and b/z_jupyter/graphics/g_aws_02.png differ diff --git a/z_jupyter/graphics/g_aws_03.png b/z_jupyter/graphics/g_aws_03.png new file mode 100644 index 0000000..42c6976 Binary files /dev/null and b/z_jupyter/graphics/g_aws_03.png differ diff --git a/z_jupyter/graphics/g_aws_04.png b/z_jupyter/graphics/g_aws_04.png new file mode 100644 index 0000000..377fc9c Binary files /dev/null and b/z_jupyter/graphics/g_aws_04.png differ diff --git a/z_jupyter/graphics/g_aws_05.png b/z_jupyter/graphics/g_aws_05.png new file mode 100644 index 0000000..134f9f5 Binary files /dev/null and b/z_jupyter/graphics/g_aws_05.png differ diff --git a/z_jupyter/graphics/g_aws_06.png b/z_jupyter/graphics/g_aws_06.png new file mode 100644 index 0000000..0896ac8 Binary files /dev/null and b/z_jupyter/graphics/g_aws_06.png differ diff --git a/z_jupyter/graphics/g_aws_07.png b/z_jupyter/graphics/g_aws_07.png new file mode 100644 index 0000000..ba582b4 Binary files /dev/null and b/z_jupyter/graphics/g_aws_07.png differ diff --git a/z_jupyter/graphics/g_aws_08.png b/z_jupyter/graphics/g_aws_08.png new file mode 100644 index 0000000..0693c53 Binary files /dev/null and b/z_jupyter/graphics/g_aws_08.png differ diff --git a/z_jupyter/graphics/g_aws_09.png b/z_jupyter/graphics/g_aws_09.png new file mode 100644 index 0000000..b80b594 Binary files /dev/null and b/z_jupyter/graphics/g_aws_09.png differ diff --git a/z_jupyter/graphics/g_aws_10.png b/z_jupyter/graphics/g_aws_10.png new file mode 100644 index 0000000..30a7dc8 Binary files /dev/null and b/z_jupyter/graphics/g_aws_10.png differ diff --git a/z_jupyter/graphics/g_aws_11.png b/z_jupyter/graphics/g_aws_11.png new file mode 100644 index 0000000..5591935 Binary files /dev/null and b/z_jupyter/graphics/g_aws_11.png differ diff --git a/z_jupyter/graphics/g_aws_11_2.png b/z_jupyter/graphics/g_aws_11_2.png new file mode 100644 index 0000000..eee1f0f Binary files /dev/null and b/z_jupyter/graphics/g_aws_11_2.png differ diff --git a/z_jupyter/graphics/g_aws_12.png b/z_jupyter/graphics/g_aws_12.png new file mode 100644 index 0000000..c7c5ba3 Binary files /dev/null and b/z_jupyter/graphics/g_aws_12.png differ diff --git a/z_jupyter/graphics/g_aws_12_2.png b/z_jupyter/graphics/g_aws_12_2.png new file mode 100644 index 0000000..cab895e Binary files /dev/null and b/z_jupyter/graphics/g_aws_12_2.png differ diff --git a/z_jupyter/graphics/g_block_01.png b/z_jupyter/graphics/g_block_01.png new file mode 100644 index 0000000..5fed75a Binary files /dev/null and b/z_jupyter/graphics/g_block_01.png differ diff --git a/z_jupyter/graphics/g_block_02.png b/z_jupyter/graphics/g_block_02.png new file mode 100644 index 0000000..1e21c13 Binary files /dev/null and b/z_jupyter/graphics/g_block_02.png differ diff --git a/z_jupyter/graphics/g_block_03.png b/z_jupyter/graphics/g_block_03.png new file mode 100644 index 0000000..6bfeb3b Binary files /dev/null and b/z_jupyter/graphics/g_block_03.png differ diff --git a/z_jupyter/graphics/g_block_04.png b/z_jupyter/graphics/g_block_04.png new file mode 100644 index 0000000..53873f3 Binary files /dev/null and b/z_jupyter/graphics/g_block_04.png differ diff --git a/z_jupyter/graphics/g_block_05.png b/z_jupyter/graphics/g_block_05.png new file mode 100644 index 0000000..47d7a40 Binary files /dev/null and b/z_jupyter/graphics/g_block_05.png differ diff --git a/z_jupyter/graphics/g_cert_01.png b/z_jupyter/graphics/g_cert_01.png new file mode 100644 index 0000000..f254c50 Binary files /dev/null and b/z_jupyter/graphics/g_cert_01.png differ diff --git a/z_jupyter/graphics/g_cert_02.png b/z_jupyter/graphics/g_cert_02.png new file mode 100644 index 0000000..f3f1050 Binary files /dev/null and b/z_jupyter/graphics/g_cert_02.png differ diff --git a/z_jupyter/graphics/g_cert_03.png b/z_jupyter/graphics/g_cert_03.png new file mode 100644 index 0000000..5e82a4c Binary files /dev/null and b/z_jupyter/graphics/g_cert_03.png differ diff --git a/z_jupyter/graphics/g_cyan_01.png b/z_jupyter/graphics/g_cyan_01.png new file mode 100644 index 0000000..c178736 Binary files /dev/null and b/z_jupyter/graphics/g_cyan_01.png differ diff --git a/z_jupyter/graphics/g_dig_pound_01.png b/z_jupyter/graphics/g_dig_pound_01.png new file mode 100644 index 0000000..8eb9c86 Binary files /dev/null and b/z_jupyter/graphics/g_dig_pound_01.png differ diff --git a/z_jupyter/graphics/g_dig_pound_02.png b/z_jupyter/graphics/g_dig_pound_02.png new file mode 100644 index 0000000..abdf813 Binary files /dev/null and b/z_jupyter/graphics/g_dig_pound_02.png differ diff --git a/z_jupyter/graphics/g_dig_pound_03.png b/z_jupyter/graphics/g_dig_pound_03.png new file mode 100644 index 0000000..3dc716d Binary files /dev/null and b/z_jupyter/graphics/g_dig_pound_03.png differ diff --git a/z_jupyter/graphics/g_dig_pound_04.png b/z_jupyter/graphics/g_dig_pound_04.png new file mode 100644 index 0000000..c650551 Binary files /dev/null and b/z_jupyter/graphics/g_dig_pound_04.png differ diff --git a/z_jupyter/graphics/g_dig_pound_05.png b/z_jupyter/graphics/g_dig_pound_05.png new file mode 100644 index 0000000..74c58ce Binary files /dev/null and b/z_jupyter/graphics/g_dig_pound_05.png differ diff --git a/z_jupyter/graphics/g_dig_pound_06.png b/z_jupyter/graphics/g_dig_pound_06.png new file mode 100644 index 0000000..73be30a Binary files /dev/null and b/z_jupyter/graphics/g_dig_pound_06.png differ diff --git a/z_jupyter/graphics/g_dig_pound_07.png b/z_jupyter/graphics/g_dig_pound_07.png new file mode 100644 index 0000000..7efe863 Binary files /dev/null and b/z_jupyter/graphics/g_dig_pound_07.png differ diff --git a/z_jupyter/graphics/g_dig_pound_08.png b/z_jupyter/graphics/g_dig_pound_08.png new file mode 100644 index 0000000..af67159 Binary files /dev/null and b/z_jupyter/graphics/g_dig_pound_08.png differ diff --git a/z_jupyter/graphics/g_dig_pound_09.png b/z_jupyter/graphics/g_dig_pound_09.png new file mode 100644 index 0000000..b951052 Binary files /dev/null and b/z_jupyter/graphics/g_dig_pound_09.png differ diff --git a/z_jupyter/graphics/g_dig_pound_10.png b/z_jupyter/graphics/g_dig_pound_10.png new file mode 100644 index 0000000..ce3c6a5 Binary files /dev/null and b/z_jupyter/graphics/g_dig_pound_10.png differ diff --git a/z_jupyter/graphics/g_dig_pound_11.png b/z_jupyter/graphics/g_dig_pound_11.png new file mode 100644 index 0000000..863f25d Binary files /dev/null and b/z_jupyter/graphics/g_dig_pound_11.png differ diff --git a/z_jupyter/graphics/g_distsig_01.png b/z_jupyter/graphics/g_distsig_01.png new file mode 100644 index 0000000..ec01800 Binary files /dev/null and b/z_jupyter/graphics/g_distsig_01.png differ diff --git a/z_jupyter/graphics/g_distsig_04.png b/z_jupyter/graphics/g_distsig_04.png new file mode 100644 index 0000000..a876c2a Binary files /dev/null and b/z_jupyter/graphics/g_distsig_04.png differ diff --git a/z_jupyter/graphics/g_distsig_05.png b/z_jupyter/graphics/g_distsig_05.png new file mode 100644 index 0000000..879af1e Binary files /dev/null and b/z_jupyter/graphics/g_distsig_05.png differ diff --git a/z_jupyter/graphics/g_distsig_06.png b/z_jupyter/graphics/g_distsig_06.png new file mode 100644 index 0000000..aea7d58 Binary files /dev/null and b/z_jupyter/graphics/g_distsig_06.png differ diff --git a/z_jupyter/graphics/g_distsig_07.png b/z_jupyter/graphics/g_distsig_07.png new file mode 100644 index 0000000..4371d77 Binary files /dev/null and b/z_jupyter/graphics/g_distsig_07.png differ diff --git a/z_jupyter/graphics/g_distsig_08.png b/z_jupyter/graphics/g_distsig_08.png new file mode 100644 index 0000000..b81be5e Binary files /dev/null and b/z_jupyter/graphics/g_distsig_08.png differ diff --git a/z_jupyter/graphics/g_distsig_10.png b/z_jupyter/graphics/g_distsig_10.png new file mode 100644 index 0000000..860dcd3 Binary files /dev/null and b/z_jupyter/graphics/g_distsig_10.png differ diff --git a/z_jupyter/graphics/g_distsig_11.png b/z_jupyter/graphics/g_distsig_11.png new file mode 100644 index 0000000..8ac7c46 Binary files /dev/null and b/z_jupyter/graphics/g_distsig_11.png differ diff --git a/z_jupyter/graphics/g_distsig_12.png b/z_jupyter/graphics/g_distsig_12.png new file mode 100644 index 0000000..4af0134 Binary files /dev/null and b/z_jupyter/graphics/g_distsig_12.png differ diff --git a/z_jupyter/graphics/g_distsig_13.png b/z_jupyter/graphics/g_distsig_13.png new file mode 100644 index 0000000..5fe822c Binary files /dev/null and b/z_jupyter/graphics/g_distsig_13.png differ diff --git a/z_jupyter/graphics/g_distsig_14.png b/z_jupyter/graphics/g_distsig_14.png new file mode 100644 index 0000000..fea9971 Binary files /dev/null and b/z_jupyter/graphics/g_distsig_14.png differ diff --git a/z_jupyter/graphics/g_distsig_14_02.png b/z_jupyter/graphics/g_distsig_14_02.png new file mode 100644 index 0000000..092b470 Binary files /dev/null and b/z_jupyter/graphics/g_distsig_14_02.png differ diff --git a/z_jupyter/graphics/g_distsig_15.png b/z_jupyter/graphics/g_distsig_15.png new file mode 100644 index 0000000..515eed0 Binary files /dev/null and b/z_jupyter/graphics/g_distsig_15.png differ diff --git a/z_jupyter/graphics/g_distsig_16.png b/z_jupyter/graphics/g_distsig_16.png new file mode 100644 index 0000000..84428fe Binary files /dev/null and b/z_jupyter/graphics/g_distsig_16.png differ diff --git a/z_jupyter/graphics/g_distsig_17.png b/z_jupyter/graphics/g_distsig_17.png new file mode 100644 index 0000000..27aaa9b Binary files /dev/null and b/z_jupyter/graphics/g_distsig_17.png differ diff --git a/z_jupyter/graphics/g_dsa_01.png b/z_jupyter/graphics/g_dsa_01.png new file mode 100644 index 0000000..dbfc62a Binary files /dev/null and b/z_jupyter/graphics/g_dsa_01.png differ diff --git a/z_jupyter/graphics/g_dsa_02.png b/z_jupyter/graphics/g_dsa_02.png new file mode 100644 index 0000000..0b07e1c Binary files /dev/null and b/z_jupyter/graphics/g_dsa_02.png differ diff --git a/z_jupyter/graphics/g_elgamal_01.png b/z_jupyter/graphics/g_elgamal_01.png new file mode 100644 index 0000000..e415d08 Binary files /dev/null and b/z_jupyter/graphics/g_elgamal_01.png differ diff --git a/z_jupyter/graphics/g_elgamal_02.png b/z_jupyter/graphics/g_elgamal_02.png new file mode 100644 index 0000000..bcc44b5 Binary files /dev/null and b/z_jupyter/graphics/g_elgamal_02.png differ diff --git a/z_jupyter/graphics/g_fun01.png b/z_jupyter/graphics/g_fun01.png new file mode 100644 index 0000000..9ff0692 Binary files /dev/null and b/z_jupyter/graphics/g_fun01.png differ diff --git a/z_jupyter/graphics/g_fun02.png b/z_jupyter/graphics/g_fun02.png new file mode 100644 index 0000000..ddded47 Binary files /dev/null and b/z_jupyter/graphics/g_fun02.png differ diff --git a/z_jupyter/graphics/g_fun03.png b/z_jupyter/graphics/g_fun03.png new file mode 100644 index 0000000..cdd6785 Binary files /dev/null and b/z_jupyter/graphics/g_fun03.png differ diff --git a/z_jupyter/graphics/g_fun04.png b/z_jupyter/graphics/g_fun04.png new file mode 100644 index 0000000..d52ca00 Binary files /dev/null and b/z_jupyter/graphics/g_fun04.png differ diff --git a/z_jupyter/graphics/g_fun05.png b/z_jupyter/graphics/g_fun05.png new file mode 100644 index 0000000..43384b7 Binary files /dev/null and b/z_jupyter/graphics/g_fun05.png differ diff --git a/z_jupyter/graphics/g_fun06.png b/z_jupyter/graphics/g_fun06.png new file mode 100644 index 0000000..5cce4d3 Binary files /dev/null and b/z_jupyter/graphics/g_fun06.png differ diff --git a/z_jupyter/graphics/g_glass_01.png b/z_jupyter/graphics/g_glass_01.png new file mode 100644 index 0000000..2714d0c Binary files /dev/null and b/z_jupyter/graphics/g_glass_01.png differ diff --git a/z_jupyter/graphics/g_glass_02.png b/z_jupyter/graphics/g_glass_02.png new file mode 100644 index 0000000..5ec824b Binary files /dev/null and b/z_jupyter/graphics/g_glass_02.png differ diff --git a/z_jupyter/graphics/g_glass_03.png b/z_jupyter/graphics/g_glass_03.png new file mode 100644 index 0000000..5972729 Binary files /dev/null and b/z_jupyter/graphics/g_glass_03.png differ diff --git a/z_jupyter/graphics/g_hash_01.png b/z_jupyter/graphics/g_hash_01.png new file mode 100644 index 0000000..e7b8d06 Binary files /dev/null and b/z_jupyter/graphics/g_hash_01.png differ diff --git a/z_jupyter/graphics/g_hash_02.png b/z_jupyter/graphics/g_hash_02.png new file mode 100644 index 0000000..ad51b80 Binary files /dev/null and b/z_jupyter/graphics/g_hash_02.png differ diff --git a/z_jupyter/graphics/g_hash_03.png b/z_jupyter/graphics/g_hash_03.png new file mode 100644 index 0000000..737e145 Binary files /dev/null and b/z_jupyter/graphics/g_hash_03.png differ diff --git a/z_jupyter/graphics/g_hash_04.png b/z_jupyter/graphics/g_hash_04.png new file mode 100644 index 0000000..9a037c5 Binary files /dev/null and b/z_jupyter/graphics/g_hash_04.png differ diff --git a/z_jupyter/graphics/g_hash_05.png b/z_jupyter/graphics/g_hash_05.png new file mode 100644 index 0000000..e1e9b07 Binary files /dev/null and b/z_jupyter/graphics/g_hash_05.png differ diff --git a/z_jupyter/graphics/g_hash_06.png b/z_jupyter/graphics/g_hash_06.png new file mode 100644 index 0000000..95f8478 Binary files /dev/null and b/z_jupyter/graphics/g_hash_06.png differ diff --git a/z_jupyter/graphics/g_hash_07.png b/z_jupyter/graphics/g_hash_07.png new file mode 100644 index 0000000..a2ad3e5 Binary files /dev/null and b/z_jupyter/graphics/g_hash_07.png differ diff --git a/z_jupyter/graphics/g_hash_08.png b/z_jupyter/graphics/g_hash_08.png new file mode 100644 index 0000000..9c81993 Binary files /dev/null and b/z_jupyter/graphics/g_hash_08.png differ diff --git a/z_jupyter/graphics/g_hom_01.png b/z_jupyter/graphics/g_hom_01.png new file mode 100644 index 0000000..1c29919 Binary files /dev/null and b/z_jupyter/graphics/g_hom_01.png differ diff --git a/z_jupyter/graphics/g_hom_02.png b/z_jupyter/graphics/g_hom_02.png new file mode 100644 index 0000000..10797ac Binary files /dev/null and b/z_jupyter/graphics/g_hom_02.png differ diff --git a/z_jupyter/graphics/g_hom_03.png b/z_jupyter/graphics/g_hom_03.png new file mode 100644 index 0000000..9b602f8 Binary files /dev/null and b/z_jupyter/graphics/g_hom_03.png differ diff --git a/z_jupyter/graphics/g_hom_04.png b/z_jupyter/graphics/g_hom_04.png new file mode 100644 index 0000000..88d265b Binary files /dev/null and b/z_jupyter/graphics/g_hom_04.png differ diff --git a/z_jupyter/graphics/g_hom_04_02.png b/z_jupyter/graphics/g_hom_04_02.png new file mode 100644 index 0000000..4f8e13d Binary files /dev/null and b/z_jupyter/graphics/g_hom_04_02.png differ diff --git a/z_jupyter/graphics/g_hom_05.png b/z_jupyter/graphics/g_hom_05.png new file mode 100644 index 0000000..8b330a2 Binary files /dev/null and b/z_jupyter/graphics/g_hom_05.png differ diff --git a/z_jupyter/graphics/g_hom_06.png b/z_jupyter/graphics/g_hom_06.png new file mode 100644 index 0000000..63f68d0 Binary files /dev/null and b/z_jupyter/graphics/g_hom_06.png differ diff --git a/z_jupyter/graphics/g_hom_07.png b/z_jupyter/graphics/g_hom_07.png new file mode 100644 index 0000000..55f8036 Binary files /dev/null and b/z_jupyter/graphics/g_hom_07.png differ diff --git a/z_jupyter/graphics/g_hom_08.png b/z_jupyter/graphics/g_hom_08.png new file mode 100644 index 0000000..3f5f24f Binary files /dev/null and b/z_jupyter/graphics/g_hom_08.png differ diff --git a/z_jupyter/graphics/g_hom_09.png b/z_jupyter/graphics/g_hom_09.png new file mode 100644 index 0000000..7f66a67 Binary files /dev/null and b/z_jupyter/graphics/g_hom_09.png differ diff --git a/z_jupyter/graphics/g_hsm_01.png b/z_jupyter/graphics/g_hsm_01.png new file mode 100644 index 0000000..9010713 Binary files /dev/null and b/z_jupyter/graphics/g_hsm_01.png differ diff --git a/z_jupyter/graphics/g_hsm_02.png b/z_jupyter/graphics/g_hsm_02.png new file mode 100644 index 0000000..d62586e Binary files /dev/null and b/z_jupyter/graphics/g_hsm_02.png differ diff --git a/z_jupyter/graphics/g_hsm_02_02.png b/z_jupyter/graphics/g_hsm_02_02.png new file mode 100644 index 0000000..80dee67 Binary files /dev/null and b/z_jupyter/graphics/g_hsm_02_02.png differ diff --git a/z_jupyter/graphics/g_hsm_03.png b/z_jupyter/graphics/g_hsm_03.png new file mode 100644 index 0000000..7820fc4 Binary files /dev/null and b/z_jupyter/graphics/g_hsm_03.png differ diff --git a/z_jupyter/graphics/g_hsm_04.png b/z_jupyter/graphics/g_hsm_04.png new file mode 100644 index 0000000..cd154fa Binary files /dev/null and b/z_jupyter/graphics/g_hsm_04.png differ diff --git a/z_jupyter/graphics/g_hsm_05.png b/z_jupyter/graphics/g_hsm_05.png new file mode 100644 index 0000000..e43dc4c Binary files /dev/null and b/z_jupyter/graphics/g_hsm_05.png differ diff --git a/z_jupyter/graphics/g_hsm_06.png b/z_jupyter/graphics/g_hsm_06.png new file mode 100644 index 0000000..07bba9b Binary files /dev/null and b/z_jupyter/graphics/g_hsm_06.png differ diff --git a/z_jupyter/graphics/g_kdc_01.png b/z_jupyter/graphics/g_kdc_01.png new file mode 100644 index 0000000..9903789 Binary files /dev/null and b/z_jupyter/graphics/g_kdc_01.png differ diff --git a/z_jupyter/graphics/g_kdc_02.png b/z_jupyter/graphics/g_kdc_02.png new file mode 100644 index 0000000..d8cbf19 Binary files /dev/null and b/z_jupyter/graphics/g_kdc_02.png differ diff --git a/z_jupyter/graphics/g_kdc_03.png b/z_jupyter/graphics/g_kdc_03.png new file mode 100644 index 0000000..55054a7 Binary files /dev/null and b/z_jupyter/graphics/g_kdc_03.png differ diff --git a/z_jupyter/graphics/g_kdf_01.png b/z_jupyter/graphics/g_kdf_01.png new file mode 100644 index 0000000..bb510d5 Binary files /dev/null and b/z_jupyter/graphics/g_kdf_01.png differ diff --git a/z_jupyter/graphics/g_kdf_02.png b/z_jupyter/graphics/g_kdf_02.png new file mode 100644 index 0000000..71d1d49 Binary files /dev/null and b/z_jupyter/graphics/g_kdf_02.png differ diff --git a/z_jupyter/graphics/g_kdf_03.png b/z_jupyter/graphics/g_kdf_03.png new file mode 100644 index 0000000..e225d51 Binary files /dev/null and b/z_jupyter/graphics/g_kdf_03.png differ diff --git a/z_jupyter/graphics/g_kdf_04.png b/z_jupyter/graphics/g_kdf_04.png new file mode 100644 index 0000000..64a5351 Binary files /dev/null and b/z_jupyter/graphics/g_kdf_04.png differ diff --git a/z_jupyter/graphics/g_kerkle_01.png b/z_jupyter/graphics/g_kerkle_01.png new file mode 100644 index 0000000..7d849c2 Binary files /dev/null and b/z_jupyter/graphics/g_kerkle_01.png differ diff --git a/z_jupyter/graphics/g_kerkle_02.png b/z_jupyter/graphics/g_kerkle_02.png new file mode 100644 index 0000000..5bb9c71 Binary files /dev/null and b/z_jupyter/graphics/g_kerkle_02.png differ diff --git a/z_jupyter/graphics/g_key_01.png b/z_jupyter/graphics/g_key_01.png new file mode 100644 index 0000000..b4c3432 Binary files /dev/null and b/z_jupyter/graphics/g_key_01.png differ diff --git a/z_jupyter/graphics/g_key_02.png b/z_jupyter/graphics/g_key_02.png new file mode 100644 index 0000000..9fe88af Binary files /dev/null and b/z_jupyter/graphics/g_key_02.png differ diff --git a/z_jupyter/graphics/g_key_03.png b/z_jupyter/graphics/g_key_03.png new file mode 100644 index 0000000..cd39c71 Binary files /dev/null and b/z_jupyter/graphics/g_key_03.png differ diff --git a/z_jupyter/graphics/g_key_04.png b/z_jupyter/graphics/g_key_04.png new file mode 100644 index 0000000..51ef3a9 Binary files /dev/null and b/z_jupyter/graphics/g_key_04.png differ diff --git a/z_jupyter/graphics/g_key_05.png b/z_jupyter/graphics/g_key_05.png new file mode 100644 index 0000000..87c828b Binary files /dev/null and b/z_jupyter/graphics/g_key_05.png differ diff --git a/z_jupyter/graphics/g_key_06.png b/z_jupyter/graphics/g_key_06.png new file mode 100644 index 0000000..e39696e Binary files /dev/null and b/z_jupyter/graphics/g_key_06.png differ diff --git a/z_jupyter/graphics/g_key_07.png b/z_jupyter/graphics/g_key_07.png new file mode 100644 index 0000000..8f93032 Binary files /dev/null and b/z_jupyter/graphics/g_key_07.png differ diff --git a/z_jupyter/graphics/g_key_08.png b/z_jupyter/graphics/g_key_08.png new file mode 100644 index 0000000..d6a9ccf Binary files /dev/null and b/z_jupyter/graphics/g_key_08.png differ diff --git a/z_jupyter/graphics/g_key_09.png b/z_jupyter/graphics/g_key_09.png new file mode 100644 index 0000000..757f0cb Binary files /dev/null and b/z_jupyter/graphics/g_key_09.png differ diff --git a/z_jupyter/graphics/g_key_10.png b/z_jupyter/graphics/g_key_10.png new file mode 100644 index 0000000..931846b Binary files /dev/null and b/z_jupyter/graphics/g_key_10.png differ diff --git a/z_jupyter/graphics/g_key_12.png b/z_jupyter/graphics/g_key_12.png new file mode 100644 index 0000000..43ba801 Binary files /dev/null and b/z_jupyter/graphics/g_key_12.png differ diff --git a/z_jupyter/graphics/g_mac_01.png b/z_jupyter/graphics/g_mac_01.png new file mode 100644 index 0000000..ec7ecf0 Binary files /dev/null and b/z_jupyter/graphics/g_mac_01.png differ diff --git a/z_jupyter/graphics/g_mac_02.png b/z_jupyter/graphics/g_mac_02.png new file mode 100644 index 0000000..0d74ab5 Binary files /dev/null and b/z_jupyter/graphics/g_mac_02.png differ diff --git a/z_jupyter/graphics/g_mac_03.png b/z_jupyter/graphics/g_mac_03.png new file mode 100644 index 0000000..326e237 Binary files /dev/null and b/z_jupyter/graphics/g_mac_03.png differ diff --git a/z_jupyter/graphics/g_mac_04.png b/z_jupyter/graphics/g_mac_04.png new file mode 100644 index 0000000..a3234e8 Binary files /dev/null and b/z_jupyter/graphics/g_mac_04.png differ diff --git a/z_jupyter/graphics/g_mac_05.png b/z_jupyter/graphics/g_mac_05.png new file mode 100644 index 0000000..c21e1f3 Binary files /dev/null and b/z_jupyter/graphics/g_mac_05.png differ diff --git a/z_jupyter/graphics/g_ml_01.png b/z_jupyter/graphics/g_ml_01.png new file mode 100644 index 0000000..6406c48 Binary files /dev/null and b/z_jupyter/graphics/g_ml_01.png differ diff --git a/z_jupyter/graphics/g_ml_02.png b/z_jupyter/graphics/g_ml_02.png new file mode 100644 index 0000000..b3634b0 Binary files /dev/null and b/z_jupyter/graphics/g_ml_02.png differ diff --git a/z_jupyter/graphics/g_ml_03.png b/z_jupyter/graphics/g_ml_03.png new file mode 100644 index 0000000..5cde466 Binary files /dev/null and b/z_jupyter/graphics/g_ml_03.png differ diff --git a/z_jupyter/graphics/g_ml_04.png b/z_jupyter/graphics/g_ml_04.png new file mode 100644 index 0000000..71bfb4e Binary files /dev/null and b/z_jupyter/graphics/g_ml_04.png differ diff --git a/z_jupyter/graphics/g_ml_05.png b/z_jupyter/graphics/g_ml_05.png new file mode 100644 index 0000000..9fe9b5c Binary files /dev/null and b/z_jupyter/graphics/g_ml_05.png differ diff --git a/z_jupyter/graphics/g_ml_06.png b/z_jupyter/graphics/g_ml_06.png new file mode 100644 index 0000000..4e6022d Binary files /dev/null and b/z_jupyter/graphics/g_ml_06.png differ diff --git a/z_jupyter/graphics/g_obl_01.png b/z_jupyter/graphics/g_obl_01.png new file mode 100644 index 0000000..c43ea15 Binary files /dev/null and b/z_jupyter/graphics/g_obl_01.png differ diff --git a/z_jupyter/graphics/g_obl_02.png b/z_jupyter/graphics/g_obl_02.png new file mode 100644 index 0000000..28eb8c5 Binary files /dev/null and b/z_jupyter/graphics/g_obl_02.png differ diff --git a/z_jupyter/graphics/g_pgp_01.png b/z_jupyter/graphics/g_pgp_01.png new file mode 100644 index 0000000..164eb1c Binary files /dev/null and b/z_jupyter/graphics/g_pgp_01.png differ diff --git a/z_jupyter/graphics/g_pgp_02.png b/z_jupyter/graphics/g_pgp_02.png new file mode 100644 index 0000000..891e7cb Binary files /dev/null and b/z_jupyter/graphics/g_pgp_02.png differ diff --git a/z_jupyter/graphics/g_pqc_01.png b/z_jupyter/graphics/g_pqc_01.png new file mode 100644 index 0000000..87cac40 Binary files /dev/null and b/z_jupyter/graphics/g_pqc_01.png differ diff --git a/z_jupyter/graphics/g_pqc_02.png b/z_jupyter/graphics/g_pqc_02.png new file mode 100644 index 0000000..9e916fa Binary files /dev/null and b/z_jupyter/graphics/g_pqc_02.png differ diff --git a/z_jupyter/graphics/g_pqc_03.png b/z_jupyter/graphics/g_pqc_03.png new file mode 100644 index 0000000..9f2472f Binary files /dev/null and b/z_jupyter/graphics/g_pqc_03.png differ diff --git a/z_jupyter/graphics/g_pqc_04.png b/z_jupyter/graphics/g_pqc_04.png new file mode 100644 index 0000000..75ea288 Binary files /dev/null and b/z_jupyter/graphics/g_pqc_04.png differ diff --git a/z_jupyter/graphics/g_proxy_01.png b/z_jupyter/graphics/g_proxy_01.png new file mode 100644 index 0000000..b885be8 Binary files /dev/null and b/z_jupyter/graphics/g_proxy_01.png differ diff --git a/z_jupyter/graphics/g_proxy_02.png b/z_jupyter/graphics/g_proxy_02.png new file mode 100644 index 0000000..5b2d2dc Binary files /dev/null and b/z_jupyter/graphics/g_proxy_02.png differ diff --git a/z_jupyter/graphics/g_proxy_03.png b/z_jupyter/graphics/g_proxy_03.png new file mode 100644 index 0000000..340f534 Binary files /dev/null and b/z_jupyter/graphics/g_proxy_03.png differ diff --git a/z_jupyter/graphics/g_proxy_04.png b/z_jupyter/graphics/g_proxy_04.png new file mode 100644 index 0000000..522325d Binary files /dev/null and b/z_jupyter/graphics/g_proxy_04.png differ diff --git a/z_jupyter/graphics/g_proxy_05.png b/z_jupyter/graphics/g_proxy_05.png new file mode 100644 index 0000000..402488a Binary files /dev/null and b/z_jupyter/graphics/g_proxy_05.png differ diff --git a/z_jupyter/graphics/g_public_01.png b/z_jupyter/graphics/g_public_01.png new file mode 100644 index 0000000..f557efa Binary files /dev/null and b/z_jupyter/graphics/g_public_01.png differ diff --git a/z_jupyter/graphics/g_public_02.png b/z_jupyter/graphics/g_public_02.png new file mode 100644 index 0000000..1d8b31e Binary files /dev/null and b/z_jupyter/graphics/g_public_02.png differ diff --git a/z_jupyter/graphics/g_public_03.png b/z_jupyter/graphics/g_public_03.png new file mode 100644 index 0000000..75eac0c Binary files /dev/null and b/z_jupyter/graphics/g_public_03.png differ diff --git a/z_jupyter/graphics/g_public_04.png b/z_jupyter/graphics/g_public_04.png new file mode 100644 index 0000000..3521192 Binary files /dev/null and b/z_jupyter/graphics/g_public_04.png differ diff --git a/z_jupyter/graphics/g_public_05.png b/z_jupyter/graphics/g_public_05.png new file mode 100644 index 0000000..c296469 Binary files /dev/null and b/z_jupyter/graphics/g_public_05.png differ diff --git a/z_jupyter/graphics/g_public_06.png b/z_jupyter/graphics/g_public_06.png new file mode 100644 index 0000000..d33a2ef Binary files /dev/null and b/z_jupyter/graphics/g_public_06.png differ diff --git a/z_jupyter/graphics/g_public_07.png b/z_jupyter/graphics/g_public_07.png new file mode 100644 index 0000000..3d87151 Binary files /dev/null and b/z_jupyter/graphics/g_public_07.png differ diff --git a/z_jupyter/graphics/g_public_08.png b/z_jupyter/graphics/g_public_08.png new file mode 100644 index 0000000..9e062d5 Binary files /dev/null and b/z_jupyter/graphics/g_public_08.png differ diff --git a/z_jupyter/graphics/g_public_09.png b/z_jupyter/graphics/g_public_09.png new file mode 100644 index 0000000..7e93337 Binary files /dev/null and b/z_jupyter/graphics/g_public_09.png differ diff --git a/z_jupyter/graphics/g_public_09_02 b/z_jupyter/graphics/g_public_09_02 new file mode 100644 index 0000000..b57a03d Binary files /dev/null and b/z_jupyter/graphics/g_public_09_02 differ diff --git a/z_jupyter/graphics/g_public_09_02.png b/z_jupyter/graphics/g_public_09_02.png new file mode 100644 index 0000000..79847c0 Binary files /dev/null and b/z_jupyter/graphics/g_public_09_02.png differ diff --git a/z_jupyter/graphics/g_public_10.png b/z_jupyter/graphics/g_public_10.png new file mode 100644 index 0000000..567d05a Binary files /dev/null and b/z_jupyter/graphics/g_public_10.png differ diff --git a/z_jupyter/graphics/g_public_11.png b/z_jupyter/graphics/g_public_11.png new file mode 100644 index 0000000..b2d615e Binary files /dev/null and b/z_jupyter/graphics/g_public_11.png differ diff --git a/z_jupyter/graphics/g_public_12.png b/z_jupyter/graphics/g_public_12.png new file mode 100644 index 0000000..1caea1a Binary files /dev/null and b/z_jupyter/graphics/g_public_12.png differ diff --git a/z_jupyter/graphics/g_ring_01.png b/z_jupyter/graphics/g_ring_01.png new file mode 100644 index 0000000..f150206 Binary files /dev/null and b/z_jupyter/graphics/g_ring_01.png differ diff --git a/z_jupyter/graphics/g_ring_02.png b/z_jupyter/graphics/g_ring_02.png new file mode 100644 index 0000000..575186a Binary files /dev/null and b/z_jupyter/graphics/g_ring_02.png differ diff --git a/z_jupyter/graphics/g_sch_01.png b/z_jupyter/graphics/g_sch_01.png new file mode 100644 index 0000000..217e770 Binary files /dev/null and b/z_jupyter/graphics/g_sch_01.png differ diff --git a/z_jupyter/graphics/g_sch_02.png b/z_jupyter/graphics/g_sch_02.png new file mode 100644 index 0000000..0a501ae Binary files /dev/null and b/z_jupyter/graphics/g_sch_02.png differ diff --git a/z_jupyter/graphics/g_sch_03.png b/z_jupyter/graphics/g_sch_03.png new file mode 100644 index 0000000..3719da2 Binary files /dev/null and b/z_jupyter/graphics/g_sch_03.png differ diff --git a/z_jupyter/graphics/g_sig_01.png b/z_jupyter/graphics/g_sig_01.png new file mode 100644 index 0000000..80e3544 Binary files /dev/null and b/z_jupyter/graphics/g_sig_01.png differ diff --git a/z_jupyter/graphics/g_sig_02.png b/z_jupyter/graphics/g_sig_02.png new file mode 100644 index 0000000..7547e1d Binary files /dev/null and b/z_jupyter/graphics/g_sig_02.png differ diff --git a/z_jupyter/graphics/g_sig_03.png b/z_jupyter/graphics/g_sig_03.png new file mode 100644 index 0000000..f0d8689 Binary files /dev/null and b/z_jupyter/graphics/g_sig_03.png differ diff --git a/z_jupyter/graphics/g_sig_04.png b/z_jupyter/graphics/g_sig_04.png new file mode 100644 index 0000000..187faca Binary files /dev/null and b/z_jupyter/graphics/g_sig_04.png differ diff --git a/z_jupyter/graphics/g_sig_05.png b/z_jupyter/graphics/g_sig_05.png new file mode 100644 index 0000000..dec786d Binary files /dev/null and b/z_jupyter/graphics/g_sig_05.png differ diff --git a/z_jupyter/graphics/g_sig_06.png b/z_jupyter/graphics/g_sig_06.png new file mode 100644 index 0000000..84b0693 Binary files /dev/null and b/z_jupyter/graphics/g_sig_06.png differ diff --git a/z_jupyter/graphics/g_sig_07.png b/z_jupyter/graphics/g_sig_07.png new file mode 100644 index 0000000..52835c2 Binary files /dev/null and b/z_jupyter/graphics/g_sig_07.png differ diff --git a/z_jupyter/graphics/g_sig_08.png b/z_jupyter/graphics/g_sig_08.png new file mode 100644 index 0000000..ce59b04 Binary files /dev/null and b/z_jupyter/graphics/g_sig_08.png differ diff --git a/z_jupyter/graphics/g_sig_09.png b/z_jupyter/graphics/g_sig_09.png new file mode 100644 index 0000000..e39f5df Binary files /dev/null and b/z_jupyter/graphics/g_sig_09.png differ diff --git a/z_jupyter/graphics/g_sig_10.png b/z_jupyter/graphics/g_sig_10.png new file mode 100644 index 0000000..0b9c255 Binary files /dev/null and b/z_jupyter/graphics/g_sig_10.png differ diff --git a/z_jupyter/graphics/g_sig_11.png b/z_jupyter/graphics/g_sig_11.png new file mode 100644 index 0000000..cba4db1 Binary files /dev/null and b/z_jupyter/graphics/g_sig_11.png differ diff --git a/z_jupyter/graphics/g_sig_12.png b/z_jupyter/graphics/g_sig_12.png new file mode 100644 index 0000000..7878a09 Binary files /dev/null and b/z_jupyter/graphics/g_sig_12.png differ diff --git a/z_jupyter/graphics/g_sym_01.png b/z_jupyter/graphics/g_sym_01.png new file mode 100644 index 0000000..dd81f7f Binary files /dev/null and b/z_jupyter/graphics/g_sym_01.png differ diff --git a/z_jupyter/graphics/g_sym_02.png b/z_jupyter/graphics/g_sym_02.png new file mode 100644 index 0000000..1fbc01c Binary files /dev/null and b/z_jupyter/graphics/g_sym_02.png differ diff --git a/z_jupyter/graphics/g_sym_03.png b/z_jupyter/graphics/g_sym_03.png new file mode 100644 index 0000000..91a66c9 Binary files /dev/null and b/z_jupyter/graphics/g_sym_03.png differ diff --git a/z_jupyter/graphics/g_sym_04.png b/z_jupyter/graphics/g_sym_04.png new file mode 100644 index 0000000..86ae87f Binary files /dev/null and b/z_jupyter/graphics/g_sym_04.png differ diff --git a/z_jupyter/graphics/g_sym_05.png b/z_jupyter/graphics/g_sym_05.png new file mode 100644 index 0000000..3efa50d Binary files /dev/null and b/z_jupyter/graphics/g_sym_05.png differ diff --git a/z_jupyter/graphics/g_sym_06.png b/z_jupyter/graphics/g_sym_06.png new file mode 100644 index 0000000..b66fecb Binary files /dev/null and b/z_jupyter/graphics/g_sym_06.png differ diff --git a/z_jupyter/graphics/g_sym_07.png b/z_jupyter/graphics/g_sym_07.png new file mode 100644 index 0000000..81b0eb0 Binary files /dev/null and b/z_jupyter/graphics/g_sym_07.png differ diff --git a/z_jupyter/graphics/g_sym_08.png b/z_jupyter/graphics/g_sym_08.png new file mode 100644 index 0000000..62a5953 Binary files /dev/null and b/z_jupyter/graphics/g_sym_08.png differ diff --git a/z_jupyter/graphics/g_sym_12.png b/z_jupyter/graphics/g_sym_12.png new file mode 100644 index 0000000..136487d Binary files /dev/null and b/z_jupyter/graphics/g_sym_12.png differ diff --git a/z_jupyter/graphics/g_sym_13.png b/z_jupyter/graphics/g_sym_13.png new file mode 100644 index 0000000..3b3e0d8 Binary files /dev/null and b/z_jupyter/graphics/g_sym_13.png differ diff --git a/z_jupyter/graphics/g_sym_14.png b/z_jupyter/graphics/g_sym_14.png new file mode 100644 index 0000000..e106e87 Binary files /dev/null and b/z_jupyter/graphics/g_sym_14.png differ diff --git a/z_jupyter/graphics/g_sym_15.png b/z_jupyter/graphics/g_sym_15.png new file mode 100644 index 0000000..e065601 Binary files /dev/null and b/z_jupyter/graphics/g_sym_15.png differ diff --git a/z_jupyter/graphics/g_sys_09.png b/z_jupyter/graphics/g_sys_09.png new file mode 100644 index 0000000..cd76e2b Binary files /dev/null and b/z_jupyter/graphics/g_sys_09.png differ diff --git a/z_jupyter/graphics/g_sys_10.png b/z_jupyter/graphics/g_sys_10.png new file mode 100644 index 0000000..e734438 Binary files /dev/null and b/z_jupyter/graphics/g_sys_10.png differ diff --git a/z_jupyter/graphics/g_sys_11.png b/z_jupyter/graphics/g_sys_11.png new file mode 100644 index 0000000..0f38888 Binary files /dev/null and b/z_jupyter/graphics/g_sys_11.png differ diff --git a/z_jupyter/graphics/g_tokens_01.png b/z_jupyter/graphics/g_tokens_01.png new file mode 100644 index 0000000..96db2d9 Binary files /dev/null and b/z_jupyter/graphics/g_tokens_01.png differ diff --git a/z_jupyter/graphics/g_true_01.png b/z_jupyter/graphics/g_true_01.png new file mode 100644 index 0000000..2481b80 Binary files /dev/null and b/z_jupyter/graphics/g_true_01.png differ diff --git a/z_jupyter/graphics/g_true_02.png b/z_jupyter/graphics/g_true_02.png new file mode 100644 index 0000000..46dd4bc Binary files /dev/null and b/z_jupyter/graphics/g_true_02.png differ diff --git a/z_jupyter/graphics/g_true_03.png b/z_jupyter/graphics/g_true_03.png new file mode 100644 index 0000000..131785c Binary files /dev/null and b/z_jupyter/graphics/g_true_03.png differ diff --git a/z_jupyter/graphics/g_tunnel_01.png b/z_jupyter/graphics/g_tunnel_01.png new file mode 100644 index 0000000..e442e59 Binary files /dev/null and b/z_jupyter/graphics/g_tunnel_01.png differ diff --git a/z_jupyter/graphics/g_tunnel_02.png b/z_jupyter/graphics/g_tunnel_02.png new file mode 100644 index 0000000..7f2d2b2 Binary files /dev/null and b/z_jupyter/graphics/g_tunnel_02.png differ diff --git a/z_jupyter/graphics/g_tunnel_03.png b/z_jupyter/graphics/g_tunnel_03.png new file mode 100644 index 0000000..784097d Binary files /dev/null and b/z_jupyter/graphics/g_tunnel_03.png differ diff --git a/z_jupyter/graphics/g_tunnel_04.png b/z_jupyter/graphics/g_tunnel_04.png new file mode 100644 index 0000000..d20af16 Binary files /dev/null and b/z_jupyter/graphics/g_tunnel_04.png differ diff --git a/z_jupyter/graphics/g_tunnel_05.png b/z_jupyter/graphics/g_tunnel_05.png new file mode 100644 index 0000000..1aab1a7 Binary files /dev/null and b/z_jupyter/graphics/g_tunnel_05.png differ diff --git a/z_jupyter/graphics/g_wrap_01.png b/z_jupyter/graphics/g_wrap_01.png new file mode 100644 index 0000000..f9b0d41 Binary files /dev/null and b/z_jupyter/graphics/g_wrap_01.png differ diff --git a/z_jupyter/graphics/g_wrap_02.png b/z_jupyter/graphics/g_wrap_02.png new file mode 100644 index 0000000..a3e06cf Binary files /dev/null and b/z_jupyter/graphics/g_wrap_02.png differ diff --git a/z_jupyter/graphics/g_yubi_01.png b/z_jupyter/graphics/g_yubi_01.png new file mode 100644 index 0000000..4157f1e Binary files /dev/null and b/z_jupyter/graphics/g_yubi_01.png differ diff --git a/z_jupyter/graphics/g_yubi_02.png b/z_jupyter/graphics/g_yubi_02.png new file mode 100644 index 0000000..8018da2 Binary files /dev/null and b/z_jupyter/graphics/g_yubi_02.png differ diff --git a/z_jupyter/graphics/g_zero-15.png b/z_jupyter/graphics/g_zero-15.png new file mode 100644 index 0000000..b309910 Binary files /dev/null and b/z_jupyter/graphics/g_zero-15.png differ diff --git a/z_jupyter/graphics/g_zero-151.png b/z_jupyter/graphics/g_zero-151.png new file mode 100644 index 0000000..e3ef562 Binary files /dev/null and b/z_jupyter/graphics/g_zero-151.png differ diff --git a/z_jupyter/graphics/g_zero_01.png b/z_jupyter/graphics/g_zero_01.png new file mode 100644 index 0000000..ff7f526 Binary files /dev/null and b/z_jupyter/graphics/g_zero_01.png differ diff --git a/z_jupyter/graphics/g_zero_02.png b/z_jupyter/graphics/g_zero_02.png new file mode 100644 index 0000000..86c080c Binary files /dev/null and b/z_jupyter/graphics/g_zero_02.png differ diff --git a/z_jupyter/graphics/g_zero_03.png b/z_jupyter/graphics/g_zero_03.png new file mode 100644 index 0000000..6b42ac2 Binary files /dev/null and b/z_jupyter/graphics/g_zero_03.png differ diff --git a/z_jupyter/graphics/g_zero_04.png b/z_jupyter/graphics/g_zero_04.png new file mode 100644 index 0000000..dd9ff73 Binary files /dev/null and b/z_jupyter/graphics/g_zero_04.png differ diff --git a/z_jupyter/graphics/g_zero_05.png b/z_jupyter/graphics/g_zero_05.png new file mode 100644 index 0000000..294a727 Binary files /dev/null and b/z_jupyter/graphics/g_zero_05.png differ diff --git a/z_jupyter/graphics/g_zero_06.png b/z_jupyter/graphics/g_zero_06.png new file mode 100644 index 0000000..cd1fbba Binary files /dev/null and b/z_jupyter/graphics/g_zero_06.png differ diff --git a/z_jupyter/graphics/g_zero_07.png b/z_jupyter/graphics/g_zero_07.png new file mode 100644 index 0000000..0a98ecb Binary files /dev/null and b/z_jupyter/graphics/g_zero_07.png differ diff --git a/z_jupyter/graphics/g_zero_08.png b/z_jupyter/graphics/g_zero_08.png new file mode 100644 index 0000000..3b0b1c8 Binary files /dev/null and b/z_jupyter/graphics/g_zero_08.png differ diff --git a/z_jupyter/graphics/g_zero_09.png b/z_jupyter/graphics/g_zero_09.png new file mode 100644 index 0000000..1426bd9 Binary files /dev/null and b/z_jupyter/graphics/g_zero_09.png differ diff --git a/z_jupyter/graphics/g_zero_10.png b/z_jupyter/graphics/g_zero_10.png new file mode 100644 index 0000000..6bf66d3 Binary files /dev/null and b/z_jupyter/graphics/g_zero_10.png differ diff --git a/z_jupyter/graphics/g_zero_11.png b/z_jupyter/graphics/g_zero_11.png new file mode 100644 index 0000000..76bbe42 Binary files /dev/null and b/z_jupyter/graphics/g_zero_11.png differ diff --git a/z_jupyter/graphics/g_zero_11_02.png b/z_jupyter/graphics/g_zero_11_02.png new file mode 100644 index 0000000..6eb0ee8 Binary files /dev/null and b/z_jupyter/graphics/g_zero_11_02.png differ diff --git a/z_jupyter/graphics/g_zero_12.png b/z_jupyter/graphics/g_zero_12.png new file mode 100644 index 0000000..aa6d141 Binary files /dev/null and b/z_jupyter/graphics/g_zero_12.png differ diff --git a/z_jupyter/graphics/hash01.png b/z_jupyter/graphics/hash01.png deleted file mode 100644 index dfd299b..0000000 Binary files a/z_jupyter/graphics/hash01.png and /dev/null differ diff --git a/z_jupyter/graphics/hkdf.png b/z_jupyter/graphics/hkdf.png deleted file mode 100644 index 210c81a..0000000 Binary files a/z_jupyter/graphics/hkdf.png and /dev/null differ diff --git a/z_jupyter/hashing.ipynb b/z_jupyter/hashing.ipynb deleted file mode 100644 index 7c390b8..0000000 --- a/z_jupyter/hashing.ipynb +++ /dev/null @@ -1,717 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "a7782a35", - "metadata": {}, - "source": [ - "# Hashing, HMAC and HKDF\n", - "In this workbook we will use the cryptography library and which integrates the Hazmat primitives. The lab examples use Python 3.7, and cover the concepts of hashing, HMAC and HKDF. This test.\n", - "\n", - "## 1 Hashing\n", - "Within hashing methods, we take data in the form of a byte array, and then create a fixed length hash value. For MD5, the length of the hash is 128 bits, for SHA-1 it is 160 bits, and for SHA-256, it is 256 bits. \n", - "\n", - "\n", - "\n", - "These hashes include MD5, SHA-1 and SHA-256. With MD5 we get a 128-bit output, and which is 32 hex characters. SHA-1 has an output of 160 bits, and SHA-256 has an output of 256 bits. MD5 should not be used in production environments as the method has weaknesses, along with the output hash begin too short. SHA-1, too, has been shown to have weaknesses, and thus we should use SHA-2 methods. These include SHA224, SHA-256, SHA-384 and SHA-512. A newer standard is known as SHA-3. \n", - "\n", - "### 1 MD5 and SHA-1\n", - "In the following we will use the hashing methods supported by the Hazmat primitive. You can change the input string by modifying Line 6, and replacing the string:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "e88c246d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Data: Hello\n", - " Hex: 48656c6c6f\n", - "MD5: 8b1a9953c4611296a827abf8c47804d7 ixqZU8RhEpaoJ6v4xHgE1w==\n", - "\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives import hashes\n", - "import binascii\n", - "import sys\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "st = \"Hello\"\n", - "\n", - "try:\n", - " data=st.encode() # Convert to a byte array\n", - "\n", - " digest = hashes.Hash(hashes.MD5(),backend=default_backend())\n", - " digest.update(data)\n", - " res=digest.finalize()\n", - " hexval=binascii.b2a_hex(res).decode() # hex format\n", - " b64val=binascii.b2a_base64(res).decode() # Base64 format\n", - "\n", - "\n", - " print (\"Data: \",st)\n", - " print (\" Hex: \",binascii.b2a_hex(data).decode())\n", - " print (f\"MD5: {hexval} {b64val}\")\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "f916c2c0", - "metadata": {}, - "source": [ - "#### 1.1 Question\n", - "\n", - "Can you determine the hash value for \"Hello\"?\n", - "\n", - "#### 1.2 Question\n", - "\n", - "Now modify Line 11 in the program below to give SHA1() and also SHA256(). What are the values (list the first two hex characters)?" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "4767a640", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Data: hello\n", - " Hex: 68656c6c6f\n", - "MD5: aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d qvTGHdzF6KLavt4PO0gs2a6pQ00=\n", - "\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives import hashes\n", - "import binascii\n", - "import sys\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "st = \"hello\"\n", - "\n", - "try:\n", - " data=st.encode()\n", - "\n", - " digest = hashes.Hash(hashes.SHA1(),backend=default_backend())\n", - " digest.update(data)\n", - " res=digest.finalize()\n", - " hexval=binascii.b2a_hex(res).decode()\n", - " b64val=binascii.b2a_base64(res).decode()\n", - "\n", - "\n", - " print (\"Data: \",st)\n", - " print (\" Hex: \",binascii.b2a_hex(data).decode())\n", - " print (f\"MD5: {hexval} {b64val}\")\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "ee4c4c0e", - "metadata": {}, - "source": [ - "In the following we integrate a wide range of hashes for both hex and Base64 hashes:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "5b576b95", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Data: hello\n", - " Hex: 68656c6c6f\n", - "\n", - "Blake2p (64 bytes): e4cfa39a3d37be31c59609e807970799caa68a19bfaa15135f165085e01d41a65ba1e1b146aeb6bd0092b49eac214c103ccfa3a365954bbbe52f74a2b3620c94 5M+jmj03vjHFlgnoB5cHmcqmihm/qhUTXxZQheAdQaZboeGxRq62vQCStJ6sIUwQPM+jo2WVS7vlL3Sis2IMlA==\n", - "\n", - "Blake2s (32 bytes): 19213bacc58dee6dbde3ceb9a47cbb330b3d86f8cca8997eb00be456f140ca25 GSE7rMWN7m294865pHy7Mws9hvjMqJl+sAvkVvFAyiU=\n", - "\n", - "MD5: 5d41402abc4b2a76b9719d911017c592 XUFAKrxLKna5cZ2REBfFkg==\n", - "\n", - "SHA1: aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d qvTGHdzF6KLavt4PO0gs2a6pQ00=\n", - "\n", - "SHA224: ea09ae9cc6768c50fcee903ed054556e5bfc8347907f12598aa24193 6gmunMZ2jFD87pA+0FRVblv8g0eQfxJZiqJBkw==\n", - "\n", - "SHA256: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=\n", - "\n", - "SHA384: 59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f WeF0h3dEjGnea4ANejO7+5/xtGPkQ1TDVTvNucZm+pASWjx5+QOXvfX2oT3oKGhP\n", - "\n", - "SHA3_224: b87f88c72702fff1748e58b87e9141a42c0dbedc29a78cb0d4a5cd81 uH+IxycC//F0jli4fpFBpCwNvtwpp4yw1KXNgQ==\n", - "\n", - "SHA3_256: 3338be694f50c5f338814986cdf0686453a888b84f424d792af4b9202398f392 Mzi+aU9QxfM4gUmGzfBoZFOoiLhPQk15KvS5ICOY85I=\n", - "\n", - "SHA3_384: 720aea11019ef06440fbf05d87aa24680a2153df3907b23631e7177ce620fa1330ff07c0fddee54699a4c3ee0ee9d887 cgrqEQGe8GRA+/Bdh6okaAohU985B7I2MecXfOYg+hMw/wfA/d7lRpmkw+4O6diH\n", - "\n", - "SHA3_512: 75d527c368f2efe848ecf6b073a36767800805e9eef2b1857d5f984f036eb6df891d75f72d9b154518c1cd58835286d1da9a38deba3de98b5a53e5ed78a84976 ddUnw2jy7+hI7Pawc6NnZ4AIBenu8rGFfV+YTwNutt+JHXX3LZsVRRjBzViDUobR2po43ro96YtaU+XteKhJdg==\n", - "\n", - "SHA512: 9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043 m3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQw==\n", - "\n", - "SHA512_224: fe8509ed1fb7dcefc27e6ac1a80eddbec4cb3d2c6fe565244374061c /oUJ7R+33O/CfmrBqA7dvsTLPSxv5WUkQ3QGHA==\n", - "\n", - "SHA512_256: e30d87cfa2a75db545eac4d61baf970366a8357c7f72fa95b52d0accb698f13a 4w2Hz6KnXbVF6sTWG6+XA2aoNXx/cvqVtS0KzLaY8To=\n", - "\n", - "SHAKE128 (64 bytes): 8eb4b6a932f280335ee1a279f8c208a349e7bc65daf831d3021c213825292463c59e22d0fe2c767cd7cacc4df42dd5f6147f0c5c512ecb9b933d14b9cc1b2974 jrS2qTLygDNe4aJ5+MIIo0nnvGXa+DHTAhwhOCUpJGPFniLQ/ix2fNfKzE30LdX2FH8MXFEuy5uTPRS5zBspdA==\n", - "\n", - "SHAKE256 (64 bytes): 1234075ae4a1e77316cf2d8000974581a343b9ebbca7e3d1db83394c30f221626f594e4f0de63902349a5ea5781213215813919f92a4d86d127466e3d07e8be3 EjQHWuSh53MWzy2AAJdFgaNDueu8p+PR24M5TDDyIWJvWU5PDeY5AjSaXqV4EhMhWBORn5Kk2G0SdGbj0H6L4w==\n", - "\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives import hashes\n", - "import binascii\n", - "import sys\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "st = \"hello\"\n", - "hex=False\n", - "showhex=\"No\"\n", - "\n", - "def show_hash(name,type,data):\n", - " digest = hashes.Hash(type,backend=default_backend())\n", - " digest.update(data)\n", - " res=digest.finalize()\n", - " hex=binascii.b2a_hex(res).decode()\n", - " b64=binascii.b2a_base64(res).decode()\n", - " print (f\"{name}: {hex} {b64}\")\n", - "\n", - "if (showhex==\"yes\"): hex=True\n", - "\n", - "try:\n", - "\tif (hex==True): data = binascii.a2b_hex(st)\n", - "\telse: data=st.encode()\n", - "\n", - "\n", - "\tprint (\"Data: \",st)\n", - "\tprint (\" Hex: \",binascii.b2a_hex(data).decode())\n", - "\tprint()\n", - "\n", - "\tshow_hash(\"Blake2p (64 bytes)\",hashes.BLAKE2b(64),data)\n", - "\tshow_hash(\"Blake2s (32 bytes)\",hashes.BLAKE2s(32),data)\n", - "\tshow_hash(\"MD5\",hashes.MD5(),data)\n", - "\tshow_hash(\"SHA1\",hashes.SHA1(),data)\t\n", - "\tshow_hash(\"SHA224\",hashes.SHA224(),data)\n", - "\tshow_hash(\"SHA256\",hashes.SHA256(),data)\n", - "\tshow_hash(\"SHA384\",hashes.SHA384(),data)\n", - "\tshow_hash(\"SHA3_224\",hashes.SHA3_224(),data)\n", - "\tshow_hash(\"SHA3_256\",hashes.SHA3_256(),data)\n", - "\tshow_hash(\"SHA3_384\",hashes.SHA3_384(),data)\n", - "\tshow_hash(\"SHA3_512\",hashes.SHA3_512(),data)\n", - "\tshow_hash(\"SHA512\",hashes.SHA512(),data)\n", - "\tshow_hash(\"SHA512_224\",hashes.SHA512_224(),data)\n", - "\tshow_hash(\"SHA512_256\",hashes.SHA512_256(),data)\n", - "\tshow_hash(\"SHAKE128 (64 bytes)\",hashes.SHAKE128(64),data)\n", - "\tshow_hash(\"SHAKE256 (64 bytes)\",hashes.SHAKE256(64),data)\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "36c0aa30", - "metadata": {}, - "source": [ - "#### 1.3 Question\n", - "\n", - "In this case the input data is \"00\". Can you run the program again, and this time use the data input of \"The quick brown fox jumps over the lazy dog\". Prove that:\n", - "\n", - "* MD5 hash value is \"9e107d9d372bb6826bd81d3542a419d6\"\n", - "* SHA-1 hash value is \"2fd4e1c67a2d28fced849ee1bb76e7391b93eb12\"\n", - "* SHA-256 hash value is \"d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592\"\n", - "\n", - "#### 1.4 Question \n", - "\n", - "How many hex characters does MD5, SHA-1 and SHA-256, and how would you determine number of characters used?" - ] - }, - { - "cell_type": "markdown", - "id": "2cc9cf56", - "metadata": {}, - "source": [ - "## 2 HMAC (Hash-based Message Authentication Code)\n", - "HMAC (hash-based message authentication code) supports the usage of a key to hash data. This key is kept secret between Bob and Alice, and can be used to authentication both the data and that the sender still knows the secret. Overall HMAC can be used with a range of different hashing methods, such as MD5, SHA-1, SHA-256 (SHA-2) and SHA-3. \n", - "\n", - "\n", - "\n", - "In this following, we implement HMAC with the MD5 hashing method:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "b5e91a0a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Data: The quick brown fox jumps over the lazy dog\n", - " Hex: 54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67\n", - "Key: b'key'\n", - " Hex: 6b6579\n", - "\n", - "HMAC-MD5: 80070713463e7749b90c2dc24911e275 gAcHE0Y+d0m5DC3CSRHidQ==\n", - "\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives import hashes, hmac\n", - "import binascii\n", - "import sys\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "st = \"The quick brown fox jumps over the lazy dog\"\n", - "\n", - "k=\"key\"\n", - "data=st.encode() # convert to byte array\n", - "key=k.encode() # convert to byte array\n", - "\n", - "print (\"Data: \",st)\n", - "print (\" Hex: \",binascii.b2a_hex(data).decode())\n", - "print (\"Key: \",key)\n", - "print (\" Hex: \",binascii.b2a_hex(key).decode())\n", - "print()\n", - "try:\n", - " digest = hmac.HMAC(key, hashes.MD5(),backend=default_backend())\n", - " digest.update(data)\n", - " res=digest.finalize()\n", - " hexval=binascii.b2a_hex(res).decode()\n", - " b64=binascii.b2a_base64(res).decode()\n", - " print (f\"HMAC-MD5: {hexval} {b64}\")\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "1fffdcae", - "metadata": {}, - "source": [ - "#### 2.1 Question\n", - "By modifying Line 18, determine the first two hex characters for a message of \"qwerty123\" for the following methods:\n", - " \n", - "* HMAC-MD5: \n", - "* HMAC-SHA1: \n", - "* HMAC-SHA224: \n", - "* HMAC-SHA256: \n", - "* HMAC-SHA512: \n", - "\n", - "The following outlines the main HMAC methods that can be implemented within the cryptography library. You can either enter as a hexademical string or an ASCII string, and the output is displayed in a hexademical and a Base-64 format." - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "7e78337d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Data: The quick brown fox jumps over the lazy dog\n", - " Hex: 54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67\n", - "Key: key\n", - " Hex: 6b6579\n", - "\n", - "HMAC-Blake2p (64 bytes): 92294f92c0dfb9b00ec9ae8bd94d7e7d8a036b885a499f149dfe2fd2199394aaaf6b8894a1730cccb2cd050f9bcf5062a38b51b0dab33207f8ef35ae2c9df51b kilPksDfubAOya6L2U1+fYoDa4haSZ8Unf4v0hmTlKqva4iUoXMMzLLNBQ+bz1Bio4tRsNqzMgf47zWuLJ31Gw==\n", - "\n", - "HMAC-Blake2s (32 bytes): f93215bb90d4af4c3061cd932fb169fb8bb8a91d0b4022baea1271e1323cd9a0 +TIVu5DUr0wwYc2TL7Fp+4u4qR0LQCK66hJx4TI82aA=\n", - "\n", - "HMAC-MD5: 80070713463e7749b90c2dc24911e275 gAcHE0Y+d0m5DC3CSRHidQ==\n", - "\n", - "HMAC-SHA1: de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9 3nybhbi3iqa8ino29wqQcBydtNk=\n", - "\n", - "HMAC-SHA224: 88ff8b54675d39b8f72322e65ff945c52d96379988ada25639747e69 iP+LVGddObj3IyLmX/lFxS2WN5mIraJWOXR+aQ==\n", - "\n", - "HMAC-SHA256: f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8 97yD9DBThCSxMpjmqm+xQ+9NWaFJRhdZl0edvC0aPNg=\n", - "\n", - "HMAC-SHA384: d7f4727e2c0b39ae0f1e40cc96f60242d5b7801841cea6fc592c5d3e1ae50700582a96cf35e1e554995fe4e03381c237 1/RyfiwLOa4PHkDMlvYCQtW3gBhBzqb8WSxdPhrlBwBYKpbPNeHlVJlf5OAzgcI3\n", - "\n", - "HMAC-SHA3_224: ff6fa8447ce10fb1efdccfe62caf8b640fe46c4fb1007912bf85100f /2+oRHzhD7Hv3M/mLK+LZA/kbE+xAHkSv4UQDw==\n", - "\n", - "HMAC-SHA3_256: 8c6e0683409427f8931711b10ca92a506eb1fafa48fadd66d76126f47ac2c333 jG4Gg0CUJ/iTFxGxDKkqUG6x+vpI+t1m12Em9HrCwzM=\n", - "\n", - "HMAC-SHA3_384: aa739ad9fcdf9be4a04f06680ade7a1bd1e01a0af64accb04366234cf9f6934a0f8589772f857681fcde8acc256091a2 qnOa2fzfm+SgTwZoCt56G9HgGgr2SsywQ2YjTPn2k0oPhYl3L4V2gfzeiswlYJGi\n", - "\n", - "HMAC-SHA3_512: 237a35049c40b3ef5ddd960b3dc893d8284953b9a4756611b1b61bffcf53edd979f93547db714b06ef0a692062c609b70208ab8d4a280ceee40ed8100f293063 I3o1BJxAs+9d3ZYLPciT2ChJU7mkdWYRsbYb/89T7dl5+TVH23FLBu8KaSBixgm3AgirjUooDO7kDtgQDykwYw==\n", - "\n", - "HMAC-SHA512: b42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a tCrwkFe6weLUFwjkipAuCbX/fxKrQopP6GZTxz3SSPuC+UilSfe3kaW0GRXuTR7Dk1NX5OIxclDQNyr6Lr7rOg==\n", - "\n", - "HMAC-SHA512_224: a1afb4f708cb63570639195121785ada3dc615989cc3c73f38e306a3 oa+09wjLY1cGORlRIXha2j3GFZicw8c/OOMGow==\n", - "\n", - "HMAC-SHA512_256: 7fb65e03577da9151a1016e9c2e514d4d48842857f13927f348588173dca6d89 f7ZeA1d9qRUaEBbpwuUU1NSIQoV/E5J/NIWIFz3KbYk=\n", - "\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives import hashes, hmac\n", - "import binascii\n", - "import sys\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "st = \"The quick brown fox jumps over the lazy dog\"\n", - "hex=False\n", - "showhex=\"No\"\n", - "k=\"key\"\n", - "\n", - "def show_hmac(name,type,data,key):\n", - " digest = hmac.HMAC(key, type,backend=default_backend())\n", - " digest.update(data)\n", - " res=digest.finalize()\n", - " hex=binascii.b2a_hex(res).decode()\n", - " b64=binascii.b2a_base64(res).decode()\n", - " print (f\"HMAC-{name}: {hex} {b64}\")\n", - "\n", - "if (showhex==\"yes\"): hex=True\n", - "\n", - "\n", - "try:\n", - "\tif (hex==True): data = binascii.a2b_hex(st)\n", - "\telse: data=st.encode()\n", - "\n", - "\tif (hex==True): key = binascii.a2b_hex(k)\n", - "\telse: key=k.encode()\n", - "\n", - "\tprint (\"Data: \",st)\n", - "\tprint (\" Hex: \",binascii.b2a_hex(data).decode())\n", - "\tprint (\"Key: \",k)\n", - "\tprint (\" Hex: \",binascii.b2a_hex(key).decode())\n", - "\tprint()\n", - "\n", - "\tshow_hmac(\"Blake2p (64 bytes)\",hashes.BLAKE2b(64),data,key)\n", - "\tshow_hmac(\"Blake2s (32 bytes)\",hashes.BLAKE2s(32),data,key)\n", - "\tshow_hmac(\"MD5\",hashes.MD5(),data,key)\n", - "\tshow_hmac(\"SHA1\",hashes.SHA1(),data,key)\t\n", - "\tshow_hmac(\"SHA224\",hashes.SHA224(),data,key)\n", - "\tshow_hmac(\"SHA256\",hashes.SHA256(),data,key)\n", - "\tshow_hmac(\"SHA384\",hashes.SHA384(),data,key)\n", - "\tshow_hmac(\"SHA3_224\",hashes.SHA3_224(),data,key)\n", - "\tshow_hmac(\"SHA3_256\",hashes.SHA3_256(),data,key)\n", - "\tshow_hmac(\"SHA3_384\",hashes.SHA3_384(),data,key)\n", - "\tshow_hmac(\"SHA3_512\",hashes.SHA3_512(),data,key)\n", - "\tshow_hmac(\"SHA512\",hashes.SHA512(),data,key)\n", - "\tshow_hmac(\"SHA512_224\",hashes.SHA512_224(),data,key)\n", - "\tshow_hmac(\"SHA512_256\",hashes.SHA512_256(),data,key)\n", - "\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "6330873e", - "metadata": {}, - "source": [ - "#### 2.2 Question\n", - "Outline the first two hex characters for a message of \"hello123\" and a key of \"qwerty\":\n", - " \n", - "* HMAC-MD5: \n", - "* HMAC-SHA1: \n", - "* HMAC-SHA224: \n", - "* HMAC-SHA256: \n", - "* HMAC-SHA512: " - ] - }, - { - "cell_type": "raw", - "id": "81f85e76", - "metadata": {}, - "source": [ - "## 3 HKDF (HMAC Key Derivation Function)\n", - "HKDF (HMAC Key Derivation Function) is used to generate an encryption key based on a password. In this case, Bob and Alice will know the password, and then be able to generate the same encryption key. The method uses a given hashing method (such as SHA-256):\n", - "\n", - "\n", - "\n", - "In the following we will create a 16 byte encryption key (128 bits) from a password of \"test\":" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "63f96ea9", - "metadata": {}, - "outputs": [], - "source": [ - "from cryptography.hazmat.primitives import hashes\n", - "from cryptography.hazmat.primitives.kdf.hkdf import HKDF\n", - "\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "\n", - "import binascii\n", - "import sys\n", - "\n", - "st = \"test\"\n", - "hex=False\n", - "showhex=\"No\"\n", - "k=\"00\"\n", - "length=16\n", - "slt=\"\"\n", - "\n", - "\n", - "try:\n", - "\n", - "\n", - " data=st.encode()\n", - " salt=slt.encode()\n", - "\n", - " print (\"Key: \",st)\n", - " print (\" Hex: \",binascii.b2a_hex(data).decode())\n", - " print (\"Salt: \",slt)\n", - " print (\" Hex: \",binascii.b2a_hex(salt).decode())\n", - "\n", - " print()\n", - " \n", - " hkdf = HKDF(algorithm=hashes.SHA256(), length=length,salt=salt, info=b\"\",backend=default_backend())\n", - " mykey=hkdf.derive(data)\n", - " hexval=binascii.b2a_hex(mykey).decode()\n", - " b64=binascii.b2a_base64(mykey).decode()\n", - " print (f\"HKDF-SHA256: {hexval} {b64}\")\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "27c108bd", - "metadata": {}, - "source": [ - "#### 3.1 Question\n", - "Can you modify the program above so that it generates a 256-bit key?\n", - "\n", - "#### 3.2 Question\n", - "Can you modify the program to integrate a 256-bit key and using SHA-512?\n", - "\n", - "#### 3.3 Question\n", - "Can you add a salt value of \"abc\", with a 256-bit key and using SHA-256? What are the first two hex characters of the key?\n", - "\n", - "\n", - "### Scrypt, PBKDF\n", - "In this case we will use a range of hashing methods, and derive a key of a given size. While HKDF is a fast method, we can use a slower hashing method such as Scrypt to PBKDF2. These slower methods can defeat GPU cracking, and will be more robust against dictionary cracking. For the following, we will generate keys for a range of hashing methods:" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "id": "144316a0", - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Key: test\n", - " Hex: 74657374\n", - "Salt: \n", - " Hex: \n", - "\n", - "HKDF Blake2p (64 bytes): 8f372bc847c2940843f8086d4dbf5acf jzcryEfClAhD+AhtTb9azw==\n", - "\n", - "HKDF Blake2s (32 bytes): 5bca0d548d6e85aafc2fbbec5a8093f5 W8oNVI1uhar8L7vsWoCT9Q==\n", - "\n", - "HKDF MD5: c86c155b5dc28eddb37a95205a58662e yGwVW13Cjt2zepUgWlhmLg==\n", - "\n", - "HKDF SHA1: a1dd66f74543371850b7a82d91530c7d od1m90VDNxhQt6gtkVMMfQ==\n", - "\n", - "HKDF SHA224: 9b9dada7f6c2b48040ac2fa9677b1a27 m52tp/bCtIBArC+pZ3saJw==\n", - "\n", - "HKDF SHA256: 578aa064bafda09ccd91c44698ae2537 V4qgZLr9oJzNkcRGmK4lNw==\n", - "\n", - "HKDF SHA384: c5ef81301332c2adccdc102e5679de43 xe+BMBMywq3M3BAuVnneQw==\n", - "\n", - "HKDF SHA3_224: ce525a89491a2de63946afcb681f35fe zlJaiUkaLeY5Rq/LaB81/g==\n", - "\n", - "HKDF SHA3_256: dbca097bd653c24d7dcd35cf25e2de27 28oJe9ZTwk19zTXPJeLeJw==\n", - "\n", - "HKDF SHA3_384: fa44e24b8e3e3de669ac7d6a5fc39161 +kTiS44+PeZprH1qX8ORYQ==\n", - "\n", - "HKDF SHA3_512: ac0d3f9cc5bc50948f3ae4962968b6c6 rA0/nMW8UJSPOuSWKWi2xg==\n", - "\n", - "HKDF SHA512: 818e212ef59c12d7102669739686d23a gY4hLvWcEtcQJmlzlobSOg==\n", - "\n", - "HKDF SHA512_224: 647a9fb1447253c51200b3b404ed92a3 ZHqfsURyU8USALO0BO2Sow==\n", - "\n", - "HKDF SHA512_256: b650b8cecab8b9f06326cae522f12014 tlC4zsq4ufBjJsrlIvEgFA==\n", - "\n", - "HKDF PBKDF2: 191907ba9a408d2851cd0fa68c2f264e GRkHuppAjShRzQ+mjC8mTg==\n", - "\n", - "HKDF Scrypt SHA256: a7af3411ca083c545d0760f8307f61e9 p680EcoIPFRdB2D4MH9h6Q==\n", - "\n", - "HKDF Concat SHA256: a8f595533ecd6cb27b6695d825f4df3a qPWVUz7NbLJ7ZpXYJfTfOg==\n", - "\n" - ] - } - ], - "source": [ - "from cryptography.hazmat.primitives import hashes\n", - "from cryptography.hazmat.primitives.kdf.hkdf import HKDF\n", - "from cryptography.hazmat.primitives.kdf.scrypt import Scrypt\n", - "from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC\n", - "from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash,ConcatKDFHMAC\n", - "from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF\n", - "from cryptography.hazmat.backends import default_backend\n", - "\n", - "\n", - "import binascii\n", - "import sys\n", - "\n", - "st = \"test\"\n", - "hex=False\n", - "showhex=\"No\"\n", - "k=\"00\"\n", - "length=16\n", - "slt=\"\"\n", - "\n", - "def show_hash(name,type,data,length,salt):\n", - "\n", - " hkdf = HKDF(algorithm=type, length=length,salt=salt, info=b\"\",backend=default_backend())\n", - " mykey=hkdf.derive(data)\n", - " hex=binascii.b2a_hex(mykey).decode()\n", - " b64=binascii.b2a_base64(mykey).decode()\n", - " print (f\"HKDF {name}: {hex} {b64}\")\n", - "\n", - "def show_hash_pbkdf2(name,type,data,length,salt):\n", - "\n", - " hkdf = PBKDF2HMAC(algorithm=type, length=length,salt=salt, iterations=1000,backend=default_backend())\n", - " mykey=hkdf.derive(data)\n", - " hex=binascii.b2a_hex(mykey).decode()\n", - " b64=binascii.b2a_base64(mykey).decode()\n", - " print (f\"HKDF {name}: {hex} {b64}\")\n", - "\n", - "def show_hash_scrypt(name,data,length,salt):\n", - "\n", - " hkdf = Scrypt(length=length,salt=salt,n=2**14,r=8, p=1,backend=default_backend())\n", - " mykey=hkdf.derive(data)\n", - " hex=binascii.b2a_hex(mykey).decode()\n", - " b64=binascii.b2a_base64(mykey).decode()\n", - " print (f\"HKDF {name}: {hex} {b64}\")\n", - "\n", - "def show_hash_concat(name,type,data,length,salt):\n", - "\n", - " hkdf = ConcatKDFHash(algorithm=type, length=length,otherinfo=b\"\",backend=default_backend())\n", - " mykey=hkdf.derive(data)\n", - " hex=binascii.b2a_hex(mykey).decode()\n", - " b64=binascii.b2a_base64(mykey).decode()\n", - " print (f\"HKDF {name}: {hex} {b64}\")\n", - "\n", - "\n", - "if (showhex==\"yes\"): hex=True\n", - "\n", - "\n", - "\n", - "try:\n", - "\tif (hex==True): data = binascii.a2b_hex(st)\n", - "\telse: data=st.encode()\n", - "\tif (hex==True): salt = binascii.a2b_hex(slt)\n", - "\telse: salt=slt.encode()\n", - "\n", - "\n", - "\tprint (\"Key: \",st)\n", - "\tprint (\" Hex: \",binascii.b2a_hex(data).decode())\n", - "\n", - "\tprint (\"Salt: \",slt)\n", - "\tprint (\" Hex: \",binascii.b2a_hex(salt).decode())\n", - "\n", - "\tprint()\n", - "\n", - "\n", - "\tshow_hash(\"Blake2p (64 bytes)\",hashes.BLAKE2b(64),data,length,salt)\n", - "\tshow_hash(\"Blake2s (32 bytes)\",hashes.BLAKE2s(32),data,length,salt)\n", - "\tshow_hash(\"MD5\",hashes.MD5(),data,length,salt)\n", - "\tshow_hash(\"SHA1\",hashes.SHA1(),data,length,salt)\t\n", - "\tshow_hash(\"SHA224\",hashes.SHA224(),data,length,salt)\n", - "\tshow_hash(\"SHA256\",hashes.SHA256(),data,length,salt)\n", - "\tshow_hash(\"SHA384\",hashes.SHA384(),data,length,salt)\n", - "\tshow_hash(\"SHA3_224\",hashes.SHA3_224(),data,length,salt)\n", - "\tshow_hash(\"SHA3_256\",hashes.SHA3_256(),data,length,salt)\n", - "\tshow_hash(\"SHA3_384\",hashes.SHA3_384(),data,length,salt)\n", - "\tshow_hash(\"SHA3_512\",hashes.SHA3_512(),data,length,salt)\n", - "\tshow_hash(\"SHA512\",hashes.SHA512(),data,length,salt)\n", - "\tshow_hash(\"SHA512_224\",hashes.SHA512_224(),data,length,salt)\n", - "\tshow_hash(\"SHA512_256\",hashes.SHA512_256(),data,length,salt)\n", - "\tshow_hash_pbkdf2(\"PBKDF2\",hashes.SHA256(),data,length,salt)\n", - "\tshow_hash_scrypt(\"Scrypt SHA256\",data,length,salt)\n", - "\tshow_hash_concat(\"Concat SHA256\",hashes.SHA256(),data,length,salt)\n", - "\n", - "except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "id": "f5179f57", - "metadata": {}, - "source": [ - "#### 3.4 Question\n", - "For a key of \"hello\", what is the first two characters for the following methods:\n", - "\n", - "* HKDF SHA512_256\n", - "* HKDF PBKDF2\n", - "* HKDF Scrypt SHA256\n", - "\n", - "#### 3.5 Question\n", - "For a key of \"hello\" and a salt value of \"xyz123\", what is the first two characters for the following methods:\n", - "\n", - "* HKDF SHA512_256\n", - "* HKDF PBKDF2\n", - "* HKDF Scrypt SHA256" - ] - }, - { - "cell_type": "markdown", - "id": "9154359d", - "metadata": {}, - "source": [] - } - ], - "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.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/z_jupyter/others/13_pqc.ipynb b/z_jupyter/others/13_pqc.ipynb new file mode 100644 index 0000000..2537957 --- /dev/null +++ b/z_jupyter/others/13_pqc.ipynb @@ -0,0 +1,149 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7782a35", + "metadata": {}, + "source": [ + "\n", + "# Tunnelling and Web Security\n", + "Objective: In this lab we will investigate the usage of SSL/TLS and VPN tunnftels.\n", + "# Post Quantum Cryptography\n", + "\n", + "## Simple Kyber example\n", + "NIST has defined that Kyber will be developed as a standard for PQC (Post Quantum Cryptography) key exchange and public key encryption. The main methods are Kyber512 (128-bit AES security level equivalent), Kyber768 (192-bit AES security level equivalent) and Kyber1024 (256-bit AES security level equivalent. In Kyber we modify the normal LWE (Learning With Errors) approach and use MLWE (Module Learning With Errors). With MLWE we use vectors of polynomials [2]. In this case, we will implement a very simple Kyber example and generate a public key and a private key, and then encrypt a message [1]. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "11f4d2ea", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 3 2\n", + "2 x + 3 x + 10 x + 3\n", + " 4 2\n", + "3 x + 14 x + 10 x + 4\n", + " 4 3\n", + "3 x + 2 x + 3 x + 7\n", + " 7 6 5 4 3 2\n", + "6 x + 9 x + 7 x + 3 x + 8 x + 1 x + 2 x + 12\n", + " 4 3 2\n", + "14 x + 2 x + 6 x + 16\n", + " 3 2\n", + "2 x + 3 x + 10 x + 3\n" + ] + } + ], + "source": [ + " import numpy as np\n", + " import sys\n", + " import numpy\n", + "\n", + " q=17\n", + "\n", + " a= [2,3,10,3] # 2x^4+3x^3+10x+3\n", + " b= [3,0,14,10,4] # 3x^5+14x^3+10x+4\n", + "\n", + " print (np.poly1d(a))\n", + " print (np.poly1d(b))\n", + "\n", + " r1 = np.floor( np.polyadd(a,b)) % q # Polynomial add\n", + " r2 = np.floor( np.polymul(a,b)) % q # Polynomial multiply\n", + " r3 = np.floor( np.polysub(a,b)) % q # Polynomial subtract\n", + " r4 = np.floor( np.polydiv(a,b)[1]) % q # Polynomial divide with remainder\n", + "\n", + "\n", + " print (np.poly1d(r1))\n", + " print (np.poly1d(r2))\n", + " print (np.poly1d(r3))\n", + " print (np.poly1d(r4))" + ] + }, + { + "cell_type": "markdown", + "id": "053856e3", + "metadata": {}, + "source": [ + "And a simple example:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "5ad4ee5a", + "metadata": {}, + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'code.polynomials'; 'code' is not a package", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[8], line 2\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[38;5;66;03m# Based on https://github.com/jack4818/kyber-py\u001b[39;00m\n\u001b[1;32m----> 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mcode\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mpolynomials\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n\u001b[0;32m 3\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mcode\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmodules\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n\u001b[0;32m 5\u001b[0m R \u001b[38;5;241m=\u001b[39m PolynomialRing(\u001b[38;5;241m17\u001b[39m, \u001b[38;5;241m4\u001b[39m)\n", + "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'code.polynomials'; 'code' is not a package" + ] + } + ], + "source": [ + "# Based on https://github.com/jack4818/kyber-py\n", + "from code.polynomials import *\n", + "from code.modules import *\n", + "\n", + "R = PolynomialRing(17, 4)\n", + "M = Module(R)\n", + "\n", + "s0 = R([0,1,-1,1])\n", + "s1 = R([0,1,0,1])\n", + "s = M([s0,s1]).transpose()\n", + "\n", + "A00 = R([11,16,16,6])\n", + "A01 = R([3,6,4,9])\n", + "A10 = R([1,10,3,5])\n", + "A11 = R([15,9,1,6])\n", + "A = M([[A00, A01],[A10, A11]])\n", + "\n", + "A00 = R([1,6,16,6])\n", + "A01 = R([10,0,5,0])\n", + "A10 = R([4,5,8,3])\n", + "A11 = R([4,2,8,9])\n", + "A = M([[A00, A01],[A10, A11]])\n", + "\n", + "e0 = R([1,0,1,0])\n", + "e1 = R([-1,1,0,0])\n", + "e = M([e0,e1]).transpose()\n", + "\n", + "t = A @ s + e\n", + "\n", + "print(\"A->\",A)\n", + "print(\"t->\",t)" + ] + } + ], + "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.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/others/14_homomorphic.ipynb b/z_jupyter/others/14_homomorphic.ipynb new file mode 100644 index 0000000..2537957 --- /dev/null +++ b/z_jupyter/others/14_homomorphic.ipynb @@ -0,0 +1,149 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7782a35", + "metadata": {}, + "source": [ + "\n", + "# Tunnelling and Web Security\n", + "Objective: In this lab we will investigate the usage of SSL/TLS and VPN tunnftels.\n", + "# Post Quantum Cryptography\n", + "\n", + "## Simple Kyber example\n", + "NIST has defined that Kyber will be developed as a standard for PQC (Post Quantum Cryptography) key exchange and public key encryption. The main methods are Kyber512 (128-bit AES security level equivalent), Kyber768 (192-bit AES security level equivalent) and Kyber1024 (256-bit AES security level equivalent. In Kyber we modify the normal LWE (Learning With Errors) approach and use MLWE (Module Learning With Errors). With MLWE we use vectors of polynomials [2]. In this case, we will implement a very simple Kyber example and generate a public key and a private key, and then encrypt a message [1]. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "11f4d2ea", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 3 2\n", + "2 x + 3 x + 10 x + 3\n", + " 4 2\n", + "3 x + 14 x + 10 x + 4\n", + " 4 3\n", + "3 x + 2 x + 3 x + 7\n", + " 7 6 5 4 3 2\n", + "6 x + 9 x + 7 x + 3 x + 8 x + 1 x + 2 x + 12\n", + " 4 3 2\n", + "14 x + 2 x + 6 x + 16\n", + " 3 2\n", + "2 x + 3 x + 10 x + 3\n" + ] + } + ], + "source": [ + " import numpy as np\n", + " import sys\n", + " import numpy\n", + "\n", + " q=17\n", + "\n", + " a= [2,3,10,3] # 2x^4+3x^3+10x+3\n", + " b= [3,0,14,10,4] # 3x^5+14x^3+10x+4\n", + "\n", + " print (np.poly1d(a))\n", + " print (np.poly1d(b))\n", + "\n", + " r1 = np.floor( np.polyadd(a,b)) % q # Polynomial add\n", + " r2 = np.floor( np.polymul(a,b)) % q # Polynomial multiply\n", + " r3 = np.floor( np.polysub(a,b)) % q # Polynomial subtract\n", + " r4 = np.floor( np.polydiv(a,b)[1]) % q # Polynomial divide with remainder\n", + "\n", + "\n", + " print (np.poly1d(r1))\n", + " print (np.poly1d(r2))\n", + " print (np.poly1d(r3))\n", + " print (np.poly1d(r4))" + ] + }, + { + "cell_type": "markdown", + "id": "053856e3", + "metadata": {}, + "source": [ + "And a simple example:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "5ad4ee5a", + "metadata": {}, + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'code.polynomials'; 'code' is not a package", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[8], line 2\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[38;5;66;03m# Based on https://github.com/jack4818/kyber-py\u001b[39;00m\n\u001b[1;32m----> 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mcode\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mpolynomials\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n\u001b[0;32m 3\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mcode\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmodules\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n\u001b[0;32m 5\u001b[0m R \u001b[38;5;241m=\u001b[39m PolynomialRing(\u001b[38;5;241m17\u001b[39m, \u001b[38;5;241m4\u001b[39m)\n", + "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'code.polynomials'; 'code' is not a package" + ] + } + ], + "source": [ + "# Based on https://github.com/jack4818/kyber-py\n", + "from code.polynomials import *\n", + "from code.modules import *\n", + "\n", + "R = PolynomialRing(17, 4)\n", + "M = Module(R)\n", + "\n", + "s0 = R([0,1,-1,1])\n", + "s1 = R([0,1,0,1])\n", + "s = M([s0,s1]).transpose()\n", + "\n", + "A00 = R([11,16,16,6])\n", + "A01 = R([3,6,4,9])\n", + "A10 = R([1,10,3,5])\n", + "A11 = R([15,9,1,6])\n", + "A = M([[A00, A01],[A10, A11]])\n", + "\n", + "A00 = R([1,6,16,6])\n", + "A01 = R([10,0,5,0])\n", + "A10 = R([4,5,8,3])\n", + "A11 = R([4,2,8,9])\n", + "A = M([[A00, A01],[A10, A11]])\n", + "\n", + "e0 = R([1,0,1,0])\n", + "e1 = R([-1,1,0,0])\n", + "e = M([e0,e1]).transpose()\n", + "\n", + "t = A @ s + e\n", + "\n", + "print(\"A->\",A)\n", + "print(\"t->\",t)" + ] + } + ], + "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.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/z_jupyter/others/15_homomorphic_ml.ipynb b/z_jupyter/others/15_homomorphic_ml.ipynb new file mode 100644 index 0000000..a207dfc --- /dev/null +++ b/z_jupyter/others/15_homomorphic_ml.ipynb @@ -0,0 +1,988 @@ +{ + "cells": [ + { + "attachments": { + "g_zero-15.png": { + "image/png": "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55rdbld8fHyTPi7fYK2oqEivvPJKnX/eu3fvbgTRvj3D3G63rr76auN1T29Pby6XS23btpVUFazZbDZT+Uan0ymLxWKU2Tx69KhfYOl93gOFmt4BoMVi0ZYtW8L9bQAAoEX66aefNHnyZM2fP79BtnfaaafpL3/5ix588EHj2hHAyemUcDcAQPPzzV6H37Ibr+ga9mBNkqJbn65ES1fTsv9UHAh3swAAaPLcbrecTme169jt9jpv2+121+m9DcHlcoVln/U95nC0u6Ha73a7ZbfbQzqG7777TlJVyFRRURHyeGiBlJWVmXp4+pZcjIyMNHqTSdLLL7/st40VK1YY06tXr/YbFy0mJkarV6825gsLC/224b3d119/3S98S0xMNHqg2mw2lZeXqyHY7fYaf48bQl1/NsP9fwEA4ORQWVmpDz/8UGPHjtW5555rCtbOPvtspaSk6JFHHtFLL72k1atX65NPPtG///1v7dy5U9u3b9eHH36o4uJiPf/887r//vvVp08ftWnTRpJ05MgRPfzww+rcubOmTp2qPXuolgScrAjXANTagYPf+y274pK4cDfrv225+Nfm9h46HO4mAQBwwqWlpSkiIsKvp5LdbldERIQiIiJkt9tVXl6ufv36KSoqSh06dFD37t1VVlZmrF9eXq7MzExFREQoPj5e3bt3l9VqrfHmusvlUmZmprp3766oqChFRUWpY8eOyszMrPWNec+xeI8b5jmGQE8Mu91uWa1W431t27Y19l1cXFznm/s5OTnGPnNycvxedzqdxjG3bdvWOOacnJygx2y1Wk3HUV5ebmp3WlqaysrKat1mzzY9vackKT093VgeqAeb3W5XWlqaOnbsaLS/e/fu1X7PAv08paWlKSoqSvHx8aagqjoZGRnavHmzX5BVW95B1+DBgwOuExMTo+TkZElSVlaW37l98sknJVX1WgtWrjEhIUGxsbGm9T1cLpdRdjI1NTVoWVbv9r3wwgu1Ok7v32+3263MzEx17NhR8fHxyszMDLheML4/g948y61Wq9xut3Jycoyf744dO2revHkh/V9QXFxs/FxERUWpX79+slqtJyQIBACcfKZNm6YePXrohRde0E8//SRJateunf73f/9X+/fv18qVK/XEE09o9OjR6tevn7p3764uXbooLi5OF198sa6++moNGDBA48eP1zPPPKO1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2E6n84Sej8Z06MeflPqnPE15vkA/Hfkl3M2ptQM/HNaDea/ozunP6ejRo+FuDgAALVJMTIxSU1MlVd3MLysr81vH7XbLYrHIYrEoIiJCbrc75O1794zzhFXehg0bZuw70DXiggULan1McXFxRrUIh8Oh5ORkU5vj4uKUnJwsSSopKQk4DpfT6VSvXr0UHx+vjh071uqYG1v//v2NsGz+/PmyWq2m9rlcLuXk5BgBTGpqapOo3mC322W32/2utxMTE42eZOnp6X4/g2632/h+SdKoUaP8tu09Zkv//v399uF0OtW/f39jPlCvs/vvv9+YHjJkiN/3vKyszChParFYGq034KBBg4zpxx9/3O9ceHrfNYYBAwYYv+uBPhd5f0b1bicAANVZvny5JOmSSy7RuHHjGmUfnodDpKqHbqKioqpdf+jQoZKqeopXVlYayydOnGiMtZaRkaGDBw+G9dwBaFiEay3YyJEjjekNGzaYXktLS1NERES14xBYrVZFRETU2GXZ5XKpuLhY/fr1U9u2bRUREaF+/fpp3rx5tW6z3W439hnoholU9SEwMzNT3bt3V1RUlNq2bWvsL9h76sput9c6WCsvL1daWpo6duyoDh06KCIiQt27d/e7USFVjeHmOd5AN5+89evXz1j3RIeYx44dU+pjeXrnw09P6H4bw+r3t+jVNf8KdzMAAGix5syZY0wnJSWpY8eOxnXavHnzdPXVVxs9foqKimpVttl7TLfBgweruLhYVqvVWDZ58mRjunfv3iouLjb22717d1OJ79pIS0tTRkaGpKrgzjeQ8TzZLFUFJZ5rv/Lycs2bN0/JycnGMS9durRJlaqOjIzU0qVLJVWFh+np6erUqZOsVqtycnLUtm1bU28/72MNF6fTqfj4eMXHxyszM9Pv9eeff96YHj58uDIzM1VcXKycnBxdffXVstlskqqCwkA9CaOjozV37lzjnFgsFuXk5BjnxGKxGN/PuXPnBvx++oauV199tebNmyer1arMzEzTZ4xXXnml0c6Vd1CYl5dn/D7m5OSoU6dOysrKarSeiN6/m8nJybJarbLb7SorK1NmZqbx+xjs+wAAgK/PPvtM7733nqSqh6patWrVKPvxjLMmSddff32N63vKhP/444/69ttvjeWXXHKJca2yd+9evf322+E6dQAaAeFaC+b9RGl1Nfbr48CBAxoyZIhSUlJM5URKSko0adIk9evXr0GfzLXb7erUqZPy8vKMD8Xe+4uPjzd9iKsPzxPGHnPnzq0xWPN82C4oKDA+cEtVN2HS09N19dVXm57aHD16tDFdWFgYdLve4zFkZGQEHTuisSz6R5He3/rvE7rPxjQrf4UOfPd9uJsBAECLFBMTI5vNZtywdzgcxnXapEmTjGu47OzsoGUjg4mMjFR2drakquurlJQUpaenGw9YRUdHm3rOpaSkmPYbGxtrhCa1NXv2bCMsKSgoMD1IFhcXp9LSUuOYPdd+FovFdMyhXE+GQ2JiomksZ0/I5h2qxcbGymaznfDr0EC8r7MD9XhKTExUaWmpsW5eXp5SUlKUlZVl+vmr7nPDxIkTjZ81T6l9zznxDtaqK2X41ltvmX5eJ02apPT0dKNspVT18F5jjmEXHR2toqIiv99Hz3FkZ2dr9uzZjbLvmJgY4+fK8zsRHx+vpKQk0znIzc1ttOMHALQsM2fONKZvv/32RtvPGWecYUyHck/VE8adcsopOvPMM02vTZs2zfg7HKx8OIDmqek8MokG5x3idOvWrVH24f30b2pqqiZMmKDnnnvOqJ1fUlKiyZMnm55griuXy6VevXqZPkxnZ2dr6NChWr58uebPn2/cCOjXr1+9ytW4XC7TE6nZ2dk1jgFgtVpNNyBSU1M1ZcoUrVu3TjNmzJDD4ZDNZlNmZqbxQT46OlrJyckqKSlRXl6eZs+eHfDJV+/xGBp7sPNA/rF+S8Dlfa68VJ07hr8sUDCVldLOvU6VfvK5afke5wE9nV+kmRPS6rhlAABOPO+xvWqzvu/7vAOoYEFJsPd6dO7cudptJCQkaPfu3crNzdX69euNh4QsFovGjh2rPn36qGvXrnU6D1OnTlXfvn1VWFiovLw8WSwW7dmzx+j5YrVaNWXKFNP1YXJysgYPHqyRI0fqiy++MNruy7Pcu/ykR2RkpNHrSKqqDDFy5EjjmjMxMVE7duzQ1KlT9dlnnxnH7Nl3nz59AoYo3ucyGO/vWaC2NcR709LS1K9fPz3++ONau3atEUIlJyfr+uuv19SpUwNep9anbdWp7ufdu/Rmv379Aq6TmJgom82m5cuXa9myZUa4OmLECN1yyy0hBbvTpk3T0KFD9cILLxjnxGKxaNiwYRo6dGiNoVhkZKSmTZuma665RqtWrVJhYaHx83j99dfXa1zsmn5HvQ0cOFC7d+/Wpk2bNH36dJWUlCgjI0O33HKL+vfvr02bNtXpdyKUtqSlpSkhIUHr1q3TihUrjN+L2NhYjR8/XpMnT24SgS0AoOn7+eefjftp7du311VXXdVo+/Iek3batGlBrzc8PNWozj//fJ122mmm19q2bau+ffsqPz9f77zzTrhOH4DGUIlmJz8/v1JSZU3fvrlz5xrrzZ071/RaampqpaTK1NTUOu3Hs9yzjcOHD5te37lzZ2VycrKxzs6dO0Pa/86dO4O+Jzs723gtOzvbb58VFRXG6xaLpfLo0aMhn1PvYzl69Kip7RkZGTW+//Dhw5WxsbHGvn3bdvTo0cqMjAxjm6WlpcZrpaWlxvKioqKA27dYLJWSKmNjY0M+poZy/Pjxyk6/HV95br97jK/zBoytfP/Tf5/wttRV5uzFpvaf2++eyoTfPRjuZgEAcFLxvT5q6futrKys1fVoU9OU2+75XBCOa2PUTzh/HwEAzZv3/bM///nPjb6//v37G/tbtGhR0PXeeuutynbt2lVKqkxLSwu4zksvvVQpqTIiIqKyvLw8bOcQQMOiLGQL5HQ6NW/ePM2YMcNY1pgDRC9atMjvacO4uDhNnz7dmH/22WfrtQ+Xy2X0CouNjdW0adP89hkTE6PS0lJlZ2fXa9yCyZMnm0pcFhYW1ljacvHixUYvt+eff96vbZGRkfrTn/5kzD/33HPGtPfYIYHOk9PpNJ4cfuyxx+p1Huviy2/26ceffzEtG3DdFerZrcsJb0td3TPQ/yna/+w/qK8d39ZhawAAoC7C1TslnL1imtLYai2p7Z9/XlWVYPz48eFuCmqJXmoAgLr65JNPjOlx48Y1+v4KCgp0+umnS5Luu+8+/e1vf/Nbp7CwUAMGDDDKQgbrCd6jRw9JUmVlpRYvXnxiTxyARtN0PzEhJL7jBCxevNgUDElVYwE01gDRqampQT8gXXHFFca0d4nKuvB+f3UfohMTE+s1joWnnKVUVbbIZrPJ4XBo1KhR1Y7JsGHDBklVwZ93WOYtJibG2KZ3vWbP2CFZWVkqKSmRy+UyndP58+cb0yNHjqzXeayLQz/+5Lfs3LPOqMOWwufcs9sFXP794R/D3TQAAADUkueavW/fvuFuCgAAOEE8D7XHxMToV7/6VaPv75xzztETTzyh+++/X0ePHtXo0aP13HPPqUePHoqIiNBnn31m3A887bTT9Oijj2rAgAEBt3XJJZfo7LPP1sGDB/Xmm2822ninAE4seq41c+np6aYv32AtPz+/xrHC6qO6Gv/R0dGyWCySzKFVXXz22WfG9In4EJ2dna3Nmzeb2l9cXBx0fU9Y5nA4FBUVpYiIiIBfnh5onn89hg4dakz7PsHiCdeSk5PrNY4cAAAA0Nx5V5TwfpgPAAC0bJ5w7bzzzjth+8zMzNTKlSvVunVrSdKHH36o+fPn6/nnn9fatWv1yy9VlZ6eeOIJTZ8+Xa1atQq4naioKKMzwL///W8dP348nKcSQAOh51ozF6i78aWXXqouXbpo8ODBjV5245xzzqn29RtvvNEvSKoLT/dqqWpw0MaUmpqqadOmSZJKSkrUoUMHSVJKSooqKioCBlx1OUa73W70KExISDB6tS1atMgIRMvLy42Lh/vvv79RjxsAAABo6o4cOaL8/Hy1a9eOEoMAAJxE9u3bJ+nEhmuSNHDgQNlsNi1btkyffPKJ/vOf/+j48eNq3769rrnmGg0ZMkTdunWrcTtJSUlauXKlJOnYsWM65RT6vADNHeFaM1ddqcITYceOHdW+vnbt2gbZT7t27YzpPXv2NFqZS4vFopdfftmYj4mJUX5+vtLT0yVJaWlpeuutt/zGoPAEYxaLRStWrAhpXxdccIFp/pFHHlF6erpsNpucTqdiYmK0fPly4/X+/fs3yjGH29d7/qMn84tV8sGnOvDDYUVESBf/+le6o991GnnjtYqNaR/uJgIAAKCJiI6OVlpaWribAQAATrDvv/9eknTGGSd+qJIuXbrokUceqfc2POi5BrQMROQnOe+xv3w9+eSTNb5//fr1QV9zuVxGj65gA3qG6rLLLjOm33nnnUY7H926dfMLztLS0oz2l5SUaMGCBQHfJ1X1YIuJiVFcXFyNX777GTx4sDG9ZMkSSVJWVpakqjKVTXlQ+braZt+jXvfmaEnJ+9r//WFVVkrHj0uf2/+jvyx6TdeP+4u+P3Q43M0EAAAAAAAh8i7hCzSUY8eOSZKOHj0a7qbUSWxsrDFdWVkZ7uYAaACEayc5Ty8pX06nM6RShyUlJXK5XAFfW7dunTFd37HCvN/vGYMsEKvVqrS0NBUXFzfoxdyiRYuMP4KTJk1SeXm56XXvseeq67lWXl4e8HxLVU/hekK8FStWmPbhPSZbS/LIPKuOHA3+fTp4yKWS9zaEu5kAAAAAADRbZWVlslqtQb/sdnvQezu15XQ61alTJ5WVlYX7sJsEu91unOeazrHT6TR9X4LdPzpZRUVFSWq+4W1LfGgeONkRrp2kvMMg37DK7XZXG2D5GjJkiOx2u2lZWVmZpkyZYszXd7yw6OhoZWdnS6oawDQnJ8fvosTpdCo9PV0FBQWmfTeE6OhorV692pjv37+/6Y/56NGjjfDtySefDHgBVFZWJovFog4dOgQtZTNhwgRJVaHlzJkzJVWVnExISGjQ42kKDn7/gzbYtte4nm33wXA3FQAAAACAZuu5555Tenp60K/4+Hi1bdtWmZmZ9Q7ZkpOT5XA4lJSU1GQCNrvd3qABYm1s2LDBOM/VhWVut1tpaWnGuosXL673g+otjWes1XB8HxvCjz/+aExHRESEuzkAGgDh2klq5MiRxnRWVpY6duyoefPmKScnR1FRUcrKypLFYqlxO7GxsSopKVF8fLzS0tJUVlam7t27Kykpyej5lp2d3SBjpE2ePNkIsLKystS2bVvl5OSorKxMOTk5pvaWlJQ0+BMhCQkJpoBv1KhRxmvR0dGaPXu2pKregB06dFC/fv1UVlZmtG/48OHGOVu0aFHAffTs2dM4xoKCAknS2LFjG/Q4moqv9+wNab1dFfvD3VQAAAAAAFq8vLw8denSpV49pkpKShQbG6uioiIlJiaG+5AkSfHx8YqPj6+20lA4ud1uDRgwQCUlJZKqAsq33nor3M1qcjp27ChJ2r17d7ibUife7SZcA1oGwrWTVExMjPLz8415h8OhSZMmmcb4CmWgzscee0zJycmSqsIg71BNqhprberUqQ3S5ujoaG3cuNFUozgrK0tJSUnKysqSw+GQJBUVFTXa0z1Tp041QryCggIVFxcbr6WlpWnu3LnGfElJiZKSkvzat3r1auNpG1+RkZEaP368adno0aMb5VjC7fTTTg1tvf/r9g8AAAAAAOouNTVVlZWVpq+KigrNnTvXuNfhcDiUmZlZ533ExMRo9+7dGjhwYLgPt1kIFqxRQtCf58H9PXv26Pvvvw93c2rtk08+MaZbtWoV7uYAaACEa81Q586dlZqaaozPVVdpaWk6evSoSktLlZycrNjYWGVkZKioqEjTpk2rdj+e5VdeeaXeeustFRUVGSGbVHUxkJ+fL6vVGvCCoHfv3kpNTTWVp5T+O+5YampqwAAqLi5Ou3fvVnZ2dsD9VVRU1OkCzrNP3/b4ioyMVElJibH+K6+8YioPOXHiRO3cuVOpqammnnSpqalG+2oq8ejdIy7YeWgJLugYq1NU85M653c4J9xNBQAAAACgRYqJidHEiRO1ZcsW08PEvsN/1AbBUOgmT55MsBYiT7jmdru1fv36cDen1t577z1j+pRTuCUPtAT8b90MJSYmNljX+sjISCUmJmrNmjW12o/VajXNDxw4UAMHDpTL5QopDJo4caImTpzotzwmJsZv24HaPG3aNGPe7XbX+8Kjpn3Wpo1xcXHG63Vp2549e4xpzxhsLVF0m9Z67pExemTuK/rhx58CrnPWGW007Porwt1UAAAAAABavLFjx2rSpEmSqsYK84QZTqfT6M02YcIE9ezZU6tXr9aqVatUWFiopUuXGveP5s2bpw0bNkj6772WsrIyPffcc5Kk3NzcaocOsVqtWrlypSRpzpw5pspEbrdb27ZtU3l5ubFO79691adPH3Xt2tV0/8W7zR5PPvmk8b4JEyYEvOdVVlamjz/+2DiGSy+9VNdcc02j9MTLyclRXl6epKohRII9oF5T+/r27et3LN7Hf+edd1bb/vLycs2cObPa89IUXHzxxcb0vHnzNGjQoHA3KWQHDx7Uxx9/LEnq27cvZSGBFoJwDQ0qHL2smvITPbVtm9vt1n333Sep6sKqZ8+e4T6ERnVH357qlfAbDfhjrvZ++53ptZ7dumjR1Hv1qxh6rgEAAAAA0NgGDRpkhGs7duwwlrtcLmNc+N69e2v69OlGbytJ2rVrlxHIbNiwwVjXE65dfPHFxrJLL73U9MC0r8mTJ8vhcMhisfgFa97lEz08242NjZXNZjPe491mD5vNZgxlMmjQIFOI5Ha7NWrUKL/3eCQnJ8tqtTbYMCQ5OTnG0Cy+bQ/E5XJpyJAhfscvVQ2Z4tu+mJgYvfvuu3I4HNq6dWu14doLL7xgHPeiRYsa5Pgaw1VXXaUzzjhDhw4d0qpVq7Rnzx6df/754W5WSMrLy3X8+HFJ0pAhQ8LdHAANhD6oQBNgt9tlt9s1efJk40Jv9uzZTTo4bCgXdDhHD08YYVoWFxujN2c/TLAGAAAAAMAJ4l1Jp0uXLgHXmTFjhhHwWCwWpaamqnPnztVuNyYmxig5mZWVZRpew1tZWZkxXv3YsWON5b7BmmdYE9+x4iwWi1wul7HP/Px85efnG9vxDNmRn59vGhbE7XarU6dOpmAtNTVVGRkZxnxJSYksFkvQttdGcXFxrYO1Ll26mII132FcSkpKlJycbGrf+PHjJVWFisHKfLpcLqP3XFMfmqRVq1Z65JFHjPl169aFu0kh85SEPP300zV8+PBwNwdAAyFcA8LM5XIpPj5e8fHxpguatLS0cDfthPnl692m+SPuo9SfBgAAAADgBCosLDSmgwVmDodDGRkZqqio0JYtW2S1WkMqI+gpOyhJmzZtqnH/o0ePNqa9xyXLzs7W3r17NWfOHGOsOE+A5nA49Mknn0iqqqyUlpZmurcyaNAgY5l3acrCwkIj1PMcm9Vq1Zw5c3T48GFlZ2cb28/Nza3z+Y2Li1NZWZlSUlKMZTUFa1LVw9ee9s2dO1eHDx+W1WqV1WrV4cOHjRDQZrOZzqEnXJOkl19+OeC2vQOq5jA0yXfffWdMB/s5aor++c9/SpKuuOIKxcbGhrs5ABoId6+BMPN90iYjI6NJd8NvDN8cOGyarzjwvX44dLiOWwMAAAAAALVRXl5uPPArVYUAweTm5ta6PGL//v2Nac/4a97cbnfQHlRz5syRzWZTUVFRwJKSgwcPNqY941qFyuVyafLkyZKqeuL5jvMWHR2tadOmGYGIp8dZXTidTlOvpdjYWLVp06bG9nn2mZycrIkTJ5rOTXR0tGbPnm3Mp6enG9PePQbnz58fcPvPPvus0ZamPjRJVlaWZs2aJUk67bTTdMstt4S7SSHZsmWLMUbeTTfdFO7mAGhAhGtAmPXv3187d+7Uzp07dfToUc2ZM6dJd8NvDP/54RfT/LHjlfrgc3u4mwUAAAAAQIvhdDqNYSnsdrvKy8uVk5Ojjh07GiGMJOXn5we9L5GRkVGnexaRkZFGD6uCggKjfKPH6tWrjelAPagSEhKCjhvmdDqNaU+IUZtz4ukV5l1y0Ndjjz1mTAcrsVgTi8Vi7Euq6glX0/hbnp54kjR9+vSg59bTu06S6dx6egw6HA6VlZX5HbunR+D48eOb9NAkf/rTn5STkyOpKlhbt25dtePINRWVlZW6++67deTIEUnSbbfdFu4mAWhATfd/TeAkERkZaSpHcDJyHvzeb9nnu/aob4/Lw900AAAAAABahJKSEsXHx1e7TkZGhkaMGBH09fr0bhozZozRO803HAm1B5Xb7da2bdtUXl6ulStXmgKiuvAO4w4cOCCr1RpwvQMHDhjTe/bsqdN9HE+wVlpaqunTp6ukpEQlJSWyWq1BhwbZtWuXMf3xxx+b5r3t37/fmHY6nUYA2qdPH2N5YWGhqYTnkiVLjGnvEpJNzbRp0zRjxgxJVWOW/e1vf2vyvew8VqxYoY8++kiSdMMNN+iaa64Jd5MANCDCNQBht7tiv9+yjbYvNGF48+jiDwAAAABAc2WxWNStWzfl5uY26sO/CQkJslgsstlsevbZZ41wzeVy1diDqry8XDNnzlRBQUGDtsk7NJs0aVJI79m1a1dI48wFUlpaqsTERL3++uvq0qWLHA6H0tPT1bt374Dn3ntcsVDbt2HDBmNb0dHRysjIUF5envLy8jR79mzj/HqGJLFYLLUu83miTJ061eh9d/rpp+u9995Tjx49wt2skHnC5NNOO03Lli1r0r0DAdQev9EAwqqyslJfO/zDtdJPtoe7aQAAAGHhPnZMka1ahbsZAIAWJjU1NWjPrBNl7NixmjRpkkpKSuR0OhUTE2Mai37UqFF+77FaraaxxDzHMmjQILVr1059+vTR2LFj6xS8nXPOOcZ0cnJySCFT586d63TsRUVFRigXHR2tpUuXKikpSVLVuHGbN2/2C1/OPfdc0zGHwrd93j0GN23apMTERNntdtlsNkn/LR3Z1Pz5z382BWv/+7//22yCtePHjysrK0tr166VJP3P//yP2rdvH+5mAWhghGsAwmqX41sdO37cb/nhn3/WHucBnR9zTh22CgAA0Dx9+ZVd+yqc6n1d87h5BABAbYwePdrogbVkyRJNnDjRKAlpsVgC9t6aPHmyMe3p+eXLe9y12vAOoqZPn17nHmmhuOyyy0zziYmJRq8ym82myZMna86cOaZ1unTpYkxPmTJFCQkJtd5vQkKCYmNj5XA4jNKQL7/8svG6d+nIpmLKlCl64oknJFUFa+vXr29WJRVXrlypxx9/3Jh/9NFHw90kAI3glHA3AMDJbfuu/wR97XP7f2qxJQAAgOZvXekGbfrgo3A3AwCARhEdHa3k5GRJVeNReZeEDNSDyul0GmOVZWRkBAy/7HZ7ncddO//8843p6dOnB13P7XY3yvmYPXu2YmNjJVWVECwrKzO97h2mvfDCC3Vu32OPPWbsw+12a9myZZKqzqlnfLam4k9/+pMpWHv55Zd1zTXXaPv27frzn/8sl8sV7iZWa+vWrUYg3KZNGy1fvrzGsQ4BNE+EawDCqmzL58Ffs31eiy0BAAA0f8tXFGvp8jfC3QwAABqNJ8QqKSnR4sWLjeWBelCdffbZRvjkXSLRm3cvrEA87/cev8wjLi5OFovFaE+w4GbUqFGKiIhQZmZmg4Y7kZGR2rhxozE/fPhw0/a7du1qCt8ChWhut1sDBgxQx44dlZmZGXCdkSNHGtMLFiwwSkKOGTOmwY6lITzyyCOaMWOGJKl169YqLS3VHXfcocrKSqWmpurxxx9XdnZ2uJsZ1Mcff6zLL79cO3bskCTdd999GjJkSLibBaCREK4BCKsOZ5+lUQOvD/jVvt2Z4W4eAADACWP79DMdOHhQUlV5SAAAWqKePXsagZEnSAnWgyoyMlLjx4+XJGVlZamsrExut1tut1tlZWXKzMxUVlZWtfvzvD8vL0/l5eVyu92mAGvFihXGdNu2bZWTkyO73S6pqldcZmamaTy3hu7pFRcXp7lz50qSHA6HKYyJjIzU0qVLjflOnTr5tW/y5MkqKSmRw+HQueee6zdumyTFxMQYPQY9ZTljY2PrVGaysaxZs0ZPPfWUMX/vvffq6quvllQVAn788ceSpFdeeSXcTQ3o+PHjyszMNOZbtWqlcePGhbtZABoR4RqAsJo44lY988f/Cfh137D+4W4eAADACfPOu+uN6Y+3lIe7OQAANArvwMxT8rG6HlTeY64lJSUpKipKUVFRSkpKUl5enpKTk43gKJBRo0YZ0xaLRVFRUZo6daqxzDvckqpCvPj4eEVERCg+Pl55eXnGe2fPnt0o52TixInGMZSUlMhqtRqvecZm85yvYO1LTk42HZev+++/3zTvKRXZFKxevVr33HOPJCkqKkqZmZnGmGUPPvigXnrpJUnSWWedVW15zHB55513ZLFYtH591bVc9+7d9a9//UsXXXRRuJsGoBERrgEAAABAmLmPHTOVg3yj6O1wNwkAgEbjHXjV1IMqOjpaNpvNCJi8JScny2q1KiYmJuj74+LiVFRUZArgCgsLTetMnDhRNpvN6FHnzWKxKDs7WyUlJQF7hTUUq9Vq7D89Pd3onSZJc+bMqbZ9c+fO1euvv15t+/r3Nz/APHr06EY7ltpYvXq1brnlFu3evVuSNGHCBP2///f/1LZtW0nS/v37JVUFa5s3b9bAgQPD3WTDL7/8oszMTPXr109bt26VJP3qV7/SW2+9pauuuirczQPQyCIqKysrw90IAE3Xx9t3qt+kGaZld6X00azM34e7aSH7z74KWX7v//TWu89PU0KXX4e7eQAAALJ9+pnGZzxkWlZStEytW58e7qYBAJo5p9Mpl8ul6OjoakOoYNxut7755htJVeUFqyuL6NmXVBVqVccTHtWmXW63W5s2bdKZZ56prl27GmFSqMfocrnkdDqrPQ6Xy6WvvvpKu3fvVp8+fepVBtKzP0m64IILagznvM9fsDZ62vfDDz/oiiuuqFX7OnbsKIfDoeTkZK1Zs6bOx9VQVq1apbFjx2r37t2KiorShAkT9Je//MUI1iTp4MGDevDBBzVs2LB6B2vHjx/XKafUv6/Jf/7zH73++ut66aWXtHnzZklSRESE7r77bj3zzDM680yGOQFOBo33uAUAAAAAICTeJSE9/v3lV7Jcflm4mwYAaOZiYmLqFKp5REZG1hiU1WVfoW7Tty2JiYl13m90dHSNYVR0dLQSEhIaZDyyUPZX2+PwtK+2iouLjTKc06dPr/ex1deqVat06623GvMTJ07UM88847fe2WefrRdffLFe+9q/f78SExO1fft2PfXUU7r33nt11lln1Xo7P/zwg2bNmqVnnnnGNG5fbGysVqxYoWuvvTaMZxTAiUa4BgAAAABh5FsS0uOdd9cTrgEAgHpzOp1KSUmRVBUE9ezZM6ztefvttzV27FhJVWOsTZw4UTk5OaqsrNS2bdt02WUNd/2zf/9+jRw5Utu3b5ckPfzww3r88cd13XXX6fLLL9eFF16oCy+8ULGxsTr11FMVFRWliIgIHThwQA6HQw6HQ1u3btUHH3yg8vJyU6h24YUXasSIEbrvvvvUqVOnsJ5TACce4RoAAAAAhNG/Pvgo4PJ/rl2v+yf9IdzNAwAAzVRmZqY+++wzVVRUGMs2btzYqGPH1eTtt9/WgAEDjPlJkyZp9uzZqqys1KhRo7Ry5Up98skn6ty5c733tX//fiUkJGjv3r2m5T/88INWr16t1atX12m7MTEx+stf/qK7775bUVFRYTuXAMKLcA0AAAAAwmjJ0tcDLj9w8KD2OvapY+x54W4iAABoZtxut/Ly8kzL5s6dW6dynA3lrbfe0r333iupqsfapEmTjB5rf/zjH/XKK69Ikm688UZ98skndSrd6OHpseYJ1q666irNmDFD+/bt07p16/Thhx9q165d+uGHH2rcVmxsrK666ipdc801uuaaa5SUlKSzzz47bOcRQNNAuAagWpGtWvktO3rsWLibVSvuIO1t1ar+g9gCAADUx08//awPP/ok6Ovln24jXAMAALV25MgR5efna9OmTbrlllvUp0+fWo3/1tDeeustDRw40JjPyMjQrFmzjB5rnmCtXbt2evfdd+sdrHn3WLvqqqv0wQcf6JRTqu4D/c///I8k6fjx4/ryyy/13nvv6YsvvtD+/ft17NgxtW3bVhdffLGuuOIKWSyWerUFQMtFuAagWu3bneG37GvHt+FuVq3Yv94TcHmHs88Md9MAAMBJ7uMt5dW+XvT2GvVPvjHczQQAAM1MdHS00tLSlJaWFu6m+PVYy8jIUHZ2tl+PtXbt2unVV1+tV++6QD3WrFarEax5O+WUU/Sb3/xGv/nNb8J9igA0Q4RrAKrVsf3Z6nTeudq9b7+xrHTLdk2a9ZKujWuvs9s13YCqslI6+N33WvzPT/xeS7riUrVvwm0HAAAnh2AlIT0+/OgTuY8dC1hNAAAAoKkrLi5WSkqKMe/dY+1//ud/9Pe//11SVbD28ccf1ytY2759u2666aagPdYAoCERrgGo0a29umvR6/805isrK1WweoMKwt2wenjivtRwNwEAAJzkDn73fbUlIT0+27ZdlssvC3dzAQAAaqW4uNjosXbqqacqIyND06dPV2Vlpe6//35TsFZQUFCvYO3NN99Uenq6Dh06JKn6HmsA0BAI1wDUKNlyoSlca+6uujRel8adH+5mAACAk9w7774X0noffvQJ4RoAAGhWioqKNGjQIGM+IyNDTz/9tCorK3XXXXfp5ZdfllQVrH300UeKj4+v8762b9/uF6zRYw1AY+N/GAA1ujmxp5794+/V5YLYcDel3uJi2yt37LBwNwMAAEBvFL0d0nrLVxSHu6kAAAAhKyoq0h/+8AdJVT3WHnroIWOMtfvvv98UrBUUFIQUrB07dkw2m02ffPKJfvzxR2P5m2++qR49etBjDcAJF1FZWVkZ7kYAaB4qKyv197fW688Llsj185FwN6dWzjmjrR76/W919203qRUXWAAAIMwOfve9Bt2eFvL6K1+z6ux2Z4W72QAAANVauXKlfvvb3xrzDz/8sJ588klVVlZq9OjR+t///V9Jte+x9vzzz2vChAnG/Pnnn682bdro3//+t7FszJgxWrhwIcEagBOCspAAQhYREaFRA29Qz8u6aNEb7+jfXzt06KefVXn8eLibFri9p5yiM1qfruRrL9eY225W69NPC3eTAAAAJEm7v9mj5Jv7hLz+gQMHCdcAAECT9uabb2rcuHGSqnqs/fGPf1RWVpbRY80TrJ111lmyWq21KgW5Y8cO0/yePXtM8w888IBmzZqliIiIcJ8GACcJeq4BAAAAQBOReHOKJKnsn0XhbgoAAEDI3nzzTd12223G/COPPKInnnhClZWVuvvuu/W3v/1NUlWw9tFHH+nCCy+sdnvvvfeeDh06pIEDByoiIkILFy40gjtfY8aM0V//+leCNQAnFD3XAAAAAAAAAAB18sYbb2j8+PGSqnqs3X///Zo2bZoqKyv1wAMPmIK1goKCGoO1N954Q7fffruOHTumDh06qFevXoqKijJev+OOO7RgwQLt3btXR44c0VVXXRXuUwDgJES4BgAAAAAAAACotTfeeEODBw825h944AHNnDlTlZWVuueee7R48WJJofdYk6TIyEgdO3ZMklRRUaEVK1aYXl++fLny8/N1zjnnhPvwAZzEGN0RAAAAAAAAAFArRUVFpjHWHn30Uf35z382eqx5B2uh9FjzuPXWW03jsXXq1MlU8tHtdus///lPuA8fwEmOcA0AAAAAAAAAELKDBw9q0KBB2rt3ryRp8uTJmjlzplq3bq0xY8bo//2//yepKljbvHmzBgwYEPK2TznlFPXr18+Y/vDDD/Xxxx9r/Pjx6tChgyZOnKjOnTuH+xQAOMkRrgEAAAAAAAAAQlZaWmpMX3/99frzn/8sSdq/f7+++OILSVXBmtVq1UUXXVTr7XvCtePHj2vq1Knq3r27nn/+ee3bt09z584N9+EDAOEaAAAAAAAAACB069evN6b//ve/q3Xr1pKk9u3ba/369UpJSdHmzZs1cODAOm3fE65J0osvvqhvv/023IcMACaR4W4AkHhziiSp7J9F4W4KAAAAAAAAWjDPfSiJe1H1UVZWJklKSEgIWKJx+fLlioqKqvP2S0pKTPPffPON2rdvH+7DBgADPdcAAAAAAAAAACE5dOiQ/vWvf0mSkpKSAq5TXbB28OBBDR8+XPfdd58OHz7s9/onn3yi3//+98b85MmTdcUVV4T7sAHAhJ5rAAAAAAAAAE4q9Fqruzlz5sjtdksKHq5VZ8SIEUbPtFdeeUVXX321evXqpV69eumbb77R5MmT9dNPP0mSpkyZotzc3HAfMgD4IVwDAAAAAAAAANTo2LFjmjVrljFfl3CtsrLSmD506JDWrl2rtWvX+q13//33E6wBaLIoCwkAAAAAAAAAqNHmzZv1/fffS5Kuv/56/frXv671Nrp06WJM9+3bV5deeqlatWplWufRRx/VM888E+7DBYCgCNcAAAAAAAAAADVav369Mf3II4/UaRve4dqDDz6obdu2aevWrZo4caJiYmL05z//WTNnzgz3oQJAtSgLCQAAAAAAAACoUVlZmSSpY8eOuuWWW+q0De9w7cCBA5KkSy65RHPnztXcuXPDfYgAEBJ6rgEAAAAAAAAAauTpuda7d29FRtat34Z3uLZ///5wHxIA1AnhGgAAAAAAAACgWq+//rq+/fZbSVJiYmKdtxMfH29Me3quAUBzQ7gGAAAAAAAAAKjWE088YUxff/31dd7ON998Y0wTrgForgjXAAAAAAAAAABB2e12bdq0SZJ0wQUXyGKx1Gk7b775pnr06GHMe5eIBIDmhHANAAAAAAAAABBUaWmpMT1hwgSdeuqptd7G9u3blZ6erkOHDkmSxowZo4kTJ4b70ACgTgjXAAAAAAAAAABBrV+/XpLUpk0bjRkzptbvf/XVV9WjRw8jWHvggQf017/+VREREeE+NACok8hwNwAAAAAAAAAA0HSVlZVJkq666iq1b9++xvX379+vkpISrVy5UitXrtR3331nvDZmzBjNmjWLYA1As0a4BgAAAAAAAAAIaNu2bfr8888lSUlJScbyyspK2e12ffHFF35f33zzjdxut9+2HnjgAYI1AC0C4RoAAAAAAAAAIKBnnnlGx44dkyR16dJFL7/8slauXKni4mK5XK6QtnHJJZdo7NixeuCBBwjWALQIhGsAAAAAAAAAAD+7du1SQUGBMV/deGutW7fWxRdfHPDrnHPOCfehAECDIlwDAAAAAAAAAPixWq3V9k6zWCxKSUnRb3/7W1133XX0SgNw0iBcAwAAAAAAAAD4ueOOO/Thhx/qm2++0UUXXaTf/OY3ph5pZ511VribCABhQbgG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PugFjW+I6KuiYp/m5s2pzykNyO1263cjR1S7b4+1a9/VpIkTql3n2WeeCVo73nMcs55+qsGPA2gunp27UBve/0A//fRzuJsCoAG1+s1vTPOntG8fMFA59Pjj+v4PfzC+flm92vT6z6+/bt7u/5VQDMQ3ePtp8eJq2/hLaakp4DqtTx//AKlNG1NpyqNbt+q4V0mf+jjiNfZNq/POU9TVV/sfk09pspD23aqVzsjNVYft23X2q6/qzCefVOthw3TWwoU6d/16tf/gg6qwLJg2bdRu2TKdu369zlq4UG3uvVdnv/qqzvv6a0U/+GBoB9eqlVrfc486bN+uc4qLTW04p7hY7W226tsAADhpfP7556b5W2sYazQyMtLv/lNNAVlh4RK/z+VTpz5Wpwent2791FS5R6q6t+E9xEggY8aM9Vt2x/BhemJmrt7fuLHRHh4OJNBxez9o7nK5dOed6X73RTp08C/3nZs7w+8e4rdOp+4YPiyktkydMqXaY1+69B9B76ncfrt5HwsWzPe7j+dyufzeX909Tt/qWr4eenBywIfy6+v9jRurfSjetw2+59zXhPvuq/b13NwZJ/RnDgCq06LCtUC9n84///wG3ceqVW/7/XEbN258nS5sNm163+8PULdul4c0vlmgXlMVFfv00IOT1eu6njrn7LP08EMP6v2NGxskaDvzjOhqvwKde+92dehwnqZOfUzln36mfyxd5ncBuXXrp6Y/ju9v3Oh3nocPv0Pln36mAwe/1z+WmsO0MffcXeMf1w4dztPTs2ar/NPPtHr1f58wu8vnhtZz8+b5vfeFF14wzf/PXXfVuK8XXnxJex0VeuHFl/wu5DwXA507d9YPh1wq//Qzv238cMilHw65Ana3D+S115b5XaQMH36H9joqVP7pZ7rxRnNpqKVL/1HtRc3WrZ/+X7mEl1T+6Wd6etZsv+PwPS/AyWTp8jf00NTpSk4ZpswHHyNoA1qIn1580TTf6rzz1K6w0D9g+/FH/bxsmenL29HNm/16mPn2UJOqeln5lqEMpSSkbwB3au/e1c7/9PLLprCtPo7885+med9ear4lIY+sW1dzuNamjdrbbNWOEdfqvPN09quv6ozcAE8st2ql9h98EPQY2z76qE6voXyYWrVSu8JCU8nPYG1oHWCMHQDAycX3YeCePa9r0O1v2vS+34PK3bpdXmMY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" + } + }, + "cell_type": "markdown", + "id": "a7782a35", + "metadata": {}, + "source": [ + "# Machine Learning & Homomorphic Encryption \n", + "\n", + "# Privacy-Preserving SVM Learning \n", + "\n", + "## Introduction \n", + "This lab explores **Fully Homomorphic Encryption (FHE)** with **Support Vector Machines (SVM)** for privacy-preserving machine learning. Data remains encrypted throughout, ensuring security. Homomorphic encryption (HE) enables computations on encrypted data without decryption, crucial for secure AI applications like healthcare and finance. Support Vector Machine (SVM) is a classification model that finds optimal decision boundaries for data separation. FHE enables training and inference on encrypted data, ensuring privacy while using third-party services. \n", + "\n", + "## Why Use Encrypted SVM? \n", + "- **Privacy** – No raw data exposure \n", + "- **Security** – Protects sensitive information \n", + "- **Real-world Applications** – Healthcare, finance, and AI security \n", + "\n", + "## How It Works \n", + "1. **Data encryption** before model input \n", + "2. **Encrypted SVM processes** data without decryption \n", + "3. **Encrypted predictions** are decrypted by the owner \n", + "\n", + "## Fully Homomorphic Encryption (FHE) includes several schemes that enable computations on encrypted data. \n", + "\n", + "Key schemes are: \n", + "\n", + "- **Gentry’s Scheme**: First viable FHE, introduced bootstrapping for noise control. \n", + "- **BGV**: Efficient for leveled FHE with arithmetic operations. \n", + "- **CKKS**: Ideal for approximate calculations like machine learning; **we will use CKKS in our experiments.** \n", + "- **TFHE**: Fast binary operations with efficient bootstrapping. \n", + "- **FHEW**: Focuses on rapid binary gate processing. \n", + "- **DM Scheme**: Early design based on integer arithmetic. \n", + "\n", + "Each scheme balances efficiency, complexity, and practical use. CKKS (Used in this lab) – Ideal for approximate machine learning computations.\n", + "\n", + "### Figure1: Secure SVM Classification with Homomorphic Encryption \n", + "\n", + "![g_zero-15.png](attachment:g_zero-15.png)\n", + "\n", + "This diagram illustrates how an **SVM model** classifies encrypted data using the **CKKS encryption scheme**, ensuring privacy-preserving machine learning. \n" + ] + }, + { + "cell_type": "markdown", + "id": "5ad1db4a", + "metadata": {}, + "source": [ + "---------------" + ] + }, + { + "cell_type": "markdown", + "id": "352c62f8", + "metadata": {}, + "source": [ + "## FHE-based SVM with OpenFHE Python: Examples A-D\n", + "\n", + "These examples demonstrate a privacy-preserving SVM classification method using FHE, supporting both linear and polynomial kernels. The process encrypts the feature vector and model parameters, performs secure computations to evaluate the decision function, and then decrypts the result for the final classification.\n", + "\n", + "### CKKS Parameters\n", + "Key parameters include:\n", + "- **Ring Dimension (N)**\n", + "- **Multiplicative Depth (D)**\n", + "- **Scaling Factor (S)**\n", + "- **Modulus Size (M)**\n", + "- **Security Level (L)**\n", + "- **Batch Size (B)**\n", + "\n", + "These settings balance performance, security, and accuracy, as shown in the diagram above.\n", + "\n", + "# A1: Practical Example: Data Generation\n", + "\n", + "To reproduce the dataset required for your experiments, follow these steps:\n", + "\n", + "1. Run the `get_data.py` file:\n", + "\n", + "```bash\n", + "python get_data.py\n", + "```\n", + "\n", + "2. The dataset will be automatically organised in the **data/** directory.\n", + "3. If the dataset requires regeneration, you can rerun the `get_data.py` script at any time to create a fresh copy.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc0643cf", + "metadata": {}, + "outputs": [], + "source": [ + "from ucimlrepo import fetch_ucirepo \n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.preprocessing import StandardScaler\n", + "import pandas as pd\n", + "SEED = 42\n", + "\n", + "# Fetch dataset \n", + "credit_approval = fetch_ucirepo(id=27) \n", + "\n", + "# Select only 4 numerical features (for simplicity)\n", + "vars = credit_approval.variables\n", + "cont_features = vars[vars['type'] == 'Continuous']['name'].values[0:4]\n", + "# Filter the X data (Dropping NA's)\n", + "X = credit_approval.data.features[cont_features].dropna()\n", + "# Drop y lines that were na in X\n", + "y = credit_approval.data.targets.loc[X.index]\n", + "# Replace + by 1 and - by 0\n", + "y = y.replace({'+': 1, '-': -1})\n", + "# Standardize X with StandardScaler\n", + "scaler = StandardScaler()\n", + "X = pd.DataFrame(scaler.fit_transform(X))\n", + "\n", + "\n", + "# Save full data\n", + "X.to_csv('data/credit_approval.csv', index=False)\n", + "y.to_csv('data/credit_approval_target.csv', index=False)\n", + "\n", + "# Split data into train and test\n", + "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.20, random_state = SEED)\n", + "\n", + "# Save train and test data\n", + "X_train.to_csv('data/credit_approval_train.csv', index=False)\n", + "y_train.to_csv('data/credit_approval_target_train.csv', index=False)\n", + "# Saving just the first line for inference\n", + "X_test.head(1).to_csv('data/credit_approval_test.csv', index=False)\n", + "y_test.head(1).to_csv('data/credit_approval_target_test.csv', index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "e35a9ee6", + "metadata": {}, + "source": [ + "\n", + "\n", + "# A2: Training the Model: Understanding the Process\n", + "\n", + "We train an SVM model to recognize data patterns using the `model_training.py` script. The process:\n", + "\n", + "1. **Load Data:** The model reads the training dataset.\n", + "2. **Optimize Parameters:** It identifies the best parameters for accurate classification.\n", + "3. **Save Model:** The trained model is saved for future use.\n", + "\n", + "Run the training with:\n", + "\n", + "```bash\n", + "python model_training.py\n", + "```\n", + "\n", + "After training, the model’s parameters are encrypted to protect its knowledge during inference.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c2b402d", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "from sklearn.svm import SVC\n", + "\n", + "# Load the data\n", + "X_train = pd.read_csv('data/credit_approval_train.csv')\n", + "X_test = pd.read_csv('data/credit_approval_test.csv')\n", + "y_train = pd.read_csv('data/credit_approval_target_train.csv')\n", + "y_test = pd.read_csv('data/credit_approval_target_test.csv')\n", + "\n", + "# Model Training\n", + "print(\"---- Starting Models Training ----\")\n", + "\n", + "print(\"Starting SVM Linear\")\n", + "svc_linear = SVC(kernel='linear')\n", + "svc_linear.fit(X_train, y_train.values.ravel())\n", + "print(\"SVM Linear Completed\")\n", + "\n", + "svc_poly = SVC(kernel='poly',degree=3,gamma=2)\n", + "svc_poly.fit(X_train, y_train.values.ravel())\n", + "print(\"SVM Poly Completed\")\n", + "\n", + "print(\"---- Model Training Completed! ----\")\n", + "\n", + "decision_function = svc_linear.decision_function(X_test)\n", + "ytestscore = decision_function[0]\n", + "\n", + "decision_function_poly = svc_poly.decision_function(X_test)\n", + "ytestscore_poly = decision_function_poly[0]\n", + "\n", + "# Saving Results\n", + "np.savetxt(\"models/weights.txt\", svc_linear.coef_)\n", + "np.savetxt(\"models/intercept.txt\", svc_linear.intercept_)\n", + "np.savetxt(\"data/ytestscore.txt\", [ytestscore])\n", + "np.savetxt(\"models/dual_coef.txt\", svc_poly.dual_coef_)\n", + "np.savetxt(\"models/support_vectors.txt\", svc_poly.support_vectors_)\n", + "np.savetxt(\"models/intercept_poly.txt\", svc_poly.intercept_)\n", + "np.savetxt(\"data/ytestscore_poly.txt\", [ytestscore_poly])\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "8c96c6c3", + "metadata": {}, + "source": [ + "------------" + ] + }, + { + "cell_type": "markdown", + "id": "a5d69762", + "metadata": {}, + "source": [ + "## A3: Running an Encrypted Linear SVM\n", + "\n", + "### Overview\n", + "This guide demonstrates how to create, save, and run an **encrypted linear SVM model** using homomorphic encryption. A linear SVM uses a straight-line decision boundary for classification.\n", + "\n", + "### Steps\n", + "1. **Open the script:**\n", + " ```bash\n", + " nano encrypted_svm_linear.py\n", + " ```\n", + "2. **Run the model:**\n", + " ```bash\n", + " python encrypted_svm_linear.py\n", + " ```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a48a1da1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import time\n", + "import csv\n", + "from openfhe import *\n", + "from sklearn import datasets\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.svm import SVC\n", + "from sklearn.metrics import accuracy_score\n", + "\n", + "def setup_crypto_context(multDepth=1, scalSize=30, firstModSize=60, securityLevel=128, batchSize=1024, ringDim=16384):\n", + " \"\"\"\n", + " Sets up the OpenFHE crypto context with specified parameters.\n", + " \"\"\"\n", + " parameters = CCParamsCKKSRNS()\n", + " parameters.SetMultiplicativeDepth(multDepth)\n", + " parameters.SetScalingModSize(scalSize)\n", + " parameters.SetFirstModSize(firstModSize)\n", + " parameters.SetRingDim(ringDim)\n", + " \n", + " if securityLevel == 128:\n", + " parameters.SetSecurityLevel(HEStd_128_classic)\n", + " elif securityLevel == 256:\n", + " parameters.SetSecurityLevel(HEStd_256_classic)\n", + " elif securityLevel == 192:\n", + " parameters.SetSecurityLevel(HEStd_192_classic)\n", + " else:\n", + " parameters.SetSecurityLevel(HEStd_NotSet)\n", + " \n", + " parameters.SetBatchSize(batchSize)\n", + " \n", + " context = GenCryptoContext(parameters)\n", + " context.Enable(PKE)\n", + " context.Enable(KEYSWITCH)\n", + " context.Enable(LEVELEDSHE)\n", + " context.Enable(ADVANCEDSHE)\n", + " \n", + " return context\n", + "\n", + "def encrypt_data(context, data, keys):\n", + " \"\"\"Encrypts data using OpenFHE's CKKSPacked encoding.\"\"\"\n", + " pt = context.MakeCKKSPackedPlaintext(data)\n", + " encrypted_data = context.Encrypt(keys.publicKey, pt)\n", + " return encrypted_data\n", + "\n", + "def test_openfhe_encryption():\n", + " \"\"\"Tests OpenFHE encryption and decryption.\"\"\"\n", + " context = setup_crypto_context()\n", + " keys = context.KeyGen()\n", + " context.EvalMultKeyGen(keys.secretKey)\n", + " context.EvalSumKeyGen(keys.secretKey)\n", + " \n", + " test_data = [1.5, 2.0, 3.5]\n", + " print(f\"Original data: {test_data}\")\n", + " \n", + " encrypted = encrypt_data(context, test_data, keys)\n", + " decrypted = context.Decrypt(keys.secretKey, encrypted)\n", + " decrypted.SetLength(len(test_data))\n", + " real_values = [val.real for val in decrypted.GetCKKSPackedValue()]\n", + " print(f\"Decrypted values (real part): {real_values}\")\n", + "\n", + "def run_encrypted_svm(trials=100, csv_filename=\"svm_results-linear.csv\"):\n", + " \"\"\"Runs encrypted Linear SVM classification using OpenFHE and logs results to a CSV file.\"\"\"\n", + " iris = datasets.load_iris()\n", + " X = StandardScaler().fit_transform(iris.data)\n", + " y = iris.target\n", + " \n", + " X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n", + " \n", + " # Change to a linear kernel\n", + " model = SVC(kernel='linear')\n", + " model.fit(X_train, y_train)\n", + " \n", + " accuracies_encrypted, accuracies_non_encrypted = [], []\n", + " total_time_encrypted, total_time_non_encrypted = 0, 0\n", + " \n", + " for _ in range(trials):\n", + " # Test the non-encrypted version\n", + " t_start = time.time()\n", + " predictions = model.predict(X_test)\n", + " accuracies_non_encrypted.append(accuracy_score(y_test, predictions))\n", + " total_time_non_encrypted += time.time() - t_start\n", + " \n", + " # Test the encrypted version\n", + " t_start = time.time()\n", + " context = setup_crypto_context()\n", + " keys = context.KeyGen()\n", + " \n", + " predictions_encrypted = []\n", + " for i in range(len(X_test)):\n", + " encrypted_sample = encrypt_data(context, X_test[i], keys)\n", + " decrypted_sample = context.Decrypt(keys.secretKey, encrypted_sample)\n", + " decrypted_sample.SetLength(len(X_test[i]))\n", + " real_values = [val.real for val in decrypted_sample.GetCKKSPackedValue()]\n", + " prediction = model.predict([real_values])\n", + " predictions_encrypted.append(prediction)\n", + " \n", + " accuracies_encrypted.append(accuracy_score(y_test, predictions_encrypted))\n", + " total_time_encrypted += time.time() - t_start\n", + " \n", + " avg_encrypted_acc = np.mean(accuracies_encrypted)\n", + " avg_non_encrypted_acc = np.mean(accuracies_non_encrypted)\n", + " avg_encrypted_time = total_time_encrypted / trials\n", + " avg_non_encrypted_time = total_time_non_encrypted / trials\n", + " \n", + " print(f\"Avg Encrypted SVM Accuracy: {avg_encrypted_acc:.4f}\")\n", + " print(f\"Avg Non-Encrypted SVM Accuracy: {avg_non_encrypted_acc:.4f}\")\n", + " print(f\"Avg Encrypted Time: {avg_encrypted_time:.4f} sec\")\n", + " print(f\"Avg Non-Encrypted Time: {avg_non_encrypted_time:.4f} sec\")\n", + " \n", + " # Append results to CSV\n", + " with open(csv_filename, mode='a', newline='') as file:\n", + " writer = csv.writer(file)\n", + " writer.writerow([1, 30, 60, 128, 1024, 16384, avg_encrypted_acc, avg_non_encrypted_acc, avg_encrypted_time, avg_non_encrypted_time])\n", + "\n", + "def main():\n", + " print(\"---- Testing OpenFHE Encryption ----\")\n", + " test_openfhe_encryption()\n", + " print(\"---- Running Encrypted Linear SVM ----\")\n", + " run_encrypted_svm(trials=100)\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()\n" + ] + }, + { + "cell_type": "markdown", + "id": "06736fba", + "metadata": {}, + "source": [ + "----------" + ] + }, + { + "cell_type": "markdown", + "id": "cfdf9183", + "metadata": {}, + "source": [ + "## A4: Running an Encrypted Polynomial SVM\n", + "\n", + "### Overview \n", + "This guide shows you how to create, save, and run an **encrypted polynomial SVM model** using homomorphic encryption for secure computations. Unlike a linear SVM that uses a straight line, a polynomial SVM uses a curved decision boundary to capture complex patterns.\n", + "\n", + "### How to Run the Encrypted Polynomial SVM \n", + "1. **Open the script:** \n", + " ```bash\n", + " nano encrypted_svm_poly.py\n", + " ``` \n", + "2. **Execute the model:** \n", + " ```bash\n", + " python encrypted_svm_poly.py\n", + " ``` \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c47a1dc6", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import time\n", + "import csv\n", + "from openfhe import *\n", + "from sklearn import datasets\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.svm import SVC\n", + "from sklearn.metrics import accuracy_score\n", + "\n", + "def setup_crypto_context(multDepth=1, scalSize=30, firstModSize=60, securityLevel=128, batchSize=1024, ringDim=131072):\n", + " \"\"\"\n", + " Sets up the OpenFHE crypto context with specified parameters.\n", + " \"\"\"\n", + " parameters = CCParamsCKKSRNS()\n", + " parameters.SetMultiplicativeDepth(multDepth)\n", + " parameters.SetScalingModSize(scalSize)\n", + " parameters.SetFirstModSize(firstModSize)\n", + " parameters.SetRingDim(ringDim)\n", + " \n", + " if securityLevel == 128:\n", + " parameters.SetSecurityLevel(HEStd_128_classic)\n", + " elif securityLevel == 256:\n", + " parameters.SetSecurityLevel(HEStd_256_classic)\n", + " elif securityLevel == 192:\n", + " parameters.SetSecurityLevel(HEStd_192_classic)\n", + " else:\n", + " parameters.SetSecurityLevel(HEStd_NotSet)\n", + " \n", + " parameters.SetBatchSize(batchSize)\n", + " \n", + " context = GenCryptoContext(parameters)\n", + " context.Enable(PKE)\n", + " context.Enable(KEYSWITCH)\n", + " context.Enable(LEVELEDSHE)\n", + " context.Enable(ADVANCEDSHE)\n", + " \n", + " return context\n", + "\n", + "def encrypt_data(context, data, keys):\n", + " \"\"\"Encrypts data using OpenFHE's CKKSPacked encoding.\"\"\"\n", + " pt = context.MakeCKKSPackedPlaintext(data)\n", + " encrypted_data = context.Encrypt(keys.publicKey, pt)\n", + " return encrypted_data\n", + "\n", + "def test_openfhe_encryption():\n", + " \"\"\"Tests OpenFHE encryption and decryption.\"\"\"\n", + " context = setup_crypto_context()\n", + " keys = context.KeyGen()\n", + " context.EvalMultKeyGen(keys.secretKey)\n", + " context.EvalSumKeyGen(keys.secretKey)\n", + " \n", + " test_data = [1.5, 2.0, 3.5]\n", + " print(f\"Original data: {test_data}\")\n", + " \n", + " encrypted = encrypt_data(context, test_data, keys)\n", + " decrypted = context.Decrypt(keys.secretKey, encrypted)\n", + " decrypted.SetLength(len(test_data))\n", + " real_values = [val.real for val in decrypted.GetCKKSPackedValue()]\n", + " print(f\"Decrypted values (real part): {real_values}\")\n", + "\n", + "def run_encrypted_svm(trials=100, csv_filename=\"svm_results-poly.csv\"):\n", + " \"\"\"Runs encrypted Polynomial SVM classification using OpenFHE and logs results to a CSV file.\"\"\"\n", + " iris = datasets.load_iris()\n", + " X = StandardScaler().fit_transform(iris.data)\n", + " y = iris.target\n", + " \n", + " X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n", + " model = SVC(kernel='poly', degree=3)\n", + " model.fit(X_train, y_train)\n", + " \n", + " accuracies_encrypted, accuracies_non_encrypted = [], []\n", + " total_time_encrypted, total_time_non_encrypted = 0, 0\n", + " \n", + " for _ in range(trials):\n", + " t_start = time.time()\n", + " predictions = model.predict(X_test)\n", + " accuracies_non_encrypted.append(accuracy_score(y_test, predictions))\n", + " total_time_non_encrypted += time.time() - t_start\n", + " \n", + " t_start = time.time()\n", + " context = setup_crypto_context()\n", + " keys = context.KeyGen()\n", + " \n", + " predictions_encrypted = []\n", + " for i in range(len(X_test)):\n", + " encrypted_sample = encrypt_data(context, X_test[i], keys)\n", + " decrypted_sample = context.Decrypt(keys.secretKey, encrypted_sample)\n", + " decrypted_sample.SetLength(len(X_test[i]))\n", + " real_values = [val.real for val in decrypted_sample.GetCKKSPackedValue()]\n", + " prediction = model.predict([real_values])\n", + " predictions_encrypted.append(prediction)\n", + " \n", + " accuracies_encrypted.append(accuracy_score(y_test, predictions_encrypted))\n", + " total_time_encrypted += time.time() - t_start\n", + " \n", + " avg_encrypted_acc = np.mean(accuracies_encrypted)\n", + " avg_non_encrypted_acc = np.mean(accuracies_non_encrypted)\n", + " avg_encrypted_time = total_time_encrypted / trials\n", + " avg_non_encrypted_time = total_time_non_encrypted / trials\n", + " \n", + " print(f\"Avg Encrypted SVM Accuracy: {avg_encrypted_acc:.4f}\")\n", + " print(f\"Avg Non-Encrypted SVM Accuracy: {avg_non_encrypted_acc:.4f}\")\n", + " print(f\"Avg Encrypted Time: {avg_encrypted_time:.4f} sec\")\n", + " print(f\"Avg Non-Encrypted Time: {avg_non_encrypted_time:.4f} sec\")\n", + " \n", + " # Append results to CSV\n", + " with open(csv_filename, mode='a', newline='') as file:\n", + " writer = csv.writer(file)\n", + " writer.writerow([1, 30, 60, 128, 1024, 131072, avg_encrypted_acc, avg_non_encrypted_acc, avg_encrypted_time, avg_non_encrypted_time])\n", + "\n", + "def main():\n", + " print(\"---- Testing OpenFHE Encryption ----\")\n", + " test_openfhe_encryption()\n", + " print(\"---- Running Encrypted Polynomial SVM ----\")\n", + " run_encrypted_svm(trials=100)\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()\n" + ] + }, + { + "cell_type": "markdown", + "id": "25c30cfd", + "metadata": {}, + "source": [ + "----------------" + ] + }, + { + "cell_type": "markdown", + "id": "cd888407", + "metadata": {}, + "source": [ + "# B: Create a new encrypted file using Python to pass the parameters as arguments for the CKKS encryption scheme. Then, observe the outputs." + ] + }, + { + "cell_type": "markdown", + "id": "71986fba", + "metadata": {}, + "source": [ + "## B1: Creating an Encrypted Linear SVM File\n", + "\n", + "### Steps:\n", + "1. **Edit the script:** \n", + " ```bash\n", + " nano encrypted_svm_lineartest.py\n", + " ```\n", + "2. **Run the process:** \n", + " ```bash\n", + " python encrypted_svm_lineartest.py\n", + " ```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08856f70", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import time\n", + "import csv\n", + "import os\n", + "from openfhe import *\n", + "from sklearn import datasets\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.svm import SVC\n", + "from sklearn.metrics import accuracy_score\n", + "\n", + "CSV_FILENAME = \"svm_results_linear.csv\"\n", + "\n", + "def get_user_input():\n", + " \"\"\"Prompts user for encryption and SVM parameters.\"\"\"\n", + " trials = int(input(\"Enter number of trials: \"))\n", + " multDepth = int(input(\"Enter Multiplicative Depth (e.g., 1): \"))\n", + " scalSize = int(input(\"Enter Scaling Mod Size (e.g., 30): \"))\n", + " firstModSize = int(input(\"Enter First Mod Size (e.g., 60): \"))\n", + " securityLevel = int(input(\"Enter Security Level (128, 192, 256): \"))\n", + " batchSize = int(input(\"Enter Batch Size (e.g., 1024): \"))\n", + " ringDim = int(input(\"Enter Ring Dimension (e.g., 16384): \"))\n", + " return trials, multDepth, scalSize, firstModSize, securityLevel, batchSize, ringDim\n", + "\n", + "def setup_crypto_context(multDepth, scalSize, firstModSize, securityLevel, batchSize, ringDim):\n", + " \"\"\"Sets up the OpenFHE crypto context with user-specified parameters.\"\"\"\n", + " parameters = CCParamsCKKSRNS()\n", + " parameters.SetMultiplicativeDepth(multDepth)\n", + " parameters.SetScalingModSize(scalSize)\n", + " parameters.SetFirstModSize(firstModSize)\n", + " parameters.SetRingDim(ringDim)\n", + " \n", + " if securityLevel == 128:\n", + " parameters.SetSecurityLevel(HEStd_128_classic)\n", + " elif securityLevel == 256:\n", + " parameters.SetSecurityLevel(HEStd_256_classic)\n", + " elif securityLevel == 192:\n", + " parameters.SetSecurityLevel(HEStd_192_classic)\n", + " else:\n", + " parameters.SetSecurityLevel(HEStd_NotSet)\n", + " \n", + " parameters.SetBatchSize(batchSize)\n", + " \n", + " context = GenCryptoContext(parameters)\n", + " context.Enable(PKE)\n", + " context.Enable(KEYSWITCH)\n", + " context.Enable(LEVELEDSHE)\n", + " context.Enable(ADVANCEDSHE)\n", + " \n", + " return context\n", + "\n", + "def encrypt_data(context, data, keys):\n", + " \"\"\"Encrypts data using OpenFHE's CKKSPacked encoding.\"\"\"\n", + " pt = context.MakeCKKSPackedPlaintext(data)\n", + " encrypted_data = context.Encrypt(keys.publicKey, pt)\n", + " return encrypted_data\n", + "\n", + "def test_openfhe_encryption():\n", + " \"\"\"Tests OpenFHE encryption and decryption.\"\"\"\n", + " print(\"---- Testing OpenFHE Encryption ----\")\n", + " context = setup_crypto_context(1, 30, 60, 128, 1024, 16384)\n", + " keys = context.KeyGen()\n", + " context.EvalMultKeyGen(keys.secretKey)\n", + " context.EvalSumKeyGen(keys.secretKey)\n", + " \n", + " test_data = [1.5, 2.0, 3.5]\n", + " print(f\"Original data: {test_data}\")\n", + " \n", + " encrypted = encrypt_data(context, test_data, keys)\n", + " decrypted = context.Decrypt(keys.secretKey, encrypted)\n", + " decrypted.SetLength(len(test_data))\n", + " real_values = [val.real for val in decrypted.GetCKKSPackedValue()]\n", + " print(f\"Decrypted values (real part): {real_values}\")\n", + "\n", + "def save_results_to_csv(results, filename=CSV_FILENAME):\n", + " \"\"\"Saves SVM results to a CSV file, including parameters and metrics.\"\"\"\n", + " file_exists = os.path.isfile(filename)\n", + " \n", + " with open(filename, mode='a', newline='') as file:\n", + " fieldnames = [\n", + " \"Trials\", \"Multiplicative Depth\", \"Scaling Mod Size\", \"First Mod Size\",\n", + " \"Security Level\", \"Batch Size\", \"Ring Dimension\",\n", + " \"Avg Encrypted SVM Accuracy\", \"Avg Non-Encrypted SVM Accuracy\",\n", + " \"Accuracy Difference (%)\",\n", + " \"Avg Encrypted Time (sec)\", \"Avg Non-Encrypted Time (sec)\",\n", + " \"Time Difference (sec)\"\n", + " ]\n", + " \n", + " writer = csv.DictWriter(file, fieldnames=fieldnames)\n", + " \n", + " # Write header if file does not exist\n", + " if not file_exists:\n", + " writer.writeheader()\n", + " \n", + " # Write results\n", + " writer.writerow(results)\n", + "\n", + "def run_encrypted_linear_svm(trials, multDepth, scalSize, firstModSize, securityLevel, batchSize, ringDim):\n", + " \"\"\"Runs encrypted Linear SVM classification using OpenFHE and saves results.\"\"\"\n", + " print(\"---- Running Encrypted Linear SVM ----\")\n", + " \n", + " iris = datasets.load_iris()\n", + " X = StandardScaler().fit_transform(iris.data)\n", + " y = iris.target\n", + " \n", + " X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n", + " model = SVC(kernel='linear')\n", + " model.fit(X_train, y_train)\n", + " \n", + " accuracies_encrypted, accuracies_non_encrypted = [], []\n", + " total_time_encrypted, total_time_non_encrypted = 0, 0\n", + " \n", + " for _ in range(trials):\n", + " # Non-encrypted SVM prediction\n", + " t_start = time.time()\n", + " predictions = model.predict(X_test)\n", + " accuracies_non_encrypted.append(accuracy_score(y_test, predictions))\n", + " total_time_non_encrypted += time.time() - t_start\n", + " \n", + " # Encrypted SVM prediction\n", + " t_start = time.time()\n", + " context = setup_crypto_context(multDepth, scalSize, firstModSize, securityLevel, batchSize, ringDim)\n", + " keys = context.KeyGen()\n", + " \n", + " predictions_encrypted = []\n", + " for i in range(len(X_test)):\n", + " encrypted_sample = encrypt_data(context, X_test[i], keys)\n", + " decrypted_sample = context.Decrypt(keys.secretKey, encrypted_sample)\n", + " decrypted_sample.SetLength(len(X_test[i]))\n", + " real_values = [val.real for val in decrypted_sample.GetCKKSPackedValue()]\n", + " prediction = model.predict([real_values])\n", + " predictions_encrypted.append(prediction[0])\n", + " \n", + " accuracies_encrypted.append(accuracy_score(y_test, predictions_encrypted))\n", + " total_time_encrypted += time.time() - t_start\n", + " \n", + " avg_encrypted_acc = np.mean(accuracies_encrypted)\n", + " avg_non_encrypted_acc = np.mean(accuracies_non_encrypted)\n", + " accuracy_difference = (avg_non_encrypted_acc - avg_encrypted_acc) * 100\n", + " avg_encrypted_time = total_time_encrypted / trials\n", + " avg_non_encrypted_time = total_time_non_encrypted / trials\n", + " time_difference = avg_encrypted_time - avg_non_encrypted_time\n", + " \n", + " print(f\"Avg Encrypted SVM Accuracy: {avg_encrypted_acc:.4f}\")\n", + " print(f\"Avg Non-Encrypted SVM Accuracy: {avg_non_encrypted_acc:.4f}\")\n", + " print(f\"Accuracy Difference: {accuracy_difference:.2f}%\")\n", + " print(f\"Avg Encrypted Time: {avg_encrypted_time:.4f} sec\")\n", + " print(f\"Avg Non-Encrypted Time: {avg_non_encrypted_time:.4f} sec\")\n", + " print(f\"Time Difference: {time_difference:.4f} sec\")\n", + "\n", + " # Save results\n", + " results = {\n", + " \"Trials\": trials,\n", + " \"Multiplicative Depth\": multDepth,\n", + " \"Scaling Mod Size\": scalSize,\n", + " \"First Mod Size\": firstModSize,\n", + " \"Security Level\": securityLevel,\n", + " \"Batch Size\": batchSize,\n", + " \"Ring Dimension\": ringDim,\n", + " \"Avg Encrypted SVM Accuracy\": avg_encrypted_acc,\n", + " \"Avg Non-Encrypted SVM Accuracy\": avg_non_encrypted_acc,\n", + " \"Accuracy Difference (%)\": accuracy_difference,\n", + " \"Avg Encrypted Time (sec)\": avg_encrypted_time,\n", + " \"Avg Non-Encrypted Time (sec)\": avg_non_encrypted_time,\n", + " \"Time Difference (sec)\": time_difference\n", + " }\n", + " save_results_to_csv(results)\n", + "\n", + "def main():\n", + " test_openfhe_encryption()\n", + " \n", + " trials, multDepth, scalSize, firstModSize, securityLevel, batchSize, ringDim = get_user_input()\n", + " \n", + " run_encrypted_linear_svm(trials, multDepth, scalSize, firstModSize, securityLevel, batchSize, ringDim)\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "2f4a7ef2", + "metadata": {}, + "source": [ + "---------------------" + ] + }, + { + "cell_type": "markdown", + "id": "e9926e87", + "metadata": {}, + "source": [ + "## B2: Creating a Polynomial Encrypted File\n", + "\n", + "### Steps:\n", + "1. **Edit the script:** \n", + " ```bash\n", + " nano encrypted_svm_polytest.py\n", + " ```\n", + "2. **Run the process:** \n", + " ```bash\n", + " python encrypted_svm_polytest.py\n", + " ```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "480beb6b", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import time\n", + "import csv\n", + "import os\n", + "from openfhe import *\n", + "from sklearn import datasets\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.svm import SVC\n", + "from sklearn.metrics import accuracy_score\n", + "\n", + "def setup_crypto_context(multDepth, scalSize, firstModSize, securityLevel, batchSize, ringDim):\n", + " \"\"\"Sets up the OpenFHE crypto context with user-defined parameters.\"\"\"\n", + " parameters = CCParamsCKKSRNS()\n", + " parameters.SetMultiplicativeDepth(multDepth)\n", + " parameters.SetScalingModSize(scalSize)\n", + " parameters.SetFirstModSize(firstModSize)\n", + " parameters.SetRingDim(ringDim)\n", + "\n", + " if securityLevel == 128:\n", + " parameters.SetSecurityLevel(HEStd_128_classic)\n", + " elif securityLevel == 256:\n", + " parameters.SetSecurityLevel(HEStd_256_classic)\n", + " elif securityLevel == 192:\n", + " parameters.SetSecurityLevel(HEStd_192_classic)\n", + " else:\n", + " parameters.SetSecurityLevel(HEStd_NotSet)\n", + "\n", + " parameters.SetBatchSize(batchSize)\n", + "\n", + " context = GenCryptoContext(parameters)\n", + " context.Enable(PKE)\n", + " context.Enable(KEYSWITCH)\n", + " context.Enable(LEVELEDSHE)\n", + " context.Enable(ADVANCEDSHE)\n", + "\n", + " return context\n", + "\n", + "def encrypt_data(context, data, keys):\n", + " \"\"\"Encrypts data using OpenFHE's CKKSPacked encoding.\"\"\"\n", + " pt = context.MakeCKKSPackedPlaintext(data)\n", + " encrypted_data = context.Encrypt(keys.publicKey, pt)\n", + " return encrypted_data\n", + "\n", + "def decrypt_data(context, encrypted_data, secret_key, data_length):\n", + " \"\"\"Decrypts encrypted data back to plaintext values.\"\"\"\n", + " decrypted = context.Decrypt(secret_key, encrypted_data)\n", + " decrypted.SetLength(data_length)\n", + " return [val.real for val in decrypted.GetCKKSPackedValue()]\n", + "\n", + "def save_results_to_csv(filename, data):\n", + " \"\"\"Saves experiment results to a CSV file, ensuring headers are included.\"\"\"\n", + " file_exists = os.path.exists(filename)\n", + "\n", + " with open(filename, mode='a', newline='') as file:\n", + " writer = csv.writer(file)\n", + "\n", + " # Write header if file does not exist\n", + " if not file_exists:\n", + " writer.writerow([\n", + " \"Trials\", \"Multiplicative Depth\", \"Scaling Mod\", \"First Mod\", \"Security Level\", \"Batch Size\", \"Ring Dim\",\n", + " \"Polynomial Degree\", \"Avg Encrypted Accuracy\", \"Avg Non-Encrypted Accuracy\",\n", + " \"Accuracy Difference (%)\", \"Avg Encrypted Time (sec)\", \"Avg Non-Encrypted Time (sec)\",\n", + " \"Time Difference (sec)\"\n", + " ])\n", + "\n", + " writer.writerow(data)\n", + "\n", + "def test_openfhe_encryption():\n", + " \"\"\"Tests OpenFHE encryption and decryption.\"\"\"\n", + " context = setup_crypto_context(1, 30, 60, 128, 1024, 16384)\n", + " keys = context.KeyGen()\n", + " context.EvalMultKeyGen(keys.secretKey)\n", + " context.EvalSumKeyGen(keys.secretKey)\n", + "\n", + " test_data = [1.5, 2.0, 3.5]\n", + " encrypted = encrypt_data(context, test_data, keys)\n", + " decrypted_values = decrypt_data(context, encrypted, keys.secretKey, len(test_data))\n", + "\n", + " print(\"---- Testing OpenFHE Encryption ----\")\n", + " print(f\"Original data: {test_data}\")\n", + " print(f\"Decrypted values (real part): {decrypted_values}\\n\")\n", + "\n", + "def run_encrypted_svm():\n", + " \"\"\"Runs an encrypted Polynomial SVM with user-defined parameters.\"\"\"\n", + " \n", + " # User Input for Parameters\n", + " trials = int(input(\"Enter number of trials: \"))\n", + " multDepth = int(input(\"Enter Multiplicative Depth (e.g., 1): \"))\n", + " scalSize = int(input(\"Enter Scaling Mod Size (e.g., 30): \"))\n", + " firstModSize = int(input(\"Enter First Mod Size (e.g., 60): \"))\n", + " securityLevel = int(input(\"Enter Security Level (128, 192, 256): \"))\n", + " batchSize = int(input(\"Enter Batch Size (e.g., 1024): \"))\n", + " ringDim = int(input(\"Enter Ring Dimension (e.g., 16384): \"))\n", + " polyDegree = int(input(\"Enter Polynomial Kernel Degree (e.g., 3): \"))\n", + "\n", + " # Load Dataset & Preprocess\n", + " iris = datasets.load_iris()\n", + " X = StandardScaler().fit_transform(iris.data)\n", + " y = iris.target\n", + "\n", + " X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n", + "\n", + " # Train Non-Encrypted SVM\n", + " model = SVC(kernel='poly', degree=polyDegree)\n", + " model.fit(X_train, y_train)\n", + "\n", + " # Track Accuracy & Time\n", + " accuracies_encrypted, accuracies_non_encrypted = [], []\n", + " total_time_encrypted, total_time_non_encrypted = 0, 0\n", + "\n", + " for _ in range(trials):\n", + " # ▪ Non-Encrypted Prediction\n", + " t_start = time.time()\n", + " predictions = model.predict(X_test)\n", + " total_time_non_encrypted += time.time() - t_start\n", + " accuracies_non_encrypted.append(accuracy_score(y_test, predictions))\n", + "\n", + " # ▪ Encrypted Prediction\n", + " t_start = time.time()\n", + " context = setup_crypto_context(multDepth, scalSize, firstModSize, securityLevel, batchSize, ringDim)\n", + " keys = context.KeyGen()\n", + " \n", + " predictions_encrypted = []\n", + " for i in range(len(X_test)):\n", + " encrypted_sample = encrypt_data(context, X_test[i], keys)\n", + " decrypted_sample = decrypt_data(context, encrypted_sample, keys.secretKey, len(X_test[i]))\n", + " prediction = model.predict([decrypted_sample])[0]\n", + " predictions_encrypted.append(prediction)\n", + "\n", + " accuracies_encrypted.append(accuracy_score(y_test, predictions_encrypted))\n", + " total_time_encrypted += time.time() - t_start\n", + "\n", + " # Compute Averages\n", + " avg_encrypted_acc = np.mean(accuracies_encrypted)\n", + " avg_non_encrypted_acc = np.mean(accuracies_non_encrypted)\n", + " avg_encrypted_time = total_time_encrypted / trials\n", + " avg_non_encrypted_time = total_time_non_encrypted / trials\n", + " accuracy_diff = (avg_non_encrypted_acc - avg_encrypted_acc) * 100 # Convert to percentage\n", + " time_diff = avg_encrypted_time - avg_non_encrypted_time # Time difference in seconds\n", + "\n", + " # Print Results\n", + " print(\"---- Running Encrypted Polynomial SVM ----\")\n", + " print(f\"Avg Encrypted SVM Accuracy: {avg_encrypted_acc:.4f}\")\n", + " print(f\"Avg Non-Encrypted SVM Accuracy: {avg_non_encrypted_acc:.4f}\")\n", + " print(f\"Accuracy Difference: {accuracy_diff:.2f}%\")\n", + " print(f\"Avg Encrypted Time: {avg_encrypted_time:.4f} sec\")\n", + " print(f\"Avg Non-Encrypted Time: {avg_non_encrypted_time:.4f} sec\")\n", + " print(f\"Time Difference: {time_diff:.4f} sec\\n\")\n", + "\n", + " # Save to CSV\n", + " csv_filename = \"svm_results_poly.csv\"\n", + " save_results_to_csv(csv_filename, [\n", + " trials, multDepth, scalSize, firstModSize, securityLevel, batchSize, ringDim, polyDegree,\n", + " avg_encrypted_acc, avg_non_encrypted_acc, accuracy_diff, avg_encrypted_time, avg_non_encrypted_time, time_diff\n", + " ])\n", + "\n", + "def main():\n", + " test_openfhe_encryption()\n", + " run_encrypted_svm()\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "a6bf4451", + "metadata": {}, + "source": [ + "## Python Coding (FHE-SVM): Lab Takeaways\n", + "\n", + "This lab implemented privacy-preserving SVM classification using Fully Homomorphic Encryption (FHE) with the **openfhe-python** library. We achieved secure, encrypted computations while maintaining model accuracy.\n", + "\n", + "### Key Insights:\n", + "- **Structured Implementation:** Efficient scripts for model training, data processing, and inference.\n", + "- **Kernel Evaluation:** Both linear and polynomial SVMs performed similarly.\n", + "- **Encryption Impact:** Performance mainly depends on ring dimension and modulus size.\n", + "- **Future Research:** The reproducible framework paves the way for further advancements in encrypted machine learning.\n", + "\n", + "This experiment underscores the balance between encryption security, computational efficiency, and model accuracy, highlighting FHE's potential for secure AI applications." + ] + } + ], + "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.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}