kiro-ruby-sasl 0.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.
- checksums.yaml +7 -0
- data/README.markdown +64 -0
- data/lib/sasl/anonymous.rb +14 -0
- data/lib/sasl/base.rb +126 -0
- data/lib/sasl/base64.rb +32 -0
- data/lib/sasl/digest_md5.rb +432 -0
- data/lib/sasl/gssapi.rb +92 -0
- data/lib/sasl/gssspnego.rb +38 -0
- data/lib/sasl/plain.rb +14 -0
- data/lib/sasl/socket.rb +88 -0
- data/lib/sasl.rb +5 -0
- data/spec/anonymous_spec.rb +19 -0
- data/spec/digest_md5_spec.rb +244 -0
- data/spec/mechanism_spec.rb +65 -0
- data/spec/plain_spec.rb +39 -0
- data/spec/socket_spec.rb +49 -0
- metadata +64 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 10e07b40466156392cccf8216fdb8ba1fe3a1a3a
|
4
|
+
data.tar.gz: 4c112a5fb128343ff32cfce1f6896762d7abfcad
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3579aa92c4f44e391aa21613c1d6398f1548b495e7784da8f9972b2803e0a3836844b02c342359662e86a30437b5004e2f9a2b9aba062fad223bb0822cf6c2d4
|
7
|
+
data.tar.gz: 6a7e9657313c56e2e5ce2166df38767d3bdfb800dfc3f601e459e3703d7b4234eacf43ea7c3b7ee511df9f6cc1bf46db1961420ab8eff761e98c927a25c42c01
|
data/README.markdown
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
Simple Authentication and Security Layer (RFC 4422) for Ruby
|
2
|
+
============================================================
|
3
|
+
|
4
|
+
Goal
|
5
|
+
----
|
6
|
+
|
7
|
+
Have a reusable library for client implementations that need to do
|
8
|
+
authentication over SASL, mainly targeted at Jabber/XMPP libraries.
|
9
|
+
New version was tested with AD LDAP.
|
10
|
+
|
11
|
+
All class carry just state, are thread-agnostic and must also work in
|
12
|
+
asynchronous environments.
|
13
|
+
|
14
|
+
Usage
|
15
|
+
-----
|
16
|
+
|
17
|
+
Derive from **SASL::Preferences** and overwrite the methods. Then,
|
18
|
+
create a mechanism instance:
|
19
|
+
|
20
|
+
# mechanisms => ['DIGEST-MD5', 'PLAIN']
|
21
|
+
sasl = SASL.new(mechanisms, my_preferences)
|
22
|
+
content_to_send = sasl.start
|
23
|
+
# [...]
|
24
|
+
content_to_send = sasl.challenge(received_content)
|
25
|
+
|
26
|
+
LDAP example (without secure_layer):
|
27
|
+
|
28
|
+
opts = {:digest_uri =>"ldap/myhost.mydomain.com",
|
29
|
+
:username => "username",
|
30
|
+
:password => "password",
|
31
|
+
}
|
32
|
+
sasl = SASL.new_mechanism('DIGEST-MD5', SASL::Preferences.new(opts))
|
33
|
+
sasl.start
|
34
|
+
...get cred...
|
35
|
+
response = sasl.receive("challenge", cred)
|
36
|
+
...answer response[1]...
|
37
|
+
...get result...
|
38
|
+
response = sasl.receive("success")
|
39
|
+
|
40
|
+
LDAP example (with secure_layer):
|
41
|
+
|
42
|
+
opts = {:digest_uri =>"ldap/myhost.mydomain.com",
|
43
|
+
:username => "username",
|
44
|
+
:password => "password",
|
45
|
+
:secure_layer => true,
|
46
|
+
:confidentiality => true, #optional
|
47
|
+
:cipher => "rc4", #optional
|
48
|
+
}
|
49
|
+
sasl = SASL.new_mechanism('DIGEST-MD5', SASL::Preferences.new(opts))
|
50
|
+
sasl.start
|
51
|
+
...get cred...
|
52
|
+
response = sasl.receive("challenge", cred)
|
53
|
+
...answer response[1]...
|
54
|
+
...get result...
|
55
|
+
response = sasl.receive("success")
|
56
|
+
securelayer_wrapper = response[1]
|
57
|
+
secured_io = securelayer_wrapper.call(io)
|
58
|
+
...
|
59
|
+
|
60
|
+
secure_io is limited to some basic methods (read, write and close). SASL::Buffering
|
61
|
+
can be used to add extra methods (like getc):
|
62
|
+
|
63
|
+
secured_io.extend(SASL::Buffering)
|
64
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SASL
|
2
|
+
##
|
3
|
+
# SASL ANONYMOUS where you only send a username that may not get
|
4
|
+
# evaluated by the server.
|
5
|
+
#
|
6
|
+
# RFC 4505:
|
7
|
+
# http://tools.ietf.org/html/rfc4505
|
8
|
+
class Anonymous < Mechanism
|
9
|
+
def start
|
10
|
+
@state = nil
|
11
|
+
['auth', preferences.username.to_s]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/sasl/base.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
##
|
2
|
+
# RFC 4422:
|
3
|
+
# http://tools.ietf.org/html/rfc4422
|
4
|
+
module SASL
|
5
|
+
##
|
6
|
+
# You must derive from class Preferences and overwrite methods you
|
7
|
+
# want to implement.
|
8
|
+
class Preferences
|
9
|
+
attr_reader :config
|
10
|
+
# key in config hash
|
11
|
+
# authzid: Authorization identitiy ('username@domain' in XMPP)
|
12
|
+
# realm: Realm ('domain' in XMPP)
|
13
|
+
# digest-uri: : serv-type/serv-name | serv-type/host/serv-name ('xmpp/domain' in XMPP)
|
14
|
+
# username
|
15
|
+
# has_password?
|
16
|
+
# allow_plaintext?
|
17
|
+
# password
|
18
|
+
# want_anonymous?
|
19
|
+
|
20
|
+
def initialize (config)
|
21
|
+
@config = {:has_password? => false, :allow_plaintext? => false, :want_anonymous? => false}.merge(config.dup)
|
22
|
+
end
|
23
|
+
def method_missing(sym, *args, &block)
|
24
|
+
@config.send "[]", sym, &block
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Will be raised by SASL.new_mechanism if mechanism passed to the
|
30
|
+
# constructor is not known.
|
31
|
+
class UnknownMechanism < RuntimeError
|
32
|
+
def initialize(mechanism)
|
33
|
+
@mechanism = mechanism
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"Unknown mechanism: #{@mechanism.inspect}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def SASL.new(mechanisms, preferences)
|
42
|
+
best_mechanism = if preferences.want_anonymous? && mechanisms.include?('ANONYMOUS')
|
43
|
+
'ANONYMOUS'
|
44
|
+
elsif preferences.has_password?
|
45
|
+
if mechanisms.include?('DIGEST-MD5')
|
46
|
+
'DIGEST-MD5'
|
47
|
+
elsif preferences.allow_plaintext?
|
48
|
+
'PLAIN'
|
49
|
+
else
|
50
|
+
raise UnknownMechanism.new(mechanisms)
|
51
|
+
end
|
52
|
+
else
|
53
|
+
raise UnknownMechanism.new(mechanisms)
|
54
|
+
end
|
55
|
+
new_mechanism(best_mechanism, preferences)
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Create a SASL Mechanism for the named mechanism
|
60
|
+
#
|
61
|
+
# mechanism:: [String] mechanism name
|
62
|
+
def SASL.new_mechanism(mechanism, preferences)
|
63
|
+
mechanism_class = case mechanism
|
64
|
+
when 'DIGEST-MD5'
|
65
|
+
DigestMD5
|
66
|
+
when 'PLAIN'
|
67
|
+
Plain
|
68
|
+
when 'ANONYMOUS'
|
69
|
+
Anonymous
|
70
|
+
when 'GSS-SPNEGO'
|
71
|
+
GssSpnego
|
72
|
+
when 'GSSAPI'
|
73
|
+
GssApi
|
74
|
+
else
|
75
|
+
raise UnknownMechanism.new(mechanism)
|
76
|
+
end
|
77
|
+
mechanism_class.new(mechanism, preferences)
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
class AbstractMethod < Exception # :nodoc:
|
82
|
+
def to_s
|
83
|
+
"Abstract method is not implemented"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Common functions for mechanisms
|
89
|
+
#
|
90
|
+
# Mechanisms implement handling of methods start and receive. They
|
91
|
+
# return: [message_name, content] or nil where message_name is
|
92
|
+
# either 'auth' or 'response' and content is either a string which
|
93
|
+
# may transmitted encoded as Base64 or nil.
|
94
|
+
class Mechanism
|
95
|
+
attr_reader :mechanism
|
96
|
+
attr_reader :preferences
|
97
|
+
|
98
|
+
def initialize(mechanism, preferences)
|
99
|
+
@mechanism = mechanism
|
100
|
+
@preferences = preferences
|
101
|
+
@state = nil
|
102
|
+
end
|
103
|
+
|
104
|
+
def success?
|
105
|
+
@state == :success
|
106
|
+
end
|
107
|
+
def failure?
|
108
|
+
@state == :failure
|
109
|
+
end
|
110
|
+
|
111
|
+
def start
|
112
|
+
raise AbstractMethod
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
def receive(message_name, content)
|
117
|
+
case message_name
|
118
|
+
when 'success'
|
119
|
+
@state = :success
|
120
|
+
when 'failure'
|
121
|
+
@state = :failure
|
122
|
+
end
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/lib/sasl/base64.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# =XMPP4R - XMPP Library for Ruby
|
2
|
+
# License:: Ruby's license (see the LICENSE file) or GNU GPL, at your option.
|
3
|
+
# Website::http://home.gna.org/xmpp4r/
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'base64'
|
7
|
+
rescue LoadError
|
8
|
+
##
|
9
|
+
# Ruby 1.9 has dropped the Base64 module,
|
10
|
+
# this is a replacement
|
11
|
+
#
|
12
|
+
# We could replace all call by Array#pack('m')
|
13
|
+
# and String#unpack('m'), but this module
|
14
|
+
# improves readability.
|
15
|
+
module Base64
|
16
|
+
##
|
17
|
+
# Encode a String
|
18
|
+
# data:: [String] Binary
|
19
|
+
# result:: [String] Binary in Base64
|
20
|
+
def self.encode64(data)
|
21
|
+
[data].pack('m')
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Decode a Base64-encoded String
|
26
|
+
# data64:: [String] Binary in Base64
|
27
|
+
# result:: [String] Binary
|
28
|
+
def self.decode64(data64)
|
29
|
+
data64.unpack('m').first
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,432 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module SASL
|
4
|
+
##
|
5
|
+
# RFC 2831:
|
6
|
+
# http://tools.ietf.org/html/rfc2831
|
7
|
+
class DigestMD5 < Mechanism
|
8
|
+
begin
|
9
|
+
require 'openssl'
|
10
|
+
##
|
11
|
+
# Set to +true+ if OpenSSL is available and LDAPS is supported.
|
12
|
+
HasOpenSSL = true
|
13
|
+
rescue LoadError
|
14
|
+
# :stopdoc:
|
15
|
+
HasOpenSSL = false
|
16
|
+
# :startdoc:
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_writer :cnonce
|
20
|
+
|
21
|
+
def initialize(*a)
|
22
|
+
super
|
23
|
+
@nonce_count = 0
|
24
|
+
preferences.config[:secure_layer]=false if preferences.config[:secure_layer]==nil
|
25
|
+
preferences.config[:confidentiality]=preferences.config[:secure_layer] if preferences.config[:confidentiality]==nil
|
26
|
+
preferences.config[:cipher]="rc4" if preferences.config[:confidentiality] and not preferences.config[:cipher]
|
27
|
+
|
28
|
+
if preferences.secure_layer and not HasOpenSSL
|
29
|
+
raise ":secure_layer in #{self.class} depends on Openssl"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def start
|
34
|
+
@state = nil
|
35
|
+
unless defined? @nonce
|
36
|
+
['auth', nil]
|
37
|
+
else
|
38
|
+
# reauthentication
|
39
|
+
receive('challenge', '')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def receive(message_name, content)
|
44
|
+
case message_name
|
45
|
+
when 'challenge'
|
46
|
+
c = decode_challenge(content)
|
47
|
+
|
48
|
+
unless c['rspauth']
|
49
|
+
response = {}
|
50
|
+
if defined?(@nonce) && response['nonce'].nil?
|
51
|
+
# Could be reauth
|
52
|
+
else
|
53
|
+
# No reauth:
|
54
|
+
@nonce_count = 0
|
55
|
+
end
|
56
|
+
@nonce ||= c['nonce']
|
57
|
+
response['username'] = preferences.username
|
58
|
+
response['realm'] = c['realm'] || preferences.realm
|
59
|
+
response['nonce'] = @nonce
|
60
|
+
@cnonce = generate_nonce unless defined? @cnonce
|
61
|
+
response['cnonce'] = @cnonce
|
62
|
+
@nc = next_nc
|
63
|
+
response['nc'] = @nc
|
64
|
+
@qop="auth"
|
65
|
+
if c['qop']
|
66
|
+
c_qop = c['qop'].split(",")
|
67
|
+
else
|
68
|
+
c_qop = []
|
69
|
+
end
|
70
|
+
if preferences.secure_layer and preferences.confidentiality and c_qop.include?("auth-conf")
|
71
|
+
response['qop'] = "auth-conf"
|
72
|
+
response['cipher'] = preferences.config[:cipher]
|
73
|
+
elsif preferences.secure_layer and not preferences.confidentiality and c_qop.include?("auth-int")
|
74
|
+
response['qop'] = "auth-int"
|
75
|
+
else
|
76
|
+
response['qop'] = 'auth'
|
77
|
+
end
|
78
|
+
@cipher=response['cipher']
|
79
|
+
@qop=response['qop']
|
80
|
+
response['digest-uri'] = preferences.digest_uri
|
81
|
+
response['charset'] = 'utf-8'
|
82
|
+
@algorithm = c['algorithm'] || "md5"
|
83
|
+
response['response'] = response_value(@algorithm, response['nonce'], response['nc'], response['cnonce'], response['qop'], response['realm'])
|
84
|
+
result=['response', encode_response(response)]
|
85
|
+
else
|
86
|
+
rspauth_expected = response_value(@algorithm, @nonce, @nc, @cnonce, @qop, '')
|
87
|
+
#p :rspauth_received=>c['rspauth'], :rspauth_expected=>rspauth_expected
|
88
|
+
if c['rspauth'] == rspauth_expected
|
89
|
+
result=['response', nil]
|
90
|
+
else
|
91
|
+
# Bogus server?
|
92
|
+
@state = :failure
|
93
|
+
result=['failure', nil]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
when 'success'
|
97
|
+
result=super
|
98
|
+
if preferences.secure_layer
|
99
|
+
securelayer_wrapper = proc {|io| DigestMD5SecureLayer.new(io, @ha1, @qop=="auth-conf", @cipher) }
|
100
|
+
result=['securelayer_wrapper', securelayer_wrapper]
|
101
|
+
end
|
102
|
+
else
|
103
|
+
# No challenge? Might be success or failure
|
104
|
+
result=super
|
105
|
+
end
|
106
|
+
result
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def decode_challenge(text)
|
112
|
+
challenge = {}
|
113
|
+
|
114
|
+
state = :key
|
115
|
+
key = ''
|
116
|
+
value = ''
|
117
|
+
|
118
|
+
text.scan(/./) do |ch|
|
119
|
+
if state == :key
|
120
|
+
if ch == '='
|
121
|
+
state = :value
|
122
|
+
elsif ch =~ /\S/
|
123
|
+
key += ch
|
124
|
+
end
|
125
|
+
|
126
|
+
elsif state == :value
|
127
|
+
if ch == ','
|
128
|
+
challenge[key] = value
|
129
|
+
key = ''
|
130
|
+
value = ''
|
131
|
+
state = :key
|
132
|
+
elsif ch == '"' and value == ''
|
133
|
+
state = :quote
|
134
|
+
else
|
135
|
+
value += ch
|
136
|
+
end
|
137
|
+
|
138
|
+
elsif state == :quote
|
139
|
+
if ch == '"'
|
140
|
+
state = :value
|
141
|
+
else
|
142
|
+
value += ch
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
challenge[key] = value unless key == ''
|
147
|
+
|
148
|
+
#p :decode_challenge => challenge
|
149
|
+
challenge
|
150
|
+
end
|
151
|
+
|
152
|
+
def encode_response(response)
|
153
|
+
#p :encode_response => response
|
154
|
+
response.collect do |k,v|
|
155
|
+
if ['username', 'cnonce', 'nonce', 'digest-uri', 'authzid','realm','qop'].include? k
|
156
|
+
v.sub!('\\', '\\\\')
|
157
|
+
v.sub!('"', '\\"')
|
158
|
+
"#{k}=\"#{v}\""
|
159
|
+
else
|
160
|
+
"#{k}=#{v}"
|
161
|
+
end
|
162
|
+
end.join(',')
|
163
|
+
end
|
164
|
+
|
165
|
+
def generate_nonce
|
166
|
+
nonce = ''
|
167
|
+
while nonce.length < 32
|
168
|
+
c = rand(128).chr
|
169
|
+
nonce += c if c =~ /^[a-zA-Z0-9]$/
|
170
|
+
end
|
171
|
+
nonce
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# Function from RFC2831
|
176
|
+
def self.h(s); Digest::MD5.digest(s); end
|
177
|
+
def h(s) self.class.h(s); end
|
178
|
+
##
|
179
|
+
# Function from RFC2831
|
180
|
+
def self.hh(s); Digest::MD5.hexdigest(s); end
|
181
|
+
def hh(s) self.class.hh(s); end
|
182
|
+
|
183
|
+
##
|
184
|
+
# Calculate the value for the response field
|
185
|
+
def response_value(algorithm, nonce, nc, cnonce, qop, realm, a2_prefix='AUTHENTICATE')
|
186
|
+
#p :response_value => {:nonce=>nonce,
|
187
|
+
# :cnonce=>cnonce,
|
188
|
+
# :qop=>qop,
|
189
|
+
# :username=>preferences.username,
|
190
|
+
# :realm=>preferences.realm,
|
191
|
+
# :password=>preferences.password,
|
192
|
+
# :authzid=>preferences.authzid}
|
193
|
+
a1 = "#{preferences.username}:#{realm}:#{preferences.password}"
|
194
|
+
if algorithm.downcase == "md5-sess"
|
195
|
+
a1 = "#{h(a1)}:#{nonce}:#{cnonce}"
|
196
|
+
end
|
197
|
+
|
198
|
+
if preferences.authzid
|
199
|
+
a1 += ":#{preferences.authzid}"
|
200
|
+
end
|
201
|
+
@ha1=h(a1)
|
202
|
+
|
203
|
+
a2="#{a2_prefix}:#{preferences.digest_uri}"
|
204
|
+
|
205
|
+
qop = "missing" if not qop
|
206
|
+
|
207
|
+
case qop.downcase
|
208
|
+
when "auth-int", "auth-conf"
|
209
|
+
a2 = "#{a2}:00000000000000000000000000000000"
|
210
|
+
end
|
211
|
+
|
212
|
+
case qop.downcase
|
213
|
+
when "auth", "auth-int", "auth-conf"
|
214
|
+
hh("#{hh(a1)}:#{nonce}:#{nc}:#{cnonce}:#{qop}:#{hh(a2)}")
|
215
|
+
when "missing"
|
216
|
+
hh("#{hh(a1)}:#{nonce}:#{hh(a2)}")
|
217
|
+
else
|
218
|
+
raise "Unknown qop=#{qop}"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def next_nc
|
223
|
+
@nonce_count += 1
|
224
|
+
s = @nonce_count.to_s
|
225
|
+
s = "0#{s}" while s.length < 8
|
226
|
+
s
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
class DigestMD5SecureLayer < SecureLayer
|
231
|
+
class DigestMD5SecureLayerError < StandardError; end
|
232
|
+
|
233
|
+
DIGEST_SESSKEY_MAGIC_CONS_C2S = "Digest session key to client-to-server signing key magic constant"
|
234
|
+
DIGEST_SESSKEY_MAGIC_CONS_S2C = "Digest session key to server-to-client signing key magic constant"
|
235
|
+
DIGEST_HA1_MAGIC_CONS_C2S = "Digest H(A1) to client-to-server sealing key magic constant"
|
236
|
+
DIGEST_HA1_MAGIC_CONS_S2C = "Digest H(A1) to server-to-client sealing key magic constant"
|
237
|
+
ONE = [1].pack("n")
|
238
|
+
|
239
|
+
# DES does not use the last bit
|
240
|
+
def self.des_key(key)
|
241
|
+
key=key.bytes.to_a
|
242
|
+
(0..(key.size)).map {|i|
|
243
|
+
left = (i>=1 ? ((key[i-1]<<(8-i))%256) : 0)
|
244
|
+
right = (i<key.size ? (key[i]>>i) : 0)
|
245
|
+
(left | right).chr
|
246
|
+
}.join
|
247
|
+
end
|
248
|
+
|
249
|
+
def initialize(io, ha1, confidentiality, cipher, is_server=false)
|
250
|
+
super(io)
|
251
|
+
@localseq=0
|
252
|
+
@remoteseq=0
|
253
|
+
|
254
|
+
@confidentiality=confidentiality
|
255
|
+
|
256
|
+
if is_server
|
257
|
+
@ki_send=self.class.kis(ha1)
|
258
|
+
@ki_recv=self.class.kic(ha1)
|
259
|
+
else
|
260
|
+
@ki_send=self.class.kic(ha1)
|
261
|
+
@ki_recv=self.class.kis(ha1)
|
262
|
+
end
|
263
|
+
|
264
|
+
if @confidentiality
|
265
|
+
cipher.downcase!
|
266
|
+
|
267
|
+
# adapt openssl 3des name
|
268
|
+
ssl_cipher=cipher
|
269
|
+
key_len=nil
|
270
|
+
case cipher
|
271
|
+
when "des"
|
272
|
+
ssl_cipher="des-cbc"
|
273
|
+
when "3des"
|
274
|
+
ssl_cipher="des-ede-cbc"
|
275
|
+
when /rc4-[0-9]*/
|
276
|
+
key_bits=cipher.split("-").last.to_i
|
277
|
+
raise "Non 8-bit multiple for key size: #{key_bits}" if not key_bits%8 == 0
|
278
|
+
key_len=key_bits/8
|
279
|
+
ssl_cipher="rc4"
|
280
|
+
end
|
281
|
+
|
282
|
+
@enc=OpenSSL::Cipher.new(ssl_cipher).encrypt
|
283
|
+
@dec=OpenSSL::Cipher.new(ssl_cipher).decrypt
|
284
|
+
|
285
|
+
# Force keylen size for rc4-* that is not rc-40 or rc4. Does it work?
|
286
|
+
[@enc,@dec].each {|cp| cp.key_len = key_len } if key_len
|
287
|
+
|
288
|
+
case cipher
|
289
|
+
# For cipher "rc4-40" n is 5;
|
290
|
+
when "rc4-40"
|
291
|
+
n=5
|
292
|
+
# for "rc4-56" n is 7;
|
293
|
+
when "rc4-56"
|
294
|
+
n=7
|
295
|
+
# for the rest n is 16
|
296
|
+
else
|
297
|
+
n=16
|
298
|
+
end
|
299
|
+
|
300
|
+
if is_server
|
301
|
+
@kc_send=self.class.kcs(ha1, n)
|
302
|
+
@kc_recv=self.class.kcc(ha1, n)
|
303
|
+
else
|
304
|
+
@kc_send=self.class.kcc(ha1, n)
|
305
|
+
@kc_recv=self.class.kcs(ha1, n)
|
306
|
+
end
|
307
|
+
|
308
|
+
# The key for the "rc-*" ciphers is all 16 bytes of Kcc or Kcs
|
309
|
+
case cipher
|
310
|
+
when /rc.*/
|
311
|
+
key_len=16
|
312
|
+
iv_len=0
|
313
|
+
# the key for "des" is the first 7 bytes
|
314
|
+
when "des"
|
315
|
+
key_len=7
|
316
|
+
iv_len=8
|
317
|
+
when "3des"
|
318
|
+
key_len=14
|
319
|
+
iv_len=8
|
320
|
+
end
|
321
|
+
|
322
|
+
kc_send=@kc_send[0,key_len]
|
323
|
+
kc_recv=@kc_recv[0,key_len]
|
324
|
+
|
325
|
+
case cipher
|
326
|
+
when "des"
|
327
|
+
# (8 bit * 7 bytes) key must be expanded to (7-bit * 8 bytes),
|
328
|
+
# skipping last bit
|
329
|
+
kc_send=self.class.des_key(kc_send)
|
330
|
+
kc_recv=self.class.des_key(kc_recv)
|
331
|
+
key_len = 8
|
332
|
+
# DES does not use padding here
|
333
|
+
[@enc,@dec].each {|cp| cp.padding=0 }
|
334
|
+
when "3des"
|
335
|
+
# (8 bit * 7 bytes) key must be expanded to (7-bit * 8 bytes),
|
336
|
+
# skipping last bit
|
337
|
+
kc_send=self.class.des_key(kc_send[0,7])+self.class.des_key(kc_send[7,7])
|
338
|
+
kc_recv=self.class.des_key(kc_recv[0,7])+self.class.des_key(kc_recv[7,7])
|
339
|
+
key_len = 16
|
340
|
+
# 3DES does not use padding here
|
341
|
+
[@enc,@dec].each {|cp| cp.padding=0 }
|
342
|
+
end
|
343
|
+
|
344
|
+
[@enc,@dec].each {|cp| cp.key_len = key_len } if key_len
|
345
|
+
|
346
|
+
@enc.key=kc_send
|
347
|
+
@enc.iv=@kc_send[-iv_len,iv_len] if iv_len >0
|
348
|
+
@dec.key=kc_recv
|
349
|
+
@dec.iv=@kc_recv[-iv_len,iv_len] if iv_len >0
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def hm(ki, msg)
|
354
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('md5'), ki, msg)
|
355
|
+
end
|
356
|
+
|
357
|
+
def mac(ki, seqnum, msg)
|
358
|
+
hm(ki, (seqnum + msg))[0..9]# + ONE + seqnum
|
359
|
+
end
|
360
|
+
|
361
|
+
def wrap(msg)
|
362
|
+
seqnum=[@localseq].pack("N")
|
363
|
+
if @confidentiality
|
364
|
+
# SEAL(Ki, Kc, SeqNum, msg) = {CIPHER(Kc, {msg, pad, HMAC(Ki, {SeqNum, msg})[0..9])}), 0x0001, SeqNum}
|
365
|
+
if @enc.block_size==1
|
366
|
+
pad=""
|
367
|
+
else
|
368
|
+
pad_len = @enc.block_size - ((msg.size + 10) % @enc.block_size)
|
369
|
+
pad=pad_len.chr*pad_len
|
370
|
+
end
|
371
|
+
buf=@enc.update(msg + pad + mac(@ki_send, seqnum, msg)) + ONE + seqnum
|
372
|
+
else
|
373
|
+
#MAC(Ki, SeqNum, msg) = (HMAC(Ki, {SeqNum, msg})[0..9], 0x0001, SeqNum)
|
374
|
+
buf=msg + mac(@ki_send, seqnum, msg) + ONE + seqnum
|
375
|
+
end
|
376
|
+
@localseq+=1
|
377
|
+
buf
|
378
|
+
end
|
379
|
+
|
380
|
+
def unwrap(buf)
|
381
|
+
msg_seqnum=buf[-4..-1]
|
382
|
+
# rfc2831 does not ask to check this
|
383
|
+
#exp_seqnum=[@remoteseq].pack("N")
|
384
|
+
#raise DigestMD5SecureLayerError, "Invalid remote sequence field! expected:#{@remoteseq}, got:#{msg_seqnum.unpack("N").first}" if not msg_seqnum == exp_seqnum
|
385
|
+
|
386
|
+
msg_one=buf[-6..-5]
|
387
|
+
raise DigestMD5SecureLayerError, "Invalid one field!" if not msg_one == ONE
|
388
|
+
|
389
|
+
if @confidentiality
|
390
|
+
msg_pad_mac=@dec.update(buf[0..-7])
|
391
|
+
msg_mac=msg_pad_mac[-10..-1]
|
392
|
+
|
393
|
+
if @enc.block_size==1
|
394
|
+
msg=msg_pad_mac[0..-11]
|
395
|
+
else
|
396
|
+
pad_len=msg_pad_mac[-11].ord
|
397
|
+
raise DigestMD5SecureLayerError, "Invalid pad size. Invalid crypto? key?" if not ((1..8).include?(pad_len))
|
398
|
+
msg=msg_pad_mac[0..(-11-(pad_len))]
|
399
|
+
end
|
400
|
+
else
|
401
|
+
msg=buf[0..-17]
|
402
|
+
msg_mac=buf[-16..-7]
|
403
|
+
end
|
404
|
+
exp_mac=mac(@ki_recv, msg_seqnum, msg)
|
405
|
+
raise DigestMD5SecureLayerError, "Invalid mac field!" if not msg_mac == exp_mac
|
406
|
+
|
407
|
+
@remoteseq+=1
|
408
|
+
msg
|
409
|
+
end
|
410
|
+
|
411
|
+
# Kic = MD5({H(A1), "Digest session key to client-to-server signing key magic constant"})
|
412
|
+
def self.kic(ha1)
|
413
|
+
DigestMD5.h(ha1 + DIGEST_SESSKEY_MAGIC_CONS_C2S)
|
414
|
+
end
|
415
|
+
def self.kis(ha1)
|
416
|
+
DigestMD5.h(ha1 + DIGEST_SESSKEY_MAGIC_CONS_S2C)
|
417
|
+
end
|
418
|
+
|
419
|
+
# Kcs = MD5({H(A1)[0..n], "Digest H(A1) to server-to-client sealing key magic constant"})
|
420
|
+
# FYI: Specs do not specify what 0..n means. According to cyrus sasl code, n is length and not position.
|
421
|
+
# More like [0,n] for ruby
|
422
|
+
def self.kcc(ha1,n=16)
|
423
|
+
DigestMD5.h(ha1[0,n] + DIGEST_HA1_MAGIC_CONS_C2S)
|
424
|
+
end
|
425
|
+
def self.kcs(ha1,n=16)
|
426
|
+
DigestMD5.h(ha1[0,n] + DIGEST_HA1_MAGIC_CONS_S2C)
|
427
|
+
end
|
428
|
+
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
|