How do I obfuscate the ids of my records in rails?

I'm trying to figure out how to obfuscate the ids of my records in rails.

For example: a typical path might look like http://domain/records/1, so it's pretty easy for people to deduce how much traffic the site is getting if they just create a new record.

One solution that I've used is to hash the id with a salt, but since I'm not sure whether that function is bijective, I end up storing it in another column in my database and double check for uniqueness.

Another option I was thinking about was generating a random hash and storing that as another column. If it isn't unique ... just generate another one.

What's the best way of doing this?


You could use the built-in OpenSSL library to encrypt and decrypt your identifiers, that way you would only need to overwrite to_param on your models. You'll also need to use Base64 to convert the encrypted data into plain text. I would stick this in a module so it can be reused:

require 'openssl'
require 'base64'

module Obfuscate
  def self.included(base)
    base.extend self

  def cipher'aes-256-cbc')

  def cipher_key

  def decrypt(value)
    c = cipher.decrypt
    c.key = Digest::SHA256.digest(cipher_key)
    c.update(Base64.decode64(value.to_s)) +

  def encrypt(value)
    c = cipher.encrypt
    c.key = Digest::SHA256.digest(cipher_key)
    Base64.encode64(c.update(value.to_s) +

So now your models would need to look something like this:

class MyModel < ActiveRecord::Base
  include Obfuscate

  def to_param
    encrypt id

Then in your controller when you need to find a record by the encrypted id, you would use something like this:

MyModel.find MyModel.decrypt(params[:id])

If you're looking to encrypt/decrypt ids without storing them in the database, this is probably the easiest way to go.

Instead of numeric ids, use some kind of friendly url or human readable slug. There are lots of tools to choose from in this department. Not only are they more friendly to your users, but well chosen slugs can give a nice advantage with search engines.

Here's a gem that keeps it numeric, requires no database migrations, and no routing changes:

I've found that this gem doesn't work in concert with some other gems, notably paper_trail. This is because of the way it replaces the find method, and paper_trail causes find to be called with the actual record id.

So I've been using the gem's "scatter_swap" functionality, but not the rest of it. Here's the model:

require 'obfuscate_id/scatter_swap'

class Page < ActiveRecord::Base
  # This is a random number that, if changed, will invalidate all existing URLs. Don't change it!
  @@obfuscate_spin = # random number here, which is essentially the encryption key

  # Generate URL parameter to be used in the URL as the "id"
  def to_param
    # Use the obfuscate_id gem's class to "spin" the id into something obfuscated
    spun_id = ScatterSwap.hash(, @@obfuscate_spin)

    # Throw any additional attributes in here that are to be included in the URL.
    "#{spun_id} #{name}".parameterize

  def self.find_by_slug!(slug)
    spun_id = slug[/^[0-9]+/]
      find_by_id! ScatterSwap.reverse_hash(spun_id, @@obfuscate_spin)
    rescue ActiveRecord::RecordNotFound => e
      raise ActiveRecord::RecordNotFound, "Couldn't find matching Page."

And in the controller:

class PagesController < InheritedResources::Base
  # Find the page using its URL slug
  before_filter :find_page, except: [:index, :create, :new]

  def find_page
    @page = Page.find_by_slug! params[:id]

    # If the URL doesn't match exactly, and this is a GET.
    # We'll redirect to the new, correct URL, but if this is a non-GET, let's let them finish their request instead.
    if params[:id] != @page.to_param && request.get?
      redirect_to url_for({ id: @page.to_param }), status: 301

As an alternative to the redirection that takes place there, you could simply include a canonical URL in the page. The redirection has the bug of ignoring any query parameters in the URL. This was not a problem for my project, as I didn't have any. But a canonical URL would be better.

It's pretty easy to generate unique random identifiers for your records either using a randomized string generator or a simple call to Digest::SHA1.hexdigest which produces reasonably random and cryptographically unique results.

For instance, you can create a secondary column called ident or unique_id that stores your public identifiers. You can then over-write to_param to use this instead:

class MyModel < ActiveRecord::Base
  before_create :assign_ident

  def self.from_param(ident)

  def to_param

  def assign_ident
    self.ident = Digest::SHA1.hexdigest(SecureRandom.random_number(1<<256).to_s)

Theoretically there is a chance of collision on SHA1 but the odds are so astronomically low you're more liable to have a software crash because of a memory error or hardware malfunction. You can test this to see if it suits your needs by generating a few billion identities to see if they ever collide, which they shouldn't. A 256-bit random number should provide a sufficient amount of data for the SHA1 algorithm to chew on.

After reading through @siannopollo's post, I created a Gem based on the idea of his post (but with some improvements):

Need Your Help

XCode 5 hangs with a timeout error when validating/uploading a "ready to be uploaded" app

iphone xcode app-store provisioning-profile ios-provisioning

I've been developing and uploading apps for some months. Whenever they needed being uploaded to the app store, I could do it without problem, but now, I'm stuck with one that doesn't want to be val...

getting the value of the single field output using the codeigniter active record

codeigniter activerecord

the following function is supposed to read the name of the give asset code from the database. but it triggers the error: "Trying to get property of non-object"