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.
- checksums.yaml +4 -4
- data/.github/workflows/pages.yml +46 -0
- data/.github/workflows/test.yml +5 -12
- data/Gemfile +1 -0
- data/README.md +15 -4
- data/Rakefile +0 -7
- data/benchmarks/generate_parser_benchmarks +52 -0
- data/benchmarks/parser.yml +578 -0
- data/benchmarks/stringprep.yml +1 -1
- data/lib/net/imap/authenticators.rb +26 -57
- data/lib/net/imap/command_data.rb +13 -6
- data/lib/net/imap/deprecated_client_options.rb +139 -0
- data/lib/net/imap/errors.rb +0 -34
- data/lib/net/imap/response_data.rb +46 -41
- data/lib/net/imap/response_parser/parser_utils.rb +230 -0
- data/lib/net/imap/response_parser.rb +667 -649
- data/lib/net/imap/sasl/anonymous_authenticator.rb +68 -0
- data/lib/net/imap/sasl/authenticators.rb +112 -0
- data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +15 -9
- data/lib/net/imap/{authenticators/digest_md5.rb → sasl/digest_md5_authenticator.rb} +74 -21
- data/lib/net/imap/sasl/external_authenticator.rb +62 -0
- data/lib/net/imap/sasl/gs2_header.rb +80 -0
- data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +19 -14
- data/lib/net/imap/sasl/oauthbearer_authenticator.rb +164 -0
- data/lib/net/imap/sasl/plain_authenticator.rb +93 -0
- data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
- data/lib/net/imap/sasl/scram_authenticator.rb +278 -0
- data/lib/net/imap/sasl/stringprep.rb +6 -66
- data/lib/net/imap/sasl/xoauth2_authenticator.rb +88 -0
- data/lib/net/imap/sasl.rb +139 -44
- 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 +987 -690
- data/net-imap.gemspec +1 -1
- data/rakelib/saslprep.rake +4 -4
- data/rakelib/string_prep_tables_generator.rb +82 -60
- metadata +30 -13
- data/lib/net/imap/authenticators/plain.rb +0 -41
- data/lib/net/imap/authenticators/xoauth2.rb +0 -20
- data/lib/net/imap/response_reader.rb +0 -75
- 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,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
|
-
#
|
8
|
-
|
9
|
-
#
|
10
|
-
#
|
11
|
-
|
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
|