The algorithm
The TOTP algorithm, as defined in RFC 6238, is a time-based one-time password (TOTP) algorithm that uses a shared secret key and the current time to generate a one-time password. The algorithm consists of the following steps:
- Generate a shared secret key, encoded in base32 or base64.
- Generate a time-based value, called the “time step” or “T”. This value is typically the number of time steps (e.g. 30 seconds) since the Unix epoch (January 1, 1970).
- Create a byte array that represents the time step value. This is typically done by converting the time step value to a 64-bit integer in little-endian byte order.
- Create a new HMAC (Hash-based Message Authentication Code) instance using the shared secret key and the SHA-1, SHA-256, or SHA-512 algorithm.
- Write the time step byte array to the HMAC instance.
- Compute the HMAC digest and get the last byte of the digest, which represents the offset to use.
- Extract a 4-byte value from the digest, starting at the offset. This is called the “binary secret”.
- Use the binary secret to generate the TOTP by taking the least significant digits of the binary secret modulo 10^digits. Where digits is the number of digits in the TOTP, typically 6 or 8.
Dry run algorithm
Let’s do a dry run of the TOTP algorithm using the secret key “12345” and a time step of 59 seconds.
- Generate a shared secret key, encoded in base32 or base64.
- The shared secret key is “12345”
- Generate a time-based value, called the “time step” or “T”. This value is typically the number of time steps (e.g. 30 seconds) since the Unix epoch (January 1, 1970).
- The time step value is 59
- Create a byte array that represents the time step value. This is typically done by converting the time step value to a 64-bit integer in little-endian byte order.
- The time step byte array is [59, 0, 0, 0, 0, 0, 0, 0]
- Create a new HMAC (Hash-based Message Authentication Code) instance using the shared secret key and the SHA-1, SHA-256, or SHA-512 algorithm.
- We’ll use SHA-1 for the example
- Write the time step byte array to the HMAC instance.
- Compute the HMAC digest and get the last byte of the digest, which represents the offset to use.
- The digest is [179, 60, 119, 152, 63, 126, 155, 180, 92, 218, 66, 34, 186, 24, 183, 179, 57, 54, 200, 96]
- The last byte of the digest is 96, so the offset is 96 % 16 = 4
- Extract a 4-byte value from the digest, starting at the offset. This is called the “binary secret”.
- The binary secret is [24, 183, 179, 57]
- Use the binary secret to generate the TOTP by taking the least significant digits of the binary secret modulo 10^digits. Where digits is the number of digits in the TOTP, typically 6 or 8.
- The binary secret represents the value of 24,183,179,57 which is 24183317957
- If the number of digits is 8 then the TOTP value is 18331795 which is the least significant 8 digit of the binary secret modulo 10^8
It’s important to note that this dry run is done for demonstration purposes, in real-life scenario, the secret key should be kept private and the time step value should be obtained from the system clock or a time-sync service. Also, it’s better to use more secure hashing algorithm like sha256 or sha512.
Prove it with code
I pick 1 result data from https://www.rfc-editor.org/rfc/rfc6238.txt.
The test token shared secret uses the ASCII string value”12345678901234567890”. With Time Step X = 30, and the Unix epoch as the initial value to count time steps, where T0 = 0, the TOTP algorithm will display the following values for specified modes and timestamps.
Time (sec) | UTC Time | Value of T (hex) | TOTP | Mode |
---|---|---|---|---|
59 | 1970-01-01 | 0000000000000001 | 94287082 | SHA1 |
00:00:59 |
Here the implemetation uses SHA1 and the string value “12345678901234567890” as the shared secret, you can try it yourself on https://go.dev/play/p/tr8ITjlA_R2.
package main
import (
"bytes"
"fmt"
"github.com/ekamwalia/totp"
)
func main() {
var sharedSecret = str2hexa("12345678901234567890")
otpgen := totp.NewDefaultSHA1(sharedSecret)
otpgen.CodeDigits = 8
totp, _ := otpgen.GenerateTOTP("1970-01-01T00:00:59Z")
fmt.Println(totp) // output - 94287082
}
func str2hexa(secret string) string {
var buffer bytes.Buffer
for _, char := range secret {
buffer.WriteString(fmt.Sprintf("%02x", char))
}
return buffer.String()
}
Keep in mind that shared secret must be in hexa characters, if it is not then use the function str2hexa
to convert to hexa string.
The generated TOTP value is a numeric string in base 10 that includes the specified number of digits. The client and server can then compare the TOTP value to ensure that the client has access to the correct shared secret key and that the time on the client is synchronized with the time on the server.
Due the TOTP algorithm is based on the time, so it’s important to keep the client and server time in synchronization, otherwise the TOTP value will be different and the authentication will fail.