kiro-ruby-sasl 0.0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|