├── tests ├── run_tests.tcl ├── valid_tokens.test ├── get_hotp.test └── get_totp.test ├── LICENSE ├── README.md └── src └── onetimepass.tcl /tests/run_tests.tcl: -------------------------------------------------------------------------------- 1 | package require tcltest 2 | namespace import ::tcltest::* 3 | runAllTests 4 | -------------------------------------------------------------------------------- /tests/valid_tokens.test: -------------------------------------------------------------------------------- 1 | # validity.test 2 | 3 | package require tcltest 4 | namespace import ::tcltest::* 5 | 6 | # Source the function to be tested 7 | source ../src/onetimepass.tcl 8 | 9 | set key 12345678901234567890 10 | 11 | # onetimepass::valid_hotp tests 12 | test valid_hotp_0th_interval {} -body { 13 | onetimepass::valid_hotp 755224 $key -1 } -result 0 14 | 15 | test valid_hotp_111th_interval {} -body { 16 | set token [onetimepass::get_hotp $key 111] onetimepass::valid_hotp $token $key -1 17 | } -result 111 18 | 19 | test valid_hotp_invalid {} -body { 20 | set token [onetimepass::get_hotp $key 1000] 21 | onetimepass::valid_hotp $token $key -1 100 22 | } -result -1 23 | 24 | # onetimepass::valid_totp tests 25 | test valid_totp_true {} -body { 26 | set token [onetimepass::get_totp $key] 27 | onetimepass::valid_totp $token $key 28 | } -result 1 29 | 30 | test valid_totp_false {} -body { 31 | set token [onetimepass::get_totp $key] 32 | append token "ERROR" 33 | onetimepass::valid_totp $token $key 34 | } -result 0 35 | 36 | cleanupTests 37 | -------------------------------------------------------------------------------- /tests/get_hotp.test: -------------------------------------------------------------------------------- 1 | # get_hotp.test 2 | 3 | package require tcltest 4 | namespace import ::tcltest::* 5 | 6 | # Source the function to be tested 7 | source ../src/onetimepass.tcl 8 | 9 | set key 12345678901234567890 10 | 11 | # All test vectors from RFC 4226: Appendix D 12 | test get_hotp_vector0 {} -body { 13 | onetimepass::get_hotp $key 0 14 | } -result 755224 15 | 16 | test get_hotp_vector1 {} -body { 17 | onetimepass::get_hotp $key 1 18 | } -result 287082 19 | 20 | test get_hotp_vector2 {} -body { 21 | onetimepass::get_hotp $key 2 22 | } -result 359152 23 | 24 | test get_hotp_vector3 {} -body { 25 | onetimepass::get_hotp $key 3 26 | } -result 969429 27 | 28 | test get_hotp_vector4 {} -body { 29 | onetimepass::get_hotp $key 4 30 | } -result 338314 31 | 32 | test get_hotp_vector5 {} -body { 33 | onetimepass::get_hotp $key 5 34 | } -result 254676 35 | 36 | test get_hotp_vector6 {} -body { 37 | onetimepass::get_hotp $key 6 38 | } -result 287922 39 | 40 | test get_hotp_vector7 {} -body { 41 | onetimepass::get_hotp $key 7 42 | } -result 162583 43 | 44 | test get_hotp_vector8 {} -body { 45 | onetimepass::get_hotp $key 8 46 | } -result 399871 47 | 48 | test get_hotp_vector9 {} -body { 49 | onetimepass::get_hotp $key 9 50 | } -result 520489 51 | 52 | cleanupTests 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, FlightAware LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | 15 | * Neither the name of the FlightAware LLC nor the names of its 16 | contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tcl OTP 2 | 3 | A Pure TCL library providing the [HOTP][1] and [TOTP][2] Algorithms. 4 | 5 | ## Exported Functions 6 | 7 | ```tcl 8 | 9 | proc get_hotp {key interval_no {digest sha1} {token_length 6}} 10 | 11 | 12 | proc get_totp {key {interval 30} {digest sha1} {token_length 6}} 13 | 14 | 15 | proc valid_hotp {token key last {trials 1000} {digest sha1} {token_length 6}} 16 | 17 | 18 | proc valid_totp {token key {interval 30} {digest sha1} {token_length 6}} 19 | ``` 20 | 21 | For example usage, see the tests/ directory and the comments above the procs for an explanation of their signatures. This API is based on the excellent Python library [onetimepass][3]. 22 | 23 | ## Tests 24 | 25 | A test suite is included in the `tests/` directory. The tests are taken from the vectors specified in the RFCs for HOTP and TOTP. 26 | 27 | ### Limitations 28 | 29 | * Since Tcl--as of the time of this writing--does not have a sha512 implementation in the standard library, the HOTP and 30 | TOTP procs only work with {sha1,sha256}-HMACs. 31 | 32 | * The TOTP implementation does not allow specifying a value for T0 as mentioned in the TOTP RFC. 33 | 34 | ### Security Issues 35 | 36 | The security of these algorithms depends on the quality of the randomness used to key the HMAC, and on the security of the HMAC. 37 | 38 | For further discussion of these issues see [Randomness Recommendations for Security][4] and [Section 3 of RFC 4868][5]. 39 | 40 | [1]: https://tools.ietf.org/html/rfc4226 41 | [2]: https://tools.ietf.org/html/rfc6238 42 | [3]: https://github.com/tadeck/onetimepass 43 | [4]: https://tools.ietf.org/html/rfc1750 44 | [5]: https://www.ietf.org/rfc/rfc4868.txt 45 | -------------------------------------------------------------------------------- /tests/get_totp.test: -------------------------------------------------------------------------------- 1 | # get_totp.test 2 | 3 | package require tcltest 4 | namespace import ::tcltest::* 5 | 6 | # Source the function to be tested 7 | source ../src/onetimepass.tcl 8 | 9 | # All test vectors from RFC 6238: Appendix B 10 | # 11 | # In order to use these vectors, the very short 12 | # get_totp function has been replicated in full 13 | # in each of the test cases, i.e., manual calls to 14 | # get_hotp (from which get_totp is derived) are used 15 | # 16 | # This is necessary b/c the test vectors occur at 17 | # times in the past whereas get_totp uses the current 18 | # epoch time for token generation 19 | 20 | # Setup 21 | set sha1_key 12345678901234567890 22 | set sha256_key 12345678901234567890123456789012 23 | set X 30 24 | set token_length 8 25 | 26 | # Tests 27 | test get_totp_vector0_sha1 {} -body { 28 | set T 0x1 29 | onetimepass::get_hotp $sha1_key $T sha1 $token_length 30 | } -result 94287082 31 | 32 | test get_totp_vector0_sha256 {} -body { 33 | set T 0x1 34 | onetimepass::get_hotp $sha256_key $T sha2 $token_length 35 | } -result 46119246 36 | 37 | test get_totp_vector1_sha1 {} -body { 38 | set T 0x23523EC 39 | onetimepass::get_hotp $sha1_key $T sha1 $token_length 40 | } -result 07081804 41 | 42 | test get_totp_vector1_sha256 {} -body { 43 | set T 0x23523EC 44 | onetimepass::get_hotp $sha256_key $T sha2 $token_length 45 | } -result 68084774 46 | 47 | test get_totp_vector2_sha1 {} -body { 48 | set T 0x23523ED 49 | onetimepass::get_hotp $sha1_key $T sha1 $token_length 50 | } -result 14050471 51 | 52 | test get_totp_vector2_sha256 {} -body { 53 | set T 0x23523ED 54 | onetimepass::get_hotp $sha256_key $T sha2 $token_length 55 | } -result 67062674 56 | 57 | test get_totp_vector3_sha1 {} -body { 58 | set T 0x273EF07 59 | onetimepass::get_hotp $sha1_key $T sha1 $token_length 60 | } -result 89005924 61 | 62 | test get_totp_vector3_sha256 {} -body { 63 | set T 0x273EF07 64 | onetimepass::get_hotp $sha256_key $T sha2 $token_length 65 | } -result 91819424 66 | 67 | test get_totp_vector4_sha1 {} -body { 68 | set T 0x3F940AA 69 | onetimepass::get_hotp $sha1_key $T sha1 $token_length 70 | } -result 69279037 71 | 72 | test get_totp_vector4_sha256 {} -body { 73 | set T 0x3F940AA 74 | onetimepass::get_hotp $sha256_key $T sha2 $token_length 75 | } -result 90698825 76 | 77 | test get_totp_vector5_sha1 {} -body { 78 | set T 0x27BC86AA 79 | onetimepass::get_hotp $sha1_key $T sha1 $token_length 80 | } -result 65353130 81 | 82 | test get_totp_vector5_sha256 {} -body { 83 | set T 0x27BC86AA 84 | onetimepass::get_hotp $sha256_key $T sha2 $token_length 85 | } -result 77737706 86 | 87 | cleanupTests 88 | -------------------------------------------------------------------------------- /src/onetimepass.tcl: -------------------------------------------------------------------------------- 1 | ## 2 | ## 3 | ## Pure TCL implementation of HOTP and TOP algorithms 4 | ## 5 | ## API based on the onetimepass Python library: 6 | ## https://github.com/tadeck/onetimepass 7 | ## 8 | ## 9 | 10 | package require sha1 11 | package require sha256 12 | 13 | namespace eval onetimepass { 14 | # Set up state 15 | variable HOTP_AND_VALUE 0x7FFFFFFF 16 | 17 | # 18 | # 19 | # HMAC-based one time password (HOTP) as specified in RFC 4226 20 | # HOTP(K, C) = Truncate(HMAC-SHA-1(K,C)) 21 | # 22 | # :param key: string used to key the HMAC, aka, the secret 23 | # :type key: string 24 | # 25 | # :param interval_no: incrementing interval number to use for 26 | # producing the token 27 | # The C in HOTP(K, C) 28 | # :type interval_no: unsigned int 29 | # 30 | # :param digest: which HMAC digest to use 31 | # currently only supports sha1 or sha256 32 | # defaults to sha1 33 | # :type digest: string from the list {sha1 sha2} 34 | # 35 | # :param token_length: how long the resulting HOTP token will be 36 | # defaults to 6 as recommended in the RFC 37 | # :type token_length: int 38 | # 39 | # :return: generated HOTP token 40 | # :rtype: 0 padded string of the HOTP int token 41 | # 42 | # 43 | proc get_hotp {key interval_no {digest sha1} {token_length 6}} { 44 | variable HOTP_AND_VALUE 45 | 46 | # The message passed to the HMAC is the big-endian 64-bit 47 | # unsigned int representation of the interval_no 48 | set message [binary format Wu $interval_no] 49 | 50 | # Obtain the HMAC as a string of hex digits using the key and the message 51 | set hmac_digest [${digest}::hmac $key $message] 52 | 53 | # Obtain the starting offset into the HMAC to use for truncation 54 | # The starting offset is obtained by grabbing the last byte of the 55 | # of the HMAC, then bitwise-AND'ing it with 0xF 56 | # offset & 0xF is multiplied by 2 b/c it is a string of hex digits 57 | # and not the raw bytes 58 | scan [string range $hmac_digest end-1 end] %x offset 59 | set offset [expr {($offset & 0xF) * 2}] 60 | 61 | # For truncation, grab four bytes, starting at offset 62 | # It is offset + 7 b/c hmac_digest is a string of hex 63 | # digits and not raw bytes 64 | set four_bytes [binary format H* [string range $hmac_digest $offset $offset+7]] 65 | 66 | # Once the last four bytes are extracted, binary scan converts 67 | # the raw bytes into an unsigned 32-bit big-endian integer 68 | binary scan $four_bytes Iu1 token_base 69 | 70 | # Penultimate step: bitwise-AND token_base with 0x7FFFFFFF 71 | set token_base [expr {$token_base & $HOTP_AND_VALUE}] 72 | 73 | # Lastly, use mod to shorten the token to passed in length 74 | set token [expr {$token_base % 10**$token_length}] 75 | 76 | # 0 pad the token so it's exactly $token_length characters 77 | return [format "%0${token_length}d" $token] 78 | } 79 | 80 | # 81 | # 82 | # Time-based one time password (TOTP) as specified in RFC 6238 83 | # TOTP(K, T) = Truncate(HMAC-SHA-1(K,T)) 84 | # Same as HOTP but with C replaced by T, a time factor 85 | # 86 | # This implementation does not support setting a different value for T0. 87 | # It always uses the Unix epoch as the initial value to count the time steps. 88 | # 89 | # :param key: string used to key the HMAC, aka, the secret 90 | # :type key: string 91 | # 92 | # :param interval: Time interval in seconds that a TOTP token 93 | # is valid 94 | # Default is 30 as recommended by the RFC 95 | # See Section 5.2 for futher discussion 96 | # :type interval: unsigned int 97 | # 98 | # :param digest: which HMAC digest to use 99 | # currently only supports sha1 or sha256 100 | # defaults to sha1 101 | # :type digest: string from the list {sha1 sha2} 102 | # 103 | # :param token_length: how long the resulting TOTP token will be 104 | # defaults to 6 105 | # :type token_length: unsigned int 106 | # 107 | # :return: generated TOTP token 108 | # :rtype: 0 padded string of the TOTP token 109 | # 110 | # 111 | proc get_totp {key {interval 30} {digest sha1} {token_length 6}} { 112 | # TOTP is HOTP(K, C) with C replaced by T, a time factor 113 | set interval_no [expr [clock seconds] / $interval] 114 | 115 | return [get_hotp $key $interval_no $digest $token_length] 116 | } 117 | 118 | # 119 | # 120 | # Check if a given HOTP token is valid for the key passed in. Returns 121 | # the interval number that was successful, or -1 if not found. 122 | # 123 | # :param token: token being checked 124 | # :type token: string 125 | # 126 | # :param key: key, or secret, for which token is checked 127 | # :type key: str 128 | # 129 | # :param last: last used interval (start checking with next one) 130 | # To check the 0'th interval, pass -1 for last 131 | # :type last: int 132 | # 133 | # :param trials: number of intervals to check after 'last' 134 | # defaults to 1000 135 | # :type trials: unsigned int 136 | # 137 | # :param digest: which HMAC digest to use 138 | # currently only supports sha1 or sha256 139 | # defaults to sha1 140 | # :type digest: string from the list {sha1 sha2} 141 | # 142 | # :param token_length: length of the token (6 by default) 143 | # :type token_length: unsigned int 144 | # 145 | # :return: interval number, or -1 if check unsuccessful 146 | # :rtype: int 147 | # 148 | # 149 | proc valid_hotp {token key last {trials 1000} {digest sha1} {token_length 6}} { 150 | # Basic sanity check before looping 151 | if {![string is digit $token] || [string length $token] ne $token_length} { 152 | return -1 153 | } 154 | 155 | # Check each interval no 156 | for {set i 0} {$i <= $trials} {incr i} { 157 | set interval_no [expr $last + $i + 1] 158 | if {[get_hotp $key $interval_no $digest $token_length] eq $token} { 159 | return $interval_no 160 | } 161 | } 162 | 163 | return -1 164 | } 165 | 166 | # 167 | # 168 | # Check if a given TOTP token is valid for a given HMAC key 169 | # 170 | # :param token: token which is being checked 171 | # :type token: int or str 172 | # 173 | # :param key: HMAC secret key for which the token is being checked 174 | # :type key: str 175 | # 176 | # :param digest: which HMAC digest to use 177 | # currently only supports sha1 or sha256 178 | # defaults to sha1 179 | # :type digest: string from the list {sha1 sha2} 180 | # 181 | # :param token_length: length of the token (6 by default) 182 | # :type token_length: int 183 | # 184 | # :param interval: length in seconds of TOTP interval 185 | # (30 by default) 186 | # :type interval: int 187 | # 188 | # :return: 1 if valid, 0 otherwise 189 | # :rtype: int 190 | # 191 | # 192 | proc valid_totp {token key {interval 30} {digest sha1} {token_length 6}} { 193 | set calculated_token [get_totp $key $interval $digest $token_length] 194 | return [expr {$calculated_token eq $token}] 195 | } 196 | 197 | }; # namespace onetimepass 198 | --------------------------------------------------------------------------------