Skip to content

Commit

Permalink
Use PBKDF2 with 10000 iteration as default password hashing method.
Browse files Browse the repository at this point in the history
  • Loading branch information
antirez committed Oct 22, 2011
1 parent 7315661 commit d23a38b
Show file tree
Hide file tree
Showing 4 changed files with 363 additions and 4 deletions.
15 changes: 11 additions & 4 deletions app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,17 @@
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of Salvatore Sanfilippo.

require 'app_config'
require 'rubygems'
require 'hiredis'
require 'redis'
require 'page'
require 'app_config'
require 'sinatra'
require 'json'
require 'digest/sha1'
require 'digest/md5'
require 'comments'
require 'pbkdf2'

before do
$r = Redis.new(:host => RedisHost, :port => RedisPort) if !$r
Expand Down Expand Up @@ -632,10 +633,16 @@ def update_auth_token(user_id)
return new_auth_token
end

# Turn the password into an hashed one, using
# SHA1(salt|password).
# Turn the password into an hashed one, using PBKDF2 with HMAC-SHA1
# and 160 bit output.
def hash_password(password)
Digest::SHA1.hexdigest(PasswordSalt+password)
p = PBKDF2.new do |p|
p.iterations = PBKDF2Iterations
p.password = password
p.salt = PasswordSalt

This comment has been minimized.

Copy link
@jneen

jneen Oct 22, 2011

...so we're still using a global salt? This still seems like a Bad Idea.

This comment has been minimized.

Copy link
@antirez

antirez Oct 22, 2011

Author Owner

my fear is to make this programming example a bit too complicated, however since after benchmarking the code with 10000 iterations it was too slow, and the default is now set to 1000, the random salt can provide another level of protection without any CPU cost, so it's worth it I guess. I'll fix it. Also woud be good to support oauth as well so that people can authenticate with Twitter and don't care at all about how we store passwords.

This comment has been minimized.

Copy link
@antirez

antirez Oct 22, 2011

Author Owner

@jayferd you can see the fix at commit "d7ff970"

This comment has been minimized.

Copy link
@jneen

jneen Oct 22, 2011

+1 for OAuth/2. I just want to take another opportunity to mention that for a redis programming example, it's much simpler just to throw bcrypt in the Gemfile than it is to re-implement PBKDF2. In ruby-land, handling dependencies is mostly a solved problem. It's much easier for developers to understand how that works :).

This comment has been minimized.

Copy link
@antirez

antirez Oct 22, 2011

Author Owner

I agree with you about simplicity, but since we had that long discussion about password hashing done right at this point I tried to do things the right way, using a nist-approved primitive (sha1), a well analyzed algorithm like PBKDF2, and setting a minimum password length.
Also using PBKDF2 I was able to depend only on pure-ruby ruby-hmac, openssl is used only if available to speedup. More than number of dependencies I'm concerned with installation problems.

Btw the hashing is not a big problem for the understanding of the system as it is into a separated file, while the user salt adds complexity to the user object. But after all... this is also instructive for the reader, since without all this stuff it is not possible to write a decent auth system. So I implemented per user salt in the latest commit.

p.key_length = 160/8
end
p.hex_string
end

# Return the user from the ID.
Expand Down
1 change: 1 addition & 0 deletions app_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
RedisPort = 10000

# Security
PBKDF2Iterations = 10000 # 10000 will make an attack harder but is slow.
PasswordSalt = "*LAMER*news*"

# Comments
Expand Down
163 changes: 163 additions & 0 deletions pbkdf2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Modified by Salvatore Sanfilippo to only support HMAC-SHA1 from
# ruby-hmac gem in order to drop the openssl dependency.
#
# Copyright (c) 2008 Sam Quigley <quigley@emerose.com>
# Copyright (c) 2011 Salvatore Sanfilippo <antirez@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# This license is sometimes referred to as "The MIT License"

require 'rubygems'
require 'hmac-sha1'

class PBKDF2
def initialize(opts={})
# override with options
opts.each_key do |k|
if self.respond_to?("#{k}=")
self.send("#{k}=", opts[k])
else
raise ArgumentError, "Argument '#{k}' is not allowed"
end
end

yield self if block_given?

# set this to the default if nothing was given
@key_length ||= 20

# make sure the relevant things got set
raise ArgumentError, "password not set" if @password.nil?
raise ArgumentError, "salt not set" if @salt.nil?
raise ArgumentError, "iterations not set" if @iterations.nil?
end
attr_reader :key_length, :iterations, :salt, :password

def key_length=(l)
raise ArgumentError, "key too short" if l < 1
raise ArgumentError, "key too long" if l > ((2**32 - 1) * 20)
@value = nil
@key_length = l
end

def iterations=(i)
raise ArgumentError, "iterations can't be less than 1" if i < 1
@value = nil
@iterations = i
end

def salt=(s)
@value = nil
@salt = s
end

def password=(p)
@value = nil
@password = p
end

def value
calculate! if @value.nil?
@value
end

alias bin_string value

def hex_string
bin_string.unpack("H*").first
end

# return number of milliseconds it takes to complete one iteration
def benchmark(iters = 400000)
iter_orig = @iterations
@iterations=iters
start = Time.now
calculate!
time = Time.now - start
@iterations = iter_orig
return (time/iters)
end

protected

# the pseudo-random function defined in the spec
def prf(data)
HMAC::SHA1.digest(@password,data)
end

# this is a translation of the helper function "F" defined in the spec
def calculate_block(block_num)
# u_1:
u = prf(salt+[block_num].pack("N"))
ret = u
# u_2 through u_c:
2.upto(@iterations) do
# calculate u_n
u = prf(u)
# xor it with the previous results
ret = ret^u
end
ret
end

# the bit that actually does the calculating
def calculate!
# how many blocks we'll need to calculate (the last may be truncated)
blocks_needed = (@key_length.to_f / 20).ceil
# reset
@value = ""
# main block-calculating loop:
1.upto(blocks_needed) do |block_num|
@value << calculate_block(block_num)
end
# truncate to desired length:
@value = @value.slice(0,@key_length)
@value
end
end

class String
if RUBY_VERSION >= "1.9"
def xor_impl(other)
result = ""
o_bytes = other.bytes.to_a
bytes.each_with_index do |c, i|
result << (c ^ o_bytes[i])
end
result
end
else
def xor_impl(other)
result = (0..self.length-1).collect { |i| self[i] ^ other[i] }
result.pack("C*")
end
end

private :xor_impl

def ^(other)
raise ArgumentError, "Can't bitwise-XOR a String with a non-String" \
unless other.kind_of? String
raise ArgumentError, "Can't bitwise-XOR strings of different length" \
unless self.length == other.length

xor_impl(other)
end
end
188 changes: 188 additions & 0 deletions spec/pbkdf2_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
require '../pbkdf2.rb'

describe PBKDF2, "when deriving keys" do
# see http://www.rfc-archive.org/getrfc.php?rfc=3962
# all examples there use HMAC-SHA1
it "should match the first test case in appendix B of RFC 3962" do
# Iteration count = 1
# Pass phrase = "password"
# Salt = "ATHENA.MIT.EDUraeburn"
# 128-bit PBKDF2 output:
# cd ed b5 28 1b b2 f8 01 56 5a 11 22 b2 56 35 15
# 256-bit PBKDF2 output:
# cd ed b5 28 1b b2 f8 01 56 5a 11 22 b2 56 35 15
# 0a d1 f7 a0 4b b9 f3 a3 33 ec c0 e2 e1 f7 08 37
p = PBKDF2.new do |p|
p.iterations = 1
p.password = "password"
p.salt = "ATHENA.MIT.EDUraeburn"
p.key_length = 128/8
end

expected = "cd ed b5 28 1b b2 f8 01 56 5a 11 22 b2 56 35 15"
p.hex_string.should == expected.gsub(' ','')

expected = "cd ed b5 28 1b b2 f8 01 56 5a 11 22 b2 56 35 15" +
"0a d1 f7 a0 4b b9 f3 a3 33 ec c0 e2 e1 f7 08 37"

p.key_length = 256/8
p.hex_string.should == expected.gsub(' ','')
end

it "should match the second test case in appendix B of RFC 3962" do
# Iteration count = 2
# Pass phrase = "password"
# Salt="ATHENA.MIT.EDUraeburn"
# 128-bit PBKDF2 output:
# 01 db ee 7f 4a 9e 24 3e 98 8b 62 c7 3c da 93 5d
# 256-bit PBKDF2 output:
# 01 db ee 7f 4a 9e 24 3e 98 8b 62 c7 3c da 93 5d
# a0 53 78 b9 32 44 ec 8f 48 a9 9e 61 ad 79 9d 86
p = PBKDF2.new do |p|
p.iterations = 2
p.password = "password"
p.salt = "ATHENA.MIT.EDUraeburn"
p.key_length = 128/8
end

expected = "01 db ee 7f 4a 9e 24 3e 98 8b 62 c7 3c da 93 5d"
p.hex_string.should == expected.gsub(' ','')

expected = "01 db ee 7f 4a 9e 24 3e 98 8b 62 c7 3c da 93 5d" +
"a0 53 78 b9 32 44 ec 8f 48 a9 9e 61 ad 79 9d 86"
p.key_length = 256/8
p.hex_string.should == expected.gsub(' ','')
end

it "should match the third test case in appendix B of RFC 3962" do
# Iteration count = 1200
# Pass phrase = "password"
# Salt = "ATHENA.MIT.EDUraeburn"
# 128-bit PBKDF2 output:
# 5c 08 eb 61 fd f7 1e 4e 4e c3 cf 6b a1 f5 51 2b
# 256-bit PBKDF2 output:
# 5c 08 eb 61 fd f7 1e 4e 4e c3 cf 6b a1 f5 51 2b
# a7 e5 2d db c5 e5 14 2f 70 8a 31 e2 e6 2b 1e 13
p = PBKDF2.new do |p|
p.iterations = 1200
p.password = "password"
p.salt = "ATHENA.MIT.EDUraeburn"
p.key_length = 128/8
end

expected = "5c 08 eb 61 fd f7 1e 4e 4e c3 cf 6b a1 f5 51 2b"
p.hex_string.should == expected.gsub(' ','')

expected = "5c 08 eb 61 fd f7 1e 4e 4e c3 cf 6b a1 f5 51 2b" +
"a7 e5 2d db c5 e5 14 2f 70 8a 31 e2 e6 2b 1e 13"
p.key_length = 256/8
p.hex_string.should == expected.gsub(' ','')
end

it "should match the fourth test case in appendix B of RFC 3962" do
# Iteration count = 5
# Pass phrase = "password"
# Salt=0x1234567878563412
# 128-bit PBKDF2 output:
# d1 da a7 86 15 f2 87 e6 a1 c8 b1 20 d7 06 2a 49
# 256-bit PBKDF2 output:
# d1 da a7 86 15 f2 87 e6 a1 c8 b1 20 d7 06 2a 49
# 3f 98 d2 03 e6 be 49 a6 ad f4 fa 57 4b 6e 64 ee
p = PBKDF2.new do |p|
p.iterations = 5
p.password = "password"
p.salt = [0x1234567878563412].pack("Q")
p.key_length = 128/8
end

expected = "d1 da a7 86 15 f2 87 e6 a1 c8 b1 20 d7 06 2a 49"
p.hex_string.should == expected.gsub(' ','')

expected = "d1 da a7 86 15 f2 87 e6 a1 c8 b1 20 d7 06 2a 49" +
"3f 98 d2 03 e6 be 49 a6 ad f4 fa 57 4b 6e 64 ee"
p.key_length = 256/8
p.hex_string.should == expected.gsub(' ','')
end

it "should match the fifth test case in appendix B of RFC 3962" do
# Iteration count = 1200
# Pass phrase = (64 characters)
# "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
# Salt="pass phrase equals block size"
# 128-bit PBKDF2 output:
# 13 9c 30 c0 96 6b c3 2b a5 5f db f2 12 53 0a c9
# 256-bit PBKDF2 output:
# 13 9c 30 c0 96 6b c3 2b a5 5f db f2 12 53 0a c9
# c5 ec 59 f1 a4 52 f5 cc 9a d9 40 fe a0 59 8e d1
p = PBKDF2.new do |p|
p.iterations = 1200
p.password = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
p.salt = "pass phrase equals block size"
p.key_length = 128/8
end

expected = "13 9c 30 c0 96 6b c3 2b a5 5f db f2 12 53 0a c9"
p.hex_string.should == expected.gsub(' ','')

expected = "13 9c 30 c0 96 6b c3 2b a5 5f db f2 12 53 0a c9" +
"c5 ec 59 f1 a4 52 f5 cc 9a d9 40 fe a0 59 8e d1"
p.key_length = 256/8
p.hex_string.should == expected.gsub(' ','')
end

it "should match the sixth test case in appendix B of RFC 3962" do
# Iteration count = 1200
# Pass phrase = (65 characters)
# "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
# Salt = "pass phrase exceeds block size"
# 128-bit PBKDF2 output:
# 9c ca d6 d4 68 77 0c d5 1b 10 e6 a6 87 21 be 61
# 256-bit PBKDF2 output:
# 9c ca d6 d4 68 77 0c d5 1b 10 e6 a6 87 21 be 61
# 1a 8b 4d 28 26 01 db 3b 36 be 92 46 91 5e c8 2a
p = PBKDF2.new do |p|
p.iterations = 1200
p.password = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
p.salt = "pass phrase exceeds block size"
p.key_length = 128/8
end

expected = "9c ca d6 d4 68 77 0c d5 1b 10 e6 a6 87 21 be 61"
p.hex_string.should == expected.gsub(' ','')

expected = "9c ca d6 d4 68 77 0c d5 1b 10 e6 a6 87 21 be 61" +
"1a 8b 4d 28 26 01 db 3b 36 be 92 46 91 5e c8 2a"
p.key_length = 256/8
p.hex_string.should == expected.gsub(' ','')
end

it "should match the seventh test case in appendix B of RFC 3962" do
# Iteration count = 50
# Pass phrase = g-clef (0xf09d849e)
# Salt = "EXAMPLE.COMpianist"
# 128-bit PBKDF2 output:
# 6b 9c f2 6d 45 45 5a 43 a5 b8 bb 27 6a 40 3b 39
# 256-bit PBKDF2 output:
# 6b 9c f2 6d 45 45 5a 43 a5 b8 bb 27 6a 40 3b 39
# e7 fe 37 a0 c4 1e 02 c2 81 ff 30 69 e1 e9 4f 52
p = PBKDF2.new do |p|
p.iterations = 50
# this is a gorram horrible test case. it took me quite a while to
# track down why 0xf09d849e should be interpreted as "\360\235\204\236"
# (which is what other code uses for this example). the mysterious
# "g-clef" annotation didn't help (turns out to be a Unicode character
# in UTF8 -- ie, 0xf0 0x9d 0x84 0x9e)
p.password = [0xf09d849e].pack("N")
p.salt = "EXAMPLE.COMpianist"
p.key_length = 128/8
end

expected = "6b 9c f2 6d 45 45 5a 43 a5 b8 bb 27 6a 40 3b 39"
p.hex_string.should == expected.gsub(' ','')

expected = "6b 9c f2 6d 45 45 5a 43 a5 b8 bb 27 6a 40 3b 39" +
"e7 fe 37 a0 c4 1e 02 c2 81 ff 30 69 e1 e9 4f 52"
p.key_length = 256/8
p.hex_string.should == expected.gsub(' ','')
end
end

0 comments on commit d23a38b

Please sign in to comment.