Backup TOTP secrets with Emacs

Software Emacs, Gnupg, Org-Mode, OTP, Security

I use FreeOTP to generate time-based OTP tokens on my phone. It’s similar to Google Authenticator et al, except that it’s Free Software, it works really well, and it can display images in addition to an account name, which is nice.

Screenshot

Plus it’s really easy to use: start setting-up the 2nd factor authentication on the service you want, when it shows a QR-code scan it with the app, and you’re done. Yeah, just like every other TOTP app.

But as I’m a little paranoid, I want to have a full backup of all my OTP secrets somewhere safe… So I did just that using Emacs, org-mode, and some Python.

The idea is to use an org-mode file with a big table with several columns:

  • Issuer: the name of the service: Google, Amazon AWS, OVH, Sentry…
  • Label: the account identifier: username, email address, account number, etc.
  • Secret: the base32-encoded shared secret. The most important field: this is what is actually used to generate the one-time passwords.
  • Image: optional, the name of an image file (hosted on my server) to be displayed in FreeOTP.
  • URI: the content of the configuration QR-code. This column is generated by org-mode using a column formula and the content of the other columns 👍

To fill this table, I created 2 backup scripts, one for Google Authenticator (used to migrate away from it 😉), and one for FreeOTP. And then I wrote another script that uses qrencode to generate QR-codes as ASCII art and store them in the very same Org file… So I can very easily re-add accounts to the app or to another device.

How to use:

  1. Add a new service using FreeOTP
  2. In the Org file, run the FreeOTP backup script (put the cursor on it and press C-c C-c): the result appears under the script
  3. Copy the secret in a new line in the table
  4. Fill the rest of the line (issuer, label, image if wanted, and put the image on my server if needed)
  5. Generate the URI by updating the table (put the cursor on it and press C-c C-c)
  6. Regenerate the QR code by running the appropriate script (put the cursor and guess what you need to do now)
  7. Re-add the service to FreeOTP with the new QR-code that includes a nice image 😉

Here is the code:

* OTP secrets
#+TBLNAME: otp-tokens
| Issuer | Label                  | Secret           | Image      | URI                                                                                                                                             |
|--------+------------------------+------------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------|
| Google | fake.address@gmail.com | fakesecret123456 | google.png | otpauth://totp/Google:fake.address@gmail.com?secret=fakesecret123456&issuer=Google&image=https%3A%2F%2Ffichiers.schnouki.net%2Fotp%2Fgoogle.png |
#+TBLFM: $5='(concat "otpauth://totp/" (if (string-blank-p $1) "" (concat $1 ":")) $2 "?" (url-build-query-string `(("secret" $3) ("issuer" $1) ("image" ,(if (string-blank-p $4) "" (concat "https://fichiers.schnouki.net/otp/" $4))))))

* Configuration QR-codes
#+name: qrcodes
#+begin_src python :var tokens=otp-tokens :results output
import subprocess
for issuer, label, secret, image, uri in tokens:
    print("%s (%s)\n" % (label, issuer if len(issuer) > 0 else "n/a"))
    code = subprocess.check_output(["qrencode", "-t", "UTF8", uri])
    print(code.decode("utf-8"))
    print("\n"*30)
#+end_src

#+RESULTS: qrcodes
#+begin_example
fake.address@gmail.com (Google)

█████████████████████████████████████████████████████
█████████████████████████████████████████████████████
████ ▄▄▄▄▄ █▀██▄▀█▀▀  ▀▀▄▄▄▀▀█▀▄▀▄▄▀█▄█▀▄█ ▄▄▄▄▄ ████
████ █   █ █▀▄▀██▄▀█▄  ▄ ▄ █▀ ██▀▀ █  █ ▀█ █   █ ████
████ █▄▄▄█ █▀▀█▀▄█▄ ▄▀█▄ ▄▄▄ █ █▀██ ▀ ▄▄▄█ █▄▄▄█ ████
████▄▄▄▄▄▄▄█▄█▄█ ▀▄█ ▀ ▀ █▄█ ▀ ▀ ▀ █ █▄▀▄█▄▄▄▄▄▄▄████
████  ▄▄▄▀▄  █▄ █▀█▀█▀▀▄▄▄▄  ▀▄██▀  ██▄▀▀ █ █▄▀▄▀████
█████▀▄▀ █▄▀▄█ ▄  █▀█▀▄▄ ██▀ ▀ ▄  ▀██▄▄ █▀▄█▀▄▄▄█████
████▀ ██ ▄▄███▄▀█▄▄ ▄ ▄ ▀█ ▀█▄▀▄▄▄▄▀▄ ▀▀▄▄▀▀▄▄ ▀▀████
████ ██   ▄▄▀█▀▄▄▄ ▀█ ▀ ▄█▀ ▄█▄  ▄ ▄▀█▀▀ ▀ █▀  ▀▄████
████▄█▀█▀▀▄▀█    ▄▄▄▀▀█ ████  ▄██▀▀▄▀▄█▀▀ ▄█▀ ▀  ████
█████▄▀▄▄█▄▄█▀▄█▀ ▄▄ █▄▄▀ █▄▀▀▄▄▀▀▀▄ ▄▄ █▄▄█▄█▄▄█████
████▄▄ ▀ ▄▄▄  █▄ ▄▄▀█▄▄▄ ▄▄▄ █▀▄▄ ▄▀▄▄▀▄ ▄▄▄    █████
████   ▄ █▄█ ▄█▄▀▀█ ▄▀ ▀ █▄█    ▀▀ █ ▄█▀ █▄█   ▀▄████
████▀   ▄ ▄▄ █▀▀▄▀█ █▀▀▀ ▄    ▄██  ▀█▀██▄ ▄  ▄█  ████
████▄▀██▀▀▄▀▀██▀█ ▄▄▀  ▀▄▄█▀▀█ ▀ ▄█ █▀  ▄ ▄█▀▄▄ ▀████
████▄ █ █▀▄██ ▀▄  ▄█▄ ▄▄▄▀▄  ▄▀▄▄ ▄▀▄▀█▀█▄█ ▄▀▀  ████
█████ █ ██▄ ▀█   ▄ ▀ ▄ █▄█▄▄▄  ▀ █ █ ▀▀█▄ ▀█ ▀ ▀▄████
████  █ ▀ ▄▀▀█▀▄ ▄▄▀▀▀█ ▀▀▀█ ▄██▀ ▀ ▀▄▄▄▀▀█▀▀▀▀▄ ████
█████▀▀▀ █▄▄█ █▄  █▀▀▄ █▄ █▀▀▄ ▀█ █▀▄  █▄ ▄██▄▄▄▀████
████▄██▄▄█▄▄ ▀▀ █▀▄█▄▀ █ ▄▄▄ ▄▀▄▄█▄▀█ ▀▀ ▄▄▄ ▄█  ████
████ ▄▄▄▄▄ █▄▀██▄▀█ ▀▀▀▀ █▄█  ▀▀ ▄▀█ ██  █▄█ █ ▀▄████
████ █   █ █ ▀▄▀▄▀█▀ ▄▀▄▄   ▄ ▄▀█ ▀ █▄▄▄ ▄▄   ▀  ████
████ █▄▄▄█ █ ▀█▀█ █▀██ ▄▀██▀  ▄ ██▄██▄▄ ▀██ ▀▄▄██████
████▄▄▄▄▄▄▄█▄█▄█▄▄▄███▄▄█▄▄▄█▄██▄▄▄▄▄▄██▄█▄▄▄█▄▄█████
█████████████████████████████████████████████████████
█████████████████████████████████████████████████████
































#+end_example

* Google Authenticator backup
#+name: google_auth
#+begin_src sh :results output
adb shell "su -c \"sqlite3 -line /data/data/com.google.android.apps.authenticator2/databases/databases 'select email, secret, original_name from accounts'\"" | dos2unix
#+end_src

#+RESULTS: google_auth
#+begin_example
        email = fake.address@gmail.com
       secret = fakesecret123456
original_name =
#+end_example

* FreeOTP backup
#+name: free_otp
#+begin_src python :results output
from xml.etree import ElementTree as ET
import base64, json, subprocess, sys

cmd = ["adb", "shell", 'su -c "cat /data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml"']
ret = subprocess.run(cmd, stdout=subprocess.PIPE, check=True)
root = ET.fromstring(ret.stdout)
for child in root:
  name = child.attrib['name']
  if child.attrib['name'] != 'tokenOrder':
    data = json.loads(child.text)['secret']
    data = ''.join('%02x' % (x % 256) for x in data)
    secret = base64.b32encode(bytes.fromhex(data)).decode('utf-8').rstrip('=').lower()
    print(name, ' ' * (40 - len(name)), secret)
#+end_src

#+RESULTS: free_otp
#+begin_example
Google:fake.address@gmail.com             fakesecret123456
#+end_example

For additional fun, I save this file in a secure location and encrypt it with GnuPG. Again, it’s really easy in Emacs: just save the file as OTP-backup.org.gpg, and it will ask for the details before passing the file to GnuPG.

I’ve been using this file for several years now and I’m really, really happy with it.

Warning: your Android phone needs to be rooted and to have USB debugging enabled for the backup scripts to work.

Comments

Join the conversation by sending an email. Your comment will be added here and to the public inbox after moderation.