scribd.com

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: hp://github.com/chrishulbert/crypto/blob/master/ruby/ruby_des.rb As a learning exercise recently, I set about implemenng a bunch of crypto algorithms from scratch, in such a way as to learn how they work, rather than simply cut and pasng some open source code. Here’s my aempt to document DES in parcular, 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: hp://orlingrabbe.com/des.htmKeep in mind that this implementaon is more aimed at learning how the algorithm works, rather than implemenng it in an opmised way, because I oen nd that these opmisaons make it harder to understand what’s really going on. So, if you keep in mind that this code isn’t producon-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 geng a bit old and insecure now, but Triple DES is sll alive and well, whichis a simple addion to the algorithm that I’ll detail at the end. Basically, there are two steps to DES:

Key expansion

Encrypon / Decrypon

Some utility functions

Ok here’s some ulity funcons 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 permutaon

Split K+ in half to produce C0 and D0

Perform a series of 1 and 2-bit shis 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 permutaon takes the 64 bit key and returns a 56 bit permutaon. So yes, DES in fact throws away 8 bits. Generally they are only used for parity. The permutaon 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 funcon 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 creang 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 shis 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 funcon, 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 permutaon to give us the subkeys K0..K16. This permutaon 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 encrypon steps:

Perform the IP permutaon on the message

Split the results into le and right, giving you L0 and R0

Do 16 encrypon rounds with the keys

o

Ln => Rn-1

o

Rn => Ln-1 + f(Rn-1,Kn)

Note that ‘+’ in the mathemacal formula above really means Xor in implementaon

Swap and concatenate the two sides into R16L16

Perform the IP-1 permutaon 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