How To Implement DES in Ruby
How To Implement DES in Ruby
As a learning exercise recently, I set about implementing a bunch of crypto algorithms from scratch, in such a way as
to learn how they work, rather than simply cut and pasting some open source code. Here’s my attempt to document
DES in particular, in as simple a way as possible to make it easy to follow and learn from.
This document references the following, which is a very good one-stop-shop for learning DES too:
http://orlingrabbe.com/des.htm
Keep in mind that this implementation is more aimed at learning how the algorithm works, rather than
implementing it in an optimised way, because I often find that these optimisations make it harder to understand
what’s really going on. So, if you keep in mind that this code isn’t production-ready, we should get along just fine.
Key expansion
Encryption / Decryption
class String
# Convert a "1010..." string into an array of bits
def to_bits
bitarr=[]
self.each_char{|c| bitarr << c.to_i if c=='0' || c=='1'}
bitarr
end
end
class Array
# Join this array into a nicely grouped string
def pretty(n=8)
s=""
self.each_with_index{|bit,i| s+=bit.to_s; s+=' ' if (i+1)%n==0}
s
end
end
Permute the 64-bit key to produce the 56 bit K+, using the PC1 permutation
Split K+ in half to produce C0 and D0
Perform a series of 1 and 2-bit shifts to produce C1..16 and D1..16
Permute each of C1D1..C16D16 to produce the subkeys K1..K16
In code, this’ll look like:
PC1 Permutation
The PC1 permutation takes the 64 bit key and returns a 56 bit permutation. So yes, DES in fact throws away 8 bits.
Generally they are only used for parity. The permutation code basically re-orders the bits like this:
class Array
# Perform a bitwise permutation on the current array, using the passed permutation table
def perm(table)
table.split(' ').map{ |bit| self[bit.to_i-1] }
end
So, the 57th bit is now the first bit, and so on.
Split
This function is simple, we are just halving a set of bits into two equal left and right halves:
class Array
# split this array into two halves
def split
[self[0,self.length/2], self[self.length/2,self.length/2]]
end
end
Shifts
At this stage of creating the subkeys, we get our left and right values from above called C0 and D0, and create
C1..C16 and D1..D16. Each time we create the next C/D pair, we do a left-shift either one or two times. The list of
shifts is this: 1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1. To make it easier for the next stage, at the end of this function, rather
than returning a list of C’s and a list of D’s, we concatenate them to form CD0,CD1.
class Array
# shift this array one or two bits left
def left(n)
self[n,self.length] + self[0,n]
end
end
PC2 permutation
Once we’ve got our list of CD0..CD16, we need to run each of them through the PC2 permutation to give us the
subkeys K0..K16. This permutation takes the 56-bit CDn and produces the 48 bit Kn:
class Array
# Perform the PC2 permutation on the current array
# This is used on each of the 56 bit "CnDn" concatenated pairs to produce
# each of the 48 bit "Kn" keys
def pc2
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"
end
end
Encrypting
Now we’ve got the subkeys, we can perform the encryption steps:
# Take a 8 byte message and the expanded keys, and des encrypt it
def des_encrypt(m,keys)
ip = m.ip # Run it through the IP permutation
l, r = ip.split # Split it to give us L0R0
(1..16).each { |i| # Run the encryption rounds
l, r = r, l.xor(f(r,keys[i])) # L => R, R => L + f(Rn-1,Kn)
}
rl = r + l # Swap and concatenate the two sides into R16L16
c = rl.ip_inverse # Run IP-1(R16L16) to give the final "c" cryptotext
end
Now we’ll need to implement the new functions introduced in the encrypt function above.
IP permutation
The IP permutation takes 64 bits, reorders them, and outputs 64 bits. The 58 th bit becomes the 1st bit, etc:
class Array
# This performs the initial permutation aka "IP"
# This is the first thing applied to the 64 bit message "M" to give us "IP"
# Inputs 64 bits, outputs 64 bits
def ip
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"
end
end
Xor
We need a way to XOR the previous left bits with the results of the f(…) function:
class Array
# xor's this and the other array
def xor(b)
i=0
self.map{|a| i+=1; a^b[i-1]}
end
end
E Bit permutation
The E-Bit permutation expands 32bits input to 48 bits output by repeating some bits. The 32 nd bit becomes the first
bit, and so on:
class Array
# This is the E-Bit selection table
# It inputs 32 bits and outputs 48 bits
# This is used in the 'f' function to calculate "E(Rn-1)" on each of the rounds
def e_bits
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"
end
end
Split6
This function takes the 48-bit output of Kn + E(Rn-1) and splits it into 8 groups of 6 bits, ready to be fed to the s-
boxes:
class Array
# splits into arrays of 6 bits
def split6
arr=[]
subarr=[]
self.each{|a|
subarr<<a
if subarr.length==6
arr<<subarr
subarr=[]
end
}
arr
end
end
Substitution Boxes
Each of the 8 groups of 6 bits, in turn, gets fed into an S-Box. There are 8 different S-boxes, one for each group. The
result is looked up, and converted to 4 bits then returned. An S-Box looks like this:
S7
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
The first and last bits of the 6-bit group represent the row, and the middle 4 bits represent the column in the S-box.
So if the bits were 100001, the row is 11 = 3+1 = 4th row (+1 because it is 0-based), and the middle bits represent
column 0, and so the S-box output is 6 as you can see in the table above. The S-box function looks like this:
class Array
# The S-Box lookup
# This takes the 6 bits input and produces 4 bits output
# The 'b' variable is which s-box table to use
# This is used in the 'f' function. "Kn+E(Rn-1)" is calculated then split
# into 6-bit blocks B1..B8, each of which is passed through the s-box S1..S8
def s_box(b)
s_tables = "
S1
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
S2
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
S3
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
S4
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
S5
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
S6
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
S7
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
S8
13 2 8 4 6 15 11 10 91 3 14 5 0 12 7
1 15 13 8 10 3 7 12 54 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
"
# Find only the table they want
s_table = s_tables[s_tables.index('S%d'%b)+3,999]
s_table = s_table[0,s_table.index('S')] if s_table.index('S')
s_table = s_table.split(' ') # Convert from text to usable array
row = self.first*2 + self.last # The row is found from the first and last bits
col = self[1]*8 + self[2]*4 + self[3]*2 + self[4] # The column is from the middle 4 bits
s_table[row*16+col].to_i.to_bits # The correct value is looked up, then converted to 4 bits output
end
end
class Integer
# Converts an integer into a 4-bit array, as used by the s-boxes
def to_bits
[self>>3, (self>>2)&1, (self>>1)&1, self&1]
end
end
P Permutation
Once all the groups of 6 bits have been passed through the s-boxes to yield 8 groups of 4 bits, they are concatenated
to form 32 bits. These 32 bits are passed through the P permutation, and the 32 bits output is returned as the result
of the f(…) function. Here is the code, where you can see the 16 th bit becomes the 1st bit, and so on:
class Array
# The P permutation
# Inputs 32 bits, outputs 32 bits
# At the end of the 'f' function, this is run on the concatenated results of the s-boxes
def perm_p
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"
end
end
Inverse IP Permutation
After using all the subkeys, R and L are concatenated and passed through the IP-1 permutation and returned as the
final encrypted output. The 40th bit becomes the 1st bit, etc:
class Array
# The IP^-1 final permutation
# Inputs 64 bits, outputs 64 bits
# At the end of the rounds, this is run over "R16L16" to produce the final result
def ip_inverse
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 61 29
36 4 44 12 52 20 60 28
35 3 43 11 51 19 59 27
34 2 42 10 50 18 58 26
33 1 41 9 49 17 57 25"
end
end
# Step 2, encode it
m = '0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111'.to_bits # The message to encode
c = des_encrypt(m,subkeys)
puts "Encrypt: " + c.pretty(8) # The output value
Decryption
Decryption is performed the same as encryption, except the subkeys are applied in reverse order:
# Take a 8 byte message and the expanded keys, and des decrypt it
def des_decrypt(m,keys)
ip = m.ip # Run it through the IP permutation
l, r = ip.split # Split it to give us L0R0
(1..16).to_a.reverse.each { |i| # Run the encryption rounds
l, r = r, l.xor(f(r,keys[i])) # L => R, R => L + f(Rn-1,Kn)
}
rl = r + l # Swap and concatenate the two sides into R16L16
c = rl.ip_inverse # Run IP-1(R16L16) to give the final "c" cryptotext
end
# Step 3, decode it
d = des_decrypt(c,subkeys)
puts "Decrypt: " + d.pretty(8) # The output value
Triple DES
Triple DES is a variant of DES that uses two DES keys that are applied three times (hence the triple), on a standard 64
bit DES message block. When encrypting, it does a (single) DES encrypt with the first key, DES decrypt with the
second key, then encrypt again with the first key. Decrypting is simple the reverse:
Triple Des Message: 00010010 00110100 01010110 01111000 10010000 10101011 11001101 11101111
Triple Des Encrypt: 00111010 00111010 11001110 01100101 00001101 10110011 10111011 11011100
Triple Des Decrypt: 00010010 00110100 01010110 01111000 10010000 10101011 11001101 11101111
That’s it, thanks for reading! The full code can be found here:
http://github.com/chrishulbert/crypto/blob/master/ruby/ruby_des.rb