#!/bin/bash ## zOTP Bash - Generates/Tests One Time Pins in Bash Scripts ## Version : 1.0 ## Author : Ze'ev Schurmann ## Git Repo : https://git.3volve.net.za/thisiszeev/zotp-bash ## Reddit : u/thisiszeev ## License : GPL3 ## ## One Time Pins generated using newOTP are unique to the minute. So from minute to minute the OTPs will be different. ## When using testOTP to verify an OTP, you can assign a validity time in minutes from 1 minute to 120 minutes. ## ## Either copy and paste the functions to your project or use "source /path/to/zotp.inc.sh" above your main code. ## If you copy and paste the functions in your project, please also include this commented out text. ## ## Don't forget to declare "thesalt" and "theseed" in your code before you call the functions newOTP and testOTP. ## ## "thesalt" must be a string unique to your project or to each instance of the project. This avoids other projects getting the same OTP at the same time. ## Example: ## declare thesalt="Name of project or service" ## **Only needs to be declared once, ideally at the start of your code** ## ## "theseed" must be a string unique to the user the OTP is for. This avoids other users getting the same OTP at the same time. ## Example: ## declare theseed="user@domain.tld" ## **Needs to be declared each time the current user changes** ## ## I do plan to port these functions to Perl, Python3 and PHP. I will make sure the OTPs created in one language can be verified in another. ## If there is demand for other languages then I am willing to put it on my to-do list. Send me a chat request on Reddit. ## Or maybe you keen to do a port to another language, send me a chat request and we can chat. I will link your port in the Git Repo for this project. function cmdAwk1 { ## cmdAwk1 is a pre/post command pipeline used by the function genHash. awk '{print $1}' } function genHash { ## genHash will calculate a hexidecimal hash string from a provided sting of any length or format using one of several Hashing Algorithms. ## Outputs the hash as a string of hexadecimal digits. Used by the functions newOTP and testOTP. ## ## Usage: ## genHash "{input-string}" {hash} ## {input-string} - A string of any kind. Supports ESCAPE codes such as \n \t \r \$ etc. [REQUIRED - MUST BE ENCLOSED IN SINGLE OR DOULBE QUOTES] ## {hash-algorithm} - The hashing algorithm used for generating the hexadecimal hash string [OPTIONAL - Defaults to MD5 if omitted] ## **The above must be given in the order of input-string first and optional hash-algorithm after. The input-string is case sensitive but the hash-algorithm is not case sensitive** ## ## Examples: ## Calculate a Blake2 Hash using a short string if text: ## variablename="$(genHash "This is a short string of text." b2)" ## ## Calculate a SHA512 Hash using a multiline string of text: ## variablename="$(genHash " ## This is the first line of text. ## This is the second line of text. ## This is the third line of text. ## " sha512)" ## ## Supported Hashing Algorithms: ## b2 - Blake2 (up to 512bit) - 128 digits - Very High Entropy - Fast ## md5 - Message Digest Algorithm 5 (128bit) - 32 digits - Very Low Entropy - Fast ## sha - Secure Hash Algorithm (160bit) - 40 digits - Low Entropy - Fast ## sha1 - Secure Hash Algorithm (160bit) - 40 digits - Low Entropy - Fast ## sha224 - Secure Hash Algorithm (224bit) - 56 digits - Medium Entropy - Slow ## sha256 - Secure Hash Algorithm (256bit) - 64 digits - High Entropy - Slow ## sha384 - Secure Hash Algorithm (384bit) - 96 digits - Very High Entropy - Medium ## sha512 - Secure Hash Algorithm (512bit) - 128 digits - Very High Entropy - Medium ## **sha and sha1 are the same algorithm, but I've included both variants as some systems execute it as shasum and others as sha1sum.** ## **Not all of these algorithms are available in Bash on all systems. Please test first. The function will output an error message** ## **to the terminal and give an exit code 3 if the chosen algorithm is not found on your system.** ## ## Exit Codes: ## 0 - Hash successfully calculated ## 11 - Function encounted an error and could not calculate the Hash (No data given) ## 23 - Function encounted an error and could not calculate the Hash (Invalid hashing algorithm given) ## 29 - Function encounted an error and could not calculate the Hash (Hashing Algorithm not found) local -A hashcmds=( [b2]="b2sum" [md5]="md5sum" [sha]="shasum" [sha1]="sha1sum" [sha224]="sha224sum" [sha256]="sha256sum" [sha384]="sha384sum" [sha512]="sha512sum" ) local -A precmds=( [b2]="" [md5]="" [sha]="" [sha1]="" [sha224]="" [sha256]="" [sha384]="" [sha512]="" ) local -A postcmds=( [b2]="cmdAwk1" [md5]="cmdAwk1" [sha]="cmdAwk1" [sha1]="cmdAwk1" [sha224]="cmdAwk1" [sha256]="cmdAwk1" [sha384]="cmdAwk1" [sha512]="cmdAwk1" ) if [[ -z "${1}" ]]; then echo "ERROR: function genHash - No data given... Must give string input..." >&2 return 11 fi if [[ -z "${2}" ]]; then local hashcmd="md5sum" local precmd="" local postcmd="cmdAwk1" else if [[ -z "${hashcmds[${2,,}]}" ]]; then echo "ERROR: function genHash - Invalid hashing algorithm given..." >&2 return 23 else local hashcmd="${hashcmds[${2,,}]}" local precmd="${precmds[${2,,}]}" local postcmd="${postcmds[${2,,}]}" fi fi if [[ -z "$(whereis "${hashcmd}" | awk '{print $2}')" ]]; then echo "ERROR: function genHash - Hashing Algorithm not found... ${hashcmd} is not supported on this system..." >&2 return 29 fi if [[ -z "${precmd}" ]] && [[ -z "${postcmd}" ]]; then echo -e "${1}" | ${hashcmd} elif [[ -z "${precmd}" ]] && [[ -n "${postcmd}" ]]; then echo -e "${1}" | ${hashcmd} | ${postcmd} elif [[ -n "${precmd}" ]] && [[ -z "${postcmd}" ]]; then echo -e "${1}" | ${precmd} | ${hashcmd} elif [[ -n "${precmd}" ]] && [[ -n "${postcmd}" ]]; then echo -e "${1}" | ${precmd} | ${hashcmd} | ${postcmd} fi return 0 } function genOTP { ## genOTP will calculate an OTP using 1 to 3 hexadecimal hash strings of 32 digits or more. Outputs the OTP as text digits. Used by the functions newOTP and testOTP. ## ## Usage: ## genOTP {hash-one} {hash-two} {hash-three} {number-of-digits} ## {hash-one} - A hexadecimal hash string of 32 digits or more [REQUIRED] ## {hash-two} - A hexadecimal hash string of 32 digits or more [OPTIONAL - Defaults to a string of zeros equal in length to hash-one if omitted] ## {hash-three} - A hexadecimal hash string of 32 digits or more [OPTIONAL - Defaults to a string of zeros equal in length to hash-one o hash-two, which ever is shorter, if omitted] ## {number-of-digits} - An integer from 4 to 16 [OPTIONAL - Defaults to 6 if omitted] ## **The above can be given in any order and is not case sensitive** ## ## Examples: ## Calculate a custom 5 digit OTP using a single hash: ## variablename="$(genOTP d03d5fc2f2b73c917cee8d0bc1153b5affe4a243 5)" ## ## Calculate a custom 8 digit OTP using three hashes: ## variablename="$(genOTP 09bf8c7563e7d577e07cca414b37c572a5e1676d 7540acafd1bb0ce0383e38859379e2c062cac45c 316fa3932ec66d9778be4df6132b4544d46ab146 8)" ## ## Exit Codes: ## 0 - OTP successfully calculated ## 11 - Function encounted an error and could not calculate OTP (No data given) ## 12 - Function encounted an error and could not calculate OTP (Too much data given) ## 13 - Function encounted an error and could not calculate OTP (Invalid data given) ## 72 - Function encounted an error and could not calculate OTP (Too many OTP size integers given) ## 81 - Function encounted an error and could not calculate OTP (No hexadecimal hash strings given) ## 82 - Function encounted an error and could not calculate OTP (Too many hexadecimal hash strings given) ## 84 - Function encounted an error and could not calculate OTP (Given hexadecimal hash string is too short) local optdigits=1 local opthashes=3 local digits=6 local -a hashes if [[ $# -eq 0 ]]; then echo "ERROR: function genOTP - No data given... Must give at least one hexadecimal hash string of at least 32 digits..." >&2 return 11 fi while [ $# -gt 0 ]; do if [[ ${optdigits} -eq 0 ]] && [[ ${opthashes} -eq 0 ]]; then echo "ERROR: function genOTP - Too much data given... Only one integer from 4 to 16 for number of OTP digits and one to three hexadecimal hash strings of 32 digits or more can be given..." >&2 return 12 fi if [[ "${1}" =~ ^([4-9]|1[0-6])$ ]]; then if [[ ${optdigits} -gt 0 ]]; then digits=${1} ((optdigits--)) else echo "ERROR: function genOTP - Too many OTP size integers given... Only one integer from 4 to 16 for number of OTP digits can be given..." >&2 return 72 fi elif [[ "${1}" =~ ^[0-9a-fA-F]+$ ]]; then if [[ ${opthashes} -gt 0 ]]; then if [[ ${#1} -lt 32 ]]; then echo "ERROR: function genOTP - Given hexadecimal hash string is too short... Only hexadecimal hash strings of 32 digits or more can be given..." >&2 return 84 fi hashes+=("${1,,}") ((opthashes--)) else echo "ERROR: function genOTP - Too many hexadecimal hash strings given... Only one to three hexadecimal hash strings of 32 digits or more can be given..." >&2 return 82 fi elif [[ "${1}" == "" ]]; then local donothing=0 else echo "ERROR: function genOTP - Invalid data given... Only one integer from 4 to 16 for number of OTP digits and one to three hexadecimal hash strings of 32 digits or more can be given..." >&2 return 13 fi shift done if [[ ${#hashes[@]} -eq 0 ]]; then echo "ERROR: function genOTP - No hexadecimal hash strings given... Must give at least one hexadecimal string of at least 32 digits..." >&2 return 81 fi local hashlength=${#hashes[0]} while [[ ${#hashes[@]} -lt 3 ]]; do hashes+=("$(printf '%0*d\n' "${hashlength}" 0)") done local n for ((n=1; n<3; n++)); do if [[ ${#hashes[${n}]} -lt ${hashlength} ]]; then hashlength=${#hashes[${n}]} fi done local -A hex2dec=( [0]=0 [1]=1 [2]=2 [3]=3 [4]=4 [5]=5 [6]=6 [7]=7 [8]=8 [9]=9 [a]=10 [b]=11 [c]=12 [d]=13 [e]=14 [f]=15 ) local -a values local position local sum for ((position=0; position&2 return 12 fi if [[ "${1}" =~ ^([4-9]|1[0-6])$ ]]; then if [[ ${optdigits} -gt 0 ]]; then digits=${1} ((optdigits--)) else echo "ERROR: function newOTP - Too many OTP size integers given... Only one integer from 4 to 16 for number of OTP digits can be given..." >&2 return 72 fi elif [[ "${1}" =~ ^[0-9a-zA-Z][0-9a-zA-Z]+$ ]]; then if [[ ${optsums} -gt 0 ]]; then hashsum="${1,,}" ((optsums--)) else echo "ERROR: function newOTP - Too many hash algorithm strings given... Only one string of 2 characters or more for Hash Algorithm can be given..." >&2 return 22 fi elif [[ "${1}" == "" ]]; then local donothing=0 else echo "ERROR: function newOTP - Invalid data given... Only one integer from 4 to 16 for number of OTP digits and one string of 2 characters or more for Hash Algorithm can be given..." >&2 return 13 fi shift done if [[ -z "${thesalt}" ]] || [[ "${thesalt}" == "" ]]; then local salthash="" echo "WARNING: Variable \${thesalt} has not been declared with a value... Assign it a string unique to the service the OTP is being used for to avoid other services getting the same OTP..." >&2 else local salthash="$(genHash "${thesalt}" "${hashsum}")" fi if [[ -z "${theseed}" ]] || [[ "${theseed}" == "" ]]; then local seedhash="" echo "WARNING: Variable \${theseed} has not been declared with a value... Assign it a string unique to the user account the OTP is being used for to avoid other users getting the same OTP..." >&2 else local seedhash="$(genHash "${theseed}" "${hashsum}")" fi local timehash="$(genHash "$(date -u "+%Y%B%d%H%M%A%U%u")" "${hashsum}")" genOTP "${seedhash}" "${salthash}" "${timehash}" ${digits} return 0 } function testOTP { ## testOTP will verify if an OTP is still valid and correct. Outputs the text "VALID" (exit code 0) or "INVALID" (exit code 255). ## ## Usage: ## testOTP {OTP} {valid-minutes} {hash-algorithm} {quiet} ## {OTP} - A 4 to 16 digit OTP to be verified [REQUIRED] ## {valid-minutes} - The time in minutes (from 1 to 120) the OTP is valid for [OPTIONAL - Defaults to 30 minutes if omitted] ## {hash-algorithm} - The hashing algorithm used for generating OTPs [OPTIONAL - Defaults to MD5 if omitted] ## See comments provided in the function genHash ## {quiet} - Disables text output when verifying allowing you to rely on exit codes instead. [OPTIONAL] ## You can use just the letter "q" as an alternative to "quiet" ## **The above can be given in any order and is not case sensitive** ## ## Examples: ## Using text output for validation over 15 minutes: ## declare thesalt="Name of project or service" ## declare theseed="user@domain.tld" ## if [[ "$(testOTP 123456 15)" == "VALID" ]]; then ## echo "OTP is valid..." ## else ## echo "OTP is invalid or expired..." ## fi ## ## Using exit code for validation over 45 minutes: ## declare thesalt="Name of project or service" ## declare theseed="user@domain.tld" ## if testOTP 123456 45 q; then ## echo "OTP is valid..." ## else ## echo "OTP is invalid or expired..." ## fi ## ## Exit Codes: ## 0 - OTP is VALID ## 11 - Function encounted an error and could not verify OTP (No data given) ## 12 - Function encounted an error and could not verify OTP (Too much data given) ## 13 - Function encounted an error and could not verify OTP (Invalid data given) ## 33 - Function encounted an error and could not verify OTP (Too many hash algorithm strings given) ## 41 - Function encounted an error and could not verify OTP (No OTP given) ## 43 - Function encounted an error and could not verify OTP (Too many OTPs given) ## 53 - Function encounted an error and could not verify OTP (Too many minute integers given) ## 55 - Function encounted an error and could not verify OTP (Minute integer given to high) ## 63 - Function encounted an error and could not verify OTP (Too many "quiet" strings given) ## 255 - OTP is INVALID or EXPIRED local optotps=1 local optvalids=1 local optsums=1 local optquiets=1 local theotp local quiet=false local validminutes=30 local hashsum="md5" local digits=0 if [[ $# -eq 0 ]]; then echo "ERROR: function testOTP - No data given... Must at least give one OTP of 4 to 16 digits..." >&2 return 11 fi while [ $# -gt 0 ]; do if [[ ${optotps} -eq 0 ]] && [[ ${optvalids} -eq 0 ]] && [[ ${optsums} -eq 0 ]] && [[ ${optquiets} -eq 0 ]]; then echo "ERROR: function testOTP - Too much data given... Only one OTP of 4 to 16 digits, one integer from 1 to 120 for valid time in minutes, one string of 2 characters or more for Hash Algorithm can be given and optional string \"quiet\" for silent mode relying on exit code 0 for MATCH and 255 for NO MATCH..." >&2 return 12 fi if [[ "${1}" =~ ^[0-9]+$ ]] && [[ ${#1} -ge 4 ]] && [[ ${#1} -le 16 ]]; then if [[ ${optotps} -gt 0 ]]; then theotp=${1} digits=${#theotp} ((optotps--)) else echo "ERROR: function testOTP - Too many OTPs given... Only one OTP of 4 to 16 digits can be given..." >&2 return 43 fi elif [[ "${1}" =~ ^([1-9]|[1-9][0-9]+)$ ]]; then if [[ ${optvalids} -gt 0 ]]; then if [[ ${1} -gt 120 ]]; then echo "ERROR: function testOTP - Minute integer given to high... Integer must be from 1 to 120 for valid time in minutes can be given..." >&2 return 55 fi validminutes=${1} ((optvalids--)) else echo "ERROR: function testOTP - Too many minute integers given... Only one integer from 1 to 120 for valid time in minutes can be given..." >&2 return 53 fi elif [[ "${1,,}" =~ ^(q|quiet)$ ]]; then if [[ ${optquiets} -gt 0 ]]; then quiet=true ((optquiets--)) else echo "ERROR: function testOTP - Too many \"quiet\" strings given... Only one optional string \"quiet\" for silent mode can be given relying on exit code 0 for MATCH and 255 for NO MATCH..." >&2 return 63 fi elif [[ "${1}" =~ ^[0-9a-zA-Z][0-9a-zA-Z]+$ ]]; then if [[ ${optsums} -gt 0 ]]; then hashsum="${1,,}" ((optsums--)) else echo "ERROR: function testOTP - Too many hash algorithm strings given... Only one string of 2 characters or more for Hash Algorithm can be given..." >&2 return 33 fi elif [[ "${1}" == "" ]]; then local donothing=0 else echo "ERROR: function testOTP - Invalid data given... Only one OTP of 4 to 16 digits, one integer from 1 to 120 for valid time in minutes, one string of 2 characters or more for Hash Algorithm can be given and optional string \"quiet\" for silent mode relying on exit code 0 for MATCH and 255 for NO MATCH..." >&2 return 13 fi shift done if [[ ${digits} -eq 0 ]]; then echo "ERROR: function testOTP - No OTP given... Must give one OTP of 4 to 16 digits..." >&2 return 41 fi if [[ -z "${thesalt}" ]] || [[ "${thesalt}" == "" ]]; then local salthash="" echo "WARNING: Variable \${thesalt} has not been declared with a value... Assign it a string unique to the service the OTP is being used for to avoid other services getting the same OTP..." >&2 else local salthash="$(genHash "${thesalt}" "${hashsum}")" fi if [[ -z "${theseed}" ]] || [[ "${theseed}" == "" ]]; then local seedhash="" echo "WARNING: Variable \${theseed} has not been declared with a value... Assign it a string unique to the user account the OTP is being used for to avoid other users getting the same OTP..." >&2 else local seedhash="$(genHash "${theseed}" "${hashsum}")" fi local n ((validminutes++)) for ((n=0; n<${validminutes}; n++)); do local timehash="$(genHash "$(date -u -d "${n} minutes ago" "+%Y%B%d%H%M%A%U%u")" "${hashsum}")" genOTP "${seedhash}" "${salthash}" "${timehash}" ${digits} done | grep -q ^${theotp}$ if [[ $? -eq 0 ]]; then if [[ ${quiet} == false ]]; then echo "VALID" fi return 0 else if [[ ${quiet} == false ]]; then echo "INVALID" fi return 255 fi }