#!/usr/bin/env bash VERSION="1.7" AUTHY_URL="https://api.authy.com" APP_ROOT=`dirname $0` CONFIG_FILE="$APP_ROOT/authy-ssh.conf" UPSTREAM_URL="https://raw.githubusercontent.com/authy/authy-ssh/master/authy-ssh" READ_TIMEOUT=60 MIN_API_KEY_SIZE=12 OK=0 FAIL=1 SEQ="seq" export TERM="xterm-256color" NORMAL=$(tput sgr0) GREEN=$(tput setaf 2; tput bold) YELLOW=$(tput setaf 3) RED=$(tput setaf 1) function red() { echo -e "$RED$*$NORMAL" } function green() { echo -e "$GREEN$*$NORMAL" } function yellow() { echo -e "$YELLOW$*$NORMAL" } function debug() { if [[ $DEBUG_AUTHY ]] then echo ">>> $*" fi } function check_dependencies() { if ! type "seq" > /dev/null 2>&1; then if ! type "jot" > /dev/null 2>&1; then red "You need installed on your system either 'seq' or 'jot' command" exit $FAIL else SEQ="jot" fi fi } function escape_input() { sed "s/[;\`\"\$\' ]//g" <<<$* } function escape_number() { sed 's/[^0-9]*//g' <<< $* } function os_version() { echo `uname -srm` } function read_input() { read -t "$READ_TIMEOUT" input echo "$(escape_input $input)" } function read_number() { read -t "$READ_TIMEOUT" number echo "$(escape_number $number)" } function require_root() { debug "Checking if user is root" find_sshd_config if [[ ! -w $SSHD_CONFIG ]] then red "root permissions are required to run this command. try again using sudo" exit $FAIL fi } function require_curl() { which curl 2>&1 > /dev/null if [ $? -eq 0 ] then return $OK fi # if `which` is not installed this check is ran curl --help 2>&1 > /dev/null return $FAIL } function user_agent() { os="$(os_version)" echo "User-Agent: AuthySSH/${VERSION} (${os})" } function find_sshd_config() { debug "Trying to find sshd_config file" if [[ -f /etc/sshd_config ]] then SSHD_CONFIG="/etc/sshd_config" elif [[ -f /etc/ssh/sshd_config ]] then SSHD_CONFIG="/etc/ssh/sshd_config" else red "Cannot find sshd_config in your server. Authy SSH will be enabled when you add the ForceCommand to it" fi } function add_force_command() { debug "Trying to add force command to $SSHD_CONFIG" find_sshd_config authy_ssh_command="$1" if [[ -w $SSHD_CONFIG ]] then yellow "Adding 'ForceCommand ${authy_ssh_command} login' to ${SSHD_CONFIG}" uninstall_authy "quiet" # remove previous installations echo -e "\nForceCommand ${authy_ssh_command} login" >> ${SSHD_CONFIG} echo "" check_sshd_config_file red " MAKE SURE YOU DO NOT MOVE/REMOVE ${authy_ssh_command} BEFORE UNINSTALLING AUTHY SSH" sleep 5 fi } function check_sshd_config_file() { yellow "Checking the validity of $SSHD_CONFIG file..." sshd -t if [ $? -ne 0 ] then red "sshd_config file is invalid. MAKE SURE YOU DO NOT RESTART THE SSH SERVER UNTIL YOU FIX IT." exit $FAIL fi } function install_authy() { source="$1" dest="$2/authy-ssh" if [[ ! $2 ]] then dest="/usr/local/bin/authy-ssh" # defaults to /usr/local/bin fi config_file="${dest}.conf" # # Ensure our target directory is present # set -e mkdir -p `dirname "${dest}"` set +e if [[ ! -r `dirname "${dest}"` ]] then red "${dest} is not writable. Try again using sudo" return $FAIL fi yellow "Copying ${source} to ${dest}..." cp "${source}" "${dest}" yellow "Setting up permissions..." chmod 755 $dest if [[ ! -f ${config_file} ]] then echo -n "Enter the Authy API key: " read authy_api_key if [ ${#authy_api_key} -lt ${MIN_API_KEY_SIZE} ] then red "you have entered a wrong API key" return $FAIL fi echo "Default action when api.authy.com cannot be contacted: " echo "" echo " 1. Disable two factor authentication until api.authy.com is back" echo " 2. Don't allow logins until api.authy.com is back" echo "" echo -n "type 1 or 2 to select the option: " read default_verify_action case $default_verify_action in 1) default_verify_action="disable" ;; 2) default_verify_action="enforce" ;; *) red "you have entered an invalid option" return $FAIL ;; esac yellow "Generating initial config on ${config_file}..." echo "banner=Good job! You've securely logged in with Authy." > "${config_file}" echo "api_key=${authy_api_key}" >> "${config_file}" echo "default_verify_action=${default_verify_action}" >> "${config_file}" else red "A config file was found on ${config_file}. Edit it manually if you want to change the API key" fi chmod 644 ${config_file} add_force_command "${dest}" echo "" echo "To enable two-factor authentication on your account type the following command: " echo "" if [[ $SUDO_USER ]] then green " sudo ${dest} enable $SUDO_USER " green " Example: sudo $0 enable $SUDO_USER myuser@example.com 1 401-390-9987" else green " sudo ${dest} enable $USER " green " Example: sudo $0 enable $USER myuser@example.com 1 401-390-9987" fi echo "" echo "To enable two-factor authentication on user account type: " echo "" green " sudo ${dest} enable " echo "" echo "To uninstall Authy SSH type:" echo "" green " sudo ${dest} uninstall" echo "" yellow " Restart the SSH server to apply changes" echo "" } function uninstall_authy() { find_sshd_config if [[ $1 != "quiet" ]] then yellow "Uninstalling Authy SSH from $SSHD_CONFIG..." fi if [[ -w $SSHD_CONFIG ]] then sed -ie '/^ForceCommand.*authy-ssh.*/d' $SSHD_CONFIG fi if [[ $1 != "quiet" ]] then green "Authy SSH was uninstalled." yellow "Now restart the ssh server to apply changes and then remove ${APP_ROOT}/authy-ssh and $CONFIG_FILE" fi } function check_config_file() { debug "Checking config file at $CONFIG_FILE" dir=`dirname ${CONFIG_FILE}` if [[ ! -r $dir ]] then red "ERROR: ${dir} cannot be written by $USER" return $FAIL fi if [[ ! -f $CONFIG_FILE ]] then red "Authy ssh has not been configured" # FIXME: add more info return $FAIL fi if [[ $1 == "writable" && ! -w $CONFIG_FILE ]] then red "$CONFIG_FILE is not writable. Please try again using sudo" exit $FAIL fi chmod 644 "$CONFIG_FILE" 2>/dev/null return $OK } # Checks if the API KEY is valid. This function receives one argument which can be: # - run: runs a shell even if the test fails. # - anything else: exits the command function check_api_key() { debug "Checking api key" if [ ${#AUTHY_API_KEY} -lt ${MIN_API_KEY_SIZE} ] then red "Cannot find a valid api key" run_shell fi return $OK } # Usage: $(read_config banner) function read_config() { key="$1" if [[ -f $CONFIG_FILE ]] then KEYFOUND=$FAIL while IFS='=' read -r ckey value do if [[ $ckey == $key ]] then echo $value # don't stop the loop so we can read repeated keys KEYFOUND=$OK fi done < $CONFIG_FILE return $KEYFOUND fi red "ERROR: $config_file couldn't be found" return $FAIL } function protect_user() { local_user="$(escape_input "$1")" if [[ $local_user == "" ]] then local_user="$USER" fi eval home_path="~$local_user" auth_keys="${home_path}/.ssh/authorized_keys" debug "Protecting user: $local_user with auth keys: $auth_keys" if [[ ! -w $home_path ]] then red "You don't have access to $auth_keys. Please try again as root." exit $FAIL fi echo -n "Enter your public ssh key: " read ssh_key grep "$ssh_key" "$auth_keys" 2>&1 >/dev/null grep_exit_code=$? if [[ $grep_exit_code -eq 0 ]] then yellow "The ssh key is already present in $auth_keys. Please remove it and try again." exit $FAIL fi register_user_on_authy "$local_user" if [[ $? -ne 0 || ! $authy_user_id ]] then exit $FAIL fi echo "command=\"$COMMAND login $authy_user_id\" $ssh_key" >> "${auth_keys}" if [[ $(whoami) == "root" ]] then chown "$local_user" "$auth_keys" fi green "The user is now protected with authy" } # usage: register_user "local_user" "" "" "" function register_user() { register_user_on_authy $* if [[ $? -ne 0 ]] then exit $FAIL fi if [[ $authy_user_id ]] then echo "user=$local_user:$authy_user_id" >> $CONFIG_FILE green "User was registered" else red "Cannot register user: $response" fi } function register_user_on_authy() { local_user="$(escape_input $1)" email="$(escape_input $2)" country_code="$(escape_number $3)" cellphone="$(escape_number $4)" if [[ $local_user == "" ]] then echo -n "Username: " local_user="$(read_input)" fi if [[ $country_code == "" ]] then echo -n "Your country code: " country_code="$(read_number)" fi if [[ $cellphone == "" ]] then echo -n "Your cellphone: " cellphone="$(read_number)" fi if [[ $email == "" ]] then echo -n "Your email: " email="$(read_input)" fi echo -e " Username:\t${local_user} Cellphone:\t(+${country_code}) ${cellphone} Email:\t${email} " echo -n "Do you want to enable this user? (y/n) " read should_continue if [[ "$should_continue" != y* ]] then return $FAIL fi url="$AUTHY_URL/protected/json/users/new?api_key=${AUTHY_API_KEY}" response=`id "${local_user}" 2>/dev/null` if [[ $? -ne 0 ]] then red "$local_user was not found in your system" return $FAIL fi useragent="$(user_agent)" response=`curl --connect-timeout 10 "${url}" -A "${useragent}" -d user[email]="${email}" -d user[country_code]="${country_code}" -d user[cellphone]="${cellphone}" -s 2>/dev/null` ok=true debug "[register-user] url: $url | response: $response | curl exit stats: $?" if [[ $response == *cellphone* ]] then yellow "Cellphone is invalid" ok=false fi if [[ $response == *email* ]] then yellow "Email is invalid" ok=false fi if [[ $ok == false ]] then return $FAIL fi if [[ $response == *user*id* ]] then authy_user_id=`echo $response | grep -o '[0-9]\{1,\}'` # match the authy id return $OK elif [[ $response == "invalid key" ]] then yellow "The api_key value in $CONFIG_FILE is not valid" else red "Unknown response: $response" fi return $FAIL } function run_shell() { if [[ "$SSH_ORIGINAL_COMMAND" != "" ]] # when user runs: ssh server then debug "running command: $SSH_ORIGINAL_COMMAND" exec /bin/bash -c "${SSH_ORIGINAL_COMMAND}" elif [ $SHELL ] # when user runs: ssh server then debug "running shell: $SHELL" exec -l $SHELL fi exit $? } function find_authy_id() { for user in `read_config user` do IFS=":"; declare -a authy_user=($user) if [[ ${authy_user[0]} == $USER ]] then echo $(escape_number ${authy_user[1]}) return $OK fi done return $FAIL } # login "token" "authy-id" function login() { mode="$(escape_input $1)" authy_token="$(escape_number $2)" authy_id="$(escape_number $3)" if [[ $authy_id == "" ]] then authy_id="$(find_authy_id)" fi debug "Logging $authy_id with $authy_token in $mode mode." if [[ $authy_token == "" ]] then red "You have to enter only digits." return $FAIL fi size=${#authy_token} if [[ $size -lt 6 || $size -gt 10 ]] then red "You have to enter a valid token." return $FAIL fi useragent="$(user_agent)" url="$AUTHY_URL/protected/json/verify/${authy_token}/${authy_id}?api_key=${AUTHY_API_KEY}&force=true" response=`curl --connect-timeout 30 -sL -w "|%{http_code}" -A "${useragent}" "${url}"` curl_exit_code=$? IFS='|' response_body=($response) # convert to array debug "[verify-token] url: $url | response: $response | curl exit status: $curl_exit_code" if [ $curl_exit_code -ne 0 ] # something went wrong when running the command, let it pass then red "Error running curl" fi default_verify_action="$(read_config default_verify_action)" if [[ $default_verify_action == "disable" ]] then debug "Checking if authy service is up." check_response=`curl --connect-timeout 10 -s "${AUTHY_URL}" -A "${useragent}" -o /dev/null` check_exit_code=$? if [[ $check_exit_code == 7 || $check_exit_code == 28 ]] then red "Letting user pass through. api.authy.com could not be reached." run_shell fi fi if [[ "${response_body[1]}" == "200" ]] && [[ "${response_body[0]}" == *\"token\":\"is*valid\"* ]] then debug "Two-factor token was accepted." if [[ ! $AUTHY_TOKEN ]] then banner_text=$(read_config banner) if [[ $? == $OK ]]; then green $banner_text fi fi if [[ $mode != "test" ]] then run_shell else debug "Test was successful" exit $OK fi else red "Invalid token. try again" fi return $FAIL # fail by default } function request_sms() { authy_id="$(escape_number $1)" url="$AUTHY_URL/protected/json/sms/${authy_id}?api_key=${AUTHY_API_KEY}&force=true" useragent="$(user_agent)" response=`curl --connect-timeout 10 -A "${useragent}" "${url}" 2>/dev/null` debug "[request sms] url: $url | response: $response | curl exit stats: $?" if [[ $response == *success*sent* ]] then green "SMS message was sent" elif [[ $response == *not*enabled* ]] then yellow "SMS is not enabled on Sandbox accounts. You need to upgrade to a Starter account at least." else red "Message couldn't be sent: $response" fi } # if this methods returns $OK the user is logged in. function ask_token_and_log_user_in() { mode="$(escape_input $1)" authy_id="$(escape_number $2)" if [[ $authy_id == "" ]] then authy_id="$(find_authy_id)" fi if [[ $authy_id == "" && $mode == "test" ]] #user is not using authy, let it go then red "Cannot find authy id for $USER in $CONFIG_FILE" red "You have to enable it using 'authy-ssh enable'" exit $FAIL fi if [[ $authy_id == "" ]] then debug "User is not protected with authy." run_shell fi times=3 if [[ $AUTHY_TOKEN ]] # env var then times=1 fi for i in `$SEQ 1 $times` do authy_token="$(escape_number $AUTHY_TOKEN)" if [[ ! $AUTHY_TOKEN ]] then echo -n "Authy Token (type 'sms' to request a SMS token): " authy_token="$(read_input)" fi if [ $? -ne 0 ] then debug "Timeout on Authy Token read." exit $? fi case $authy_token in sms) request_sms "${authy_id}" ;; *) authy_token="$(escape_number $authy_token)" login "${mode}" "${authy_token}" "${authy_id}" ;; esac done return $FAIL } function update_authy() { temp_file="$(mktemp -t tmpXXXXXXXXX)" curl "${UPSTREAM_URL}" -o "$temp_file" chmod 755 "$temp_file" echo -n "Do you want to overwrite ${COMMAND} (y/n)? " read opt if [[ "$opt" == "y" ]] then mv "${temp_file}" "${COMMAND}" green "Now type authy-ssh test to verify everything is working." else rm "${temp_file}" yellow "Authy SSH was not updated." fi } require_curl # get the absolute path to the command cd `dirname $0` COMMAND="$PWD/`basename $0`" cd - >/dev/null case $1 in install) check_dependencies install_authy "$0" "$2" ;; update) check_dependencies require_root update_authy ;; uninstall) require_root uninstall_authy ;; test|check) check_config_file check_dependencies AUTHY_API_KEY="$(read_config api_key)" check_api_key || exit ask_token_and_log_user_in "test" ;; enable|register) check_dependencies require_root check_config_file "writable" AUTHY_API_KEY="$(read_config api_key)" check_api_key register_user "$2" "$3" "$4" "$5" ;; login) check_config_file check_dependencies AUTHY_API_KEY="$(read_config api_key)" check_api_key ask_token_and_log_user_in "login" "$2" ;; protect) check_config_file check_dependencies AUTHY_API_KEY="$(read_config api_key)" protect_user "$2" ;; version) echo "Authy SSH v${VERSION}" exit 0 ;; *) cat <<__EOF__ Usage: authy-ssh VERSION $VERSION Available commands: install installs Authy SSH in the given directory. This command needs sudo if the directory is not writable. sudo $0 install /usr/local/bin update updates Authy SSH using the main authy-ssh script: ${UPSTREAM_URL} uninstall uninstalls Authy SSH from sshd_config sudo $0 uninstall test tests if the Authy SSH is working correctly enable receives a list of arguments needed to register a user. usage: sudo $0 enable [local-user] [email] [numeric country code] [cellphone] Example: sudo $0 enable myuser myuser@example.com 1 401-390-9987 protect installs authy-ssh for the given user $0 protect [user] login ask a token to the user if it is already registered. version prints the Authy SSH version __EOF__ ;; esac