net-imap 0.3.7 → 0.4.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/pages.yml +46 -0
- data/.github/workflows/test.yml +5 -12
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/README.md +15 -4
- data/Rakefile +0 -7
- data/docs/styles.css +0 -12
- data/lib/net/imap/authenticators.rb +26 -57
- data/lib/net/imap/command_data.rb +13 -6
- data/lib/net/imap/data_encoding.rb +14 -2
- data/lib/net/imap/deprecated_client_options.rb +139 -0
- data/lib/net/imap/errors.rb +20 -0
- data/lib/net/imap/fetch_data.rb +518 -0
- data/lib/net/imap/response_data.rb +116 -252
- data/lib/net/imap/response_parser/parser_utils.rb +240 -0
- data/lib/net/imap/response_parser.rb +1535 -1003
- data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
- data/lib/net/imap/sasl/authentication_exchange.rb +107 -0
- data/lib/net/imap/sasl/authenticators.rb +118 -0
- data/lib/net/imap/sasl/client_adapter.rb +72 -0
- data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +21 -11
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +180 -0
- data/lib/net/imap/sasl/external_authenticator.rb +83 -0
- data/lib/net/imap/sasl/gs2_header.rb +80 -0
- data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +25 -16
- data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
- data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
- data/lib/net/imap/sasl/protocol_adapters.rb +45 -0
- data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
- data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
- data/lib/net/imap/sasl/stringprep.rb +6 -66
- data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
- data/lib/net/imap/sasl.rb +144 -43
- data/lib/net/imap/sasl_adapter.rb +21 -0
- data/lib/net/imap/sequence_set.rb +67 -0
- data/lib/net/imap/stringprep/nameprep.rb +70 -0
- data/lib/net/imap/stringprep/saslprep.rb +69 -0
- data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
- data/lib/net/imap/stringprep/tables.rb +146 -0
- data/lib/net/imap/stringprep/trace.rb +85 -0
- data/lib/net/imap/stringprep.rb +159 -0
- data/lib/net/imap.rb +1055 -612
- data/net-imap.gemspec +4 -3
- data/rakelib/benchmarks.rake +91 -0
- data/rakelib/saslprep.rake +4 -4
- data/rakelib/string_prep_tables_generator.rb +82 -60
- metadata +31 -13
- data/benchmarks/stringprep.yml +0 -65
- data/benchmarks/table-regexps.yml +0 -39
- data/lib/net/imap/authenticators/digest_md5.rb +0 -115
- data/lib/net/imap/authenticators/plain.rb +0 -41
- data/lib/net/imap/authenticators/xoauth2.rb +0 -20
- data/lib/net/imap/sasl/saslprep.rb +0 -55
- data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
- 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,30 +17,39 @@
|
|
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
|
20
|
+
class Net::IMAP::SASL::LoginAuthenticator
|
21
|
+
STATE_USER = :USER
|
22
|
+
STATE_PASSWORD = :PASSWORD
|
23
|
+
STATE_DONE = :DONE
|
24
|
+
private_constant :STATE_USER, :STATE_PASSWORD, :STATE_DONE
|
25
|
+
|
26
|
+
def initialize(user = nil, pass = nil,
|
27
|
+
authcid: nil, username: nil,
|
28
|
+
password: nil, secret: nil,
|
29
|
+
warn_deprecation: true,
|
30
|
+
**)
|
31
|
+
if warn_deprecation
|
32
|
+
warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead."
|
33
|
+
end
|
34
|
+
@user = authcid || username || user
|
35
|
+
@password = password || secret || pass
|
36
|
+
@state = STATE_USER
|
37
|
+
end
|
38
|
+
|
39
|
+
def initial_response?; false end
|
40
|
+
|
21
41
|
def process(data)
|
22
42
|
case @state
|
23
43
|
when STATE_USER
|
24
44
|
@state = STATE_PASSWORD
|
25
45
|
return @user
|
26
46
|
when STATE_PASSWORD
|
47
|
+
@state = STATE_DONE
|
27
48
|
return @password
|
49
|
+
when STATE_DONE
|
50
|
+
raise ResponseParseError, data
|
28
51
|
end
|
29
52
|
end
|
30
53
|
|
31
|
-
|
32
|
-
|
33
|
-
STATE_USER = :USER
|
34
|
-
STATE_PASSWORD = :PASSWORD
|
35
|
-
|
36
|
-
def initialize(user, password, warn_deprecation: true, **_ignored)
|
37
|
-
if warn_deprecation
|
38
|
-
warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead."
|
39
|
-
end
|
40
|
-
@user = user
|
41
|
-
@password = password
|
42
|
-
@state = STATE_USER
|
43
|
-
end
|
44
|
-
|
45
|
-
Net::IMAP.add_authenticator "LOGIN", self
|
54
|
+
def done?; @state == STATE_DONE end
|
46
55
|
end
|
@@ -0,0 +1,199 @@
|
|
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. The
|
18
|
+
# identity form is application protocol specific. If not provided or
|
19
|
+
# left blank, the server derives an authorization identity from the
|
20
|
+
# authentication identity. The server is responsible for verifying the
|
21
|
+
# client's credentials and verifying that the identity it associates
|
22
|
+
# with the client's authentication identity is allowed to act as (or on
|
23
|
+
# behalf of) the authorization identity.
|
24
|
+
#
|
25
|
+
# For example, an administrator or superuser might take on another role:
|
26
|
+
#
|
27
|
+
# imap.authenticate "PLAIN", "root", passwd, authzid: "user"
|
28
|
+
#
|
29
|
+
attr_reader :authzid
|
30
|
+
alias username authzid
|
31
|
+
|
32
|
+
# Hostname to which the client connected. (optional)
|
33
|
+
attr_reader :host
|
34
|
+
|
35
|
+
# Service port to which the client connected. (optional)
|
36
|
+
attr_reader :port
|
37
|
+
|
38
|
+
# HTTP method. (optional)
|
39
|
+
attr_reader :mthd
|
40
|
+
|
41
|
+
# HTTP path data. (optional)
|
42
|
+
attr_reader :path
|
43
|
+
|
44
|
+
# HTTP post data. (optional)
|
45
|
+
attr_reader :post
|
46
|
+
|
47
|
+
# The query string. (optional)
|
48
|
+
attr_reader :qs
|
49
|
+
alias query qs
|
50
|
+
|
51
|
+
# Stores the most recent server "challenge". When authentication fails,
|
52
|
+
# this may hold information about the failure reason, as JSON.
|
53
|
+
attr_reader :last_server_response
|
54
|
+
|
55
|
+
# Creates an RFC7628[https://tools.ietf.org/html/rfc7628] OAuth
|
56
|
+
# authenticator.
|
57
|
+
#
|
58
|
+
# ==== Parameters
|
59
|
+
#
|
60
|
+
# See child classes for required parameter(s). The following parameters
|
61
|
+
# are all optional, but it is worth noting that <b>application protocols
|
62
|
+
# are allowed to require</b> #authzid (or other parameters, such as
|
63
|
+
# #host or #port) <b>as are specific server implementations</b>.
|
64
|
+
#
|
65
|
+
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
|
66
|
+
#
|
67
|
+
# _optional_ #username — An alias for #authzid.
|
68
|
+
#
|
69
|
+
# Note that, unlike some other authenticators, +username+ sets the
|
70
|
+
# _authorization_ identity and not the _authentication_ identity. The
|
71
|
+
# authentication identity is established for the client by the OAuth
|
72
|
+
# token.
|
73
|
+
#
|
74
|
+
# * _optional_ #host — Hostname to which the client connected.
|
75
|
+
# * _optional_ #port — Service port to which the client connected.
|
76
|
+
# * _optional_ #mthd — HTTP method
|
77
|
+
# * _optional_ #path — HTTP path data
|
78
|
+
# * _optional_ #post — HTTP post data
|
79
|
+
# * _optional_ #qs — HTTP query string
|
80
|
+
#
|
81
|
+
# _optional_ #query — An alias for #qs
|
82
|
+
#
|
83
|
+
# Any other keyword parameters are quietly ignored.
|
84
|
+
def initialize(authzid: nil, host: nil, port: nil,
|
85
|
+
username: nil, query: nil,
|
86
|
+
mthd: nil, path: nil, post: nil, qs: nil, **)
|
87
|
+
@authzid = authzid || username
|
88
|
+
@host = host
|
89
|
+
@port = port
|
90
|
+
@mthd = mthd
|
91
|
+
@path = path
|
92
|
+
@post = post
|
93
|
+
@qs = qs || query
|
94
|
+
@done = false
|
95
|
+
end
|
96
|
+
|
97
|
+
# The {RFC7628 §3.1}[https://www.rfc-editor.org/rfc/rfc7628#section-3.1]
|
98
|
+
# formatted response.
|
99
|
+
def initial_client_response
|
100
|
+
kv_pairs = {
|
101
|
+
host: host, port: port, mthd: mthd, path: path, post: post, qs: qs,
|
102
|
+
auth: authorization, # authorization is implemented by subclasses
|
103
|
+
}.compact
|
104
|
+
[gs2_header, *kv_pairs.map {|kv| kv.join("=") }, "\1"].join("\1")
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns initial_client_response the first time, then "<tt>^A</tt>".
|
108
|
+
def process(data)
|
109
|
+
@last_server_response = data
|
110
|
+
done? ? "\1" : initial_client_response
|
111
|
+
ensure
|
112
|
+
@done = true
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns true when the initial client response was sent.
|
116
|
+
#
|
117
|
+
# The authentication should not succeed unless this returns true, but it
|
118
|
+
# does *not* indicate success.
|
119
|
+
def done?; @done end
|
120
|
+
|
121
|
+
# Value of the HTTP Authorization header
|
122
|
+
#
|
123
|
+
# <b>Implemented by subclasses.</b>
|
124
|
+
def authorization; raise "must be implemented by subclass" end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
# Authenticator for the "+OAUTHBEARER+" SASL mechanism, specified in
|
129
|
+
# RFC7628[https://tools.ietf.org/html/rfc7628]. Authenticates using OAuth
|
130
|
+
# 2.0 bearer tokens, as described in
|
131
|
+
# RFC6750[https://tools.ietf.org/html/rfc6750]. Use via
|
132
|
+
# Net::IMAP#authenticate.
|
133
|
+
#
|
134
|
+
# RFC6750[https://tools.ietf.org/html/rfc6750] requires Transport Layer
|
135
|
+
# Security (TLS) to secure the protocol interaction between the client and
|
136
|
+
# the resource server. TLS _MUST_ be used for +OAUTHBEARER+ to protect
|
137
|
+
# the bearer token.
|
138
|
+
class OAuthBearerAuthenticator < OAuthAuthenticator
|
139
|
+
|
140
|
+
# An OAuth 2.0 bearer token. See {RFC-6750}[https://www.rfc-editor.org/rfc/rfc6750]
|
141
|
+
attr_reader :oauth2_token
|
142
|
+
alias secret oauth2_token
|
143
|
+
|
144
|
+
# :call-seq:
|
145
|
+
# new(oauth2_token, **options) -> authenticator
|
146
|
+
# new(authzid, oauth2_token, **options) -> authenticator
|
147
|
+
# new(oauth2_token:, **options) -> authenticator
|
148
|
+
#
|
149
|
+
# Creates an Authenticator for the "+OAUTHBEARER+" SASL mechanism.
|
150
|
+
#
|
151
|
+
# Called by Net::IMAP#authenticate and similar methods on other clients.
|
152
|
+
#
|
153
|
+
# ==== Parameters
|
154
|
+
#
|
155
|
+
# * #oauth2_token — An OAuth2 bearer token
|
156
|
+
#
|
157
|
+
# All other keyword parameters are passed to
|
158
|
+
# {super}[rdoc-ref:OAuthAuthenticator::new] (see OAuthAuthenticator).
|
159
|
+
# The most common ones are:
|
160
|
+
#
|
161
|
+
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
|
162
|
+
#
|
163
|
+
# _optional_ #username — An alias for #authzid.
|
164
|
+
#
|
165
|
+
# Note that, unlike some other authenticators, +username+ sets the
|
166
|
+
# _authorization_ identity and not the _authentication_ identity. The
|
167
|
+
# authentication identity is established for the client by
|
168
|
+
# #oauth2_token.
|
169
|
+
#
|
170
|
+
# * _optional_ #host — Hostname to which the client connected.
|
171
|
+
# * _optional_ #port — Service port to which the client connected.
|
172
|
+
#
|
173
|
+
# Although only oauth2_token is required by this mechanism, it is worth
|
174
|
+
# noting that <b><em>application protocols are allowed to
|
175
|
+
# require</em></b> #authzid (<em>or other parameters, such as</em> #host
|
176
|
+
# _or_ #port) <b><em>as are specific server implementations</em></b>.
|
177
|
+
def initialize(arg1 = nil, arg2 = nil,
|
178
|
+
oauth2_token: nil, secret: nil,
|
179
|
+
**args, &blk)
|
180
|
+
username, oauth2_token_arg = arg2.nil? ? [nil, arg1] : [arg1, arg2]
|
181
|
+
super(username: username, **args, &blk)
|
182
|
+
@oauth2_token = oauth2_token || secret || oauth2_token_arg or
|
183
|
+
raise ArgumentError, "missing oauth2_token"
|
184
|
+
end
|
185
|
+
|
186
|
+
# :call-seq:
|
187
|
+
# initial_response? -> true
|
188
|
+
#
|
189
|
+
# +OAUTHBEARER+ sends an initial client response.
|
190
|
+
def initial_response?; true end
|
191
|
+
|
192
|
+
# Value of the HTTP Authorization header
|
193
|
+
def authorization; "Bearer #{oauth2_token}" end
|
194
|
+
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,101 @@
|
|
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
|
+
alias authcid username
|
26
|
+
|
27
|
+
# A password or passphrase that matches the #username.
|
28
|
+
attr_reader :password
|
29
|
+
alias secret password
|
30
|
+
|
31
|
+
# Authorization identity: an identity to act as or on behalf of. The identity
|
32
|
+
# form is application protocol specific. If not provided or left blank, the
|
33
|
+
# server derives an authorization identity from the authentication identity.
|
34
|
+
# The server is responsible for verifying the client's credentials and
|
35
|
+
# verifying that the identity it associates with the client's authentication
|
36
|
+
# identity is allowed to act as (or on behalf of) the authorization identity.
|
37
|
+
#
|
38
|
+
# For example, an administrator or superuser might take on another role:
|
39
|
+
#
|
40
|
+
# imap.authenticate "PLAIN", "root", passwd, authzid: "user"
|
41
|
+
#
|
42
|
+
attr_reader :authzid
|
43
|
+
|
44
|
+
# :call-seq:
|
45
|
+
# new(username, password, authzid: nil, **) -> authenticator
|
46
|
+
# new(username:, password:, authzid: nil, **) -> authenticator
|
47
|
+
# new(authcid:, password:, authzid: nil, **) -> authenticator
|
48
|
+
#
|
49
|
+
# Creates an Authenticator for the "+PLAIN+" SASL mechanism.
|
50
|
+
#
|
51
|
+
# Called by Net::IMAP#authenticate and similar methods on other clients.
|
52
|
+
#
|
53
|
+
# ==== Parameters
|
54
|
+
#
|
55
|
+
# * #authcid ― Authentication identity that is associated with #password.
|
56
|
+
#
|
57
|
+
# #username ― An alias for #authcid.
|
58
|
+
#
|
59
|
+
# * #password ― A password or passphrase associated with the #authcid.
|
60
|
+
#
|
61
|
+
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
|
62
|
+
#
|
63
|
+
# When +authzid+ is not set, the server should derive the authorization
|
64
|
+
# identity from the authentication identity.
|
65
|
+
#
|
66
|
+
# Any other keyword parameters are quietly ignored.
|
67
|
+
def initialize(user = nil, pass = nil,
|
68
|
+
authcid: nil, secret: nil,
|
69
|
+
username: nil, password: nil, authzid: nil, **)
|
70
|
+
username ||= authcid || user or
|
71
|
+
raise ArgumentError, "missing username (authcid)"
|
72
|
+
password ||= secret || pass or raise ArgumentError, "missing password"
|
73
|
+
raise ArgumentError, "username contains NULL" if username.include?(NULL)
|
74
|
+
raise ArgumentError, "password contains NULL" if password.include?(NULL)
|
75
|
+
raise ArgumentError, "authzid contains NULL" if authzid&.include?(NULL)
|
76
|
+
@username = username
|
77
|
+
@password = password
|
78
|
+
@authzid = authzid
|
79
|
+
@done = false
|
80
|
+
end
|
81
|
+
|
82
|
+
# :call-seq:
|
83
|
+
# initial_response? -> true
|
84
|
+
#
|
85
|
+
# +PLAIN+ can send an initial client response.
|
86
|
+
def initial_response?; true end
|
87
|
+
|
88
|
+
# Responds with the client's credentials.
|
89
|
+
def process(data)
|
90
|
+
return "#@authzid\0#@username\0#@password"
|
91
|
+
ensure
|
92
|
+
@done = true
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns true when the initial client response was sent.
|
96
|
+
#
|
97
|
+
# The authentication should not succeed unless this returns true, but it
|
98
|
+
# does *not* indicate success.
|
99
|
+
def done?; @done end
|
100
|
+
|
101
|
+
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
|