Generazione OTP via bash
(pubblicato il 30 gennaio 2021) 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:
- la chiave sia cifrata e decifrata solamente quando occorre;
- 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
- Estremamente compatto
- Si sfrutta nativamente l'autocomplete col TAB per i file da dare all'input
- L'univocità dei nomi è garantita dal file system
SVANTAGGI
- Crea un file per ogni codice e, alla lunga potrebbe generare confusione
- Necessità di creare una password di cifratura per ogni file (potenzialmente tutte diverse)
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
- Unico entry-point per la gestione degli account (un'unica password per la cifratura del file
- Più semplice da allocare
SVANTAGGI
- L'univocità degli account deve essere garantita dall'utente.
- Perdita della funzionalità di autocomplete in fase di inserimento dati
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