How to get 2FA on the command line

Getting Time-based One-Time-Password for 2FA on the command line.

Abstract

This HOW-TO has been succesfully tested on Ubuntu 22.04.3 LTS so let's assume you have a similar setup.
There is no shortage of OTP 2FA apps availiable for your phone, such as Authy , FreeOTP or even the not so recommended Google Authenticator.
These apps take an initial secret code and create a TOTP anytime you need a 2FA code for login.
Some advantages of doing 2FA on the CLI are:
  1. Easy to add, maintain, and backup with a simple key=val text file
  2. Copy/Paste is easier than typing digits displayed on your phone
  3. No issues with being locked out due to dead/lost/new phones

Installation

Make sure you're logged in as a regular user (not as root).
Install the two utility with:
sudo apt install oathtool gpg
We'll use a helper script as well as a file of initial secrets encrypted with GnuPG for better security.
sudo touch /usr/local/bin/totp
and, with your editor of choice, put the content below on the file and save it.
#!/bin/bash # # Time-based One-time Password algorithm (TOTP) helper script # Save shared secrets on disk protected with GnuPG encryption # Easily generate OTPs for two-factor authorization (2FA) # # Setup: # Install requirements with `sudo apt install oathtool gpg` # Setup gpg as per https://keyring.debian.org/creating-key.html # # Adapt the 3 variables below: # - KEYFILE: file that holds the name/key pairs # - UID: GnuPG user ID to use for encryption # - KEYID: GnuPG key ID to use for encryption # # Good to know: # - get gpg keys with: gpg --list-keys --keyid-format short user@example.com # # - the $KEYFILE itself is in clear and has the format: # aws=hQIMAxevVAas6A+AAQ//cJL/v3O6CCurdzVkCk5yEGa6sZgWWw6AkH/QenVmTSj... # twitter=hQIMAxevVAas6A+AAQ/9H8h0yde7zErfF/8qwohD5Zw7q85FlI+IIFC1Kk5Ifpw... # github=hQIMAxevVAas6A+AARAAm8T//mqNyBEz4Y/HGGlNgFUzk8vOaylMdE/TbDzVI... # # - the shared secrets are stored encrypted with gpg then base64-ed # - keys are never deleted, only appended # - the last available key for the chosen service is used # - to restore the previous key, manually delete the last key from $KEYFILE # # Authors: # - https://www.sendthemtomir.com/blog/cli-2-factor-authentication and # - https://karl-voit.at/2019/03/03/oathtool-otp/, Karl Voit, tools@Karl-Voit.at # - Paolo Greppi, paolo.greppi@libpf.com # LICENSE: GPLv3 set -e KEYFILE="$HOME/.totpkeys" UID="user@example.com" KEYID="9E2A4CEF" if [ -z "$1" ]; then echo echo "Usage:" echo " totp list" echo " totp get google" echo " totp set google QUBAYAYXV5KANLHI" exit fi if [ "$1" = 'list' ]; then KEYS=$(sed 's/^\([^=]*\)=.*$/- \1/g' "$KEYFILE") echo "Available keys:" echo "$KEYS" exit fi if [ "$1" = 'get' ]; then if [ -z "$2" ]; then echo "$0: Missing service name" $0 exit fi TOTPKEY=$(sed -n "s/${2}=//p" "$KEYFILE" | tail -n 1) if [ -z "$TOTPKEY" ]; then echo "$0: Bad Service Name '$2'" $0 exit fi TOTPKEY=$(echo "$TOTPKEY" | base64 -d | gpg --decrypt -r "$UID" -u "$KEYID" 2> /dev/null) oathtool --totp -b "$TOTPKEY" exit fi if [ "$1" = 'set' ]; then if [ -z "$2" ]; then echo "$0: Missing service name" $0 exit fi if [ -z "$3" ]; then echo "$0: Missing key" $0 exit fi oathtool --totp -b "$3" > /dev/null # verify secret TOTPKEY=$(echo "$3" | gpg --encrypt -r "$UID" -u "$KEYID" | base64 -w0) echo "$2=$TOTPKEY" >> "$KEYFILE" exit fi echo "Command $1 unknown" $0
Make it executable with :
sudo chmod +x /usr/local/bin/totp
If all went well, we can get a 2FA code on command line with:
$ totp twitter 078321

That's all folks.

Now you have a Time-based One-Time-Password for 2FA on the command line. Enjoy !!
Good writeup. Here are some security suggestions:
  1. Improved File Permissions:
    • Ensure that the key file ($KEYFILE) and the script itself have strict file permissions. This can be done using chmod to restrict access to only the necessary users, typically just the owner.
    chmod 600 "$KEYFILE" chmod 700 /path/to/your/script.sh
  2. Input Validation:
    • Add validation for the inputs, especially for the service name and key, to prevent injection attacks or accidental misconfiguration.
    if [[ ! "$2" =~ ^[a-zA-Z0-9_]+$ ]]; then echo "Invalid service name" exit 1 fi
  3. Error Handling and Logging:
    • Implement better error handling and logging to track script usage and errors. This can help in auditing and troubleshooting.
    log_file="/var/log/totp_script.log" log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$log_file" } # Example usage within the script: log "Generating TOTP for service $2"
  4. Encrypted Backups:
    • Create a mechanism for encrypted backups of the $KEYFILE. This can be a simple script that encrypts and copies the file to a secure location.
    backup_file="$HOME/.totpkeys_backup_$(date '+%Y%m%d')" cp "$KEYFILE" "$backup_file" gpg --encrypt -r "$UID" "$backup_file"
  5. Enhanced GnuPG Handling:
    • Ensure that the GnuPG configuration is secure. This may include setting up a strong key passphrase, using a secure keyring, and keeping the GnuPG software up to date.
  6. Avoid Hardcoded Information:
    • Instead of hardcoding the GnuPG user ID and key ID, consider passing them as arguments or setting them as environment variables.
    UID=${TOTP_UID:-"default_user@example.com"} KEYID=${TOTP_KEYID:-"default_keyid"}
  7. Restrict Script Execution:
    • Restrict the script to be executable only by the intended users. This can be done by checking the user ID at the beginning of the script.
    if [ "$(id -u)" -ne "expected_user_id" ]; then echo "This script can only be run by a specific user." exit 1 fi
  8. Prompt for Confirmation on Sensitive Actions:
    • For operations like setting a new key, prompt for user confirmation to prevent accidental changes.
    read -p "Are you sure you want to set a new key for $2? [y/N] " response if [[ ! "$response" =~ ^[Yy]$ ]]; then echo "Operation canceled." exit 1 fi
  9. Use Temporary Files for Sensitive Data:
    • Instead of directly writing sensitive data to files, use temporary files with restricted permissions and ensure they are securely deleted after use.
    tmpfile=$(mktemp /tmp/.totp.XXXXXX) chmod 600 "$tmpfile" # Use $tmpfile for intermediate steps rm -f "$tmpfile"
reply
Thank you so much. I'll include each and everyone of your suggestions.
reply