Skip to Content

Ruby - Firebase SCrypt in Ruby

Posted on

Kịch bản: 1 legacy mobile app đang sử dụng Firebase Authentication để authen/quản lý người dùng. Bây giờ họ muốn build BackEnd, và đưa phần quản lý users về bên BE.

Chúng ta có thể dump data từ bên Firebase và import vào bên Rails. Kèm theo đó là implement SCrypt function để verify password của user. (Do bên Firebase Authentication đang sử dụng thuật toán Scrypt)

Ruby đã có thư viện scrypt rồi, tuy nhiên chúng ta không sử dụng trực tiếp được, do Firebase Authentication đã thay đổi 1 chút về cách implementation.

Ref: Ruby scrypt issue #79

Bên python đã có người implement lại thuật toán này, nên mình đã convert script này từ Python về Ruby để tiện sử dụng trong dự án Rails.

require 'base64'
require 'scrypt'

def generate_derived_key password, salt, salt_separator, rounds, mem_cost
  # Generates derived key from known parameters
  n = 2 ** mem_cost
  p = 1
  r = rounds

  # We're only using first 32 bytes of the derived key to match expected key length.
  derived_key_length = 32

  user_salt = Base64.decode64(salt).b
  salt_separator = Base64.decode64(salt_separator).b
  password = password.b

  # https://stackoverflow.com/questions/40409876/encrypting-a-private-key-in-ruby-using-aes-128-ctr-scrypt
  SCrypt::Engine.scrypt(password, (user_salt + salt_separator), n, r, p, derived_key_length)
end

# https://gist.github.com/treble37/55459f5f0f218ab9b5ebe74b325f4a41#file-gistfile1-L33-L43
def encrypt signer_key, derived_key
  # https://github.com/spreedly/gala/blob/master/lib/gala/payment_token.rb#L113-L116
  iv = 0.chr * 16

  cipher = OpenSSL::Cipher::AES256.new :CTR
  cipher.encrypt
  cipher.iv = iv
  cipher.key = derived_key

  cipher.update(signer_key) + cipher.final
end

def verify_password password, known_hash, salt, salt_separator, signer_key, rounds, mem_cost
  derived_key = generate_derived_key password, salt, salt_separator, rounds, mem_cost
  signer_key = Base64.decode64(signer_key).b

  result = encrypt(signer_key, derived_key)

  # We use `strict_encode64` instead of `encode64` to ignore new line in result
  # https://stackoverflow.com/questions/2620975/strange-n-in-base64-encoded-string-in-ruby
  password_hash = Base64.strict_encode64(result).encode('utf-8')

  # Please change this line to `Rack::Utils.secure_compare(a, b)` or `ActiveSupport::SecurityUtils.secure_compare(a, b)`
  # If we use `==` method, it could lead to timing attacks.
  # https://github.com/mailgun/documentation/issues/133
  password_hash == known_hash
end


# Testing
salt_separator = "Bw=="
signer_key = "jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZJzxUECKbm0QsEmYUSDzZvpjeJ9WmXA=="
rounds= 8
mem_cost=14

password = "user1password"
salt = "42xEC+ixf3L2lw=="
password_hash="lSrfV15cpx95/sZS2W9c9Kp6i/LVgQNDNC/qzrCnh1SAyZvqmZqAjTdn3aoItz+VHjoZilo78198JAdRuid5lQ=="

is_valid = verify_password(password, password_hash, salt, salt_separator, signer_key, rounds, mem_cost)
puts is_valid

You could view this script via gist.

comments powered by Disqus