00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016 package require Tcl 8.2
00017
00018 namespace ::SASL {
00019 variable version 1.3.1
00020 variable rcsid {$Id: sasl.tcl,v 1.11 2006/10/02 21:21:57 patthoyts Exp $}
00021
00022 variable uid
00023 if {![info exists uid]} { uid = 0 }
00024
00025 variable mechanisms
00026 if {![info exists mechanisms]} {
00027 mechanisms = [list]
00028 }
00029 }
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041 ret ::SASL::mechanisms (optional type =client , optional minimum =0) {
00042 variable mechanisms
00043 set r [list]
00044 foreach mech $mechanisms {
00045 if {[lindex $mech 0] < $minimum} { continue }
00046 switch -exact -- $type {
00047 client {
00048 if {[string length [lindex $mech 2]] > 0} {
00049 lappend r [lindex $mech 1]
00050 }
00051 }
00052 server {
00053 if {[string length [lindex $mech 3]] > 0} {
00054 lappend r [lindex $mech 1]
00055 }
00056 }
00057 default {
00058 return -code error "invalid type \"$type\":\
00059 must be either client or server"
00060 }
00061 }
00062 }
00063 return $r
00064 }
00065
00066
00067
00068
00069
00070
00071
00072
00073 ret ::SASL::register (type mechanism , type preference , type clientproc , optional serverproc ={)} {
00074 variable mechanisms
00075 set ndx [lsearch -regexp $mechanisms $mechanism]
00076 set mech [list $preference $mechanism $clientproc $serverproc]
00077 if {$ndx == -1} {
00078 lappend mechanisms $mech
00079 } else {
00080 mechanisms = [lreplace $mechanisms $ndx $ndx $mech]
00081 }
00082 mechanisms = [lsort -index 0 -decreasing -integer $mechanisms]
00083 return
00084 }
00085
00086
00087
00088
00089
00090 ret ::SASL::uid () {
00091 variable uid
00092 return [incr uid]
00093 }
00094
00095
00096
00097
00098
00099 ret ::SASL::response (type context) {
00100 upvar #0 $context ctx
00101 return $ctx(response)
00102 }
00103
00104
00105
00106
00107
00108
00109 ret ::SASL::reset (type context) {
00110 upvar #0 $context ctx
00111 array set ctx [list step 0 response "" valid false count 0]
00112 return $context
00113 }
00114
00115
00116
00117
00118
00119 ret ::SASL::cleanup (type context) {
00120 if {[info exists $context]} {
00121 unset $context
00122 }
00123 return
00124 }
00125
00126
00127
00128
00129
00130 ret ::SASL::new (type args) {
00131 set context [namespace current]::[uid]
00132 upvar #0 $context ctx
00133 array set ctx [list mech {} callback {} proc {} service smtp server {} \
00134 step 0 response "" valid false type client count 0]
00135 eval [linsert $args 0 [namespace origin configure] $context]
00136 return $context
00137 }
00138
00139
00140
00141
00142
00143 ret ::SASL::configure (type context , type args) {
00144 variable mechanisms
00145 upvar #0 $context ctx
00146 while {[string match -* [set option [lindex $args 0]]]} {
00147 switch -exact -- $option {
00148 -service {
00149 set ctx(service) [Pop args 1]
00150 }
00151 -server - -serverFQDN {
00152 set ctx(server) [Pop args 1]
00153 }
00154 -mech - -mechanism {
00155 set mech [string toupper [Pop args 1]]
00156 set ctx(proc) {}
00157 foreach m $mechanisms {
00158 if {[string equal [lindex $m 1] $mech]} {
00159 set ctx(mech) $mech
00160 if {[string equal $ctx(type) "server"]} {
00161 set ctx(proc) [lindex $m 3]
00162 } else {
00163 set ctx(proc) [lindex $m 2]
00164 }
00165 break
00166 }
00167 }
00168 if {[string equal $ctx(proc) {}]} {
00169 return -code error "mechanism \"$mech\" not available:\
00170 must be one of those given by \[sasl::mechanisms\]"
00171 }
00172 }
00173 -callback - -callbacks {
00174 set ctx(callback) [Pop args 1]
00175 }
00176 -type {
00177 set type [Pop args 1]
00178 if {[lsearch -exact {server client} $type] != -1} {
00179 set ctx(type) $type
00180 if {![string equal $ctx(mech) ""]} {
00181 configure $context -mechanism $ctx(mech)
00182 }
00183 } else {
00184 return -code error "bad value \"$type\":\
00185 must be either client or server"
00186 }
00187 }
00188 default {
00189 return -code error "bad option \"$option\":\
00190 must be one of -mechanism, -service, -server -type\
00191 or -callbacks"
00192 }
00193 }
00194 Pop args
00195 }
00196
00197 }
00198
00199 ret ::SASL::step (type context , type challenge , type args) {
00200 upvar #0 $context ctx
00201 incr ctx(count)
00202 return [eval [linsert $args 0 $ctx(proc) $context $challenge]]
00203 }
00204
00205
00206 ret ::SASL::Pop (type varname , optional nth =0) {
00207 upvar $varname args
00208 set r [lindex $args $nth]
00209 set args [lreplace $args $nth $nth]
00210 return $r
00211 }
00212
00213 ret ::SASL::md5_init () {
00214 variable md5_inited
00215 if {[info exists md5_inited]} {return} else {set md5_inited 1}
00216 # Deal with either version of md5. We'd like version 2 but someone
00217 # may have already loaded version 1.
00218 set md5major [lindex [split [package require md5] .] 0]
00219 if {$md5major < 2} {
00220 # md5 v1, no options, and returns a hex string ready for us.
00221 proc ::SASL::md5_hex {data} { return [::md5::md5 $data] }
00222 proc ::SASL::md5_bin {data} { return [binary format H* [::md5::md5 $data]] }
00223 proc ::SASL::hmac_hex {pass data} { return [::md5::hmac $pass $data] }
00224 proc ::SASL::hmac_bin {pass data} { return [binary format H* [::md5::hmac $pass $data]] }
00225 } else {
00226 # md5 v2 requires -hex to return hash as hex-encoded non-binary string.
00227 proc ::SASL::md5_hex {data} { return [string tolower [::md5::md5 -hex $data]] }
00228 proc ::SASL::md5_bin {data} { return [::md5::md5 $data] }
00229 proc ::SASL::hmac_hex {pass data} { return [::md5::hmac -hex -key $pass $data] }
00230 proc ::SASL::hmac_bin {pass data} { return [::md5::hmac -key $pass $data] }
00231 }
00232 }
00233
00234
00235
00236
00237
00238
00239
00240
00241
00242
00243
00244
00245
00246
00247
00248 ret ::SASL::CRAM-MD5:client (type context , type challenge , type args) {
00249 upvar #0 $context ctx
00250 md5_init
00251 if {$ctx(step) != 0} {
00252 return -code error "unexpected state: CRAM-MD5 has only 1 step"
00253 }
00254 if {[string length $challenge] == 0} {
00255 set ctx(response) ""
00256 return 1
00257 }
00258 set password [eval $ctx(callback) [list $context password]]
00259 set username [eval $ctx(callback) [list $context username]]
00260 set reply [hmac_hex $password $challenge]
00261 set reply "$username [string tolower $reply]"
00262 set ctx(response) $reply
00263 incr ctx(step)
00264 return 0
00265 }
00266
00267 ret ::SASL::CRAM-MD5:server (type context , type clientrsp , type args) {
00268 upvar #0 $context ctx
00269 md5_init
00270 incr ctx(step)
00271 switch -exact -- $ctx(step) {
00272 1 {
00273 set ctx(realm) [eval $ctx(callback) [list $context realm]]
00274 set ctx(response) "<[pid].[clock seconds]@$ctx(realm)>"
00275 return 1
00276 }
00277 2 {
00278 foreach {user hash} $clientrsp break
00279 set hash [string tolower $hash]
00280 set pass [eval $ctx(callback) [list $context password $user $ctx(realm)]]
00281 set check [hmac_bin $pass $ctx(response)]
00282 binary scan $check H* cx
00283 if {[string equal $cx $hash]} {
00284 return 0
00285 } else {
00286 return -code error "authentication failed"
00287 }
00288 }
00289 default {
00290 return -code error "invalid state"
00291 }
00292 }
00293 }
00294
00295 ::SASL::register CRAM-MD5 30 ::SASL::CRAM-MD5:client ::SASL::CRAM-MD5:server
00296
00297
00298
00299
00300
00301
00302
00303
00304
00305
00306
00307
00308 ret ::SASL::PLAIN:client (type context , type challenge , type args) {
00309 upvar #0 $context ctx
00310 incr ctx(step)
00311 set authzid [eval $ctx(callback) [list $context login]]
00312 set username [eval $ctx(callback) [list $context username]]
00313 set password [eval $ctx(callback) [list $context password]]
00314 set ctx(response) "$authzid\x00$username\x00$password"
00315 return 0
00316 }
00317
00318 ret ::SASL::PLAIN:server (type context , type clientrsp , type args) {
00319 upvar \#0 $context ctx
00320 if {[string length $clientrsp] < 1} {
00321 set ctx(response) ""
00322 return 1
00323 } else {
00324 foreach {authzid authid pass} [split $clientrsp \0] break
00325 set realm [eval $ctx(callback) [list $context realm]]
00326 set check [eval $ctx(callback) [list $context password $authid $realm]]
00327 if {[string equal $pass $check]} {
00328 return 0
00329 } else {
00330 return -code error "authentication failed"
00331 }
00332 }
00333 }
00334
00335 ::SASL::register PLAIN 10 ::SASL::PLAIN:client ::SASL::PLAIN:server
00336
00337
00338
00339
00340
00341
00342
00343
00344
00345
00346
00347
00348
00349 ret ::SASL::LOGIN:client (type context , type challenge , type args) {
00350 upvar #0 $context ctx
00351 if {$ctx(step) == 0 && [string length $challenge] == 0} {
00352 set ctx(response) ""
00353 return 1
00354 }
00355 incr ctx(step)
00356 switch -exact -- $ctx(step) {
00357 1 {
00358 set ctx(response) [eval $ctx(callback) [list $context username]]
00359 set r 1
00360 }
00361 2 {
00362 set ctx(response) [eval $ctx(callback) [list $context password]]
00363 set r 0
00364 }
00365 default {
00366 return -code error "unexpected state \"$ctx(step)\":\
00367 LOGIN has only 2 steps"
00368 }
00369 }
00370 return $r
00371 }
00372
00373 ret ::SASL::LOGIN:server (type context , type clientrsp , type args) {
00374 upvar #0 $context ctx
00375 incr ctx(step)
00376 switch -exact -- $ctx(step) {
00377 1 {
00378 set ctx(response) "Username:"
00379 return 1
00380 }
00381 2 {
00382 set ctx(username) $clientrsp
00383 set ctx(response) "Password:"
00384 return 1
00385 }
00386 3 {
00387 set user $ctx(username)
00388 set realm [eval $ctx(callback) [list $context realm]]
00389 set pass [eval $ctx(callback) [list $context password $user $realm]]
00390 if {[string equal $clientrsp $pass]} {
00391 return 0
00392 } else {
00393 return -code error "authentication failed"
00394 }
00395 }
00396 default {
00397 return -code error "invalid state"
00398 }
00399 }
00400 }
00401
00402 ::SASL::register LOGIN 20 ::SASL::LOGIN:client ::SASL::LOGIN:server
00403
00404
00405
00406
00407
00408
00409
00410
00411
00412 ret ::SASL::ANONYMOUS:client (type context , type challenge , type args) {
00413 upvar #0 $context ctx
00414 set user [eval $ctx(callback) [list $context username]]
00415 set realm [eval $ctx(callback) [list $context realm]]
00416 set ctx(response) $user@$realm
00417 return 0
00418 }
00419
00420 ret ::SASL::ANONYMOUS:server (type context , type clientrsp , type args) {
00421 upvar #0 $context ctx
00422 set ctx(response) ""
00423 if {[string length $clientrsp] < 1} {
00424 if {$ctx(count) > 2} {
00425 return -code error "authentication failed"
00426 }
00427 return 1
00428 } else {
00429 set ctx(trace) $clientrsp
00430 return 0
00431 }
00432 }
00433
00434 ::SASL::register ANONYMOUS 5 ::SASL::ANONYMOUS:client ::SASL::ANONYMOUS:server
00435
00436
00437
00438
00439
00440
00441
00442
00443
00444 ret ::SASL::DIGEST-MD5:client (type context , type challenge , type args) {
00445 variable digest_md5_noncecount
00446 upvar #0 $context ctx
00447 md5_init
00448 if {$ctx(step) == 0 && [string length $challenge] == 0} {
00449 set ctx(response) ""
00450 return 1
00451 }
00452 incr ctx(step)
00453 set result 0
00454 switch -exact -- $ctx(step) {
00455 1 {
00456 array set params [DigestParameters $challenge]
00457
00458 if {![info exists digest_md5_noncecount]} {
00459 set digest_md5_noncecount 0
00460 }
00461 set nonce $params(nonce)
00462 set cnonce [CreateNonce]
00463 set noncecount [format %08u [incr digest_md5_noncecount]]
00464 set qop auth
00465
00466 set username [eval $ctx(callback) [list $context username]]
00467 set password [eval $ctx(callback) [list $context password]]
00468 if {[info exists params(realm)]} {
00469 set realm $params(realm)
00470 } else {
00471 set realm [eval $ctx(callback) [list $context realm]]
00472 }
00473
00474 set uri "$ctx(service)/$realm"
00475 set R [DigestResponse $username $realm $password $uri \
00476 $qop $nonce $noncecount $cnonce]
00477
00478 set ctx(response) "username=\"$username\",realm=\"$realm\",nonce=\"$nonce\",nc=\"$noncecount\",cnonce=\"$cnonce\",digest-uri=\"$uri\",response=\"$R\",qop=$qop"
00479 set result 1
00480 }
00481
00482 2 {
00483 set ctx(response) ""
00484 set result 0
00485 }
00486 default {
00487 return -code error "invalid state"
00488 }
00489 }
00490 return $result
00491 }
00492
00493 ret ::SASL::DIGEST-MD5:server (type context , type challenge , type args) {
00494 variable digest_md5_noncecount
00495 upvar #0 $context ctx
00496 md5_init
00497 incr ctx(step)
00498 set result 0
00499 switch -exact -- $ctx(step) {
00500 1 {
00501 set realm [eval $ctx(callback) [list $context realm]]
00502 set ctx(nonce) [CreateNonce]
00503 set ctx(response) "realm=\"$realm\",nonce=\"$ctx(nonce)\",qop=\"auth\",charset=utf-8,algorithm=md5-sess"
00504 set result 1
00505 }
00506 2 {
00507 array set params [DigestParameters $challenge]
00508 set realm [eval $ctx(callback) [list $context realm]]
00509 set password [eval $ctx(callback)\
00510 [list $context password $params(username) $realm]]
00511 set uri "$ctx(service)/$realm"
00512 set R [DigestResponse $params(username) $realm $password \
00513 $uri auth $ctx(nonce) $params(nc) $params(cnonce)]
00514 if {[string equal $R $params(response)]} {
00515 set R2 [DigestResponse $params(username) $realm $password \
00516 $uri auth $ctx(nonce) $params(nc) $params(cnonce)]
00517 set ctx(response) "rspauth=$R2"
00518 set ctx(nc) $params(nc)
00519 set result 1
00520 } else {
00521 return -code error "authentication failed"
00522 }
00523 }
00524 3 {
00525 set ctx(response) ""
00526 set result 0
00527 }
00528 default {
00529 return -code error "invalid state"
00530 }
00531 }
00532 return $result
00533 }
00534
00535
00536
00537
00538 ret ::SASL::DigestParameters (type challenge) {
00539 set sep "\\\]\\\[\\\\()<>@,;:\\\"\\\?=\\\{\\\} \t"
00540 set tok {0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\-\|\~\!\#\$\%\&\*\+\.\^\_\`}
00541 set sqot {(?:\'(?:\\.|[^\'\\])*\')}
00542 set dqot {(?:\"(?:\\.|[^\"\\])*\")}
00543 set parameters {}
00544 regsub -all "(\[${tok}\]+)=(${dqot}|(?:\[${tok}\]+))(?:\[${sep}\]+|$)" $challenge {\1 \2 } parameters
00545 return $parameters
00546 }
00547
00548 /* RFC 2831 2.1.2.1*/
00549 /* */
00550 ret ::SASL::DigestResponse (type user , type realm , type pass , type uri , type qop , type nonce , type noncecount , type cnonce) {
00551 set A1 [md5_bin "$user:$realm:$pass"]
00552 set A2 "AUTHENTICATE:$uri"
00553 if {![string equal $qop "auth"]} {
00554 append A2 :[string repeat 0 32]
00555 }
00556 set A1h [md5_hex "${A1}:$nonce:$cnonce"]
00557 set A2h [md5_hex $A2]
00558 set R [md5_hex $A1h:$nonce:$noncecount:$cnonce:$qop:$A2h]
00559 return $R
00560 }
00561
00562 /* RFC 2831 2.1.2.2*/
00563 /* */
00564 ret ::SASL::DigestResponse2 (type user , type realm , type pass , type uri , type qop , type nonce , type noncecount , type cnonce) {
00565 set A1 [md5_bin "$user:$realm:$pass"]
00566 set A2 ":$uri"
00567 if {![string equal $qop "auth"]} {
00568 append A2 :[string repeat 0 32]
00569 }
00570 set A1h [md5_hex "${A1}:$nonce:$cnonce"]
00571 set A2h [md5_hex $A2]
00572 set R [md5_hex $A1h:$nonce:$noncecount:$cnonce:$qop:$A2h]
00573 return $R
00574 }
00575
00576 /* Get 16 random bytes for a nonce value. If we can use /dev/random, do so*/
00577 /* otherwise we hash some values.*/
00578 /* */
00579 ret ::SASL::CreateNonce () {
00580 set bytes {}
00581 if {[file readable /dev/urandom]} {
00582 catch {
00583 set f [open /dev/urandom r]
00584 fconfigure $f -translation binary -buffering none
00585 set bytes [read $f 16]
00586 close $f
00587 }
00588 }
00589 if {[string length $bytes] < 1} {
00590 set bytes [md5_bin [clock seconds]:[pid]:[expr {rand()}]]
00591 }
00592 return [binary scan $bytes h* r; set r]
00593 }
00594
00595 ::SASL::register DIGEST-MD5 40 \
00596 ::SASL::DIGEST-MD5:client ::SASL::DIGEST-MD5:server
00597
00598 /* -------------------------------------------------------------------------*/
00599
00600 /* OTP SASL MECHANISM*/
00601 /* */
00602 /* Implementation of the OTP SASL mechanism (RFC2444).*/
00603 /* */
00604 /* Comments:*/
00605 /* */
00606 /* RFC 2289: A One-Time Password System*/
00607 /* RFC 2444: OTP SASL Mechanism*/
00608 /* RFC 2243: OTP Extended Responses*/
00609 /* Client initializes with authid\0authzid*/
00610 /* Server responds with extended OTP responses */
00611 /* eg: otp-md5 498 bi32123 ext*/
00612 /* Client responds with otp result as:*/
00613 /* hex:xxxxxxxxxxxxxxxx*/
00614 /* or*/
00615 /* word:WWWW WWW WWWW WWWW WWWW*/
00616 /* */
00617 /* To support changing the otp sequence the extended commands have:*/
00618 /* init-hex:<current>:<new params>:<new>*/
00619 /* eg: init-hex:xxxxxxxxxxxx:md5 499 seed987:xxxxxxxxxxxxxx*/
00620 /* or init-word*/
00621
00622 ret ::SASL::OTP:client (type context , type challenge , type args) {
00623 upvar #0 $context ctx
00624 package require otp
00625 incr ctx(step)
00626 switch -exact -- $ctx(step) {
00627 1 {
00628 set authzid [eval $ctx(callback) [list $context login]]
00629 set username [eval $ctx(callback) [list $context username]]
00630 set ctx(response) "$authzid\x00$username"
00631 set cont 1
00632 }
00633 2 {
00634 foreach {type count seed ext} $challenge break
00635 set type [lindex [split $type -] 1]
00636 if {[lsearch -exact {md4 md5 sha1 rmd160} $type] == -1} {
00637 return -code error "unsupported digest algorithm \"$type\":\
00638 must be one of md4, md5, sha1 or rmd160"
00639 }
00640 set challenge [lrange $challenge 3 end]
00641 set password [eval $ctx(callback) [list $context password]]
00642 set otp [::otp::otp-$type -word -seed $seed \
00643 -count $count $password]
00644 if {[string match "ext*" $ext]} {
00645 set otp word:$otp
00646 }
00647 set ctx(response) $otp
00648 set cont 0
00649 }
00650 default {
00651 return -code error "unexpected state \"$ctx(step)\":\
00652 the SASL OTP mechanism only has 2 steps"
00653 }
00654 }
00655 return $cont
00656 }
00657
00658 ::SASL::register OTP 45 ::SASL::OTP:client
00659
00660 /* -------------------------------------------------------------------------*/
00661
00662 package provide SASL $::SASL::version
00663
00664 /* -------------------------------------------------------------------------*/
00665 /* */
00666 /* Local variables:*/
00667 /* indent-tabs-mode: nil*/
00668 /* End:*/
00669