htauth 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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