Simple Data Encryption Standard (SDES) in Zig (Part-2)

This is part–2 of a two–part post implementing SDES in Zig. In part–1, we generated two 8-bit keys for the two rounds of encryption in SDES. Here, we will explore the encryption process in detail.

The plaintext is passed through permutation, complex function and switch blocks to generate the ciphertext
The plaintext is passed through permutation, complex function and switch blocks to generate the ciphertext

Let’s begin by importing the zig standard library and the key module created in part–1:

const std = @import("std");
const keys = @import("key");

Then, we generate two 8–bit keys by calling the generateKey function with the 10–bit key 1010000010:

const key_1_2: struct {u8, u8} = try keys.generateKey(0b1010000010);
const key_1: u8 = key_1_2[0];
const key_2: u8 = key_1_2[1];

Key Generation

  • Key–1: 10100100
  • Key–2: 01000011

Step–1: Initial Permutation (IP)

Let’s assume our plaintext is 01110010. Initially, the bits are permutated in the following way:

IP permutation scheme. Bit–0 is mapped to bit–2, bit–1 is mapped to bit–0 and so on
IP permutation scheme. Bit–0 is mapped to bit–2, bit–1 is mapped to bit–0 and so on

We implement this with bit shifts:

const plaintext: u8 = 0b01110010;
const ip_output: u8 = ((plaintext >> 0) & 0b1) << 2 | // bit0 -> bit2
                      ((plaintext >> 1) & 0b1) << 0 | // bit1 -> bit0 
                      ((plaintext >> 2) & 0b1) << 6 | // bit2 -> bit6 
                      ((plaintext >> 3) & 0b1) << 1 | // bit3 -> bit1 
                      ((plaintext >> 4) & 0b1) << 3 | // bit4 -> bit3 
                      ((plaintext >> 5) & 0b1) << 5 | // bit5 -> bit5 
                      ((plaintext >> 6) & 0b1) << 7 | // bit6 -> bit7 
                      ((plaintext >> 7) & 0b1) << 4 ; // bit7 -> bit4 

The output of IP is 10101001.

IP

  • Input : 01110010
  • Output: 10101001

Step–2: Complex Function f_k

The next step is to apply the complex function f_k. The encryption block diagram shows that this function is applied twice in the whole process. The structure of the function looks like this:

Block diagram of complex function f_k
Block diagram of complex function f_k

The function takes the 3 inputs: an 8–bit key, the left and right half of the IP output.So, we have to split the output of IP into two halves of 4–bits:

const ip_right_nibble: u4 = @truncate(ip_output);
const ip_left_nibble: u4 = @truncate(ip_output >> 4);  

IP Split

  • Left : 1010
  • Right: 1001

Now we enter the complex function. The first step here is to extend the right half of IP (1001) using the Extended Permutation (EP):

Extended Permutation Scheme. Here, the four bits are extended into eight bits. Bit–0 is mapped to both bit–1 and bit–7, and so on
Extended Permutation Scheme

Before starting the bit shifts, we need to represent the right half (u4) as a u8:

const ip_right_nibble_expanded = @as(u8, ip_right_nibble);
const ep_output: u8 = ((ip_right_nibble_expanded >> 0) & 0b1) << 1 | // bit0 -> bit1
                      ((ip_right_nibble_expanded >> 0) & 0b1) << 7 | // bit0 -> bit7
                      ((ip_right_nibble_expanded >> 1) & 0b1) << 2 | // bit1 -> bit2 
                      ((ip_right_nibble_expanded >> 1) & 0b1) << 4 | // bit1 -> bit4 
                      ((ip_right_nibble_expanded >> 2) & 0b1) << 3 | // bit2 -> bit3 
                      ((ip_right_nibble_expanded >> 2) & 0b1) << 5 | // bit2 -> bit5 
                      ((ip_right_nibble_expanded >> 3) & 0b1) << 0 | // bit3 -> bit1 
                      ((ip_right_nibble_expanded >> 3) & 0b1) << 6 ; // bit3 -> bit1 

The output of EP is 11000011.

EP

  • Input : 1001
  • Output: 11000011


Next, we XOR the output of EP with Key–1:

const xor_ep_key: u8 = ep_output ^ key_1;

The result of the operation is 01100111.

XOR

  • Input : 11000011
  • Output: 01100111


The output of the XOR operation is again split into two halves (0110 and 0111):

const left: u4 = @truncate(xor_ep_key >> 4);
const right: u4 = @truncate(xor_ep_key);

These two halves are then fed into two substitution schemes known as the S–boxes. A S–box in SDES is defined as a 4x4 matrix. We have two S–boxes in the complex function, S0 and S1:

// the binary values are written in their decimal representations
const S0: [16]u2 = .{1, 0, 3, 2,
                     3, 2, 1, 0,
                     0, 2, 1, 3,
                     3, 1, 3, 2};

const S1: [16]u2 = .{0, 1, 2, 3,
                     2, 0, 1, 3,
                     3, 0, 1, 0,
                     2, 1, 0, 3};

Let’s calculate the output of S0 box first. The output is one of the 16 elements from the matrix. Because its a matrix, all the elements are identified by their row and column indices. For S0, we find the row and column indices from the left half of the XOR output:

Finding row and column for S0
Finding row and column for S0

And then we find the output of S0 at row 0, column 3 of the S0 matrix:

We repeat this process for the right half and S1. Finally, we combine the outputs of S0 and S1 boxes into a 4–bit binary.

const s0_row: u2 = @truncate(((left >> 3) & 0b1) << 1 | (left & 0b1)); 
const s0_col: u2 = @truncate(((left >> 2) & 0b1) << 1 | ((left >> 1) & 0b1)); 
const s0_idx: u8 = @as(u8, s0_row) * 4 + s0_col;
const s0_output:u2 = S0[s0_idx];

const s1_row: u2 = @truncate(((right >> 3) & 0b1) << 1 | (right & 0b1)); 
const s1_col: u2 = @truncate(((right >> 2) & 0b1) << 1 | ((right >> 1) & 0b1)); 
const s1_idx: u8 = @as(u8, s1_row) * 4 + s1_col;
const s1_output:u2 = S1[s1_idx];

const s0_s1_combined: u4 = @as(u4, s0_output) << 2 | s1_output;

S–Boxes

  • S0

Input : 0110
Output: 10

  • S1

Input : 0111
Output: 11

  • Combined: 1011


The S–boxes are followed by a P4 permutation block. P4 rearranges the 4–bit output from the S-boxes in the following way:

P4 permutation scheme
P4 permutation scheme

const p4_output: u4 = ((s0_s1_combined >> 0) & 0b1) << 2 | // bit0 -> bit2
                      ((s0_s1_combined >> 1) & 0b1) << 1 | // bit1 -> bit1 
                      ((s0_s1_combined >> 2) & 0b1) << 3 | // bit2 -> bit3 
                      ((s0_s1_combined >> 3) & 0b1) << 0 ; // bit3 -> bit0 

P4

  • Input : 1011
  • Output: 0111


Finally, the complex function ends with another XOR operation. The operands are the output of P4, and the left half of IP (1010) that we have calculated back at step–2:

const fk1_left_output = ip_left_nibble ^ p4_output;
const fk1_right_output = ip_right_nibble;

The outputs of f_k are the result of the XOR and the right half from the IP.

f_k

  • Output:

Left : 1101
Right: 1001

Step–3: Switch SW

The left and right halves are swapped or switched before entering the 2nd complex function (f_k) block:

SW

  • Input:

Left : 1101
Right: 1001

  • Output:

Left : 1001
Right: 1101

Step–4: Second Complex Function

The bits from SW are passed to the complex function again. Hence, all the steps of Step–2 (except IP) are repeated to find the output of f_k:

f_k

  • Output:

Left : 1110
Right: 1101

Step–5: Inverse Initial Permutation

Output of f_k are combined to form the 8–bit number 11101101.

const fk_2_output_combined: u8 = @as(u8, fk2_left_output) << 4 | fk2_right_output;

This 8–bit number is rearranged with a scheme that is inverse of the Initial Permutation (IP):

Inverse IP permutation scheme
Inverse IP permutation scheme

const inverse_ip_output: u8 = ((fk_2_output_combined >> 2) & 0b1) << 0 | //bit2 -> bit0 
                              ((fk_2_output_combined >> 0) & 0b1) << 1 | //bit0 -> bit1  
                              ((fk_2_output_combined >> 6) & 0b1) << 2 | //bit6 -> bit2  
                              ((fk_2_output_combined >> 1) & 0b1) << 3 | //bit1 -> bit3  
                              ((fk_2_output_combined >> 3) & 0b1) << 4 | //bit3 -> bit4  
                              ((fk_2_output_combined >> 5) & 0b1) << 5 | //bit5 -> bit5  
                              ((fk_2_output_combined >> 7) & 0b1) << 6 | //bit7 -> bit6  
                              ((fk_2_output_combined >> 4) & 0b1) << 7 ; //bit4 -> bit7  

Inverse IP

  • Input : 11101101
  • Output: 01110111

The output of the inverse IP is our ciphertext:

ciphertext

01110111

Wrapping Up

We can write a function to wrap the complex function logic. We can then just call this function twice with SW code sandwitched between them.

fn complexFunc(key: u8, left_nibble: u4, right_nibble: u4) struct {u4, u4}{
    const ip_right_nibble_expanded = @as(u8, right_nibble);
    const ep_output: u8 = ((ip_right_nibble_expanded >> 0) & 0b1) << 1 | // bit0 -> bit1
                          ((ip_right_nibble_expanded >> 0) & 0b1) << 7 | // bit0 -> bit7
                          ((ip_right_nibble_expanded >> 1) & 0b1) << 2 | // bit1 -> bit2 
                          ((ip_right_nibble_expanded >> 1) & 0b1) << 4 | // bit1 -> bit4 
                          ((ip_right_nibble_expanded >> 2) & 0b1) << 3 | // bit2 -> bit3 
                          ((ip_right_nibble_expanded >> 2) & 0b1) << 5 | // bit2 -> bit5 
                          ((ip_right_nibble_expanded >> 3) & 0b1) << 0 | // bit3 -> bit1 
                          ((ip_right_nibble_expanded >> 3) & 0b1) << 6 ; // bit3 -> bit1 


    const xor_ep_key: u8 = ep_output ^ key;
    const left: u4 = @truncate(xor_ep_key >> 4);
    const right: u4 = @truncate(xor_ep_key);

    const S0: [16]u2 = .{1, 0, 3, 2,
                         3, 2, 1, 0,
                         0, 2, 1, 3,
                         3, 1, 3, 2};

    const S1: [16]u2 = .{0, 1, 2, 3,
                         2, 0, 1, 3,
                         3, 0, 1, 0,
                         2, 1, 0, 3};

    const s0_row: u2 = @truncate(((left >> 3) & 0b1) << 1 | (left & 0b1)); 
    const s0_col: u2 = @truncate(((left >> 2) & 0b1) << 1 | ((left >> 1) & 0b1)); 
    const s0_idx: u8 = @as(u8, s0_row) * 4 + s0_col;
    const s0_output:u2 = S0[s0_idx];

    const s1_row: u2 = @truncate(((right >> 3) & 0b1) << 1 | (right & 0b1)); 
    const s1_col: u2 = @truncate(((right >> 2) & 0b1) << 1 | ((right >> 1) & 0b1)); 
    const s1_idx: u8 = @as(u8, s1_row) * 4 + s1_col;
    const s1_output:u2 = S1[s1_idx];

    const s0_s1_combined: u4 = @as(u4, s0_output) << 2 | s1_output;

    const p4_output: u4 = ((s0_s1_combined >> 0) & 0b1) << 2 | // bit0 -> bit2
                          ((s0_s1_combined >> 1) & 0b1) << 1 | // bit1 -> bit1 
                          ((s0_s1_combined >> 2) & 0b1) << 3 | // bit2 -> bit3 
                          ((s0_s1_combined >> 3) & 0b1) << 0 ; // bit3 -> bit0 

    const left_output = left_nibble ^ p4_output;
    return .{left_output, right_nibble};
}



We can write another function encrypt that will do IP, call the complex functions, and finish off with the Inverse IP.

pub fn encrypt(key: u10, plaintext: u8) u8 {

    const key_1_2 = try keys.generateKey(key);
    const ip_output: u8 = ((plaintext >> 0) & 0b1) << 2 | // bit0 -> bit2
                          ((plaintext >> 1) & 0b1) << 0 | // bit1 -> bit0 
                          ((plaintext >> 2) & 0b1) << 6 | // bit2 -> bit6 
                          ((plaintext >> 3) & 0b1) << 1 | // bit3 -> bit1 
                          ((plaintext >> 4) & 0b1) << 3 | // bit4 -> bit3 
                          ((plaintext >> 5) & 0b1) << 5 | // bit5 -> bit5 
                          ((plaintext >> 6) & 0b1) << 7 | // bit6 -> bit7 
                          ((plaintext >> 7) & 0b1) << 4 ; // bit7 -> bit4 
    const ip_right_nibble: u4 = @truncate(ip_output);
    const ip_left_nibble: u4 = @truncate(ip_output >> 4);  

    const fk_1_output = complexFunc(key_1_2[0], ip_left_nibble, ip_right_nibble);

    const fk_2_output = complexFunc(key_1_2[1], fk_1_output[1], fk_1_output[0]);
    const fk_2_output_combined: u8 = @as(u8, fk_2_output[0]) << 4 | fk_2_output[1];


    const inverse_ip_output: u8 = ((fk_2_output_combined >> 2) & 0b1) << 0 | // bit0 -> bit2
                                  ((fk_2_output_combined >> 0) & 0b1) << 1 | // bit1 -> bit0 
                                  ((fk_2_output_combined >> 6) & 0b1) << 2 | // bit2 -> bit6 
                                  ((fk_2_output_combined >> 1) & 0b1) << 3 | // bit3 -> bit1 
                                  ((fk_2_output_combined >> 3) & 0b1) << 4 | // bit4 -> bit3 
                                  ((fk_2_output_combined >> 5) & 0b1) << 5 | // bit5 -> bit5 
                                  ((fk_2_output_combined >> 7) & 0b1) << 6 | // bit6 -> bit7 
                                  ((fk_2_output_combined >> 4) & 0b1) << 7 ; // bit7 -> bit4 
    

    return inverse_ip_output;
}
Simple Data Encryption Standard (SDES) in Zig (Part-1)