Skip to content

Instantly share code, notes, and snippets.

@tsdorsey
Last active December 3, 2015 22:33
Show Gist options
  • Save tsdorsey/d56fdb25d83246f18942 to your computer and use it in GitHub Desktop.
Save tsdorsey/d56fdb25d83246f18942 to your computer and use it in GitHub Desktop.
Ruby version of .NET membership password hashing
require 'base64'
require 'openssl'
require 'securerandom'
# Creates and interprets .NET RFC2898 version 0 encoded passwords.
# Password format is Base64.strict_encode64(\0x0 + salt + subkey)
def encode(str)
Base64.strict_encode64(str)
end
def decode(str)
Base64.strict_decode64(str)
end
ITERATIONS = 1000
SUBKEYLENGTH = 256 / 8 # 256 bit aka 32 bytes.
SALTLENGTH = 128 / 8 # 128 bit aka 16 bytes.
VERSION = decode('AA==') # 0b00000000.
# From the .NET helper WebMatrix.
def hashPassword(password)
raise ArgumentError.new('password') if password.nil?
saltBytes = SecureRandom.random_bytes(SALTLENGTH)
return _encodePasswordV0(saltBytes, rfc2898DeriveBytes(password, saltBytes, ITERATIONS, SUBKEYLENGTH))
end
def verifyHashedPassword(hashedPassword, password)
_validateHashedPassword(hashedPassword)
raise ArgumentError.new('password') if password.nil?
saltBytes = extractSaltBytes(hashedPassword)
subkeyBytes = extractSubkeyBytes(hashedPassword)
generatedSubkeyBytes = rfc2898DeriveBytes(password, saltBytes, ITERATIONS, SUBKEYLENGTH)
return generatedSubkeyBytes == subkeyBytes
end
# From .NET cryto.
def extractSaltBytes(hashedPassword)
_validateHashedPassword(hashedPassword)
hashedPasswordBytes = decode(hashedPassword)
# Pull the salt out. It's the second byte and runs until salt length.
return hashedPasswordBytes[1..SALTLENGTH]
end
def extractSubkeyBytes(hashedPassword)
_validateHashedPassword(hashedPassword)
hashedPasswordBytes = decode(hashedPassword)
# Pull out the subkey. It's at 1+SALTLENGTH until the end (we've already
# checked total length).
subkeyBytes = hashedPasswordBytes[(1+SALTLENGTH)..-1]
end
def rfc2898DeriveBytes(password, salt, iterations, subkeyLength)
return OpenSSL::PKCS5.pbkdf2_hmac_sha1(password, salt, iterations, subkeyLength)
end
def _encodePasswordV0(salt, subkey)
return encode(VERSION + salt + subkey)
end
def _validateHashedPassword(hashedPassword)
raise ArgumentError.new('hashedPassword') if hashedPassword.nil?
hashedPasswordBytes = decode(hashedPassword)
raise ArgumentError.new('hash length') unless hashedPasswordBytes.length == (1 + SALTLENGTH + SUBKEYLENGTH)
raise ArgumentError.new('hash version') unless hashedPasswordBytes[0] == VERSION
end
# Test cases to validate the verifyHashedPassword method.
def testKnown()
for password, hash in [
# Top 25 most used passwords in 2014.
['123456', 'AH9EGs/zg3gzLLdKuEe44+ShE07rl7K1uoQ1fXFM1GjdWdz8MamHpQhaeFgaUDzv0A=='],
['password', 'ACHYpKal/KMrWR+b2ucEFmYhCc5K/RfXJPUq0KAXxUjvN76EuxSnH7Cr0wNsnmPTmA=='],
['12345', 'ADIYyvBkHa8lySq31mg0pAsT9sLe429VDDecKSP0dsVs2iR0ZJn5y2jAIaK0d6n5kg=='],
['12345678', 'AO1d73Vwc9T7ELETWKQmZXM9Hvuq1X54dPsIzG67JghRUPGYiWfKiAchJwhu6p8TJA=='],
['qwerty', 'AKOG2irQDjpBop3XaPQBqhCH86PoIGf9HJixEfp8WvepsYCALj1JnPhSICoi4smBiw=='],
['123456789', 'AIFzYbLfY8ucUJf3F4BltNx6YLAHysEM7gO5vuAd+QkV9OO1KjJwRgxCAIxS21oK0A=='],
['1234', 'AHjq0nf1wqG05TRTMTJiBWNYFECfqkndMpvKxyXbsbVXgADdkmN/jLB8yT7+hSV5Dw=='],
['baseball', 'AGvOuBwVMzUTjL0xupvqnv4eRbTioc3uIQcc5z8elsBWnb5crvWrXcZDBzPh5HC5Tw=='],
['dragon', 'AL7Ur4DS9FJLqNHaT5nWIrX5HBRC7IWIG6fYvnvESlcTAIjC1rCxzlZUpOmqOuAo6g=='],
['football', 'AJlA1pBtUFGzEHCqYA8Ns9Fh2zdZzzSwhNCK23S50FDK20YNDnbiDzQIW4xNlB5Jug=='],
['1234567', 'AM1alPNB4IPW2lIVst6bK28KDhWo+2Ezm7R/yC7+dhDhhq2RZ3JZ5oZrLE5974ASWw=='],
['monkey', 'AKn4uYPd63v47cBLll96E8PS1aEm90P3pwlLG+oQs1KFkeDYszdn6K2g0xIiDOOOuw=='],
['letmein', 'AL0Dy8TxuFGa/E4sda2YXv6hJIH+KCwE0WlwcNmiuMDuj7Fec0eEGDgRbFYGklUCYA=='],
['abc123', 'AGYl/6ZefhOGZTQuJ6itnjSOOokRpvfx1lCCXnRTXE/zHYDKwW2mFfhjdRvjnLI5Zg=='],
['111111', 'AGf75M6AboBq6yxzIbk7+26IZzQ3coDKUTVNBqLC3j6KKD2F1IzK8sPAaYSNdTtXDg=='],
['mustang', 'AAcX+qlW53M+USTu+Q5AN899NJTf4AJM/GQ8lQ5jaWVWZjceTJk/F6LUAuxHqWN7pQ=='],
['access', 'AKNT+A6rEp/D0/WFOgZAijZ1ifAs8k+GrWQGNQFUYAyUrt+HzA3ibo3UiVb445CU3g=='],
['shadow', 'AN/cBXaQ86Lksk2EDmr5Iu+MfOZjxwHuhKUPRFM67IuK/EpEuTBfMoBX9MwRVBH71w=='],
['master', 'AJuGySYECMScUY3lU4m2+BdeMNk/Qg1FUlZCyz5U8wYxtZrBgO2Vm0bMoPQGOc1mRA=='],
['michael', 'AEhCExzV1169K6PTGOYYm2z7X0kBs/+D+CqAp4prsgYePHJVfKcugmkMchb0/e9mKg=='],
['superman', 'ABdvz5mu9pqVPibF+Y/xHkyIasMblxk+t6XQQ4vRGiXTAZ4Aue+BINc5U36I2etMoQ=='],
['696969', 'ACq4T2oUestvtjOrMnUkoOSMRAd/d8ZD5BgsDgrldtJGvF/rSJkWrZJtwg08Xs5Q+w=='],
['123123', 'AKJfsCwXdHqfs5O+3FJwnkTP+tKafirFlRcOYWqVDvWQ2oWkUphUiXz5i3nSLhDF5g=='],
['batman', 'AKXTvBN0teL3OksQYtxp7//5bRlT97cgjivBmTYkYb80CUMPtADffdik6REO2935qQ=='],
['trustno1', 'AAzh2KHhb/qln6ZcxPweacXhITuaAWmOYV9+8/xAdGW3XMVWBWtp9iE2Rs4gQPy7mw==']
]
if verifyHashedPassword(hash, password)
puts "#{hash} === #{password}"
else
puts "#{hash} =/= #{password}"
end
end
return nil
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment