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 (24)

  1. Berit Sandvik

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

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Marco Meoni

that's a hell of an article Bruce, great job! And lots of techs to learn.

  Attachments
Your account does not have privileges to view attachments in the comment
  Comment was last edited about 4 years ago by Marco Meoni Marco Meoni
  1. Rob Stevens

Hi Bruce
This has been great. However, I'm having issues when it is running in a browser after deploying via PowerServer. I put in some debug messages and found that the count below returns 0.

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

Note that the call to getunixepoch worked fine. Was wondering if Appeon has some issues with the CrypterObject?

Would you expect this to work in the Appeon browser environment?

Kind regards
Rob

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Bruce Armstrong

CryterObject is one of the unsupported objects in PowerServer:

https://docs.appeon.com/ps2020/features_help_for_appeon_mobile/unsupported_objects.html

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Rob Stevens

Thanks for your prompt response Bruce. I assume that list is true for the web browser as we are not deploying to mobiles.

Any ideas as a workaround in order to get this going in Appeon on a browser or should I contact them? Our client is requiring MFA so this is a bit of show stopper at this stage.

Regards
Rob

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Bruce Armstrong

Actually, CrypterObject is supported in web, including the HMAC function I'm calling:

https://docs.appeon.com/ps2020/features_help_for_appeon_web/Supported_Objects_for_web.html#CrypterObject

The GetByteArray function that is called just before the point where that is failing is also supposed to be supported. All I can suggest is checking before and after that function to ensure you're getting appropriate responses from the functions before it.

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Bruce Armstrong

Also, just to confirm, are you on PowerServer 2020?

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. George Mikhailovsky    Bruce Armstrong

I resolved my problem with 2FA in web applications reported few days ago by rewriting code of of_copylongtobytes() subroutine provided in your tutorial (see above) as follows:

of_copylongtobytes (byte a_byte, number al_number) returns none

long li_number
integer i = 1, j, li_byte[], li_ibyte[], li_data[]
string ls_msg = ''

li_number = al_number

//Populate li_byte array
Do While Truncate(li_number / 256, 0) > 0
li_byte[i] = Mod(li_number, 256)
i++
li_number = Truncate(li_number/256, 0)
Loop
li_byte[i] = Mod(li_number, 256)

//Copy over data into li_ibyte array in inverse order
For i = 4 To 1 STEP -1
j ++
li_ibyte[j] = li_byte[i]
Next

//Populate li_data array with zeros
For i = 1 To 4
li_data[i] = 0
Next

//Populate li_data array from li_ibyte one
For i = 5 To 8
li_data[i] = li_ibyte[i - 4]
Next

//Conversion li_data integer array to a_byte byte array passed by ref
For j = 1 to 8
a_byte[j] += Byte(String(li_data[j]))
Next

Now your generatepin() function that calls of_copylongtobytes() returns the pin that matches with the token displayed by DouMobile. Before, although it returns some pin with 6 numbers instead of '000000', this pin didn't match with DuoMobile token.

Thank you and Marco Meoni for all your help in resolving my problem,

George

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Rob Stevens

Hi
Yes we are using PowerServer 2020 and PowerBuilder 2019 R2. I will do some more testing.
Thanks

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Rob Stevens

Hi Bruce
I've done some more debugging.
Doesn't look like Appeon likes the call to CopyLongToBytes as while it returns an array of 4 bytes they are all zero.

I overrode that just to return a result in order to check if anything else isn't working and also found the below doesn't work:

counterBlob = Blob(counterBytes)

If I check the length of counterBlob, it returns 0.

I had a look around and found there are some APIs that you can call to validate a pin. For example:
https://www.authenticatorApi.com/Validate.aspx?Pin=123456&;SecretCode=12345678BXYT

Apologies but I'm not up to speed with being able to call this sort of thing from PowerBuilder. Is this possible?

Regards
Rob

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Rob Stevens

Hi
Further to this the team at Appeon have provided me with a workaround for the CopyLongToBytes and Blob functions. This now works great in the Appeon browser environment.
Regards
Rob

Workaround CopyLongToBytes():
---------------------------------------
public subroutine of_copylongtobytes (ref byte a_byte[], long al_number);
long li_number
integer i = 1, j, li_byte[]
string ls_msg = ''

li_number = al_number

Do While Truncate(li_number / 256, 0) > 0
li_byte[i] = Mod(li_number, 256)
i++
li_number = Truncate(li_number/256, 0)
Loop
li_byte[i] = Mod(li_number, 256)

For j = 1 to i
a_byte[j] += Byte(String(li_byte[j]))
Next
end subroutine

Workaround Blob()
---------------------------------------
public function blob of_blob (byte a_byte[]);Integer i
Blob lblb

lblb = Blob ( Space(UpperBound(a_byte)), EncodingUTF8! )
for i = 1 to UpperBound(a_byte)
SetByte(lblb, i, a_byte[i])
Next

Return lblb
end function

  Attachments
Your account does not have privileges to view attachments in the comment
 
There are no comments posted here yet
Load More