htauth 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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