htauth 1.2.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 68e39a2811eb3436eb1de8ceac4bf0fd4ad458b9
4
- data.tar.gz: 3c432666f3fabec0d5b0ef6e3fc4720bfcf2dcb8
3
+ metadata.gz: 64406e1178b07885f40df5db23c5962c040dff9a
4
+ data.tar.gz: a4d90738e83e05cfc38e857426be2e95ed4e8700
5
5
  SHA512:
6
- metadata.gz: 3139d1a21b6e3ccd5a31fc6cb27d1f39ef1d898fc50e078b154f90659fbdc9fb8f119d56fb9c182e1af95422d9e308fb9632a4a9fc8f47781c1837a28d3bb8da
7
- data.tar.gz: 8bb24b28ce66741222787a1c965e27e5d6c3f5d43aae96699e6d95f1f671d59876ccb2f3476e5db4154dbb39e4dbe272649f1245432fc38962be0bf98cd5a179
6
+ metadata.gz: 680bdc7d37adccad0a9358fdf391440abce27e8adc65e50dfeb394d0fbd884f3c3d951c2688590e33ef841a4bfb699035b167c2b57a764d93858b0dce1eb878e
7
+ data.tar.gz: 5c39dd6133939c4ce2eb3845fcac8135f355fc141930ade586bfeca16cc43d5fe4df556891cdb44be7de26ff862fd2aa8dd4b5ff22adb3bb5d10ba90eb61771b
data/HISTORY.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## Version 2.0.0 - 2015-09-13
4
+
5
+ * Remove highline dependency - [#9](https://github.com/copiousfreetime/htauth/pull/9)
6
+ * Tomdoc the public interface - [#10](https://github.com/copiousfreetime/htauth/issues/10)
7
+ * Extract the commandline objects to their own module - [#2](https://github.com/copiousfreetime/htauth/issues/2)
8
+ * Use a secure comparison when comparing digests - [#11](https://github.com/copiousfreetime/htauth/issues/11)
9
+
3
10
  ## Version 1.2.0 2015-07-18
4
11
 
5
12
  * Clarify project license (its MIT) - [#7](https://github.com/copiousfreetime/htauth/issues/7)
@@ -8,28 +8,30 @@ bin/htdigest-ruby
8
8
  bin/htpasswd-ruby
9
9
  lib/htauth.rb
10
10
  lib/htauth/algorithm.rb
11
+ lib/htauth/cli.rb
12
+ lib/htauth/cli/digest.rb
13
+ lib/htauth/cli/passwd.rb
14
+ lib/htauth/console.rb
11
15
  lib/htauth/crypt.rb
12
- lib/htauth/digest.rb
13
16
  lib/htauth/digest_entry.rb
14
17
  lib/htauth/digest_file.rb
15
18
  lib/htauth/entry.rb
16
- lib/htauth/errors.rb
19
+ lib/htauth/error.rb
17
20
  lib/htauth/file.rb
18
21
  lib/htauth/md5.rb
19
- lib/htauth/passwd.rb
20
22
  lib/htauth/passwd_entry.rb
21
23
  lib/htauth/passwd_file.rb
22
24
  lib/htauth/plaintext.rb
23
25
  lib/htauth/sha1.rb
24
26
  lib/htauth/version.rb
27
+ spec/cli/digest_spec.rb
28
+ spec/cli/passwd_spec.rb
25
29
  spec/crypt_spec.rb
26
30
  spec/digest_entry_spec.rb
27
31
  spec/digest_file_spec.rb
28
- spec/digest_spec.rb
29
32
  spec/md5_spec.rb
30
33
  spec/passwd_entry_spec.rb
31
34
  spec/passwd_file_spec.rb
32
- spec/passwd_spec.rb
33
35
  spec/plaintext_spec.rb
34
36
  spec/sha1_spec.rb
35
37
  spec/spec_helper.rb
data/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  ## HTAuth
2
2
 
3
- * [Homepage](http://copiousfreetime.rubyforge.org/htauth)
4
3
  * [Github](http://github.com/copiousfreetime/htauth/tree/master)
5
4
  * email jeremy at copiousfreetime dot org
6
5
 
data/Rakefile CHANGED
@@ -11,8 +11,6 @@ This.ruby_gemspec do |spec|
11
11
  spec.add_development_dependency( 'minitest' , '~> 5.0' )
12
12
  spec.add_development_dependency( 'rdoc' , '~> 4.0' )
13
13
  spec.add_development_dependency( 'simplecov', '~> 0.9' )
14
-
15
- spec.add_dependency("highline", "~> 1.6")
16
14
  end
17
15
 
18
16
  load 'tasks/default.rake'
@@ -1,14 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- begin
4
- require 'highline'
5
- rescue LoadError
6
- require 'rubygems'
7
- require 'highline'
8
- end
9
-
10
3
  begin
11
- require 'htauth'
4
+ require 'htauth/cli'
12
5
  rescue LoadError
13
6
  path = File.expand_path(File.join(File.dirname(__FILE__),"..","lib"))
14
7
  raise if $:.include?(path)
@@ -16,4 +9,4 @@ rescue LoadError
16
9
  retry
17
10
  end
18
11
 
19
- HTAuth::Digest.new.run(ARGV, ENV)
12
+ HTAuth::CLI::Digest.new.run(ARGV, ENV)
@@ -1,14 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  begin
4
- require 'highline'
5
- rescue LoadError
6
- require 'rubygems'
7
- require 'highline'
8
- end
9
-
10
- begin
11
- require 'htauth'
4
+ require 'htauth/cli'
12
5
  rescue LoadError
13
6
  path = File.expand_path(File.join(File.dirname(__FILE__),"..","lib"))
14
7
  raise if $:.include?(path)
@@ -16,4 +9,4 @@ rescue LoadError
16
9
  retry
17
10
  end
18
11
 
19
- HTAuth::Passwd.new.run(ARGV, ENV)
12
+ HTAuth::CLI::Passwd.new.run(ARGV, ENV)
@@ -30,15 +30,14 @@ end
30
30
 
31
31
  require 'htauth/version'
32
32
  require 'htauth/algorithm'
33
+ require 'htauth/console'
33
34
  require 'htauth/crypt'
34
- require 'htauth/digest'
35
35
  require 'htauth/digest_entry'
36
36
  require 'htauth/digest_file'
37
37
  require 'htauth/entry'
38
- require 'htauth/errors'
38
+ require 'htauth/error'
39
39
  require 'htauth/file'
40
40
  require 'htauth/md5'
41
- require 'htauth/passwd'
42
41
  require 'htauth/passwd_entry'
43
42
  require 'htauth/passwd_file'
44
43
  require 'htauth/plaintext'
@@ -1,11 +1,29 @@
1
+ require 'htauth/error'
2
+ require 'securerandom'
1
3
  module HTAuth
2
- class InvalidAlgorithmError < StandardError ; end
3
- # base class all the Passwd algorithms derive from
4
+ class InvalidAlgorithmError < Error; end
5
+
6
+ # Internal: Base class all the password algorithms derive from
7
+ #
4
8
  class Algorithm
5
9
 
6
10
  SALT_CHARS = (%w[ . / ] + ("0".."9").to_a + ('A'..'Z').to_a + ('a'..'z').to_a).freeze
7
- DEFAULT = "md5"
8
- EXISTING = "existing"
11
+
12
+ # Public: flag for the md5 algorithm
13
+ MD5 = "md5".freeze
14
+ # Public: flag for the sha1 algorithm
15
+ SHA1 = "sha1".freeze
16
+ # Public: flag for the plaintext algorithm
17
+ PLAINTEXT = "plaintext".freeze
18
+ # Public: flag for the crypt algorithm
19
+ CRYPT = "crypt".freeze
20
+
21
+ # Public: flag for the default algorithm
22
+ DEFAULT = MD5
23
+
24
+ # Public: flag to indicate using the existing algorithm of the entry
25
+ EXISTING = "existing".freeze
26
+
9
27
 
10
28
  class << self
11
29
  def algorithm_from_name(a_name, params = {})
@@ -16,14 +34,14 @@ module HTAuth
16
34
  def algorithms_from_field(password_field)
17
35
  matches = []
18
36
 
19
- if password_field.index(sub_klasses['sha1'].new.prefix) then
20
- matches << sub_klasses['sha1'].new
21
- elsif password_field.index(sub_klasses['md5'].new.prefix) then
37
+ if password_field.index(sub_klasses[SHA1].new.prefix) then
38
+ matches << sub_klasses[SHA1].new
39
+ elsif password_field.index(sub_klasses[MD5].new.prefix) then
22
40
  p = password_field.split("$")
23
- matches << sub_klasses['md5'].new( :salt => p[2] )
41
+ matches << sub_klasses[MD5].new( :salt => p[2] )
24
42
  else
25
- matches << sub_klasses['plaintext'].new
26
- matches << sub_klasses['crypt'].new( :salt => password_field[0,2] )
43
+ matches << sub_klasses[PLAINTEXT].new
44
+ matches << sub_klasses[CRYPT].new( :salt => password_field[0,2] )
27
45
  end
28
46
 
29
47
  return matches
@@ -37,19 +55,41 @@ module HTAuth
37
55
  def sub_klasses
38
56
  @sub_klasses ||= {}
39
57
  end
58
+
59
+ # Internal: Constant time string comparison.
60
+ #
61
+ # From https://github.com/rack/rack/blob/master/lib/rack/utils.rb
62
+ #
63
+ # NOTE: the values compared should be of fixed length, such as strings
64
+ # that have already been processed by HMAC. This should not be used
65
+ # on variable length plaintext strings because it could leak length info
66
+ # via timing attacks.
67
+ def secure_compare(a, b)
68
+ return false unless a.bytesize == b.bytesize
69
+
70
+ l = a.unpack("C*")
71
+
72
+ r, i = 0, -1
73
+ b.each_byte { |v| r |= v ^ l[i+=1] }
74
+ r == 0
75
+ end
40
76
  end
41
77
 
78
+ # Internal
42
79
  def prefix ; end
80
+
81
+ # Internal
43
82
  def encode(password) ; end
44
83
 
45
- # 8 bytes of random items from SALT_CHARS
84
+ # Internal: 8 bytes of random items from SALT_CHARS
46
85
  def gen_salt
47
86
  chars = []
48
- 8.times { chars << SALT_CHARS[rand(SALT_CHARS.size)] }
49
- chars.join('')
87
+ 8.times { chars << SALT_CHARS[SecureRandom.random_number(SALT_CHARS.size)] }
88
+ chars.join('')
50
89
  end
51
90
 
52
- # this is not the Base64 encoding, this is the to64() method from apr
91
+ # Internal: this is not the Base64 encoding, this is the to64()
92
+ # method from the apache protable runtime library
53
93
  def to_64(number, rounds)
54
94
  r = StringIO.new
55
95
  rounds.times do |x|
@@ -0,0 +1,8 @@
1
+ require 'htauth'
2
+ module HTAuth
3
+ module CLI
4
+
5
+ end
6
+ end
7
+ require 'htauth/cli/digest'
8
+ require 'htauth/cli/passwd'
@@ -0,0 +1,130 @@
1
+ require 'htauth/version'
2
+ require 'htauth/error'
3
+ require 'htauth/digest_file'
4
+ require 'htauth/console'
5
+
6
+ require 'ostruct'
7
+ require 'optparse'
8
+
9
+ module HTAuth
10
+ module CLI
11
+ # Internal: Implemenation of the commandline htdigest-ruby
12
+ class Digest
13
+
14
+ MAX_PASSWD_LENGTH = 255
15
+
16
+ attr_accessor :digest_file
17
+
18
+ def initialize
19
+ @digest_file = nil
20
+ @option_parser = nil
21
+ @options = nil
22
+ end
23
+
24
+ def options
25
+ if @options.nil? then
26
+ @options = ::OpenStruct.new
27
+ @options.show_version = false
28
+ @options.show_help = false
29
+ @options.file_mode = DigestFile::ALTER
30
+ @options.passwdfile = nil
31
+ @options.realm = nil
32
+ @options.username = nil
33
+ @options.delete_entry = false
34
+ end
35
+ @options
36
+ end
37
+
38
+ def option_parser
39
+ if not @option_parser then
40
+ @option_parser = OptionParser.new do |op|
41
+ op.banner = "Usage: #{op.program_name} [options] passwordfile realm username"
42
+ op.on("-c", "--create", "Create a new digest password file; this overwrites an existing file.") do |c|
43
+ options.file_mode = DigestFile::CREATE
44
+ end
45
+
46
+ op.on("-D", "--delete", "Delete the specified user.") do |d|
47
+ options.delete_entry = d
48
+ end
49
+
50
+ op.on("-h", "--help", "Display this help.") do |h|
51
+ options.show_help = h
52
+ end
53
+
54
+ op.on("-v", "--version", "Show version info.") do |v|
55
+ options.show_version = v
56
+ end
57
+ end
58
+ end
59
+ @option_parser
60
+ end
61
+
62
+ def show_help
63
+ $stdout.puts option_parser
64
+ exit 1
65
+ end
66
+
67
+ def show_version
68
+ $stdout.puts "#{option_parser.program_name}: version #{HTAuth::VERSION}"
69
+ exit 1
70
+ end
71
+
72
+ def parse_options(argv)
73
+ begin
74
+ option_parser.parse!(argv)
75
+ show_version if options.show_version
76
+ show_help if options.show_help or argv.size < 3
77
+
78
+ options.passwdfile = argv.shift
79
+ options.realm = argv.shift
80
+ options.username = argv.shift
81
+ rescue ::OptionParser::ParseError => pe
82
+ $stderr.puts "ERROR: #{option_parser.program_name} - #{pe}"
83
+ $stderr.puts "Try `#{option_parser.program_name} --help` for more information"
84
+ exit 1
85
+ end
86
+ end
87
+
88
+ def run(argv, env = ENV)
89
+ begin
90
+ parse_options(argv)
91
+ digest_file = DigestFile.new(options.passwdfile, options.file_mode)
92
+
93
+ if options.delete_entry then
94
+ digest_file.delete(options.username, options.realm)
95
+ else
96
+ console = Console.new
97
+
98
+ action = digest_file.has_entry?(options.username, options.realm) ? "Changing" : "Adding"
99
+
100
+ console.say "#{action} password for #{options.username} in realm #{options.realm}."
101
+
102
+ pw_in = console.ask(" New password: ")
103
+ raise PasswordError, "password '#{pw_in}' too long" if pw_in.length >= MAX_PASSWD_LENGTH
104
+
105
+ pw_validate = console.ask("Re-type new password: ")
106
+ raise PasswordError, "They don't match, sorry." unless pw_in == pw_validate
107
+
108
+ digest_file.add_or_update(options.username, options.realm, pw_in)
109
+ end
110
+
111
+ digest_file.save!
112
+
113
+ rescue HTAuth::FileAccessError => fae
114
+ msg = "Could not open password file #{options.passwdfile} "
115
+ $stderr.puts "#{msg}: #{fae.message}"
116
+ $stderr.puts fae.backtrace.join("\n")
117
+ exit 1
118
+ rescue HTAuth::Error => pe
119
+ $stderr.puts "#{pe.message}"
120
+ exit 1
121
+ rescue SignalException => se
122
+ $stderr.puts
123
+ $stderr.puts "Interrupted #{se}"
124
+ exit 1
125
+ end
126
+ exit 0
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,179 @@
1
+ require 'htauth/error'
2
+ require 'htauth/passwd_file'
3
+ require 'htauth/console'
4
+
5
+ require 'ostruct'
6
+ require 'optparse'
7
+
8
+ module HTAuth
9
+ module CLI
10
+ # Internal: Implemenation of the commandline htpasswd-ruby
11
+ class Passwd
12
+
13
+ MAX_PASSWD_LENGTH = 255
14
+
15
+ attr_accessor :passwd_file
16
+
17
+ def initialize
18
+ @passwd_file = nil
19
+ @option_parser = nil
20
+ @options = nil
21
+ end
22
+
23
+ def options
24
+ if @options.nil? then
25
+ @options = ::OpenStruct.new
26
+ @options.batch_mode = false
27
+ @options.file_mode = File::ALTER
28
+ @options.passwdfile = nil
29
+ @options.algorithm = Algorithm::EXISTING
30
+ @options.send_to_stdout = false
31
+ @options.show_version = false
32
+ @options.show_help = false
33
+ @options.username = nil
34
+ @options.delete_entry = false
35
+ @options.password = ""
36
+ end
37
+ @options
38
+ end
39
+
40
+ def option_parser
41
+ if not @option_parser then
42
+ @option_parser = OptionParser.new do |op|
43
+ op.banner = <<-EOB
44
+ Usage:
45
+ #{op.program_name} [-cmdpsD] passwordfile username
46
+ #{op.program_name} -b[cmdpsD] passwordfile username password
47
+
48
+ #{op.program_name} -n[mdps] username
49
+ #{op.program_name} -nb[mdps] username password
50
+ EOB
51
+
52
+ op.separator ""
53
+
54
+ op.on("-b", "--batch", "Batch mode, get the password from the command line, rather than prompt") do |b|
55
+ options.batch_mode = b
56
+ end
57
+
58
+ op.on("-c", "--create", "Create a new file; this overwrites an existing file.") do |c|
59
+ options.file_mode = HTAuth::File::CREATE
60
+ end
61
+
62
+ op.on("-d", "--crypt", "Force CRYPT encryption of the password.") do |c|
63
+ options.algorithm = Algorithm::CRYPT
64
+ end
65
+
66
+ op.on("-D", "--delete", "Delete the specified user.") do |d|
67
+ options.delete_entry = d
68
+ end
69
+
70
+ op.on("-h", "--help", "Display this help.") do |h|
71
+ options.show_help = h
72
+ end
73
+
74
+ op.on("-m", "--md5", "Force MD5 encryption of the password (default).") do |m|
75
+ options.algorithm = Algorithm::MD5
76
+ end
77
+
78
+ op.on("-n", "--stdout", "Do not update the file; Display the results on stdout instead.") do |n|
79
+ options.send_to_stdout = true
80
+ options.passwdfile = HTAuth::File::STDOUT_FLAG
81
+ end
82
+
83
+ op.on("-p", "--plaintext", "Do not encrypt the password (plaintext).") do |p|
84
+ options.algorithm = Algorithm::PLAINTEXT
85
+ end
86
+
87
+ op.on("-s", "--sha1", "Force SHA encryption of the password.") do |s|
88
+ options.algorithm = Algorithm::SHA1
89
+ end
90
+
91
+ op.on("-v", "--version", "Show version info.") do |v|
92
+ options.show_version = v
93
+ end
94
+
95
+ op.separator ""
96
+
97
+ op.separator "The SHA algorihtm does not use a salt and is less secure than the MD5 algorithm"
98
+ end
99
+ end
100
+ @option_parser
101
+ end
102
+
103
+ def show_help
104
+ $stdout.puts option_parser
105
+ exit 1
106
+ end
107
+
108
+ def show_version
109
+ $stdout.puts "#{option_parser.program_name}: version #{HTAuth::VERSION}"
110
+ exit 1
111
+ end
112
+
113
+ def parse_options(argv)
114
+ begin
115
+ option_parser.parse!(argv)
116
+ show_version if options.show_version
117
+ show_help if options.show_help
118
+
119
+ raise ::OptionParser::ParseError, "Unable to send to stdout AND create a new file" if options.send_to_stdout and (options.file_mode == File::CREATE)
120
+ raise ::OptionParser::ParseError, "a username is needed" if options.send_to_stdout and argv.size < 1
121
+ raise ::OptionParser::ParseError, "a username and password are needed" if options.send_to_stdout and options.batch_mode and ( argv.size < 2 )
122
+ raise ::OptionParser::ParseError, "a passwordfile, username and password are needed " if not options.send_to_stdout and options.batch_mode and ( argv.size < 3 )
123
+ raise ::OptionParser::ParseError, "a passwordfile and username are needed" if argv.size < 2
124
+
125
+ options.passwdfile = argv.shift unless options.send_to_stdout
126
+ options.username = argv.shift
127
+ options.password = argv.shift if options.batch_mode
128
+
129
+ rescue ::OptionParser::ParseError => pe
130
+ $stderr.puts "ERROR: #{option_parser.program_name} - #{pe}"
131
+ show_help
132
+ exit 1
133
+ end
134
+ end
135
+
136
+ def run(argv, env = ENV)
137
+ begin
138
+ parse_options(argv)
139
+ passwd_file = PasswdFile.new(options.passwdfile, options.file_mode)
140
+
141
+ if options.delete_entry then
142
+ passwd_file.delete(options.username)
143
+ else
144
+ unless options.batch_mode
145
+ console = Console.new
146
+
147
+ action = passwd_file.has_entry?(options.username) ? "Changing" : "Adding"
148
+
149
+ console.say "#{action} password for #{options.username}."
150
+
151
+ pw_in = console.ask(" New password: ")
152
+ raise PasswordError, "password '#{pw_in}' too long" if pw_in.length >= MAX_PASSWD_LENGTH
153
+
154
+ pw_validate = console.ask("Re-type new password: ")
155
+ raise PasswordError, "They don't match, sorry." unless pw_in == pw_validate
156
+ options.password = pw_in
157
+ end
158
+ passwd_file.add_or_update(options.username, options.password, options.algorithm)
159
+ end
160
+
161
+ passwd_file.save!
162
+
163
+ rescue HTAuth::FileAccessError => fae
164
+ msg = "Password file failure (#{options.passwdfile}) "
165
+ $stderr.puts "#{msg}: #{fae.message}"
166
+ exit 1
167
+ rescue HTAuth::Error => pe
168
+ $stderr.puts "#{pe.message}"
169
+ exit 1
170
+ rescue SignalException => se
171
+ $stderr.puts
172
+ $stderr.puts "Interrupted #{se}"
173
+ exit 1
174
+ end
175
+ exit 0
176
+ end
177
+ end
178
+ end
179
+ end