DEV Community

okarin
okarin

Posted on

Implementing one-time password

When login with two factor authentication, you may be required to enter one-time password(6 digits number).
I wonder how it is implemented, so I tried to implement it.
In this article, I'll explain abount TOTP(Time-Based One-Time Password) which can be easily generated using Google Authenticator.

Code

You can try two factor authentication easily using this code.
https://github.com/ksrnnb/otp

Specification

TOTP

TOTP is defined in RFC6238 as follows.

HOTP(HMAC-Based One-Time Password) is described later.

TOTP = HOTP(K, T)
T = (Current Unix time - T0) / X

K: a key to generate HOTP
T: an integer and represents the number of time steps between the initial counter time T0 and the current Unix time.
T0: the Unix time to start counting time steps (default value is 0)
X: the time step in seconds (default value X = 30 seconds)
Enter fullscreen mode Exit fullscreen mode

Golang code is below.

// TOTP time step
const timeStepSecond int64 = 30

func New(secret []byte) string {
    return hotp.New(secret, counter())
}

func counter() uint64 {
    return uint64(time.Now().Unix() / timeStepSecond)
}
Enter fullscreen mode Exit fullscreen mode

HOTP

HOTP is defined in RFC4226 as follows.

HMAC-SHA-1 is calculated from secret key K and counter C, then it is trancated.
In the case TOTP, counter is calculated by T = (Current Unix time - T0) / X.

HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
Enter fullscreen mode Exit fullscreen mode

Here, I'll explain about Trancate function.

Step 1

First, simply calculate HMAC-SHA-1. This result is 20 bytes.

HS = HMAC-SHA-1(K,C)
Enter fullscreen mode Exit fullscreen mode

Golang gcode is below.
binary pacakge is used to convert from number to []byte.

package hotp

import (
    "crypto/hmac"
    "crypto/sha1"
    "encoding/binary"
)

func hmacSha1(secret []byte, counter uint64) []byte {
    mac := hmac.New(sha1.New, secret)

    // uint64 => 8 byte
    byteCounter := make([]byte, 8)
    binary.BigEndian.PutUint64(byteCounter, counter)

    mac.Write(byteCounter)
    return mac.Sum(nil)
}
Enter fullscreen mode Exit fullscreen mode

Step 2

Generate 4 bytes string and convert to number from HMAC-SHA-1 value.
It is written in RFC4226 how to generate 4 bytes generate.

DT(String) // String = String[0]...String[19]
   Let OffsetBits be the low-order 4 bits of String[19]
   Offset = StToNum(OffsetBits) // 0 <= OffSet <= 15
   Let P = String[OffSet]...String[OffSet+3]
   Return the Last 31 bits of P
Enter fullscreen mode Exit fullscreen mode

Golang code is below.
The offset the lower 4 bits is calculated by the logical product of the trailing character and 0xf.
Then, the last 31 bits is obtained from offset ~ offset+3

This example is written like specification, but it maybe easy to understand to replace binary.BigEndian.Uint32(hs[offset:offset+4]) & 0x7fffffff.

func dynamicTruncate(hs []byte) uint32 {
    // get low-order 4 bits of hs[tail]
    // 0xf => 0000 1111
    offset := hs[len(hs)-1] & 0xf

    // get last 31 bits for hs[offset]...hs[offset + 3]
    // 0x7F => 0111 1111
    return uint32(hs[offset]&0x7f)<<24 |
        uint32(hs[offset+1]&0xff)<<16 |
        uint32(hs[offset+2]&0xff)<<8 |
        uint32(hs[offset+3]&0xff)
}
Enter fullscreen mode Exit fullscreen mode

Step 3

Let d be the number of digits of the one-time password, and A remainder obtained by dividing the value obtained in Step 2 by 10^d is HOTP value.

Golang code is below.

num := dynamicTruncate(hs)
codeNum := num % uint32(math.Pow10(digits))

f := fmt.Sprintf("%%0%dd", digits)
oneTimePassword := fmt.Sprintf(f, code)
Enter fullscreen mode Exit fullscreen mode

Example

Set up Google Authenticator

Here, I'll try to use one-time password. (README)

First, install Google Authenticator and scan QR code.

You can get string value otpauth://totp/otp_example?secret=NBSWY3DP from QR code. This formt is defined in [Google Authenticator Key Uri Format].(https://github.com/google/google-authenticator/wiki/Key-Uri-Format)

Here, NBSWY3DP is encoded value hello. When you use one-time password, you have to unique and random string each users.

qr code image

Run server

Run server by executing make run.

Login

Access to localhost:8080, and login using ID/Password.

login page

name value
id hogehoge
password hogehoge

After that, you'll be required one-time password, then enter 6 digits number generated by Google Authenticator.

one-time password page

After validate this value and generated value at server, you can login.

More detail

  • one-time password used once cannot use again.
  • You can also use one-time password before 1 time step. (considering network latency)

The validation system should compare OTPs not only with the receiving timestamp but also the past timestamps that are within the transmission delay.

Others

TOTP is defined more detail, for example, resynchronization but I omit it to be simple.
If you want to know more detail, I recommend to read references.

References

Top comments (0)