How to Implement DES in Ruby
- ️Mon Apr 28 2025
How to implement DES and Triple DES from scratch: A simple Ruby implementation with lots of comments
By Chris Hulbert - chris.hulbert@gmail.com Full code can be found here: hp://github.com/chrishulbert/crypto/blob/master/ruby/ruby_des.rb As a learning exercise recently, I set about implemenng a bunch of crypto algorithms from scratch, in such a way as to learn how they work, rather than simply cut and pasng some open source code. Here’s my aempt to document DES in parcular, 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: hp://orlingrabbe.com/des.htmKeep in mind that this implementaon is more aimed at learning how the algorithm works, rather than implemenng it in an opmised way, because I oen nd that these opmisaons make it harder to understand what’s really going on. So, if you keep in mind that this code isn’t producon-ready, we should get along just ne.
DES quick summary
DES is a block cipher, eg it can encrypt/decrypt a block at a me. It uses 64 bit blocks, and 64 bit keys – in other words, it works on 8 bytes at a me. It’s geng a bit old and insecure now, but Triple DES is sll alive and well, whichis a simple addion to the algorithm that I’ll detail at the end. Basically, there are two steps to DES:
Key expansion
Encrypon / Decrypon
Some utility functions
Ok here’s some ulity funcons we’ll need to get started:
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 endendclass 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 endend
Expanding the key
Expanding the DES key is performed by the following steps:
Permute the 64-bit key to produce the 56 bit K+, using the PC1 permutaon
Split K+ in half to produce C0 and D0
Perform a series of 1 and 2-bit shis to produce C1..16 and D1..16
Permute each of C1D1..C16D16 to produce the subkeys K1..K16
In code, this’ll look like:
# Take a 64 bit key, and return all the subkeys K0..K16def expand(k) kplus = k.pc1 # Run the key through PC1 to give us "K+" c0, d0 = kplus.split # Split K+ into C0D0 cdn = shifts(c0, d0) # Do the shifts to give us CnDn cdn.map{|cd| cd.pc2} # For each CnDn, run it through PC2 to give us "Kn"end
So lets cover each step in detail:
PC1 Permutation
The PC1 permutaon takes the 64 bit key and returns a 56 bit permutaon. So yes, DES in fact throws away 8 bits. Generally they are only used for parity. The permutaon 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 # Perform the PC1 permutation on the current array # This is used to take the original 64 bit key "K" and return 56 bits "K+" def pc1 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 " endend
So, the 57
th
bit is now the rst bit, and so on.
Split
This funcon is simple, we are just halving a set of bits into two equal le 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]] endend
Shifts
At this stage of creang the subkeys, we get our le and right values from above called C0 and D0, and create C1..C16 and D1..D16. Each me we create the next C/D pair, we do a le-shi either one or two mes. The list of shis 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 funcon, rather than returning a list of C’s and a list of D’s, we concatenate them to form CD0,CD1.
# Performs the shifts to produce CnDndef shifts(c0,d0) cn, dn = [c0], [d0] # This is the schedule of shifts. Each CnDn is produced by shifting the previous by 1 or 2 bits [1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1].each{|n| cn << cn.last.left(n) dn << dn.last.left(n) } cdn=[] cn.zip(dn) {|c,d| cdn << (c+d)} # Concatenate the c's and d's to produce CDn cdnendclass Array # shift this array one or two bits left def left(n)
self[n,self.length] + self[0,n] endend
PC2 permutation
Once we’ve got our list of CD0..CD16, we need to run each of them through the PC2 permutaon to give us the subkeys K0..K16. This permutaon 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" endend
Testing the key expansion:
Let’s test that the subkeys are made correctly:
# Step 1, make the subkeysk = '00010011 00110100 01010111 01111001 10011011 10111100 11011111 11110001'.to_bits # This is the keysubkeys = expand(k)puts "Key: " + k.pretty(8)subkeys.each_with_index { |sk,i| puts "Subkey %2d: %s" % [i,sk.pretty(6)]}
Your subkey #16 should be:
110010 110011 110110 001011 000011 100001 011111 110101
Encrypting
Now we’ve got the subkeys, we can perform the encrypon steps:
Perform the IP permutaon on the message
Split the results into le and right, giving you L0 and R0
Do 16 encrypon rounds with the keys
o
Ln => Rn-1
o
Rn => Ln-1 + f(Rn-1,Kn)
Note that ‘+’ in the mathemacal formula above really means Xor in implementaon
Swap and concatenate the two sides into R16L16
Perform the IP-1 permutaon to give the resultIn code, this looks like:
# Take a 8 byte message and the expanded keys, and des encrypt itdef 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" cryptotextend