Project

General

Profile

Feature #14940 ยป 0001-Support-bcrypt-password-hashing-in-webrick.patch

jeremyevans0 (Jeremy Evans), 07/25/2018 11:31 PM

View differences:

lib/webrick/httpauth/basicauth.rb
24 24
    #
25 25
    #   config = { :Realm => 'BasicAuth example realm' }
26 26
    #
27
    #   htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file'
27
    #   htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file', password_hash: :bcrypt
28 28
    #   htpasswd.set_passwd config[:Realm], 'username', 'password'
29 29
    #   htpasswd.flush
30 30
    #
......
81 81
          error("%s: the user is not allowed.", userid)
82 82
          challenge(req, res)
83 83
        end
84
        if password.crypt(encpass) != encpass
84

  
85
        case encpass
86
        when /\A\$2[aby]\$/
87
          password_matches = BCrypt::Password.new(encpass.sub(/\A\$2[aby]\$/, '$2a$')) == password
88
        else
89
          password_matches = password.crypt(encpass) == encpass
90
        end
91

  
92
        unless password_matches
85 93
          error("%s: password unmatch.", userid)
86 94
          challenge(req, res)
87 95
        end
lib/webrick/httpauth/htpasswd.rb
35 35
      ##
36 36
      # Open a password database at +path+
37 37

  
38
      def initialize(path)
38
      def initialize(path, password_hash: nil)
39 39
        @path = path
40 40
        @mtime = Time.at(0)
41 41
        @passwd = Hash.new
42 42
        @auth_type = BasicAuth
43
        @password_hash = password_hash
44

  
45
        case @password_hash
46
        when nil
47
          # begin
48
          #   require "string/crypt"
49
          # rescue LoadError
50
          #   warn("Unable to load string/crypt, proceeding with deprecated use of String#crypt, consider using password_hash: :bcrypt")
51
          # end
52
          @password_hash = :crypt
53
        when :crypt
54
          # require "string/crypt"
55
        when :bcrypt
56
          require "bcrypt"
57
        else
58
          raise ArgumentError, "only :crypt and :bcrypt are supported for password_hash keyword argument"
59
        end
60

  
43 61
        File.open(@path,"a").close unless File.exist?(@path)
44 62
        reload
45 63
      end
......
55 73
            while line = io.gets
56 74
              line.chomp!
57 75
              case line
58
              when %r!\A[^:]+:[a-zA-Z0-9./]{13}\z!
76
              when %r!\A[^:]+:[a-zA-Z0-9./]{13}\z! 
77
                if @password_hash == :bcrypt
78
                  raise StandardError, ".htpasswd file contains crypt password, only bcrypt passwords supported"
79
                end
80
                user, pass = line.split(":")
81
              when %r!\A[^:]+:\$2[aby]\$\d{2}\$.{53}\z! 
82
                if @password_hash == :crypt
83
                  raise StandardError, ".htpasswd file contains bcrypt password, only crypt passwords supported"
84
                end
59 85
                user, pass = line.split(":")
60 86
              when /:\$/, /:{SHA}/
61 87
                raise NotImplementedError,
......
102 128
      # Sets a password in the database for +user+ in +realm+ to +pass+.
103 129

  
104 130
      def set_passwd(realm, user, pass)
105
        @passwd[user] = make_passwd(realm, user, pass)
131
        if @password_hash == :bcrypt
132
          # Cost of 5 to match Apache default, and because the
133
          # bcrypt default of 10 will introduce significant delays
134
          # for every request.
135
          @passwd[user] = BCrypt::Password.create(pass, :cost=>5)
136
        else
137
          @passwd[user] = make_passwd(realm, user, pass)
138
        end
106 139
      end
107 140

  
108 141
      ##
test/webrick/test_httpauth.rb
37 37
    }
38 38
  end
39 39

  
40
  def test_basic_auth2
41
    log_tester = lambda {|log, access_log|
42
      log.reject! {|line| /\A\s*\z/ =~ line }
43
      pats = [
44
        /ERROR Basic WEBrick's realm: webrick: password unmatch\./,
45
        /ERROR WEBrick::HTTPStatus::Unauthorized/
46
      ]
47
      pats.each {|pat|
48
        assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}")
49
        log.reject! {|line| pat =~ line }
50
      }
51
      assert_equal([], log)
52
    }
53
    TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
54
      realm = "WEBrick's realm"
55
      path = "/basic_auth2"
56

  
57
      Tempfile.create("test_webrick_auth") {|tmpfile|
58
        tmpfile.close
59
        tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path)
60
        tmp_pass.set_passwd(realm, "webrick", "supersecretpassword")
61
        tmp_pass.set_passwd(realm, "foo", "supersecretpassword")
62
        tmp_pass.flush
63

  
64
        htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path)
65
        users = []
66
        htpasswd.each{|user, pass| users << user }
67
        assert_equal(2, users.size, log.call)
68
        assert(users.member?("webrick"), log.call)
69
        assert(users.member?("foo"), log.call)
70

  
71
        server.mount_proc(path){|req, res|
72
          auth = WEBrick::HTTPAuth::BasicAuth.new(
73
            :Realm => realm, :UserDB => htpasswd,
74
            :Logger => server.logger
75
          )
76
          auth.authenticate(req, res)
77
          res.body = "hoge"
78
        }
79
        http = Net::HTTP.new(addr, port)
80
        g = Net::HTTP::Get.new(path)
81
        g.basic_auth("webrick", "supersecretpassword")
82
        http.request(g){|res| assert_equal("hoge", res.body, log.call)}
83
        g.basic_auth("webrick", "not super")
84
        http.request(g){|res| assert_not_equal("hoge", res.body, log.call)}
85
      }
86
    }
87
  end
88

  
89
  def test_basic_auth3
40
  def test_basic_auth_sha
90 41
    Tempfile.create("test_webrick_auth") {|tmpfile|
91 42
      tmpfile.puts("webrick:{SHA}GJYFRpBbdchp595jlh3Bhfmgp8k=")
92 43
      tmpfile.flush
......
94 45
        WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path)
95 46
      }
96 47
    }
48
  end
97 49

  
50
  def test_basic_auth_md5
98 51
    Tempfile.create("test_webrick_auth") {|tmpfile|
99 52
      tmpfile.puts("webrick:$apr1$IOVMD/..$rmnOSPXr0.wwrLPZHBQZy0")
100 53
      tmpfile.flush
......
104 57
    }
105 58
  end
106 59

  
107
  def test_bad_username_with_control_characters
108
    log_tester = lambda {|log, access_log|
109
      assert_equal(2, log.length)
110
      assert_match(/ERROR Basic WEBrick's realm: foo\\ebar: the user is not allowed./, log[0])
111
      assert_match(/ERROR WEBrick::HTTPStatus::Unauthorized/, log[1])
112
    }
113
    TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
114
      realm = "WEBrick's realm"
115
      path = "/basic_auth"
60
  [nil, :crypt, :bcrypt].each do |hash_algo|
61
    begin
62
      case hash_algo
63
      when :crypt
64
        # require 'string/crypt'
65
      when :bcrypt
66
        require 'bcrypt'
67
      end
68
    rescue LoadError
69
      next
70
    end
116 71

  
117
      Tempfile.create("test_webrick_auth") {|tmpfile|
118
        tmpfile.close
119
        tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path)
120
        tmp_pass.set_passwd(realm, "webrick", "supersecretpassword")
121
        tmp_pass.set_passwd(realm, "foo", "supersecretpassword")
122
        tmp_pass.flush
72
    define_method(:"test_basic_auth_htpasswd_#{hash_algo}") do
73
      log_tester = lambda {|log, access_log|
74
        log.reject! {|line| /\A\s*\z/ =~ line }
75
        pats = [
76
          /ERROR Basic WEBrick's realm: webrick: password unmatch\./,
77
          /ERROR WEBrick::HTTPStatus::Unauthorized/
78
        ]
79
        pats.each {|pat|
80
          assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}")
81
          log.reject! {|line| pat =~ line }
82
        }
83
        assert_equal([], log)
84
      }
85
      TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
86
        realm = "WEBrick's realm"
87
        path = "/basic_auth2"
123 88

  
124
        htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path)
125
        users = []
126
        htpasswd.each{|user, pass| users << user }
127
        server.mount_proc(path){|req, res|
128
          auth = WEBrick::HTTPAuth::BasicAuth.new(
129
            :Realm => realm, :UserDB => htpasswd,
130
            :Logger => server.logger
131
          )
132
          auth.authenticate(req, res)
133
          res.body = "hoge"
89
        Tempfile.create("test_webrick_auth") {|tmpfile|
90
          tmpfile.close
91
          tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo)
92
          tmp_pass.set_passwd(realm, "webrick", "supersecretpassword")
93
          tmp_pass.set_passwd(realm, "foo", "supersecretpassword")
94
          tmp_pass.flush
95

  
96
          htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo)
97
          users = []
98
          htpasswd.each{|user, pass| users << user }
99
          assert_equal(2, users.size, log.call)
100
          assert(users.member?("webrick"), log.call)
101
          assert(users.member?("foo"), log.call)
102

  
103
          server.mount_proc(path){|req, res|
104
            auth = WEBrick::HTTPAuth::BasicAuth.new(
105
              :Realm => realm, :UserDB => htpasswd,
106
              :Logger => server.logger
107
            )
108
            auth.authenticate(req, res)
109
            res.body = "hoge"
110
          }
111
          http = Net::HTTP.new(addr, port)
112
          g = Net::HTTP::Get.new(path)
113
          g.basic_auth("webrick", "supersecretpassword")
114
          http.request(g){|res| assert_equal("hoge", res.body, log.call)}
115
          g.basic_auth("webrick", "not super")
116
          http.request(g){|res| assert_not_equal("hoge", res.body, log.call)}
134 117
        }
135
        http = Net::HTTP.new(addr, port)
136
        g = Net::HTTP::Get.new(path)
137
        g.basic_auth("foo\ebar", "passwd")
138
        http.request(g){|res| assert_not_equal("hoge", res.body, log.call) }
139 118
      }
140
    }
119
    end
120

  
121
    define_method(:"test_basic_auth_bad_username_htpasswd_#{hash_algo}") do
122
      log_tester = lambda {|log, access_log|
123
        assert_equal(2, log.length)
124
        assert_match(/ERROR Basic WEBrick's realm: foo\\ebar: the user is not allowed./, log[0])
125
        assert_match(/ERROR WEBrick::HTTPStatus::Unauthorized/, log[1])
126
      }
127
      TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
128
        realm = "WEBrick's realm"
129
        path = "/basic_auth"
130

  
131
        Tempfile.create("test_webrick_auth") {|tmpfile|
132
          tmpfile.close
133
          tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo)
134
          tmp_pass.set_passwd(realm, "webrick", "supersecretpassword")
135
          tmp_pass.set_passwd(realm, "foo", "supersecretpassword")
136
          tmp_pass.flush
137

  
138
          htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo)
139
          users = []
140
          htpasswd.each{|user, pass| users << user }
141
          server.mount_proc(path){|req, res|
142
            auth = WEBrick::HTTPAuth::BasicAuth.new(
143
              :Realm => realm, :UserDB => htpasswd,
144
              :Logger => server.logger
145
            )
146
            auth.authenticate(req, res)
147
            res.body = "hoge"
148
          }
149
          http = Net::HTTP.new(addr, port)
150
          g = Net::HTTP::Get.new(path)
151
          g.basic_auth("foo\ebar", "passwd")
152
          http.request(g){|res| assert_not_equal("hoge", res.body, log.call) }
153
        }
154
      }
155
    end
141 156
  end
142 157

  
143 158
  DIGESTRES_ = /
144
-