# md4.tcl - Copyright (C) 2003 Pat Thoyts <patthoyts@users.sourceforge.net>
#
# This is a Tcl-only implementation of the MD4 hash algorithm as described in 
# RFC 1320 ( http://www.ietf.org/rfc/rfc1320.txt )
#
# -------------------------------------------------------------------------
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
# -------------------------------------------------------------------------
#
# $Id: md4.tcl,v 1.20 2008/04/29 10:07:45 patthoyts Exp $

package require Tcl 8.2;                # tcl minimum version
catch {package require md4c 1.0};       # tcllib critcl alternative

# @mdgen EXCLUDE: md4c.tcl

namespace eval ::md4 {
    variable version 1.0.5
    variable rcsid {$Id: md4.tcl,v 1.20 2008/04/29 10:07:45 patthoyts Exp $}
    variable accel
    array set accel {critcl 0 cryptkit 0}

    namespace export md4 hmac MD4Init MD4Update MD4Final

    variable uid
    if {![info exists uid]} {
        set uid 0
    }
}

# -------------------------------------------------------------------------

# MD4Init - create and initialize an MD4 state variable. This will be
# cleaned up when we call MD4Final
#
proc ::md4::MD4Init {} {
    variable uid
    variable accel
    set token [namespace current]::[incr uid]
    upvar #0 $token state

    # RFC1320:3.3 - Initialize MD4 state structure
    array set state \
        [list \
             A [expr {0x67452301}] \
             B [expr {0xefcdab89}] \
             C [expr {0x98badcfe}] \
             D [expr {0x10325476}] \
             n 0 i "" ]
    if {$accel(cryptkit)} {
        cryptkit::cryptCreateContext state(ckctx) CRYPT_UNUSED CRYPT_ALGO_MD4
    }
    return $token
}

proc ::md4::MD4Update {token data} {
    variable accel
    upvar #0 $token state

    if {$accel(critcl)} {
        if {[info exists state(md4c)]} {
            set state(md4c) [md4c $data $state(md4c)]
        } else {
            set state(md4c) [md4c $data]
        }
        return
    } elseif {[info exists state(ckctx)]} {
        if {[string length $data] > 0} {
            cryptkit::cryptEncrypt $state(ckctx) $data
        }
        return
    }

    # Update the state values
    incr state(n) [string length $data]
    append state(i) $data

    # Calculate the hash for any complete blocks
    set len [string length $state(i)]
    for {set n 0} {($n + 64) <= $len} {} {
        MD4Hash $token [string range $state(i) $n [incr n 64]]
    }

    # Adjust the state for the blocks completed.
    set state(i) [string range $state(i) $n end]
    return
}

proc ::md4::MD4Final {token} {
    upvar #0 $token state

    if {[info exists state(md4c)]} {
        set r $state(md4c)
        unset state
        return $r
    } elseif {[info exists state(ckctx)]} {
        cryptkit::cryptEncrypt $state(ckctx) ""
        cryptkit::cryptGetAttributeString $state(ckctx) \
            CRYPT_CTXINFO_HASHVALUE r 16
        cryptkit::cryptDestroyContext $state(ckctx)
        # If nothing was hashed, we get no r variable set!
        if {[info exists r]} {
            unset state
            return $r
        }
    }

    # RFC1320:3.1 - Padding
    #
    set len [string length $state(i)]
    set pad [expr {56 - ($len % 64)}]
    if {$len % 64 > 56} {
        incr pad 64
    }
    if {$pad == 0} {
        incr pad 64
    }
    append state(i) [binary format a$pad \x80]

    # RFC1320:3.2 - Append length in bits as little-endian wide int.
    append state(i) [binary format ii [expr {8 * $state(n)}] 0]

    # Calculate the hash for the remaining block.
    set len [string length $state(i)]
    for {set n 0} {($n + 64) <= $len} {} {
        MD4Hash $token [string range $state(i) $n [incr n 64]]
    }

    # RFC1320:3.5 - Output
    set r [bytes $state(A)][bytes $state(B)][bytes $state(C)][bytes $state(D)]
    unset state
    return $r
}

# -------------------------------------------------------------------------
# HMAC Hashed Message Authentication (RFC 2104)
#
# hmac = H(K xor opad, H(K xor ipad, text))
#
proc ::md4::HMACInit {K} {

    # Key K is adjusted to be 64 bytes long. If K is larger, then use
    # the MD4 digest of K and pad this instead.
    set len [string length $K]
    if {$len > 64} {
        set tok [MD4Init]
        MD4Update $tok $K
        set K [MD4Final $tok]
        set len [string length $K]
    }
    set pad [expr {64 - $len}]
    append K [string repeat \0 $pad]

    # Cacluate the padding buffers.
    set Ki {}
    set Ko {}
    binary scan $K i16 Ks
    foreach k $Ks {
        append Ki [binary format i [expr {$k ^ 0x36363636}]]
        append Ko [binary format i [expr {$k ^ 0x5c5c5c5c}]]
    }

    set tok [MD4Init]
    MD4Update $tok $Ki;                 # initialize with the inner pad
    
    # preserve the Ko value for the final stage.
    # FRINK: nocheck
    set [subst $tok](Ko) $Ko

    return $tok
}

proc ::md4::HMACUpdate {token data} {
    MD4Update $token $data
    return
}

proc ::md4::HMACFinal {token} {
    # FRINK: nocheck
    variable $token
    upvar 0 $token state

    set tok [MD4Init];                  # init the outer hashing function
    MD4Update $tok $state(Ko);          # prepare with the outer pad.
    MD4Update $tok [MD4Final $token];   # hash the inner result
    return [MD4Final $tok]
}

# -------------------------------------------------------------------------

set ::md4::MD4Hash_body {
    variable $token
    upvar 0 $token state

    # RFC1320:3.4 - Process Message in 16-Word Blocks
    binary scan $msg i* blocks
    foreach {X0 X1 X2 X3 X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15} $blocks {
        set A $state(A)
        set B $state(B)
        set C $state(C)
        set D $state(D)

        # Round 1
        # Let [abcd k s] denote the operation
        #   a = (a + F(b,c,d) + X[k]) <<< s.
        # Do the following 16 operations.
        # [ABCD  0  3]  [DABC  1  7]  [CDAB  2 11]  [BCDA  3 19]
        set A [expr {($A + [F $B $C $D] + $X0) <<< 3}]
        set D [expr {($D + [F $A $B $C] + $X1) <<< 7}]
        set C [expr {($C + [F $D $A $B] + $X2) <<< 11}]
        set B [expr {($B + [F $C $D $A] + $X3) <<< 19}]
        # [ABCD  4  3]  [DABC  5  7]  [CDAB  6 11]  [BCDA  7 19]
        set A [expr {($A + [F $B $C $D] + $X4) <<< 3}]
        set D [expr {($D + [F $A $B $C] + $X5) <<< 7}]
        set C [expr {($C + [F $D $A $B] + $X6) <<< 11}]
        set B [expr {($B + [F $C $D $A] + $X7) <<< 19}]
        # [ABCD  8  3]  [DABC  9  7]  [CDAB 10 11]  [BCDA 11 19]
        set A [expr {($A + [F $B $C $D] + $X8) <<< 3}]
        set D [expr {($D + [F $A $B $C] + $X9) <<< 7}]
        set C [expr {($C + [F $D $A $B] + $X10) <<< 11}]
        set B [expr {($B + [F $C $D $A] + $X11) <<< 19}]
        # [ABCD 12  3]  [DABC 13  7]  [CDAB 14 11]  [BCDA 15 19]
        set A [expr {($A + [F $B $C $D] + $X12) <<< 3}]
        set D [expr {($D + [F $A $B $C] + $X13) <<< 7}]
        set C [expr {($C + [F $D $A $B] + $X14) <<< 11}]
        set B [expr {($B + [F $C $D $A] + $X15) <<< 19}]

        # Round 2.
        # Let [abcd k s] denote the operation
        #   a = (a + G(b,c,d) + X[k] + 5A827999) <<< s
        # Do the following 16 operations.
        # [ABCD  0  3]  [DABC  4  5]  [CDAB  8  9]  [BCDA 12 13]
        set A [expr {($A + [G $B $C $D] + $X0  + 0x5a827999) <<< 3}]
        set D [expr {($D + [G $A $B $C] + $X4  + 0x5a827999) <<< 5}]
        set C [expr {($C + [G $D $A $B] + $X8  + 0x5a827999) <<< 9}]
        set B [expr {($B + [G $C $D $A] + $X12 + 0x5a827999) <<< 13}]
        # [ABCD  1  3]  [DABC  5  5]  [CDAB  9  9]  [BCDA 13 13]
        set A [expr {($A + [G $B $C $D] + $X1  + 0x5a827999) <<< 3}]
        set D [expr {($D + [G $A $B $C] + $X5  + 0x5a827999) <<< 5}]
        set C [expr {($C + [G $D $A $B] + $X9  + 0x5a827999) <<< 9}]
        set B [expr {($B + [G $C $D $A] + $X13 + 0x5a827999) <<< 13}]
        # [ABCD  2  3]  [DABC  6  5]  [CDAB 10  9]  [BCDA 14 13]
        set A [expr {($A + [G $B $C $D] + $X2  + 0x5a827999) <<< 3}]
        set D [expr {($D + [G $A $B $C] + $X6  + 0x5a827999) <<< 5}]
        set C [expr {($C + [G $D $A $B] + $X10 + 0x5a827999) <<< 9}]
        set B [expr {($B + [G $C $D $A] + $X14 + 0x5a827999) <<< 13}]
        # [ABCD  3  3]  [DABC  7  5]  [CDAB 11  9]  [BCDA 15 13]
        set A [expr {($A + [G $B $C $D] + $X3  + 0x5a827999) <<< 3}]
        set D [expr {($D + [G $A $B $C] + $X7  + 0x5a827999) <<< 5}]
        set C [expr {($C + [G $D $A $B] + $X11 + 0x5a827999) <<< 9}]
        set B [expr {($B + [G $C $D $A] + $X15 + 0x5a827999) <<< 13}]
        
        # Round 3.
        # Let [abcd k s] denote the operation
        #   a = (a + H(b,c,d) + X[k] + 6ED9EBA1) <<< s.
        # Do the following 16 operations.
        # [ABCD  0  3]  [DABC  8  9]  [CDAB  4 11]  [BCDA 12 15]
        set A [expr {($A + [H $B $C $D] + $X0  + 0x6ed9eba1) <<< 3}]
        set D [expr {($D + [H $A $B $C] + $X8  + 0x6ed9eba1) <<< 9}]
        set C [expr {($C + [H $D $A $B] + $X4  + 0x6ed9eba1) <<< 11}]
        set B [expr {($B + [H $C $D $A] + $X12 + 0x6ed9eba1) <<< 15}]
        # [ABCD  2  3]  [DABC 10  9]  [CDAB  6 11]  [BCDA 14 15]
        set A [expr {($A + [H $B $C $D] + $X2  + 0x6ed9eba1) <<< 3}]
        set D [expr {($D + [H $A $B $C] + $X10 + 0x6ed9eba1) <<< 9}]
        set C [expr {($C + [H $D $A $B] + $X6  + 0x6ed9eba1) <<< 11}]
        set B [expr {($B + [H $C $D $A] + $X14 + 0x6ed9eba1) <<< 15}]
        # [ABCD  1  3]  [DABC  9  9]  [CDAB  5 11]  [BCDA 13 15]
        set A [expr {($A + [H $B $C $D] + $X1  + 0x6ed9eba1) <<< 3}]
        set D [expr {($D + [H $A $B $C] + $X9  + 0x6ed9eba1) <<< 9}]
        set C [expr {($C + [H $D $A $B] + $X5  + 0x6ed9eba1) <<< 11}]
        set B [expr {($B + [H $C $D $A] + $X13 + 0x6ed9eba1) <<< 15}]
        # [ABCD  3  3]  [DABC 11  9]  [CDAB  7 11]  [BCDA 15 15]
        set A [expr {($A + [H $B $C $D] + $X3  + 0x6ed9eba1) <<< 3}]
        set D [expr {($D + [H $A $B $C] + $X11 + 0x6ed9eba1) <<< 9}]
        set C [expr {($C + [H $D $A $B] + $X7  + 0x6ed9eba1) <<< 11}]
        set B [expr {($B + [H $C $D $A] + $X15 + 0x6ed9eba1) <<< 15}]

        # Then perform the following additions. (That is, increment each
        # of the four registers by the value it had before this block
        # was started.)
        incr state(A) $A
        incr state(B) $B
        incr state(C) $C
        incr state(D) $D
    }

    return
}

proc ::md4::byte {n v} {expr {((0xFF << (8 * $n)) & $v) >> (8 * $n)}}
proc ::md4::bytes {v} { 
    #format %c%c%c%c [byte 0 $v] [byte 1 $v] [byte 2 $v] [byte 3 $v]
    format %c%c%c%c \
        [expr {0xFF & $v}] \
        [expr {(0xFF00 & $v) >> 8}] \
        [expr {(0xFF0000 & $v) >> 16}] \
        [expr {((0xFF000000 & $v) >> 24) & 0xFF}]
}

# 32bit rotate-left
proc ::md4::<<< {v n} {
    return [expr {((($v << $n) \
                        | (($v >> (32 - $n)) \
                               & (0x7FFFFFFF >> (31 - $n))))) \
                      & 0xFFFFFFFF}]
}

# Convert our <<< pseudo-operator into a procedure call.
regsub -all -line \
    {\[expr {(.*) <<< (\d+)}\]} \
    $::md4::MD4Hash_body \
    {[<<< [expr {\1}] \2]} \
    ::md4::MD4Hash_body

# RFC1320:3.4 - function F
proc ::md4::F {X Y Z} {
    return [expr {($X & $Y) | ((~$X) & $Z)}]
}

# Inline the F function
regsub -all -line \
    {\[F (\$[ABCD]) (\$[ABCD]) (\$[ABCD])\]} \
    $::md4::MD4Hash_body \
    {( (\1 \& \2) | ((~\1) \& \3) )} \
    ::md4::MD4Hash_body
    
# RFC1320:3.4 - function G
proc ::md4::G {X Y Z} {
    return [expr {($X & $Y) | ($X & $Z) | ($Y & $Z)}]
}

# Inline the G function
regsub -all -line \
    {\[G (\$[ABCD]) (\$[ABCD]) (\$[ABCD])\]} \
    $::md4::MD4Hash_body \
    {((\1 \& \2) | (\1 \& \3) | (\2 \& \3))} \
    ::md4::MD4Hash_body

# RFC1320:3.4 - function H
proc ::md4::H {X Y Z} {
    return [expr {$X ^ $Y ^ $Z}]
}

# Inline the H function
regsub -all -line \
    {\[H (\$[ABCD]) (\$[ABCD]) (\$[ABCD])\]} \
    $::md4::MD4Hash_body \
    {(\1 ^ \2 ^ \3)} \
    ::md4::MD4Hash_body

# Define the MD4 hashing procedure with inline functions.
proc ::md4::MD4Hash {token msg} $::md4::MD4Hash_body
unset ::md4::MD4Hash_body

# -------------------------------------------------------------------------

if {[package provide Trf] != {}} {
    interp alias {} ::md4::Hex {} ::hex -mode encode --
} else {
    proc ::md4::Hex {data} {
        binary scan $data H* result
        return [string toupper $result]
    }
}

# -------------------------------------------------------------------------

# LoadAccelerator --
#
#	This package can make use of a number of compiled extensions to
#	accelerate the digest computation. This procedure manages the
#	use of these extensions within the package. During normal usage
#	this should not be called, but the test package manipulates the
#	list of enabled accelerators.
#
proc ::md4::LoadAccelerator {name} {
    variable accel
    set r 0
    switch -exact -- $name {
        critcl {
            if {![catch {package require tcllibc}]
                || ![catch {package require md4c}]} {
                set r [expr {[info command ::md4::md4c] != {}}]
            }
        }
        cryptkit {
            if {![catch {package require cryptkit}]} {
                set r [expr {![catch {cryptkit::cryptInit}]}]
            }
        }
        #trf {
        #    if {![catch {package require Trf}]} {
        #        set r [expr {![catch {::md4 aa} msg]}]
        #    }
        #}
        default {
            return -code error "invalid accelerator package:\
                must be one of [join [array names accel] {, }]"
        }
    }
    set accel($name) $r
}

# -------------------------------------------------------------------------

# Description:
#  Pop the nth element off a list. Used in options processing.
#
proc ::md4::Pop {varname {nth 0}} {
    upvar $varname args
    set r [lindex $args $nth]
    set args [lreplace $args $nth $nth]
    return $r
}

# -------------------------------------------------------------------------

# fileevent handler for chunked file hashing.
#
proc ::md4::Chunk {token channel {chunksize 4096}} {
    # FRINK: nocheck
    variable $token
    upvar 0 $token state
    
    if {[eof $channel]} {
        fileevent $channel readable {}
        set state(reading) 0
    }
        
    MD4Update $token [read $channel $chunksize]
}

# -------------------------------------------------------------------------

proc ::md4::md4 {args} {
    array set opts {-hex 0 -filename {} -channel {} -chunksize 4096}
    while {[string match -* [set option [lindex $args 0]]]} {
        switch -glob -- $option {
            -hex       { set opts(-hex) 1 }
            -file*     { set opts(-filename) [Pop args 1] }
            -channel   { set opts(-channel) [Pop args 1] }
            -chunksize { set opts(-chunksize) [Pop args 1] }
            default {
                if {[llength $args] == 1} { break }
                if {[string compare $option "--"] == 0 } { Pop args; break }
                set err [join [lsort [array names opts]] ", "]
                return -code error "bad option $option:\
                    must be one of $err"
            }
        }
        Pop args
    }

    if {$opts(-filename) != {}} {
        set opts(-channel) [open $opts(-filename) r]
        fconfigure $opts(-channel) -translation binary
    }

    if {$opts(-channel) == {}} {

        if {[llength $args] != 1} {
            return -code error "wrong # args:\
                should be \"md4 ?-hex? -filename file | string\""
        }
        set tok [MD4Init]
        MD4Update $tok [lindex $args 0]
        set r [MD4Final $tok]

    } else {

        set tok [MD4Init]
        # FRINK: nocheck
        set [subst $tok](reading) 1
        fileevent $opts(-channel) readable \
            [list [namespace origin Chunk] \
                 $tok $opts(-channel) $opts(-chunksize)]
        vwait [subst $tok](reading)
        set r [MD4Final $tok]

        # If we opened the channel - we should close it too.
        if {$opts(-filename) != {}} {
            close $opts(-channel)
        }
    }
    
    if {$opts(-hex)} {
        set r [Hex $r]
    }
    return $r
}

# -------------------------------------------------------------------------

proc ::md4::hmac {args} {
    array set opts {-hex 0 -filename {} -channel {} -chunksize 4096}
    while {[string match -* [set option [lindex $args 0]]]} {
        switch -glob -- $option {
            -key       { set opts(-key) [Pop args 1] }
            -hex       { set opts(-hex) 1 }
            -file*     { set opts(-filename) [Pop args 1] }
            -channel   { set opts(-channel) [Pop args 1] }
            -chunksize { set opts(-chunksize) [Pop args 1] }
            default {
                if {[llength $args] == 1} { break }
                if {[string compare $option "--"] == 0 } { Pop args; break }
                set err [join [lsort [array names opts]] ", "]
                return -code error "bad option $option:\
                    must be one of $err"
            }
        }
        Pop args
    }

    if {![info exists opts(-key)]} {
        return -code error "wrong # args:\
            should be \"hmac ?-hex? -key key -filename file | string\""
    }

    if {$opts(-filename) != {}} {
        set opts(-channel) [open $opts(-filename) r]
        fconfigure $opts(-channel) -translation binary
    }

    if {$opts(-channel) == {}} {

        if {[llength $args] != 1} {
            return -code error "wrong # args:\
                should be \"hmac ?-hex? -key key -filename file | string\""
        }
        set tok [HMACInit $opts(-key)]
        HMACUpdate $tok [lindex $args 0]
        set r [HMACFinal $tok]

    } else {

        set tok [HMACInit $opts(-key)]
        # FRINK: nocheck
        set [subst $tok](reading) 1
        fileevent $opts(-channel) readable \
            [list [namespace origin Chunk] \
                 $tok $opts(-channel) $opts(-chunksize)]
        vwait [subst $tok](reading)
        set r [HMACFinal $tok]

        # If we opened the channel - we should close it too.
        if {$opts(-filename) != {}} {
            close $opts(-channel)
        }
    }
    
    if {$opts(-hex)} {
        set r [Hex $r]
    }
    return $r
}

# -------------------------------------------------------------------------

# Try and load a compiled extension to help.
namespace eval ::md4 {
    foreach e {critcl cryptkit} { if {[LoadAccelerator $e]} { break } }
    unset e
}

package provide md4 $::md4::version

# -------------------------------------------------------------------------
# Local Variables:
#   mode: tcl
#   indent-tabs-mode: nil
# End:


