TOTP/2FA support with ANY password manager (you read that right)
September 28, 2021
September 28, 2021
As Iām writing an article on how I reversed the Authy proprietary 2FA protocol to generate their codes from my own password manager and authenticator, I realized that I didnāt write about the hack I use to support TOTP in any password manager (especially the ones that donāt support it out of the box).
So Iāll start with that.
See, I explained some time ago why I switched to Firefox Lockwise as my password manager. In this post, I compare it to Bitwarden, and I go through some of the Lockwise cons, one of them being the lack of TOTP support.
I said that while it would be nice to have native support for TOTP, I already had my own authenticator app based on the totp-generator package. I since made that app a lot nicer and shared it on totp.vercel.app! š¦
Itās called TOTP with a password manager that doesnāt support TOTP š (yes, the emoji is part of the name) because I suck at naming things and thatās the most explicit name that I came up with.
Note: everything you need to know in order to use it is on the link above, but if you want to learn how it works in more technical details, keep reading!
Maybe like me you really like Lockwise or another password manager that doesnāt support TOTP, and thatās not enough of a reason to migrate to another app like Bitwarden, especially where TOTP support only comes in the paid version.
And you donāt want to install Yet Another Appā¢ to do this.
Maybe a bit of a niche, Iāll admit.
First things first, you probably want to check out the repo on GitHub.
The principle is pretty simple. Most password managers will recognize login forms on websites (e.g. username and password fields). Upon submission, theyāll prompt you to save the credentials you just entered, so that the next time you encounter this form, itāll be able to autofill the boxes and you just have to click ālog inā.
Additionally, if you log in later with a different username on the same site, itāll also ask you to save it, and now every time you come back to that form, youāll be able to chose amongst all the credentials that you saved.
TOTP with a password manager that doesnāt support TOTP š looks like a login form, walks like a login form, and quacks like a login form:
<input type="text" name="username">
<input type="password" name="password">
(Yes, itās that easy.)
This means that whatever you put in there, your password manager will prompt you to store upon submission of the form. With that trick, we can store arbitrary data in any password manager.
To retrieve that data, the user simply clicks on the username or
password field and chose from the list of all the saved items. We can
then read the values directly from the form fields. For example if the
above HTML was in a <form id="form">
:
const { username, password } = form.elements
console.log(username.value, password.value)
Since itās a bit tricky
to reliably detect when inputs are autofilled (the change
event is not
usually triggered), weāll use a Big Blue Buttonā¢ that reads āGet
TOTP šā instead. Good enough.
When that button is clicked, we use totp-generator to generate a code for the secret thatās in the password field.
If the TOTP settings differ from the standard SHA-1 algorithm, 6 digits
and 30 seconds period, you can also paste a somewhat standard
otpauth://
URI
thanks to the following code:
const totp = require('totp-generator')
function totpFromUriOrSecret (value) {
if (!value.startsWith('otpauth://')) {
// Directly the secret, use default options.
return totp(value)
}
const search = new URLSearchParams(new URL(value).search)
const { secret, algorithm, digits, period } = Object.fromEntries(search)
return totp(secret, { algorithm, digits, period })
}
For convenience, we can automatically copy the code to the userās clipboard:
navigator.clipboard.writeText(code)
But thatās not enough. While some services will give us an option to
retrieve the plaintext secret or a otpauth://
URI, a lot will only
give a QR code to scan, and some will even give nothing and force you
to use Authyās proprietary TOTP implementation (luckily, I already
reversed that so that you donāt have to).
For that, Iāll add two options: scan a QR code using the device camera, or import a QR code from an existing image (like a screenshot).
Luckily, thereās a QR scanner package that makes that really easy.
const QrScanner = require('qr-scanner')
function handleTotpUri (uri) {
const search = new URLSearchParams(new URL(uri).search)
form.username.value = search.get('issuer')
form.password.value = uri
}
const video = document.querySelector('video')
const qrScanner = new QrScanner(video, result => {
qrScanner.stop()
handleTotpUri(result)
})
qrScanner.start()
During the scan, the QR scanner will show the camera feed in the given video element.
On successful scan, we parse the URI to get the issuer
value (the
service that issued that secret) and fill the username field with it to
give a meaningful name to our secret. Then we can store the full URI in
the password field.
Weāll support two ways to upload files. With a regular file input, and with drag and drop.
For the file input, the following will do:
<input type="file" name="file">
const { file } = form.elements
file.addEventListener('change', () => {
handleFile(file.files[0])
})
And for the drag and drop, the drag-drop package makes it trivial for us:
const dragDrop = require('drag-drop')
dragDrop('body', {
onDrop (files) {
handleFile(files[0])
},
onDragEnter (event) {
document.body.classList.add('drag-drop')
},
onDragLeave (event) {
document.body.classList.remove('drag-drop')
}
})
Here we toggle a class on the <body>
element during drag and drop to
make it obvious that weāre accepting files to be dropped here.
Now, all we need is to write the handleFile
function thatās used by
both of those, where we scan the uploaded file and parse the TOTP URI in it.
function handleFile (file) {
QrScanner.scanImage(file)
.then(result => {
handleTotpUri(result)
})
}
Lastly, we might encounter situations where we only have the secret and specific TOTP settings (algorithm, digits and period), but no TOTP URI.
To support that, we need to add an āadvancedā mode allowing to edit the individual settings, and automatically generating the proper URI in the password field to be stored.
Weāll start by adding a hamburger menu that will open the detailed settings.
This will toggle the following form:
Here, I added for convenience a āAuthyā button that will automatically set the settings to 7 digits and a 10 seconds period, because thatās the main use case I have for this.
When any of those settings change, I generate a new URI to put in the password field:
const { username, password, secret, algorithm, digits, period } = form.elements
function updatePasswordFromDetails () {
const uri = new URL(password.value.startsWith('otpauth://') ? password.value : `otpauth://totp/${encodeURIComponent(username.value)}`)
const search = new URLSearchParams(uri.search)
search.set('secret', secret.value)
search.set('algorithm', algorithm.value)
search.set('digits', digits.value)
search.set('period', period.value)
uri.search = search
password.value = uri.toString()
}
secret.addEventListener('change', updatePasswordFromDetails)
algorithm.addEventListener('change', updatePasswordFromDetails)
digits.addEventListener('change', updatePasswordFromDetails)
period.addEventListener('change', updatePasswordFromDetails)
And this is it! You know everything thatās behind totp.vercel.app.
Did you find this useful? Did you like that I explained some of the code behind it in this blog post? Donāt hesitate to let me know and ping me on Twitter!
Until next time, keep hacking! šæļø