├── .gitignore ├── LICENSE ├── README.md ├── gendoc.sh ├── otplib.nim ├── otplib.nimble ├── otplib └── private │ └── utils.nim └── tests ├── config.nims └── test1.nim /.gitignore: -------------------------------------------------------------------------------- 1 | /src/otplib 2 | /otplib.out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OTPLIB - One Time Password Library for Nim [![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/) 2 | [![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com) 3 | 4 | 5 | OTPLIB is a Nim package for generating, managing and verifying one-time passwords. 6 | It can be used to implement 2FA or MFA in web applications and other systems that require users to log in. 7 | 8 | Multi-factor Authentication standards are defined in: 9 | 10 | - [RFC 4226](https://tools.ietf.org/html/rfc4226) - HOTP 11 | - [RFC 6238](https://tools.ietf.org/html/rfc6238) - TOTP 12 | 13 | OTPLIB was inspired by other OTP libraries like [GOTP](https://github.com/xlzd/gotp) and [PyOTP](https://github.com/pyauth/pyotp). 14 | 15 | ## TODO 16 | - [ ] Handle Google's key URI format 17 | - [ ] Add support for more hash modes 18 | 19 | ## Installation 20 | To install run: 21 | ```bash 22 | $ nimble install otplib 23 | ``` 24 | 25 | 26 | To include it in your project add this to your nimble file: 27 | ```nim 28 | requires "otplib" 29 | ``` 30 | and import it: 31 | ```nim 32 | import otplib 33 | ``` 34 | 35 | ## Usage 36 | **See:** [Documentation](https://dimspith.com/docs/otplib/) 37 | 38 | ## Contributing, feature requests and bug reports 39 | Contributions are welcome 💕 40 | 41 | Make sure to run `nimpretty` on your changes to maintain a consistent style. 42 | -------------------------------------------------------------------------------- /gendoc.sh: -------------------------------------------------------------------------------- 1 | #!env bash 2 | dir=$(dirname "$0") 3 | cd "$dir" 4 | rm -rf "$dir"/htmldocs 5 | nim doc --project --index:on --git.url:https://github.com/dimspith/otplib --outdir:htmldocs otplib.nim 6 | -------------------------------------------------------------------------------- /otplib.nim: -------------------------------------------------------------------------------- 1 | ## This library handles generation and usage of One Time Passwords (HOTP & TOTP). 2 | ## It aims to be easy to use and easy to read and understand. 3 | 4 | runnableExamples: 5 | let 6 | secret = genRandomSecret(30) 7 | totp = newTOTP(secret) 8 | echo "Current code: ", totp.gen() 9 | 10 | runnableExamples: 11 | let 12 | secret = genRandomSecret(30) 13 | hotp = newHOTP(secret) 14 | echo "Code at iteration 1:", hotp.gen(1) 15 | 16 | runnableExamples: 17 | let 18 | secret = genRandomSecret(30) 19 | totp = newTOTP(secret) 20 | echo "Current code is valid for: ", codeValidFor(totp.interval), " seconds." 21 | 22 | import std/sha1, math, std/random 23 | from times import epochTime, getTime, toUnix, nanosecond 24 | 25 | import otplib/private/[utils] 26 | 27 | # Type definitions of HOTP and TOTP 28 | type 29 | HOTP = object 30 | digits: int 31 | secret: string 32 | TOTP = object 33 | hotp: HOTP 34 | interval*: int 35 | 36 | proc genRandomSecret*(length: int): string = 37 | ## Generate a random secret. Secrets are randomized using the current time down to nanoseconds as a seed. 38 | 39 | doAssert length > 0, msg = "Secret length must not be 0!" 40 | 41 | # Initialize the random number generator 42 | let now = getTime() 43 | randomize(now.toUnix * 1_000_000_000 + now.nanosecond) 44 | 45 | # Pick `length` random characters. 46 | const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" 47 | result = newString(length) 48 | for i in 0..= 1.6.0" 13 | -------------------------------------------------------------------------------- /otplib/private/utils.nim: -------------------------------------------------------------------------------- 1 | import strutils, std/sha1 2 | 3 | proc decodeBase32*(s: string): string = 4 | ## Decodes a base32 string. 5 | var ch, index, bits, buffer: int = 0 6 | 7 | # The resulting string is 5/8 the length of the original (rounded up) 8 | let strLen = (s.len * 5) div 8 9 | result.setLen(strLen) 10 | 11 | # Loop through the string, excluding the padding (=) 12 | for i in 0 ..< (s.len - s.count('=')): 13 | 14 | # Map character to it's base32 symbol chart value 15 | ch = s[i].ord 16 | case ch 17 | of 65..90: # A-Z 18 | ch -= 65 19 | of 97..122: # a-z 20 | ch -= 97 21 | of 50..55: # 2-7 22 | ch -= 24 23 | else: 24 | raise newException(ValueError, "Non-base32 digit found: " & $ch) 25 | 26 | # Increase buffer's binary size by 5 27 | # i.e 10101 -> 1010100000 28 | buffer = buffer shl 5 29 | 30 | # Append the character's value 31 | # i.e 1010100000 + 01110 -> 1010101110 32 | buffer = buffer or ch 33 | 34 | bits += 5 35 | 36 | if bits >= 8: 37 | bits -= 8 38 | 39 | # The result is the buffer truncated to 8 bits (1 char) 40 | result[index] = chr(buffer shr bits and 255) 41 | index += 1 42 | 43 | proc toString(data: openarray[byte], start, stop: int): string = 44 | ## Slice a raw data blob into a string 45 | ## This is an inclusive slice 46 | ## The output string is null-terminated for raw C-compat 47 | assert start in 0 ..< data.len 48 | assert stop in 0 ..< data.len 49 | 50 | let len = stop - start + 1 51 | assert len in 0 .. data.len 52 | 53 | result = newString(len) 54 | copyMem(result[0].addr, data[start].unsafeAddr, len) 55 | 56 | proc int_to_bytestring*(input: int, padding: int = 8): string {.inline.} = 57 | var input = input 58 | 59 | var arr: seq[char] = @[] 60 | while input != 0: 61 | arr.add(char(input and 0xFF)) 62 | input = input shr 8 63 | 64 | while arr.len < padding: 65 | arr.add('\0') 66 | 67 | result = newString(arr.len) 68 | for i in 0..arr.len-1: 69 | result[i] = arr[arr.len - i - 1] 70 | 71 | 72 | proc hmac_sha1*(key: string, message: string): SecureHash = 73 | 74 | # Default block size for sha1 75 | const blockSize: int = 64 76 | 77 | var 78 | newKey: seq[byte] 79 | outPadKey: seq[byte] 80 | inPadKey: seq[byte] 81 | 82 | # Shorten keys longer than blockSize by hashing 83 | if key.len > blockSize: 84 | for i in $secureHash(key): 85 | newKey.add(i.byte) 86 | else: 87 | for i in key: 88 | newKey.add(i.byte) 89 | 90 | # Pad key with zeros to the right if shorter than blocksize 91 | if newKey.len < blockSize: 92 | for i in 0 ..< blockSize - key.len: 93 | newKey.add(0.byte) 94 | 95 | # Generate outer padded and inner padded keys 96 | for i in 0 ..< blockSize: 97 | outPadKey.add(newKey[i] xor 0x5c) 98 | inPadKey.add(newKey[i] xor 0x36) 99 | 100 | # Append message bytes to outPadKey 101 | for bt in secureHash(inPadKey.toString(0, inPadKey.len-1) & 102 | message).Sha1Digest: 103 | outPadKey.add(bt) 104 | 105 | # Calculate the resulting hash 106 | result = secureHash(outPadKey.toString(0, outPadKey.len-1)) 107 | 108 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /tests/test1.nim: -------------------------------------------------------------------------------- 1 | # This is just an example to get you started. You may wish to put all of your 2 | # tests into a single file, or separate them into multiple `test1`, `test2` 3 | # etc. files (better names are recommended, just make sure the name starts with 4 | # the letter 't'). 5 | # 6 | # To run these tests, simply execute `nimble test`. 7 | 8 | import unittest 9 | 10 | import otplib 11 | test "can add": 12 | check add(5, 5) == 10 13 | --------------------------------------------------------------------------------