How do One-Time passwords work?

One-time passwords (OTP) are short sequences of numbers that authenticate user for a single login. They are often used as an additional layer of security when the 2-factor authorisation is enabled.

A picture is worth a thousand words, here’s what I’m talking about:

google auth

How are these numbers generated and can we reproduce them?

Import the secret

Let’s take Google’s 2-factor authorization as an example. It all starts with telling the authenticator app the secret key from your account. It is what you get when you scan the QR code during the 2FA setup for your Google account. And luckily there is an option for those who don’t have a QR reader:

  1. Go to “Google account” - https://myaccount.google.com/
  2. Go to the “Security” section in the menu on the left.
  3. Select the “2-Step Verification” section.
  4. You would probably be asked for the password.
  5. Select the “Change Phone” item in the “Authenticator App” section.
  6. A pop-up should appear asking if you have an Android or iOS device, choose any of those.
  7. When the QR code appears - select the “Can’t scan it” link below it.

This should reveal the secret key used for your account OTP generator. Keep it secret.

If you wonder, the QR there contains a special link, such as otpauth-migration://offline?data=.... Data parameter is a base64-encoded Protobuf message, containing the hashing algorithm (SHA1/256/512 or MD5), number of digits in the OTP (six or eight), OTP type (time-based or HMAC-based), and the actual secret bytes.

We wouldn’t bother with deciphering protobuf here, instead we would just copy the plain text secret that Google offers when you choose “Can’t scan it”. Further on I will be referring to is as $SECRET environment variable.

HMAC-based passwords

HMAC-based one-time passwords are documented in RFC 4226. The algorithm takes as an input:

The algorithm itself does the following three steps. It generates a HMAC-SHA-1 value, where secret is the key and counter is the message to be hashed using the key. Then it truncates the 20-byte hash string to a shorter value.

The truncation is rather uncomplicated, the last byte from the 20-byte string is taken, the lower 4 bits are considered the “offset”. This makes the offset to be always in the range from 0 to 15.

Then, the sequential 4 bytes starting from the offset are taken and converted into the 32-bit integer. Finally, the integer is printed as decimal, stripped down to the 6 digits, and that is the requested OTP.

Here’s an example from the RFC to make it clear:

-------------------------------------------------------------
| Byte Number                                               |
-------------------------------------------------------------
|00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|
-------------------------------------------------------------
| Byte Value                                                |
-------------------------------------------------------------
|1f|86|98|69|0e|02|ca|16|61|85|50|ef|7f|19|da|8e|94|5b|55|5a|
-------------------------------***********----------------++|

The message above is a HMAC-SHA-1 value, a string of 20 bytes. The last byte is 0x5a. The lower 4 bits are 0xa, or 10 in decimal. This means we should take the bytes 10, 11, 12 and 13 and read them as a 32-bit big-endian integer 0x50ef7f19. In decimal this integer would be 1357872921. The last 6 digits would be “872921” and that would be the OTP.

Not that complicated, huh?

Time-based passwords

The RFC that describes one-time passwords is very brief. It only mentions that the counter value from the HOTP algorithm above would be the time difference between “now” and some fixed start point (usually 1 Jan 1970), divided by some fixed time interval (often: 30 seconds).

The rest is handled by the algorithm above.

Knowing this, we can finally implement time-based OTP (here the code is in Go, but you may use the language of your choice):

func totp(secret []byte) (string, error) {
  // Counter is UNIX time in seconds, divided by interval of 30 seconds
  counter := time.Now().Unix() / 30
  // Decode Base-32 secret key
  key := make([]byte, 64)
  if _, err := base32.StdEncoding.Decode(key, bytes.ToUpper([]byte(secret))); err != nil {
    return "", err
  }
  // Write counter as 8 bytes, big-endian
  b := make([]byte, 8)
  binary.BigEndian.PutUint64(b, uint64(counter))
  // Calcular HMAC-SHA-1
  mac160 := hmac.New(sha1.New, key)
  mac160.Write(b)
  b = mac160.Sum(nil)
  // Find offset, lower 4 bits of the last byte
  offset := b[len(b)-1] & 0xf
  // Read 4 bytes from offset as 32-bit integer
  n := binary.BigEndian.Uint32(b[offset : offset+4])
  // Covert it to decimal
  s := fmt.Sprintf("%06d", int(n & 0x7fffffff))
  // Return last 6 digits
  return s[len(s)-6:], nil
}

That’s just 16 lines of code! If you call totp() function passing it your secret key – you should get a 6-digit number that matches the one in the Google Authenticator:

otp, err := totp([]byte(os.Getenv("SECRET")))
if err != nil {
  log.Fatal(err)
}
log.Println("OTP:", otp)

Now you can use this little utility to get one-time passwords quickly without reaching for the phone.

I hope you’ve enjoyed this article. You can follow – and contribute to – on Github, Mastodon, Twitter or subscribe via rss.

Apr 28, 2021

See also: my minimalistic agile issue tracker and more.