RADAuthenticator Part 2 - Generate one time password tokens in Delphi using TOTP

Updated: Nov 1, 2021

In part one of this blog post series on an upcoming multi-platform RADAuthenticator Delphi app, we went over base32 encoding which is used for managing the secret key in Google Authenticator compatible one-time password apps (typically emedded within a QR Code link during setup.) In this blog post we will cover the heart of the process and that is to generate one-time use, dynamic time-based password tokens used by many applications for multi-factor authentication (MFA).

The Time-Based One-Time Password Algorithm (TOTP) is covered in RFC-6238 which relies on the HMAC-Based One-Time Password Algorithm (HOTP) as defined in RFC-4226.

TOTP Generation in Delphi

From the RFC

The output of the HMAC-SHA-1 calculation is truncated to obtain user-friendly values:

      HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))

where Truncate represents the function that can convert an HMAC-SHA-1 value into an HOTP value.  K and C represent the shared secret and counter value. TOTP is the time-based variant of this algorithm, where a value T, derived from a time reference and a time step, replaces the counter C in the HOTP computation.

The counter value used for TOTP is an ever-increasing time value passed to the HOTP password generator. Since the prover and verifier must always match input values this time value is expressed in UTC which bypasses any time-zone issues. The standard is to express this value in Unix time format and encoded as the number of seconds since midnight at the start of January 1, 1970. (This means that both sides need to have fairly accurate time, with the default verfication method allowing for 30 seconds of drift.)

Let us first look at the interface section of TOTP which is pretty minimal. There is an overloaded GeneratePassword function call which requires the Secret Key to be passed in and it returns a 6-digit result by default. (The routine allows you specify a 6, 7, or 8 digit response as needed.) There is also a protected function for retrieving the current timestamp to be used as a Counter Value for the HOTP generation.

The implementation section of TOTP is also short and easy to follow. We are simply forwarding the GeneratePassword call to the underlying THOTP routines with the GetCurrentUnixTimeStamp value as the Counter Value parameter.

HOTP Generation in Delphi

From the RFC

We can describe the operations in 3 distinct steps:

Step 1: Generate an HMAC-SHA-1 value Let HS = HMAC-SHA-1(K,C)  
Step 2: Generate a 4-byte string (Dynamic Truncation)
Step 3: Compute an HOTP value

The reason for masking the most significant bit of P is to avoid
confusion about signed vs. unsigned modulo computations.  Different
processors perform these operations differently, and masking out the
signed bit removes all ambiguity.

The following code example describes the extraction of a dynamic
binary code given that hmac_result is a byte array with the HMAC-
SHA-1 result:
     int offset   =  hmac_result[19] & 0xf ;
     int bin_code = (hmac_result[offset]  & 0x7f) << 24
        | (hmac_result[offset+1] & 0xff) << 16
        | (hmac_result[offset+2] & 0xff) <<  8
        | (hmac_result[offset+3] & 0xff) ;                          

The bulk of the code is in the HOTP class and we can see its interface section below. We define a custom Exception class which is thrown if the secret key is not long enough (which is defined as 'must be' 128 bits in the RFC) and we also allow for three different output lengths as specified in the RFC. There are two overloaded public GeneratePassword method calls taking the Secret Key and Counter Value as required inputs. There are a few private variables for use later in the code (ModTable allows for trimming of the Hash result based on output length desired, FormatTable is used for padding the string result based on desired length and a RFCMinimumKeyLengthBytes to specify the minimum length of the secret key.)

If you follow the industry standard convention of storing your Secret Keys as base32 encoded UTF-8 strings, use the first overloaded method and pass in the encoded password. Otherwise, pass the plain text Secret Key as an array of bytes using the second overloaded GeneratePassword method (keep in mind the rule that the prover and verifier must always match input values so be careful that client and server use the same string encoding.)

As you can see, the first overloaded method simply converts the base32 encoded Secret Key string into an array of bytes and calls the main GeneratePassword method.

We need to calculate the HMAC of the Counter Value and Secret Key (SHA-1 is the most common variant used today and other hash types may be added in the future.) Four bytes of this 160-bit digest value is used as the OTP result and the RFC dictates how to extract that data. (We define an offset based on the last byte of the hash digest and then extract 4-bytes from the hash starting at that offset value.) We then truncate value to the length requested (via a mod operation) and left-pad the integer result with zeros.

OTP Unit Tests

The RFCs provides test vectors to validate our custom implementation which can easily be implemented in two DUnit tests as shown below:

Next Steps

Combined with the base32 encoding from the first article in the series, we now have everything needed to implement one-time passwords in Delphi! This code will continue to be used in this blog series dedicated to building a custom RADAuthenticator app intended to replace the Google Authenticator app. Part 3 will focus on extending the testing platform.

For proof of concept purposes, there is a sample VCL app provided which takes a Secret Key for input and outputs the One Time Password value using the TOTP routines defined above.

The code is released as Open Source under the Apache-2.0 license and is found within the rad-authenticator repository under my RADProgrammer organization on GitHub.

I have setup a RADProgrammer chat space on Discord dedicated to RADProgrammer projects such as this. Here is an invitation link to join this Discord. See you online!

877 views0 comments