Demystifying Time-based OTP

This article is written as an extensive explanation of how a Time-based OTP algorithm works and some guidelines on how to implement this in your F5.

What is a TOTP?

TOTP (aka Time-based OTP) is a way to use a code that is changing every 30 seconds instead of using a static password.

REF - https://en.wikipedia.org/wiki/Time-based_one-time_password
REF - https://datatracker.ietf.org/doc/html/rfc6238

So, in summary, every user has one secret associated that is shared between them and a third entity (F5), with this secret, it is possible to generate a 6-digit code that changes every 30 seconds, as Google and other vendors do. Take into account that most of the vendors are using the same algorithm, so, working with Google Authenticator is the same as using any other 6-digits TOTP (Microsoft Authenticator, FortiToken Mobile, etc.).

How to implement TOTP in production?

TOTP is composed of 3 steps:

  1. Generation of the secret
  2. Distribution of the secret
  3. Validation of the secret

How a secret is generated?

You can generate the code in many ways, but your goal is to get a 16-digit word (base32) for each user.

Next below, we are showing how to get this secret using TCL commands.

 

# Generate a random number as seed
set num         [expr rand()]
# OUTPUT: 0.586026769404

# generate a hash of this seed
set num_hash    [md5 $num]
# OUTPUT: Ï�àD½È�W\ݼú�Uä

# Encode this hash using base64
set num_b64     [b64encode $num_hash]
# OUTPUT: Cc+e4ES9yJRXXN28+o5V5A==

# Take only the first 10 digits of this previous code (10 digits x 8 bits = 80 bits)
set secret_raw  [string range $num_b64 0 9]
# OUTPUT: Cc+e4ES9yJ

# Encode the previous code using base32 (80 bits / 5 bits by word = 16 words)
set secret_b32  [call b32encode $secret_raw]
# OUTPUT: INRSWZJUIVJTS6KK

 

BTW, this is how a Base32 dictionary works, I mean, the equivalence between words and bits.

 

00000 - A         00001 - B         00010 - C         00011 - D
00100 - E         00101 - F         00110 - G         00111 - H
01000 - I         01001 - J         01010 - K         01011 - L
01100 - M         01101 - N         01110 - O         01111 - P
10000 - Q         10001 - R         10010 - S         10011 - T
10100 - U         10101 - V         10110 - W         10111 - X
11000 - Y         11001 - Z         11010 - 2         11011 - 3
11100 - 4         11101 - 5         11110 - 6         11111 - 7
0000  - A===      0001  - C===      0010  - E===      0011  - G===
0100  - I===      0101  - K===      0110  - M===      0111  - O===
1000  - Q===      1001  - S===      1010  - U===      1011  - W===
1100  - Y===      1101  - 2===      1110  - 4===      1111  - 6===
000   - A======   001   - E======   010   - I======   011   - M======
100   - Q======   101   - U======   110   - Y======   111   - 4======
00    - A=        01    - I=        10    - Q=        11    - Y=
0     - A====     1     - Q====

 

REF - https://datatracker.ietf.org/doc/html/rfc4648#page-8

If you are interested, there are other iRules to generate base32 codes. Here are some examples:
https://community.f5.com/t5/crowdsrc/tcl-procedures-for-base32-encoding-decoding/ta-p/286602
https://community.f5.com/t5/technical-articles/base32-encoding-and-decoding-with-irules/ta-p/277299

How a secret is distributed?

Most of the time, the secret is distributed using QR codes, because it’s an easy way to distribute it to dummy users.

Google Authenticator and any other vendors use this scheme:

 

# EXAMPLE:
otpauth://totp/ACME:john@acme.com?secret=INRSWZJUIVJTS6KK

## WHERE:
ACME                                                          - Company
john@acme.com                                                 - User Account
secret=INRSWZJUIVJTS6KK                                       - Secret

 

REF - https://github.com/google/google-authenticator/wiki/Key-Uri-Format

So, the best plan is to inject this previous sentence into a QR code. Here is an example:
https://rootprojects.org/authenticator/

With the example above, is clear how a user can get the secret in their smartphone, but take into account that both entities (user and F5) have to know the secret in order to be able to perform those authentications. Later on, we will show you some tips to store the key from the F5 perspective.

How a secret is validated?

When both (the user and the F5) know the secret, they can authenticate using a TOTP.

Next below, we are showing the steps required to generate a Time-based code from the secret.

 

# We start knowing the secret (base32)
set secret_b32     "INRSWZJUIVJTS6KK"
# OUTPUT: INRSWZJUIVJTS6KK

# Decode the secret from a b32 code (translating to a 10 digits secret)
set secret_raw     [call b32decode $secret_b32]
# OUTPUT: Cc+e4ES9yJ

# ----------------------------------
# There are other ways to decode b32, here is another example
set secret_b32     "INRSWZJUIVJTS6KK"
# OUTPUT: INRSWZJUIVJTS6KK
set secret_binary  [string map -nocase $static::b32_to_binary $secret_b32]
# OUTPUT: 01000011 01100011 00101011 01100101 00110100 01000101 01010011 00111001 01111001 01001010
set secret_raw     [binary format B80 $secret_binary]
# OUTPUT: Cc+e4ES9yJ
# ----------------------------------

# Get a UNIX timestamp and divide it by 30 (to get gaps of 30 seconds)
set clock          [expr { [clock seconds] / 30 } ]
# OUTPUT: 53704892

# Translate the previous code into binary
set clock_raw      [binary format W* $clock]]
# OUTPUT: 00000000 00000000 00000000 00000000 00000011 00110011 01111000 10111100

# Sign the clock value using the secret value, which means "HMAC-SHA1[secret,clock]"
set hmac_raw       [CRYPTO::sign -alg hmac-sha1 -key $secret_raw $clock_raw]
# OUTPUT: Ùòbàc¹´Í¬{�ü�s)�3

# Translate the previous code to hexadecimal
binary scan        $hmac_raw H* hmac
# OUTPUT: 1cd9f262e063b9b4cd13ac7b8dfc8a7329801733

# Take the last digit of this hexadecimal code ("3" in this case)
set last_char      [string index $hmac end]
# OUTPUT: 3

# Multiply the last value by 2 to generate a range of 16 possible 4-bytes words, as it's shown below
# Note that the last two digits are always ignored
set offset         [expr { "0x$last_char" * 2 } ]
# OUTPUT: 6

# Example:
# 0:   1cd9f262 e063b9b4cd13ac7b8dfc8a7329801733
# 1:  1c d9f262e0 63b9b4cd13ac7b8dfc8a7329801733
# 2:  1cd9 f262e063 b9b4cd13ac7b8dfc8a7329801733
# 3: [1cd9f2 62e063b9 b4cd13ac7b8dfc8a7329801733] <- This word is selected (last digit = '3')
# 4:  1cd9f262 e063b9b4 cd13ac7b8dfc8a7329801733
# 5:  1cd9f262e0 63b9b4cd 13ac7b8dfc8a7329801733
# 6:  1cd9f262e063 b9b4cd13 ac7b8dfc8a7329801733
# 7:  1cd9f262e063b9 b4cd13ac 7b8dfc8a7329801733
# 8:  1cd9f262e063b9b4 cd13ac7b 8dfc8a7329801733
# 9:  1cd9f262e063b9b4cd 13ac7b8d fc8a7329801733
# a:  1cd9f262e063b9b4cd13 ac7b8dfc 8a7329801733
# b:  1cd9f262e063b9b4cd13ac 7b8dfc8a 7329801733
# c:  1cd9f262e063b9b4cd13ac7b 8dfc8a73 29801733
# d:  1cd9f262e063b9b4cd13ac7b8d fc8a7329 801733
# e:  1cd9f262e063b9b4cd13ac7b8dfc 8a732980 1733
# f:  1cd9f262e063b9b4cd13ac7b8dfc8a 73298017 33

# Get the word from the table based on the last digit (see example above)
set word           [string range $hmac $offset [expr { $offset + 7 } ]]
# OUTPUT: 62e063b9

# Translate the previous code to base10 (removing negative values)
set us_word        [expr { "0x$word" & 0x7FFFFFFF } ]
# OUTPUT: 1658872761 (62e063b9)

# Apply a modulus 1000000 to get a 6-digits range number [000000 - 999999]
set token          [format %06d [expr { $us_word % 1000000 } ]]
# OUTPUT: 872761

# The previous value is the token that the user should use during authentication
# This value is changing every 30 seconds.

 

There are many iRules you can use to validate your user input codes. Here are some examples:
https://community.f5.com/t5/crowdsrc/google-authenticator-verification-irule-tmos-v11-1-optimized/ta-p/286672
https://community.f5.com/t5/crowdsrc/apm-google-authenticator-http-api/ta-p/287952
https://community.f5.com/t5/crowdsrc/google-authenticator-token-verification-irule-for-apm/ta-p/277510

How a secret is stored?

At this point, the user knows their secret (they already got their QR code with the secret), but the F5 still doesn't know how to get the secret to check if the TOTP provided by the user is correct.

There are many ways:

  • Store a key pair of "user-secret" in a data group.

It is really simple to implement, but not secure in a production environment because the secrets are stored in cleartext.

  • Store a key pair of "user-encrypted(secret)" in a data group.

That solves the problem of storing the secrets in cleartext, but it’s not scalable.

  • As Stan_PIRON_F5 pointed out here. There is a way to store those secrets in AD fields in an encrypted way that could suit a production environment.

Here below, we describe those steps, using Powershell scripts that should be running on the Windows Server where the AD resides.

1. Generate a symmetric key to encrypt the secrets.

 

function Create-AesKey($KeySize) {
    $AesManaged = New-Object "System.Security.Cryptography.AesManaged"
    $AesManaged.KeySize = $KeySize
    $AesManaged.GenerateKey()
    [System.Convert]::ToBase64String($AesManaged.Key)
}

$size= $Args[0]
$key = Create-AesKey $size

Write-Output $key

 

Input:

.\CreateKey.ps1 256

Output:

pnnqLfua6Mk/Oh3xqWV/6NTLd0r0aYaO4je3irwDbng=

2. Store each user secret in the ‘pager’ field of the AD.

 

function Encrypt-Data($AesKey, $Data) {
    $Data = [System.Text.Encoding]::UTF8.GetBytes($Data)
    $AesManaged = New-Object "System.Security.Cryptography.AesManaged"
    $AesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $AesManaged.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
    $AesManaged.BlockSize = 128
    $AesManaged.KeySize = 256
    $AesManaged.Key = [System.Convert]::FromBase64String($AesKey)
    $Encryptor = $AesManaged.CreateEncryptor()
    $EncryptedData = $Encryptor.TransformFinalBlock($Data, 0, $Data.Length);
    [byte[]] $EncryptedData = $AesManaged.IV + $EncryptedData
    $AesManaged.Dispose()
    [System.Convert]::ToBase64String($EncryptedData)
}
$username = $Args[0]

$encryptKey = "pnnqLfua6Mk/Oh3xqWV/6NTLd0r0aYaO4je3irwDbng="
[String]$userkey = ""
1..16 | % { $userkey += $(Get-Random -InputObject A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,2,3,4,5,6,7) }

$encrypted = Encrypt-Data $encryptKey $userkey
Write-Output "Key: $userkey ; Encrypted: $encrypted"
Set-AdUser -Identity $username -replace @{"pager"="$encrypted"}

 

Input:

.\EncryptData.ps1 myuser INRSWZJUIVJTS6KK

Output:

Key: INRSWZJUIVJTS6KK Encrypted: i6GoODygXJ05vG2xWcatNjrl1NubA1xHEZpMTzOlsdx52oeEp1a4891CdM5/aCMg

3. Validate that the secret was stored correctly

 

function Decrypt-Data($AesKey, $Data) {
    $Data = [System.Convert]::FromBase64String($Data)
    $AesManaged = New-Object "System.Security.Cryptography.AesManaged"
    $AesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $AesManaged.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
    $AesManaged.BlockSize = 128
    $AesManaged.KeySize = 256
    $AesManaged.IV = $Data[0..15]
    $AesManaged.Key = [System.Convert]::FromBase64String($AesKey)
    $Decryptor = $AesManaged.CreateDecryptor();
    $DecryptedData = $Decryptor.TransformFinalBlock($Data, 16, $Data.Length - 16);
    $aesManaged.Dispose()
    [System.Text.Encoding]::UTF8.GetString($DecryptedData)
}
$encryptKey = "pnnqLfua6Mk/Oh3xqWV/6NTLd0r0aYaO4je3irwDbng="
$userkey = $Args[0]

$decrypted = Decrypt-Data $encryptKey $userkey
Write-Output "Key: $decrypted"

 

Input:

.\DecryptData.ps1 i6GoODygXJ05vG2xWcatNjrl1NubA1xHEZpMTzOlsdx52oeEp1a4891CdM5/aCMg

Output:

INRSWZJUIVJTS6KK

How to generate a QR code?

There are many ways to generate a QR code from a secret word.

1. Google has an API to generate QR codes, still works but it’s in a deprecated state.
REF - https://developers.google.com/chart/infographics/docs/qr_codes

 

## EXAMPLE:
https://chart.googleapis.com/chart?cht=qr&chs=200x200&chld=M|0&chl=otpauth://totp/myser@mydomain.com?secret=AAAAAAAAAAAAAAAA

## WHERE:
cht=qr                                                        - QR Code
chs=200x200                                                   - Sizing
chld=M|0                                                      - Redundancy 'M' and Margin '0'
chl=otpauth://totp/myser@mydomain.com?secret=AAAAAAAAAAAAAAAA - Message

 

2. Similar to Google, there are other APIs to generate those QR codes, but like with the previous API from Google, using them is a wrong decision because you are sending your secret to an external entity.
REF - https://quickchart.io/documentation/#qr

 

## EXAMPLE
https://quickchart.io/qr?size=200&ecLevel=M&margin=1&text=otpauth://totp/myser@mydomain.com?secret=AAAAAAAAAAAAAAAA

## WHERE:
size=200                                                       - Sizing
ecLevel=M                                                      - Redundancy 'M'
margin=1                                                       - Margin '1'
text=otpauth://totp/myser@mydomain.com?secret=AAAAAAAAAAAAAAAA - Message

 

3. The best way to implement this in a production environment is to configure a dedicated server to generate those QR codes. There are many options on the internet, here is an example:
REF - https://github.com/edent/QR-Generator-PHP

Requirements:

yum install php php-mysql php-fpm
yum install php-gd
Updated Jun 30, 2022
Version 5.0
  • Regarding the last section "How to generate a QR code?", Stan_PIRON_F5 shared a new approach that I show you below.

    Taking into account that our user secret is stored in our Active Directory, one idea is to configure a new VS with an access policy (APM). This access policy could get the encrypted secret from the AD using an AD Query. After that, it would only be necessary to configure one iRule to decrypt that secret and present a web page with the QR Code (using javascript). Here is one example:

     

    when RULE_INIT {
    	# Symmetric Key to decrypt the User Secrets
        set static::b64key "pnnqLfua6Mk/Oh3xqWV/6NTLd0r0aYaO4je3irwDbng="
        set static::KEY [b64decode $static::b64key]
    }
    
    when ACCESS_ACL_ALLOWED {
    	# Get domain and user from logon page
    	set DOMAIN [ACCESS::session data get "session.logon.last.domain"]
    	set USER_NAME [ACCESS::session data get "session.logon.last.username"]
    	# Get the encrypted secret from the AD query
    	if {[set aes_cipherstring [b64decode [ACCESS::session data get session.ad.last.attr.pager]]] equals ""} {return}
    	# Split IV/CipherText
    	binary scan $aes_cipherstring a16a* aes_iv aes_ciphertext
    	# Decrypt User Secret
    	set USER_KEY [CRYPTO::decrypt -alg aes-256-cbc -key $static::KEY -iv $aes_iv $aes_ciphertext]
    	# Reply to the user with an HTML page with the QRCode
    	ACCESS::respond 200 content "<html>
    	<body>
    	<h1>Your TOTP QR Code is: </h1>
    	<input id=\"label\" type=\"hidden\" value=\"$DOMAIN\" />
    	<input id=\"user\" type=\"hidden\" value=\"$USER_NAME\" />
    	<input id=\"key\" type=\"hidden\" value=\"$USER_KEY\" />
    	<input id=\"digits\" type=\"hidden\" value=\"\" />
    	<input id=\"period\" type=\"hidden\" value=\"\" />
    	<div id=\"url\"></div>
    	<div id=\"qrcode\"></div>
    	<script type=\"text/javascript\" src=\"https://dan.hersam.com/tools/js/qrcodejs/qrcode.min.js\"></script>
    	<script type=\"text/javascript\" src=\"https://dan.hersam.com/tools/js/gen-qr-code.js\"></script>
    	</body>
    	</html>"
    }