#!/usr/bin/env bash
# ux2fa.sh - a second factor you control: sends you a 6-digit code with
# UserAlertX and only continues if the code is typed back in (3 tries,
# 5 minutes). The code is sent to you by email.
#
# Guard one command:
#   ./ux2fa.sh sudo reboot
#   ./ux2fa.sh ./deploy-to-production.sh
# Just check (exit 0 = right code, 1 = not), for your own scripts:
#   ./ux2fa.sh && echo "verified"
#
# Guard SSH logins - after your password or key, SSH also asks for the code.
# In /etc/ssh/sshd_config (as root; then restart sshd):
#   Match User you
#       ForceCommand /usr/local/bin/ux2fa.sh --ssh
#       AllowTcpForwarding no
# Addresses you trust can skip the code: --ssh 192.168. 10.0.0.
# Keep a second session logged in while you test, so a mistake can't lock you
# out. Non-interactive SSH (scp, sftp, "ssh host command") is refused because
# there's nowhere to type a code; allow those from trusted addresses instead.
# The API key must be readable by the user logging in (~/.useralertx_key).
#
# If the code can't be sent (no network, bad key), access is refused.
# API key: USERALERTX_API_KEY env var, else ~/.useralertx_key (save it once
# with uxsend.sh). USERALERTX_DRYRUN=1 prints the code instead of sending it.

URL_API="${USERALERTX_URL:-https://useralertx.com/cgi-bin/itmessages.cgi}"
METHOD="${UX2FA_METHOD:-email}"

run_ssh_session() {
  [ -n "$SSH_ORIGINAL_COMMAND" ] && exec /bin/sh -c "$SSH_ORIGINAL_COMMAND"
  exec "${SHELL:-/bin/sh}" -l
}

SSH_MODE=""
if [ "$1" = "--ssh" ]; then
  SSH_MODE=1; shift
  FROM="${SSH_CONNECTION%% *}"
  for trusted in "$@"; do
    case "$FROM" in "$trusted"*) run_ssh_session ;; esac
  done
  set --
fi

if [ ! -t 0 ]; then
  echo "ux2fa: needs a terminal to type the code - refused." >&2
  exit 1
fi

KEY="${USERALERTX_API_KEY:-}"
[ -z "$KEY" ] && [ -r "$HOME/.useralertx_key" ] && KEY="$(tr -d '[:space:]' < "$HOME/.useralertx_key")"

CODE="$(printf '%06d' $(( $(od -An -N4 -tu4 /dev/urandom | tr -d ' ') % 1000000 )))"
WHAT="${*:-${SSH_MODE:+SSH login}}"; WHAT="${WHAT:-verification}"
FROM_TXT="${SSH_CONNECTION:+ from ${SSH_CONNECTION%% *}}"
MSG="Your code is $CODE for $WHAT as $(id -un) on $(hostname)$FROM_TXT. Expires in 5 minutes. If this wasn't you, someone has your password."

if [ -n "${USERALERTX_DRYRUN:-}" ]; then
  echo "[dry run] would send: $MSG"
elif [ -z "$KEY" ]; then
  echo "ux2fa: no API key (set USERALERTX_API_KEY or run uxsend.sh once to save it) - refused." >&2
  exit 1
else
  HTTP="$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 -A 'UserAlertX-example-ux2fa/1.0' \
    -H "X-Api-Key: $KEY" --data-urlencode "body=$MSG" --data-urlencode "method=$METHOD" "$URL_API")"
  if [ "$HTTP" != "200" ]; then
    echo "ux2fa: could not send the code (HTTP ${HTTP:-none}) - refused." >&2
    exit 1
  fi
  echo "A code was sent to you by $METHOD."
fi

DEADLINE=$(( $(date +%s) + 300 ))
for try in 1 2 3; do
  LEFT=$(( DEADLINE - $(date +%s) ))
  [ "$LEFT" -le 0 ] && break
  read -r -t "$LEFT" -p "Code: " GOT || break
  if [ "${GOT//[[:space:]]/}" = "$CODE" ]; then
    [ -n "$SSH_MODE" ] && run_ssh_session
    [ $# -gt 0 ] && exec "$@"
    exit 0
  fi
  echo "Wrong code."
done
echo "ux2fa: not verified - refused." >&2
exit 1
