Skip to content

Instantly share code, notes, and snippets.

@umrysh
Last active September 15, 2021 07:22
Show Gist options
  • Save umrysh/e26ec78bf8428ad4e279 to your computer and use it in GitHub Desktop.
Save umrysh/e26ec78bf8428ad4e279 to your computer and use it in GitHub Desktop.
Time-based One-time Passwords for Email
#!/bin/bash
# Example Dovecot checkpassword script that may be used as both passdb or userdb.
#
# Originally written by Nikolay Vizovitin, 2013.
# Assumes authentication DB is in /etc/dovecot/users, each line has '<user>:<password>' format.
# Place this script into /etc/dovecot/checkpassword.sh file and make executable.
# Implementation guidelines at http://wiki2.dovecot.org/AuthDatabase/CheckPassword
# The first and only argument is path to checkpassword-reply binary.
# It should be executed at the end if authentication succeeds.
CHECKPASSWORD_REPLY_BINARY="$1"
# Messages to stderr will end up in mail log (prefixed with "dovecot: auth: Error:")
LOG=/dev/stderr
# User and password will be supplied on file descriptor 3.
INPUT_FD=3
# Error return codes.
ERR_PERMFAIL=1
ERR_NOUSER=3
ERR_TEMPFAIL=111
# Make testing this script easy. To check it just run:
# printf '%s\0%s\0' <user> <password> | ./checkpassword.sh test; echo "$?"
if [ "$CHECKPASSWORD_REPLY_BINARY" = "test" ]; then
CHECKPASSWORD_REPLY_BINARY=/bin/true
INPUT_FD=0
fi
# Credentials lookup function. Given a user name it should output 'user:password' if such
# account exists or nothing if it does not. Return non-zero code in case of error.
credentials_lookup()
{
local db="$1"
local user="$2"
awk -F ':' -v USER="$user" '($1 == USER) {print}' "$db" 2>>$LOG
}
# Credentials verification function. Given a user name and password it should output non-empty
# string (this implementation outputs 'user:password') in case supplied credentials are valid
# or nothing if they are not. Return non-zero code in case of error.
credentials_verify()
{
local db="$1"
local user="$2"
local pass="$3"
local cached="$4"
local ipfile="$5"
local ip="$TCPREMOTEIP"
#local ip="$TCPLOCALIP"
#local ip="192.168.149.100"
local timestamp="$(date +%s)"
#local defaultTime=300
local defaultTime=10
local expire=""
if [ -f "$cached" ]; then
expire=`awk -F ':' -v USER="$user" -v IP="$ip" -v PASS="$pass" '($1 == USER && $2 == IP && $3 == PASS) {print $4}' "$cached"`
fi
if [ ! -z "$expire" ]; then
if [ "$timestamp" -gt "$expire" ]; then
# Remove from cache
sed -i "/$user:$ip:$pass/d" "$cached"
#echo "cached is old. fail log in"
log_result_basic "cached is old. fail log in"
else
#echo "cache is current. allow log on"
log_result_basic "cache is current. allow log on"
echo "true"
fi
else
if python2.7 /etc/dovecot/getTOTP.py -v `awk -F ':' -v USER="$user" '($1 == USER) {print $2}' "$db"` "$pass" | grep 'True';
then
#echo "allow log on"
log_result_basic "allow log on"
if [ -f "$ipfile" ]; then
timeforip=`awk -F ':' -v IP="$ip" '($1 == IP) {print $2}' "$ipfile"`
if [ -z "$timeforip" ]; then
# Use default time cache
#echo "Using default time"
log_result_basic "Using default time"
echo "$user:$ip:$pass:$(($timestamp+$defaultTime))" >> "$cached"
else
#echo "Using stored time"
log_result_basic "Using stored time"
echo "$user:$ip:$pass:$(($timestamp+$timeforip))" >> "$cached"
fi
else
# Use default time cache
#echo "Using default time"
log_result_basic "Using default time"
echo "$user:$ip:$pass:$(($timestamp+$defaultTime))" >> "$cached"
fi
echo "true"
fi
fi
}
# Just a simple logging helper.
log_result()
{
echo "$*; Input: $USER:$PASS; Home: $HOME; Reply binary: $CHECKPASSWORD_REPLY_BINARY" >>$LOG
}
# Just a simpler logging helper.
log_result_basic()
{
echo "$*; Input: $USER:$PASS" >>$LOG
}
# Read input data. It is available from $INPUT_FD as "${USER}\0${PASS}\0".
# Password may be empty if not available (i.e. if doing credentials lookup).
read -d $'\0' -r -u $INPUT_FD USER
read -d $'\0' -r -u $INPUT_FD PASS
# Both mailbox and domain directories should be in lowercase on file system.
# So let's convert login user name to lowercase and tell Dovecot that 'user' and 'home'
# (which overrides 'mail_home' global parameter) values should be updated.
# Of course, conversion to lowercase may be done in Dovecot configuration as well.
export USER="`echo \"$USER\" | tr 'A-Z' 'a-z'`"
#mail_name="`echo \"$USER\" | awk -F '@' '{ print $1 }'`"
#domain_name="`echo \"$USER\" | awk -F '@' '{ print $2 }'`"
export HOME=`python2.7 /etc/dovecot/getHome.py $USER`
# CREDENTIALS_LOOKUP=1 environment is set when doing non-plaintext authentication.
if [ "$CREDENTIALS_LOOKUP" = 1 ]; then
action=credentials_lookup
else
action=credentials_verify
fi
# Perform credentials lookup/verification.
lookup_result=`$action "/etc/dovecot/users" "$USER" "$PASS" "/etc/dovecot/cached" "/etc/dovecot/ipfile"` || {
# If it failed, consider it an internal temporary error.
# This usually happens due to permission problems.
log_result "internal error (ran as `id`)"
exit $ERR_TEMPFAIL
}
if [ -n "$lookup_result" ]; then
# Dovecot calls the script with AUTHORIZED=1 environment set when performing a userdb lookup.
# The script must acknowledge this by changing the environment to AUTHORIZED=2,
# otherwise the lookup fails.
[ "$AUTHORIZED" != 1 ] || export AUTHORIZED=2
# And here's how to return extra fields from userdb/passdb lookup, e.g. 'uid' and 'gid'.
# All virtual mail users in Plesk actually run under 'popuser'.
# See also:
# http://wiki2.dovecot.org/PasswordDatabase/ExtraFields
# http://wiki2.dovecot.org/UserDatabase/ExtraFields
# http://wiki2.dovecot.org/VirtualUsers
export userdb_uid=vmail
export userdb_gid=vmail
export EXTRA="userdb_uid userdb_gid $EXTRA"
if [ "$CREDENTIALS_LOOKUP" = 1 ]; then
# If this is a credentials lookup, return password together with its scheme.
# The password scheme that Dovecot wants is available in SCHEME environment variable
# (e.g. SCHEME=CRAM-MD5), however 'PLAIN' scheme can be converted to anything internally
# by Dovecot, so we'll just return 'PLAIN' password.
found_password="`echo \"$lookup_result\" | awk -F ':' '{ print $2 }'`"
export password="{PLAIN}$found_password"
export EXTRA="password $EXTRA"
log_result "credentials lookup result: '$password' [SCHEME='$SCHEME', EXTRA='$EXTRA']"
else
log_result "lookup result: '$lookup_result'"
fi
# At the end of successful authentication execute checkpassword-reply binary.
exec $CHECKPASSWORD_REPLY_BINARY
else
# If matching credentials were not found, return proper error code depending on lookup mode.
if [ "$AUTHORIZED" = 1 -a "$CREDENTIALS_LOOKUP" = 1 ]; then
log_result "lookup failed (user not found)"
exit $ERR_NOUSER
else
log_result "lookup failed (credentials are invalid)"
exit $ERR_PERMFAIL
fi
fi
# Built for python 2.7
import MySQLdb as mdb
import sys
host = "127.0.0.1"
database = "vmail"
username = "vmail"
password = "VeNUlA6xKjpfceGsN3Ull8hLmAVjc3"
def main():
if len(sys.argv) == 2:
con = mdb.connect(host=host, port=3306,user=username, passwd=password, db=database)
cur = con.cursor()
cur.execute("SELECT CONCAT(mailbox.storagebasedirectory, '/', mailbox.storagenode, '/', mailbox.maildir) AS home FROM mailbox where username = '%s'" % con.escape_string(sys.argv[1]))
row = cur.fetchone()
if row is not None:
sys.stdout.write("%s" % row[0])
else:
sys.stdout.write("")
else:
sys.stdout.write("")
sys.stdout.flush()
sys.exit(0)
if __name__ == "__main__":
main()
# Built for python 2.7
import onetimepass as otp
import sys,base64,os
def main():
if len(sys.argv) >= 2:
if sys.argv[1] == "-v":
sys.stdout.write("%s" % otp.valid_totp(token=sys.argv[3], secret=sys.argv[2]))
elif sys.argv[1] == "-c":
try:
sys.stdout.write("%s" % otp.get_totp(sys.argv[2]))
except:
sys.stdout.write("")
elif sys.argv[1] == "-g":
sys.stdout.write("%s" % base64.b32encode(os.urandom(10)))
else:
sys.stdout.write("")
sys.stdout.flush()
sys.exit(0)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment