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