Last active
August 30, 2020 11:31
-
-
Save presidentbeef/5ae26d1f55221a5f2297b334a036b6b4 to your computer and use it in GitHub Desktop.
Dropbox-style Password Storage
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.