Generazione OTP via bash

(pubblicato il 30 gennaio 2021) otp Fonte: Otp icons created by Chanut-is-Industries – Flaticon

Per buona parte del mio tempo sul pc apro una shell, per cui trovo comodo spostare il meno possibile le mani dalla tastiera. Usando spesso 2FA, ho pensato ad un modo per avere un OTP usando bash.

Il tool che fa al caso mio è oathtool che, per i miei scopi, è sufficiente invocare in questo modo:

oathtool --totp -b <key>

dove <key> è una stringa base32 bella lunga.

Chiaramente non voglio doverla scrivere ogni volta, nè voglio lasciarla in chiaro. Occorre che:

  1. la chiave sia cifrata e decifrata solamente quando occorre;
  2. l'output non arrivi su stdout per non doverlo copiare a mia volta (e lasciarlo comunque visibile)

Al punto 1. si risponde con gpg o openssl.

Al punto 2. si risponde con xsel (Gnu/Linux) o pbcopy (MacOS) che trasferiscono l'output di un comando nella clipboard.

oathtool --totp -b <<< $(gpg -q --decrypt --pinentry-mode loopback <file_chiave_cifrato>) | xsel -bi

E veniamo quindi alla:

Versione 1

#!/bin/bash

# Path del file cifrato contentente la chiave # supponiamo sia di google
KEY_2FA_FILE="$1"

oathtool --totp -b <<< $(gpg -q --decrypt --pinentry-mode loopback "${KEY_2FA_FILE}") | xsel -bi

Non è nemmeno uno script. Potrebbe essere direttamente un alias.

VANTAGGI

SVANTAGGI

Versione 2

Mi occorre quindi un file nome-valore con separatore. Ad es. il simbolo “:”

account_1:JD88EIDIKJDKMDI3IEJDMDKJKDJKDKPA
account_2:JDNBLASPUQRIEKM89478JMFKOVJDOQKJ
account_3:OIJWGDVLOIQ94KDKSUD9KSLWOER9W3ODF
account_4:MVNMWIQPQYSKGPRIGNFG24KFG9E49RPWQ
    
    ...
    
account_n:IWPQSLNCGK49TOWODIR483IRIWOFOPIQO

Nello script, la variabile KEYS_2FA_FILE memorizza il full path del file cifrato dei codici, mentre l'account deve essere fornito in input. In caso contrario, viene restituita la lista degli account disponibili.

Se l'account è valido, il file viene decifrato e viene estratto il corrispondente codice che viene memorizzato direttamente nella clipboard.

#!/usr/bin/bash

# Path del file cifrato contentente i codici 2FA
KEYS_2FA_FILE="<path>/keys_2fa.gpg"

# Acquisisce il nome account
ACCOUNT=$1

# Decifra e carica in ram il contenuto del file cifrato
KEYS_2FA_FILE_DEC=$(gpg --decrypt  --pinentry-mode loopback  "${KEYS_2FA_FILE}" 2>/dev/null)

# Se non c'è nessun argomento, restituisce la lista degli account
if [[ -z "${ACCOUNT}" ]]; then
    while read LINE; do
        cut -d":" -f 1 <<< ${LINE}
    done <<< $(echo "${KEYS_2FA_FILE_DEC}")
    exit
else
    # Estrae l'elemento contenente l'id dato in input
    KEY=$(echo "${KEYS_2FA_FILE_DEC}" | grep ${ACCOUNT} | cut -d":" -f 2)

    # Se la chiave esiste, restituisce l'otp direttamente nella clipboard
    # altrimenti esce con errore
    if [[ -z "${KEY}" ]]; then
        echo "Account non esistente"
        exit 1
    else
        # Calcola l'otp e lo trasferisce nella clipboard con pbcopy
        oathtool --totp -b "${KEY}" | xsel -bi
        echo "Fatto."
    fi
fi

VANTAGGI

SVANTAGGI

Provo ad esplorare anche una struttura differente, più espressiva se vogliamo. Invece di un file nome-valore, uso un json così strutturato:

Versione 3

{
  "accounts": [
    {
      "id": "account_1",
      "secret": "JD88EIDIKJDKMDI3IEJDMDKJKDJKDKPA"
    },
    {
      "id": "account_2",
      "secret": "JDNBLASPUQRIEKM89478JMFKOVJDOQKJ"
    },
    {
      "id": "account_3",
      "secret": "OIJWGDVLOIQ94KDKSUD9KSLWOER9W3ODF"
    },
    {
      "id": "account_4",
      "secret": "MVNMWIQPQYSKGPRIGNFG24KFG9E49RPWQ"
    },
    
    ...
    
    {
      "id": "account_n",
      "secret": "IWPQSLNCGK49TOWODIR483IRIWOFOPIQO"
    }
  ]
}

Un array, accounts, di oggetti nome-valore.

Lo script che fa il parsing e l'estrazione non è molto diverso dal precedente.

#!/usr/local/bin/bash

# Path del file cifrato contentente i codici 2FA
JSON_2FA_FILE=<path>/json_2fa.gpg

# Acquisisce il nome account
ACCOUNT=$1

# Carica in ram il contenuto del file json cifrato
JSON_2FA_FILE_DEC=$(gpg --decrypt  --pinentry-mode loopback  "${JSON_2FA_FILE}" 2>/dev/null)

# Se non c'è nessun argomento, restituisce la lista degli account
# - jq -c '.accounts[]' restituisce una lista contenenente tante linee per quanti sono gli elementi.
# - La lista viene data in pasto al ciclo while attraverso l'operatore <<< (here-string)
# - Da ogni linea viene estratto il valore del campo "id" sempre attraverso l'operatore <<<

if [[ -z "${ACCOUNT}" ]]; then
    while read LINE; do
        jq -r '.id' <<< ${LINE}
    done <<< $(echo "${JSON_2FA_FILE_DEC}" | jq -c '.accounts[]')
    exit
else
    # Estrae dall'array l'elemento contenente l'id dato in input
    KEY=$(jq -r --arg ID ${ACCOUNT} '.accounts[] | select(.id | contains($ID)).secret' <<< ${JSON_2FA_FILE_DEC})

    # Se la chiave esiste, restituisce l'otp direttamente nella clipboard
    # altrimenti esce con errore
    if [[ -z "${KEY}" ]]; then
        echo "Account non esistente"
        exit 1
    else
        # Calcola l'otp e lo trasferisce nella clipboard con xsel
        oathtool --totp -b "${KEY}" | xsel -bi
        echo "Fatto."
    fi
fi

#bash #otp #scripting #shell