Recently my phone went out of order for a day. I immediately switched over to my backup phone and was back to normal after one hour.
This was a great opportunity to check if I still had all T’s crossed and I’s dotted and full control over my data in case of an unexpected event - like missing the phone.
Surprise: I wasn’t fully prepared.
While most applications and their data were recovered, easily restored and no data was lost, I realized that the Multi-Factor-token generator that I use on my phone had no backup. I used it everyday to get access to customer sites and resources.
Using my phone as the token generator had put me in a situation where I was dependent on that particular phone. Some websites offer one-time codes during the setup of the MFA and those I still had, but the token generator itself was a single point of failure.
Out of pure luck I was able to return to a stable phone situation within a day. That made looking at that problem easier.
The One-time-password (OTP) generator I use on the phone allows me to export and import the accounts. The exported data also includes the secret used to setup the account/token. Interesting.
I would rather not have that come astray in a random backup being taken in the background. I wanted a fallback application on my desktop in order to be able to generate the tokens there as well in case I needed to.
Almost all secrets I have are stored in pass and that keeps them GPG encrypted. This is the natural place for the token secrets as well in my case. The solution needed to be able to fetch the secrets dynamically from the store and use them to generate the one-time passwords.
A solution forms.
oathtool is precisely designed to generate one-time passwords from secrets. Neat.
The command for generating a token would look like this:
$ oathtool --totp --base32 -d 6 $(echo '$secret' | base32) 055131
Click to expand...
--totp: The default for this parameter is SHA1, which is identical to the information I got from the exported file from the token generator.
--base32|-b: The tokens are usually base43 encoded, so we do this as well.
--digits|-d: The default number of digits according to my export was six. This parameter defines the same number of digits in the one-time password.
Now we need to fetch the secret from the password store and place it inside the command.
$ oathtool --totp -b -d 6 $(pass show otp/customer_a | head -n 1) 636296
That seems to work. The returned token has changed, since some minutes had passed since I ran it the last time.
Quick and dirty and secure integration
Now, how to integrate this rather messy command into the daily workflow?
For starters I add a bash alias:
alias otp.customer_a="oathtool --totp -b -d 6 $(pass show otp/customer_a | head -n 1)"
As mentioned in this article this is rather simple in my setup, but an equivalent entry in
.bashrc should do the trick as well.
However: This does not work.
When running the alias the one-time password never changes. It stays the same, no matter how long I wait.
I managed to stumble on a quirky limitation of bash
alias. Aliases evaluate their command when they are being defined, not when they are being executed. That means the alias
otp.customer_a was not calling the password store to fetch the secret, but rather had done this already once when initialised. Just check the alias definition:
# Set alias $ alias otp.customer_a="oathtool --totp -b -d 6 $(pass show otp/customer_a | head -n 1)" # Show alias $ alias otp.customer_a alias otp.customer_a='oathtool --totp -b -d 6 secret_string'
Let’s re-arrange the command a bit to solve this:
$ alias otp.customer_a="pass show otp/customer_a | head -n 1 | tr -d '\n' | xargs -0 oathtool --totp -b -d 6" $ alias otp.customer_a alias otp.customer_a='pass show otp/customer_a | head -n 1 | tr -d '\''\n'\'' | xargs -0 oathtool --totp -b -d 6'
- Now the alias executes the pass command every time.
head -n 1limits the return from pass to the first line. That line contains usually the password (in this case the secret) string.
td -d '\n'removes any trailing newlines. The additional
'-characters just escape the ones in the command.
xargs -0hands the result from pass over to
The result is local one-time password generator and backup in case the phone is not available. Still the secrets are protected. Probably better than on the phone.
A quick verification of the codes with the phone based generator showed the same results and I was able to get access using this one-time passwords as well.
- I should keep up to date with the data and configuration on my phone and think more often about security in that context. Though the phone itself is protected against malicious access, there are a couple of more steps that can be taken to increase the security further.
- I need to store the secret for the MFA setup safe in order to be able to restore access to customer resources more quickly.
- Other procedures for setting up the backup-phone worked as planned and no data would be lost in case of a loss.
- Reading the man page of bash back in the day did more good than harm. Though it’s quite long, I still benefit from it when I can recall those limitation on
A colleague hinted that
pass already has a otp-plugin available.
After a quick look it looks like I have re-invented the wheel. That happens in the middle of a crisis.
The installation is (under Fedora) fairly simple:
sudo dnf install -y pass-otp
All other steps in the post above can be compressed further:
Put the OTP key in URI format into
$ pass otp insert otp/customer_a Enter otpauth:// URI for customer_a: Retype otpauth:// URI for customer_a:
All this does is putting the string into the pass GPG file at the first line. Adding additional information into the other lines below will work as usual.
Fetch the OTP token when needed:
$ pass otp otp/customer_a 123456
Configure the alias:
alias otp.customer_a="pass otp otp/customer_a"
This solution is cleaner, easier to implement and to maintain as well. Thanks @kid for pointing to that plugin. Guess I have to go and re-configure this now really quick.
Using this over several months made me add a small extension to the alias.
alias otp.customer_a="pass otp otp/customer_a | xclip && echo 'OTP copied to clipboard.'"