For the past 30 years or so, we’ve settled on using a password-username pair as a happy compromise between usability and security. You have some knowledge that no one else has, so it must be you who’s trying to log in.
Passwords, however, aren’t foolproof. People pick bad ones and even the good ones are at risk of being exploited (for example, by keyloggers). That’s why a second factor can be useful. On top of something you know, you must also provide something you have to be authenticated.
Types of Two-Factor Authentication
A server can use any of these three options to prove that you own the trusted device:
- Your phone number (via SMS)
- A software key (also known as TOTP)
- A hardware key
While their implementation differs a little bit, the authentication flow for all of them is very similar:
- The user sends their username and password to the server to be authenticated as usual
- The server confirms the credentials are correct and asks for a second input from the user
- In the case of the first two methods, it’s a one-time passcode (OTP); we’ll talk about hardware keys in a future article
- The user provides the OTP, and the server finally authenticates the user
With mobile phones being as ubiquitous as they are today, they’re an obvious choice for “thing you have,” so perhaps it won’t surprise you that SMS 2FA is the most widely adopted of the three (you can check a list of services and what kind of 2FA they support here).
The application generates a unique passcode that is valid for a fixed amount of time, generally a few minutes or until a new one is generated, and sends it to the user via SMS.
The point of generating “one-time passwords” (or OTPs) like these is to prove that you have ownership of the phone number on file, which should be unique to the user.
It protects them from a scenario where the password is known by a third party, but they don’t own the phone number. And in the event they’re re-using passwords across services, which should be avoided at all costs, the passcode is different for each service, giving them an extra layer of security.
However, there are some shortcomings with this approach. Even though the latest NIST guidelines didn’t remove the recommendation of SMS as a second channel for authentication, some experts such as Brian Krebs have cautioned about using them as identity proof.
Phone numbers were never designed to be used for security purposes. It’s meant to be given out publicly — it’s not that hard for an attacker to find out what your phone number is. From there, they have many options to get the OTP: social engineering or phishing you, or even attacking the carrier.
One of such attacks, maybe the most common (at least the one that gets more press coverage), is known as a SIM swap attack. Carriers will often swap a phone’s service to a new SIM card if the customer requests it (presumably because they lost their phone).
The SIM swap attack consists of an attacker requesting the transfer of the victim’s SIM service to another SIM card that they control, and it can happen with inside help via rogue employees or without it by social engineering the support staff.
And because it must include third parties, such as the phone carrier, and the SMS protocol wasn’t designed with security in mind, the attack surface of SMS 2FA is larger than the other two methods.
Why is it so widely used, then? The adoption of two-factor as a whole is low (less than 10% of Gmail users use 2FA according to a 2018 report), so trying to drive the adoption rate up by using something that the users are already very familiar with, like SMS, is desirable.
However, there’s a way to keep the good things about SMS 2FA while removing the bad: by using software 2FA. Now the carrier is no longer involved with the process, but the codes can still be read from the user’s phone.
The biggest problem with software 2FA, in my experience, is that it’s the most difficult type of authentication for people to understand.
While everyone has received an SMS before, and they also intuitively understand that hardware keys work like real-world keys, it’s often not clear how software 2FA works because there’s a lot of “magic” happening under the hood.
So today, we’re going to peel back the curtains and see how it works, how the codes are generated, what it protects and doesn’t protect from.
How 2FA Software (or TOTP) Works
The main difference between software 2FA and SMS 2FA is that the codes are generated on the user’s trusted device instead of being sent by SMS.
For that to happen, a key (i.e., a random string) is shared between the server and a user’s device ahead of time. This generally means scanning a QR code over a secure connection.
The user will have to install an application capable of storing the shared key and derivating the passcode locally on their phone. The server and the app will perform the same operations to derive a passcode, and given that they used the same key (or seed), the codes should match up, and the user will be authenticated.
To constantly update the passcode, the current time is appended to the key; otherwise, the passcode would just become a second password vulnerable to replay attacks and not a second factor. This is why this method is also known as TOTP 2FA or time-based one-time password.
Illustrating with Python
Let’s go over this process one more time, this time more slowly and with code snippets, so you can understand better what’s happening under the hood.
Note: this is meant to be used as an illustration and should not be used in production. Always use vetted libraries or a dedicated service like Auth0 to handle authentication for your application._
The RFC6238 states that the shared key “should be randomly generated or derived using key derivation algorithms.” We’ll use Python’s built-in library secrets to generate our 16-character, base32-encoded seed:
import secrets def generate_seed(): return secrets.token_hex(16)
This will generate a result like:
The current time must be hashed together with the shared key to make the passcode constantly change the hash digest. To make sure we are in sync with the server, the current time must be represented in Unix time, that is, the time elapsed, in seconds, since Jan 1, 1970 (UTC). Let’s import the necessary libraries and calculate the current time:
import time current_time = time.time()
However, if we proceed like this, the user would have no time to authenticate before the passcode changed again. So the protocol establishes a time step (default value of 30 s) for the passcode to be updated. The resulting number should then be rounded down to the nearest 30 s using the floor function:
import math time_step = 30 # In seconds t = math.floor(current_time / time_step)
The number of time steps T is hashed together with the seed. The RFC6238 protocol uses HMAC-SHA-1 by default but says we may also use HMAC-SHA-256 or HMAC-SHA-512 as our hash function. We’ll stick with the default here and use SHA-1:
import hashlib import hmac h = hmac.new( bytes(seed, encoding="utf-8"), t.to_bytes(length=8, byteorder="big"), hashlib.sha1, ) digest = h.hexdigest()
And we’ll be able to produce a SHA-1 hash like:
Unfortunately, it’s very hard to type this string in 30 seconds, so the protocol also dictates that this string must be truncated, from where the 6-digit passcode is derived.
We do this by a process called dynamic truncation. First, let’s display the hash as if it were 20 individual 1-byte strings, like in the image below, for easier visualization:
As specified in RFC 4226, we first look at the last character of the hash to determine a starting point for our truncated hash. In this case, the last character is 1:
So we’ll have to grab the next 31 bits, starting with offset 1. Since we start counting the blocks from 0, we must grab four blocks starting from the 2nd block:
So our truncated value from the original hash is cf2f7d74. To get the TOTP we just have to convert it to decimal and show the last d digits by calculating the modulo 10^d of the string. Let’s do that:
offset = int(digest[-1], 16) binary = int(digest[(offset * 2):((offset * 2) + 8)], 16) & 0x7fffffff passcode = binary % 10 ** digits
Since the inputs are hashed, when time elapses enough for the counter to increase, the hash will change completely. For example, using the seed generated above, the passcode on Dec 1, 2021, at 4:10 PM would be 938370, and just one minute later, at 4:11 PM, it would be 351242.
Putting it all together, we have this:
import hashlib import hmac import math import secrets import time def generate_seed(): return secrets.token_hex(16) def generate_totp(seed, digits): current_time = time.time() time_step = 30 # In seconds t = math.floor(current_time / time_step) h = hmac.new( bytes(seed, encoding="utf-8"), t.to_bytes(length=8, byteorder="big"), hashlib.sha1, ) digest = h.hexdigest() return truncate(digest, digits) def truncate(digest, digits): offset = int(digest[-1], 16) binary = int(digest[(offset * 2):((offset * 2) + 8)], 16) & 0x7fffffff passcode = binary % 10 ** digits return str(passcode).zfill(digits) # Pads passcode with 0s if necessary
What Makes It Better than SMS 2FA?
Software 2FA is superior to SMS 2FA for a few reasons. In terms of security, your attack surface is reduced since the seed is shared over a secure channel instead of SMS, which is not very secure.
The opportunity window for an attacker is also reduced since the code changes more frequently — every 30 seconds (and up to 90 seconds since the protocol specifies a margin of up to one step in both directions to account for connection delays). This is not to say it’s impossible — in 2019, an attack on famous YouTube channels used a reverse-proxy to make the victim believe they were logging into a legitimate Google site, and passed the user credentials, including the TOTP code, to the legitimate website fast enough for the TOTP code to still be valid. Nevertheless, it’s harder to pull off less sophisticated attacks.
It also scores better in the usability score. You can store seeds in as many devices as you want, and they don’t need cellular service to work — you can store them in smartwatches, tablets, and computers on top of your phone.
What Software 2FA Is Good for and What It’s Not Good For
The biggest point of contention of this topic is what they are good for and not good for. On one side, you have experts like Bruce Schneier arguing that they don’t help as much as people believe they do.
In an essay from 2005 that is still very relevant today, Schneier says that 2FA protects users from passive threats, like eavesdropping, that was true in the 90s but aren’t as prevalent anymore.
Egor Homakov, CEO of security firm Sakurity, agrees with him and argues that what makes 2FA secure is the fact that users are given a strong seed from which the TOTPs are derived, not the second factor itself, so software 2FA is, in practice, a substitute for password managers.
On the flip side, Troy Hunt says this argument misses a point, and he believes that implementing any kind of 2FA, even SMS 2FA, is always beneficial.
Even if 2FA is not a perfect substitute for good passwords and cyber hygiene, that doesn’t mean it’s not useful. A system administrator that can’t get employees to buy in into using a password manager can still force them to use 2FA. The same is true for app developers, who stand to lose a lot if customers use insecure passwords or a data breach happens — Hey.com, for example, mandates that users use 2FA to use the service.
The biggest problem with 2FA is that it gives a false sense of security for less security-conscious people, and those also tend to not have good passwords (Troy also agrees with this). Bad actors will try to exploit that.
So, understand that software 2FA doesn’t make you impenetrable, but it helps. And assuming you are using greats passwords everywhere you can, here are some scenarios where using 2FA can protect you:
- Password reuse. You should never reuse passwords, and enabling 2FA doesn’t excuse it... but in the event, you’re forced to repeat passwords, for whatever reason, having a different passcode for each application you use gives you some additional protection.
- Password breaches. If your password has been breached and somehow cracked, a second factor can still protect your account for enough time for you to change the password before the attacker can cause more trouble.
- Keyloggers on untrusted devices. A keylogger could record both your password and the TOTP code, but by the time the attacker tries to get into your account again, your TOTP code has changed. (Assuming you’re storing your seeds on a trusted device and entering the passcodes by hand.)
- Social engineering the user (kind of). The attacker could trick you into giving him your password, and even both the password and the TOTP code, but the window of opportunity is much smaller. However, a crafty bad actor will know he has to act fast to circumvent this.
And what it doesn’t protect you against:
- Phishing with more sophisticated MITM attacks. If an attacker sets up a fake website with a reverse proxy and successfully phishes a user, they can pass through the user’s credentials to the legitimate website and log in.
- Phishing or social engineering of an employee of the website you want to access. If an employee is socially engineered into giving access to the back-end of the service for an attack, your account could be compromised regardless if you have 2FA enabled or not. This happened to Twitter last year — multiple accounts were hacked, some of which had 2FA enabled, via a phishing attack targeting Twitter employees.
- Password and seed leaks. Anyone can generate your TOTP codes if they have your seed. However, it would take having both the seeds and your password (which are hopefully hashed) to log into your account.
Where to Store the 2FA Seeds?
A few parting thoughts on where to store your seeds, even if it’s hard to make broad recommendations as each person should evaluate their threat models individually. No matter which route you choose, these two principles are hard to beat:
- It’s not annoying
- It doesn’t depend on human action to work
Having to type OTPs every time you log in can get annoying pretty quickly. Some password managers will store the seeds for you, and auto-copy the passcode to your clipboard when you’re logging into an app, so you can easily paste it when required.
And, of course, it’s paramount that these seeds are kept secure and somewhere where third parties can’t access them. If you store them on your password manager, they would be inaccessible without your master password.
If you’d rather use a separate authenticator app, check what kind of security features they offer. Some will require a PIN or fingerprint to reveal your codes, others will rely on the security provided by the device itself (e.g., by storing them in storage space restricted to the app, normally inaccessible, unless the device is rooted or jailbroken), so make sure the developer is trustworthy, and the device itself is secured.
And about the second bullet, storing the seeds offline makes them more secure but also can create huge headaches if you lose the device where you store the seeds and have no backup. You are at the mercy of admins to give you a second chance to regain control of your account. At the very least, you’ll have to spend some time with a support person and send them the required documents to prove that you are who you say you are.
For that reason, it’s important to have backups of your seeds. Some apps like Authy will make cloud backups and sync them to your other devices. Most cloud-based password managers can do the same.
If you are worried about storing your seeds and password in the same place, ask yourself this:
What risks do you incur by putting them in the same place as your passwords, and are they worth it?
It’s true that if you don’t secure your password manager with 2FA (either with a seed stored in a different app or a hardware key), the attacker would only need to know your master password to obtain your seeds.
However, if you don’t use 2FA because it’s too cumbersome for you to care, your accounts would be compromised either way in case the vault is compromised. Storing the seeds in your vault at least protects you from the scenarios we went over before. And if the convenience of storing the seeds in your password manager is what makes you use 2FA regularly, I’d say it’s a net positive.
Now, if you wanted to secure the password manager itself with 2FA, then yes, you’d need to use a second app to store the password manager seed (at which point you may decide to, heck, just store all of your seeds there instead — it’s your prerogative)—or perhaps using a hardware key, which is the most secure kind of 2FA there is, and also the topic of my next article.