2FA and OTP

I have been thinking some things around unique hashes and One Time Passwords (OTP) as well as how Two Factor Authentication (2FA) works so I thought I would look it up.

I found this reasonably description on YouTube

How does the Google Authenticator Work? HOTP TOTP Difference 2FA Authentication

How does the Google Authenticator Work? HOTP TOTP Difference 2FA Authentication

The author mentions the implementation and rfc6238 - TOTP: Time-Based One-Time Password Algorithm. This also reminded me of the time I wanted to better understand YubiKey. Which led me to find the OpenSource alternative OnlyKey. That led me to think how one can build this with an Arduino and sure enought there are projects such as https://github.com/lucadentella/TOTP-Arduino all based on RFC6238.

Basically the idea is you have a secret and a counter. The counter is just unix time. To make the OTP not rotate too quickly time is usually divided by 30 seconds and depending on the implementation, some level of drift in the code entered to allow for network latency or clocks not being the same.

The HMAC-based One-Time Password (HOTP) algorithm then sha1 HMAC’s the secret and counter, looks at the last byte to work out which part of the generated hash to read. Then reads the parts of the hash and finally converts it to a 6 digit number which is the secret. If you type it in, the server will check it and you are good to go.

A common way to share the secret is via a QR code which you can then read into an app like Google Authenticator and from there authenticator will generate the codes for you as needed.

Ofcourse what happens on the server side. I had assumed that there was some kind of 1 way encryption going on, a bit like with passwords. With a password, you type it in, it gets processed and is compared with the result that is stored on the server. It is not trivial to find a password to create the same result and there is no way to change the result back into the password.

Well with OTP it seems that the secret is stored encrypted on the server. It is then decrypted and runs through the same algorithm to generate the same code. This means that if anyone can get to the server with the keys for encryption, they can easily decrypt the secret and start generating any number of one time passwords for any of the users. Note that at this point in time knowing the password is harder as they cannot reverse that.

If I have rails using [Devise] (https://github.com/heartcombo/devise) with Devise-two-factor which uses ROTP under the covers this is as simple as

bin/rails runner "User.where('encrypted_otp_secret IS NOT NULL').
  each{|u| pp [u.email, u.otp_secret, ROTP::TOTP.new(u.otp_secret).now] }"

["user_1@example.com", "LXCJAK6OBEUU2ZDQMFRKXHDH", "653883"]
["user_2@example.com", "4QZOTOTYD6FIEJ2ABW4GA3WQ", "288030"]

The middle column above is the secret in this case. Anyone with that secret will now be able to easily generate the OTP with

bin/rails runner "pp ROTP::TOTP.new('LXCJAK6OBEUU2ZDQMFRKXHDH').now"

"653883"

Ok but the secret is stored encrypted in the database

 psql -d rails_2fa_play_development \
-c "SELECT email, encrypted_otp_secret FROM users WHERE email = 'user_1@example.com';"

       email        |                   encrypted_otp_secret
--------------------+----------------------------------------------------------
 user_1@example.com | zxEW23kkTcTbOP06xlxd5o+UHfhwG3FiGiqF95ZOzUG3F7s7fVHtlQ==+
                    |
(1 row)

so how do we decrypt that.

Well first I re-recreated a rails app with devise and devise-two-factor which you can find here https://github.com/saramic/100-days-of-code/tree/main/2fa_play/rails_2fa_play

Then re-running the above commands, you can see that some of these things are setup differently by default

psql -d rails_2fa_play_development \
        -c "SELECT email, otp_secret FROM users;"

 email |            otp_secret
-------+------------------------------------------
 m@m.m | {"p":"eBD8EHxoL7nbW1lqjrE2dCnBui3Bo0Hr",
          "h":{"iv":"IMEXj/ujOBwsjfrI",
               "at":"0F7+0GLgMXUSCyu7bJePmA==",
                "e":"QVNDSUktOEJJVA=="}}

and in rails

bin/rails runner 'pp User.all.map{|u| [u.email, u.otp_secret] }'

[["m@m.m", "6H4ZOX4EFJCWGEZDADWEN5YD"]]

finally to generate an OTP

bin/rails runner "pp ROTP::TOTP.new(
  User.find_by(email: 'm@m.m').otp_secret).now"

"090549"

Next time

so how do we decrypt this? and how does the algorithm work? and how do we rate other frameworks like Rodauth