Cryptography - Hill Cipher



In the context of classical cryptography, the Hill Cipher uses a polygraphic substitution cipher, which means homogeneous substitution over many levels of blocks. This polygraphic substitution cipher allows Hill Cipher to function easily with digraphs (two-letter blocks), trigraphs (three-letter blocks), or any other multiple-sized blocks to create a uniform cipher.

Hill Cipher is based on linear algebra, advanced matrices (matrix multiplication and matrix inverses), and modulo arithmetic principles. Obviously, it is a more mathematical cipher than others.

Hill Cipher is also a block cipher. A block cipher uses a deterministic algorithm and a symmetric key to encrypt a block of text. Unlike stream ciphers, it does not require encrypting one bit at a time. Hill Cipher is a block cipher, which means it can function with any block size.

While Hill Cipher is digraphic in nature, it can grow to multiply any letter size, adding complexity and reliability for improved usage. Because most of Hill Ciphers' problems and solutions are mathematical in nature, it is simple to hide letters with precision.

Hill Cipher

Since the Hill cipher is fairly difficult, let's encrypt the text "CODE" and then decipher the resulting ciphertext to learn how it works. To keep the example basic, we will use a simple substitution method in which the letter A is mapped to 0, B is mapped to 1, and so on to adhere to a 2x2 key matrix. The Hill cipher becomes more complicated as the key matrix size grows.

History

By using unique methods and techniques, cryptography-the study and practice of secure communication-prevents unauthorised people or teams from obtaining confidential data. Concepts like secrecy, data integrity, authentication, etc. are important in modern cryptography.

The famous American mathematician Lester S. Hill developed and improved the Hill Cipher technique in 1929. The Hill Cipher uses a number of mathematical techniques, which correlate to many key techniques in traditional cryptography.

Encryption

Encrypting using the Hill cipher depends on the following operations −

E(K, P) = (K*P) mod 26

Here K is our key matrix, and P is the vectorized plaintext. Matrix multiplying these two terms gives the encrypted ciphertext. Let's get started this step by step −

  • Choose a keyword for encrypting your plaintext message. Let us use the random keyword "DCDF". Using the substitution technique, change this term into a numerical 2x2 key matrix.

Hill Cipher Encryption
  • Then we will convert our plaintext message to vector format. Because our key matrix is 2x2, matrix multiplication needs a vector of size 2x1. In our example, our message is four letters long, so we can break it into two-letter blocks and then substitute to get our plaintext vectors.

Hill Cipher example
  • The final ciphertext, "WWVA," can be generated by matrix multiplying the key matrix with each 2x1 plaintext vector, taking the moduli of the resulting 2x1 vectors by 26, and concatenating the results. So for 22 22 21 0 will be WWVA.

Final Cipher

Decryption

The Hill cipher decryption process is based on the following operation −

D(K, C) = (K-1 *C) mod 26

Here C is the vectorized ciphertext and K is our key matrix. The decrypted plaintext is obtained by matrix multiplying the reverse of the key matrix with the ciphertext. Let us proceed step-by-step using "WWVA" as our ciphertext −

  • We first calculate the key matrix's inverse. To do this, we must use modulo 26 to maintain the result between 0 and 25. For this reason, the modular multiplicative inverse of the key matrix determinant is found using the Extended Euclidean method.

  • Following that, we will multiply the ciphertext's 2x1 blocks by the key matrix's inverse in order to recover our original plaintext message, "CODE."

Implementation using Python

This Python code builds the Hill Cipher encryption algorithm with the help of NumPy for matrix operations. It creates functions to define the key matrix from a given key, encrypt a message with the help of the generated key matrix, and do the Hill Cipher encryption. The hill_cipher function accepts a message and a key as input, creates the key matrix, also encrypts the message with the key matrix, and prints the output as ciphertext.

Example

Following is the Python implementation of Hill Cipher using numpy library of Python −

import numpy as np

key_matrix = np.zeros((3, 3), dtype=int)
message_vector = np.zeros((3, 1), dtype=int)
cipher_matrix = np.zeros((3, 1), dtype=int)

def get_key_matrix(key):
   k = 0
   for i in range(3):
      for j in range(3):
         key_matrix[i][j] = ord(key[k]) % 65
         k += 1

def encrypt(message_vector):
   for i in range(3):
      cipher_matrix[i][0] = 0
      for x in range(3):
         cipher_matrix[i][0] += (key_matrix[i][x] * message_vector[x][0])
      cipher_matrix[i][0] = cipher_matrix[i][0] % 26

def hill_cipher(message, key):
   get_key_matrix(key)
   for i in range(3):
      message_vector[i][0] = ord(message[i]) % 65
   encrypt(message_vector)
   ciphertext = [chr(cipher_matrix[i][0] + 65) for i in range(3)]
   print("The Ciphertext:", "".join(ciphertext))

message = "DOG"
key = "YHGINUKER"
hill_cipher(message, key)

Following is the output of the above example −

Input/Output

The Ciphertext: YOG

Implementation using Java

This Java code performs both encryption and decryption using the Hill Cipher algorithm. Encryption function takes plaintext and a key matrix. Returns encrypted ciphertext. And decryption function takes ciphertext and a key matrix. Returns original plaintext. And determinant calculates the determinant of a matrix. And computes the adjoint (cofactor matrix) of a matrix. Converts a matrix along its diagonal to transpose a matrix.

Example

See the below code for Java implementation of Hill Cipher −

import java.util.Arrays;

public class HillCipher {
   private static final int MOD = 26;

   public static String encryptText(String plaintext, int[][] key) {
      plaintext = plaintext.toUpperCase().replaceAll(" ", "");
      int n = key.length;
      int padding = n - plaintext.length() % n;
      if (padding != n) {
         plaintext += "X".repeat(padding);
      }

      StringBuilder ciphertext = new StringBuilder();
      for (int i = 0; i < plaintext.length(); i += n) {
         int[] block = new int[n];
         for (int j = 0; j < n; j++) {
            block[j] = plaintext.charAt(i + j) - 'A';
         }
         int[] encryptedBlock = multiplyMatrix(key, block);
         for (int value : encryptedBlock) {
            ciphertext.append((char) (value + 'A'));
         }
      }
      return ciphertext.toString();
   }

   public static String decryptText(String ciphertext, int[][] key) {
      int determinant = determinant(key);
      int adjoint[][] = adjoint(key);
      int n = key.length;
      int[][] inverseKey = new int[n][n];

      for (int i = 0; i < n; i++) {
         for (int j = 0; j < n; j++) {
            inverseKey[i][j] = (adjoint[i][j] * determinant) % MOD;
            if (inverseKey[i][j] < 0) {
               inverseKey[i][j] += MOD;
            }
         }
      }
      return encryptText(ciphertext, inverseKey);
   }

   private static int[] multiplyMatrix(int[][] key, int[] block) {
      int n = key.length;
      int[] result = new int[n];
      for (int i = 0; i < n; i++) {
         for (int j = 0; j < n; j++) {
            result[i] += key[i][j] * block[j];
         }
         result[i] %= MOD;
      }
      return result;
   }

   private static int determinant(int[][] matrix) {
      if (matrix.length == 1) {
         return matrix[0][0];
      }
      int det = 0;
      for (int i = 0; i < matrix.length; i++) {
         int[][] minor = new int[matrix.length - 1][matrix.length - 1];
         for (int j = 1; j < matrix.length; j++) {
            for (int k = 0, col = 0; k < matrix.length; k++) {
               if (k == i) continue;
               minor[j - 1][col++] = matrix[j][k];
            }
         }
         det += Math.pow(-1, i) * matrix[0][i] * determinant(minor);
      }
      return det;
   }

   private static int[][] adjoint(int[][] matrix) {
      int n = matrix.length;
      int[][] adjoint = new int[n][n];
      for (int i = 0; i < n; i++) {
         for (int j = 0; j < n; j++) {
            int[][] minor = new int[n - 1][n - 1];
            for (int k = 0, row = 0; k < n; k++) {
               if (k == i) continue;
               for (int l = 0, col = 0; l < n; l++) {
                  if (l == j) continue;
                  minor[row][col++] = matrix[k][l];
               }
               row++;
            }
            adjoint[i][j] = (int) Math.pow(-1, i + j) * determinant(minor);
         }
      }
      return transpose(adjoint);
   }

   private static int[][] transpose(int[][] matrix) {
      int[][] result = new int[matrix.length][matrix.length];
      for (int i = 0; i < matrix.length; i++) {
         for (int j = 0; j < matrix.length; j++) {
            result[i][j] = matrix[j][i];
         }
      }
      return result;
   }

   public static void main(String[] args) {
      int[][] key = {{6, 24, 1}, {13, 16, 10}, {20, 17, 15}};
      String plaintext = "POINT";
      String ciphertext = encryptText(plaintext, key);
      System.out.println("The Encrypted Text: " + ciphertext);
      String decrypted = decryptText(ciphertext, key);
      System.out.println("The Decrypted Text: " + decrypted);
   }
}

Following is the output of the above example −

Input/Output

The Encrypted Text: SFILBS
The Decrypted Text: POINTX

Implementation using C++

This C++ code implements the Hill Cipher encryption and decryption algorithms. The multiplyMatrix function will perform matrix multiplication. After that The determinant function will calculate the determinant of a matrix. And then the adjoint function calculates the adjoint of a matrix. The encrypt function encrypts plaintext using a key matrix. The decrypt function decrypts ciphertext using a key matrix.

Example

See the C++ implementation of Hill Cipher below −

#include <iostream>
#include <vector>

using namespace std;

const int MOD = 26;

vector<vector<int>> multiplyMatrix(const vector<vector<int>>& key, const vector<int>& block) {
   int n = key.size();
   vector<vector<int>> result(n, vector<int>(1, 0));
   for (int i = 0; i < n; i++) {
      for (int j = 0; j < 1; j++) {
         for (int k = 0; k < n; k++) {
            result[i][j] += key[i][k] * block[k];
         }
         result[i][j] %= MOD;
      }
   }
   return result;
}

   int determinant(const vector<vector<int>>& matrix) {
      if (matrix.size() == 1) {
         return matrix[0][0];
      }
      int det = 0;
      int sign = 1;
      for (int i = 0; i < matrix.size(); i++) {
         vector<vector<int>> minor(matrix.size() - 1, vector<int>(matrix.size() - 1, 0));
         for (int j = 1; j < matrix.size(); j++) {
            for (int k = 0, col = 0; k < matrix.size(); k++) {
               if (k == i) continue;
               minor[j - 1][col++] = matrix[j][k];
            }
         }
         det += sign * matrix[0][i] * determinant(minor);
         sign *= -1;
      }
      return det;
   }

vector<vector<int>> adjoint(const vector<vector<int>>& matrix) {
      int n = matrix.size();
      vector<vector<int>> adjoint(n, vector<int>(n, 0));
      for (int i = 0; i < n; i++) {
         for (int j = 0; j < n; j++) {
            vector<vector<int>> minor(n - 1, vector<int>(n - 1, 0));
            for (int k = 0, row = 0; k < n; k++) {
               if (k == i) continue;
               for (int l = 0, col = 0; l < n; l++) {
                  if (l == j) continue;
                  minor[row][col++] = matrix[k][l];
               }
               row++;
            }
            adjoint[i][j] = determinant(minor) * ((i + j) % 2 == 0 ? 1 : -1);
         }
      }
      vector<vector<int>> result = adjoint;
      for (int i = 0; i < n; i++) {
         for (int j = i + 1; j < n; j++) {
            swap(result[i][j], result[j][i]);
         }
      }
      return result;
   }

   string encrypt(const string& plaintext, const vector<vector<int>>& key) {
      string modifiedPlaintext = plaintext;
      int n = key.size();
      int padding = n - modifiedPlaintext.size() % n;
      if (padding != n) {
         modifiedPlaintext += string(padding, 'X');
      }

      string ciphertext = "";
      for (int i = 0; i < modifiedPlaintext.size(); i += n) {
         vector<int> block(n, 0);
         for (int j = 0; j < n; j++) {
            block[j] = modifiedPlaintext[i + j] - 'A';
         }
         vector<vector<int>> encryptedBlock = multiplyMatrix(key, block);
         for (const auto& row : encryptedBlock) {
            for (int value : row) {
               ciphertext += (char) (value + 'A');
            }
         }
      }
      return ciphertext;
   }

   string decrypt(const string& ciphertext, const vector<vector<int>>& key) {
      int det = determinant(key);
      vector<vector<int>> adj = adjoint(key);
      vector<vector<int>> inverseKey = key;

      int n = key.size();
      for (int i = 0; i < n; i++) {
         for (int j = 0; j < n; j++) {
            inverseKey[i][j] = (adj[i][j] * det) % MOD;
            if (inverseKey[i][j] < 0) {
               inverseKey[i][j] += MOD;
            }
         }
      }
      return encrypt(ciphertext, inverseKey);
   }

int main() {
   vector<vector<int>> key = {{6, 24, 1}, {13, 16, 10}, {20, 17, 15}};
   string plaintext = "HELLO";
   string ciphertext = encrypt(plaintext, key);
   cout << "The Encrypted Text: " << ciphertext << endl;
   string decrypted = decrypt(ciphertext, key);
   cout << "The Decrypted Text: " << decrypted << endl;
   return 0;
}

Following is the output of the above example −

Input/Output

The Encrypted Text: TFJJZX
The Decrypted Text: HELLOX

Advantages

The Hill Cipher encryption algorithm has the following advantages −

  • Security − As the hill cipher works with blocks of letters rather than single letters, it offers more security than other conventional substitution ciphers. It is thus more vulnerable to attacks using frequency analysis.

  • Flexibility − Messages with capital and lowercase letters, punctuation, and spaces can be encrypted and decrypted using the Hill Cipher. Because of its adaptability, it can be used to encrypt a variety of text-based files.

  • Mathematical Background − The Hill Cipher is based on the ideas of linear algebra, and provides a framework for understanding and creating advanced methods for encryption. It provides a chance to look into the connection between encryption techniques and matrices.

  • Key Strength − The size and unpredictability of the encryption key matrix directly affect the security of the Hill cipher. The encryption strength can be raised by applying a larger key matrix, which will make it more challenging for unauthorised parties to decrypt the message.

  • Complexity − The Hill cipher is more difficult to understand without the encryption key since it uses matrix operations in its encryption procedure. This improves the algorithm's security more.

Security of Hill Cipher

It is simple to solve the Hill Cipher when working with 2x2 matrices. However, Hill Ciphers can be quite ineffective when it comes to modern encryption systems that offer 256 possible number possibilities.

It previously proven, the Hill Cipher's linear dependency presents a known vulnerability when it comes to defending against known-plaintext attacks. The Hill Cipher matrices are easily broken by any system that has pairs of linear ciphertexts because it only uses conventional algebraic procedures for solution.

However, increasing the number of matrix multiplications does not much to strengthen system security. On the other hand, it can support diffusion when combined with non-linear procedures. Different diffusion is used by modern advanced encryption techniques like AES to better strengthen system security.

Lester S. Hill developed a special machine for a 6x6 matrix cipher that proved increased security. Its practical applications were limited by the fact that its main settings were not adjustable.

Advertisements