<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>totp &amp;mdash; Cyberdyne Systems</title>
    <link>https://noblogo.org/aytin/tag:totp</link>
    <description>&#34;Fare o non fare. Non c&#39;è provare!&#34;</description>
    <pubDate>Thu, 30 Apr 2026 09:58:43 +0000</pubDate>
    <item>
      <title>Gestione TOTP in bash</title>
      <link>https://noblogo.org/aytin/gestione-totp-in-bash</link>
      <description>&lt;![CDATA[totp&#xA;&#xA;In una giornata in cui avevo un po’ di tempo da investire, ho ripreso la versione 2 dello script &#34;Generazione OTP via bash&#34;, e ho deciso di renderlo un po’ più flessibile introducendo una logica crud. Anzi, il termine corretto dovrebbe essere &#34;dave&#34; (Delete, Add, View, Edit).&#xA;!--more--&#xA;Che altro aggiungere?&#xA;&#xA;Il file è ancora un insieme di coppie chiave-valore (nome account e chiave privata).&#xA;Il separatore è il simbolo &#34;:&#34;. Ma può essere configurato cambiando il valore della variabile &#34;DELIMITER&#34;.&#xA;Lo script è discretamente commentato così, in futuro, riuscirò a ricordare perché ho fatto quello che ho fatto (tipo &#34;Memento&#34;).&#xA;Le opzioni di gpg per la cifratura/decifratura del file sono configurabili nello script modificando il valore delle variabili &#34;GPG\ENC\OPTIONS&#34; e &#34;GPG\DEC\OPTIONS&#34;.&#xA;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.&#xA;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.&#xA;&#xA;!/usr/local/bin/bash&#xA;&#xA;init() {&#xA;    # CODICI D&#39;ERRORE&#xA;    DECRYPTERR=64&#xA;    INVALIDOPTIONERR=65&#xA;    INVALIDACCOUNTERR=66&#xA;    ACCOUNTNOTFOUNDERR=67&#xA;    ACCOUNTNAMEEMPTYERR=68&#xA;    ACCOUNTEXISTSERR=69&#xA;    INVALIDKEYERR=70&#xA;&#xA;    # Colorscheme&#xA;    LIGHTGREEN=&#39;\033[1;32m&#39;&#xA;    LIGHTRED=&#39;\033[1;31m&#39;&#xA;    NC=&#39;\033[0m&#39;&#xA;&#xA;    # Path del file cifrato contentente i codici 2FA&#xA;    KEYS2FAFILE=&#34;path/fileenc.gpg&#34;&#xA;&#xA;    # Separatore delle coppie chiave-valore&#xA;    DELIMITER=&#34;:&#34;&#xA;&#xA;    # path gpg e opzioni di cifratura/decifratura&#xA;    GPGTTY=$(tty)&#xA;    export GPGTTY&#xA;&#xA;    #GPGCOMMAND=&#34;/usr/local/bin/gpg&#34;&#xA;    GPGENCOPTIONS=&#34;--yes -c --s2k-cipher-algo aes256 --s2k-digest-algo sha512 --s2k-mode 3 --s2k-count 260000 - &#34;&#xA;    GPGDECOPTIONS=&#39;-d&#39;&#xA;&#xA;    # Caratteri consentiti nel nome account&#xA;    PATTERN=&#39;^[a-zA-Z0-9.-]+$&#39;;&#xA;&#xA;    # Su MacOS è disponibile &#34;pbcopy&#34;, su nix &#34;xsel&#34;&#xA;    case &#34;$(sysctl hw.targettype 2  /dev/null)&#34; in&#xA;        &#34;&#34;)&#xA;            CLIPBOARD=&#34;xsel -bi&#34;&#xA;            GPGCOMMAND=&#34;/usr/bin/gpg&#34;;;&#xA;         )&#xA;            CLIPBOARD=&#34;pbcopy&#34;&#xA;            GPGCOMMAND=&#34;/usr/local/bin/gpg&#34;;;&#xA;    esac&#xA;&#xA;    # Decifra e carica in ram il contenuto del file cifrato.&#xA;    echo &#34;Decifratura del file degli account in corso...&#34;&#xA;    KEYS2FAFILEDEC=$(${GPGCOMMAND} ${GPGDECOPTIONS} &#34;${KEYS2FAFILE}&#34; 2  /dev/null)&#xA;&#xA;    # Se la password non è corretta, restituisce un errore.&#xA;    [[ -z &#34;${KEYS2FAFILEDEC}&#34; ]] &amp;&amp; { errmsg &#34;${DECRYPTERR}&#34; 1; }&#xA;    infomsg &#34;Fatto.&#34;&#xA;}&#xA;&#xA;getaccount() {&#xA;    ACCOUNTSTDIO=&#34;$1&#34;&#xA;&#xA;    # Check nome account vuoto&#xA;    [[ -z &#34;${ACCOUNTSTDIO}&#34; ]] &amp;&amp; return &#34;${ACCOUNTNAMEEMPTYERR}&#34;&#xA;&#xA;    # Check caratteri invalidi nel nome account&#xA;    [[ ! &#34;${ACCOUNTSTDIO}&#34; =~ $PATTERN ]] &amp;&amp; return &#34;${INVALIDACCOUNTERR}&#34;&#xA;&#xA;    # Estrae l&#39;elemento contenente l&#39;id dato in input&#xA;    OTPLINE=$(echo &#34;${KEYS2FAFILEDEC}&#34; | grep &#34;${ACCOUNTSTDIO}:&#34;)&#xA;&#xA;    # Se l&#39;account non esiste, restituisce un errore&#xA;    [[ -z &#34;${OTPLINE}&#34; ]] &amp;&amp; return &#34;${ACCOUNTNOTFOUNDERR}&#34;&#xA;&#xA;    ACCOUNT=$(echo $OTPLINE | cut -d&#34;${DELIMITER}&#34; -f 1)&#xA;    KEY=$(echo $OTPLINE | cut -d&#34;${DELIMITER}&#34; -f 2)&#xA;}&#xA;&#xA;cryptaccountfile() {&#xA;    ACCOUNTFILE=&#34;$1&#34;&#xA;    echo -e &#34;\nCifratura del file degli account in corso...&#34;&#xA;    ${GPGCOMMAND} -o &#34;$KEYS2FAFILE&#34; ${GPGENCOPTIONS} &lt;&lt;&lt; $(echo -e &#34;${ACCOUNTFILE}&#34;)&#xA;}&#xA;&#xA;noparam() {&#xA;    clear&#xA;    help&#xA;    exit 0&#xA;}&#xA;&#xA;Visualizza messaggi di errore su stdio&#xA;errmsg() {&#xA;    if [[ $# -ne 2 ]]; then&#xA;        echo -r&#34;${LIGHTRED}[DEBUG] [errmsg] Numero di parametri non corretto.${NC}.&#34;; exit 1&#xA;    else&#xA;        case $1 in&#xA;            &#34;${DECRYPTERR}&#34;            ) echo -e &#34;${LIGHTRED}[ERRORE] Impossibile decriptare il file.${NC}&#34;;;&#xA;            &#34;${INVALIDOPTIONERR}&#34;     ) echo -e &#34;${LIGHTRED}[ERRORE] Opzione \&#34;$OPTARG\&#34; non valida.${NC}&#34;;;&#xA;            &#34;${INVALIDACCOUNTERR}&#34;    ) echo -e &#34;${LIGHTRED}ERRORE] Rilevati caratteri non consentiti.\nI caratteri ammessi sono: [a-z[0-9].-${NC}&#34;;;&#xA;            &#34;${ACCOUNTNOTFOUNDERR}&#34;  ) echo -e &#34;${LIGHTRED}[ERRORE] Account non trovato${NC}&#34;;;&#xA;            &#34;${ACCOUNTNAMEEMPTYERR}&#34; ) echo -e &#34;${LIGHTRED}[ERRORE] Nome account vuoto.${NC}&#34;;;&#xA;            &#34;${ACCOUNTEXISTSERR}&#34;     ) echo -e &#34;${LIGHTRED}[ERRORE] Nome account già esistente${NC}&#34;;;&#xA;            &#34;${INVALIDKEYERR}&#34;        ) echo -e &#34;${LIGHTRED}[ERRORE] Chiave privata nulla o non corretta.\nLa chiave privata deve essere base32.\n${NC}&#34;;;&#xA;            ) echo -e &#34;${LIGHTRED}[ERRORE] Errore generico.${NC}&#34;;;&#xA;        esac&#xA;&#xA;        [[ $2 -eq 1 ]] &amp;&amp; exit $2;&#xA;    fi&#xA;}&#xA;&#xA;Visualizza messaggi info su stdio&#xA;infomsg() {&#xA;    echo -e &#34;${LIGHTGREEN}$1${NC}&#34;&#xA;}&#xA;&#xA;continuemsg() {&#xA;    case $1 in&#xA;        &#34;insert&#34;    ) ACTION=&#34;creato&#34;;;&#xA;        &#34;edit&#34;      ) ACTION=&#34;modificato&#34;;;&#xA;        &#34;delete&#34;    ) ACTION=&#34;cancellato&#34;;;&#xA;    esac&#xA;&#xA;    echo -n &#34;L&#39;account \&#34;${ACCOUNT}\&#34; sta per essere ${ACTION}. Vuoi continuare? [y|N]: &#34;; read ANSWER;&#xA;}&#xA;&#xA;continueyn() {&#xA;        ANSWER=&#34;0&#34;&#xA;        while [[ &#34;${ANSWER}&#34; != &#34;y&#34; ]]; do&#xA;            [[ -z &#34;${ANSWER}&#34; || &#34;${ANSWER}&#34; == &#34;N&#34; ]] &amp;&amp; { echo &#34;Operazione annullata&#34;; exit; }&#xA;            [[ &#34;${ANSWER}&#34; != &#34;y&#34; || &#34;${ANSWER}&#34; != &#34;N&#34; ]] &amp;&amp; continuemsg &#34;$1&#34;&#xA;        done&#xA;}&#xA;&#xA;totp() {&#xA;    # Se la&#39;ccount non esiste o è invalido, esco con errore&#xA;    getaccount &#34;$1&#34;; RETCODE=$?&#xA;    case &#34;${RETCODE}&#34; in&#xA;        &#34;${ACCOUNTNOTFOUNDERR}&#34;  ) errmsg &#34;${ACCOUNTNOTFOUNDERR}&#34; 1;;&#xA;        &#34;${INVALIDACCOUNTERR}&#34;    ) errmsg &#34;${INVALIDACCOUNTERR}&#34; 1;;&#xA;    esac&#xA;&#xA;    echo -e &#34;\nCopia OTP nella clipboard...&#34;&#xA;    # Calcola l&#39;otp e lo trasferisce nella clipboard con xsel&#xA;    oathtool --totp -b &#34;${KEY}&#34; | $CLIPBOARD&#xA;    infomsg &#34;Fatto.&#34;&#xA;    exit&#xA;}&#xA;&#xA;insert() {&#xA;    # Se l&#39;account non esiste o è invalido, esco con errore&#xA;    getaccount &#34;$1&#34;; RETCODE=$?&#xA;    case &#34;${RETCODE}&#34; in&#xA;        0                           ) errmsg &#34;${ACCOUNTEXISTSERR}&#34; 1;;&#xA;        &#34;${INVALIDACCOUNTERR}&#34;    ) errmsg &#34;${INVALIDACCOUNTERR}&#34; 1;;&#xA;    esac&#xA;&#xA;    ACCOUNT=&#34;$1&#34;&#xA;&#xA;    # Inserisci la chiave&#xA;    echo -en &#34;Nuovo account: ${ACCOUNT}\nInserisci la nuova chiave privata: &#34;; read KEY&#xA;    while [[ -z &#34;${KEY}&#34; || ! $(echo ${KEY} | oathtool -b - 2  /dev/null) ]]; do&#xA;        errmsg &#34;${INVALIDKEYERR}&#34; 0&#xA;        echo -n &#34;Inserisci la chiave privata: &#34;; read KEY&#xA;    done&#xA;&#xA;    infomsg &#34;\nNuovo account:        ${ACCOUNT}\nNuova chiave privata: ${KEY}&#34;&#xA;    continueyn &#34;insert&#34;&#xA;&#xA;    # Appendo il nuovo record nel file degli account&#xA;    KEYS2FAFILEDEC+=&#34;\n${ACCOUNT}${DELIMITER}${KEY}&#34;&#xA;    infomsg &#34;L&#39;account \&#34;${ACCOUNT}\&#34; è stato creato con successo.&#34;&#xA;&#xA;    # Cifratura del file degli account&#xA;    cryptaccountfile &#34;${KEYS2FAFILEDEC}&#34;&#xA;    infomsg &#34;Fatto.&#34;&#xA;    exit&#xA;}&#xA;&#xA;edit() {&#xA;    # Se la&#39;ccount non esiste o è invalido, esco con errore&#xA;    getaccount &#34;$1&#34;; RETCODE=$?&#xA;    case &#34;${RETCODE}&#34; in&#xA;        &#34;${ACCOUNTNOTFOUNDERR}&#34;  ) errmsg &#34;${ACCOUNTNOTFOUNDERR}&#34; 1;;&#xA;        &#34;${INVALIDACCOUNTERR}&#34;    ) errmsg &#34;${INVALIDACCOUNTERR}&#34; 1;;&#xA;    esac&#xA;&#xA;    # Faccio una copia dell&#39;account per un eventuale ripristino durante&#xA;    # la fase di input&#xA;    ACCOUNTTEMP=&#34;${ACCOUNT}&#34;&#xA;    ACCOUNTMOD=&#34;${ACCOUNT}&#34;&#xA;&#xA;    # Inserisco il nuovo nome account.&#xA;    # 1. Se è vuoto: è la conferma dell&#39;account inserito all&#39;inizio e proseguo&#xA;    # 2. Se il nome è invalido: sollevo un&#39;eccezione e reitero&#xA;    # 3. Se il nome è valido:&#xA;    #    a. Se è un nome nuovo: acquisisco il nuovo nome, recupero nome e chiave dell&#39;account di partenza e proseguo&#xA;    #    b. Sè è lo stesso nome di partenza: è la conferma dell&#39;account iniziale e proseguo&#xA;    #    c. Se è un altro nome esistente o invalido: come il punto 2.&#xA;    while true; do&#xA;        echo&#xA;        echo -en &#34;Account: ${ACCOUNT}\nInserisci nuovo nome account: &#34;; read ACCOUNTMOD;&#xA;        getaccount &#34;${ACCOUNTMOD}&#34;; RETCODE=$?&#xA;        case &#34;${RETCODE}&#34; in&#xA;            # Punto 1.&#xA;            &#34;${ACCOUNTNAMEEMPTYERR}&#34; ) ACCOUNTMOD=&#34;${ACCOUNT}&#34;; break;;&#xA;            # Punto 2&#xA;            &#34;${INVALIDACCOUNTERR}&#34;    ) errmsg &#34;${INVALIDACCOUNTERR}&#34; 0;;&#xA;            # Punto 3a.&#xA;            &#34;${ACCOUNTNOTFOUNDERR}&#34;  ) getaccount &#34;${ACCOUNTTEMP}&#34;; break;;&#xA;            # Punti 3b e 3c, rispettivamente&#xA;            0                           ) [[ &#34;${ACCOUNTMOD}&#34; == &#34;${ACCOUNTTEMP}&#34; ]] &amp;&amp; break \&#xA;                                                                                 || { getaccount &#34;${ACCOUNTTEMP}&#34;; errmsg &#34;${ACCOUNTEXISTSERR}&#34; 0; };;&#xA;        esac&#xA;    done&#xA;&#xA;    [[ &#34;${ACCOUNT}&#34; != &#34;${ACCOUNTMOD}&#34; &amp;&amp; ! -z &#34;${ACCOUNTMOD}&#34; ]] &amp;&amp; msg=&#34;\nL&#39;account \&#34;${ACCOUNT}\&#34; è stato modificato in \&#34;${ACCOUNTMOD}\&#34;.&#34; \&#xA;                                                                    || msg=&#34;\nNessuna modifica rilevata.\n&#34;&#xA;    infomsg &#34;${msg}&#34;&#xA;&#xA;    # Modifica la chiave privata&#xA;    KEYMOD=&#34;${KEY}&#34;&#xA;    echo&#xA;&#xA;    while true; do&#xA;        echo -en &#34;Chiave privata: ${KEY}\nInserisci la nuova chiave privata: &#34;; read KEYMOD&#xA;        if [[ -z $KEYMOD ]]; then&#xA;            KEYMOD=&#34;${KEY}&#34;; break&#xA;        elif [[ ! $(echo &#34;${KEYMOD}&#34; | oathtool -b - 2  /dev/null) ]]; then&#xA;            errmsg &#34;${INVALIDKEYERR}&#34; 0; KEYMOD=&#34;${KEY}&#34;&#xA;        else&#xA;            break&#xA;        fi&#xA;    done&#xA;&#xA;    # Se c&#39;è stata almeno una modifica (account/key), sovrascrivo il record. Altrimenti esco senza fare nulla.&#xA;    if [[ &#34;${ACCOUNT}&#34; != &#34;${ACCOUNTMOD}&#34; || &#34;${KEY}&#34; != &#34;${KEYMOD}&#34; ]]; then&#xA;        # Conferma modifica&#xA;&#xA;        infomsg &#34;\nAccount:              ${ACCOUNT}\nChiave privata:       ${KEY}\nNuovo account:        ${ACCOUNTMOD}\nNuova chiave privata: ${KEYMOD}\n&#34;&#xA;        continueyn &#34;edit&#34;&#xA;&#xA;        # Modifica del file degli account&#xA;        KEYS2FAFILEDECTEMP=$(sed &#34;s/${ACCOUNT}${DELIMITER}${KEY}/${ACCOUNTMOD}${DELIMITER}${KEYMOD}/&#34; &lt;&lt;&lt; &#34;${KEYS2FAFILEDEC}&#34;)&#xA;        infomsg &#34;L&#39;account \&#34;${ACCOUNT}\&#34; è stato modificato con successo.&#34;&#xA;&#xA;        # Cifratura del file degli account&#xA;        cryptaccountfile &#34;${KEYS2FAFILEDECTEMP}&#34;&#xA;    else&#xA;        infomsg &#34;Nessuna modifica rilevata.&#34;&#xA;    fi&#xA;    infomsg &#34;Fatto.&#34;&#xA;}&#xA;&#xA;delete() {&#xA;    if getaccount &#34;$1&#34;; then&#xA;        # Conferma eliminazione&#xA;        infomsg &#34;\nAccount:              ${ACCOUNT}\nChiave privata:       ${KEY}\n&#34;&#xA;        continueyn &#34;delete&#34;&#xA;&#xA;        # Cancellazione account&#xA;        KEYS2FAFILEDECTEMP=$(sed &#34;/$ACCOUNT/d&#34; &lt;&lt;&lt; &#34;${KEYS2FAFILEDEC}&#34;)&#xA;        infomsg &#34;L&#39;account \&#34;$ACCOUNT\&#34; è stato eliminato.&#34;&#xA;&#xA;        # Cifratura del nuovo file degli account&#xA;        cryptaccountfile &#34;${KEYS2FAFILEDECTEMP}&#34;&#xA;        infomsg &#34;Fatto.&#34;&#xA;    else&#xA;        errmsg &#34;${ACCOUNTNOTFOUNDERR}&#34; 1&#xA;    fi&#xA;}&#xA;&#xA;list() {&#xA;    echo&#xA;    while read LINE; do&#xA;        cut -d&#34;${DELIMITER}&#34; -f 1 &lt;&lt;&lt; &#34;${LINE}&#34;&#xA;    done &lt;&lt;&lt; &#34;${KEYS2FAFILEDEC}&#34;&#xA;}&#xA;&#xA;help() {&#xA;cat&lt;&lt;EOF&#xA;Usa come: ./totp.sh [options] ARG&#xA;dove:&#xA;    [options]&#xA;        -l restituisce la lista degli account.&#xA;        -t nomeaccount restituisce l&#39;otp per quell&#39;account.&#xA;        -i nomeaccount inserisce un nuovo account.&#xA;        -d nomeaccount cancella nomeaccount.&#xA;        -e nomeaccount modifica nomeaccount.&#xA;        -h stampa questa pagina di help.&#xA;&#xA;ESEMPI:&#xA;    RESTITUISCE LA LISTA DEGLI ACCOUNT&#xA;    totp.sh -l&#xA;&#xA;    INSERISCE UN ACCOUNT&#xA;    totp.sh -i dropbox&#xA;&#xA;    CANCELLA UN ACCOUNT&#xA;    totp.sh -d megaupload&#xA;&#xA;    MODIFICA UN ACCOUNT&#xA;    totp.sh -e paypal&#xA;&#xA;    GENERA OTP&#xA;    totp.sh -t firefoxsync&#xA;EOF&#xA;}&#xA;&#xA;main() {&#xA;    init&#xA;    while getopts &#34;lt:i:e:d:&#34; opt; do&#xA;        case $opt in&#xA;            l ) list ;;&#xA;            t ) totp $OPTARG ;;&#xA;            i ) insert $OPTARG ;;&#xA;            e ) edit $OPTARG ;;&#xA;            d ) delete $OPTARG ;;&#xA;            ) clear;errmsg &#34;${INVALIDOPTIONERR}&#34; 1;;&#xA;        esac&#xA;    done&#xA;    shift $(($OPTIND - 1))&#xA;}&#xA;&#xA;[[ $# -eq 0 || $# -eq 1 &amp;&amp; &#34;$1&#34; == &#34;-h&#34; ]] &amp;&amp; noparam || main $*&#xA;&#xA;#bash #totp #cryptography #gpg #scripting]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://pixelfed.uno/storage/m/_v2/489827599091373610/7c649620b-69b6c5/x5Ykru5oywKu/RE9KMTKwgIzhwDrkGnZqsTThRVvDSlNlyB2XwSv4.webp" alt="totp"></p>

<p>In una giornata in cui avevo un po’ di tempo da investire, ho ripreso la versione 2 dello script “<a href="https://noblogo.org/aytin/generazione-otp-via-bash" rel="nofollow">Generazione OTP via bash</a>”, e ho deciso di renderlo un po’ più flessibile introducendo una logica <strong>crud</strong>. Anzi, il termine corretto dovrebbe essere “<strong>dave</strong>” (<strong>D</strong>elete, <strong>A</strong>dd, <strong>V</strong>iew, <strong>E</strong>dit).

Che altro aggiungere?</p>
<ul><li>Il file è ancora un insieme di coppie chiave-valore (nome account e chiave privata).</li>
<li>Il separatore è il simbolo “<strong>:</strong>”. Ma può essere configurato cambiando il valore della variabile “<strong>DELIMITER</strong>”.</li>
<li>Lo script è discretamente commentato così, in futuro, riuscirò a ricordare perché ho fatto quello che ho fatto (tipo “Memento”).</li>
<li>Le opzioni di gpg per la cifratura/decifratura del file sono configurabili nello script modificando il valore delle variabili “<strong>GPG_ENC_OPTIONS</strong>” e “<strong>GPG_DEC_OPTIONS</strong>”.</li>
<li>L’uso di <strong>getopts,</strong> 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.</li>
<li>Ero partito per rimpiazzare ogni costrutto <strong>if</strong> 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.</li></ul>

<pre><code class="language-bash">
#!/usr/local/bin/bash


init() {
    # CODICI D&#39;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=&#39;\033[1;32m&#39;
    LIGHT_RED=&#39;\033[1;31m&#39;
    NC=&#39;\033[0m&#39;

    # Path del file cifrato contentente i codici 2FA
    KEYS_2FA_FILE=&#34;&lt;path/file_enc.gpg&gt;&#34;

    # Separatore delle coppie chiave-valore
    DELIMITER=&#34;:&#34;

    # path gpg e opzioni di cifratura/decifratura
    GPG_TTY=$(tty)
    export GPG_TTY

    #GPG_COMMAND=&#34;/usr/local/bin/gpg&#34;
    GPG_ENC_OPTIONS=&#34;--yes -c --s2k-cipher-algo aes256 --s2k-digest-algo sha512 --s2k-mode 3 --s2k-count 260000 - &#34;
    GPG_DEC_OPTIONS=&#39;-d&#39;

    # Caratteri consentiti nel nome account
    PATTERN=&#39;^[a-zA-Z0-9._-]+$&#39;;

    # Su MacOS è disponibile &#34;pbcopy&#34;, su *nix &#34;xsel&#34;
    case &#34;$(sysctl hw.targettype 2&gt;/dev/null)&#34; in
        &#34;&#34;)
            CLIPBOARD=&#34;xsel -bi&#34;
            GPG_COMMAND=&#34;/usr/bin/gpg&#34;;;
         *)
            CLIPBOARD=&#34;pbcopy&#34;
            GPG_COMMAND=&#34;/usr/local/bin/gpg&#34;;;
    esac

    # Decifra e carica in ram il contenuto del file cifrato.
    echo &#34;Decifratura del file degli account in corso...&#34;
    KEYS_2FA_FILE_DEC=$(${GPG_COMMAND} ${GPG_DEC_OPTIONS} &#34;${KEYS_2FA_FILE}&#34; 2&gt;/dev/null)

    # Se la password non è corretta, restituisce un errore.
    [[ -z &#34;${KEYS_2FA_FILE_DEC}&#34; ]] &amp;&amp; { err_msg &#34;${DECRYPT_ERR}&#34; 1; }
    info_msg &#34;Fatto.&#34;
}


get_account() {
    ACCOUNT_STDIO=&#34;$1&#34;

    # Check nome account vuoto
    [[ -z &#34;${ACCOUNT_STDIO}&#34; ]] &amp;&amp; return &#34;${ACCOUNT_NAME_EMPTY_ERR}&#34;

    # Check caratteri invalidi nel nome account
    [[ ! &#34;${ACCOUNT_STDIO}&#34; =~ $PATTERN ]] &amp;&amp; return &#34;${INVALID_ACCOUNT_ERR}&#34;

    # Estrae l&#39;elemento contenente l&#39;id dato in input
    OTP_LINE=$(echo &#34;${KEYS_2FA_FILE_DEC}&#34; | grep &#34;${ACCOUNT_STDIO}:&#34;)

    # Se l&#39;account non esiste, restituisce un errore
    [[ -z &#34;${OTP_LINE}&#34; ]] &amp;&amp; return &#34;${ACCOUNT_NOT_FOUND_ERR}&#34;

    ACCOUNT=$(echo $OTP_LINE | cut -d&#34;${DELIMITER}&#34; -f 1)
    KEY=$(echo $OTP_LINE | cut -d&#34;${DELIMITER}&#34; -f 2)
}


crypt_account_file() {
    ACCOUNT_FILE=&#34;$1&#34;
    echo -e &#34;\nCifratura del file degli account in corso...&#34;
    ${GPG_COMMAND} -o &#34;$KEYS_2FA_FILE&#34; ${GPG_ENC_OPTIONS} &lt;&lt;&lt; $(echo -e &#34;${ACCOUNT_FILE}&#34;)
}


no_param() {
    clear
    help
    exit 0
}


# Visualizza messaggi di errore su stdio
err_msg() {
    if [[ $# -ne 2 ]]; then
        echo -r&#34;${LIGHT_RED}[DEBUG] [err_msg] Numero di parametri non corretto.${NC}.&#34;; exit 1
    else
        case $1 in
            &#34;${DECRYPT_ERR}&#34;            ) echo -e &#34;${LIGHT_RED}[ERRORE] Impossibile decriptare il file.${NC}&#34;;;
            &#34;${INVALID_OPTION_ERR}&#34;     ) echo -e &#34;${LIGHT_RED}[ERRORE] Opzione \&#34;$OPTARG\&#34; non valida.${NC}&#34;;;
            &#34;${INVALID_ACCOUNT_ERR}&#34;    ) echo -e &#34;${LIGHT_RED}[ERRORE] Rilevati caratteri non consentiti.\nI caratteri ammessi sono: [a-z][A-Z][0-9].-_${NC}&#34;;;
            &#34;${ACCOUNT_NOT_FOUND_ERR}&#34;  ) echo -e &#34;${LIGHT_RED}[ERRORE] Account non trovato${NC}&#34;;;
            &#34;${ACCOUNT_NAME_EMPTY_ERR}&#34; ) echo -e &#34;${LIGHT_RED}[ERRORE] Nome account vuoto.${NC}&#34;;;
            &#34;${ACCOUNT_EXISTS_ERR}&#34;     ) echo -e &#34;${LIGHT_RED}[ERRORE] Nome account già esistente${NC}&#34;;;
            &#34;${INVALID_KEY_ERR}&#34;        ) echo -e &#34;${LIGHT_RED}[ERRORE] Chiave privata nulla o non corretta.\nLa chiave privata deve essere base32.\n${NC}&#34;;;
            *                           ) echo -e &#34;${LIGHT_RED}[ERRORE] Errore generico.${NC}&#34;;;
        esac

        [[ $2 -eq 1 ]] &amp;&amp; exit $2;
    fi
}


# Visualizza messaggi info su stdio
info_msg() {
    echo -e &#34;${LIGHT_GREEN}$1${NC}&#34;
}


continue_msg() {
    case $1 in
        &#34;insert&#34;    ) ACTION=&#34;creato&#34;;;
        &#34;edit&#34;      ) ACTION=&#34;modificato&#34;;;
        &#34;delete&#34;    ) ACTION=&#34;cancellato&#34;;;
    esac

    echo -n &#34;L&#39;account \&#34;${ACCOUNT}\&#34; sta per essere ${ACTION}. Vuoi continuare? [y|N]: &#34;; read ANSWER;
}


continue_yn() {
        ANSWER=&#34;0&#34;
        while [[ &#34;${ANSWER}&#34; != &#34;y&#34; ]]; do
            [[ -z &#34;${ANSWER}&#34; || &#34;${ANSWER}&#34; == &#34;N&#34; ]] &amp;&amp; { echo &#34;Operazione annullata&#34;; exit; }
            [[ &#34;${ANSWER}&#34; != &#34;y&#34; || &#34;${ANSWER}&#34; != &#34;N&#34; ]] &amp;&amp; continue_msg &#34;$1&#34;
        done
}


totp() {
    # Se la&#39;ccount non esiste o è invalido, esco con errore
    get_account &#34;$1&#34;; RET_CODE=$?
    case &#34;${RET_CODE}&#34; in
        &#34;${ACCOUNT_NOT_FOUND_ERR}&#34;  ) err_msg &#34;${ACCOUNT_NOT_FOUND_ERR}&#34; 1;;
        &#34;${INVALID_ACCOUNT_ERR}&#34;    ) err_msg &#34;${INVALID_ACCOUNT_ERR}&#34; 1;;
    esac

    echo -e &#34;\nCopia OTP nella clipboard...&#34;
    # Calcola l&#39;otp e lo trasferisce nella clipboard con xsel
    oathtool --totp -b &#34;${KEY}&#34; | $CLIPBOARD
    info_msg &#34;Fatto.&#34;
    exit
}


insert() {
    # Se l&#39;account non esiste o è invalido, esco con errore
    get_account &#34;$1&#34;; RET_CODE=$?
    case &#34;${RET_CODE}&#34; in
        0                           ) err_msg &#34;${ACCOUNT_EXISTS_ERR}&#34; 1;;
        &#34;${INVALID_ACCOUNT_ERR}&#34;    ) err_msg &#34;${INVALID_ACCOUNT_ERR}&#34; 1;;
    esac

    ACCOUNT=&#34;$1&#34;

    # Inserisci la chiave
    echo -en &#34;Nuovo account: ${ACCOUNT}\nInserisci la nuova chiave privata: &#34;; read KEY
    while [[ -z &#34;${KEY}&#34; || ! $(echo ${KEY} | oathtool -b - 2&gt;/dev/null) ]]; do
        err_msg &#34;${INVALID_KEY_ERR}&#34; 0
        echo -n &#34;Inserisci la chiave privata: &#34;; read KEY
    done

    info_msg &#34;\nNuovo account:        ${ACCOUNT}\nNuova chiave privata: ${KEY}&#34;
    continue_yn &#34;insert&#34;

    # Appendo il nuovo record nel file degli account
    KEYS_2FA_FILE_DEC+=&#34;\n${ACCOUNT}${DELIMITER}${KEY}&#34;
    info_msg &#34;L&#39;account \&#34;${ACCOUNT}\&#34; è stato creato con successo.&#34;

    # Cifratura del file degli account
    crypt_account_file &#34;${KEYS_2FA_FILE_DEC}&#34;
    info_msg &#34;Fatto.&#34;
    exit
}


edit() {
    # Se la&#39;ccount non esiste o è invalido, esco con errore
    get_account &#34;$1&#34;; RET_CODE=$?
    case &#34;${RET_CODE}&#34; in
        &#34;${ACCOUNT_NOT_FOUND_ERR}&#34;  ) err_msg &#34;${ACCOUNT_NOT_FOUND_ERR}&#34; 1;;
        &#34;${INVALID_ACCOUNT_ERR}&#34;    ) err_msg &#34;${INVALID_ACCOUNT_ERR}&#34; 1;;
    esac

    # Faccio una copia dell&#39;account per un eventuale ripristino durante
    # la fase di input
    ACCOUNT_TEMP=&#34;${ACCOUNT}&#34;
    ACCOUNT_MOD=&#34;${ACCOUNT}&#34;

    # Inserisco il nuovo nome account.
    # 1. Se è vuoto: è la conferma dell&#39;account inserito all&#39;inizio e proseguo
    # 2. Se il nome è invalido: sollevo un&#39;eccezione e reitero
    # 3. Se il nome è valido:
    #    a. Se è un nome nuovo: acquisisco il nuovo nome, recupero nome e chiave dell&#39;account di partenza e proseguo
    #    b. Sè è lo stesso nome di partenza: è la conferma dell&#39;account iniziale e proseguo
    #    c. Se è un altro nome esistente o invalido: come il punto 2.
    while true; do
        echo
        echo -en &#34;Account: ${ACCOUNT}\nInserisci nuovo nome account: &#34;; read ACCOUNT_MOD;
        get_account &#34;${ACCOUNT_MOD}&#34;; RET_CODE=$?
        case &#34;${RET_CODE}&#34; in
            # Punto 1.
            &#34;${ACCOUNT_NAME_EMPTY_ERR}&#34; ) ACCOUNT_MOD=&#34;${ACCOUNT}&#34;; break;;
            # Punto 2
            &#34;${INVALID_ACCOUNT_ERR}&#34;    ) err_msg &#34;${INVALID_ACCOUNT_ERR}&#34; 0;;
            # Punto 3a.
            &#34;${ACCOUNT_NOT_FOUND_ERR}&#34;  ) get_account &#34;${ACCOUNT_TEMP}&#34;; break;;
            # Punti 3b e 3c, rispettivamente
            0                           ) [[ &#34;${ACCOUNT_MOD}&#34; == &#34;${ACCOUNT_TEMP}&#34; ]] &amp;&amp; break \
                                                                                 || { get_account &#34;${ACCOUNT_TEMP}&#34;; err_msg &#34;${ACCOUNT_EXISTS_ERR}&#34; 0; };;
        esac
    done

    [[ &#34;${ACCOUNT}&#34; != &#34;${ACCOUNT_MOD}&#34; &amp;&amp; ! -z &#34;${ACCOUNT_MOD}&#34; ]] &amp;&amp; msg=&#34;\nL&#39;account \&#34;${ACCOUNT}\&#34; è stato modificato in \&#34;${ACCOUNT_MOD}\&#34;.&#34; \
                                                                    || msg=&#34;\nNessuna modifica rilevata.\n&#34;
    info_msg &#34;${msg}&#34;

    # Modifica la chiave privata
    KEY_MOD=&#34;${KEY}&#34;
    echo

    while true; do
        echo -en &#34;Chiave privata: ${KEY}\nInserisci la nuova chiave privata: &#34;; read KEY_MOD
        if [[ -z $KEY_MOD ]]; then
            KEY_MOD=&#34;${KEY}&#34;; break
        elif [[ ! $(echo &#34;${KEY_MOD}&#34; | oathtool -b - 2&gt;/dev/null) ]]; then
            err_msg &#34;${INVALID_KEY_ERR}&#34; 0; KEY_MOD=&#34;${KEY}&#34;
        else
            break
        fi
    done

    # Se c&#39;è stata almeno una modifica (account/key), sovrascrivo il record. Altrimenti esco senza fare nulla.
    if [[ &#34;${ACCOUNT}&#34; != &#34;${ACCOUNT_MOD}&#34; || &#34;${KEY}&#34; != &#34;${KEY_MOD}&#34; ]]; then
        # Conferma modifica

        info_msg &#34;\nAccount:              ${ACCOUNT}\nChiave privata:       ${KEY}\nNuovo account:        ${ACCOUNT_MOD}\nNuova chiave privata: ${KEY_MOD}\n&#34;
        continue_yn &#34;edit&#34;

        # Modifica del file degli account
        KEYS_2FA_FILE_DEC_TEMP=$(sed &#34;s/${ACCOUNT}${DELIMITER}${KEY}/${ACCOUNT_MOD}${DELIMITER}${KEY_MOD}/&#34; &lt;&lt;&lt; &#34;${KEYS_2FA_FILE_DEC}&#34;)
        info_msg &#34;L&#39;account \&#34;${ACCOUNT}\&#34; è stato modificato con successo.&#34;

        # Cifratura del file degli account
        crypt_account_file &#34;${KEYS_2FA_FILE_DEC_TEMP}&#34;
    else
        info_msg &#34;Nessuna modifica rilevata.&#34;
    fi
    info_msg &#34;Fatto.&#34;
}


delete() {
    if get_account &#34;$1&#34;; then
        # Conferma eliminazione
        info_msg &#34;\nAccount:              ${ACCOUNT}\nChiave privata:       ${KEY}\n&#34;
        continue_yn &#34;delete&#34;

        # Cancellazione account
        KEYS_2FA_FILE_DEC_TEMP=$(sed &#34;/$ACCOUNT/d&#34; &lt;&lt;&lt; &#34;${KEYS_2FA_FILE_DEC}&#34;)
        info_msg &#34;L&#39;account \&#34;$ACCOUNT\&#34; è stato eliminato.&#34;

        # Cifratura del nuovo file degli account
        crypt_account_file &#34;${KEYS_2FA_FILE_DEC_TEMP}&#34;
        info_msg &#34;Fatto.&#34;
    else
        err_msg &#34;${ACCOUNT_NOT_FOUND_ERR}&#34; 1
    fi
}


list() {
    echo
    while read LINE; do
        cut -d&#34;${DELIMITER}&#34; -f 1 &lt;&lt;&lt; &#34;${LINE}&#34;
    done &lt;&lt;&lt; &#34;${KEYS_2FA_FILE_DEC}&#34;
}


help() {
cat&lt;&lt;EOF
Usa come: ./totp.sh [options] ARG
dove:
    [options]
        -l restituisce la lista degli account.
        -t &lt;nome_account&gt; restituisce l&#39;otp per quell&#39;account.
        -i &lt;nome_account&gt; inserisce un nuovo account.
        -d &lt;nome_account&gt; cancella &lt;nome_account&gt;.
        -e &lt;nome_account&gt; modifica &lt;nome_account&gt;.
        -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 &#34;lt:i:e:d:&#34; opt; do
        case $opt in
            l ) list ;;
            t ) totp $OPTARG ;;
            i ) insert $OPTARG ;;
            e ) edit $OPTARG ;;
            d ) delete $OPTARG ;;
            * ) clear;err_msg &#34;${INVALID_OPTION_ERR}&#34; 1;;
        esac
    done
    shift $(($OPTIND - 1))
}

[[ $# -eq 0 || $# -eq 1 &amp;&amp; &#34;$1&#34; == &#34;-h&#34; ]] &amp;&amp; no_param || main $*

</code></pre>

<p><a href="/aytin/tag:bash" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">bash</span></a> <a href="/aytin/tag:totp" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">totp</span></a> <a href="/aytin/tag:cryptography" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">cryptography</span></a> <a href="/aytin/tag:gpg" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">gpg</span></a> <a href="/aytin/tag:scripting" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">scripting</span></a></p>
]]></content:encoded>
      <guid>https://noblogo.org/aytin/gestione-totp-in-bash</guid>
      <pubDate>Wed, 21 Feb 2024 15:37:02 +0000</pubDate>
    </item>
  </channel>
</rss>