Last active
December 3, 2015 22:33
-
-
Save tsdorsey/d56fdb25d83246f18942 to your computer and use it in GitHub Desktop.
Ruby version of .NET membership password hashing
This file contains hidden or 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
| 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