Exploring Cryptography Fundamentals in Ruby

Jesus Castello
Share

Cryptography is used all over the world to protect data from prying eyes. If you want to protect your own data you should be familiar with this topic.

To help you get started, I will show you the main concepts & algorithms that you need to know, and I will include some Ruby examples so you can see the concepts in action.

Primitive Ciphers

Cryptography was already in use way before computers existed. For example you may have heard about the Caesar cipher.

In the Caesar cipher letters are shifted X positions. The number of positions is the key needed to recover the original message.

It’s trivial to break this cipher using a computer, so what we use today are algorithms that take advantage of some mathematical problems (like prime factorization) that are hard to solve, even for the most powerful computers in the world.

Modern cryptography algorithms can be divided into three groups:

  • Symmetric ciphers
  • Asymmetric ciphers
  • Hash functions

Symmetric Ciphers

A symmetric algorithm uses one secret key to encode & decode data. This is the kind of algorithm that you would use to protect an important file on your computer.

Note that you should not use this for storing passwords because a symmetric algorithm can easily be reversed if you have the key (and the key needs to be on the system if you want to use the encrypted data without human interaction). What you want for storing passwords is called a ‘hashing function’ (covered later in this article).

Here are some popular symmetric algorithms:

  • DES
  • Triple DES
  • AES
  • Blowfish

You can use the OpenSSL library to work with many symmetric ciphers in Ruby.

Example:

require 'openssl'

cipher = OpenSSL::Cipher.new('aes256')
cipher.encrypt

key = cipher.random_key
iv  = cipher.random_iv

encrypted = cipher.update('test') + cipher.final

# You need to save both the IV (initialization vector) + the key

If you want to use your own key instead of the one generated by OpenSSL, you need to use a password derivation function like PBKDF#2 (Password-Based Key Derivation Function 2). Here is how you can do that:

require 'openssl'

cipher = OpenSSL::Cipher.new('aes256')
cipher.encrypt

iv  = cipher.random_iv

# Password derivation
salt = OpenSSL::Random.random_bytes(16)
key  = OpenSSL::PKCS5.pbkdf2_hmac_sha1('password', salt, 20_000, cipher.key_len)

cipher.key = key

encrypted = cipher.update('test') + cipher.final

For decryption you only need to change cipher.encrypt to cipher.decrypt & provide the salt and IV generated during encryption.

Asymmetric Ciphers

Asymmetric algorithms use two keys: a public key & a private key. The private key is used for decoding & signing messages. The public key is used for encoding & verification.

One practical application is the initial exchange in an SSL connection. The Internet is a hostile network and both the client & the server need to agree on a shared secret key to be able to secure the communication between them.

The problem is that this key can’t be sent in the clear because anyone could just intercept it and use it to spy on the encrypted communication.

The solution is to use an asymmetric cipher, like RSA, or a dedicated algorithm, like DH (Diffie-Hellman Key Exchange). After a secret key has been agreed upon, the secure SSL session can start using a regular symmetric cipher like AES.

You can generate a pair of public/private keys using the openssl command. Like this:

openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in private_key.pem -out public_key.pem

You can also generate a key pair from Ruby:

key = OpenSSL::PKey::RSA.new(2048)

File.open('private_key.pem', 'w') { |f| f.write(key.to_pem) }
File.open('public_key.pem', 'w')  { |f| f.write(key.public_key.to_pem) }

Hash Functions

A hash function takes in an input and returns a fixed-size output. In a cryptographic hash function, the output is typically represented as a hexadecimal string.

It’s important to note that a hash function is not meant to be reversed, it’s a one-way function. This is what makes it great for storing passwords.

Hash functions can also be used to compare if two files are identical. This is done by hashing the contents of the file & comparing the resulting hashes.

Some common hash functions are:

  • MD5
  • SHA1
  • SHA2
  • bcrypt

The Ruby standard library includes the Digest module to help you produce the first 3.

Here is an example:

require 'digest'

Digest::MD5.hexdigest('test')
# "098f6bcd4621d373cade4e832627b4f6"

Digest::SHA1.hexdigest('test')
# "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"

The same input will always produce the same output, but it’s also possible for two or more inputs to map to the same output, this is called a ‘hash collision’. Lower bit functions, like MD5, are more susceptible to collisions, so that’s one reason why it’s preferable to use a stronger function.

Here is the documentation for Digest: http://ruby-doc.org/stdlib-2.2.0/libdoc/digest/rdoc/Digest.html

Another option is to use bcrypt (which is what Devise uses):

require 'bcrypt'

BCrypt::Password.create("test")
# "$2a$10$zccXimeuNdA083RSDFy7VeVgs538d5XRQurRbqjdEd3h0kU7Q0j2e"

The advantage of using bcrypt over something like SHA-1 is that bcrypt is slower, which is a good thing because it makes the resulting hashes stronger against brute-force attacks.

Here is the documentation for the bcrypt gem: https://rubygems.org/gems/bcrypt/

Adding a Little Salt

One problem with hash functions is that it’s possible to pre-calculate the hash values for many common words & passwords, which makes ‘cracking’ a set of hashes much faster than pure brute-force. In fact, you can download sets of pre-computed hashes called ‘Rainbow tables’.

The solution is to make all your hashes unique using what we call a ‘salt’ value. This value is used like this:

sha1(salt + password)

And the salt is saved with the hashed password, so it can be used for login attempts. In the case of bcrypt, the salt is generated by the algorithm & included in the hash output.

Note: This is already done for you if you use something like Devise, but it’s still good to know :)

One more thing: make sure to use a unique salt for every password, otherwise you won’t get the full benefits of salting.

Conclusion

Cryptography is a complex world, but you want to be familiar with it if you deal with important data. One word of warning: don’t try to be clever & invent your own ciphers, you are probably going to shoot yourself in the foot.

CSS Master, 3rd Edition