I have broken my phone screen. Not a big deal but I use it for 2FA. While replacement is on the way I will extract credentials from FreeOTP and import them in password store. Be aware that it is completeley against the idea of 2FA, because now both password and otp are on the same device.
So, first I need to find where FreeOTP stores OTP secrets. Android apps usually use sqlite db for that, but not in this case. FreeOTP uses shared_prefs.
Pull prefs from the phone (root needed).
adb root
adb pull /data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml
Here is what they look like.
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="Amazon Web Services:sarg@organization">{"algo":"SHA1","counter":0,"digits":6,"issuerExt":"Amazon Web Services","issuerInt":"Amazon Web Services","label":"sarg@organization","period":30,"secret":[52,19,105,64,67,-120,-45],"type":"TOTP"}</string>
<string name="tokenOrder">["Amazon Web Services:sarg@organization","Google:sarg@sarg.org.ru","GitHub:sarg"]</string>
</map>
Storing JSON in XML - that’s a beaty. If you decode the value, it looks better:
{
"type": "TOTP",
"secret": [52, 19, 105, 64, 67, -120, -45],
"period": 30,
"label": "sarg@organization",
"issuerInt": "Amazon Web Services",
"issuerExt": "Amazon Web Services",
"digits": 6,
"counter": 0,
"algo": "SHA1"
}
Now I need to:
- parse xml
- extract json from xml
- extract secret from json and encode it with base32
- construct otpauth:// uri using details from json
URI format is described in google-authenticator wiki.
Provision a TOTP key for user alice@google.com, to use with a service provided
by Example, Inc:
otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
For one-time scripts I prefer using python. There is a handy library for dealing with html/xml - BeautifulSoup. And I will consult with FreeOTP sources to get things right.
#!/usr/bin/env python3
import urllib.parse
import base64
import json
from bs4 import BeautifulSoup
inp = open('tokens.xml')
soup = BeautifulSoup(inp, 'lxml')
for tag in soup.find_all('string'):
# it's a special setting, skip it
if tag['name'] == 'tokenOrder':
continue
token = json.loads(tag.text)
# secret is stored as signed byte array
# so first make it unsigned by adding 256 modulo 256
# and then convert to python's bytearray
secret = bytearray(map(lambda x: (x+256)%256, token['secret']))
issuer = token.get('issuerExt', '')
print('otpauth://{type}/{issuer}?{args}'.format(
type=token['type'].lower(),
# issuer should be URL encoded
issuer=urllib.parse.quote(
':'.join(filter(None, (issuer, token['label'])))),
args=urllib.parse.urlencode({
'secret': base64.b32encode(secret).decode('utf-8'),
'algo ': token['algo'],
'digits': token['digits'],
'period': token['period']
})
))
inp.close()
Now what’s left is just to import everything into pass with pass otp insert -e
.
Or you can manually put otpauth://
string in an appropriate pass file. I did
the latter way, because my already existing entries have had another names.
Then check that everything went fine - pass otp 'Amazon Web Services/sarg@organization'