Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Login Form from Scratch.

Login Form from Scratch.

At Tokyu RubyKaigi 06 on June 29, 2012

Naoto Takai

June 29, 2013
Tweet

More Decks by Naoto Takai

Other Decks in Programming

Transcript

  1. def create @login = Login.new(login_params) digest = Digest::SHA1.hexdigest(@login.password) if (user

    = User.find_by(email: @login.email, password_digest: digest)) session[:user] = user.id redirect_to @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' render :new end end
  2. def create @login = Login.new(login_params) digest = Digest::SHA1.hexdigest(@login.password) if (user

    = User.find_by(email: @login.email, password_digest: digest)) session[:user] = user.id redirect_to @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' render :new end end
  3. NUM_OF_ITERATIONS = 20000 DIGEST_ALGORITHM = OpenSSL::Digest::SHA512.new KEY_LEN = DIGEST_ALGORITHM.length salt

    = SecureRandom.hex(32) OpenSSL::PKCS5.pbkdf2_hmac(password, salt, NUM_OF_ITERATIONS, KEY_LEN, DIGEST_ALGORITHM) # => "L\xBEv-\x83\xC2\x98\x1D..."  A minimum of 1,000 iterations is recommended in RFC 2898 (over ten years ago).  Use random and different salt for every password.
  4. def create @login = Login.new(login_params) user = User.find_by(email: @login.email) ||

    User.new(salt: '') digest_bytes = OpenSSL::PKCS5.pbkdf2_hmac(@login.password, user.salt, NUM_OF_ITERATIONS, KEY_LEN, DIGEST_ALGORITHM) password_digest = digest_bytes.unpack('H*').first if user.password_digest == password_digest session[:user] = user.id redirect_to @login.original_url || root_url ...
  5. def create @login = Login.new(login_params) user = User.find_by(email: @login.email) ||

    User.new(salt: '') digest_bytes = OpenSSL::PKCS5.pbkdf2_hmac(@login.password, user.salt, NUM_OF_ITERATIONS, KEY_LEN, DIGEST_ALGORITHM) password_digest = digest_bytes.unpack('H*').first if user.password_digest == password_digest session[:user] = user.id redirect_to @login.original_url || root_url ...
  6. Use secure_compare instead of == to avoid a timing attack.

     Timing attacks would not be viable in our case.
  7. module Rack module Utils # Constant time string comparison. def

    secure_compare(a, b) return false unless bytesize(a) == bytesize(b) l = a.unpack("C*") r, i = 0, -1 b.each_byte { |v| r |= v ^ l[i+=1] } r == 0 end module_function :secure_compare  https://github.com/rack/rack/blob/rack-1.5/lib/rack/utils.rb#L398-L408
  8. lhs = '7319bf2143beb945070723bf54965b2d' lhs2 = '7319bf2143beb945070723bf54965b2d' rhs = '1e64ac58521914da0e3804326e8a8a75' include

    Rack::Utils Benchmark.bm(12) do |x| x.report('#1 == (diff)') { i.times { lhs == rhs }} x.report('#2 == (same)') { i.times { lhs == lhs2 }} x.report('#3 sc (diff)') { i.times { secure_compare(lhs, rhs) }} x.report('#4 sc (same)') { i.times { secure_compare(lhs, lhs2) }} end __END__ user system total real #1 == (diff) 0.080000 0.000000 0.080000 ( 0.080617) #2 == (same) 0.090000 0.000000 0.090000 ( 0.085593) #3 sc (diff) 6.520000 0.070000 6.590000 ( 6.587122) #4 sc (same) 6.500000 0.060000 6.560000 ( 6.561539)
  9. ... digest_bytes = OpenSSL::PKCS5.pbkdf2_hmac(@login.password, user.salt, NUM_OF_ITERATIONS, KEY_LEN, DIGEST_ALGORITHM) password_digest =

    digest_bytes.unpack('H*').first if Rack::Utils.secure_compare(user.password_digest, password_digest) session[:user] = user.id redirect_to @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' ...
  10. ... digest_bytes = OpenSSL::PKCS5.pbkdf2_hmac(@login.password, user.salt, NUM_OF_ITERATIONS, KEY_LEN, DIGEST_ALGORITHM) password_digest =

    digest_bytes.unpack('H*').first if Rack::Utils.secure_compare(user.password_digest, password_digest) session[:user] = user.id redirect_to @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' ...
  11. Attacker Victim Web App issues a session id feeds the

    session id accesses with the session id logs in
  12. ... password_digest = digest_bytes.unpack('H*').first if Rack::Utils.secure_compare(user.password_digest, password_digest) reset_session session[:user] =

    user.id redirect_to @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' render :new end end ...
  13. ... password_digest = digest_bytes.unpack('H*').first if Rack::Utils.secure_compare(user.password_digest, password_digest) reset_session session[:user] =

    user.id redirect_to @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' render :new end end ...
  14. Attacker Victim Secure zone data sensitive data sni cookies session:

    e8a744 session: e8a744 spoof session: e8a744
  15. Attacker Victim Secure zone data sensitive data sni cookies session:

    71fc6e session: e8a744 spoof session: e8a744
  16. def set_login_cookies(user_id) key = SecureRandom.uuid secure_key = SecureRandom.uuid SessionStorage.set(key, user_id)

    SessionStorage.set(secure_key, user_id) cookies.signed[:user_session] = { expires: 1.week.from_now, value: key, httponly: true } cookies.signed[:secure_user_session] = { expires: 1.week.from_now, value: secure_key, httponly: true, secure: true } end
  17. ... password_digest = digest_bytes.unpack('H*').first if Rack::Utils.secure_compare(user.password_digest, password_digest) reset_session set_login_cookies(user.id) redirect_to

    @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' render :new end end ...
  18. ... password_digest = digest_bytes.unpack('H*').first if Rack::Utils.secure_compare(user.password_digest, password_digest) reset_session set_login_cookies(user.id) redirect_to

    @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' render :new end end ...
  19. ... set_login_cookies(user.id) redirect_to sanitize_url(@login.original_url) || root_url else @login.errors[:base] << 'Please

    enter a correct username and password.' render :new end end ... def sanitize_url(url) URI.parse(url).host == request.host ? url : nil rescue nil end
  20. == form_for :login, url: login_path do |f| .row .large-4.columns ==

    f.label :email == f.text_field :email, autocomplete: 'off' .row .large-4.columns == f.label :password == f.password_field :password .row .large-4.columns == f.hidden_field :original_url == f.submit Login, class: 'button'