Des
I have been reading Applied Cryptography by Bruce Schneider, although not too seriously because it is very outdated. It is still useful because there really aren't many books on Cryptography or any books that actually explain implementations. While reading about block ciphers, I found it helpful to actually implement DES in order to understand what Schneider is saying when it comes to design, security, and attacks on encryption algorithms.
Background
The DES algorithm uses a symmetric-key and is a (64 bit)-block cipher that was once used very widely for encryption. By symmetric-key I mean there's one private key used to both encrypt and decrypt the data, and it does so by acting 64 bit blocks of the data. Exactly how that key is generated and how DES performs cumultaively on data longer than 64 bits are general separate problems for encryption algorithms and block ciphers.
DES Security
DES uses a 64-bit key, but that isn't actually the "true key" in the algorithm. The true key is a 56-bit key obtained from the 64-bit key by ignoring every 8th bit. This makes the algorithm weaker and, according to Schneider, was partly due to the NSA's involvement in the algorithm's development. Schneider speculates that the NSA wanted this to ensure they could utilize sophisticated and expensive hardware to brute force communications if they ever wanted to. As time went on this seemed plausible as researchers created proof of concept hardware specifically desgined for fast DES brute forcing that was within a country's intelligence budget.
However, Schneider also notes that for a period of time there were many attempts by researchers to come up with a better DES, but these replacements were usually pointed out to be weaker by other researchers, and most feasible attacks on DES were usually on a smaller numbers of rounds. So it seems the NSA did something right. Much later did more sophisticated attacks become possible and today the algorithm can be easily brute forced since the necessary hardware to do so has become cheaper over time.
How it works
DES performs 16 repetitions of a sequence of procedures, and these are called "rounds". The \(i\)-th round takes in two 32 bit blocks \(L_i\), \(R_i\), and a 48 bit key \(K_i\). Each \(K_i\) is generated from the secret key \(K\) by a separate procedure. During a round we use a function \(f\) to compute \(f(R_i, K_{i+1})\). We then set \begin{align} L_{i+1} &= R_i\\ R_{i+1} &= L_i \oplus f(R_i, K_{i+1}) \end{align} (where \(\oplus\) denotes XOR) and use these quantities for the next round. The last round is slightly different. Below is an overview of how the whole thing works.
Generating L_0, R_0
To generate \(L_0\) and \(R_0\), we take the 64 bit input and apply an initial permutation (denoted by IP in the diagram above). The initial permutation is given below.
initial_perm = [
58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4,
62, 54, 46, 38, 30, 22, 14, 6, 64, 56, 48, 40, 32, 24, 16, 8,
57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3,
61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7
]
Generating the keys K_i
The \(i\)-th round takes in a 48 bit key \(K_i\) which can be generated on the fly or once before the algorithm. It is calculated as follows.
First, we take the 64 bit key \(K\) and apply a permutation that ignores every 8th bit to obtain a 56 bit key \(K'\). That permutation is given below.
# The initial permutation reducing the 64 bit key to a 56-bit subkey
key_perm = [
57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18,
10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60, 52, 44, 36,
63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22,
14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 28, 20, 12, 4
]
# The size of the circular shift to do in each round
num_key_shifts = [
1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1
]
# The compression permutation to apply to the 56 bit key after circular shift
compression_perm = [
14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10,
23, 19, 12, 4, 26, 8, 16, 7, 27, 20, 13, 2,
41, 52, 31, 37, 47, 55, 30, 40, 51, 45, 33, 48,
44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32
]
The DES rounds
Now that we know how \(L_0\), \(R_0\), and \(K_i\) are calculated, all we need to know is how the function \(f\) works.
The value \(f(R_i, K_{i+1})\) is computed as follows. The first thing we do is apply an expansion permutation to the right half \(R_i\), which produces a 48 bit value from \(R_i\)'s 32 bits by permuting and duplicating its bits. The expansion permutation is below.
# Expansion permutation to apply to right half of input
expansion_perm = [
32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9,
8, 9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17,
16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25,
24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32, 1
]
The S-box substitutions takes the 48 bit value \(E_i\oplus K_i\) and returns a 32 bit value \(S(E_i \oplus K_i)\). It does this by using 8 different S-boxes, which are displayed below.
# S-Boxes
sbox_1 = [
14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13,
]
sbox_2 = [
15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5,
0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9
]
sbox_3 = [
10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12
]
sbox_4 = [
7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14
]
sbox_5 = [
2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3
]
sbox_6 = [
12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13,
]
sbox_7 = [
4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12,
]
sbox_8 = [
13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11
]
The way that an S-box chooses a value in its table to replace the 6 bit value works as follows. Denote the S-box as \(S_i\), and our six bits as \(b_1\), \(b_2\), \(b_3\), \(b_4\), \(b_5\), \(b_6\). First we compute the 2 bit value \(b_1b_6\), which is a number from 0 to 3. Then we compute the 4 bit value \(b_2b_3b_4b_5\) which is a number from 0 to 15. We then replace these 6 bits with the 4 bit value represented in row \(b_1b_6\), column \(b_2b_3b_4b_5\) of S-box \(S_i\).
Once we replace each 6 bit chunk with a 4 bit chunk, we get a 32 bit value, which we may denote as \(S(E_i \oplus K_i)\). We then permute the 32 bit value \(S(E_i \oplus K_i)\) by the permutation below.
# The permutation for the result of the sbox operation
sbox_perm = [
16, 7, 20, 21, 29, 12, 28, 17, 1, 15, 23, 26, 5, 18, 31, 10,
2, 8, 24, 14, 32, 27, 3, 9, 19, 13, 30, 6, 22, 11, 4, 25
]
The final round
We perform 16 rounds, which are all the same except the last round. In the last step of the last round, we instead let \(R_{16} = L_{15}\oplus f(R_{15}, K_{16})\) and \(L_{16} = R_{15}\). We then reconcatenate these two, and apply the inverse of original permutation (denoted by \(\text{IP}^{-1}\) in the diagram above), which is given by the table below.
# The final permutation after the 16 rounds
final_perm = [
40, 8, 48, 16, 56, 24, 64, 32, 39, 7, 47, 15, 55, 23, 63, 31,
38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 6, 29,
36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27,
34, 2, 32, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25,
]
Decryption
What is nice about DES is that the same algorithm can be used for decryption, except you use the keys \(K_i\) in reverse order. This means that decryption is dead simple only if you have the private key, which is a good feature of a block cipher.
Python Code
Now that the algorithm details are explained, here comes the Python code. In this implementation we treat the binary values as a list of ones and zeros. The overall DES function can be described by this simple function.
Note the main function below is for both encryption and decryption. It accepts input and the fake key (i.e. the 64 bit secret key, which I called fake since some of its digits get ignored), and encrypts by default.
def DES(input: list[int], fake_key: list[int], decrypt: bool=False) -> list[int]:
"""The DES encryption algorithm."""
# Apply the initial permutation to the 64-bit input
input = [input[new_pos - 1] for new_pos in initial_perm] # -1 since they count from 1, not 0.
left = input[:32]
right = input[32:]
# Reduce the 64-bit key to a 56-bit subkey, then generate the 16 48 bit subkeys
key = [fake_key[new_pos - 1] for new_pos in key_perm]
sub_keys = generate_subkeys(key, decrypt)
# Perform the 16 rounds of DES
for round in range(0, 16):
left, right = DES_round(left, right, sub_keys[round])
# For the last round, undo the swapping of the left and right blocks
last_block = right + left
last_block = [last_block[new_pos - 1] for new_pos in final_perm]
return last_block
def generate_subkeys(key: list[int], decrypt: bool=False) -> list[list[int]]:
"""Generate the 16 keys used in the DES algorithm."""
sub_keys = []
for round in range(16):
# Split the 56-bit key into two halves
left_key_half = key[:28]
right_key_half = key[28:]
# Circularly shift the bits left, depending on the round
shift = num_key_shifts[round]
left_key_half = [left_key_half[ind % 28] for ind in range(shift, shift + 28)]
right_key_half = [right_key_half[ind % 28] for ind in range(shift, shift + 28)]
# Pick 48 out of the 56 circularly shift bits to form the compressed key
compressed_key = [(left_key_half + right_key_half)[new_pos - 1] for new_pos in compression_perm]
sub_keys.append(compressed_key)
# Preserve the shift, repeat
key = left_key_half + right_key_half
return sub_keys
DES_round
, which computes a single round of DES.
def DES_round(left_half: list[int], right_half: list[int], sub_key: list[int]) -> tuple:
"""Performs a single round of DES."""
# Apply the expansion permutation to the right half, growing from 32 to 48 bits
expanded_right = [right_half[ind - 1] for ind in expansion_perm]
# XOR the compressed key and expanded right
key_input_xor = XOR(sub_key, expanded_right)
# Now we perform the sbox substitutions to obtain a 32 bit number
sbox_res = []
sbox_res += sbox_subsitute(key_input_xor[: 6], sbox_1)
sbox_res += sbox_subsitute(key_input_xor[6:12], sbox_2)
sbox_res += sbox_subsitute(key_input_xor[12:18], sbox_3)
sbox_res += sbox_subsitute(key_input_xor[18:24], sbox_4)
sbox_res += sbox_subsitute(key_input_xor[24:30], sbox_5)
sbox_res += sbox_subsitute(key_input_xor[30:36], sbox_6)
sbox_res += sbox_subsitute(key_input_xor[36:42], sbox_7)
sbox_res += sbox_subsitute(key_input_xor[42:48], sbox_8)
# Permute the result from the sbox operation
sbox_res_permute = [sbox_res[new_pos - 1] for new_pos in sbox_perm]
# Finally we XOR this permutation with the left half of the input to obtain the right half for the next round
next_round_right_half = XOR(left_half, sbox_res_permute)
return right_half, next_round_right_half
def sbox_subsitute(input: list[int], sbox: list[int]) -> list[int]:
"""Return the 4 bit value in sbox to replace the 6 bit input value."""
row = 2*input[0] + input[5]
column = 8 * input[1] + 4 * input[2] + 2 * input[3] + input[4]
return four_bit_binary(sbox[row * 16 + column])
def four_bit_binary(integer: int) -> list[int]:
"""Convert an integer from 0 to 15 to a list of 4 bits."""
return [int(bit) for bit in format(integer, '04b')]
def XOR(A: list[int], B: list[int]) -> list[int]:
"""Simple XOR function."""
assert len(A) == len(B)
xor_val = []
for i in range(len(A)):
a = A[i]
b = B[i]
xor_val.append(int((a or b) and not (a and b)))
return xor_val
An Example
Testing that this works is actually tricky, since most "online DES encoder/decoders" are simply incorrect garbage. Fortunately, I found an example of a DES computation on this website. In the example provided, the message to decode is \(M = 0123456789\text{ABCDEF}\), or $$ M = 00000001 \ 00100011 \ 01000101 \ 01100111 \ 10001001 \ 10101011 \ 11001101 \ 11101111 $$ in binary. They then used the hexidecimal key \(133457799\text{BBCDFF}1\), which in binary is $$ K = 00010011 \ 00110100 \ 01010111 \ 01111001 \ 10011011 \ 10111100 \ 11011111 \ 11110001 $$ The encrypted message becomes $$ C = 10000101 \ 11101000 \ 00010011 \ 01010100 \ 00001111 \ 00001010 \ 10110100 \ 00000101 $$ We can put the message, key in a list of integers
input = [0, 0, 0, 0, 0, 0, 0, 1,
0, 0, 1, 0, 0, 0, 1, 1,
0, 1, 0, 0, 0, 1, 0, 1,
0, 1, 1, 0, 0, 1, 1, 1,
1, 0, 0, 0, 1, 0, 0, 1,
1, 0, 1, 0, 1, 0, 1, 1,
1, 1, 0, 0, 1, 1, 0, 1,
1, 1, 1, 0, 1, 1, 1, 1]
key = [0, 0, 0, 1, 0, 0, 1, 1,
0, 0, 1, 1, 0, 1, 0, 0,
0, 1, 0, 1, 0, 1, 1, 1,
0, 1, 1, 1, 1, 0, 0, 1,
1, 0, 0, 1, 1, 0, 1, 1,
1, 0, 1, 1, 1, 1, 0, 0,
1, 1, 0, 1, 1, 1, 1, 1,
1, 1, 1, 1, 0, 0, 0, 1]
DES(input, key)
returns the value
[1, 0, 0, 0, 0, 1, 0, 1,
1, 1, 1, 0, 1, 0, 0, 0,
0, 0, 0, 1, 0, 0, 1, 1,
0, 1, 0, 1, 0, 1, 0, 0,
0, 0, 0, 0, 1, 1, 1, 1,
0, 0, 0, 0, 1, 0, 1, 0,
1, 0, 1, 1, 0, 1, 0, 0,
0, 0, 0, 0, 0, 1, 0, 1]
The author also points out that the message hexidecimal message M = 8787878787878787" encrypted with the DES key "0E329232EA6D0D73" produces the cipher text "0000000000000000", which also checks out with our function.