30 August 2017

FreeOTP Backup and Recovery



  1. Make sure adb can see your phone: adb devices
  2. Get a backup of the FreeOTP data: adb backup -f freeotp.bak org.fedorahosted.freeotp
  3. Grab the FreeOTP recovery tarball: https://drive.google.com/file/d/0B20nsfvgGNDgSlp2am9sWkNlalE/view?usp=sharing
    This tarball has a built version of android-backup-extractor, and a virtualenv with the python3 dependencies needed and a copy of freeotp-redisplay.py
  4. Install the JCE Unlimited libraries 
  5. Install Python3 if not already installed.
  6. Install qrencode (brew install qrencode on OSX)
  7. In the recovery tarball there's https://github.com/nelenkov/android-backup-extractor patched to remove the version check in AndroidBackup.java
  8. java -jar abe-all.jar unpack freeotp.bak freeotp.tar backup_password
  9. tar xvf freeotp.tar
  10.  . venv/bin/activate
  11. python3 freeotp-redisplay.py apps/org.fedorahosted.freeotp/sp/tokens.xml 

This will print something like:

Image 1: 0123456789ABCDEF - SomeSite:SomeDetails

The Image # is the PNG of a QR code that can be used to move the token to another device.

The Hex code can be passed to oathtool to get a one time token (it assumes SHA1 as the digest algorithm):
  1. Install oathtool ( brew install oath-toolkit on OSX)
  2. Make sure the current time is correct.
  3. oathtool --totp=sha1 0123456789ABCDEF
------

Copy of freeotp-redisplay.py:

#!/usr/bin/env python

from __future__ import print_function

import base64
import ctypes
import json
import subprocess
import sys
import xml.etree.ElementTree as ET
from urllib.parse import urlencode


def main():
    tree = ET.parse(sys.argv[1])
    root = tree.getroot()
    assert root.tag == 'map'

    count = 0
    for child in root:
        assert child.tag == 'string'
        name = child.attrib['name']
        if name == 'tokenOrder':
            continue

        if ':' in name:
            service, user = name.split(':', 1)
        else:
            user = name
            service = 'Unknown'

        info = json.loads(child.text)
        #print("Info for %s: %r" % (name, info))

        # Ensure that we only have unsigned values, then get secret
        secret_bytes = [ctypes.c_ubyte(x).value for x in info['secret']]
        secret_hex = ''.join('%02X' % (x,) for x in secret_bytes)
        secret = bytes.fromhex(secret_hex)
        secret_b32 = base64.b32encode(secret)

        # Make fancy URL.
        params = {
            'secret': secret_b32,
            'issuer': service,
            'counter': info['counter'],
            'digits': info['digits'],
            'period': info['period'],
            'algorithm': info['algo'],
        }

        tmpl = 'otpauth://{type}/{service}:{user}?{params}'
        url = tmpl.format(
            type=info['type'].lower(),
            service=service,
            user=user,
            params=urlencode(params),
        )

        count=count+1

        print("Image {0}: {2} -  {1}".format(count, name, secret_hex))
        process_qrencode = subprocess.Popen(['qrencode', '-o', "qr-{0}.png".format(count), url])


if __name__ == "__main__":
    main()

No comments: