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.
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:
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:
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):
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:
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:
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):
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;
}