htauth 2.2.0 → 3.0.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.
- checksums.yaml +4 -4
- data/HISTORY.md +21 -1
- data/Manifest.txt +5 -27
- data/README.md +51 -31
- data/exe/htdigest-ruby +14 -0
- data/exe/htpasswd-ruby +14 -0
- data/htauth.gemspec +33 -0
- data/lib/htauth/algorithm.rb +42 -29
- data/lib/htauth/argon2.rb +86 -0
- data/lib/htauth/bcrypt.rb +17 -11
- data/lib/htauth/cli/digest.rb +42 -49
- data/lib/htauth/cli/passwd.rb +127 -114
- data/lib/htauth/cli.rb +5 -4
- data/lib/htauth/console.rb +9 -6
- data/lib/htauth/crypt.rb +11 -9
- data/lib/htauth/descendant_tracker.rb +11 -9
- data/lib/htauth/digest_entry.rb +22 -19
- data/lib/htauth/digest_file.rb +25 -18
- data/lib/htauth/entry.rb +3 -1
- data/lib/htauth/error.rb +6 -5
- data/lib/htauth/file.rb +35 -39
- data/lib/htauth/md5.rb +35 -34
- data/lib/htauth/passwd_entry.rb +29 -38
- data/lib/htauth/passwd_file.rb +32 -27
- data/lib/htauth/plaintext.rb +7 -5
- data/lib/htauth/sha1.rb +9 -7
- data/lib/htauth/version.rb +3 -1
- data/lib/htauth.rb +29 -28
- metadata +24 -113
- data/Rakefile +0 -27
- data/bin/htdigest-ruby +0 -12
- data/bin/htpasswd-ruby +0 -12
- data/spec/algorithm_spec.rb +0 -8
- data/spec/bcrypt_spec.rb +0 -33
- data/spec/cli/digest_spec.rb +0 -149
- data/spec/cli/passwd_spec.rb +0 -330
- data/spec/crypt_spec.rb +0 -12
- data/spec/digest_entry_spec.rb +0 -60
- data/spec/digest_file_spec.rb +0 -65
- data/spec/md5_spec.rb +0 -13
- data/spec/passwd_entry_spec.rb +0 -159
- data/spec/passwd_file_spec.rb +0 -84
- data/spec/plaintext_spec.rb +0 -11
- data/spec/sha1_spec.rb +0 -11
- data/spec/spec_helper.rb +0 -28
- data/spec/test.add.digest +0 -3
- data/spec/test.add.passwd +0 -3
- data/spec/test.delete.digest +0 -1
- data/spec/test.delete.passwd +0 -1
- data/spec/test.original.digest +0 -2
- data/spec/test.original.passwd +0 -2
- data/spec/test.update.digest +0 -2
- data/spec/test.update.passwd +0 -2
- data/tasks/default.rake +0 -242
- data/tasks/this.rb +0 -208
- /data/{LICENSE → LICENSE.txt} +0 -0
data/lib/htauth/digest_entry.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "htauth/error"
|
|
4
|
+
require "htauth/entry"
|
|
5
|
+
require "htauth/algorithm"
|
|
6
|
+
require "digest/md5"
|
|
4
7
|
|
|
5
8
|
module HTAuth
|
|
6
9
|
# Internal: Object version of a single record from an htdigest file
|
|
7
10
|
class DigestEntry
|
|
8
|
-
|
|
9
11
|
# Internal: The user of this entry
|
|
10
12
|
attr_accessor :user
|
|
11
13
|
# Internal: The realm of this entry
|
|
@@ -20,10 +22,10 @@ module HTAuth
|
|
|
20
22
|
#
|
|
21
23
|
# Returns an instance of DigestEntry
|
|
22
24
|
def from_line(line)
|
|
23
|
-
parts =
|
|
25
|
+
parts = entry!(line)
|
|
24
26
|
d = DigestEntry.new(parts[0], parts[1])
|
|
25
27
|
d.digest = parts[2]
|
|
26
|
-
|
|
28
|
+
d
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
# Internal: test if the given line is valid for this Entry class
|
|
@@ -36,30 +38,31 @@ module HTAuth
|
|
|
36
38
|
#
|
|
37
39
|
# Returns the individual parts of the line
|
|
38
40
|
# Raises InvalidDigestEntry if it is not a a valid entry
|
|
39
|
-
def
|
|
40
|
-
raise InvalidDigestEntry, "line commented out" if line
|
|
41
|
+
def entry!(line)
|
|
42
|
+
raise InvalidDigestEntry, "line commented out" if line.start_with?("#")
|
|
43
|
+
|
|
41
44
|
parts = line.strip.split(":")
|
|
42
45
|
raise InvalidDigestEntry, "line must be of the format username:realm:md5checksum" if parts.size != 3
|
|
43
|
-
raise InvalidDigestEntry, "md5 checksum is not 32 characters long" if parts.last.size
|
|
44
|
-
raise InvalidDigestEntry, "md5 checksum has invalid characters"
|
|
45
|
-
|
|
46
|
+
raise InvalidDigestEntry, "md5 checksum is not 32 characters long" if parts.last.size != 32
|
|
47
|
+
raise InvalidDigestEntry, "md5 checksum has invalid characters" unless /\A[[:xdigit:]]{32}\Z/.match?(parts.last)
|
|
48
|
+
|
|
49
|
+
parts
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
# Internal: Returns whether or not the line is a valid entry
|
|
49
53
|
#
|
|
50
54
|
# Returns true or false
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return false
|
|
57
|
-
end
|
|
55
|
+
def entry?(line)
|
|
56
|
+
entry!(line)
|
|
57
|
+
true
|
|
58
|
+
rescue InvalidDigestEntry
|
|
59
|
+
false
|
|
58
60
|
end
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
# Internal: Create a new Entry with the given user, realm and password
|
|
62
64
|
def initialize(user, realm, password = "")
|
|
65
|
+
super()
|
|
63
66
|
@user = user
|
|
64
67
|
@realm = realm
|
|
65
68
|
@digest = calc_digest(password)
|
|
@@ -78,7 +81,7 @@ module HTAuth
|
|
|
78
81
|
# Public: Check if the given password is the password of this entry.
|
|
79
82
|
def authenticated?(check_password)
|
|
80
83
|
check = calc_digest(check_password)
|
|
81
|
-
|
|
84
|
+
Algorithm.secure_compare(check, digest)
|
|
82
85
|
end
|
|
83
86
|
|
|
84
87
|
# Internal: Returns the key of this entry
|
data/lib/htauth/digest_file.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
4
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "htauth/error"
|
|
5
|
+
require "htauth/file"
|
|
6
|
+
require "htauth/digest_entry"
|
|
5
7
|
|
|
6
8
|
module HTAuth
|
|
7
9
|
# Public: An API for managing an 'htdigest' file
|
|
@@ -15,7 +17,6 @@ module HTAuth
|
|
|
15
17
|
# end
|
|
16
18
|
#
|
|
17
19
|
class DigestFile < HTAuth::File
|
|
18
|
-
|
|
19
20
|
# Private: The class implementing a single entry in the DigestFile
|
|
20
21
|
ENTRY_KLASS = HTAuth::DigestEntry
|
|
21
22
|
|
|
@@ -32,7 +33,7 @@ module HTAuth
|
|
|
32
33
|
# Returns true or false if the username/realm combination is found.
|
|
33
34
|
def has_entry?(username, realm)
|
|
34
35
|
test_entry = DigestEntry.new(username, realm)
|
|
35
|
-
@entries.
|
|
36
|
+
@entries.key?(test_entry.key)
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
# Public: remove the given username / realm from the file.
|
|
@@ -48,10 +49,10 @@ module HTAuth
|
|
|
48
49
|
#
|
|
49
50
|
# Returns nothing
|
|
50
51
|
def delete(username, realm)
|
|
51
|
-
if has_entry?(username, realm)
|
|
52
|
+
if has_entry?(username, realm)
|
|
52
53
|
ir = internal_record(username, realm)
|
|
53
|
-
line_index = ir[
|
|
54
|
-
@entries.delete(ir[
|
|
54
|
+
line_index = ir["line_index"]
|
|
55
|
+
@entries.delete(ir["entry"].key)
|
|
55
56
|
@lines[line_index] = nil
|
|
56
57
|
dirty!
|
|
57
58
|
end
|
|
@@ -76,7 +77,7 @@ module HTAuth
|
|
|
76
77
|
#
|
|
77
78
|
# Returns nothing.
|
|
78
79
|
def add_or_update(username, realm, password)
|
|
79
|
-
if has_entry?(username, realm)
|
|
80
|
+
if has_entry?(username, realm)
|
|
80
81
|
update(username, realm, password)
|
|
81
82
|
else
|
|
82
83
|
add(username, realm, password)
|
|
@@ -97,14 +98,16 @@ module HTAuth
|
|
|
97
98
|
# Returns nothing.
|
|
98
99
|
# Raises DigestFileError if the give username / realm already exists.
|
|
99
100
|
def add(username, realm, password)
|
|
100
|
-
raise DigestFileError, "Unable to add already existing user #{username} in realm #{realm}" if has_entry?(
|
|
101
|
+
raise DigestFileError, "Unable to add already existing user #{username} in realm #{realm}" if has_entry?(
|
|
102
|
+
username, realm
|
|
103
|
+
)
|
|
101
104
|
|
|
102
105
|
new_entry = DigestEntry.new(username, realm, password)
|
|
103
106
|
new_index = @lines.size
|
|
104
107
|
@lines << new_entry.to_s
|
|
105
|
-
@entries[new_entry.key] = {
|
|
108
|
+
@entries[new_entry.key] = { "entry" => new_entry, "line_index" => new_index }
|
|
106
109
|
dirty!
|
|
107
|
-
|
|
110
|
+
nil
|
|
108
111
|
end
|
|
109
112
|
|
|
110
113
|
# Public: Updates an existing username / relam entry with a new password
|
|
@@ -121,12 +124,15 @@ module HTAuth
|
|
|
121
124
|
# Returns nothing
|
|
122
125
|
# Raises DigestfileError if the username / realm is not found in the file
|
|
123
126
|
def update(username, realm, password)
|
|
124
|
-
raise DigestFileError, "Unable to update non-existent user #{username} in realm #{realm}" unless has_entry?(
|
|
127
|
+
raise DigestFileError, "Unable to update non-existent user #{username} in realm #{realm}" unless has_entry?(
|
|
128
|
+
username, realm
|
|
129
|
+
)
|
|
130
|
+
|
|
125
131
|
ir = internal_record(username, realm)
|
|
126
|
-
ir[
|
|
127
|
-
@lines[ir[
|
|
132
|
+
ir["entry"].password = password
|
|
133
|
+
@lines[ir["line_index"]] = ir["entry"].to_s
|
|
128
134
|
dirty!
|
|
129
|
-
|
|
135
|
+
nil
|
|
130
136
|
end
|
|
131
137
|
|
|
132
138
|
# Public: Returns the given DigestEntry from the file.
|
|
@@ -145,8 +151,9 @@ module HTAuth
|
|
|
145
151
|
# Returns nil if the entry is not found
|
|
146
152
|
def fetch(username, realm)
|
|
147
153
|
return nil unless has_entry?(username, realm)
|
|
154
|
+
|
|
148
155
|
ir = internal_record(username, realm)
|
|
149
|
-
|
|
156
|
+
ir["entry"].dup
|
|
150
157
|
end
|
|
151
158
|
|
|
152
159
|
# Internal: returns the class used for each entry
|
data/lib/htauth/entry.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module HTAuth
|
|
2
4
|
# Internal: base class from which all entries are derived
|
|
3
5
|
class Entry
|
|
4
6
|
# Internal: return a new instance of this entry
|
|
5
7
|
def dup
|
|
6
|
-
self.class.from_line(
|
|
8
|
+
self.class.from_line(to_s)
|
|
7
9
|
end
|
|
8
10
|
end
|
|
9
11
|
end
|
data/lib/htauth/error.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
2
4
|
# Copyrigth (c) 2008 Jeremy Hinegardner
|
|
3
5
|
# All rights reserved. See LICENSE and/or COPYING for details
|
|
4
6
|
#++
|
|
@@ -7,12 +9,11 @@ module HTAuth
|
|
|
7
9
|
class Error < StandardError; end
|
|
8
10
|
|
|
9
11
|
class ConsoleError < Error; end
|
|
10
|
-
class DigestFileError < Error
|
|
12
|
+
class DigestFileError < Error; end
|
|
11
13
|
class FileAccessError < Error; end
|
|
12
14
|
class InvalidDigestEntry < Error; end
|
|
13
|
-
class InvalidPasswdEntry < Error
|
|
15
|
+
class InvalidPasswdEntry < Error; end
|
|
14
16
|
class PasswordError < Error; end
|
|
15
|
-
class PasswdFileError < Error
|
|
17
|
+
class PasswdFileError < Error; end
|
|
16
18
|
class TempFileError < Error; end
|
|
17
19
|
end
|
|
18
|
-
|
data/lib/htauth/file.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "htauth/error"
|
|
3
5
|
|
|
4
6
|
module HTAuth
|
|
5
7
|
# Internal: A base class for DigestFile and PasswordFile to inherit from.
|
|
@@ -7,22 +9,20 @@ module HTAuth
|
|
|
7
9
|
# This class should not be instantiated directly. You must use DigestFile or
|
|
8
10
|
# PasswordFile.
|
|
9
11
|
class File
|
|
10
|
-
|
|
11
12
|
# Public: The mode to pass to #open for updating a file
|
|
12
|
-
ALTER = "alter"
|
|
13
|
+
ALTER = "alter"
|
|
13
14
|
|
|
14
15
|
# Public: The mode to pass to #open for creating a new file
|
|
15
|
-
CREATE = "create"
|
|
16
|
+
CREATE = "create"
|
|
16
17
|
|
|
17
18
|
# Public: A special 'filename' that may be passed to #open for 'saving' to $stdout
|
|
18
|
-
STDOUT_FLAG = "-"
|
|
19
|
+
STDOUT_FLAG = "-"
|
|
19
20
|
|
|
20
|
-
attr_reader :filename
|
|
21
|
-
attr_reader :file
|
|
21
|
+
attr_reader :filename, :file
|
|
22
22
|
|
|
23
23
|
class << self
|
|
24
24
|
# Public: The method to use to open a DigestFile or PasswordFile.
|
|
25
|
-
# Altering a non-existent file is an error. Creating an existing file
|
|
25
|
+
# Altering a non-existent file is an error. Creating an existing file
|
|
26
26
|
# results in a truncation and overwrite of the existing file.
|
|
27
27
|
#
|
|
28
28
|
# filename - The name of the file to open
|
|
@@ -50,23 +50,23 @@ module HTAuth
|
|
|
50
50
|
# Returns the DigestFile or PasswordFile as appropriate.
|
|
51
51
|
# Raises FileAccessError if an invalid mode is used.
|
|
52
52
|
# Raises FileAccessError if ALTERing a non-existent file.
|
|
53
|
-
def open(filename, mode = ALTER)
|
|
54
|
-
f =
|
|
53
|
+
def open(filename, mode = ALTER)
|
|
54
|
+
f = new(filename, mode)
|
|
55
55
|
if block_given?
|
|
56
56
|
begin
|
|
57
57
|
yield f
|
|
58
58
|
ensure
|
|
59
|
-
f.save! if f
|
|
59
|
+
f.save! if f&.dirty?
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
|
-
|
|
62
|
+
f
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# Public: Create a new DigestFile or PasswordFile.
|
|
67
67
|
# Generally you do not need to use this method. Use #open instead.
|
|
68
68
|
#
|
|
69
|
-
# Altering a non-existent file is an error. Creating an existing file
|
|
69
|
+
# Altering a non-existent file is an error. Creating an existing file
|
|
70
70
|
# results in a truncation and overwrite of the existing file.
|
|
71
71
|
#
|
|
72
72
|
# filename - The name of the file to open
|
|
@@ -87,17 +87,17 @@ module HTAuth
|
|
|
87
87
|
@mode = mode
|
|
88
88
|
@dirty = false
|
|
89
89
|
|
|
90
|
-
raise FileAccessError, "Invalid mode #{mode}" unless [
|
|
90
|
+
raise FileAccessError, "Invalid mode #{mode}" unless [ALTER, CREATE].include?(mode)
|
|
91
91
|
|
|
92
|
-
if (filename != STDOUT_FLAG)
|
|
93
|
-
raise FileAccessError, "Could not open passwd file #{filename} for reading."
|
|
92
|
+
if (filename != STDOUT_FLAG) && (mode == ALTER) && !::File.exist?(filename)
|
|
93
|
+
raise FileAccessError, "Could not open passwd file #{filename} for reading."
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
begin
|
|
97
97
|
@entries = {}
|
|
98
98
|
@lines = []
|
|
99
|
-
load_entries if (@mode == ALTER)
|
|
100
|
-
rescue => e
|
|
99
|
+
load_entries if (@mode == ALTER) && (filename != STDOUT_FLAG)
|
|
100
|
+
rescue StandardError => e
|
|
101
101
|
raise FileAccessError, e.message
|
|
102
102
|
end
|
|
103
103
|
end
|
|
@@ -127,19 +127,15 @@ module HTAuth
|
|
|
127
127
|
# Returns nothing
|
|
128
128
|
# Raises FileAccessError if there was a problem writing the file
|
|
129
129
|
def save!
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
::File.open(@filename,"w") do |f|
|
|
136
|
-
f.write(contents)
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
@dirty = false
|
|
140
|
-
rescue => e
|
|
141
|
-
raise FileAccessError, "Error saving file #{@filename} : #{e.message}"
|
|
130
|
+
case filename
|
|
131
|
+
when STDOUT_FLAG
|
|
132
|
+
$stdout.write(contents)
|
|
133
|
+
else
|
|
134
|
+
::File.write(@filename, contents)
|
|
142
135
|
end
|
|
136
|
+
@dirty = false
|
|
137
|
+
rescue StandardError => e
|
|
138
|
+
raise FileAccessError, "Error saving file #{@filename} : #{e.message}"
|
|
143
139
|
end
|
|
144
140
|
|
|
145
141
|
# Internal: Return the String of the entire file contents
|
|
@@ -147,7 +143,7 @@ module HTAuth
|
|
|
147
143
|
# Returns String
|
|
148
144
|
def contents
|
|
149
145
|
c = StringIO.new
|
|
150
|
-
@lines.each do |l|
|
|
146
|
+
@lines.each do |l|
|
|
151
147
|
c.puts l if l
|
|
152
148
|
end
|
|
153
149
|
c.string
|
|
@@ -160,13 +156,13 @@ module HTAuth
|
|
|
160
156
|
# out in the same order it was read with the appropriate entries updated or
|
|
161
157
|
# deleted.
|
|
162
158
|
def load_entries
|
|
163
|
-
@lines
|
|
164
|
-
@lines.each_with_index do |line,idx|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
159
|
+
@lines = IO.readlines(@filename)
|
|
160
|
+
@lines.each_with_index do |line, idx|
|
|
161
|
+
next unless entry_klass.entry?(line)
|
|
162
|
+
|
|
163
|
+
entry = entry_klass.from_line(line)
|
|
164
|
+
v = { "entry" => entry, "line_index" => idx }
|
|
165
|
+
@entries[entry.key] = v
|
|
170
166
|
end
|
|
171
167
|
nil
|
|
172
168
|
end
|
data/lib/htauth/md5.rb
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "htauth/algorithm"
|
|
4
|
+
require "digest/md5"
|
|
3
5
|
|
|
4
6
|
module HTAuth
|
|
5
7
|
# Internal: an implementation of the MD5 based encoding algorithm
|
|
6
8
|
# as used in the apache htpasswd -m option
|
|
7
9
|
class Md5 < Algorithm
|
|
8
|
-
|
|
9
10
|
DIGEST_LENGTH = 16
|
|
10
11
|
PAD_LENGTH = 6
|
|
11
|
-
PREFIX = "$apr1$"
|
|
12
|
-
SALT_CHARS_STR = SALT_CHARS.join
|
|
13
|
-
ENTRY_REGEX =
|
|
12
|
+
PREFIX = "$apr1$"
|
|
13
|
+
SALT_CHARS_STR = SALT_CHARS.join
|
|
14
|
+
ENTRY_REGEX = /
|
|
14
15
|
\A
|
|
15
16
|
#{Regexp.escape(PREFIX)}
|
|
16
17
|
[#{SALT_CHARS_STR}]{#{SALT_LENGTH}}
|
|
17
|
-
#{Regexp.escape(
|
|
18
|
+
#{Regexp.escape('$')}
|
|
18
19
|
[#{SALT_CHARS_STR}]{#{DIGEST_LENGTH + PAD_LENGTH}}
|
|
19
20
|
\z
|
|
20
|
-
|
|
21
|
+
/x
|
|
21
22
|
|
|
22
23
|
def self.handles?(password_entry)
|
|
23
24
|
ENTRY_REGEX.match?(password_entry)
|
|
@@ -25,15 +26,16 @@ module HTAuth
|
|
|
25
26
|
|
|
26
27
|
def self.extract_salt_from_existing_password_field(existing)
|
|
27
28
|
p = existing.split("$")
|
|
28
|
-
|
|
29
|
+
p[2]
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def initialize(params = {})
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
super()
|
|
34
|
+
@salt = if (existing = params["existing"] || params[:existing])
|
|
35
|
+
self.class.extract_salt_from_existing_password_field(existing)
|
|
36
|
+
else
|
|
37
|
+
params[:salt] || params["salt"] || gen_salt
|
|
38
|
+
end
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
# this algorigthm pulled straight from apr_md5_encode() and converted to ruby syntax
|
|
@@ -46,8 +48,8 @@ module HTAuth
|
|
|
46
48
|
md5_t = ::Digest::MD5.digest("#{password}#{@salt}#{password}")
|
|
47
49
|
|
|
48
50
|
l = password.length
|
|
49
|
-
while l
|
|
50
|
-
slice_size =
|
|
51
|
+
while l.positive?
|
|
52
|
+
slice_size = [l, DIGEST_LENGTH].min
|
|
51
53
|
primary << md5_t[0, slice_size]
|
|
52
54
|
l -= DIGEST_LENGTH
|
|
53
55
|
end
|
|
@@ -59,7 +61,7 @@ module HTAuth
|
|
|
59
61
|
when 1
|
|
60
62
|
primary << 0.chr
|
|
61
63
|
when 0
|
|
62
|
-
primary << password[0,1]
|
|
64
|
+
primary << password[0, 1]
|
|
63
65
|
end
|
|
64
66
|
l >>= 1
|
|
65
67
|
end
|
|
@@ -71,33 +73,32 @@ module HTAuth
|
|
|
71
73
|
# apr_md5_encode has this comment about a 60Mhz Pentium above this loop.
|
|
72
74
|
1000.times do |x|
|
|
73
75
|
ctx = ::Digest::MD5.new
|
|
74
|
-
ctx << ((
|
|
75
|
-
(ctx << @salt) unless (
|
|
76
|
-
(ctx << password) unless (
|
|
77
|
-
ctx << ((
|
|
76
|
+
ctx << (x.allbits?(1) ? password : pd[0, DIGEST_LENGTH])
|
|
77
|
+
(ctx << @salt) unless (x % 3).zero?
|
|
78
|
+
(ctx << password) unless (x % 7).zero?
|
|
79
|
+
ctx << (x.nobits?(1) ? password : pd[0, DIGEST_LENGTH])
|
|
78
80
|
pd = ctx.digest
|
|
79
81
|
end
|
|
80
82
|
|
|
81
|
-
|
|
82
83
|
pd = pd.bytes.to_a
|
|
83
84
|
|
|
84
|
-
l = (pd[
|
|
85
|
-
encoded_password <<
|
|
85
|
+
l = (pd[0] << 16) | (pd[6] << 8) | pd[12]
|
|
86
|
+
encoded_password << to64(l, 4)
|
|
86
87
|
|
|
87
|
-
l = (pd[
|
|
88
|
-
encoded_password <<
|
|
88
|
+
l = (pd[1] << 16) | (pd[7] << 8) | pd[13]
|
|
89
|
+
encoded_password << to64(l, 4)
|
|
89
90
|
|
|
90
|
-
l = (pd[
|
|
91
|
-
encoded_password <<
|
|
91
|
+
l = (pd[2] << 16) | (pd[8] << 8) | pd[14]
|
|
92
|
+
encoded_password << to64(l, 4)
|
|
92
93
|
|
|
93
|
-
l = (pd[
|
|
94
|
-
encoded_password <<
|
|
94
|
+
l = (pd[3] << 16) | (pd[9] << 8) | pd[15]
|
|
95
|
+
encoded_password << to64(l, 4)
|
|
95
96
|
|
|
96
|
-
l = (pd[
|
|
97
|
-
encoded_password <<
|
|
98
|
-
encoded_password <<
|
|
97
|
+
l = (pd[4] << 16) | (pd[10] << 8) | pd[5]
|
|
98
|
+
encoded_password << to64(l, 4)
|
|
99
|
+
encoded_password << to64(pd[11], 2)
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
encoded_password
|
|
101
102
|
end
|
|
102
103
|
end
|
|
103
104
|
end
|
data/lib/htauth/passwd_entry.rb
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "htauth/error"
|
|
4
|
+
require "htauth/entry"
|
|
5
|
+
require "htauth/algorithm"
|
|
4
6
|
|
|
5
7
|
module HTAuth
|
|
6
8
|
# Internal: Object version of a single entry from a htpasswd file
|
|
7
9
|
class PasswdEntry < Entry
|
|
8
|
-
|
|
9
10
|
# Internal: the user of this entry
|
|
10
11
|
attr_accessor :user
|
|
11
12
|
# Internal: the password digest of this entry
|
|
@@ -22,11 +23,11 @@ module HTAuth
|
|
|
22
23
|
#
|
|
23
24
|
# Returns an instance of PasswdEntry
|
|
24
25
|
def from_line(line)
|
|
25
|
-
parts =
|
|
26
|
+
parts = entry!(line)
|
|
26
27
|
d = PasswdEntry.new(parts[0])
|
|
27
28
|
d.digest = parts[1]
|
|
28
29
|
d.algorithm = Algorithm.algorithm_from_field(parts[1])
|
|
29
|
-
|
|
30
|
+
d
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
# Internal: test if the given line is valid for this Entry class
|
|
@@ -39,37 +40,39 @@ module HTAuth
|
|
|
39
40
|
#
|
|
40
41
|
# Returns the individual parts of the line
|
|
41
42
|
# Raises InvalidPasswdEntry if it is not an valid entry
|
|
42
|
-
def
|
|
43
|
-
raise InvalidPasswdEntry, "line commented out" if line
|
|
43
|
+
def entry!(line)
|
|
44
|
+
raise InvalidPasswdEntry, "line commented out" if line.start_with?("#")
|
|
45
|
+
|
|
44
46
|
parts = line.strip.split(":")
|
|
45
47
|
raise InvalidPasswdEntry, "line must be of the format username:password" if parts.size != 2
|
|
46
|
-
|
|
48
|
+
|
|
49
|
+
parts
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
# Internal: Returns whether or not the line is a valid entry
|
|
50
53
|
#
|
|
51
54
|
# Returns true or false
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return false
|
|
58
|
-
end
|
|
55
|
+
def entry?(line)
|
|
56
|
+
entry!(line)
|
|
57
|
+
true
|
|
58
|
+
rescue InvalidPasswdEntry
|
|
59
|
+
false
|
|
59
60
|
end
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
# Internal: Create a new Entry with the given user, password, and algorithm
|
|
63
|
-
def initialize(user, password = nil, alg = Algorithm::DEFAULT, alg_params = {}
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
def initialize(user, password = nil, alg = Algorithm::DEFAULT, alg_params = {})
|
|
65
|
+
super()
|
|
66
|
+
@user = user
|
|
67
|
+
alg = Algorithm::DEFAULT if alg == Algorithm::EXISTING
|
|
66
68
|
@algorithm = Algorithm.algorithm_from_name(alg, alg_params)
|
|
67
69
|
@digest = calc_digest(password)
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
# Internal: set the algorithm for the entry
|
|
71
73
|
def algorithm=(alg)
|
|
72
|
-
return @algorithm if Algorithm::EXISTING
|
|
74
|
+
return @algorithm if alg == Algorithm::EXISTING
|
|
75
|
+
|
|
73
76
|
case alg
|
|
74
77
|
when String
|
|
75
78
|
@algorithm = Algorithm.algorithm_from_name(alg)
|
|
@@ -78,7 +81,7 @@ module HTAuth
|
|
|
78
81
|
else
|
|
79
82
|
raise InvalidAlgorithmError, "Unable to assign #{alg} to algorithm"
|
|
80
83
|
end
|
|
81
|
-
|
|
84
|
+
@algorithm
|
|
82
85
|
end
|
|
83
86
|
|
|
84
87
|
# Internal: set fields on the algorithm
|
|
@@ -93,38 +96,26 @@ module HTAuth
|
|
|
93
96
|
#
|
|
94
97
|
# If we have an array of algorithms, then we set it to CRYPT
|
|
95
98
|
def password=(new_password)
|
|
96
|
-
if algorithm.
|
|
97
|
-
@algorithm = Algorithm.algorithm_from_name(Algorithm::CRYPT)
|
|
98
|
-
end
|
|
99
|
+
@algorithm = Algorithm.algorithm_from_name(Algorithm::CRYPT) if algorithm.is_a?(HTAuth::Plaintext)
|
|
99
100
|
@digest = calc_digest(new_password)
|
|
100
101
|
end
|
|
101
102
|
|
|
102
103
|
# Internal: calculate the new digest of the given password
|
|
103
104
|
def calc_digest(password)
|
|
104
105
|
return nil unless password
|
|
106
|
+
|
|
105
107
|
algorithm.encode(password)
|
|
106
108
|
end
|
|
107
109
|
|
|
108
110
|
# Public: Check if the given password is the password of this entry
|
|
109
|
-
#
|
|
110
|
-
# tries all of the ones that it thinks it could be, and marks the algorithm if it matches
|
|
111
|
-
# when looking for a matche, we always compare all of them, no short
|
|
112
|
-
# circuiting
|
|
111
|
+
#
|
|
113
112
|
def authenticated?(check_password)
|
|
114
|
-
|
|
115
|
-
if algorithm.kind_of?(Bcrypt) then
|
|
116
|
-
bc = ::BCrypt::Password.new(digest)
|
|
117
|
-
authed = bc.is_password?(check_password)
|
|
118
|
-
else
|
|
119
|
-
encoded = algorithm.encode(check_password)
|
|
120
|
-
authed = Algorithm.secure_compare(encoded, digest)
|
|
121
|
-
end
|
|
122
|
-
return authed
|
|
113
|
+
algorithm.verify_password?(check_password, @digest)
|
|
123
114
|
end
|
|
124
115
|
|
|
125
116
|
# Internal: Returns the key of this entry
|
|
126
117
|
def key
|
|
127
|
-
|
|
118
|
+
user.to_s
|
|
128
119
|
end
|
|
129
120
|
|
|
130
121
|
# Internal: Returns the file line for this entry
|