{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Assignment 6 - Linear Cryptanalysis\n", "\n", "\n", "Assignment description and detailed writeup of how linear cryptanalysis works\n", " https://sites.cs.ucsb.edu/~chris/teaching/cs177/projects/proj6.html\n", " \n", "Thanks to Perri rewritten with a lot more details and hints!" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "#\n", "# Import our SPN functions and data\n", "#\n", "from spn import S_BOX, S_BOX_BITS, P_BOX, inverse_map, encrypt_block, step_sbox, step_pbox, step_key, INV_S_BOX, INV_P_BOX\n", "\n", "\n", "#\n", "#Helper for nice pictures :)\n", "#\n", "\n", "from mpl_toolkits.mplot3d import Axes3D # noqa: F401 unused import\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "\n", "def scatter_plot_3d(xs, labels):\n", " fig = plt.figure()\n", " x_label, y_label, z_label = labels\n", " ax = fig.add_subplot(111, projection='3d')\n", " for x, y, z in high_biases:\n", " ax.scatter(x, y, z)\n", " ax.set_xlabel(x_label)\n", " ax.set_ylabel(y_label)\n", " ax.set_zlabel(z_label)\n", " plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Analyzing our substitution box for linear biases" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# also defined in spn.py but put here for clarity\n", "\n", "# this counts the number of bits set to 1 in the number's binary representation\n", "def bcnt(n):\n", " return bin(n)[2:].count('1')\n", "\n", "# this calculates the parity of the masked input and output\n", "def parity(masked_input, masked_output):\n", " return (bcnt(masked_input) + bcnt(masked_output)) % 2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you uncomment the S_BOX in the next box, this one is from a secure algorithm (AES, aka Rijndael).\n", "The linear analysis on this S_BOX should not result in any significantly biased linear relationships." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "#######################################################################\n", "# Secure AES S_BOX, if you uncomment this you can see how it is #\n", "# supposed to be when the S_BOX is not vulnerable. #\n", "#######################################################################\n", "\n", "# S_BOX = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,\n", "# 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,\n", "# 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,\n", "# 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,\n", "# 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,\n", "# 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,\n", "# 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,\n", "# 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,\n", "# 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,\n", "# 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,\n", "# 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,\n", "# 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,\n", "# 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,\n", "# 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,\n", "# 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,\n", "# 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We represent linear relationships by applying bitmasks to the values and then counting the number of 1s in the result.\n", "We know:\n", "\n", "$1 \\bigoplus 1 == 0$ and $0 \\bigoplus 0 == 0$ \n", "\n", "This means that the result of a sequence of bits XOR'ed with each other is 0 if the number of bits with value 1 was even, and the result is 1 if it was odd.\n", "\n", "This next function takes two bitmasks, one for the input and one for the output and prints out what linear equation they encode if we check the resulting parity to be 0." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "def masks_to_linear_relationship(in_mask, out_mask):\n", " input_vars = [f\"X{7-i}\" for i, b in enumerate('{:08b}'.format(in_mask)) if b == '1']\n", " output_vars = [f\"Y{7-i}\" for i, b in enumerate('{:08b}'.format(out_mask)) if b == '1']\n", " result = ''\n", " result += '^'.join(input_vars) if input_vars else '0'\n", " result += ' = '\n", " result += '^'.join(output_vars) if output_vars else '0'\n", " return result" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "X4^X1 = Y0\n" ] } ], "source": [ "print(masks_to_linear_relationship(0x12, 0x1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Actually analyzing the biases of our S_box\n", "\n", "Now we're at the point where we actually have to figure out which linear relationships in our S_BOX have high biases, so that we can use them to determine the correct key! To do this we basically bruteforce all possible input-bitmask-output-bitmask pairs ($2^8$ possible input bitmasks $*$ $2^8$ possible output bitmasks $==$ $2^{16}$ possible options).\n", "\n", "This is equivalent to testing every single possible linear relationship as we saw above.\n", "\n", "For each of them we test for how many of our possible inputs ($2^8$ options) the relationship holds by simply counting.\n", "\n", "Then we filter out those relationships that have biases above some minimum cutoff to reduce the number of candidates." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def compute_counts():\n", " biases = [[0 for _ in range(1 << S_BOX_BITS)] for _ in range(1 << S_BOX_BITS)]\n", " for in_mask in range(1 << S_BOX_BITS):\n", " for out_mask in range(1 << S_BOX_BITS):\n", " for possible_input in range(1 << S_BOX_BITS):\n", " if parity(possible_input & in_mask, S_BOX[possible_input] & out_mask) == 0:\n", " biases[in_mask][out_mask] += 1\n", " return biases\n", "\n", "def cutoff_biases(biases, minval):\n", " rem = []\n", " for in_mask, out_biases in enumerate(biases):\n", " for out_mask, bias in enumerate(out_biases):\n", " if in_mask != 0 and out_mask != 0 and abs(bias) >= minval:\n", " rem.append((in_mask, out_mask, bias))\n", " return rem\n", " " ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "counts = compute_counts()" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "#print(masks_to_linear_relationship(0x1, 0))\n", "#print(masks_to_linear_relationship(0x1, 1))\n", "#print(masks_to_linear_relationship(0x1, 2))\n", "#counts[1]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we calculate the deviations from the expected value ($128 = \\frac{1}{2} * 2^8$) and sort all relationships that are at least 16 away from the expected value by their absolute bias." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "biases = [[c - 128 for c in outcounts] for outcounts in counts]\n", "high_biases = list(sorted(cutoff_biases(biases, 16), key=lambda x: abs(x[2]), reverse=True))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's print out to 20 best linear relationships nicely with their probability and the actual equations." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "with probability 210/256(82%) with a bias of +82: X7^X5^X4^X3^X2^X1 = Y7^Y3^Y2^Y0\n", "with probability 206/256(80%) with a bias of +78: X6^X3^X0 = Y7^Y5^Y4^Y2^Y1^Y0\n", "with probability 204/256(80%) with a bias of +76: X5^X3^X1 = Y5^Y4^Y2^Y1\n", "with probability 52/256(20%) with a bias of -76: X6^X5^X4^X0 = Y6^Y5^Y3^Y2^Y1^Y0\n", "with probability 204/256(80%) with a bias of +76: X6^X5^X4^X2^X1 = Y7^Y5^Y3^Y2\n", "with probability 204/256(80%) with a bias of +76: X7^X6^X5^X4^X3^X2^X0 = Y4^Y1^Y0\n", "with probability 54/256(21%) with a bias of -74: X4^X3^X1^X0 = Y7^Y6^Y4^Y3^Y1^Y0\n", "with probability 202/256(79%) with a bias of +74: X7^X4^X2 = Y7^Y5^Y4^Y3^Y1^Y0\n", "with probability 202/256(79%) with a bias of +74: X7^X6^X5^X3^X1^X0 = Y5^Y1\n", "with probability 202/256(79%) with a bias of +74: X7^X6^X5^X4^X2 = Y5^Y2^Y1\n", "with probability 56/256(22%) with a bias of -72: X5^X3^X2^X0 = Y7^Y6^Y5^Y4^Y2^Y0\n", "with probability 200/256(78%) with a bias of +72: X6^X1^X0 = Y7^Y4^Y3^Y2^Y1\n", "with probability 56/256(22%) with a bias of -72: X6^X4^X1 = Y6^Y5^Y3^Y2\n", "with probability 200/256(78%) with a bias of +72: X6^X5 = Y7^Y4^Y3^Y2^Y0\n", "with probability 200/256(78%) with a bias of +72: X6^X5^X1^X0 = Y7^Y0\n", "with probability 56/256(22%) with a bias of -72: X7^X3^X2 = Y6^Y5^Y4^Y3^Y2\n", "with probability 200/256(78%) with a bias of +72: X7^X4^X1 = Y7^Y6^Y5^Y4^Y3^Y2^Y1^Y0\n", "with probability 200/256(78%) with a bias of +72: X7^X5^X2^X1^X0 = Y7^Y6^Y3^Y2^Y0\n", "with probability 200/256(78%) with a bias of +72: X7^X6^X4^X3^X2^X1 = Y4\n", "with probability 58/256(23%) with a bias of -70: X3 = Y7^Y5^Y4^Y1\n" ] } ], "source": [ "for in_mask, out_mask, bias in high_biases[:20]:\n", " equation = masks_to_linear_relationship(in_mask, out_mask)\n", " c = counts[in_mask][out_mask]\n", " print(f'with probability {c:3d}/256({round(100*c/256)}%) with a bias of {bias:+d}: {equation}')" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "# This graphic is not super helpful other than to recognize the scale \n", "# and number of biased relationships but takes a while to compute, so\n", "# I commented it out for now. You can take a look if you wish, it's a\n", "# bit more informative if you have a higher minimum deviation (e.g. 32)\n", "\n", "# scatter_plot_3d(high_biases, ('Input Mask', 'Output Mask', 'Bias'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's take a look at how our biases are distributed. We can see clear spikes in the center (the gap is because of the treshold we picked above). We can see the large peak around the expected value ($bias = 0$), but then you can see two unexpected spikes of good linear approximations with biases in $[-80, -50]$ and $[50, 80]$.\n", "\n", "These are the relationships we'll end up exploiting. " ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "