User Rating: 5 / 5

Star ActiveStar ActiveStar ActiveStar ActiveStar Active
 

Two factor authentication is a way of increasing the security of an application by requiring the user to provide more than a simple password (one factor authentication).  Two factor authentication utilizes two of the following factors to identify the user:

1.  Knowledge - something you know - for example, your password

2.  Possession - something you have - for example, your cell phone or access to your email account

3.  Inherent - something you are - for example, fingerprints or eye iris

The third factor is out of scope for this particular article We're going to look at adding the second form (possession) to a PowerBuilder application. Specifically, we're What going to use Google Authenticator, an application for mobile devices (and the desktop) that generates time based one time temporary passwords (TOTP) for use with 2FA.

The sample code for this article is available on CodeXchange.

 

 

High level overview

When an application is developed to work with Google Authenticator (or similar services such as Authy), it generates a QR code for the user which the user then scans using the 2FA application.  The QR code contains:

  • The name of the application (for display in the 2FA app)
  • The user id for the user (for display in the 2FA app)
  • A secret key (unique for the user and is used in the generation of the TOTP).

The figure below shows the QR code being generated by the sample code for this article using the application name, user id and secret code entered into the form.  Your application would not show the secret key to the user, it's only shown in the sample app so you can try different values.

 

The user then scans the QR code into the Google Authenticator app.  At that point, Google Authenticator starts generating 6 digit OTTP codes every 30 seconds.

When you want to use the application, you enter:

  • Your username
  • Your password
  • The PIN generated by Google Authenticator

Only if the username/password combination is correct and the PIN is correct does the application allow login

Mid Level Overview

The QR code is generated by the following:

  • Take the secret code for the user:
    • 12345678901234567890
  • Base32 encode the value (not Base64):
    • GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
  • Create an identifier by concatenating the name of the application and the user id with a colon between them:
    • GoogleAuthenticatorDemo:me@brucearmstrong.org
  • Insert them into the following:
    • ‘otpauth://totp/’ + identifier + ‘?secret=‘ + encodedkey
    • otpauth://totp/GoogleAuthenticatorDemo:me@brucearmstrong.org?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
  • URLEncode the string:
    • otpauth%3A%2F%2Ftotp%2FGoogleAuthenticatorDemo%3Ame%40brucearmstrong.org%3Fsecret%3DGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
  • Generate a QR code for the string by calling a Google charting API
    • https://www.google.com/chart?&cht=qr&chs={size}&chl={code}
    • https://www.google.com/chart?chs=200x200&cht=qr&chl=otpauth%3A%2F%2Ftotp%2FGoogleAuthenticatorDemo%3Ame%40brucearmstrong.org%3Fsecret%3DGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ

To validate the PIN:

The application uses the same key and algorithm to also generate the PIN.  If the PIN entered by the user matches the PIN generated by the app, it is valid.  Since the calculated PIN is time based, the value the app generates could be different if the computer time is off.  The 30 second interval between PIN regenerations means the computer could be off by 30 seconds and still generate the same value.  You may want to generate a PIN for the 30 second interval before and after the current one, which would allow for a one minute difference in computer times.

To generate the PIN:

  • Get the current UTC (GMT) time expressed as a Unix Epoch (number of seconds since midnight January 1, 1970)
    • 1,556,697,600 is May 1, 2019 at 8:00 AM
  • Divide the value by 30 (seconds) to get the current interval
    • 1,556,697,600 / 30 = 51,889,920‬
  • Convert that into a 4 byte array (the Windows API function I used to do this returns the values in Little Endian format)
    • [0] [199] [23] [3]
  • Copy that inverted (Big Endian format) into an 8 byte array with 4 leading 0 bytes
    • [0] [0] [0] [0] [3] [23] [199] [0]
  • HMAC encode the byte array using SHA1 and the secret key for the user (12345678901234567890)
    • [52] [47] [143] [246] [134] [207] [78] [148] [217] [39] [227] [252] [85] [204] [53] [199] [117] [230] [20] [175]
  • Take the last byte and do a bitwise AND with F
    • 174 &F = 15
  • That determines the offset at which we copy 4 bytes from the encoded byte array
    • [199] [117] [230] [20]
  • Take the first byte and do a bitwise AND with 7F
    • 119 &7F = 71‬
  • Multiply each of the bytes by the following (shifting bytes left):
    • First bit: 2^24
      • 71 * 2*24 = 1,191,182,336
    • Second bit: 2^16
      • 117 * 2^16 = 7,667,712
    • Third bit: 2^8
      • 230 * 2^8 = 58,880
    • Fourth bit: 1
      • 20
    • Add the values together:
      • 1,191,182,336 + 7,667,712 + 58,880 + 20 = 1,198,908,948
  • Determine the Mod of the value and 1,000,000
    • Mod (1198908948, 1000000 ) = 908948
  • If the value is less that 6 characters long, pad it with zeros on the left to make 6 characters
    • 908948

Low Level Overview

Generating the QR image:

This calls the charting API to get the QR code as a PNG file.  We download that as a blob, and then assign that blob to the picture control in the window.

blob lblb_qrcode, lblb_provision_url, lblb_encoded_url
string ls_keystring, ls_provision_url, ls_chart_url, ls_response
u_base32 lnv_base32
CoderObject co
HTTPClient client

co = create CoderObject
client = create HTTPClient

ls_keystring = lnv_base32.of_encode( key )
ls_provision_url = 'otpauth://totp/' + identifier + '?secret=' + ls_keystring
lblb_provision_url = Blob ( ls_provision_url, EncodingUTF8! )
ls_provision_url = co.urlencode( lblb_provision_url ) 

ls_chart_url = 'https://chart.apis.google.com/chart?cht=qr&chs=' + String ( width ) + 'x' + String ( height ) + '&chl=' + ls_provision_url 

client.sendrequest( 'GET', ls_chart_url, ls_response )
client.getresponsebody( lblb_qrcode ) 

Destroy co
Destroy client

Return lblb_qrcode

Base32 Encoding the key:

Base32 uses a character set composed of the capital letters A to Z and the numbers 2 through 7.  Each character in the input string is converted to an 8 digit binary value.  The resulting value is then broken into 5 character chucks.  Each chunk is then converted back to a decimal value and the corresponding value pulled from the 32 character set.

constant string base32alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
integer li_index, li_count, li_asc, li_pos, li_pad, li_mod
string ls_return = '', ls_char, ls_binary = ''

li_count = Len ( a_data )
FOR li_index = 1 TO li_count
  ls_char = Mid ( a_data, li_index, 1 )
  li_asc = Asc ( ls_char )
  ls_binary += of_num_to_bin ( li_asc, 8 )
NEXT

//Pad the result if necessary to get a multiple of 5 bytes
li_count = Len ( ls_binary )
li_mod = Mod ( li_count, 5 )
IF li_mod > 0 THEN
  li_pad = 5 - li_mod
  ls_binary += Fill ( '0', li_pad )
END IF

//Get the new size and then step through in 5 byte chunks
li_count = Len ( ls_binary )
FOR li_index = 1 To li_count STEP 5
  ls_char = Mid ( ls_binary, li_index, 5 )
  li_pos = of_bin_to_num ( ls_char ) + 1
  ls_return += Mid ( base32alphabet, li_pos, 1 )
NEXT

//Add padding
CHOOSE CASE li_pad
  CASE 1
    ls_return += '==='
  CASE 2
    ls_return += '======' 
  CASE 3
    ls_return += '=' 
  CASE 4
    ls_return += '===='
  CASE ELSE
    //
END CHOOSE

Return ls_return

of_num_to_bin

This function, copied from PFC, is called from the Base32 encoding routine to convert a decimal number to a binary string.

integer  li_len
integer  li_bit
long  ll_num
string  ls_binary
string   ls_padding

ll_num = a_num
DO WHILE ll_num > 0
  li_bit = Mod ( ll_num, 2 )
  ls_binary = String ( li_bit ) + ls_binary
  ll_num = ll_num / 2
LOOP

li_len = Len ( ls_binary )
ls_padding = Fill ( '0', a_bits - li_len )
ls_binary = ls_padding + ls_binary

RETURN ls_binary

of_bin_to_num

This function, copied from PFC, is called form the Base32 encoding routine to convert a binary string to a decimal value.

Char      lc_digit[]
Integer   li_index, li_count
Long     ll_factor = 1, ll_decimal = 0

lc_digit = as_binary
li_count = Len ( as_binary )
For li_index = li_count To 1 Step -1
  If lc_digit[li_index] = '1' Then ll_decimal += ll_factor
  ll_factor *= 2
Next

Return ll_decimal

GeneratePIN

This is the main routine used to generate the 6 digit TOTP.  It gets the current time in Unix Epoch format, divides by 30 to get the 30 second interval and then generates the TOTP using the secret key for the user. It's an overloaded method that then calls the other function with the same name but additional arguments (see below).

long  TotalSeconds, CurrentInterval, IntervalLength = 30

TotalSeconds = getunixepoch()
CurrentInterval = TotalSeconds/IntervalLength

return generatepin ( key, CurrentInterval )

GenerateUnixEpoch

This routine is used to get the current time in Unix Epoch format (number of seconds since January 1, 1970 GMT).  A Windows API function is used to get the time zone for the application so that the PowerBuilder generated current date/time can be converted to GMT.  Another WIndows API function is used to do the time zone adjustment.  After that, the amount of time since January 1, 1970 is then calculated in seconds.

long  days
long  seconds
long   epoch
ulong   rc1
Boolean    rc2
TIME_ZONE_INFORMATION  tzi
SYSTEMTIME   local
SYSTEMTIME   utc
datetime  l_datetime_utc

rc1 = GetTimeZoneInformation(tzi)

//Convert PowerBuilder datetime to SYSTEMTIME
local.wYear = Year(Today())
local.wMonth = Month(Today())
local.wDay = Day(Today())
local.wHour = Hour(Now())
local.wMinute = Minute(Now())
local.wSecond = Second(Now())
local.wMilliseconds = 0

//Get the time in UTC
rc2 = TzSpecificLocalTimeToSystemTime(tzi, local, utc)

//Convert the UTC SYSTEMTIME back to a PowerBuilder datetime
l_datetime_utc = DateTime ( Date ( utc.wYear, utc.wMonth, utc.wDay ), Time ( utc.wHour, utc.wMinute, utc.wSecond ) )
days = DaysAfter ( Date ( '1970/01/01' ), Date ( l_datetime_utc ) )
seconds = SecondsAfter ( Time ( '00:00:00' ), Time ( l_datetime_utc ) )
epoch = ( days * 24 * 60 * 60 ) + seconds

Return epoch

GeneratePIN

This is the routine that actually generates the TOTP.  The current interval is converted into a blob so it can then be hashed using the user's secret key.  The resulting value is converted to a byte array.  The last byte is then used as a pointer to a value in the array.  Four bytes are copied from that point.  The first value is stripped of it's most significant bit, the values are then multiplied by different amounts based on their position and the results added together.  The TOTP is then determined by taking the mod of that and 1.000,000.

byte  hashBytes[], counterBytes[], offset, selectedByte
blob  counterHashBlob, counterBlob, keyBlob
string  counterHash, pin
integer  li_count, i, pinLen
long  selectedLong
long  selectedMod
CrypterObject   co

//Convert the counter to byte array and then to blob
longtobytearray ( counter, counterBytes )
counterBlob = Blob ( counterBytes )


//Convert key to blob and hash counter
keyBlob= Blob ( key, EncodingUTF8! )
co = create CrypterObject
counterHashBlob = co.Hmac( HMACSHA1! , counterBlob, keyBlob )
Destroy co

//Convert result back to byte array
hashBytes = GetByteArray ( counterHashBlob )

//Get the last byte
li_count = UpperBound ( hashBytes )

//And use that to determine the offset into the byte array that we'll start with
offset = bitwiseand ( hashBytes[li_count], 15 )

//Calculate the selectedLong value using the selected bytes
for i = 1 to 4
  selectedByte =   hashBytes[offset + i]
  CHOOSE CASE i
    CASE 1
      //Strip the most significant bit
      selectedByte = bitwiseand ( selectedByte, 127 ) 
      selectedLong = selectedLong + selectedByte * 2^24
    CASE 2
      selectedLong = selectedLong + selectedByte * 2^16
    CASE 3
      selectedLong = selectedLong + selectedByte * 2^8
    CASE 4
      selectedLong = selectedLong + selectedByte
  END CHOOSE
next

selectedMod = Mod ( selectedLong, 1000000 )
pin = String ( selectedMod )
pinLen = Len ( pin )
pin = Fill ( '0', 6 - pinLen ) + pin

Return pin

longtobytearray

This function is used by the GeneratePIN method to convert a long value to a byte array.  The Windows API function used created the result in Little Endian format, which we then invert to get the Big Endian format required to the TOTP generation.

byte  l_data[4]
long  l_len = 4
integer  i, j

CopyLongToBytes ( l_data, al_data, l_len )

//pad with 4 zeros
for i = 1 To 4
  j = UpperBound(a_data) +1
  a_data[j] = 0
next

//Copy over data in inverse order
for i = 4 to 1 STEP -1
  j = UpperBound (a_data) + 1
  a_data[j] = l_data[i]
next

Return 1

bitwiseand

This function, copied from PFC, is used by the GeneratePIN method to do bitwise AND.

Integer li_i
Byte     lbyte_result, lbyte_factor
lbyte_result = 0

For li_i = 1 To 8
  If a_value1 = 0 Or a_value2 = 0 Then Exit
  If li_i = 1 Then
    lbyte_factor = 1
  Else
    lbyte_factor *= 2
  End If

  If Mod(a_value1, 2) = 1 And Mod(a_value2, 2) = 1 Then
    lbyte_result += lbyte_factor
  End If

  a_value1 /= 2
  a_value2 /= 2

Next

Return lbyte_result 

Local External Function

This is the declaration for the Windows API function used to convert a long value to a byte array

subroutine CopyLongToBytes ( ref byte dest[4], ref long source, long length ) Library "kernel32.dll" Alias For RtlMoveMemory

This is the declaration for the Windows API functions used to handle time zone manipulation  

Function ulong GetTimeZoneInformation (ref TIME_ZONE_INFORMATION lpTimeZoneInformation) Library "kernel32"

Function boolean TzSpecificLocalTimeToSystemTime(TIME_ZONE_INFORMATION lpTimeZone, SYSTEMTIME lpLocalTime, ref SYSTEMTIME lpUniversalTime) Library "kernel32"

Structures

This is the declaration for the SYSTEMTIME structure used in the TzSpecificLocalTimeToSystemTime Windows API function.  It's also used as part of the TIME_ZONE_INFORMATION structure below:

global type systemtime from structure
  integer wYear
  integer wMonth
  integer wDayofWeek
  integer wDay
  integer wHour
  integer wMinute
  integer wSecond
  integer wMilliseconds
end type

This is the declaration for the TIME_ZONE_INFORMATION structure used in the GetTimeZoneInformation Windows API call:

global type time_zone_information from structure
  long bias
  integer standardname[31]
  systemtime standarddate
  long standardbias
  integer daylightname[31]
  systemtime daylighttime
  long daylightbias
end type

 

 

Comments (1)

  1. Berit Sandvik

Thank you very much for sharing this article. It has saved us a lot of work.

 
There are no comments posted here yet