DEV Community

Shobhit🎈✨
Shobhit🎈✨

Posted on

Simple String encryption in Rails

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 integration to my app which notifies when a certificate is going to expire soon, 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:

# 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
Enter fullscreen mode Exit fullscreen mode

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:

len   = ActiveSupport::MessageEncryptor.key_len
salt  = SecureRandom.hex len
Enter fullscreen mode Exit fullscreen mode

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

key   = ActiveSupport::KeyGenerator.new(Rails.application.secrets.secret_key_base).generate_key salt, len
Enter fullscreen mode Exit fullscreen mode

Using this key, we create an encryption object crypt

crypt = ActiveSupport::MessageEncryptor.new key
Enter fullscreen mode Exit fullscreen mode

and then we encrypt our text via:

encrypted_data = crypt.encrypt_and_sign text
Enter fullscreen mode Exit fullscreen mode

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:

"#{salt}$$#{encrypted_data}"
Enter fullscreen mode Exit fullscreen mode

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).

salt, data = text.split("$$")
Enter fullscreen mode Exit fullscreen mode

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

len   = ActiveSupport::MessageEncryptor.key_len
key   = ActiveSupport::KeyGenerator.new(Rails.application.secrets.secret_key_base).generate_key salt, len
crypt = ActiveSupport::MessageEncryptor.new key
Enter fullscreen mode Exit fullscreen mode

And then, we decrypt the data:

crypt.decrypt_and_verify data
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (4)

Collapse
 
bartosz profile image
Bartosz Wójcik

You might be interested in this thing called StringEncrypt.

stringencrypt.com/

It can take your strings, encrypt them with random algorithm and generate clean decryption Ruby code, for example:

# encrypted with https://www.stringencrypt.com (v1.3.0) [Ruby]
# test = "hello dev.to"
test = "\u8C71\u0C61\u3C51\u2441\u1431\u7424\u1C12\uFC01\u5BF0\u03E2\u3BD0\uCBCF"

test.codepoints.each_with_index do |zjxxo, npbyw|
  zjxxo = ((zjxxo << 12) | ( (zjxxo & 0xFFFF) >> 4)) & 0xFFFF
  zjxxo -= 0x6C44
  zjxxo += npbyw
  zjxxo += 0xE0A5
  zjxxo = ((zjxxo << 7) | ( (zjxxo & 0xFFFF) >> 9)) & 0xFFFF
  zjxxo += 1
  zjxxo += npbyw
  zjxxo += 0xAE9C
  zjxxo ^= 0x5839
  zjxxo = ((zjxxo << 2) | ( (zjxxo & 0xFFFF) >> 14)) & 0xFFFF
  zjxxo ^= 0x6B00
  zjxxo += npbyw
  test[npbyw] = [zjxxo & 0xFFFF].pack('U').force_encoding('UTF-8')
end

puts test
Collapse
 
victormartins profile image
Victor Martins

Hey Shobhit, thank you for this thread it helped me quite a bit :)

I'm leaving a contribution since I add to change it it a little bit in order to transmit the encrypted data across servers without the need for a string splitter to get the salt.
So instead of a salt I use an IV (Initialization Vector).

I've also changed from SecureRandom.hex to SecureRandom.random_bytes to get the same amount of chars than the length. (hex produces the double amount) and introduced an ENV var instead of the Rails secret_key_base so I can share the password easily between servers.

class MessageEncryptor
  def encrypt_and_sign(text)
    text  = String(text)
    iv    = SecureRandom.random_bytes(key_length)
    key   = generate_key(iv)
    crypt = encryptor(key)

    encrypted_data = crypt.encrypt_and_sign text
    "#{iv}#{encrypted_data}"
  end

  def decrypt(encrypted_text)
    iv, encrypted_data = decompose(encrypted_text)
    key   = generate_key(iv)
    crypt = encryptor(key)
    crypt.decrypt_and_verify(encrypted_data)
  end

  private

  def encryptor(key)
    ActiveSupport::MessageEncryptor.new(key)
  end

  def generate_key(iv)
    ActiveSupport::KeyGenerator.new(password).generate_key(iv, key_length)
  end

  def key_length
    ActiveSupport::MessageEncryptor.key_len
  end

  def password
    ENV.fetch('MESSAGE_ENCRYPTOR_SECRET')
  end

  def decompose(encrypted_text)
    [
      encrypted_text[0...key_length],
      encrypted_text[key_length .. -1]
    ]
  end
end

Hope it helps someone :)

Collapse
 
blankfella profile image
blank

Hi, and thanks for the clean post. I am more curious about how would you do that in Ruby? I used ActiveSupport separately from rails but its a huge dependency, can you suggest any other way for simple string encryption?

Collapse
 
shobhitic profile image
Shobhit🎈✨

Yes, you can do it, let me go through some notes and write up a post real quick.