Gestione TOTP in bash
In una giornata in cui avevo un po’ di tempo da investire, ho ripreso la versione 2 dello script “Generazione OTP via bash”, e ho deciso di renderlo un po’ più flessibile introducendo una logica crud. Anzi, il termine corretto dovrebbe essere “dave” (Delete, Add, View, Edit).
Che altro aggiungere?
- Il file è ancora un insieme di coppie chiave-valore (nome account e chiave privata).
- Il separatore è il simbolo “:”. Ma può essere configurato cambiando il valore della variabile “DELIMITER”.
- Lo script è discretamente commentato così, in futuro, riuscirò a ricordare perché ho fatto quello che ho fatto (tipo “Memento”).
- Le opzioni di gpg per la cifratura/decifratura del file sono configurabili nello script modificando il valore delle variabili “GPG_ENC_OPTIONS” e “GPG_DEC_OPTIONS”.
- L’uso di getopts, mi rendo conto, è solo un esercizio di stile. Non è molto utile per come l’ho usato, visto che ogni optarg corrisponde ad un’operazione autoconclusiva.
- Ero partito per rimpiazzare ogni costrutto if con combinazioni di lista and / lista or ma le condizioni risultavano talmente complesse da rendere il codice francamente molto più incomprensibile di adesso. In alternativa avrei potuto delegare l’esecuzione delle condizioni ad un insieme di funzioni con una stratificazione tale da ricadere nel punto precedente: offuscamento permanente del codice che va a vanificare l’intenzione iniziale di semplificare il codice e renderlo più leggibile.
#!/usr/local/bin/bash
init() {
# CODICI D'ERRORE
DECRYPT_ERR=64
INVALID_OPTION_ERR=65
INVALID_ACCOUNT_ERR=66
ACCOUNT_NOT_FOUND_ERR=67
ACCOUNT_NAME_EMPTY_ERR=68
ACCOUNT_EXISTS_ERR=69
INVALID_KEY_ERR=70
# Colorscheme
LIGHT_GREEN='\033[1;32m'
LIGHT_RED='\033[1;31m'
NC='\033[0m'
# Path del file cifrato contentente i codici 2FA
KEYS_2FA_FILE="<path/file_enc.gpg>"
# Separatore delle coppie chiave-valore
DELIMITER=":"
# path gpg e opzioni di cifratura/decifratura
GPG_TTY=$(tty)
export GPG_TTY
#GPG_COMMAND="/usr/local/bin/gpg"
GPG_ENC_OPTIONS="--yes -c --s2k-cipher-algo aes256 --s2k-digest-algo sha512 --s2k-mode 3 --s2k-count 260000 - "
GPG_DEC_OPTIONS='-d'
# Caratteri consentiti nel nome account
PATTERN='^[a-zA-Z0-9._-]+$';
# Su MacOS è disponibile "pbcopy", su *nix "xsel"
case "$(sysctl hw.targettype 2>/dev/null)" in
"")
CLIPBOARD="xsel -bi"
GPG_COMMAND="/usr/bin/gpg";;
*)
CLIPBOARD="pbcopy"
GPG_COMMAND="/usr/local/bin/gpg";;
esac
# Decifra e carica in ram il contenuto del file cifrato.
echo "Decifratura del file degli account in corso..."
KEYS_2FA_FILE_DEC=$(${GPG_COMMAND} ${GPG_DEC_OPTIONS} "${KEYS_2FA_FILE}" 2>/dev/null)
# Se la password non è corretta, restituisce un errore.
[[ -z "${KEYS_2FA_FILE_DEC}" ]] && { err_msg "${DECRYPT_ERR}" 1; }
info_msg "Fatto."
}
get_account() {
ACCOUNT_STDIO="$1"
# Check nome account vuoto
[[ -z "${ACCOUNT_STDIO}" ]] && return "${ACCOUNT_NAME_EMPTY_ERR}"
# Check caratteri invalidi nel nome account
[[ ! "${ACCOUNT_STDIO}" =~ $PATTERN ]] && return "${INVALID_ACCOUNT_ERR}"
# Estrae l'elemento contenente l'id dato in input
OTP_LINE=$(echo "${KEYS_2FA_FILE_DEC}" | grep "${ACCOUNT_STDIO}:")
# Se l'account non esiste, restituisce un errore
[[ -z "${OTP_LINE}" ]] && return "${ACCOUNT_NOT_FOUND_ERR}"
ACCOUNT=$(echo $OTP_LINE | cut -d"${DELIMITER}" -f 1)
KEY=$(echo $OTP_LINE | cut -d"${DELIMITER}" -f 2)
}
crypt_account_file() {
ACCOUNT_FILE="$1"
echo -e "\nCifratura del file degli account in corso..."
${GPG_COMMAND} -o "$KEYS_2FA_FILE" ${GPG_ENC_OPTIONS} <<< $(echo -e "${ACCOUNT_FILE}")
}
no_param() {
clear
help
exit 0
}
# Visualizza messaggi di errore su stdio
err_msg() {
if [[ $# -ne 2 ]]; then
echo -r"${LIGHT_RED}[DEBUG] [err_msg] Numero di parametri non corretto.${NC}."; exit 1
else
case $1 in
"${DECRYPT_ERR}" ) echo -e "${LIGHT_RED}[ERRORE] Impossibile decriptare il file.${NC}";;
"${INVALID_OPTION_ERR}" ) echo -e "${LIGHT_RED}[ERRORE] Opzione \"$OPTARG\" non valida.${NC}";;
"${INVALID_ACCOUNT_ERR}" ) echo -e "${LIGHT_RED}[ERRORE] Rilevati caratteri non consentiti.\nI caratteri ammessi sono: [a-z][A-Z][0-9].-_${NC}";;
"${ACCOUNT_NOT_FOUND_ERR}" ) echo -e "${LIGHT_RED}[ERRORE] Account non trovato${NC}";;
"${ACCOUNT_NAME_EMPTY_ERR}" ) echo -e "${LIGHT_RED}[ERRORE] Nome account vuoto.${NC}";;
"${ACCOUNT_EXISTS_ERR}" ) echo -e "${LIGHT_RED}[ERRORE] Nome account già esistente${NC}";;
"${INVALID_KEY_ERR}" ) echo -e "${LIGHT_RED}[ERRORE] Chiave privata nulla o non corretta.\nLa chiave privata deve essere base32.\n${NC}";;
* ) echo -e "${LIGHT_RED}[ERRORE] Errore generico.${NC}";;
esac
[[ $2 -eq 1 ]] && exit $2;
fi
}
# Visualizza messaggi info su stdio
info_msg() {
echo -e "${LIGHT_GREEN}$1${NC}"
}
continue_msg() {
case $1 in
"insert" ) ACTION="creato";;
"edit" ) ACTION="modificato";;
"delete" ) ACTION="cancellato";;
esac
echo -n "L'account \"${ACCOUNT}\" sta per essere ${ACTION}. Vuoi continuare? [y|N]: "; read ANSWER;
}
continue_yn() {
ANSWER="0"
while [[ "${ANSWER}" != "y" ]]; do
[[ -z "${ANSWER}" || "${ANSWER}" == "N" ]] && { echo "Operazione annullata"; exit; }
[[ "${ANSWER}" != "y" || "${ANSWER}" != "N" ]] && continue_msg "$1"
done
}
totp() {
# Se la'ccount non esiste o è invalido, esco con errore
get_account "$1"; RET_CODE=$?
case "${RET_CODE}" in
"${ACCOUNT_NOT_FOUND_ERR}" ) err_msg "${ACCOUNT_NOT_FOUND_ERR}" 1;;
"${INVALID_ACCOUNT_ERR}" ) err_msg "${INVALID_ACCOUNT_ERR}" 1;;
esac
echo -e "\nCopia OTP nella clipboard..."
# Calcola l'otp e lo trasferisce nella clipboard con xsel
oathtool --totp -b "${KEY}" | $CLIPBOARD
info_msg "Fatto."
exit
}
insert() {
# Se l'account non esiste o è invalido, esco con errore
get_account "$1"; RET_CODE=$?
case "${RET_CODE}" in
0 ) err_msg "${ACCOUNT_EXISTS_ERR}" 1;;
"${INVALID_ACCOUNT_ERR}" ) err_msg "${INVALID_ACCOUNT_ERR}" 1;;
esac
ACCOUNT="$1"
# Inserisci la chiave
echo -en "Nuovo account: ${ACCOUNT}\nInserisci la nuova chiave privata: "; read KEY
while [[ -z "${KEY}" || ! $(echo ${KEY} | oathtool -b - 2>/dev/null) ]]; do
err_msg "${INVALID_KEY_ERR}" 0
echo -n "Inserisci la chiave privata: "; read KEY
done
info_msg "\nNuovo account: ${ACCOUNT}\nNuova chiave privata: ${KEY}"
continue_yn "insert"
# Appendo il nuovo record nel file degli account
KEYS_2FA_FILE_DEC+="\n${ACCOUNT}${DELIMITER}${KEY}"
info_msg "L'account \"${ACCOUNT}\" è stato creato con successo."
# Cifratura del file degli account
crypt_account_file "${KEYS_2FA_FILE_DEC}"
info_msg "Fatto."
exit
}
edit() {
# Se la'ccount non esiste o è invalido, esco con errore
get_account "$1"; RET_CODE=$?
case "${RET_CODE}" in
"${ACCOUNT_NOT_FOUND_ERR}" ) err_msg "${ACCOUNT_NOT_FOUND_ERR}" 1;;
"${INVALID_ACCOUNT_ERR}" ) err_msg "${INVALID_ACCOUNT_ERR}" 1;;
esac
# Faccio una copia dell'account per un eventuale ripristino durante
# la fase di input
ACCOUNT_TEMP="${ACCOUNT}"
ACCOUNT_MOD="${ACCOUNT}"
# Inserisco il nuovo nome account.
# 1. Se è vuoto: è la conferma dell'account inserito all'inizio e proseguo
# 2. Se il nome è invalido: sollevo un'eccezione e reitero
# 3. Se il nome è valido:
# a. Se è un nome nuovo: acquisisco il nuovo nome, recupero nome e chiave dell'account di partenza e proseguo
# b. Sè è lo stesso nome di partenza: è la conferma dell'account iniziale e proseguo
# c. Se è un altro nome esistente o invalido: come il punto 2.
while true; do
echo
echo -en "Account: ${ACCOUNT}\nInserisci nuovo nome account: "; read ACCOUNT_MOD;
get_account "${ACCOUNT_MOD}"; RET_CODE=$?
case "${RET_CODE}" in
# Punto 1.
"${ACCOUNT_NAME_EMPTY_ERR}" ) ACCOUNT_MOD="${ACCOUNT}"; break;;
# Punto 2
"${INVALID_ACCOUNT_ERR}" ) err_msg "${INVALID_ACCOUNT_ERR}" 0;;
# Punto 3a.
"${ACCOUNT_NOT_FOUND_ERR}" ) get_account "${ACCOUNT_TEMP}"; break;;
# Punti 3b e 3c, rispettivamente
0 ) [[ "${ACCOUNT_MOD}" == "${ACCOUNT_TEMP}" ]] && break \
|| { get_account "${ACCOUNT_TEMP}"; err_msg "${ACCOUNT_EXISTS_ERR}" 0; };;
esac
done
[[ "${ACCOUNT}" != "${ACCOUNT_MOD}" && ! -z "${ACCOUNT_MOD}" ]] && msg="\nL'account \"${ACCOUNT}\" è stato modificato in \"${ACCOUNT_MOD}\"." \
|| msg="\nNessuna modifica rilevata.\n"
info_msg "${msg}"
# Modifica la chiave privata
KEY_MOD="${KEY}"
echo
while true; do
echo -en "Chiave privata: ${KEY}\nInserisci la nuova chiave privata: "; read KEY_MOD
if [[ -z $KEY_MOD ]]; then
KEY_MOD="${KEY}"; break
elif [[ ! $(echo "${KEY_MOD}" | oathtool -b - 2>/dev/null) ]]; then
err_msg "${INVALID_KEY_ERR}" 0; KEY_MOD="${KEY}"
else
break
fi
done
# Se c'è stata almeno una modifica (account/key), sovrascrivo il record. Altrimenti esco senza fare nulla.
if [[ "${ACCOUNT}" != "${ACCOUNT_MOD}" || "${KEY}" != "${KEY_MOD}" ]]; then
# Conferma modifica
info_msg "\nAccount: ${ACCOUNT}\nChiave privata: ${KEY}\nNuovo account: ${ACCOUNT_MOD}\nNuova chiave privata: ${KEY_MOD}\n"
continue_yn "edit"
# Modifica del file degli account
KEYS_2FA_FILE_DEC_TEMP=$(sed "s/${ACCOUNT}${DELIMITER}${KEY}/${ACCOUNT_MOD}${DELIMITER}${KEY_MOD}/" <<< "${KEYS_2FA_FILE_DEC}")
info_msg "L'account \"${ACCOUNT}\" è stato modificato con successo."
# Cifratura del file degli account
crypt_account_file "${KEYS_2FA_FILE_DEC_TEMP}"
else
info_msg "Nessuna modifica rilevata."
fi
info_msg "Fatto."
}
delete() {
if get_account "$1"; then
# Conferma eliminazione
info_msg "\nAccount: ${ACCOUNT}\nChiave privata: ${KEY}\n"
continue_yn "delete"
# Cancellazione account
KEYS_2FA_FILE_DEC_TEMP=$(sed "/$ACCOUNT/d" <<< "${KEYS_2FA_FILE_DEC}")
info_msg "L'account \"$ACCOUNT\" è stato eliminato."
# Cifratura del nuovo file degli account
crypt_account_file "${KEYS_2FA_FILE_DEC_TEMP}"
info_msg "Fatto."
else
err_msg "${ACCOUNT_NOT_FOUND_ERR}" 1
fi
}
list() {
echo
while read LINE; do
cut -d"${DELIMITER}" -f 1 <<< "${LINE}"
done <<< "${KEYS_2FA_FILE_DEC}"
}
help() {
cat<<EOF
Usa come: ./totp.sh [options] ARG
dove:
[options]
-l restituisce la lista degli account.
-t <nome_account> restituisce l'otp per quell'account.
-i <nome_account> inserisce un nuovo account.
-d <nome_account> cancella <nome_account>.
-e <nome_account> modifica <nome_account>.
-h stampa questa pagina di help.
ESEMPI:
RESTITUISCE LA LISTA DEGLI ACCOUNT
totp.sh -l
INSERISCE UN ACCOUNT
totp.sh -i dropbox
CANCELLA UN ACCOUNT
totp.sh -d megaupload
MODIFICA UN ACCOUNT
totp.sh -e paypal
GENERA OTP
totp.sh -t firefoxsync
EOF
}
main() {
init
while getopts "lt:i:e:d:" opt; do
case $opt in
l ) list ;;
t ) totp $OPTARG ;;
i ) insert $OPTARG ;;
e ) edit $OPTARG ;;
d ) delete $OPTARG ;;
* ) clear;err_msg "${INVALID_OPTION_ERR}" 1;;
esac
done
shift $(($OPTIND - 1))
}
[[ $# -eq 0 || $# -eq 1 && "$1" == "-h" ]] && no_param || main $*