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.


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:

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 | | fakesecret123456 | google.png | otpauth://totp/ |
#+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 "" $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])

#+RESULTS: qrcodes
#+begin_example (Google)

β–ˆβ–ˆβ–ˆβ–ˆ β–„β–„β–„β–„β–„ β–ˆβ–€β–ˆβ–ˆβ–„β–€β–ˆβ–€β–€  β–€β–€β–„β–„β–„β–€β–€β–ˆβ–€β–„β–€β–„β–„β–€β–ˆβ–„β–ˆβ–€β–„β–ˆ β–„β–„β–„β–„β–„ β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆ β–ˆ   β–ˆ β–ˆβ–€β–„β–€β–ˆβ–ˆβ–„β–€β–ˆβ–„  β–„ β–„ β–ˆβ–€ β–ˆβ–ˆβ–€β–€ β–ˆ  β–ˆ β–€β–ˆ β–ˆ   β–ˆ β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–„β–„β–„β–ˆ β–ˆβ–€β–€β–ˆβ–€β–„β–ˆβ–„ β–„β–€β–ˆβ–„ β–„β–„β–„ β–ˆ β–ˆβ–€β–ˆβ–ˆ β–€ β–„β–„β–„β–ˆ β–ˆβ–„β–„β–„β–ˆ β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆβ–„β–„β–„β–„β–„β–„β–„β–ˆβ–„β–ˆβ–„β–ˆ β–€β–„β–ˆ β–€ β–€ β–ˆβ–„β–ˆ β–€ β–€ β–€ β–ˆ β–ˆβ–„β–€β–„β–ˆβ–„β–„β–„β–„β–„β–„β–„β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆ  β–„β–„β–„β–€β–„  β–ˆβ–„ β–ˆβ–€β–ˆβ–€β–ˆβ–€β–€β–„β–„β–„β–„  β–€β–„β–ˆβ–ˆβ–€  β–ˆβ–ˆβ–„β–€β–€ β–ˆ β–ˆβ–„β–€β–„β–€β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–€β–„β–€ β–ˆβ–„β–€β–„β–ˆ β–„  β–ˆβ–€β–ˆβ–€β–„β–„ β–ˆβ–ˆβ–€ β–€ β–„  β–€β–ˆβ–ˆβ–„β–„ β–ˆβ–€β–„β–ˆβ–€β–„β–„β–„β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆβ–€ β–ˆβ–ˆ β–„β–„β–ˆβ–ˆβ–ˆβ–„β–€β–ˆβ–„β–„ β–„ β–„ β–€β–ˆ β–€β–ˆβ–„β–€β–„β–„β–„β–„β–€β–„ β–€β–€β–„β–„β–€β–€β–„β–„ β–€β–€β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆ   β–„β–„β–€β–ˆβ–€β–„β–„β–„ β–€β–ˆ β–€ β–„β–ˆβ–€ β–„β–ˆβ–„  β–„ β–„β–€β–ˆβ–€β–€ β–€ β–ˆβ–€  β–€β–„β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆβ–„β–ˆβ–€β–ˆβ–€β–€β–„β–€β–ˆ    β–„β–„β–„β–€β–€β–ˆ β–ˆβ–ˆβ–ˆβ–ˆ  β–„β–ˆβ–ˆβ–€β–€β–„β–€β–„β–ˆβ–€β–€ β–„β–ˆβ–€ β–€  β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–„β–€β–„β–„β–ˆβ–„β–„β–ˆβ–€β–„β–ˆβ–€ β–„β–„ β–ˆβ–„β–„β–€ β–ˆβ–„β–€β–€β–„β–„β–€β–€β–€β–„ β–„β–„ β–ˆβ–„β–„β–ˆβ–„β–ˆβ–„β–„β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆβ–„β–„ β–€ β–„β–„β–„  β–ˆβ–„ β–„β–„β–€β–ˆβ–„β–„β–„ β–„β–„β–„ β–ˆβ–€β–„β–„ β–„β–€β–„β–„β–€β–„ β–„β–„β–„    β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆ   β–„ β–ˆβ–„β–ˆ β–„β–ˆβ–„β–€β–€β–ˆ β–„β–€ β–€ β–ˆβ–„β–ˆ    β–€β–€ β–ˆ β–„β–ˆβ–€ β–ˆβ–„β–ˆ   β–€β–„β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆβ–€   β–„ β–„β–„ β–ˆβ–€β–€β–„β–€β–ˆ β–ˆβ–€β–€β–€ β–„    β–„β–ˆβ–ˆ  β–€β–ˆβ–€β–ˆβ–ˆβ–„ β–„  β–„β–ˆ  β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆβ–„β–€β–ˆβ–ˆβ–€β–€β–„β–€β–€β–ˆβ–ˆβ–€β–ˆ β–„β–„β–€  β–€β–„β–„β–ˆβ–€β–€β–ˆ β–€ β–„β–ˆ β–ˆβ–€  β–„ β–„β–ˆβ–€β–„β–„ β–€β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆβ–„ β–ˆ β–ˆβ–€β–„β–ˆβ–ˆ β–€β–„  β–„β–ˆβ–„ β–„β–„β–„β–€β–„  β–„β–€β–„β–„ β–„β–€β–„β–€β–ˆβ–€β–ˆβ–„β–ˆ β–„β–€β–€  β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆ β–ˆβ–ˆβ–„ β–€β–ˆ   β–„ β–€ β–„ β–ˆβ–„β–ˆβ–„β–„β–„  β–€ β–ˆ β–ˆ β–€β–€β–ˆβ–„ β–€β–ˆ β–€ β–€β–„β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆ  β–ˆ β–€ β–„β–€β–€β–ˆβ–€β–„ β–„β–„β–€β–€β–€β–ˆ β–€β–€β–€β–ˆ β–„β–ˆβ–ˆβ–€ β–€ β–€β–„β–„β–„β–€β–€β–ˆβ–€β–€β–€β–€β–„ β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–€β–€β–€ β–ˆβ–„β–„β–ˆ β–ˆβ–„  β–ˆβ–€β–€β–„ β–ˆβ–„ β–ˆβ–€β–€β–„ β–€β–ˆ β–ˆβ–€β–„  β–ˆβ–„ β–„β–ˆβ–ˆβ–„β–„β–„β–€β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆβ–„β–ˆβ–ˆβ–„β–„β–ˆβ–„β–„ β–€β–€ β–ˆβ–€β–„β–ˆβ–„β–€ β–ˆ β–„β–„β–„ β–„β–€β–„β–„β–ˆβ–„β–€β–ˆ β–€β–€ β–„β–„β–„ β–„β–ˆ  β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆ β–„β–„β–„β–„β–„ β–ˆβ–„β–€β–ˆβ–ˆβ–„β–€β–ˆ β–€β–€β–€β–€ β–ˆβ–„β–ˆ  β–€β–€ β–„β–€β–ˆ β–ˆβ–ˆ  β–ˆβ–„β–ˆ β–ˆ β–€β–„β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆ β–ˆ   β–ˆ β–ˆ β–€β–„β–€β–„β–€β–ˆβ–€ β–„β–€β–„β–„   β–„ β–„β–€β–ˆ β–€ β–ˆβ–„β–„β–„ β–„β–„   β–€  β–ˆβ–ˆβ–ˆβ–ˆ
β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–„β–„β–„β–ˆ β–ˆ β–€β–ˆβ–€β–ˆ β–ˆβ–€β–ˆβ–ˆ β–„β–€β–ˆβ–ˆβ–€  β–„ β–ˆβ–ˆβ–„β–ˆβ–ˆβ–„β–„ β–€β–ˆβ–ˆ β–€β–„β–„β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ


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

#+RESULTS: google_auth
        email =
       secret = fakesecret123456
original_name =

* 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 =, 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)

#+RESULTS: free_otp
#+begin_example             fakesecret123456

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, 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.


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