Simple String encryption in Rails
Tags: rails, ruby, encryption
Published: 2018-05-25 07:46:53

---
title: Simple String encryption in Rails
published: true
description: A small helper script to encrypt and decrypt user id's and relay states to share with 3rd party services
tags: rails, ruby, encryption
---

If you've ever implemented any kind of SSO, you'll have encountered "relay state". Relay state is a parameter you send to your identity party, and they send it back to you without any modification so you can identify the user who just authorized.

It's a pretty common flow, used by Google as well as many other OAuth providers.

When I was a new programmer, I simply sent the `user_id` of the user as relay state and did a `User.find(user_id)` to fetch user.

## Why do I need to encrypt User ID

So, imagine this, if you signed up for my service, and then later wanted to add your Google credentials, you'd click on a button and it'll take you through the whole authorization workflow, in the end making a Webhook request with relay state as your user id, in plain text.

This was pretty bad as now some user capable of doing Inspect element can change the `user_id` from their number to any integer and my application would think the other user has authorized. Now they can use "Login with Google" on the sign in page and simply log in as that other user.

Insecure af.

When I was adding a [Stride](https://stride.com) integration to my app which notifies when a [certificate is going to expire soon](https://monitorcertificates.com), I had the same problem as their official docs ask us to add a button where you add a `relayState` parameter. Their application sends us a webhook once the user has added our app. The user is not redirected back to our site, unlike Slack.

So the only way of identifying the user was with that `relayState` and leaving it to plan `user_id` would mean if you change the button's value and click on it, you will potentially get notified when other user's certificates are going to expire.

## Encrypting the User ID

To combat this issue, I added two little functions to my helper class and called them `encrypt` and `decrypt`. The functions looked like this:

```ruby
# Assuming your Secret Key Base is in Rails.application.secrets.secret_key_base

def encrypt text
  text = text.to_s unless text.is_a? String

  len   = ActiveSupport::MessageEncryptor.key_len
  salt  = SecureRandom.hex len
  key   = ActiveSupport::KeyGenerator.new(Rails.application.secrets.secret_key_base).generate_key salt, len
  crypt = ActiveSupport::MessageEncryptor.new key
  encrypted_data = crypt.encrypt_and_sign text
  "#{salt}$$#{encrypted_data}"
end

def decrypt text
  salt, data = text.split "$$"
  
  len   = ActiveSupport::MessageEncryptor.key_len
  key   = ActiveSupport::KeyGenerator.new(Rails.application.secrets.secret_key_base).generate_key salt, len
  crypt = ActiveSupport::MessageEncryptor.new key
  crypt.decrypt_and_verify data
end
```

## How `encrypt` works

Encrypting a `text` requires a `key` and `salt`. Decrypting an encrypted `text` requires the same `key` and `salt`, otherwise this won't work.

We generate a salt with these lines:

```ruby
len   = ActiveSupport::MessageEncryptor.key_len
salt  = SecureRandom.hex len
```

Once we have our salt, we can use the Rails' `secret_key_base` as a "key" to generate a cryptographic key.

```ruby
key   = ActiveSupport::KeyGenerator.new(Rails.application.secrets.secret_key_base).generate_key salt, len
``` 

Using this key, we create an encryption object `crypt`

```ruby
crypt = ActiveSupport::MessageEncryptor.new key
```

and then we encrypt our text via:

```ruby
encrypted_data = crypt.encrypt_and_sign text
```

Now, we could have simply returned this `encrypted_data` and provided this as a relay state, but since `salt` was generated randomly, we would know what it was and so won't be able to decrypt this data.

I used a trick from Rails' `has_secure_password` and `bcrypt`, and returned a string which contains `salt` and `encrypted_data` as you can see from the last line of `encrypt` method:

```ruby
"#{salt}$$#{encrypted_data}"
```

This returns a string where you have both `salt` and `encrypted_data` and the user can't change this to other user's ID without knowing your `secret_key_base`.

## How `decrypt` works

Once you understand the `encrypt` method, `decrypt` id fairly straightforward.

The `decrypt` method accepts a `text` parameter, from which it extracts `salt` and `data` (or rather `encrypted_data`).

```ruby
salt, data = text.split("$$")
```

Once you have the salt with us, we create the `crypt` object, just like we did in the `encrypt` method:

```ruby
len   = ActiveSupport::MessageEncryptor.key_len
key   = ActiveSupport::KeyGenerator.new(Rails.application.secrets.secret_key_base).generate_key salt, len
crypt = ActiveSupport::MessageEncryptor.new key
```

And then, we decrypt the data:

```ruby
crypt.decrypt_and_verify data
```

which we return back from the method.

## Conclusion

This is a pretty simple method of encrypting and decrypting a `user_id` or any other data.

In an ideal scenario, you would create a table and keep a user's token for an application, which you would send as a `relayState` and use it again to identify the user back. It takes time to get that perfect, so this simple encryption and decryption works amazing to get up and running as quickly as possible.

But in the long run, you'd build out a table to keep all this information secure and more robust. That's what I've done with my app. :)

---

If you're an expert in security, could you tell me if there's something that I've done here which would make it unsecure? Thanks!