sequel_password 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c7be80a93422a9c18bd61e2d309b61481fb4c4d1
4
+ data.tar.gz: b89e1934f8373ba7e341d50312b1620574f93f19
5
+ SHA512:
6
+ metadata.gz: a15ae5358ab1e5601c50dc6cc4c5b10bd4c223c3f3e5d2726e8a022b8dd28989a24e8b7b0e5321add1835549fcc5bbc1277e8df0ee12866db2748bb090e07863
7
+ data.tar.gz: 1014dcda3e264885bb94301736b00bfce78f574444919b11d6f0c71c53079192fb81345403385cc838e552ddc79d021d65d2acc7a7ede82d426af3822ef7909a
@@ -0,0 +1 @@
1
+ coverage
@@ -0,0 +1,44 @@
1
+ Style/AlignHash:
2
+ Enabled: false
3
+
4
+ Style/AlignArray:
5
+ Enabled: false
6
+
7
+ Style/AlignParameters:
8
+ Enabled: false
9
+
10
+ Style/Documentation:
11
+ Enabled: false
12
+
13
+ Style/CaseIndentation:
14
+ Enabled: false
15
+
16
+ Style/IndentHash:
17
+ Enabled: false
18
+
19
+ Style/NumericLiterals:
20
+ Enabled: false
21
+
22
+ Style/SignalException:
23
+ Enabled: false
24
+
25
+ Style/StringLiterals:
26
+ Enabled: false
27
+
28
+ Style/MultilineOperationIndentation:
29
+ Enabled: false
30
+
31
+ Style/StringLiterals:
32
+ Enabled: false
33
+
34
+ Metrics/LineLength:
35
+ Max: 250
36
+
37
+ Metrics/MethodLength:
38
+ Max: 250
39
+
40
+ Metrics/ClassLength:
41
+ Max: 250
42
+
43
+ Lint/UselessAssignment:
44
+ Enabled: false
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.2.1
5
+ sudo: false
6
+ script: bundle exec rspec
7
+ notifications:
8
+ email: false
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,45 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sequel_password (0.1)
5
+ bcrypt (~> 3.1.10)
6
+ pbkdf2-ruby (~> 0.2.1)
7
+ sequel (~> 4.21.0)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ bcrypt (3.1.10)
13
+ diff-lcs (1.2.5)
14
+ docile (1.1.5)
15
+ multi_json (1.11.0)
16
+ pbkdf2-ruby (0.2.1)
17
+ rspec (3.2.0)
18
+ rspec-core (~> 3.2.0)
19
+ rspec-expectations (~> 3.2.0)
20
+ rspec-mocks (~> 3.2.0)
21
+ rspec-core (3.2.2)
22
+ rspec-support (~> 3.2.0)
23
+ rspec-expectations (3.2.0)
24
+ diff-lcs (>= 1.2.0, < 2.0)
25
+ rspec-support (~> 3.2.0)
26
+ rspec-mocks (3.2.1)
27
+ diff-lcs (>= 1.2.0, < 2.0)
28
+ rspec-support (~> 3.2.0)
29
+ rspec-support (3.2.2)
30
+ sequel (4.21.0)
31
+ simplecov (0.9.2)
32
+ docile (~> 1.1.0)
33
+ multi_json (~> 1.0)
34
+ simplecov-html (~> 0.9.0)
35
+ simplecov-html (0.9.0)
36
+ sqlite3 (1.3.10)
37
+
38
+ PLATFORMS
39
+ ruby
40
+
41
+ DEPENDENCIES
42
+ rspec (~> 3.2.0)
43
+ sequel_password!
44
+ simplecov (~> 0.9.2)
45
+ sqlite3 (~> 1.3.10)
@@ -0,0 +1,4 @@
1
+ # Sequel password
2
+
3
+ This sequel plugin adds authentication and password hashing to Sequel models.
4
+ It supports pbkdf2 and bcrypt hashers.
@@ -0,0 +1,186 @@
1
+ require "base64"
2
+ require "bcrypt"
3
+ require "openssl"
4
+ require "pbkdf2"
5
+ require "securerandom"
6
+
7
+ module Sequel
8
+ module Plugins
9
+ module Password
10
+ class InvalidHasherException < Exception; end
11
+
12
+ def self.configure(model, options = {})
13
+ model.instance_eval do
14
+ @column = options.fetch(:column, :digest)
15
+ @hashers = options.fetch(:hashers,
16
+ pbkdf2_sha256: PBKDF2Hasher.new,
17
+ bcrypt_sha256: BCryptSHA256Hasher.new,
18
+ bcrypt: BCryptHasher.new,
19
+ sha1: SHA1Hasher.new)
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ attr_reader :column, :hashers
25
+
26
+ Plugins.inherited_instance_variables(self,
27
+ "@column": :digest, "@hashers": {})
28
+
29
+ def make_password(password, salt: nil, algorithm: :default)
30
+ return "!#{SecureRandom.hex(20)}" if password.nil?
31
+
32
+ salt = hasher(algorithm).salt if salt.nil?
33
+ hasher(algorithm).encode(password, salt)
34
+ end
35
+
36
+ def hasher(algorithm = :default)
37
+ @hashers.fetch(algorithm.to_sym, @hashers.values.first)
38
+ end
39
+
40
+ def usable_password?(encoded)
41
+ return false if encoded.nil? || encoded.start_with?("!")
42
+
43
+ algorithm = encoded.split('$').first
44
+ !hasher(algorithm).nil?
45
+ end
46
+
47
+ def check_password(password, encoded, setter: nil, algorithm: :default)
48
+ return false if password.nil? || !usable_password?(encoded)
49
+
50
+ preferred = hasher(algorithm)
51
+ hasher = hasher(encoded.split('$').first)
52
+
53
+ must_update = hasher.algorithm != preferred.algorithm
54
+ must_update = preferred.must_update(encoded) unless must_update
55
+
56
+ correct = hasher.verify(password, encoded)
57
+ setter.call(password) if !setter.nil? && correct && must_update
58
+
59
+ correct
60
+ end
61
+ end
62
+
63
+ module InstanceMethods
64
+ def authenticate(password)
65
+ encoded = send(model.column)
66
+ model.check_password(password, encoded, setter: method(:"password="))
67
+ end
68
+
69
+ def password=(password)
70
+ send("#{model.column}=", model.make_password(password))
71
+ end
72
+
73
+ def set_unusable_password
74
+ send("#{model.column}=", model.make_password(nil))
75
+ end
76
+ end
77
+
78
+ class Hasher
79
+ attr_reader :algorithm
80
+
81
+ def salt
82
+ # 72 bits
83
+ SecureRandom.hex(9)
84
+ end
85
+
86
+ def verify(password, encoded)
87
+ raise NotImplementedError
88
+ end
89
+
90
+ def encode(password, salt)
91
+ raise NotImplementedError
92
+ end
93
+
94
+ def must_update(encoded)
95
+ false
96
+ end
97
+
98
+ private
99
+
100
+ def constant_time_compare(a, b)
101
+ check = a.bytesize ^ b.bytesize
102
+ a.bytes.zip(b.bytes) { |x, y| check |= x ^ y }
103
+ check == 0
104
+ end
105
+ end
106
+
107
+ class PBKDF2Hasher < Hasher
108
+ def initialize
109
+ @algorithm = :pbkdf2_sha256
110
+ @iterations = 24000
111
+ @digest = OpenSSL::Digest::SHA256.new
112
+ end
113
+
114
+ def encode(password, salt, iterations = nil)
115
+ iterations = @iterations if iterations.nil?
116
+ hash = PBKDF2.new(password: password, salt: salt,
117
+ iterations: iterations, hash_function: @digest)
118
+ hash = Base64.strict_encode64(hash.value)
119
+ "#{@algorithm}$#{iterations}$#{salt}$#{hash}"
120
+ end
121
+
122
+ def verify(password, encoded)
123
+ algorithm, iterations, salt, hash = encoded.split('$', 4)
124
+ hash = encode(password, salt, iterations.to_i)
125
+ constant_time_compare(encoded, hash)
126
+ end
127
+
128
+ def must_update(encoded)
129
+ algorithm, iterations, salt, hash = encoded.split('$', 4)
130
+ iterations.to_i != @iterations
131
+ end
132
+ end
133
+
134
+ class BCryptSHA256Hasher < Hasher
135
+ def initialize
136
+ @algorithm = :bcrypt_sha256
137
+ @cost = 12
138
+ @digest = OpenSSL::Digest::SHA256.new
139
+ end
140
+
141
+ def salt
142
+ BCrypt::Engine.generate_salt(@cost)
143
+ end
144
+
145
+ def encode(password, salt)
146
+ password = @digest.digest(password) unless @digest.nil?
147
+ hash = BCrypt::Engine.hash_secret(password, salt)
148
+ "#{@algorithm}$#{hash}"
149
+ end
150
+
151
+ def verify(password, encoded)
152
+ algorithm, data = encoded.split('$', 2)
153
+ password = @digest.digest(password) unless @digest.nil?
154
+ hash = BCrypt::Engine.hash_secret(password, data)
155
+ constant_time_compare(data, hash)
156
+ end
157
+ end
158
+
159
+ class BCryptHasher < BCryptSHA256Hasher
160
+ def initialize
161
+ @algorithm = :bcrypt
162
+ @cost = 12
163
+ @digest = nil
164
+ end
165
+ end
166
+
167
+ class SHA1Hasher < Hasher
168
+ def initialize
169
+ @algorithm = :sha1
170
+ @digest = OpenSSL::Digest::SHA1.new
171
+ end
172
+
173
+ def encode(password, salt)
174
+ hash = @digest.digest(salt + password).unpack('H*').first
175
+ "#{@algorithm}$#{salt}$#{hash}"
176
+ end
177
+
178
+ def verify(password, encoded)
179
+ algorithm, salt, hash = encoded.split('$', 3)
180
+ hash = encode(password, salt)
181
+ constant_time_compare(encoded, hash)
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.authors = ["Timothée Peignier"]
5
+ gem.email = ["timothee.peignier@tryphon.org"]
6
+ gem.description = %q{Sequel plugins to handle password hashing}
7
+ gem.summary = %q{Add passwords hashing to sequel models.}
8
+ gem.homepage = "http://rubygems.org/gems/sequel_password"
9
+ gem.license = 'MIT'
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "sequel_password"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = '0.1'
17
+
18
+ gem.add_runtime_dependency 'sequel', '~> 4.21', '>= 4.21.0'
19
+ gem.add_runtime_dependency 'bcrypt', '~> 3.1', '>= 3.1.10'
20
+ gem.add_runtime_dependency 'pbkf2-ruby', '~> 0.2.1'
21
+
22
+ gem.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
23
+ gem.add_development_dependency 'simplecov', '~> 0.9.2'
24
+ gem.add_development_dependency 'sqlite3', '~> 1.3', '>= 1.3.10'
25
+ end
@@ -0,0 +1,115 @@
1
+ require "spec_helper"
2
+
3
+ describe Sequel::Plugins::Password do
4
+ subject(:user) { DefaultUser.new }
5
+
6
+ it "has an inherited instance variable @column" do
7
+ expect(DefaultUser.inherited_instance_variables).to include(:@column)
8
+ end
9
+
10
+ it "has an inherited instance variable @hashers" do
11
+ expect(DefaultUser.inherited_instance_variables).to include(:@hashers)
12
+ end
13
+
14
+ describe "set_unusable_password" do
15
+ let(:secret) { "lètmein" }
16
+
17
+ before { user.password = secret }
18
+
19
+ it "sets an unusable password" do
20
+ expect { user.set_unusable_password }.to change(user, :digest)
21
+ expect(user.digest).to match(/^!/)
22
+ expect(user.digest.length).to eq(41)
23
+ end
24
+ end
25
+
26
+ describe "#authenticate" do
27
+ let(:secret) { "lètmein" }
28
+
29
+ before { user.password = secret }
30
+
31
+ it "returns true if authentication is successful" do
32
+ expect(user.authenticate(secret)).to be_truthy
33
+ end
34
+
35
+ it "returns false when authentication fails" do
36
+ expect(user.authenticate("")).to be_falsey
37
+ end
38
+
39
+ it "upgrade to newest hasher" do
40
+ user.digest = "sha1$seasalt$cff36ea83f5706ce9aa7454e63e431fc726b2dc8"
41
+ expect { user.authenticate(secret) }.to change(user, :digest)
42
+ expect(user.digest).to match(/^pbkdf2_sha256\$/)
43
+ end
44
+
45
+ it "upgrade to new iterations values" do
46
+ user.digest = "pbkdf2_sha256$20000$seasalt$oBSd886ysm3AqYun62DOdin8YcfbU1z9cksZSuLP9r0="
47
+ expect { user.authenticate(secret) }.to change(user, :digest)
48
+ expect(user.digest).to match(/^pbkdf2_sha256\$24000\$/)
49
+ end
50
+ end
51
+
52
+ describe Sequel::Plugins::Password::PBKDF2Hasher do
53
+ let(:hasher) { described_class.new }
54
+ let(:password) { 'lètmein' }
55
+ let(:salt) { 'seasalt' }
56
+
57
+ it "encodes the password properly" do
58
+ encoded = hasher.encode(password, salt)
59
+ expect(encoded).to eq("pbkdf2_sha256$24000$#{salt}$V9DfCAVoweeLwxC/L2mb+7swhzF0XYdyQMqmusZqiTc=")
60
+ expect(hasher.verify(password, encoded)).to be_truthy
61
+ expect(hasher.verify(password.reverse, encoded)).to be_falsey
62
+ end
63
+
64
+ it "allows blank password" do
65
+ blank_encoded = hasher.encode('', salt)
66
+ expect(blank_encoded).to match(/^pbkdf2_sha256\$/)
67
+ expect(hasher.verify('', blank_encoded)).to be_truthy
68
+ expect(hasher.verify(' ', blank_encoded)).to be_falsey
69
+ end
70
+ end
71
+
72
+ describe Sequel::Plugins::Password::BCryptSHA256Hasher do
73
+ let(:hasher) { described_class.new }
74
+ let(:password) { 'lètmein' }
75
+
76
+ it "encodes the password properly" do
77
+ encoded = hasher.encode(password, hasher.salt)
78
+ expect(encoded).to match(/^bcrypt_sha256\$/)
79
+ expect(hasher.verify(password, encoded)).to be_truthy
80
+ expect(hasher.verify(password.reverse, encoded)).to be_falsey
81
+ end
82
+ end
83
+
84
+ describe Sequel::Plugins::Password::BCryptHasher do
85
+ let(:hasher) { described_class.new }
86
+ let(:password) { 'lètmein' }
87
+
88
+ it "encodes the password properly" do
89
+ encoded = hasher.encode(password, hasher.salt)
90
+ expect(encoded).to match(/^bcrypt\$/)
91
+ expect(hasher.verify(password, encoded)).to be_truthy
92
+ expect(hasher.verify(password.reverse, encoded)).to be_falsey
93
+ end
94
+ end
95
+
96
+ describe Sequel::Plugins::Password::SHA1Hasher do
97
+ let(:hasher) { described_class.new }
98
+ let(:password) { 'lètmein' }
99
+ let(:salt) { 'seasalt' }
100
+
101
+ it "encodes the password properly" do
102
+ encoded = hasher.encode(password, salt)
103
+ expect(encoded).to eq("sha1$#{salt}$cff36ea83f5706ce9aa7454e63e431fc726b2dc8")
104
+ expect(hasher.verify(password, encoded)).to be_truthy
105
+ expect(hasher.verify(password.reverse, encoded)).to be_falsey
106
+ end
107
+
108
+ it "allows blank password" do
109
+ blank_encoded = hasher.encode('', salt)
110
+ expect(blank_encoded).to match(/^sha1\$/)
111
+ expect(hasher.verify('', blank_encoded)).to be_truthy
112
+ expect(hasher.verify(' ', blank_encoded)).to be_falsey
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,64 @@
1
+ require "bundler"
2
+ Bundler.require
3
+
4
+ require "simplecov"
5
+ SimpleCov.start do
6
+ add_filter('spec/')
7
+ end
8
+
9
+ require "sequel"
10
+ require "sequel_password"
11
+
12
+ RSpec.configure do |config|
13
+ config.order = 'random'
14
+
15
+ config.before(:suite) do
16
+ Sequel::Model.plugin(:schema)
17
+ Sequel.connect('sqlite:/')
18
+
19
+ class DefaultUser < Sequel::Model
20
+ set_schema do
21
+ primary_key :id
22
+ varchar :digest
23
+ end
24
+
25
+ plugin :password
26
+ end
27
+
28
+ class BCryptUser < Sequel::Model
29
+ set_schema do
30
+ primary_key :id
31
+ varchar :digest
32
+ end
33
+
34
+ plugin :password, hashers: { bcrypt: Sequel::Plugins::Password::BCryptHasher.new }
35
+ end
36
+
37
+ class BCryptSHA256User < Sequel::Model
38
+ set_schema do
39
+ primary_key :id
40
+ varchar :digest
41
+ end
42
+
43
+ plugin :password, hashers: { bcrypt: Sequel::Plugins::Password::BCryptSHA256Hasher.new }
44
+ end
45
+
46
+ class AlternateColumnUser < Sequel::Model
47
+ set_schema do
48
+ primary_key :id
49
+ varchar :password_digest
50
+ end
51
+
52
+ plugin :password, column: :digest
53
+ end
54
+
55
+ DefaultUser.create_table!
56
+ BCryptUser.create_table!
57
+ BCryptSHA256User.create_table!
58
+ AlternateColumnUser.create_table!
59
+ end
60
+
61
+ config.around(:each) do |example|
62
+ Sequel::Model.db.transaction(rollback: :always) { example.run }
63
+ end
64
+ end
metadata ADDED
@@ -0,0 +1,164 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sequel_password
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Timothée Peignier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sequel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.21'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 4.21.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '4.21'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 4.21.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: bcrypt
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.1'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 3.1.10
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '3.1'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 3.1.10
53
+ - !ruby/object:Gem::Dependency
54
+ name: pbkf2-ruby
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: 0.2.1
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: 0.2.1
67
+ - !ruby/object:Gem::Dependency
68
+ name: rspec
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '3.2'
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 3.2.0
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '3.2'
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 3.2.0
87
+ - !ruby/object:Gem::Dependency
88
+ name: simplecov
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: 0.9.2
94
+ type: :development
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: 0.9.2
101
+ - !ruby/object:Gem::Dependency
102
+ name: sqlite3
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: '1.3'
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 1.3.10
111
+ type: :development
112
+ prerelease: false
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.3'
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: 1.3.10
121
+ description: Sequel plugins to handle password hashing
122
+ email:
123
+ - timothee.peignier@tryphon.org
124
+ executables: []
125
+ extensions: []
126
+ extra_rdoc_files: []
127
+ files:
128
+ - ".gitignore"
129
+ - ".rubocop.yml"
130
+ - ".travis.yml"
131
+ - Gemfile
132
+ - Gemfile.lock
133
+ - README.md
134
+ - lib/sequel_password.rb
135
+ - sequel_password.gemspec
136
+ - spec/sequel_password_spec.rb
137
+ - spec/spec_helper.rb
138
+ homepage: http://rubygems.org/gems/sequel_password
139
+ licenses:
140
+ - MIT
141
+ metadata: {}
142
+ post_install_message:
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubyforge_project:
158
+ rubygems_version: 2.4.5
159
+ signing_key:
160
+ specification_version: 4
161
+ summary: Add passwords hashing to sequel models.
162
+ test_files:
163
+ - spec/sequel_password_spec.rb
164
+ - spec/spec_helper.rb