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.
@@ -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