Ruby - Firebase SCrypt in Ruby
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.
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.