Git Product home page Git Product logo

hashids.rb's Introduction

Important

Hashids has been rebranded to ๐Ÿฆ‘ Sqids, and there is a Ruby implementation of it that I suggest you use instead of Hashids.rb

Hashids

A small Ruby gem to generate YouTube-like ids from one or many numbers. Use hashids when you do not want to expose your database ids to the user.

http://hashids.org/ruby/

Build Status (push)

What is it?

hashids (Hash ID's) creates short, unique, decodable hashes from unsigned integers.

(NOTE: This is NOT a true cryptographic hash, since it is reversible)

It was designed for websites to use in URL shortening, tracking stuff, or making pages private (or at least unguessable).

This algorithm tries to satisfy the following requirements:

  1. Hashes must be unique and decodable.
  2. They should be able to contain more than one integer (so you can use them in complex or clustered systems).
  3. You should be able to specify minimum hash length.
  4. Hashes should not contain basic English curse words (since they are meant to appear in public places - like the URL).

Instead of showing items as 1, 2, or 3, you could show them as jR, k5, and l5. You don't have to store these hashes in the database, but can encode + decode on the fly.

All integers need to be greater than or equal to zero.

Installation

Add this line to your application's Gemfile:

gem 'hashids'

And then execute:

$ bundle

Or install it yourself as:

$ gem install hashids

Usage

Encoding one number

You can pass a unique salt value so your hashes differ from everyone else's. I use this is my salt as an example.

hashids = Hashids.new("this is my salt")
hash = hashids.encode(12345)

hash is now going to be:

NkK9

Decoding

Notice during decoding, same salt value is used:

hashids = Hashids.new("this is my salt")
numbers = hashids.decode("NkK9")

numbers is now going to be:

[ 12345 ]

Decoding with different salt

Decoding will not work if salt is changed:

hashids = Hashids.new("this is my pepper")
numbers = hashids.decode("NkK9")

numbers is now going to be:

[]

Encoding several numbers

hashids = Hashids.new("this is my salt")
hash = hashids.encode(683, 94108, 123, 5)

hash is now going to be:

aBMswoO2UB3Sj

Decoding is done the same way

hashids = Hashids.new("this is my salt")
numbers = hashids.decode("aBMswoO2UB3Sj")

numbers is now going to be:

[ 683, 94108, 123, 5 ]

Encoding and specifying minimum hash length

Here we encode integer 1, and set the minimum hash length to 8 (by default it's 0 -- meaning hashes will be the shortest possible length).

hashids = Hashids.new("this is my salt", 8)
hash = hashids.encode(1)

hash is now going to be:

gB0NV05e

Decoding with minimum hash length

hashids = Hashids.new("this is my salt", 8)
numbers = hashids.decode("gB0NV05e")

numbers is now going to be:

[ 1 ]

Specifying custom hash alphabet

Here we set the alphabet to consist of: "abcdefghijkABCDEFGHIJK12345"

hashids = Hashids.new("this is my salt", 0, "abcdefghijkABCDEFGHIJK12345")
hash = hashids.encode(1, 2, 3, 4, 5)

hash is now going to be:

dEc4iEHeF3

Randomness

The primary purpose of hashids is to obfuscate ids. It's not meant or tested to be used for security purposes or compression. Having said that, this algorithm does try to make these hashes unguessable and unpredictable:

Repeating numbers

hashids = Hashids.new("this is my salt")
hash = hashids.encode(5, 5, 5, 5)

You don't see any repeating patterns that might show there's 4 identical numbers in the hash:

1Wc8cwcE

Same with incremented numbers:

hashids = Hashids.new("this is my salt")
hash = hashids.encode(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

hash is now going to be:

kRHnurhptKcjIDTWC3sx

Incrementing number ids:

hashids = Hashids.new("this is my salt")

hashids.encode 1 #=> NV
hashids.encode 2 #=> 6m
hashids.encode 3 #=> yD
hashids.encode 4 #=> 2l
hashids.encode 5 #=> rD

Encoding using a HEX string

hashids = Hashids.new("this is my salt")
hash = hashids.encode_hex('DEADBEEF')

hash is now going to be:

kRNrpKlJ

Decoding to a HEX string

hashids = Hashids.new("this is my salt")
hex_str = hashids.decode_hex("kRNrpKlJ")

hex_str is now going to be:

DEADBEEF

Changelog

1.0.6

  • Fixed using lib with frozen strings
  • Remove deprecated global use of must_equal and must_raise
  • Use GitHub Actions instead of Travis-CI

1.0.5

  • Improve shuffle performance
  • Update rubies used by Travis-CI

1.0.4

  • Improved encode/decode performance

1.0.3

  • Support for Ruby 2.4.0

1.0.2

  • Handle invalid input by raising InputError

1.0.1

  • Final alphabet length can now be shorter than the minimum alphabet length
  • validate_alphabet now run before setting up seps & guards

1.0.0

  • Public functions renamed to be more appropriate:
  • encrypt changed to encode
  • encrypt_hex changed to encode_hex
  • decrypt changed to decode
  • decrypt_hex changed to decode_hex

0.3.0

  • Bumped the version number since hashids.rb now support the new algorithm
  • Support for encrypt_hex and decrypt_hex

0.0.3

  • Default salt (Allows for Hashids.new.encrypt(91) #=> "kBy")
  • Further tweaking of the private methods (tr/delete over gsub, scan over split)

0.0.2

  • Minitest required if RUBY_VERSION < 1.9.3
  • Using scan over split where appropriate

0.0.1

Contact

Follow me @peterhellberg

Or http://c7.se/

License

MIT License. See the LICENSE.txt file.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

hashids.rb's People

Contributors

alessio-signorini avatar aried3r avatar dmytrostepaniuk avatar ferdinandrosario avatar gogainda avatar kivanio avatar ksss avatar m-nakamura145 avatar morgoth avatar niclas avatar opsidao avatar peterhellberg avatar tcrouch avatar xemexpress avatar zhong-z avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hashids.rb's Issues

Is this thread-safe?

It is not mentioned in README. For example, if I initialize a hashids instance at startup, and have several concurrent threads encoding and decoding with the instance, would there be race condition? In other words, will the #encode, #decode, #encode_hex and #decode_hex modify the hashids instance?

Make the usage more easy

Most people here probably know these gems:
https://github.com/namick/obfuscate_id
https://github.com/mguymon/obfuscate
They are quite easy to integrate in existing models by simply adding one line so i tried to port this to this hashids gem. While doing this i stumbled across a problem which you will see in step 2.

Step 1:
Create a file called 'obfuscate_id.rb' in 'app/models/concerns' and put this code inside:

module ObfuscateId
  require 'hashids'
  extend ActiveSupport::Concern

  module ClassMethods
    def hashids
      Hashids.new(table_name, 8)
    end

    def encode_id(id)
      hashids.encode(id)
    end

    def decode_id(id)
      hashids.decode(id).first
    end
  end

  included do
    define_method :to_param do
      self.class.encode_id(id)
    end

    define_singleton_method :find do |*args|
      super(decode_id(args.first))
    end

    define_method :is_obfuscated do
    end
  end
end

Now you can add this line to your model if you want to obfuscate its id:

include ObfuscateId

At this state you have the functionality of the 2 Gems i mentioned it the beginning, but are using hashids

Step 2:
The problem now is that this will only work if you call the find directly on the model
Because if you call something like for example: User.all.find(...) the find will be called on ActiveRecord::AssociationRelation and not on the model, and we didn't overwrite this.
Same problem with current_user.company.users.find(...) but this would be ActiveRecord::Associations::CollectionProxy
This also causes for example the cancancan gem to not work anymore.

I have a solution, but it's ugly and was the reason for me to open this issue, so i hope others can contribute on how to solve this in a better way.

Solution: Create an initializer and put this code inside:

require 'hashids'

class ActiveRecord::Associations::CollectionProxy
  def find(*args)
    if first.class.method_defined? :is_obfuscated
      args = hashids.decode(args.first).first
    end
    super(args)
  end
end

class ActiveRecord::AssociationRelation
  def find(*args)
    if first.class.method_defined? :is_obfuscated
      args = hashids.decode(args.first).first
    end
    super(args)
  end
end

protected method `encode' called for #<Hashids>

What's is wrong with my code ?

Thanks

2.1.1 :007 > require 'hashids'
 => true 
2.1.1 :008 > h = Hashids.new
 => #<Hashids:0x000000016506c0 @salt="", @min_hash_length=0, @alphabet="gjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890", @seps="cfhistuCFHISTU", @guards="abde"> 
2.1.1 :009 > h.encode(111)
NoMethodError: protected method `encode' called for #<Hashids:0x000000016506c0>
    from (irb):9
    from /home/user/.rvm/rubies/ruby-2.1.1/bin/irb:11:in `<main>'

Support for UUID?

Many use UUID as primary keys, because of the many advantages this brings (with UUIDv7, they become even more interesting).

Thus, it would be awesome to be able to use your gem in these situations, too.

Validate salt length?

The salt appears to have a maximum length after which it has no longer impacts the generated hash.

Here some examples:

Hashids.new('a' * 41).encode(1)
=> "pn" 
Hashids.new('a' * 42).encode(1)
=> "nd" 
Hashids.new('a' * 43).encode(1)
=> "yQ" 
Hashids.new('a' * 44).encode(1)
=> "yQ" 
Hashids.new('a' * 45).encode(1)
=> "yQ" 

If i change the alphabet it also affects this maximum length:

Hashids.new('a' * 22, 0, 'abcdefghijkmnopqrstuvwxyz023456789').encode(1)
=> "b0" 
Hashids.new('a' * 23, 0, 'abcdefghijkmnopqrstuvwxyz023456789').encode(1)
=> "bd" 
Hashids.new('a' * 24, 0, 'abcdefghijkmnopqrstuvwxyz023456789').encode(1)
=> "bd" 
Hashids.new('a' * 25, 0, 'abcdefghijkmnopqrstuvwxyz023456789').encode(1)
=> "bd" 

Looks like something around (alphabet.length * 0.7).floor is the maximum length?

Should this be validated and raise a SaltError when the salt is too long?

Discussion: use array of chars instead string as a alphabet

Hey!
Now we can use only string of chars as a alphabet for generating HashID. WDYT if gem will allow to use array of chars as a alphabet too?

Something like:

alphabet = ['a1', 'b2']
hashids = Hashids.new("this is my salt", 0, alphabet)
hash = hashids.encode(1) # => "b2a1b2"

It will be helpful for generating hashes without any bad words or generating hashes with specific chars combinations. WDYT?

Duplicate hashids

Hello and thanks for this gem!

If i'm creating hashids for different ids it gave me duplicate hashes:

hashids = Hashids.new('Whoops is when you accidentally douche with Drano!', 12)
hashids.encode(16137)
hashids.encode(16181)

hashids gem 1.0.3 rails 4.2.9 ruby-2.1.10

Can you reproduce it?

Thanks and best wishes.

Hash collision

I discovered a hash that decodes to 2 values in my codebase. My understanding from the documentation is there should be no collisions. Is this a bug?

hex = "xaFG9d"
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
h1 = Hashids.new("developments", 6, alphabet)
h1.decode(hex)
# => [29, 76161]
h2 = Hashids.new("tenants", 6, alphabet)
h2.decode(hex)
# => [42, 28612]

MIN_ALPHABET_LENGTH is incorrect

The following piece of code:

hashids = Hashids.new('this is my salt', 0, '0123456789abcdef')
id = hashids.encode(18446744073709551615)
numbers = hashids.decode(id)

raises a Hashids::AlphabetError with message Alphabet must contain at least 16 unique characters.

The validate_alphabet call needs to be moved after @alphabet assignment.

Not compatible with frozen strings

Hi @peterhellberg

I would like to setup my app to have frozen strings by default.
Example description: https://www.mikeperham.com/2018/02/28/ruby-optimization-with-one-magic-comment/

Using hashids is not possible with globally enabled frozen string. This can be seen by:

#> RUBYOPT="--enable=frozen-string-literal" irb
irb(main):001:0> require 'hashids'
=> true
irb(main):002:0> Hashids.new('secret')
/home/wojtek/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/hashids-1.0.5/lib/hashids.rb:217:in `delete!': can't modify frozen String: "cfhistuCFHISTU" (FrozenError)
	from /home/wojtek/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/hashids-1.0.5/lib/hashids.rb:217:in `setup_seps'
	from /home/wojtek/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/hashids-1.0.5/lib/hashids.rb:199:in `setup_alphabet'
	from /home/wojtek/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/hashids-1.0.5/lib/hashids.rb:23:in `initialize'

I could prepare a PR if you're interested in making it fixed.

Duplicate Hash Values

var hashids = new Hashids("a5ifxlwfM6qCpOfvlgvW", 8, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");

var a = hashids.encode(2713);
var b = hashids.encode(3057);

// a : "MrQkaLzd"
// b : "MrQkaLzd"

Using latest version.

Validate alphabet before decoding

Presuming that default alphabet is used:

Hashids.new('salt').decode('asdf-')
NoMethodError: undefined method '*' for nil:NilClass
from /gems/hashids-1.0.1/lib/hashids.rb:183:in `block in unhash'

Edit: Ok, taking it further:

Hashids.new('endpoints').decode('-')
=> []

Support for 0.3.0

Iโ€™ve started working on the support for 0.3.0 but it is slow going.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.