htauth 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -20,12 +20,16 @@ module HTAuth
20
20
 
21
21
  def ask(prompt)
22
22
  output.print prompt
23
- answer = input.noecho(&:gets)
23
+ answer = read_answer
24
24
  output.puts
25
25
  raise ConsoleError, "No input given" if answer.nil?
26
26
  answer.strip!
27
27
  raise ConsoleError, "No input given" if answer.length == 0
28
28
  return answer
29
29
  end
30
+
31
+ def read_answer
32
+ input.noecho(&:gets)
33
+ end
30
34
  end
31
35
  end
@@ -4,12 +4,23 @@ module HTAuth
4
4
  # Internal: The basic crypt algorithm
5
5
  class Crypt < Algorithm
6
6
 
7
- def initialize(params = {})
8
- @salt = params[:salt] || params['salt'] || gen_salt
7
+ ENTRY_LENGTH = 13
8
+ ENTRY_REGEX = %r{\A[^$:\s]{#{ENTRY_LENGTH}}\z}
9
+
10
+ def self.handles?(password_entry)
11
+ ENTRY_REGEX.match?(password_entry)
9
12
  end
10
13
 
11
- def prefix
12
- ""
14
+ def self.extract_salt_from_existing_password_field(existing)
15
+ existing[0,2]
16
+ end
17
+
18
+ def initialize(params = {})
19
+ if existing = (params['existing'] || params[:existing]) then
20
+ @salt = self.class.extract_salt_from_existing_password_field(existing)
21
+ else
22
+ @salt = params[:salt] || params['salt'] || gen_salt
23
+ end
13
24
  end
14
25
 
15
26
  def encode(password)
@@ -0,0 +1,46 @@
1
+ module HTAuth
2
+ #
3
+ # Use by either
4
+ #
5
+ # class Foo
6
+ # extend DescendantTracker
7
+ # end
8
+ #
9
+ # or
10
+ #
11
+ # class Foo
12
+ # class << self
13
+ # include DescendantTracker
14
+ # end
15
+ # end
16
+ #
17
+ # It will track all the classes that inherit from the extended class and keep
18
+ # them in a Set that is available via the 'children' method.
19
+ #
20
+ module DescendantTracker
21
+ def inherited( klass )
22
+ return unless klass.instance_of?( Class )
23
+ self.children << klass
24
+ end
25
+
26
+ #
27
+ # The list of children that are registered
28
+ #
29
+ def children
30
+ unless defined? @children
31
+ @children = Array.new
32
+ end
33
+ return @children
34
+ end
35
+
36
+ #
37
+ # find the child that returns truthy for then given method and
38
+ # parameters
39
+ #
40
+ def find_child( method, *args )
41
+ children.find do |child|
42
+ child.send( method, *args )
43
+ end
44
+ end
45
+ end
46
+ end
@@ -6,21 +6,41 @@ module HTAuth
6
6
  # as used in the apache htpasswd -m option
7
7
  class Md5 < Algorithm
8
8
 
9
- DIGEST_LENGTH = 16
9
+ DIGEST_LENGTH = 16
10
+ PAD_LENGTH = 6
11
+ PREFIX = "$apr1$".freeze
12
+ SALT_CHARS_STR = SALT_CHARS.join('')
13
+ ENTRY_REGEX = %r[
14
+ \A
15
+ #{Regexp.escape(PREFIX)}
16
+ [#{SALT_CHARS_STR}]{#{SALT_LENGTH}}
17
+ #{Regexp.escape("$")}
18
+ [#{SALT_CHARS_STR}]{#{DIGEST_LENGTH + PAD_LENGTH}}
19
+ \z
20
+ ]x
21
+
22
+ def self.handles?(password_entry)
23
+ ENTRY_REGEX.match?(password_entry)
24
+ end
10
25
 
11
- def initialize(params = {})
12
- @salt = params['salt'] || params[:salt] || gen_salt
26
+ def self.extract_salt_from_existing_password_field(existing)
27
+ p = existing.split("$")
28
+ return p[2]
13
29
  end
14
30
 
15
- def prefix
16
- "$apr1$"
31
+ def initialize(params = {})
32
+ if existing = (params['existing'] || params[:existing]) then
33
+ @salt = self.class.extract_salt_from_existing_password_field(existing)
34
+ else
35
+ @salt = params[:salt] || params['salt'] || gen_salt
36
+ end
17
37
  end
18
38
 
19
39
  # this algorigthm pulled straight from apr_md5_encode() and converted to ruby syntax
20
40
  def encode(password)
21
41
  primary = ::Digest::MD5.new
22
42
  primary << password
23
- primary << prefix
43
+ primary << PREFIX
24
44
  primary << @salt
25
45
 
26
46
  md5_t = ::Digest::MD5.digest("#{password}#{@salt}#{password}")
@@ -46,7 +66,7 @@ module HTAuth
46
66
 
47
67
  pd = primary.digest
48
68
 
49
- encoded_password = "#{prefix}#{@salt}$"
69
+ encoded_password = "#{PREFIX}#{@salt}$"
50
70
 
51
71
  # apr_md5_encode has this comment about a 60Mhz Pentium above this loop.
52
72
  1000.times do |x|
@@ -12,6 +12,8 @@ module HTAuth
12
12
  attr_accessor :digest
13
13
  # Internal: the algorithm used to create the digest of this entry
14
14
  attr_reader :algorithm
15
+ # Internal: the algorithm arguments used to create the digest of this entry
16
+ attr_reader :algorithm_args
15
17
 
16
18
  class << self
17
19
  # Internal: Create an instance of this class from a line of text
@@ -23,7 +25,7 @@ module HTAuth
23
25
  parts = is_entry!(line)
24
26
  d = PasswdEntry.new(parts[0])
25
27
  d.digest = parts[1]
26
- d.algorithm = Algorithm.algorithms_from_field(parts[1])
28
+ d.algorithm = Algorithm.algorithm_from_field(parts[1])
27
29
  return d
28
30
  end
29
31
 
@@ -67,23 +69,31 @@ module HTAuth
67
69
 
68
70
  # Internal: set the algorithm for the entry
69
71
  def algorithm=(alg)
70
- if alg.kind_of?(Array) then
71
- if alg.size == 1 then
72
- @algorithm = alg.first
73
- else
74
- @algorithm = alg
75
- end
72
+ return @algorithm if Algorithm::EXISTING == alg
73
+ case alg
74
+ when String
75
+ @algorithm = Algorithm.algorithm_from_name(alg)
76
+ when ::HTAuth::Algorithm
77
+ @algorithm = alg
76
78
  else
77
- @algorithm = Algorithm.algorithm_from_name(alg) unless Algorithm::EXISTING == alg
79
+ raise InvalidAlgorithmError, "Unable to assign #{alg} to algorithm"
78
80
  end
79
81
  return @algorithm
80
82
  end
81
83
 
84
+ # Internal: set fields on the algorithm
85
+ def algorithm_args=(args)
86
+ args.each do |key, value|
87
+ method = "#{key}="
88
+ @algorithm.send(method, value) if @algorithm.respond_to?(method)
89
+ end
90
+ end
91
+
82
92
  # Internal: Update the password of the entry with its new value
83
93
  #
84
94
  # If we have an array of algorithms, then we set it to CRYPT
85
95
  def password=(new_password)
86
- if algorithm.kind_of?(Array) then
96
+ if algorithm.kind_of?(HTAuth::Plaintext) then
87
97
  @algorithm = Algorithm.algorithm_from_name(Algorithm::CRYPT)
88
98
  end
89
99
  @digest = calc_digest(new_password)
@@ -102,14 +112,9 @@ module HTAuth
102
112
  # circuiting
103
113
  def authenticated?(check_password)
104
114
  authed = false
105
- if algorithm.kind_of?(Array) then
106
- algorithm.each do |alg|
107
- encoded = alg.encode(check_password)
108
- if Algorithm.secure_compare(encoded, digest) then
109
- @algorithm = alg
110
- authed = true
111
- end
112
- end
115
+ if algorithm.kind_of?(Bcrypt) then
116
+ bc = ::BCrypt::Password.new(digest)
117
+ authed = bc.is_password?(check_password)
113
118
  else
114
119
  encoded = algorithm.encode(check_password)
115
120
  authed = Algorithm.secure_compare(encoded, digest)
@@ -66,9 +66,13 @@ module HTAuth
66
66
  # The file is not written to disk until #save! is called.
67
67
  #
68
68
  # username - the username of the entry
69
- # password - the username of the entry
69
+ # password - the password of the entry
70
70
  # algorithm - the algorithm to use (default: "md5"). Valid options are:
71
- # "md5", "sha1", "plaintext", or "crypt"
71
+ # "md5", "bcrypt", "sha1", "plaintext", or "crypt"
72
+ # algorithm_args - key-value pairs of arguments that are passed to the
73
+ # algorithm, currently this is only used to pass the cost
74
+ # to the bcrypt algorithm
75
+ #
72
76
  #
73
77
  # Examples
74
78
  #
@@ -79,20 +83,23 @@ module HTAuth
79
83
  # passwd_file.save!
80
84
  #
81
85
  # Returns nothing.
82
- def add_or_update(username, password, algorithm = Algorithm::DEFAULT)
86
+ def add_or_update(username, password, algorithm = Algorithm::DEFAULT, algorithm_args = {})
83
87
  if has_entry?(username) then
84
- update(username, password, algorithm)
88
+ update(username, password, algorithm, algorithm_args)
85
89
  else
86
- add(username, password, algorithm)
90
+ add(username, password, algorithm, algorithm_args)
87
91
  end
88
92
  end
89
93
 
90
94
  # Public: Add a new record to the file.
91
95
  #
92
96
  # username - the username of the entry
93
- # password - the username of the entry
97
+ # password - the password of the entry
94
98
  # algorithm - the algorithm to use (default: "md5"). Valid options are:
95
- # "md5", "sha1", "plaintext", or "crypt"
99
+ # "md5", "bcrypt", "sha1", "plaintext", or "crypt"
100
+ # algorithm_args - key-value pairs of arguments that are passed to the
101
+ # algorithm, currently this is only used to pass the cost
102
+ # to the bcrypt algorithm
96
103
  #
97
104
  # Examples
98
105
  #
@@ -102,11 +109,14 @@ module HTAuth
102
109
  # passwd_file.add("newuser", "password", "sha1")
103
110
  # passwd_file.save!
104
111
  #
112
+ # passwd_file.add("newuser", "password", "bcrypt", { cost: 12 })
113
+ # passwd_file.save!
114
+ #
105
115
  # Returns nothing.
106
116
  # Raises PasswdFileError if the give username already exists.
107
- def add(username, password, algorithm = Algorithm::DEFAULT)
117
+ def add(username, password, algorithm = Algorithm::DEFAULT, algorithm_args = {})
108
118
  raise PasswdFileError, "Unable to add already existing user #{username}" if has_entry?(username)
109
- new_entry = PasswdEntry.new(username, password, algorithm)
119
+ new_entry = PasswdEntry.new(username, password, algorithm, algorithm_args)
110
120
  new_index = @lines.size
111
121
  @lines << new_entry.to_s
112
122
  @entries[new_entry.key] = { 'entry' => new_entry, 'line_index' => new_index }
@@ -121,9 +131,12 @@ module HTAuth
121
131
  # setting the `algorithm` parameter.
122
132
  #
123
133
  # username - the username of the entry
124
- # password - the username of the entry
134
+ # password - the password of the entry
125
135
  # algorithm - the algorithm to use (default: "existing"). Valid options are:
126
- # "existing", "md5", "sha1", "plaintext", or "crypt"
136
+ # "existing", "md5", "bcrypt", "sha1", "plaintext", or "crypt"
137
+ # algorithm_args - key-value pairs of arguments that are passed to the
138
+ # algorithm, currently this is only used to pass the cost
139
+ # to the bcrypt algorithm
127
140
  #
128
141
  # Examples
129
142
  #
@@ -133,12 +146,16 @@ module HTAuth
133
146
  # passwd_file.update("newuser", "password", "sha1")
134
147
  # passwd_file.save!
135
148
  #
149
+ # passwd_file.update("newuser", "password", "bcrypt", { cost: 12 })
150
+ # passwd_file.save!
151
+ #
136
152
  # Returns nothing.
137
153
  # Raises PasswdFileError if the give username does not exist.
138
- def update(username, password, algorithm = Algorithm::EXISTING)
154
+ def update(username, password, algorithm = Algorithm::EXISTING, algorithm_args = {})
139
155
  raise PasswdFileError, "Unable to update non-existent user #{username}" unless has_entry?(username)
140
156
  ir = internal_record(username)
141
157
  ir['entry'].algorithm = algorithm
158
+ ir['entry'].algorithm_args = algorithm_args.dup
142
159
  ir['entry'].password = password
143
160
  @lines[ir['line_index']] = ir['entry'].to_s
144
161
  dirty!
@@ -164,6 +181,23 @@ module HTAuth
164
181
  return ir['entry'].dup
165
182
  end
166
183
 
184
+ # Public: authenticates the password of a given username
185
+ #
186
+ # Check the password file for the given user, and check the input password
187
+ # against the existing one.
188
+ #
189
+ # Examples
190
+ #
191
+ # authenticated = password_file.authenticated?("alice", "a secret")
192
+ #
193
+ # Returns true or false if the user exists
194
+ # Raises PasswordFileErrorif the given username does not exist
195
+ def authenticated?(username, password)
196
+ raise PasswdFileError, "Unable to authenticate a non-existent user #{username}" unless has_entry?(username)
197
+ ir = internal_record(username)
198
+ return ir['entry'].authenticated?(password)
199
+ end
200
+
167
201
  # Internal: returns the class used for each entry
168
202
  #
169
203
  # Returns a Class
@@ -3,12 +3,19 @@ require 'htauth/algorithm'
3
3
  module HTAuth
4
4
  # Internal: the plaintext algorithm, which does absolutly nothing
5
5
  class Plaintext < Algorithm
6
- # ignore parameters
7
- def initialize(params = {})
6
+
7
+ ENTRY_REGEX = /\A[^$:]*\Z/
8
+
9
+ def self.entry_matches?(entry)
10
+ ENTRY_REGEX.match?(entry)
8
11
  end
9
12
 
10
- def prefix
11
- ""
13
+ def self.handles?(password_entry)
14
+ false
15
+ end
16
+
17
+ # ignore parameters
18
+ def initialize(params = {})
12
19
  end
13
20
 
14
21
  def encode(password)
@@ -8,16 +8,19 @@ module HTAuth
8
8
  #
9
9
  class Sha1 < Algorithm
10
10
 
11
- # ignore the params
12
- def initialize(params = {})
11
+ PREFIX = '{SHA}'.freeze
12
+ ENTRY_REGEX = %r[\A#{Regexp.escape(PREFIX)}[A-Za-z0-9+\/=]{28}\z].freeze
13
+
14
+ def self.handles?(password_entry)
15
+ ENTRY_REGEX.match?(password_entry)
13
16
  end
14
17
 
15
- def prefix
16
- "{SHA}"
18
+ # ignore the params
19
+ def initialize(params = {})
17
20
  end
18
21
 
19
22
  def encode(password)
20
- "#{prefix}#{Base64.encode64(::Digest::SHA1.digest(password)).strip}"
23
+ "#{PREFIX}#{Base64.encode64(::Digest::SHA1.digest(password)).strip}"
21
24
  end
22
25
  end
23
26
  end
@@ -1,4 +1,4 @@
1
1
  module HTAuth
2
2
  # Public: The version of the htauth library
3
- VERSION = "2.0.0"
3
+ VERSION = "2.1.0"
4
4
  end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+ require 'htauth/algorithm'
3
+
4
+ describe HTAuth::Algorithm do
5
+ it "raises an error if it encouners an unknown algorithm" do
6
+ _ { HTAuth::Algorithm.algorithm_from_name("unknown") }.must_raise(::HTAuth::InvalidAlgorithmError)
7
+ end
8
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'htauth/bcrypt'
3
+
4
+ describe HTAuth::Bcrypt do
5
+ it "encrypts the same way that apache does by default" do
6
+ apache_hash = '$2y$05$X7XeXxp0uAO92AGG2P4/fu0mj7MrRDQnlBTkwZLd9rKiH2OUBb9/K'
7
+ reparsed = ::BCrypt::Password.new(apache_hash)
8
+ cost = reparsed.cost
9
+
10
+ _(cost).must_equal HTAuth::Bcrypt::DEFAULT_APACHE_COST
11
+ _(reparsed.is_password?("a secret")).must_equal true
12
+
13
+ bcrypt = HTAuth::Bcrypt.new(:cost => cost)
14
+ local_hash = bcrypt.encode("a secret")
15
+
16
+ _(local_hash.is_password?("a secret")).must_equal true
17
+ _(local_hash.cost).must_equal cost
18
+ end
19
+
20
+ it "encrypts the same way that apache does with different cost" do
21
+ apache_hash = '$2y$12$O3mBah33UilOkwXrS0kXuOPFBKLBCIp7V.AVvEZQcbnAM5SJLQnfq'
22
+ reparsed = ::BCrypt::Password.new(apache_hash)
23
+ cost = reparsed.cost
24
+
25
+ _(reparsed.is_password?("a secret")).must_equal true
26
+
27
+ bcrypt = HTAuth::Bcrypt.new(:cost => cost)
28
+ local_hash = bcrypt.encode("a secret")
29
+
30
+ _(local_hash.is_password?("a secret")).must_equal true
31
+ _(local_hash.cost).must_equal cost
32
+ end
33
+ end
@@ -41,8 +41,8 @@ describe HTAuth::CLI::Digest do
41
41
  begin
42
42
  @rdigest.run([ "-h" ])
43
43
  rescue SystemExit => se
44
- se.status.must_equal 1
45
- @stdout.string.must_match( /passwordfile realm username/m )
44
+ _(se.status).must_equal 1
45
+ _(@stdout.string).must_match( /passwordfile realm username/m )
46
46
  end
47
47
  end
48
48
 
@@ -50,8 +50,8 @@ describe HTAuth::CLI::Digest do
50
50
  begin
51
51
  @rdigest.run([ "--version" ])
52
52
  rescue SystemExit => se
53
- se.status.must_equal 1
54
- @stdout.string.must_match( /version #{HTAuth::VERSION}/ )
53
+ _(se.status).must_equal 1
54
+ _(@stdout.string).must_match( /version #{HTAuth::VERSION}/ )
55
55
  end
56
56
  end
57
57
 
@@ -62,8 +62,8 @@ describe HTAuth::CLI::Digest do
62
62
  @stdin.rewind
63
63
  @rdigest.run([ "-c", @new_file, "htauth", "bob" ])
64
64
  rescue SystemExit => se
65
- se.status.must_equal 0
66
- IO.read(@new_file).must_equal IO.readlines(DIGEST_ORIGINAL_TEST_FILE).first
65
+ _(se.status).must_equal 0
66
+ _(IO.read(@new_file)).must_equal IO.readlines(DIGEST_ORIGINAL_TEST_FILE).first
67
67
  end
68
68
  end
69
69
 
@@ -74,8 +74,8 @@ describe HTAuth::CLI::Digest do
74
74
  @stdin.rewind
75
75
  @rdigest.run([ "-c", @tf.path, "htauth", "bob"])
76
76
  rescue SystemExit => se
77
- se.status.must_equal 0
78
- IO.read(@tf.path).must_equal IO.read(DIGEST_DELETE_TEST_FILE)
77
+ _(se.status).must_equal 0
78
+ _(IO.read(@tf.path)).must_equal IO.read(DIGEST_DELETE_TEST_FILE)
79
79
  end
80
80
  end
81
81
 
@@ -86,8 +86,8 @@ describe HTAuth::CLI::Digest do
86
86
  @stdin.rewind
87
87
  @rdigest.run([ @tf.path, "htauth-new", "charlie" ])
88
88
  rescue SystemExit => se
89
- se.status.must_equal 0
90
- IO.read(@tf.path).must_equal IO.read(DIGEST_ADD_TEST_FILE)
89
+ _(se.status).must_equal 0
90
+ _(IO.read(@tf.path)).must_equal IO.read(DIGEST_ADD_TEST_FILE)
91
91
  end
92
92
  end
93
93
 
@@ -98,9 +98,9 @@ describe HTAuth::CLI::Digest do
98
98
  @stdin.rewind
99
99
  @rdigest.run([ @tf.path, "htauth", "alice" ])
100
100
  rescue SystemExit => se
101
- @stderr.string.must_equal ""
102
- se.status.must_equal 0
103
- IO.read(@tf.path).must_equal IO.read(DIGEST_UPDATE_TEST_FILE)
101
+ _(@stderr.string).must_equal ""
102
+ _(se.status).must_equal 0
103
+ _(IO.read(@tf.path)).must_equal IO.read(DIGEST_UPDATE_TEST_FILE)
104
104
  end
105
105
  end
106
106
 
@@ -108,9 +108,9 @@ describe HTAuth::CLI::Digest do
108
108
  begin
109
109
  @rdigest.run([ "-d", @tf.path, "htauth", "alice" ])
110
110
  rescue SystemExit => se
111
- @stderr.string.must_equal ""
112
- se.status.must_equal 0
113
- IO.read(@tf.path).must_equal IO.read(DIGEST_DELETE_TEST_FILE)
111
+ _(@stderr.string).must_equal ""
112
+ _(se.status).must_equal 0
113
+ _(IO.read(@tf.path)).must_equal IO.read(DIGEST_DELETE_TEST_FILE)
114
114
  end
115
115
  end
116
116
 
@@ -121,8 +121,8 @@ describe HTAuth::CLI::Digest do
121
121
  @stdin.rewind
122
122
  @rdigest.run([ "-c", "/etc/you-cannot-create-me", "htauth", "alice"])
123
123
  rescue SystemExit => se
124
- @stderr.string.must_match( %r{Could not open password file /etc/you-cannot-create-me}m )
125
- se.status.must_equal 1
124
+ _(@stderr.string).must_match( %r{Could not open password file /etc/you-cannot-create-me}m )
125
+ _(se.status).must_equal 1
126
126
  end
127
127
  end
128
128
 
@@ -133,8 +133,8 @@ describe HTAuth::CLI::Digest do
133
133
  @stdin.rewind
134
134
  @rdigest.run([ @tf.path, "htauth", "alice"])
135
135
  rescue SystemExit => se
136
- @stderr.string.must_match( /They don't match, sorry./m )
137
- se.status.must_equal 1
136
+ _(@stderr.string).must_match( /They don't match, sorry./m )
137
+ _(se.status).must_equal 1
138
138
  end
139
139
  end
140
140
 
@@ -142,9 +142,8 @@ describe HTAuth::CLI::Digest do
142
142
  begin
143
143
  @rdigest.run(["--blah"])
144
144
  rescue SystemExit => se
145
- @stderr.string.must_match( /ERROR:/m )
146
- se.status.must_equal 1
145
+ _(@stderr.string).must_match( /ERROR:/m )
146
+ _(se.status).must_equal 1
147
147
  end
148
148
  end
149
-
150
149
  end