Implementing one-time password
Published: 2022-06-27 00:12:18

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](https://datatracker.ietf.org/doc/html/rfc6238#section-4.2) 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)
```

Golang code is below.
```go
// 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)
}
```

## HOTP
HOTP is defined in [RFC4226](https://datatracker.ietf.org/doc/html/rfc4226#section-5.2) 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))
```

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)
```

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

```go
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)
}
```

### 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
```

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`.

```go
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)
}
```

### 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.
```go
num := dynamicTruncate(hs)
codeNum := num % uint32(math.Pow10(digits))

f := fmt.Sprintf("%%0%dd", digits)
oneTimePassword := fmt.Sprintf(f, code)
```

# Example

## Set up Google Authenticator
Here, I'll try to use one-time password. ([README](https://github.com/ksrnnb/otp#getting-started))

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](https://storage.googleapis.com/zenn-user-upload/6f92df460f0b-20220626.png)

## Run server
Run server by executing `make run`.

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

![login page](https://storage.googleapis.com/zenn-user-upload/55021a51954b-20220626.png)

| 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](https://storage.googleapis.com/zenn-user-upload/6a9096b3c090-20220626.png)

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
- [RFC4226](https://datatracker.ietf.org/doc/html/rfc4226)
- [RFC6238](https://datatracker.ietf.org/doc/html/rfc6238)
- [Google Authenticator Key Uri Format](https://github.com/google/google-authenticator/wiki/Key-Uri-Format)