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
- First bit: 2^24
- 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_countLong 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 bytesfor 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 (24)