Problem z BCrypt. String stringowi nie równy

Tworzę autentykację poprzez API opartą na BCrypt. Z weryfikacją hasła nie było żadnych problemów, ale chciałbym api keye trzymać w bazie zaszyfrowane, tak jak hasła. I tu zaczynają się schody. W przeglądarce trzymam surowe api keye. Po wysłaniu requesta z api keyem chciałbym go zahaszować i po tym haszu znaleźć użytkownika. Problem w tym, że kolejne wykonanie tej samej funkcji haszującej na tym samym stringu daje inny hasz. Sprawdziłem jak to wygląda dla funkcji authenticate w BCrypt i odkryłem coś naprawdę dziwnego.

=> "$2a$10$6ixLZejoaE3gqITMotMhnOh4K6LQcLwQAQAJEk7ZUxyYJcVcV/hKe"
irb(main):002:0> a = BCrypt::Password.create('abc')
=> "$2a$10$N5S8iWV3453PAsYzqU5mSOPBBZ0O3w.ge27EyoJiGPZWO1bxZwC8K"
irb(main):003:0> a
=> "$2a$10$N5S8iWV3453PAsYzqU5mSOPBBZ0O3w.ge27EyoJiGPZWO1bxZwC8K"
irb(main):004:0> a == 'abc'
=> true
irb(main):005:0> "$2a$10$N5S8iWV3453PAsYzqU5mSOPBBZ0O3w.ge27EyoJiGPZWO1bxZwC8K" == 'abc'
=> false
irb(main):006:0> a.inspect
=> "\"$2a$10$N5S8iWV3453PAsYzqU5mSOPBBZ0O3w.ge27EyoJiGPZWO1bxZwC8K\""
irb(main):007:0> "$2a$10$N5S8iWV3453PAsYzqU5mSOPBBZ0O3w.ge27EyoJiGPZWO1bxZwC8K".inspect
=> "\"$2a$10$N5S8iWV3453PAsYzqU5mSOPBBZ0O3w.ge27EyoJiGPZWO1bxZwC8K\""
irb(main):008:0> 

Żeby sprawdzić poprawność hasła funkcja authenticate robi coś takiego:

irb(main):011:0> BCrypt::Password.new('$2a$10$N5S8iWV3453PAsYzqU5mSOPBBZ0O3w.ge27EyoJiGPZWO1bxZwC8K') == 'abc'
=> true

Problem w tym, że:

irb(main):012:0> BCrypt::Password.new('$2a$10$N5S8iWV3453PAsYzqU5mSOPBBZ0O3w.ge27EyoJiGPZWO1bxZwC8K')
=> "$2a$10$N5S8iWV3453PAsYzqU5mSOPBBZ0O3w.ge27EyoJiGPZWO1bxZwC8K"
irb(main):013:0> "$2a$10$N5S8iWV3453PAsYzqU5mSOPBBZ0O3w.ge27EyoJiGPZWO1bxZwC8K" == 'abc'
=> false

Mógłby mi ktoś wyjaśnić dlaczego tak się dzieje?

Dlatego:

2.2.1 :002 > a = BCrypt::Password.create 'abc'
 => "$2a$10$icKLZ4ayWIx97nPixlS6r.L0JZFBLMJfRhJ2lkH8RaVmuxnSWWZBq" 
2.2.1 :003 > a == 'abc'
 => true 
2.2.1 :004 > a.class
 => BCrypt::Password 
2.2.1 :005 > a.class.superclass
 => String 

W skrócie, mimo że wyświetlany jest jako zwykły string (co zresztą sprawdziłeś, bo to zależy wyłącznie od tego, co zwraca metoda inspect), nie jest to String, a obiekt klasy BCrypt::Password, która dziedziczy po klasie String.

Czegoś takiego się spodziewałem. Bardziej mnie dziwi jednak to, że porównanie takiego obiektu z czystym stringiem daje true. Jak dotrzeć do kodu, który to implementuje?

Prawdopodobnie po prostu klasa BCrypt::Password implementuje metodę ==.

Proszę bardzo: :slight_smile:

Polecam także używać narzędzie pry. W momencie gdy sprawdziłeś, że wyrażenie a == 'abc' zwraca true (co oczywiście było zaskoczeniem) powinieneś sprawdzić jakiego typu obiekt masz po lewej stronie (a.class) a także co robi metoda == wywołana na zmiennej a. I to jest najprościej zrobić w pry:

$ pry
[1] pry(main)> require "bcrypt"
=> true
[2] pry(main)> a = BCrypt::Password.create('abc')
=> "$2a$10$uara64IQDwbnjuBSBLPJu.0PRmYaJXlnkmLKiCj9hDqUu2pPB6NTu"
[3] pry(main)> a == 'abc'
=> true
[4] pry(main)> $ a.==

From: /Users/radarek/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/bcrypt-3.1.11/lib/bcrypt/password.rb @ line 65:
Owner: BCrypt::Password
Visibility: public
Number of lines: 3

def ==(secret)
  super(BCrypt::Engine.hash_secret(secret, @salt))
end