Gestione TOTP in bash

totp

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?


#!/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 $*

#bash #totp #cryptography #gpg #scripting