13

Given I've got a site where most of the resources have numerical IDs (i.e. user.id question.id etc.) but that like the Germans looking back on WWII I'd rather not reveal these to the observers, what's the best way to obfuscate them?

I presume the method is going to involve the .to_param and then some symmetric encryption algorithm but I'm not sure what's the most efficient encryption to do and how it'll impact lookup times in the DB etc.

Any advice from the road trodden would be much appreciated.

Peter Nixey
  • 16,187
  • 14
  • 79
  • 133

6 Answers6

12

I published a Rails plugin that does this called obfuscate_id. I didn't need it to be secure, but just to make the id in the url non-obvious to the casual user. I also wanted it to look cleaner than a long hash.

It also has the advantage of needing no migrations or database changes. It's pretty simple.

Just add the gem to your Gemfile:

gem 'obfuscate_id'

And add call the obfuscate id in your model:

class Post < ActiveRecord::Base
  obfuscate_id
end

This will create urls like this:

# post 7000
http://example.com/posts/5270192353
# post 7001
http://example.com/posts/7107163820
# post 7002
http://example.com/posts/3296163828

You also don't need to look up the records in any special way, ActiveRecord find just works.

Post.find(params[:id])

More information here:

https://github.com/namick/obfuscate_id

nathan amick
  • 1,133
  • 9
  • 7
  • This is sweet! But, I have one question. Is there any option with this gem to pass in the title of the post, so I can get something like: http://example.com/posts/5270192353/my-page-title . If so, that would be lovely! – Michael Giovanni Pumo Feb 22 '14 at 00:23
  • This doesn't seem to work in Rails 4.0.2 - having issues installing it! It says Rails 3.2.1 is required. – Michael Giovanni Pumo Apr 01 '14 at 11:05
  • @MichaelGiovanniPumo, see the issues, https://github.com/namick/obfuscate_id/issues/19 – Ari May 15 '14 at 18:59
  • This gem doesn't work on rails 5, if you're not dead please just push one of the several pull requests people have made to add rails 5 support. – Mark Kramer Oct 31 '17 at 17:47
  • 2
    While I applaud Nathan's efforts and understand that not everyone has time to maintain the libraries they create, I'd encourage anyone reading this to let this be a lesson to them that in the long run, it is often a better idea to roll your own code so that you aren't left scrambling to replace dependencies that aren't maintained when upgrading your core framework. – Greg Blass Nov 15 '17 at 15:10
11

I usually use a salted Hash and store it in the DB in an indexed field. It depends on the level of security you expect, but I use one salt for all.

This method makes the creation a bit more expensive, because you are going to have an INSERT and an UPDATE, but your lookups will be quite fast.

Pseudo code:

class MyModel << ActiveRecord::Base

  MY_SALT = 'some secret string'

  after_create :generate_hashed_id

  def to_param
    self.hashed_id
  end

  def generate_hashed_id
    self.update_attributes(:hashed_id => Digest::SHA1.hexdigest("--#{MY_SALT}--#{self.id}--"))
  end

end

Now you can look up the record with MyModel.find_by_hashed_id(params[:id]) without any performance repercussions.

Wukerplank
  • 4,156
  • 2
  • 28
  • 45
  • Very helpful, ty. Do you have any idea whether there much of a performance hit coming from using a string ID rather than an integer in PostGres? – Peter Nixey Jul 13 '11 at 08:40
  • I don't know for sure, but if you put an index on the string field, the performance penalty can't be significant. – Wukerplank Jul 13 '11 at 09:10
7

Here's a solution. It's the same concept as Wukerplank's answer, but there's a couple of important differences.

1) There's no need to insert the record then update it. Just set the uuid before inserting by using the before_create callback. Also note the set_uuid callback is private.

2) There's a handy library called SecureRandom. Use it! I like to use uuid's, but SecureRandom can generate other types of random numbers as well.

3) To find the record use User.find_by_uuid!(params[:id]). Notice the "!". That will raise an error if the record is not found just like User.find(params[:id]) would.

class User
  before_create :set_uuid

  def to_param
    uuid
  end

private

  def set_uuid
    self.uuid = SecureRandom.uuid
  end

end
tybro0103
  • 48,327
  • 33
  • 144
  • 170
5

Hashids is a great cross-platform option.

user456584
  • 86,427
  • 15
  • 75
  • 107
4

You can try using this gem,

https://github.com/wbasmayor/masked_id

it obfuscates your id and at the same time giving each model it's own obfuscated code so all no. 1 id won't have the same hash. Also, it does not override anything on the rails side, it just provides new method so it doesn't mess up your rails if your also extending them.

Andro Selva
  • 53,910
  • 52
  • 193
  • 240
Berimbolo
  • 293
  • 2
  • 12
3

Faced with a similar problem, I created a gem to handle the obfuscation of Model ids using Blowfish. This allows the creation of nice 11 character obfuscated ids on the fly. The caveat is, the id must be within 99,999,999, e.g. a max length of 8.

https://github.com/mguymon/obfuscate

To use with Rails, create an initializer in config/initializers with:

require 'obfuscate/obfuscatable'
Obfuscate.setup do |config|
  config.salt = "A weak salt ..."
end

Now add to models that you want to be Obfuscatable:

class Message < ActiveRecord::Base
  obfuscatable # a hash of config overrides can be passed.
end

To get the 11 character obfuscated_id, which uses the Blowfish single block encryption:

message = Message.find(1)
obfuscated = message.obfuscated_id           # "NuwhZTtHnko"
clarified = message.clarify_id( obfuscated ) # "1"

Message.find_by_obfuscated_id( obfuscated )

Or obfuscate a block of text using Blowfish string encryption, allowing longer blocks of text to be obfuscated:

obfuscated = message.obfuscate( "if you use your imagination, this is a long block of text" ) # "GoxjVCCuBQgaLvttm7mXNEN9U6A_xxBjM3CYWBrsWs640PVXmkuypo7S8rBHEv_z1jP3hhFqQzlI9L1s2DTQ6FYZwfop-xlA"
clarified = message.clarify( obfuscated ) # "if you use your imagination, this is a long block of text"
mguymon
  • 8,946
  • 2
  • 39
  • 61