net-imap 0.3.9 → 0.4.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.

Potentially problematic release.


This version of net-imap might be problematic. Click here for more details.

Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +46 -0
  3. data/.github/workflows/test.yml +5 -12
  4. data/Gemfile +1 -0
  5. data/README.md +15 -4
  6. data/Rakefile +0 -7
  7. data/benchmarks/generate_parser_benchmarks +52 -0
  8. data/benchmarks/parser.yml +578 -0
  9. data/benchmarks/stringprep.yml +1 -1
  10. data/lib/net/imap/authenticators.rb +26 -57
  11. data/lib/net/imap/command_data.rb +13 -6
  12. data/lib/net/imap/deprecated_client_options.rb +139 -0
  13. data/lib/net/imap/errors.rb +0 -34
  14. data/lib/net/imap/response_data.rb +46 -41
  15. data/lib/net/imap/response_parser/parser_utils.rb +230 -0
  16. data/lib/net/imap/response_parser.rb +667 -649
  17. data/lib/net/imap/sasl/anonymous_authenticator.rb +68 -0
  18. data/lib/net/imap/sasl/authenticators.rb +112 -0
  19. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +15 -9
  20. data/lib/net/imap/{authenticators/digest_md5.rb → sasl/digest_md5_authenticator.rb} +74 -21
  21. data/lib/net/imap/sasl/external_authenticator.rb +62 -0
  22. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  23. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +19 -14
  24. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +164 -0
  25. data/lib/net/imap/sasl/plain_authenticator.rb +93 -0
  26. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  27. data/lib/net/imap/sasl/scram_authenticator.rb +278 -0
  28. data/lib/net/imap/sasl/stringprep.rb +6 -66
  29. data/lib/net/imap/sasl/xoauth2_authenticator.rb +88 -0
  30. data/lib/net/imap/sasl.rb +139 -44
  31. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  32. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  33. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  34. data/lib/net/imap/stringprep/tables.rb +146 -0
  35. data/lib/net/imap/stringprep/trace.rb +85 -0
  36. data/lib/net/imap/stringprep.rb +159 -0
  37. data/lib/net/imap.rb +987 -690
  38. data/net-imap.gemspec +1 -1
  39. data/rakelib/saslprep.rake +4 -4
  40. data/rakelib/string_prep_tables_generator.rb +82 -60
  41. metadata +30 -13
  42. data/lib/net/imap/authenticators/plain.rb +0 -41
  43. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  44. data/lib/net/imap/response_reader.rb +0 -75
  45. data/lib/net/imap/sasl/saslprep.rb +0 -55
  46. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  47. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gs2_header"
4
+
5
+ module Net
6
+ class IMAP < Protocol
7
+ module SASL
8
+
9
+ # Abstract base class for the SASL mechanisms defined in
10
+ # RFC7628[https://tools.ietf.org/html/rfc7628]:
11
+ # * OAUTHBEARER[rdoc-ref:OAuthBearerAuthenticator]
12
+ # (OAuthBearerAuthenticator)
13
+ # * OAUTH10A
14
+ class OAuthAuthenticator
15
+ include GS2Header
16
+
17
+ # Authorization identity: an identity to act as or on behalf of.
18
+ #
19
+ # If no explicit authorization identity is provided, it is usually
20
+ # derived from the authentication identity. For the OAuth-based
21
+ # mechanisms, the authentication identity is the identity established by
22
+ # the OAuth credential.
23
+ attr_reader :authzid
24
+
25
+ # Hostname to which the client connected.
26
+ attr_reader :host
27
+
28
+ # Service port to which the client connected.
29
+ attr_reader :port
30
+
31
+ # HTTP method. (optional)
32
+ attr_reader :mthd
33
+
34
+ # HTTP path data. (optional)
35
+ attr_reader :path
36
+
37
+ # HTTP post data. (optional)
38
+ attr_reader :post
39
+
40
+ # The query string. (optional)
41
+ attr_reader :qs
42
+
43
+ # Stores the most recent server "challenge". When authentication fails,
44
+ # this may hold information about the failure reason, as JSON.
45
+ attr_reader :last_server_response
46
+
47
+ # Creates an RFC7628[https://tools.ietf.org/html/rfc7628] OAuth
48
+ # authenticator.
49
+ #
50
+ # === Options
51
+ #
52
+ # See child classes for required configuration parameter(s). The
53
+ # following parameters are all optional, but protocols or servers may
54
+ # add requirements for #authzid, #host, #port, or any other parameter.
55
+ #
56
+ # * #authzid ― Identity to act as or on behalf of.
57
+ # * #host — Hostname to which the client connected.
58
+ # * #port — Service port to which the client connected.
59
+ # * #mthd — HTTP method
60
+ # * #path — HTTP path data
61
+ # * #post — HTTP post data
62
+ # * #qs — HTTP query string
63
+ #
64
+ def initialize(authzid: nil, host: nil, port: nil,
65
+ mthd: nil, path: nil, post: nil, qs: nil, **)
66
+ @authzid = authzid
67
+ @host = host
68
+ @port = port
69
+ @mthd = mthd
70
+ @path = path
71
+ @post = post
72
+ @qs = qs
73
+ @done = false
74
+ end
75
+
76
+ # The {RFC7628 §3.1}[https://www.rfc-editor.org/rfc/rfc7628#section-3.1]
77
+ # formatted response.
78
+ def initial_client_response
79
+ kv_pairs = {
80
+ host: host, port: port, mthd: mthd, path: path, post: post, qs: qs,
81
+ auth: authorization, # authorization is implemented by subclasses
82
+ }.compact
83
+ [gs2_header, *kv_pairs.map {|kv| kv.join("=") }, "\1"].join("\1")
84
+ end
85
+
86
+ # Returns initial_client_response the first time, then "<tt>^A</tt>".
87
+ def process(data)
88
+ @last_server_response = data
89
+ done? ? "\1" : initial_client_response
90
+ ensure
91
+ @done = true
92
+ end
93
+
94
+ # Returns true when the initial client response was sent.
95
+ #
96
+ # The authentication should not succeed unless this returns true, but it
97
+ # does *not* indicate success.
98
+ def done?; @done end
99
+
100
+ # Value of the HTTP Authorization header
101
+ #
102
+ # <b>Implemented by subclasses.</b>
103
+ def authorization; raise "must be implemented by subclass" end
104
+
105
+ end
106
+
107
+ # Authenticator for the "+OAUTHBEARER+" SASL mechanism, specified in
108
+ # RFC7628[https://tools.ietf.org/html/rfc7628]. Authenticates using OAuth
109
+ # 2.0 bearer tokens, as described in
110
+ # RFC6750[https://tools.ietf.org/html/rfc6750]. Use via
111
+ # Net::IMAP#authenticate.
112
+ #
113
+ # RFC6750[https://tools.ietf.org/html/rfc6750] requires Transport Layer
114
+ # Security (TLS) to secure the protocol interaction between the client and
115
+ # the resource server. TLS _MUST_ be used for +OAUTHBEARER+ to protect
116
+ # the bearer token.
117
+ class OAuthBearerAuthenticator < OAuthAuthenticator
118
+
119
+ # An OAuth2 bearer token, generally the access token.
120
+ attr_reader :oauth2_token
121
+
122
+ # :call-seq:
123
+ # new(oauth2_token, **options) -> authenticator
124
+ # new(oauth2_token:, **options) -> authenticator
125
+ #
126
+ # Creates an Authenticator for the "+OAUTHBEARER+" SASL mechanism.
127
+ #
128
+ # Called by Net::IMAP#authenticate and similar methods on other clients.
129
+ #
130
+ # === Options
131
+ #
132
+ # Only +oauth2_token+ is required by the mechanism, however protocols
133
+ # and servers may add requirements for #authzid, #host, #port, or any
134
+ # other parameter.
135
+ #
136
+ # * #oauth2_token — An OAuth2 bearer token or access token. *Required.*
137
+ # May be provided as either regular or keyword argument.
138
+ # * #authzid ― Identity to act as or on behalf of.
139
+ # * #host — Hostname to which the client connected.
140
+ # * #port — Service port to which the client connected.
141
+ # * See OAuthAuthenticator documentation for less common parameters.
142
+ #
143
+ def initialize(oauth2_token_arg = nil, oauth2_token: nil, **args, &blk)
144
+ super(**args, &blk) # handles authzid, host, port, etc
145
+ oauth2_token && oauth2_token_arg and
146
+ raise ArgumentError, "conflicting values for oauth2_token"
147
+ @oauth2_token = oauth2_token || oauth2_token_arg or
148
+ raise ArgumentError, "missing oauth2_token"
149
+ end
150
+
151
+ # :call-seq:
152
+ # initial_response? -> true
153
+ #
154
+ # +OAUTHBEARER+ sends an initial client response.
155
+ def initial_response?; true end
156
+
157
+ # Value of the HTTP Authorization header
158
+ def authorization; "Bearer #{oauth2_token}" end
159
+
160
+ end
161
+ end
162
+
163
+ end
164
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Authenticator for the "+PLAIN+" SASL mechanism, specified in
4
+ # RFC-4616[https://tools.ietf.org/html/rfc4616]. See Net::IMAP#authenticate.
5
+ #
6
+ # +PLAIN+ authentication sends the password in cleartext.
7
+ # RFC-3501[https://tools.ietf.org/html/rfc3501] encourages servers to disable
8
+ # cleartext authentication until after TLS has been negotiated.
9
+ # RFC-8314[https://tools.ietf.org/html/rfc8314] recommends TLS version 1.2 or
10
+ # greater be used for all traffic, and deprecate cleartext access ASAP. +PLAIN+
11
+ # can be secured by TLS encryption.
12
+ class Net::IMAP::SASL::PlainAuthenticator
13
+
14
+ NULL = -"\0".b
15
+ private_constant :NULL
16
+
17
+ # Authentication identity: the identity that matches the #password.
18
+ #
19
+ # RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
20
+ # "Authentication identity" is the generic term used by
21
+ # RFC-4422[https://tools.ietf.org/html/rfc4422].
22
+ # RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs abbreviate
23
+ # this to +authcid+.
24
+ attr_reader :username
25
+
26
+ # A password or passphrase that matches the #username.
27
+ attr_reader :password
28
+
29
+ # Authorization identity: an identity to act as or on behalf of. The identity
30
+ # form is application protocol specific. If not provided or left blank, the
31
+ # server derives an authorization identity from the authentication identity.
32
+ # The server is responsible for verifying the client's credentials and
33
+ # verifying that the identity it associates with the client's authentication
34
+ # identity is allowed to act as (or on behalf of) the authorization identity.
35
+ #
36
+ # For example, an administrator or superuser might take on another role:
37
+ #
38
+ # imap.authenticate "PLAIN", "root", passwd, authzid: "user"
39
+ #
40
+ attr_reader :authzid
41
+
42
+ # :call-seq:
43
+ # new(username, password, authzid: nil, **) -> authenticator
44
+ # new(username:, password:, authzid: nil, **) -> authenticator
45
+ #
46
+ # Creates an Authenticator for the "+PLAIN+" SASL mechanism.
47
+ #
48
+ # Called by Net::IMAP#authenticate and similar methods on other clients.
49
+ #
50
+ # === Parameters
51
+ #
52
+ # * #username ― Identity whose +password+ is used.
53
+ # * #password ― Password or passphrase associated with this username+.
54
+ # * #authzid ― Alternate identity to act as or on behalf of. Optional.
55
+ #
56
+ # See attribute documentation for more details.
57
+ def initialize(user = nil, pass = nil,
58
+ username: nil, password: nil, authzid: nil, **)
59
+ [username, user].compact.count == 1 or
60
+ raise ArgumentError, "conflicting values for username"
61
+ [password, pass].compact.count == 1 or
62
+ raise ArgumentError, "conflicting values for password"
63
+ username ||= user or raise ArgumentError, "missing username"
64
+ password ||= pass or raise ArgumentError, "missing password"
65
+ raise ArgumentError, "username contains NULL" if username.include?(NULL)
66
+ raise ArgumentError, "password contains NULL" if password.include?(NULL)
67
+ raise ArgumentError, "authzid contains NULL" if authzid&.include?(NULL)
68
+ @username = username
69
+ @password = password
70
+ @authzid = authzid
71
+ @done = false
72
+ end
73
+
74
+ # :call-seq:
75
+ # initial_response? -> true
76
+ #
77
+ # +PLAIN+ can send an initial client response.
78
+ def initial_response?; true end
79
+
80
+ # Responds with the client's credentials.
81
+ def process(data)
82
+ return "#@authzid\0#@username\0#@password"
83
+ ensure
84
+ @done = true
85
+ end
86
+
87
+ # Returns true when the initial client response was sent.
88
+ #
89
+ # The authentication should not succeed unless this returns true, but it
90
+ # does *not* indicate success.
91
+ def done?; @done end
92
+
93
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ module SASL
6
+
7
+ # For method descriptions,
8
+ # see {RFC5802 §2.2}[https://www.rfc-editor.org/rfc/rfc5802#section-2.2]
9
+ # and {RFC5802 §3}[https://www.rfc-editor.org/rfc/rfc5802#section-3].
10
+ module ScramAlgorithm
11
+ def Normalize(str) SASL.saslprep(str) end
12
+
13
+ def Hi(str, salt, iterations)
14
+ length = digest.digest_length
15
+ OpenSSL::KDF.pbkdf2_hmac(
16
+ str,
17
+ salt: salt,
18
+ iterations: iterations,
19
+ length: length,
20
+ hash: digest,
21
+ )
22
+ end
23
+
24
+ def H(str) digest.digest str end
25
+
26
+ def HMAC(key, data) OpenSSL::HMAC.digest(digest, key, data) end
27
+
28
+ def XOR(str1, str2)
29
+ str1.unpack("C*")
30
+ .zip(str2.unpack("C*"))
31
+ .map {|a, b| a ^ b }
32
+ .pack("C*")
33
+ end
34
+
35
+ def auth_message
36
+ [
37
+ client_first_message_bare,
38
+ server_first_message,
39
+ client_final_message_without_proof,
40
+ ]
41
+ .join(",")
42
+ end
43
+
44
+ def salted_password
45
+ Hi(Normalize(password), salt, iterations)
46
+ end
47
+
48
+ def client_key; HMAC(salted_password, "Client Key") end
49
+ def server_key; HMAC(salted_password, "Server Key") end
50
+ def stored_key; H(client_key) end
51
+ def client_signature; HMAC(stored_key, auth_message) end
52
+ def server_signature; HMAC(server_key, auth_message) end
53
+ def client_proof; XOR(client_key, client_signature) end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "securerandom"
5
+
6
+ require_relative "gs2_header"
7
+ require_relative "scram_algorithm"
8
+
9
+ module Net
10
+ class IMAP
11
+ module SASL
12
+
13
+ # Abstract base class for the "+SCRAM-*+" family of SASL mechanisms,
14
+ # defined in RFC5802[https://tools.ietf.org/html/rfc5802]. Use via
15
+ # Net::IMAP#authenticate.
16
+ #
17
+ # Directly supported:
18
+ # * +SCRAM-SHA-1+ --- ScramSHA1Authenticator
19
+ # * +SCRAM-SHA-256+ --- ScramSHA256Authenticator
20
+ #
21
+ # New +SCRAM-*+ mechanisms can easily be added for any hash algorithm
22
+ # supported by
23
+ # OpenSSL::Digest[https://ruby.github.io/openssl/OpenSSL/Digest.html].
24
+ # Subclasses need only set an appropriate +DIGEST_NAME+ constant.
25
+ #
26
+ # === SCRAM algorithm
27
+ #
28
+ # See the documentation and method definitions on ScramAlgorithm for an
29
+ # overview of the algorithm. The different mechanisms differ only by
30
+ # which hash function that is used (or by support for channel binding with
31
+ # +-PLUS+).
32
+ #
33
+ # See also the methods on GS2Header.
34
+ #
35
+ # ==== Server messages
36
+ #
37
+ # As server messages are received, they are validated and loaded into
38
+ # the various attributes, e.g: #snonce, #salt, #iterations, #verifier,
39
+ # #server_error, etc.
40
+ #
41
+ # Unlike many other SASL mechanisms, the +SCRAM-*+ family supports mutual
42
+ # authentication and can return server error data in the server messages.
43
+ # If #process raises an Error for the server-final-message, then
44
+ # server_error may contain error details.
45
+ #
46
+ # === TLS Channel binding
47
+ #
48
+ # <em>The <tt>SCRAM-*-PLUS</tt> mechanisms and channel binding are not
49
+ # supported yet.</em>
50
+ #
51
+ # === Caching SCRAM secrets
52
+ #
53
+ # <em>Caching of salted_password, client_key, stored_key, and server_key
54
+ # is not supported yet.</em>
55
+ #
56
+ class ScramAuthenticator
57
+ include GS2Header
58
+ include ScramAlgorithm
59
+
60
+ # :call-seq:
61
+ # new(username, password, **options) -> auth_ctx
62
+ # new(username:, password:, **options) -> auth_ctx
63
+ #
64
+ # Creates an authenticator for one of the "+SCRAM-*+" SASL mechanisms.
65
+ # Each subclass defines #digest to match a specific mechanism.
66
+ #
67
+ # Called by Net::IMAP#authenticate and similar methods on other clients.
68
+ #
69
+ # === Parameters
70
+ #
71
+ # * #username ― Identity whose #password is used. Aliased as #authcid.
72
+ # * #password ― Password or passphrase associated with this #username.
73
+ # * #authzid ― Alternate identity to act as or on behalf of. Optional.
74
+ # * #min_iterations - Overrides the default value (4096). Optional.
75
+ #
76
+ # See the documentation on the corresponding attributes for more.
77
+ def initialize(username_arg = nil, password_arg = nil,
78
+ username: nil, password: nil, authcid: nil, authzid: nil,
79
+ min_iterations: 4096, # see both RFC5802 and RFC7677
80
+ cnonce: nil, # must only be set in tests
81
+ **options)
82
+ @username = username || username_arg || authcid or
83
+ raise ArgumentError, "missing username (authcid)"
84
+ [username, username_arg, authcid].compact.count == 1 or
85
+ raise ArgumentError, "conflicting values for username (authcid)"
86
+ @password = password || password_arg or
87
+ raise ArgumentError, "missing password"
88
+ [password, password_arg].compact.count == 1 or
89
+ raise ArgumentError, "conflicting values for password"
90
+ @authzid = authzid
91
+
92
+ @min_iterations = Integer min_iterations
93
+ @min_iterations.positive? or
94
+ raise ArgumentError, "min_iterations must be positive"
95
+ @cnonce = cnonce || SecureRandom.base64(32)
96
+ end
97
+
98
+ # Authentication identity: the identity that matches the #password.
99
+ attr_reader :username
100
+ alias authcid username
101
+
102
+ # A password or passphrase that matches the #username.
103
+ attr_reader :password
104
+
105
+ # Authorization identity: an identity to act as or on behalf of. The
106
+ # identity form is application protocol specific. If not provided or
107
+ # left blank, the server derives an authorization identity from the
108
+ # authentication identity. For example, an administrator or superuser
109
+ # might take on another role:
110
+ #
111
+ # imap.authenticate "SCRAM-SHA-256", "root", passwd, authzid: "user"
112
+ #
113
+ # The server is responsible for verifying the client's credentials and
114
+ # verifying that the identity it associates with the client's
115
+ # authentication identity is allowed to act as (or on behalf of) the
116
+ # authorization identity.
117
+ attr_reader :authzid
118
+
119
+ # The minimal allowed iteration count. Lower #iterations will raise an
120
+ # Error.
121
+ attr_reader :min_iterations
122
+
123
+ # The client nonce, generated by SecureRandom
124
+ attr_reader :cnonce
125
+
126
+ # The server nonce, which must start with #cnonce
127
+ attr_reader :snonce
128
+
129
+ # The salt used by the server for this user
130
+ attr_reader :salt
131
+
132
+ # The iteration count for the selected hash function and user
133
+ attr_reader :iterations
134
+
135
+ # An error reported by the server during the \SASL exchange.
136
+ #
137
+ # Does not include errors reported by the protocol, e.g.
138
+ # Net::IMAP::NoResponseError.
139
+ attr_reader :server_error
140
+
141
+ # Returns a new OpenSSL::Digest object, set to the appropriate hash
142
+ # function for the chosen mechanism.
143
+ #
144
+ # <em>The class's +DIGEST_NAME+ constant must be set to the name of an
145
+ # algorithm supported by OpenSSL::Digest.</em>
146
+ def digest; OpenSSL::Digest.new self.class::DIGEST_NAME end
147
+
148
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
149
+ # +client-first-message+.
150
+ def initial_client_response
151
+ "#{gs2_header}#{client_first_message_bare}"
152
+ end
153
+
154
+ # responds to the server's challenges
155
+ def process(challenge)
156
+ case (@state ||= :initial_client_response)
157
+ when :initial_client_response
158
+ initial_client_response.tap { @state = :server_first_message }
159
+ when :server_first_message
160
+ recv_server_first_message challenge
161
+ final_message_with_proof.tap { @state = :server_final_message }
162
+ when :server_final_message
163
+ recv_server_final_message challenge
164
+ "".tap { @state = :done }
165
+ else
166
+ raise Error, "server sent after complete, %p" % [challenge]
167
+ end
168
+ rescue Exception => ex
169
+ @state = ex
170
+ raise
171
+ end
172
+
173
+ # Is the authentication exchange complete?
174
+ #
175
+ # If false, another server continuation is required.
176
+ def done?; @state == :done end
177
+
178
+ private
179
+
180
+ # Need to store this for auth_message
181
+ attr_reader :server_first_message
182
+
183
+ def format_message(hash) hash.map { _1.join("=") }.join(",") end
184
+
185
+ def recv_server_first_message(server_first_message)
186
+ @server_first_message = server_first_message
187
+ sparams = parse_challenge server_first_message
188
+ @snonce = sparams["r"] or
189
+ raise Error, "server did not send nonce"
190
+ @salt = sparams["s"]&.unpack1("m") or
191
+ raise Error, "server did not send salt"
192
+ @iterations = sparams["i"]&.then {|i| Integer i } or
193
+ raise Error, "server did not send iteration count"
194
+ min_iterations <= iterations or
195
+ raise Error, "too few iterations: %d" % [iterations]
196
+ mext = sparams["m"] and
197
+ raise Error, "mandatory extension: %p" % [mext]
198
+ snonce.start_with? cnonce or
199
+ raise Error, "invalid server nonce"
200
+ end
201
+
202
+ def recv_server_final_message(server_final_message)
203
+ sparams = parse_challenge server_final_message
204
+ @server_error = sparams["e"] and
205
+ raise Error, "server error: %s" % [server_error]
206
+ verifier = sparams["v"].unpack1("m") or
207
+ raise Error, "server did not send verifier"
208
+ verifier == server_signature or
209
+ raise Error, "server verify failed: %p != %p" % [
210
+ server_signature, verifier
211
+ ]
212
+ end
213
+
214
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
215
+ # +client-first-message-bare+.
216
+ def client_first_message_bare
217
+ @client_first_message_bare ||=
218
+ format_message(n: gs2_saslname_encode(SASL.saslprep(username)),
219
+ r: cnonce)
220
+ end
221
+
222
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
223
+ # +client-final-message+.
224
+ def final_message_with_proof
225
+ proof = [client_proof].pack("m0")
226
+ "#{client_final_message_without_proof},p=#{proof}"
227
+ end
228
+
229
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
230
+ # +client-final-message-without-proof+.
231
+ def client_final_message_without_proof
232
+ @client_final_message_without_proof ||=
233
+ format_message(c: [cbind_input].pack("m0"), # channel-binding
234
+ r: snonce) # nonce
235
+ end
236
+
237
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
238
+ # +cbind-input+.
239
+ #
240
+ # >>>
241
+ # *TODO:* implement channel binding, appending +cbind-data+ here.
242
+ alias cbind_input gs2_header
243
+
244
+ # RFC5802 specifies "that the order of attributes in client or server
245
+ # messages is fixed, with the exception of extension attributes", but
246
+ # this parses it simply as a hash, without respect to order. Note that
247
+ # repeated keys (violating the spec) will use the last value.
248
+ def parse_challenge(challenge)
249
+ challenge.split(/,/).to_h {|pair| pair.split(/=/, 2) }
250
+ rescue ArgumentError
251
+ raise Error, "unparsable challenge: %p" % [challenge]
252
+ end
253
+
254
+ end
255
+
256
+ # Authenticator for the "+SCRAM-SHA-1+" SASL mechanism, defined in
257
+ # RFC5802[https://tools.ietf.org/html/rfc5802].
258
+ #
259
+ # Uses the "SHA-1" digest algorithm from OpenSSL::Digest.
260
+ #
261
+ # See ScramAuthenticator.
262
+ class ScramSHA1Authenticator < ScramAuthenticator
263
+ DIGEST_NAME = "SHA1"
264
+ end
265
+
266
+ # Authenticator for the "+SCRAM-SHA-256+" SASL mechanism, defined in
267
+ # RFC7677[https://tools.ietf.org/html/rfc7677].
268
+ #
269
+ # Uses the "SHA-256" digest algorithm from OpenSSL::Digest.
270
+ #
271
+ # See ScramAuthenticator.
272
+ class ScramSHA256Authenticator < ScramAuthenticator
273
+ DIGEST_NAME = "SHA256"
274
+ end
275
+
276
+ end
277
+ end
278
+ end
@@ -1,72 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "stringprep_tables"
4
-
5
3
  module Net::IMAP::SASL
6
4
 
7
- # Regexps and utility methods for implementing stringprep profiles. The
8
- # \StringPrep algorithm is defined by
9
- # {RFC-3454}[https://www.rfc-editor.org/rfc/rfc3454.html]. Each
10
- # codepoint table defined in the RFC-3454 appendices is matched by a Regexp
11
- # defined in this module.
12
- #--
13
- # TODO: generic StringPrep mapping (not needed for SASLprep implementation)
14
- #++
15
- module StringPrep
16
-
17
- # Returns a Regexp matching the given +table+ name.
18
- def self.[](table)
19
- TABLE_REGEXPS.fetch(table)
20
- end
21
-
22
- module_function
23
-
24
- # Checks +string+ for any codepoint in +tables+. Raises a
25
- # ProhibitedCodepoint describing the first matching table.
26
- #
27
- # Also checks bidirectional characters, when <tt>bidi: true</tt>, which may
28
- # raise a BidiStringError.
29
- #
30
- # +profile+ is an optional string which will be added to any exception that
31
- # is raised (it does not affect behavior).
32
- def check_prohibited!(string, *tables, bidi: false, profile: nil)
33
- tables = TABLE_TITLES.keys.grep(/^C/) if tables.empty?
34
- tables |= %w[C.8] if bidi
35
- table = tables.find {|t| TABLE_REGEXPS[t].match?(string) }
36
- raise ProhibitedCodepoint.new(
37
- table, string: string, profile: nil
38
- ) if table
39
- check_bidi!(string, profile: profile) if bidi
40
- end
41
-
42
- # Checks that +string+ obeys all of the "Bidirectional Characters"
43
- # requirements in RFC-3454, §6:
44
- #
45
- # * The characters in \StringPrep\[\"C.8\"] MUST be prohibited
46
- # * If a string contains any RandALCat character, the string MUST NOT
47
- # contain any LCat character.
48
- # * If a string contains any RandALCat character, a RandALCat
49
- # character MUST be the first character of the string, and a
50
- # RandALCat character MUST be the last character of the string.
51
- #
52
- # This is usually combined with #check_prohibited!, so table "C.8" is only
53
- # checked when <tt>c_8: true</tt>.
54
- #
55
- # Raises either ProhibitedCodepoint or BidiStringError unless all
56
- # requirements are met. +profile+ is an optional string which will be
57
- # added to any exception that is raised (it does not affect behavior).
58
- def check_bidi!(string, c_8: false, profile: nil)
59
- check_prohibited!(string, "C.8", profile: profile) if c_8
60
- if BIDI_FAILS_REQ2.match?(string)
61
- raise BidiStringError.new(
62
- BIDI_DESC_REQ2, string: string, profile: profile,
63
- )
64
- elsif BIDI_FAILS_REQ3.match?(string)
65
- raise BidiStringError.new(
66
- BIDI_DESC_REQ3, string: string, profile: profile,
67
- )
68
- end
69
- end
5
+ # Alias for Net::IMAP::StringPrep::SASLprep.
6
+ SASLprep = Net::IMAP::StringPrep::SASLprep
7
+ StringPrep = Net::IMAP::StringPrep # :nodoc:
8
+ BidiStringError = Net::IMAP::StringPrep::BidiStringError # :nodoc:
9
+ ProhibitedCodepoint = Net::IMAP::StringPrep::ProhibitedCodepoint # :nodoc:
10
+ StringPrepError = Net::IMAP::StringPrep::StringPrepError # :nodoc:
70
11
 
71
- end
72
12
  end