net-imap 0.3.7 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) 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/response_data.rb +46 -41
  14. data/lib/net/imap/response_parser/parser_utils.rb +230 -0
  15. data/lib/net/imap/response_parser.rb +665 -627
  16. data/lib/net/imap/sasl/anonymous_authenticator.rb +68 -0
  17. data/lib/net/imap/sasl/authentication_exchange.rb +107 -0
  18. data/lib/net/imap/sasl/authenticators.rb +118 -0
  19. data/lib/net/imap/sasl/client_adapter.rb +72 -0
  20. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +15 -9
  21. data/lib/net/imap/{authenticators/digest_md5.rb → sasl/digest_md5_authenticator.rb} +74 -21
  22. data/lib/net/imap/sasl/external_authenticator.rb +62 -0
  23. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  24. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +19 -14
  25. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +164 -0
  26. data/lib/net/imap/sasl/plain_authenticator.rb +93 -0
  27. data/lib/net/imap/sasl/protocol_adapters.rb +45 -0
  28. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  29. data/lib/net/imap/sasl/scram_authenticator.rb +278 -0
  30. data/lib/net/imap/sasl/stringprep.rb +6 -66
  31. data/lib/net/imap/sasl/xoauth2_authenticator.rb +88 -0
  32. data/lib/net/imap/sasl.rb +144 -43
  33. data/lib/net/imap/sasl_adapter.rb +21 -0
  34. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  35. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  36. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  37. data/lib/net/imap/stringprep/tables.rb +146 -0
  38. data/lib/net/imap/stringprep/trace.rb +85 -0
  39. data/lib/net/imap/stringprep.rb +159 -0
  40. data/lib/net/imap.rb +976 -590
  41. data/net-imap.gemspec +1 -1
  42. data/rakelib/saslprep.rake +4 -4
  43. data/rakelib/string_prep_tables_generator.rb +82 -60
  44. metadata +30 -11
  45. data/lib/net/imap/authenticators/plain.rb +0 -41
  46. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  47. data/lib/net/imap/sasl/saslprep.rb +0 -55
  48. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  49. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP < Protocol
5
+ module SASL
6
+
7
+ # Originally defined for the GS2 mechanism family in
8
+ # RFC5801[https://tools.ietf.org/html/rfc5801],
9
+ # several different mechanisms start with a GS2 header:
10
+ # * +GS2-*+ --- RFC5801[https://tools.ietf.org/html/rfc5801]
11
+ # * +SCRAM-*+ --- RFC5802[https://tools.ietf.org/html/rfc5802]
12
+ # (ScramAuthenticator)
13
+ # * +SAML20+ --- RFC6595[https://tools.ietf.org/html/rfc6595]
14
+ # * +OPENID20+ --- RFC6616[https://tools.ietf.org/html/rfc6616]
15
+ # * +OAUTH10A+ --- RFC7628[https://tools.ietf.org/html/rfc7628]
16
+ # * +OAUTHBEARER+ --- RFC7628[https://tools.ietf.org/html/rfc7628]
17
+ # (OAuthBearerAuthenticator)
18
+ #
19
+ # Classes that include this module must implement +#authzid+.
20
+ module GS2Header
21
+ NO_NULL_CHARS = /\A[^\x00]+\z/u.freeze # :nodoc:
22
+
23
+ ##
24
+ # Matches {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
25
+ # +saslname+. The output from gs2_saslname_encode matches this Regexp.
26
+ RFC5801_SASLNAME = /\A(?:[^,=\x00]|=2C|=3D)+\z/u.freeze
27
+
28
+ # The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
29
+ # +gs2-header+, which prefixes the #initial_client_response.
30
+ #
31
+ # >>>
32
+ # <em>Note: the actual GS2 header includes an optional flag to
33
+ # indicate that the GSS mechanism is not "standard", but since all of
34
+ # the SASL mechanisms using GS2 are "standard", we don't include that
35
+ # flag. A class for a nonstandard GSSAPI mechanism should prefix with
36
+ # "+F,+".</em>
37
+ def gs2_header
38
+ "#{gs2_cb_flag},#{gs2_authzid},"
39
+ end
40
+
41
+ # The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
42
+ # +gs2-cb-flag+:
43
+ #
44
+ # "+n+":: The client doesn't support channel binding.
45
+ # "+y+":: The client does support channel binding
46
+ # but thinks the server does not.
47
+ # "+p+":: The client requires channel binding.
48
+ # The selected channel binding follows "+p=+".
49
+ #
50
+ # The default always returns "+n+". A mechanism that supports channel
51
+ # binding must override this method.
52
+ #
53
+ def gs2_cb_flag; "n" end
54
+
55
+ # The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
56
+ # +gs2-authzid+ header, when +#authzid+ is not empty.
57
+ #
58
+ # If +#authzid+ is empty or +nil+, an empty string is returned.
59
+ def gs2_authzid
60
+ return "" if authzid.nil? || authzid == ""
61
+ "a=#{gs2_saslname_encode(authzid)}"
62
+ end
63
+
64
+ module_function
65
+
66
+ # Encodes +str+ to match RFC5801_SASLNAME.
67
+ def gs2_saslname_encode(str)
68
+ str = str.encode("UTF-8")
69
+ # Regexp#match raises "invalid byte sequence" for invalid UTF-8
70
+ NO_NULL_CHARS.match str or
71
+ raise ArgumentError, "invalid saslname: %p" % [str]
72
+ str
73
+ .gsub(?=, "=3D")
74
+ .gsub(?,, "=2C")
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+ end
@@ -17,21 +17,11 @@
17
17
  # compatibility with existing servers. See
18
18
  # {draft-murchison-sasl-login}[https://www.iana.org/go/draft-murchison-sasl-login]
19
19
  # for both specification and deprecation.
20
- class Net::IMAP::LoginAuthenticator
21
- def process(data)
22
- case @state
23
- when STATE_USER
24
- @state = STATE_PASSWORD
25
- return @user
26
- when STATE_PASSWORD
27
- return @password
28
- end
29
- end
30
-
31
- private
32
-
20
+ class Net::IMAP::SASL::LoginAuthenticator
33
21
  STATE_USER = :USER
34
22
  STATE_PASSWORD = :PASSWORD
23
+ STATE_DONE = :DONE
24
+ private_constant :STATE_USER, :STATE_PASSWORD, :STATE_DONE
35
25
 
36
26
  def initialize(user, password, warn_deprecation: true, **_ignored)
37
27
  if warn_deprecation
@@ -42,5 +32,20 @@ class Net::IMAP::LoginAuthenticator
42
32
  @state = STATE_USER
43
33
  end
44
34
 
45
- Net::IMAP.add_authenticator "LOGIN", self
35
+ def initial_response?; false end
36
+
37
+ def process(data)
38
+ case @state
39
+ when STATE_USER
40
+ @state = STATE_PASSWORD
41
+ return @user
42
+ when STATE_PASSWORD
43
+ @state = STATE_DONE
44
+ return @password
45
+ when STATE_DONE
46
+ raise ResponseParseError, data
47
+ end
48
+ end
49
+
50
+ def done?; @state == STATE_DONE end
46
51
  end
@@ -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,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ module SASL
6
+
7
+ module ProtocolAdapters
8
+ # This API is experimental, and may change.
9
+ module Generic
10
+ def command_name; "AUTHENTICATE" end
11
+ def service; raise "Implement in subclass or module" end
12
+ def host; client.host end
13
+ def port; client.port end
14
+ def encode_ir(string) string.empty? ? "=" : encode(string) end
15
+ def encode(string) [string].pack("m0") end
16
+ def decode(string) string.unpack1("m0") end
17
+ def cancel_response; "*" end
18
+ end
19
+
20
+ # See RFC-3501 (IMAP4rev1), RFC-4959 (SASL-IR capability),
21
+ # and RFC-9051 (IMAP4rev2).
22
+ module IMAP
23
+ include Generic
24
+ def service; "imap" end
25
+ end
26
+
27
+ # See RFC-4954 (AUTH capability).
28
+ module SMTP
29
+ include Generic
30
+ def command_name; "AUTH" end
31
+ def service; "smtp" end
32
+ end
33
+
34
+ # See RFC-5034 (SASL capability).
35
+ module POP
36
+ include Generic
37
+ def command_name; "AUTH" end
38
+ def service; "pop" end
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
45
+ 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