Let's try to turn of 2FA TOTP on lichess.org website (the user is temp_user12)
What's in the QR code? (I made a screenshot and extracted the QR code using zbarimg.)
It's:
otpauth://totp/lichess.org:temp_user12?secret=XYYUFRCR3YP7DMHSZOLAQWBRTEODNAZL&issuer=lichess.org
The essence in the 'secret' part, base32-encoded binary blob.
Using Google Authenticator app, import that key (scan QR code), and it will show something like this. (The app doesn't allow taking screenshots, so this is a photo of my device.)
The code changes every 30 second. A pie-chart at right shows how much time left till next change.
Let's enter the code (it has changed while I work on this blog post).
Now lichess.org website will ask for a 6-digit code each time during login. A user have to run Google Authenticator app each time and read the code.
Now let's dig into internals. Internally, it's just HMAC-SHA-1, where that base32-encoded secret is key, and the message is Unix timestamp divided by 30:
#!/usr/bin/env python3
import hmac, hashlib, time, base64, sys, re
def hotp(key: bytes, ctr: int, length: int) -> str:
m=hmac.new(key, digestmod=hashlib.sha1)
m.update(ctr.to_bytes(8, 'big'))
mac = m.digest()
offset = mac[-1] & 0xf
truncated = bytearray(mac[offset:offset+4])
truncated[0] &= 0x7f
value = int.from_bytes(truncated, 'big') % (10**length)
return str(value).rjust(length, '0')
URL=sys.argv[1]
result=re.search('secret=(.*)&', URL)
assert result!=None
secret=result.group(1)
s=base64.b32decode(secret.upper())
print ("The code:", hotp(s,int(time.time()/30),6))
time_remaining=int(30-(time.time() % 30)) # remainder from division
print (f"The code is valid for {time_remaining} seconds")
Pass an QR-decoded URL as an argument to the Python script and get a one-time code for login.
The server can calculate and verify entered code, because it have secret and knows current Unix timestamp.
Also, someone wrote a Python module for TOTP, pyotp, which makes things simpler/shorter:
#!/usr/bin/env python3
import pyotp, sys, time, datetime, re
URL=sys.argv[1]
result=re.search('secret=(.*)&', URL)
assert result!=None
secret=result.group(1)
totp=pyotp.TOTP(secret)
print ("The code:", totp.now())
time_remaining=int(totp.interval - datetime.datetime.now().timestamp() % totp.interval)
print (f"The code is valid for {time_remaining} seconds")
Corresponding RFCs: 4226, 6238.
Some interesting Wikipedia articles: 1, 2, 3, 4, 5.
I like to use TOTP, not because I'm paranoid, but because it's easier to use it with many popular online services (like Google, Github) instead of SMS or email confirmation.
But one problem is: that QR code or URL must be saved somewhere, because your smartphone with Google Authenticator installed may be lost or broken.
However, many online services, like Google, offer to download 'backup codes' that can help to restore access to your account without TOTP code.
But beware: the password and the QR code and/or backup codes should be hidden in separate places, because if an attacker will get password + QR code/backup codes at a single place, he/she will be able to login to your account. Or, it all should be properly encrypted and stored, maybe using a password manager.
Also, do not forget that Google Authenticator itself doesn't have any protection at all -- it has a convenient export function, to export the stored URL (with secret) in form of QR code.
