55
66require_relative "gs2_header"
77require_relative "scram_algorithm"
8+ require_relative "scram_cache"
89
910module Net
1011 class IMAP
@@ -50,9 +51,22 @@ module SASL
5051 #
5152 # === Caching SCRAM secrets
5253 #
53- # <em>Caching of salted_password, client_key, stored_key, and server_key
54- # is not supported yet.</em>
54+ # The values for salted_password, client_key, and server_key are stored in
55+ # #cache, a SASL::ScramCache object. This object can be saved and re-used
56+ # across multiple authentication exchanges. When the #salt and #iteration
57+ # are unchanged, the stored keys will be reused. When they change, the
58+ # cache object is updated with the new values.
5559 #
60+ # **NOTE:** <em>The cache object must be handled with the same level of
61+ # caution as the password itself.</em> For example, it should always
62+ # be encrypted at rest.
63+ #
64+ # When +cache+ contains the client and server keys (or the salted
65+ # password), +password+ is optional. But authentication will fail if
66+ # #salt or #iterations change and #password hasn't been provided.
67+ #
68+ # Note that SASL::ScramCache is <em>not thread-safe</em>. Concurrent
69+ # authentications should dup or clone the cache object.
5670 class ScramAuthenticator
5771 include GS2Header
5872 include ScramAlgorithm
@@ -73,20 +87,32 @@ class ScramAuthenticator
7387 #
7488 # #username - An alias for #authcid.
7589 # * #password ― Password or passphrase associated with this #username.
90+ # * _optional_ #cache - A pre-existing SASL::ScramCache object.
7691 # * _optional_ #authzid ― Alternate identity to act as or on behalf of.
7792 # * _optional_ #min_iterations - Overrides the default value (4096).
7893 #
7994 # Any other keyword parameters are quietly ignored.
95+ #
96+ # === Caching salted credentials
97+ #
98+ # When +cache+ contains the client and server keys (or the salted
99+ # password), +password+ is optional.
100+ #
101+ # See ScramAuthenticator@Caching+SCRAM+secrets and SASL::ScramCache.
80102 def initialize ( username_arg = nil , password_arg = nil ,
81103 authcid : nil , username : nil ,
82104 authzid : nil ,
83105 password : nil , secret : nil ,
84106 min_iterations : 4096 , # see both RFC5802 and RFC7677
85107 cnonce : nil , # must only be set in tests
108+ cache : ScramCache . new ,
86109 **options )
87110 @username = username || username_arg || authcid or
88111 raise ArgumentError , "missing username (authcid)"
89- @password = password || secret || password_arg or
112+ cache => ScramCache
113+ @cache = cache
114+ @password = password || secret || password_arg
115+ @password || @cache . sufficient? or
90116 raise ArgumentError , "missing password"
91117 @authzid = authzid
92118
@@ -100,9 +126,6 @@ def initialize(username_arg = nil, password_arg = nil,
100126 @server_first_message = @snonce = @salt = @iterations = nil
101127 @server_error = nil
102128
103- # Memoized after @salt and @iterations have been sent.
104- @salted_password = @client_key = @server_key = nil
105-
106129 # These values are created and cached in response to server challenges
107130 @client_first_message_bare = nil
108131 @client_final_message_without_proof = nil
@@ -152,20 +175,34 @@ def initialize(username_arg = nil, password_arg = nil,
152175 # The iteration count for the selected hash function and user
153176 attr_reader :iterations
154177
178+ # Caches salted_password, client_key, and server_key, based on a
179+ # specific #salt and #iterations.
180+ #
181+ # See SASL::ScramCache and ScramAuthenticator@Caching+SCRAM+secrets.
182+ attr_reader :cache
183+
155184 # An error reported by the server during the \SASL exchange.
156185 #
157186 # Does not include errors reported by the protocol, e.g.
158187 # Net::IMAP::NoResponseError.
159188 attr_reader :server_error
160189
161- # Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
162- def salted_password = @salted_password ||= compute_salted { super }
190+ # Cached value for ScramAlgorithm#salted_password.
191+ # Requires +salt+ and +iterations+, from the server.
192+ def salted_password
193+ salted_cache_read ( :salted_password ) {
194+ password or raise Error , "invalid cache: salt or iteration changed"
195+ super
196+ }
197+ end
163198
164- # Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
165- def client_key = @client_key ||= compute_salted { super }
199+ # Cached value for ScramAlgorithm#client_key.
200+ # Requires +salt+ and +iterations+, from the server.
201+ def client_key = salted_cache_read ( :client_key ) { super }
166202
167- # Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
168- def server_key = @server_key ||= compute_salted { super }
203+ # Cached value for ScramAlgorithm#server_key.
204+ # Requires +salt+ and +iterations+, from the server.
205+ def server_key = salted_cache_read ( :server_key ) { super }
169206
170207 # Returns a new OpenSSL::Digest object, set to the appropriate hash
171208 # function for the chosen mechanism.
@@ -206,11 +243,8 @@ def done?; @state == :done end
206243
207244 private
208245
209- # Checks for +salt+ and +iterations+ before yielding
210- def compute_salted
211- salt in String or raise Error , "unknown salt"
212- iterations in Integer or raise Error , "unknown iterations"
213- yield
246+ def salted_cache_read ( name )
247+ cache . read ( name , salt :, iterations :) { yield }
214248 end
215249
216250 # Need to store this for auth_message
0 commit comments