Files
zotp-bash/zotp.inc.sh
2026-06-06 21:36:42 +02:00

520 lines
21 KiB
Bash

#!/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<hashlength; position++)); do
(( sum =
hex2dec[${hashes[0]:position:1}] +
hex2dec[${hashes[1]:position:1}] +
hex2dec[${hashes[2]:position:1}]
))
(( value = (sum + sum % 3 + 1) % 16 ))
values+=(${value})
done
local offset=$((hashlength/digits))
local value
for ((digit=0; digit<digits; digit++)); do
value=0
for ((position=0; position<offset; position++)); do
value=$((value+${values[$((position*digits+digit))]}))
done
value=$((value%10))
echo -n ${value}
done
echo
return 0
}
function newOTP {
## newOTP will create a new OTP based on the current time (to the minute). Outputs the OTP as text digits.
##
## Usage:
## newOTP {number-of-digits} {hash-algorithm}
## {number-of-digits} - An integer from 4 to 16 [OPTIONAL - Defaults to 6 if omitted]
## {hash-algorithm} - The hashing algorithm used for generating OTPs [OPTIONAL - Defaults to MD5 if omitted]
## See comments provided in the function genHash
## **The above can be given in any order and is not case sensitive**
##
## Examples:
## Create a new 7 digit OTP using Default Hashing Agorithm:
## declare thesalt="Name of project or service"
## declare theseed="user@domain.tld"
## variablename="$(newOTP 7)"
##
## Create a new 4 digit OTP using SHA384:
## declare thesalt="Name of project or service"
## declare theseed="user@domain.tld"
## variablename="$(newOTP 4 sha384)"
##
## Exit Codes:
## 0 - OTP successfully created
## 12 - Function encounted an error and could not create OTP (Too much data given)
## 13 - Function encounted an error and could not create OTP (Invalid data given)
## 22 - Function encounted an error and could not create OTP (Too many hash algorithm strings given)
## 72 - Function encounted an error and could not create OTP (Too many OTP size integers given)
local optdigits=1
local optsums=1
local digits=6
local hashsum="md5"
while [ $# -gt 0 ]; do
if [[ ${optdigits} -eq 0 ]] && [[ ${optsums} -eq 0 ]]; then
echo "ERROR: function newOTP - Too much 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 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
}