passlib 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +726 -0
- data/lib/passlib/argon2.rb +48 -0
- data/lib/passlib/balloon.rb +36 -0
- data/lib/passlib/bcrypt.rb +48 -0
- data/lib/passlib/bcrypt_sha256.rb +82 -0
- data/lib/passlib/configuration/context.rb +121 -0
- data/lib/passlib/configuration/passlib.rb +17 -0
- data/lib/passlib/configuration.rb +187 -0
- data/lib/passlib/context.rb +76 -0
- data/lib/passlib/internal/dependency.rb +39 -0
- data/lib/passlib/internal/dsl.rb +59 -0
- data/lib/passlib/internal/register.rb +10 -0
- data/lib/passlib/internal.rb +22 -0
- data/lib/passlib/ldap_digest.rb +106 -0
- data/lib/passlib/md5_crypt.rb +152 -0
- data/lib/passlib/password.rb +169 -0
- data/lib/passlib/pbkdf2.rb +133 -0
- data/lib/passlib/phpass.rb +127 -0
- data/lib/passlib/scrypt.rb +89 -0
- data/lib/passlib/sha1_crypt.rb +108 -0
- data/lib/passlib/sha2_crypt.rb +172 -0
- data/lib/passlib/version.rb +7 -0
- data/lib/passlib/yescrypt.rb +31 -0
- data/lib/passlib.rb +91 -0
- metadata +160 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Passlib
|
|
4
|
+
# Handles Argon2 password hashing via the {https://rubygems.org/gems/argon2 argon2} gem.
|
|
5
|
+
#
|
|
6
|
+
# Supports all three Argon2 variants: +argon2i+, +argon2id+ (recommended),
|
|
7
|
+
# and +argon2d+. Hash format: +$argon2id$v=19$m=65536,t=2,p=1$...$...+
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# hash = Passlib::Argon2.create("hunter2")
|
|
11
|
+
# hash.verify("hunter2") # => true
|
|
12
|
+
#
|
|
13
|
+
# @!method self.create(secret, **options)
|
|
14
|
+
# Creates a new Argon2 hash.
|
|
15
|
+
# @param secret [String] the plaintext password
|
|
16
|
+
# @option options [String] :secret optional pepper (server-side secret mixed into the hash)
|
|
17
|
+
# @option options [String] :salt custom salt (normally auto-generated)
|
|
18
|
+
# @option options [Symbol] :profile pre-defined cost profile (see argon2 gem docs)
|
|
19
|
+
# @option options [Integer] :t_cost time cost — number of iterations
|
|
20
|
+
# @option options [Integer] :m_cost memory cost in KiB
|
|
21
|
+
# @option options [Integer] :p_cost parallelism factor
|
|
22
|
+
# @return [Argon2]
|
|
23
|
+
class Argon2 < Password
|
|
24
|
+
external "argon2", "~> 2.3"
|
|
25
|
+
register mcf: %w[argon2i argon2id argon2d]
|
|
26
|
+
options :secret, :salt, :profile, :t_cost, :m_cost, :p_cost
|
|
27
|
+
|
|
28
|
+
def verify(secret) = ::Argon2::Engine.argon2_verify(secret, string, config.secret)
|
|
29
|
+
def create(secret) = ::Argon2::Password.new(**config).create(secret)
|
|
30
|
+
|
|
31
|
+
def upgrade?
|
|
32
|
+
defaults = ::Argon2::Profiles[config.profile] if config.profile
|
|
33
|
+
|
|
34
|
+
defaults ||= {
|
|
35
|
+
t_cost: ::Argon2::Password::DEFAULT_T_COST,
|
|
36
|
+
m_cost: ::Argon2::Password::DEFAULT_M_COST,
|
|
37
|
+
p_cost: ::Argon2::Password::DEFAULT_P_COST,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
params = Internal.mcf_params(string)
|
|
41
|
+
|
|
42
|
+
params[:t] != (config.t_cost || defaults[:t_cost]) ||
|
|
43
|
+
params[:m] != (1 << (config.m_cost || defaults[:m_cost])) ||
|
|
44
|
+
params[:p] != (config.p_cost || defaults[:p_cost]) ||
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Passlib
|
|
4
|
+
# Handles Balloon hashing via the
|
|
5
|
+
# {https://rubygems.org/gems/balloon_hashing balloon_hashing} gem.
|
|
6
|
+
#
|
|
7
|
+
# Hash format: +$balloon$v=1$alg=sha256,s=1024,t=3$<salt>$<checksum>+
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# hash = Passlib::Balloon.create("hunter2")
|
|
11
|
+
# hash.verify("hunter2") # => true
|
|
12
|
+
#
|
|
13
|
+
# @!method self.create(secret, **options)
|
|
14
|
+
# Creates a new Balloon hash.
|
|
15
|
+
# @param secret [String] the plaintext password
|
|
16
|
+
# @option options [Integer] :s_cost space cost (memory usage)
|
|
17
|
+
# @option options [Integer] :t_cost time cost (iterations)
|
|
18
|
+
# @option options [String] :algorithm digest algorithm name (e.g. +"sha256"+)
|
|
19
|
+
# @return [Balloon]
|
|
20
|
+
class Balloon < Password
|
|
21
|
+
register mcf: "balloon"
|
|
22
|
+
external "balloon_hashing", ">= 0.1.0"
|
|
23
|
+
options :s_cost, :t_cost, :algorithm
|
|
24
|
+
|
|
25
|
+
def verify(secret) = BalloonHashing.verify(secret, string)
|
|
26
|
+
def create(secret) = BalloonHashing.create(secret, **config)
|
|
27
|
+
|
|
28
|
+
def upgrade?
|
|
29
|
+
params = Internal.mcf_params(string)
|
|
30
|
+
params[:alg] != (config.algorithm || BalloonHashing::DEFAULT_ALGORITHM) ||
|
|
31
|
+
params[:s] != (config.s_cost || BalloonHashing::DEFAULT_S_COST) ||
|
|
32
|
+
params[:t] != (config.t_cost || BalloonHashing::DEFAULT_T_COST) ||
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Passlib
|
|
4
|
+
# Handles bcrypt password hashing via the {https://rubygems.org/gems/bcrypt bcrypt} gem.
|
|
5
|
+
#
|
|
6
|
+
# Recognized hash formats: +$2a$+, +$2b$+, +$2x$+, +$2y$+ (all variants are
|
|
7
|
+
# accepted on load, new hashes are always produced in +$2a$+ format by the
|
|
8
|
+
# underlying gem).
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# hash = Passlib::BCrypt.create("hunter2", cost: 12)
|
|
12
|
+
# hash.verify("hunter2") # => true
|
|
13
|
+
# hash.to_s # => "$2a$12$..."
|
|
14
|
+
#
|
|
15
|
+
# @!method self.create(secret, **options)
|
|
16
|
+
# Creates a new bcrypt hash.
|
|
17
|
+
# @param secret [String] the plaintext password
|
|
18
|
+
# @option options [String] :salt custom bcrypt salt string (normally auto-generated,
|
|
19
|
+
# must include the cost factor in standard bcrypt format)
|
|
20
|
+
# @option options [Integer] :cost bcrypt cost factor, 4–31
|
|
21
|
+
# (default: +BCrypt::Engine::DEFAULT_COST+)
|
|
22
|
+
# @return [BCrypt]
|
|
23
|
+
class BCrypt < Password
|
|
24
|
+
external "bcrypt", "~> 3.0"
|
|
25
|
+
register mcf: %w[2a 2b 2x 2y]
|
|
26
|
+
options :salt, :cost
|
|
27
|
+
|
|
28
|
+
# @param secret [String] the plaintext password to re-hash
|
|
29
|
+
# @return [BCrypt] a new instance hashed with the same salt
|
|
30
|
+
def create_comparable(secret) = self.class.create(secret, salt: @salt)
|
|
31
|
+
|
|
32
|
+
def upgrade?
|
|
33
|
+
cost = @salt.split("$")[2].to_i
|
|
34
|
+
cost != (config.cost || ::BCrypt::Engine::DEFAULT_COST)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create(secret)
|
|
38
|
+
@salt = config.salt || ::BCrypt::Engine.generate_salt(config.cost || ::BCrypt::Engine::DEFAULT_COST)
|
|
39
|
+
::BCrypt::Engine.hash_secret(secret, @salt)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def load(string)
|
|
43
|
+
bcrypt = ::BCrypt::Password.new(string)
|
|
44
|
+
@salt = bcrypt.salt
|
|
45
|
+
bcrypt.to_str
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Passlib
|
|
4
|
+
# Handles bcrypt-sha256 password hashing via the
|
|
5
|
+
# {https://rubygems.org/gems/bcrypt bcrypt} gem.
|
|
6
|
+
#
|
|
7
|
+
# A hybrid scheme that works around bcrypt's 72-byte password truncation
|
|
8
|
+
# limit: the password is first run through HMAC-SHA256 (keyed with the
|
|
9
|
+
# bcrypt salt), the resulting 32-byte digest is base64-encoded, and the
|
|
10
|
+
# base64 string is then hashed with standard bcrypt. Passwords of any
|
|
11
|
+
# length are handled correctly, and the output is a self-contained MCF
|
|
12
|
+
# string that embeds all parameters needed for verification.
|
|
13
|
+
#
|
|
14
|
+
# Hash format: +$bcrypt-sha256$v=2,t=2b,r=12$<salt22>$<digest31>+
|
|
15
|
+
#
|
|
16
|
+
# This format is compatible with Python's
|
|
17
|
+
# {https://passlib.readthedocs.io/en/stable/lib/passlib.hash.bcrypt_sha256.html
|
|
18
|
+
# passlib.hash.bcrypt_sha256}.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# hash = Passlib::BcryptSHA256.create("hunter2")
|
|
22
|
+
# hash.verify("hunter2") # => true
|
|
23
|
+
# hash.to_s # => "$bcrypt-sha256$v=2,t=2b,r=12$..."
|
|
24
|
+
#
|
|
25
|
+
# @!method self.create(secret, **options)
|
|
26
|
+
# Creates a new bcrypt-sha256 hash.
|
|
27
|
+
# @param secret [String] the plaintext password
|
|
28
|
+
# @option options [Integer] :cost bcrypt cost factor, 4–31
|
|
29
|
+
# (default: +BCrypt::Engine::DEFAULT_COST+)
|
|
30
|
+
# @option options [String] :salt custom bcrypt salt string (normally
|
|
31
|
+
# auto-generated; must be in standard bcrypt format +$2b$NN$<22chars>+)
|
|
32
|
+
# @return [BcryptSHA256]
|
|
33
|
+
class BcryptSHA256 < Password
|
|
34
|
+
identifier :bcrypt_sha256
|
|
35
|
+
external "bcrypt", "~> 3.0"
|
|
36
|
+
register mcf: "bcrypt-sha256"
|
|
37
|
+
options :cost, :salt
|
|
38
|
+
|
|
39
|
+
FORMAT = /\A\$bcrypt-sha256\$v=2,t=2b,r=(\d+)\$([.\/A-Za-z0-9]{22})\$([.\/A-Za-z0-9]{31})\z/
|
|
40
|
+
private_constant :FORMAT
|
|
41
|
+
|
|
42
|
+
# Re-hashes +secret+ with the same salt and cost.
|
|
43
|
+
# @param secret [String] the plaintext password
|
|
44
|
+
# @return [BcryptSHA256]
|
|
45
|
+
def create_comparable(secret) = self.class.create(secret, salt: @bcrypt_salt)
|
|
46
|
+
|
|
47
|
+
# @return [Boolean] +true+ if the stored cost differs from the configured cost
|
|
48
|
+
def upgrade?
|
|
49
|
+
@cost != (config.cost || ::BCrypt::Engine::DEFAULT_COST)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def load(string)
|
|
55
|
+
m = FORMAT.match(string) or raise ArgumentError, "invalid bcrypt-sha256 hash: #{string.inspect}"
|
|
56
|
+
@cost = m[1].to_i
|
|
57
|
+
@bcrypt_salt = "$2b$#{m[1].rjust(2, "0")}$#{m[2]}"
|
|
58
|
+
string
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def create(secret)
|
|
62
|
+
cost = config.cost || ::BCrypt::Engine::DEFAULT_COST
|
|
63
|
+
@bcrypt_salt = config.salt || ::BCrypt::Engine.generate_salt(cost).sub(/\A\$2[a-z]\$/, "$2b$")
|
|
64
|
+
parts = @bcrypt_salt.split("$") # ["", "2b", "NN", "22chars"]
|
|
65
|
+
@cost = parts[2].to_i
|
|
66
|
+
salt22 = parts[3]
|
|
67
|
+
digest31 = bcrypt_digest(secret, @bcrypt_salt)
|
|
68
|
+
"$bcrypt-sha256$v=2,t=2b,r=#{@cost}$#{salt22}$#{digest31}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Computes the bcrypt digest of the HMAC-SHA256 pre-hash.
|
|
72
|
+
#
|
|
73
|
+
# 1. HMAC-SHA256(key: bcrypt_salt, msg: UTF-8 password) → 32 bytes
|
|
74
|
+
# 2. Standard base64-encode → 44-char ASCII string (with = padding)
|
|
75
|
+
# 3. BCrypt-hash that string with the same salt → take the last 31 chars
|
|
76
|
+
def bcrypt_digest(secret, bcrypt_salt)
|
|
77
|
+
hmac = OpenSSL::HMAC.digest("SHA256", bcrypt_salt, secret.encode("UTF-8"))
|
|
78
|
+
encoded = [hmac].pack("m0") # standard base64, no newlines, with = padding
|
|
79
|
+
::BCrypt::Engine.hash_secret(encoded, bcrypt_salt)[-31..]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Mixin that provides configuration, hash-loading, and hash-creation helpers.
|
|
4
|
+
#
|
|
5
|
+
# Extended into the {Passlib} module (so all methods are available as
|
|
6
|
+
# +Passlib.load+, +Passlib.create+, etc.)
|
|
7
|
+
module Passlib::Configuration::Context
|
|
8
|
+
# Returns the active {Configuration} for this object.
|
|
9
|
+
#
|
|
10
|
+
# The first call initializes the configuration, inheriting from
|
|
11
|
+
# {Passlib.configuration} unless this object *is* the {Passlib} module.
|
|
12
|
+
#
|
|
13
|
+
# @return [Configuration]
|
|
14
|
+
def configuration = @configuration ||= Passlib::Configuration.new(base_config)
|
|
15
|
+
|
|
16
|
+
# Replaces the current configuration options.
|
|
17
|
+
#
|
|
18
|
+
# @param value [Configuration, Hash] new configuration or option overrides
|
|
19
|
+
# @return [void]
|
|
20
|
+
# @see Configuration#set
|
|
21
|
+
def configuration=(value)
|
|
22
|
+
configuration.set(value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
alias config configuration
|
|
26
|
+
alias config= configuration=
|
|
27
|
+
|
|
28
|
+
# Yields the current configuration to a block for modification.
|
|
29
|
+
# @yieldparam config [Configuration] the current configuration
|
|
30
|
+
# @return [void]
|
|
31
|
+
def configure = yield(config)
|
|
32
|
+
alias setup configure
|
|
33
|
+
|
|
34
|
+
# Parses a stored hash string and returns a {Password} instance.
|
|
35
|
+
#
|
|
36
|
+
# Delegates to {Password.load} with the current configuration applied.
|
|
37
|
+
#
|
|
38
|
+
# @param payload [String] the stored password hash string
|
|
39
|
+
# @return [Password] a {Password} instance for the detected algorithm
|
|
40
|
+
# @raise [ArgumentError] if the hash format is not recognized
|
|
41
|
+
# @raise [Passlib::UnknownHashFormat] if the format is recognized but invalid
|
|
42
|
+
def load(payload, **) = Passlib::Password.load(payload, config, **)
|
|
43
|
+
|
|
44
|
+
# Creates a new password hash for the given secret using the current configuration.
|
|
45
|
+
#
|
|
46
|
+
# Delegates to {Password.create} with the current configuration applied.
|
|
47
|
+
# A preferred algorithm must be configured or a concrete subclass used.
|
|
48
|
+
#
|
|
49
|
+
# @param secret [String] the plaintext password to hash
|
|
50
|
+
# @return [Password] a new {Password} instance
|
|
51
|
+
def create(secret, **) = Passlib::Password.create(secret, config, **)
|
|
52
|
+
|
|
53
|
+
# Verifies a plaintext secret against a stored password hash.
|
|
54
|
+
#
|
|
55
|
+
# The argument order may be swapped: if the first argument is a
|
|
56
|
+
# {Password} instance and the second is a String, they are treated as
|
|
57
|
+
# +(hash, secret)+.
|
|
58
|
+
#
|
|
59
|
+
# Equivalent to +Passlib.load(hash).verify(secret)+.
|
|
60
|
+
#
|
|
61
|
+
# @param secret [String] the plaintext password to verify
|
|
62
|
+
# @param hash [String] the stored password hash string
|
|
63
|
+
# @return [Boolean] +true+ if the secret matches the hash, +false+ otherwise
|
|
64
|
+
# @raise [ArgumentError] if the hash format is not recognized
|
|
65
|
+
# @raise [Passlib::UnsupportedAlgorithm] if the algorithm is unavailable
|
|
66
|
+
# @see Password#verify
|
|
67
|
+
def verify(secret, hash)
|
|
68
|
+
secret, hash = hash, secret if secret.is_a? Passlib::Password and not hash.is_a? Passlib::Password
|
|
69
|
+
hash = load(hash) unless hash.is_a? Passlib::Password
|
|
70
|
+
hash.verify(secret)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
alias match? verify
|
|
74
|
+
alias valid_secret? verify
|
|
75
|
+
alias valid_password? verify
|
|
76
|
+
|
|
77
|
+
# Returns whether a stored hash should be re-hashed.
|
|
78
|
+
#
|
|
79
|
+
# Returns +false+ immediately when no preferred scheme is configured.
|
|
80
|
+
# Returns +true+ when the hash uses a different algorithm than the
|
|
81
|
+
# preferred scheme. When the algorithm already matches, delegates to
|
|
82
|
+
# {Password#upgrade?} to check whether the cost parameters are weaker
|
|
83
|
+
# than those in the current configuration.
|
|
84
|
+
#
|
|
85
|
+
# @param hash [String, Password] the stored password hash to evaluate
|
|
86
|
+
# @return [Boolean] +true+ if the hash should be upgraded, +false+ otherwise
|
|
87
|
+
def upgrade?(hash)
|
|
88
|
+
return false unless target = config.preferred_scheme
|
|
89
|
+
hash = load(hash) unless hash.is_a? Passlib::Password
|
|
90
|
+
return true unless hash.is_a? Passlib[target]
|
|
91
|
+
hash.upgrade?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Re-hashes a password if the stored hash is outdated.
|
|
95
|
+
#
|
|
96
|
+
# First verifies +secret+ against +hash+ (unless +verify: false+).
|
|
97
|
+
# Returns +nil+ if verification fails or no upgrade is needed.
|
|
98
|
+
# Otherwise creates and returns a new hash using the current
|
|
99
|
+
# configuration (algorithm and cost parameters).
|
|
100
|
+
#
|
|
101
|
+
# The argument order may be swapped: if the first argument is a
|
|
102
|
+
# {Password} instance and the second is a String, they are treated as
|
|
103
|
+
# +(hash, secret)+.
|
|
104
|
+
#
|
|
105
|
+
# @param secret [String, Password] the plaintext password (or hash if swapped)
|
|
106
|
+
# @param hash [String, Password] the stored password hash (or secret if swapped)
|
|
107
|
+
# @param verify [Boolean] when +false+, skip password verification before upgrading
|
|
108
|
+
# (default: +true+)
|
|
109
|
+
# @return [Password, nil] a new {Password} if an upgrade was performed, +nil+ otherwise
|
|
110
|
+
def upgrade(secret, hash, verify: true)
|
|
111
|
+
secret, hash = hash, secret if secret.is_a? Passlib::Password and not hash.is_a? Passlib::Password
|
|
112
|
+
hash = load(hash) unless hash.is_a? Passlib::Password
|
|
113
|
+
return if verify and not verify(secret, hash)
|
|
114
|
+
return unless upgrade?(hash)
|
|
115
|
+
create(secret)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def base_config = Passlib.configuration
|
|
121
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Passlib::Configuration::Passlib < Passlib::Configuration
|
|
4
|
+
option :preferred_schemes, %i[ yescrypt argon2 balloon scrypt bcrypt pbkdf2 ].freeze
|
|
5
|
+
|
|
6
|
+
def preferred_scheme=(scheme)
|
|
7
|
+
self.preferred_schemes = Array(scheme)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def preferred_scheme
|
|
11
|
+
return preferred_schemes.first if preferred_schemes.size == 1
|
|
12
|
+
preferred_schemes.detect { ::Passlib.available? it }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
alias default_scheme preferred_scheme
|
|
16
|
+
alias default_scheme= preferred_scheme=
|
|
17
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Passlib
|
|
4
|
+
# Thread-safe, chainable configuration object.
|
|
5
|
+
#
|
|
6
|
+
# Each algorithm class has its own configuration subclass (e.g.
|
|
7
|
+
# +Configuration::BCrypt+) whose options are auto-registered via the +options+
|
|
8
|
+
# DSL macro in the algorithm class body. A child configuration inherits
|
|
9
|
+
# defaults from a parent and can override individual options without mutating
|
|
10
|
+
# the parent.
|
|
11
|
+
#
|
|
12
|
+
# The global Passlib configuration is accessible via {Passlib.configuration}
|
|
13
|
+
# (aliased as {Passlib.config}).
|
|
14
|
+
#
|
|
15
|
+
# @example Reading a configuration value
|
|
16
|
+
# Passlib.configuration.bcrypt.cost # => nil (use algorithm default)
|
|
17
|
+
#
|
|
18
|
+
# @example Setting a global default
|
|
19
|
+
# Passlib.config.bcrypt.cost = 12
|
|
20
|
+
class Configuration
|
|
21
|
+
autoload :Context, "passlib/configuration/context"
|
|
22
|
+
autoload :Passlib, "passlib/configuration/passlib"
|
|
23
|
+
|
|
24
|
+
# @api private
|
|
25
|
+
def self.new(...) = self == Configuration ? Passlib.new(...) : super(...)
|
|
26
|
+
|
|
27
|
+
# @api private
|
|
28
|
+
def self.option(key, default = nil)
|
|
29
|
+
@available_options << key unless available_options.include? key
|
|
30
|
+
|
|
31
|
+
if const_defined?(:Generated, false)
|
|
32
|
+
mixin = const_get(:Generated, false)
|
|
33
|
+
else
|
|
34
|
+
mixin = Module.new
|
|
35
|
+
const_set(:Generated, mixin)
|
|
36
|
+
include mixin
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
mixin.module_eval do
|
|
40
|
+
if default.is_a? Configuration
|
|
41
|
+
define_method("#{key}=") { public_send(key).set(it) }
|
|
42
|
+
define_method(key) do
|
|
43
|
+
@options.compute_if_absent(key) do
|
|
44
|
+
result = default.class.new(parent ? parent.public_send(key) : default)
|
|
45
|
+
frozen? ? result.freeze : result
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
define_method("#{key}=") do |value|
|
|
50
|
+
raise FrozenError, "can't modify frozen configuration" if frozen?
|
|
51
|
+
@options[key] = value
|
|
52
|
+
end
|
|
53
|
+
define_method(key) do
|
|
54
|
+
@options.fetch(key) do
|
|
55
|
+
value = parent ? parent.public_send(key) : default
|
|
56
|
+
value = value.dup.freeze unless value.frozen?
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @api private
|
|
65
|
+
def self.options(*list)
|
|
66
|
+
list.each do |entry|
|
|
67
|
+
case entry
|
|
68
|
+
when Symbol then option(entry)
|
|
69
|
+
when Hash then entry.each { option(_1, _2) }
|
|
70
|
+
else raise ArgumentError, "invalid option: #{entry.inspect}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @api private
|
|
76
|
+
def self.available_options
|
|
77
|
+
inherited = superclass.respond_to?(:available_options) ? superclass.available_options : []
|
|
78
|
+
@available_options ||= []
|
|
79
|
+
inherited + @available_options
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Creates a new configuration instance.
|
|
83
|
+
#
|
|
84
|
+
# @param arguments [Array<Configuration, Hash, Proc, nil>] zero or more
|
|
85
|
+
# parent configurations (at most one) and/or option hashes to apply
|
|
86
|
+
# immediately, +nil+ entries are silently ignored
|
|
87
|
+
# @param block [Proc] if given, used as a lazy parent configuration
|
|
88
|
+
def initialize(*arguments, &block)
|
|
89
|
+
super()
|
|
90
|
+
arguments << block if block
|
|
91
|
+
|
|
92
|
+
@parent = nil
|
|
93
|
+
@options = Concurrent::Map.new
|
|
94
|
+
|
|
95
|
+
arguments.flatten.each do |argument|
|
|
96
|
+
case argument
|
|
97
|
+
when nil
|
|
98
|
+
# ignore
|
|
99
|
+
when Configuration, Proc
|
|
100
|
+
raise ArgumentError, "cannot set multiple parent configurations" if @parent
|
|
101
|
+
@parent = argument
|
|
102
|
+
else
|
|
103
|
+
apply(argument.to_hash)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The parent configuration from which defaults are inherited, or +nil+.
|
|
109
|
+
# @return [Configuration, nil]
|
|
110
|
+
def parent = @parent&.call
|
|
111
|
+
|
|
112
|
+
# Replaces all current options.
|
|
113
|
+
#
|
|
114
|
+
# If +options+ is a {Configuration} it becomes the new parent, otherwise
|
|
115
|
+
# the options map is cleared and the hash is applied via {#apply}.
|
|
116
|
+
#
|
|
117
|
+
# @param options [Configuration, #to_hash] replacement options or new parent
|
|
118
|
+
# @return [void]
|
|
119
|
+
def set(options)
|
|
120
|
+
@options.clear
|
|
121
|
+
options.is_a?(Configuration) ? @parent = options : apply(options)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Applies a hash of option values by calling the corresponding setters.
|
|
125
|
+
#
|
|
126
|
+
# @param options [#each] key/value pairs to apply
|
|
127
|
+
# @return [void]
|
|
128
|
+
def apply(options) = options.each { public_send(:"#{_1}=", _2) }
|
|
129
|
+
|
|
130
|
+
# Returns all explicitly-set options (and nested configurations) as a Hash.
|
|
131
|
+
#
|
|
132
|
+
# Nested {Configuration} values are converted recursively, empty nested
|
|
133
|
+
# hashes are omitted.
|
|
134
|
+
#
|
|
135
|
+
# @return [Hash{Symbol => Object}]
|
|
136
|
+
def to_h
|
|
137
|
+
return @to_h if instance_variable_defined?(:@to_h)
|
|
138
|
+
self.class.available_options.to_h do |key|
|
|
139
|
+
value = public_send(key)
|
|
140
|
+
if value.is_a? Configuration
|
|
141
|
+
value = value.to_h
|
|
142
|
+
value = nil if value.empty?
|
|
143
|
+
end
|
|
144
|
+
[key, value]
|
|
145
|
+
end.compact
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
alias to_hash to_h
|
|
149
|
+
|
|
150
|
+
# Returns a hash value for this configuration, based on the hash of the
|
|
151
|
+
# options hash returned by {#to_h}.
|
|
152
|
+
# @return [Integer]
|
|
153
|
+
def hash = to_h.hash
|
|
154
|
+
|
|
155
|
+
# Freezes this configuration and all nested option values in place.
|
|
156
|
+
# Changes to a parent configuration will no longer have any effect.
|
|
157
|
+
#
|
|
158
|
+
# @return [self]
|
|
159
|
+
def freeze
|
|
160
|
+
return self if frozen?
|
|
161
|
+
@parent = @parent&.call
|
|
162
|
+
self.class.available_options.each { @options[it] = public_send(it).freeze }
|
|
163
|
+
@to_h ||= to_h.freeze
|
|
164
|
+
super
|
|
165
|
+
rescue FrozenError
|
|
166
|
+
self
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Returns a human-readable representation of this configuration.
|
|
170
|
+
# @return [String]
|
|
171
|
+
def inspect = "#<Passlib::Configuration #{to_h.inspect}>"
|
|
172
|
+
|
|
173
|
+
# Pretty-print support for +pp+.
|
|
174
|
+
# @param pp [PP] the pretty-printer instance
|
|
175
|
+
# @return [void]
|
|
176
|
+
def pretty_print(pp)
|
|
177
|
+
pp.group(1, "#<Passlib::Configuration", ">") do
|
|
178
|
+
pp.breakable
|
|
179
|
+
pp.pp to_h
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
protected
|
|
184
|
+
|
|
185
|
+
def call = self
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Passlib
|
|
4
|
+
# An isolated configuration context for creating and verifying password hashes.
|
|
5
|
+
#
|
|
6
|
+
# A {Context} bundles a {Configuration} with the full {Configuration::Context}
|
|
7
|
+
# interface (+load+, +create+, +verify+, etc.), letting different parts of an
|
|
8
|
+
# application use different algorithms or settings independently without
|
|
9
|
+
# touching the global {Passlib} configuration.
|
|
10
|
+
#
|
|
11
|
+
# By default a new context inherits the global {Passlib} configuration as its
|
|
12
|
+
# parent, so any option not explicitly overridden falls back to the global
|
|
13
|
+
# setting. Pass another context or configuration as the first argument to
|
|
14
|
+
# inherit from that instead.
|
|
15
|
+
#
|
|
16
|
+
# @example Creating a context with a fixed algorithm
|
|
17
|
+
# context = Passlib::Context.new(preferred_scheme: :bcrypt)
|
|
18
|
+
# hash = context.create("hunter2")
|
|
19
|
+
# hash.class # => Passlib::BCrypt
|
|
20
|
+
# context.verify("hunter2", hash) # => true
|
|
21
|
+
#
|
|
22
|
+
# @example Inheriting from a parent context
|
|
23
|
+
# parent = Passlib::Context.new(preferred_scheme: :bcrypt)
|
|
24
|
+
# context = Passlib::Context.new(parent)
|
|
25
|
+
# context.create("hunter2").class # => Passlib::BCrypt
|
|
26
|
+
#
|
|
27
|
+
# @example Reconfiguring after creation
|
|
28
|
+
# context = Passlib::Context.new
|
|
29
|
+
# context.configure { |c| c.preferred_scheme = :bcrypt }
|
|
30
|
+
# context.create("hunter2").class # => Passlib::BCrypt
|
|
31
|
+
#
|
|
32
|
+
# @example Using as a drop-in for a subset of Passlib's interface
|
|
33
|
+
# context = Passlib::Context.new(preferred_scheme: :bcrypt)
|
|
34
|
+
# context.load("$2a$12$...").verify("hunter2") # => true
|
|
35
|
+
#
|
|
36
|
+
# @see Configuration::Context
|
|
37
|
+
class Context
|
|
38
|
+
include Passlib::Configuration::Context
|
|
39
|
+
|
|
40
|
+
# Creates a new context.
|
|
41
|
+
#
|
|
42
|
+
# @overload initialize()
|
|
43
|
+
# Creates a context inheriting defaults from {Passlib.configuration}.
|
|
44
|
+
#
|
|
45
|
+
# @overload initialize(parent)
|
|
46
|
+
# Creates a context inheriting defaults from another context or configuration.
|
|
47
|
+
# @param parent [Context, Configuration] parent to inherit defaults from
|
|
48
|
+
#
|
|
49
|
+
# @overload initialize(**options)
|
|
50
|
+
# Creates a context with the given options applied on top of the global defaults.
|
|
51
|
+
# @param options [Hash] option key/value pairs (e.g. +preferred_scheme: :bcrypt+)
|
|
52
|
+
#
|
|
53
|
+
# @overload initialize(parent, **options)
|
|
54
|
+
# Creates a context inheriting from a parent with additional option overrides.
|
|
55
|
+
# @param parent [Context, Configuration] parent to inherit defaults from
|
|
56
|
+
# @param options [Hash] option overrides applied on top of the parent
|
|
57
|
+
def initialize(input = nil, **options)
|
|
58
|
+
@base_config = nil
|
|
59
|
+
|
|
60
|
+
case input
|
|
61
|
+
when Passlib::Configuration::Context then @base_config = input.config
|
|
62
|
+
when Passlib::Configuration then @base_config = input
|
|
63
|
+
when Hash then @configuration = Passlib::Configuration.new(base_config, input)
|
|
64
|
+
when nil
|
|
65
|
+
else raise ArgumentError, "invalid context input: #{input.inspect}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
config.apply(options)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def instance_variables_to_inspect = [:@configuration]
|
|
74
|
+
def base_config = @base_config || super
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Passlib::Internal
|
|
4
|
+
class Dependency < Module
|
|
5
|
+
KNOWN ||= {}
|
|
6
|
+
|
|
7
|
+
def self.[](...) = new(...)
|
|
8
|
+
|
|
9
|
+
def self.new(dependency, *requirements, &)
|
|
10
|
+
KNOWN[dependency] ||= []
|
|
11
|
+
KNOWN[dependency].concat(requirements)
|
|
12
|
+
super(dependency, &)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(dependency, &)
|
|
16
|
+
super()
|
|
17
|
+
@dependency = dependency.to_s
|
|
18
|
+
require @dependency
|
|
19
|
+
define_method("#{dependency}_available?") { true }
|
|
20
|
+
rescue LoadError
|
|
21
|
+
begin
|
|
22
|
+
# Reraise so Exception#cause is set
|
|
23
|
+
raise Passlib::MissingDependency, "missing dependency: #{dependency}"
|
|
24
|
+
rescue Passlib::MissingDependency => exception
|
|
25
|
+
define_method("#{dependency}_available?") { false }
|
|
26
|
+
define_method("available?") { false }
|
|
27
|
+
define_method(:create) { |*| raise exception }
|
|
28
|
+
define_method(:load) { |*| raise exception }
|
|
29
|
+
end
|
|
30
|
+
ensure
|
|
31
|
+
private "#{dependency}_available?"
|
|
32
|
+
super(&) if block_given?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def inspect
|
|
36
|
+
"#{self.class.inspect}[#{@dependency.inspect}]"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|