Skip to content

Instantly share code, notes, and snippets.

@presidentbeef
Last active August 30, 2020 11:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save presidentbeef/5ae26d1f55221a5f2297b334a036b6b4 to your computer and use it in GitHub Desktop.
Save presidentbeef/5ae26d1f55221a5f2297b334a036b6b4 to your computer and use it in GitHub Desktop.
Dropbox-style Password Storage
# Based on https://blogs.dropbox.com/tech/2016/09/how-dropbox-securely-stores-your-passwords/
require 'bcrypt' # bcrypt gem
require 'digest/sha2'
require 'openssl'
# Generate an encrypted hash from a plaintext password,
# given an AES key and AES IV.
def store(password, key, iv)
# Hash password to get good 512 bits
# because bcrypt only uses the first 72 bytes.
#
# Use base64 or hex digest to avoid null bytes,
# which cause bcrypt to truncate input!
hash = Digest::SHA512.new.base64digest(password)
# Hash the SHA hash again with bcrypt.
adaptive_hash = BCrypt::Password.create(hash)
# Encrypt the hash with a global key
cipher = OpenSSL::Cipher::AES256.new(:CBC)
cipher.encrypt
cipher.key = key
cipher.iv = iv
final_cipher_text = cipher.update(adaptive_hash.to_s) + cipher.final
end
# Compare the stored encrypted hash with a plaintext password
# given the same key and IV as used previously.
def compare(stored_cipher_text, input_password, key, iv)
# Hash plaintext password to get good 512 bits and avoid bcrypt truncation
hash = Digest::SHA512.new.base64digest(input_password)
# Decrypt the stored encrypted hash using global key/IV
decipher = OpenSSL::Cipher::AES256.new(:CBC)
decipher.decrypt
decipher.key = key
decipher.iv = iv
# This gets us back to the stored bcrypted hash
stored_bcrypted = decipher.update(stored_cipher_text) + decipher.final
# Now compare the stored bcrypt hash with base64 encoded hash -
# this is a little confusing because the BCrypt::Password class
# will automatically bcrypt the value being compared.
# So internally it will bcrypt(hash) before comparing,
# we don't have to do it ourselves.
#
# Also note we call BCrypt::Password.new, not create, because
# we are passing in the existing bcrypt hash, not plaintext.
BCrypt::Password.new(stored_bcrypted) == hash
end
# Below demonstrates setting up and using the above functions
# Generate and store a global AES key and IV
cipher = OpenSSL::Cipher::AES256.new(:CBC)
cipher.encrypt
key = cipher.random_key # Store somewhere global
iv = cipher.random_iv # Store somewhere global
print "Input test password: "
password = gets.chomp
# Generate the encrypted hash
stored_password_hash = store(password, key, iv)
puts "Stored, encrypted hash: #{stored_password_hash}"
print "Input comparison password: "
second_password = gets.chomp
# Compare the encrypted hash with an input plaintext password
print "Matched? "
puts compare(stored_password_hash, second_password, key, iv)
@presidentbeef
Copy link
Author

presidentbeef commented Feb 26, 2019

This is a demonstration of using the style of password hash storage described by this post from Dropbox implemented in Ruby.

While it looks "wrong" to say "encrypted hash", that is exactly what this code does: it hashes the password with SHA2-512, hashes the Base64 encoded hash with bcrypt, then encrypts the bcrypted hash with AES.

As with any cryptography-related information, don't trust random code you find on the Internet - this is for demonstration purposes only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment