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 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
@@ -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
+