2024 Oct 19 2:16 PM - edited 2024 Oct 29 10:13 AM
I need to talk to a server using REST with HMAC authentication. Before dealing with the server, I thought I would first write and validate the HMAC part and... that's exactly where I am stuck: the values I calculate in SAP are different from those I get using online calculators.
Here is my utility class:
CLASS zcl_auth_util DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
CONSTANTS c_algo_sha256 TYPE string VALUE `SHA256`.
CLASS-METHODS build_key
IMPORTING if_password TYPE string
if_salt TYPE string
EXPORTING ef_key TYPE xstring.
CLASS-METHODS build_hmac_response
IMPORTING if_key TYPE xstring
if_iterations TYPE i
if_data TYPE string
EXPORTING ef_result TYPE string
ef_resultx TYPE xstring
ef_resultb TYPE string.
ENDCLASS.
CLASS ZCL_AUTH_UTIL IMPLEMENTATION.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_AUTH_UTIL=>BUILD_HMAC_RESPONSE
* +-------------------------------------------------------------------------------------------------+
* | [--->] IF_KEY TYPE XSTRING
* | [--->] IF_ITERATIONS TYPE I
* | [--->] IF_DATA TYPE STRING
* | [<---] EF_RESULT TYPE STRING
* | [<---] EF_RESULTX TYPE XSTRING
* | [<---] EF_RESULTB TYPE STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD build_hmac_response.
DATA lv_hmacxstring TYPE xstring.
DATA lv_hmacb64string TYPE string.
DATA(lv_data) = if_data.
DO if_iterations TIMES.
TRY.
cl_abap_hmac=>calculate_hmac_for_char( EXPORTING if_algorithm = c_algo_sha256
if_key = if_key
if_data = lv_data
IMPORTING ef_hmacstring = lv_data
ef_hmacxstring = lv_hmacxstring
ef_hmacb64string = lv_hmacb64string ).
CATCH cx_root INTO DATA(e_txt).
WRITE / e_txt->get_text( ).
ENDTRY.
ENDDO.
ef_result = lv_data.
ef_resultx = lv_hmacxstring.
ef_resultb = lv_hmacb64string.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_AUTH_UTIL=>BUILD_KEY
* +-------------------------------------------------------------------------------------------------+
* | [--->] IF_PASSWORD TYPE STRING
* | [--->] IF_SALT TYPE STRING
* | [<---] EF_KEY TYPE XSTRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD build_key.
DATA(lv_keyx) = cl_abap_hmac=>string_to_xstring( |{ if_password }{ if_salt }| ).
TRY.
DATA(lr_digest) = cl_abap_message_digest=>get_instance( if_algorithm = c_algo_sha256 ).
lr_digest->update( if_data = lv_keyx ).
lr_digest->digest( ).
ef_key = cl_abap_hmac=>string_to_xstring( lr_digest->to_string( ) ).
CATCH cx_abap_message_digest. "
ENDTRY.
ENDMETHOD.
ENDCLASS.
And this is the test class:
*"* use this source file for your ABAP unit test classes
CLASS ltc_auth_util_test DEFINITION FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.
PUBLIC SECTION.
METHODS _01_key_hash FOR TESTING.
METHODS _02_hmac_1_iter_no_sha FOR TESTING.
METHODS _03_hmac_1_iter_with_sha FOR TESTING.
PRIVATE SECTION.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS setup.
METHODS teardown.
CONSTANTS c_password TYPE string VALUE `this_is_a_test_password`.
CONSTANTS c_salt TYPE string VALUE `N1cbsxNB2X1uR0i20YmE9iV9L5icQtXQQ5V+7quww24VK32IStgnMhp/BIcgIoFH7FTUDaYHyZvsVrLRZRH4Nw==`.
CONSTANTS c_pwd_salt_sha256 TYPE string VALUE `55557945A262E5FB6C553CBA833D0F75A11D42B17EEF27C7288B2A6C8B7CB930`.
CONSTANTS c_testdata TYPE string VALUE `this is a test message to be signed`.
CONSTANTS c_hmac_of_pwd_salt_testdata TYPE string VALUE `5B78CD8ADA9CABC8A65AAF5B0AD8648DD021FF1C285BEA51A3DDD6CC2D9D7A07`.
DATA mo_cut TYPE REF TO zcl_auth_util.
ENDCLASS.
CLASS ltc_auth_util_test IMPLEMENTATION.
METHOD _01_key_hash.
DATA l_exp TYPE xstring.
DATA l_act TYPE xstring.
TRY.
l_exp = cl_abap_hmac=>string_to_xstring( if_input = c_pwd_salt_sha256 ).
CATCH cx_abap_message_digest. "
ENDTRY.
mo_cut->build_key(
EXPORTING if_password = c_password
if_salt = c_salt
IMPORTING ef_key = l_act ).
cl_abap_unit_assert=>assert_equals( exp = l_exp
act = l_act
msg = `Hashed key (pwd/salt) does not match expected value` ).
ENDMETHOD.
METHOD _02_hmac_1_iter_no_sha.
DATA l_act_string TYPE string.
TRY.
DATA(lv_keyx) = cl_abap_hmac=>string_to_xstring(
* This is the correct SHA256 of the password/salt from the first test
* if_input = c_pwd_salt_sha256
* but first we try with raw password+salt
if_input = |{ c_password }{ c_salt }| ).
CATCH cx_abap_message_digest. "
ENDTRY.
mo_cut->build_hmac_response( EXPORTING if_key = lv_keyx
if_iterations = 1
if_data = c_testdata
IMPORTING ef_result = l_act_string ).
cl_abap_unit_assert=>assert_differs( exp = c_hmac_of_pwd_salt_testdata
act = l_act_string
msg = `HMAC with RAW password/salt matches hash, that's odd!` ).
ENDMETHOD.
METHOD _03_hmac_1_iter_with_sha.
DATA l_act_string TYPE string.
TRY.
DATA(lv_keyx) = cl_abap_hmac=>string_to_xstring(
* This is the correct SHA256 of the password/salt from the first test
if_input = c_pwd_salt_sha256 ).
CATCH cx_abap_message_digest. "
ENDTRY.
mo_cut->build_hmac_response( EXPORTING if_key = lv_keyx
if_iterations = 1
if_data = c_testdata
IMPORTING ef_result = l_act_string ).
cl_abap_unit_assert=>assert_equals( exp = c_hmac_of_pwd_salt_testdata
act = l_act_string
msg = 'HMAC with SHA256 of password/salt does not match expected hash' ).
ENDMETHOD.
METHOD class_setup.
ENDMETHOD.
METHOD class_teardown.
ENDMETHOD.
METHOD setup.
mo_cut = NEW #( ).
ENDMETHOD.
METHOD teardown.
CLEAR mo_cut.
ENDMETHOD.
ENDCLASS.
Test 1 and 2 pass, test 3 fails. Actually, test 2 was an afterthought: maybe the HMAC class is already doing a SHA256 on the key and I do not have to do it again? But no, apparently that is not the case and I am still clueless as to what might be the issue here.
Regardless of whether the remote server is expecting the key to be the SHA256 of password+salt or just of the password, it would be nice if my calculated values matched what the online calculators say 🙂
Any ideas? The code above is complete and runs ok in the latest Docker image for sure (but even at the office using a relatively ancient release, for that matter)
Request clarification before answering.
Ok, at last I found out what was wrong:
METHOD build_hmac_response.
DATA lv_hmacstring TYPE string.
DATA lv_hmacb64string TYPE string.
DATA lv_hmacxstring TYPE xstring.
DATA(lv_datax) = cl_abap_hmac=>string_to_xstring( if_input = if_data ).
DO if_iterations TIMES.
TRY.
cl_abap_hmac=>calculate_hmac_for_raw( EXPORTING if_algorithm = c_algo_sha256
if_key = if_key
if_data = lv_datax
IMPORTING ef_hmacstring = lv_hmacstring
ef_hmacxstring = lv_hmacxstring
ef_hmacb64string = lv_hmacb64string ).
* Note: importing directly "lv_datax" clobbers the returns and will give the wrong result;
* to support the (yet untested) case of iterations > 1 just copy the string before looping!
lv_datax = lv_hmacxstring.
CATCH cx_abap_message_digest INTO DATA(e_txt).
WRITE / e_txt->get_text( ).
ENDTRY.
ENDDO.
ef_result = lv_hmacstring.
ef_resultx = lv_hmacxstring.
ef_resultb = lv_hmacb64string.
ENDMETHOD.
And corresponding test method:
METHOD _02_hmac_1_iter_no_sha.
DATA l_act_string TYPE string.
TRY.
* MEMO: we must NOT run SHA256 on the key, the HMAC method will take care of that!
DATA(lv_keyx) = cl_abap_hmac=>string_to_xstring( if_input = |{ c_password }{ c_salt }| ).
CATCH cx_abap_message_digest.
ENDTRY.
mo_cut->build_hmac_response( EXPORTING if_key = lv_keyx
if_iterations = 1
if_data = c_testdata
IMPORTING ef_result = l_act_string ).
cl_abap_unit_assert=>assert_equals( exp = c_hmac_of_pwd_salt_testdata
act = l_act_string
msg = `HMAC with RAW password/salt does not match hash!` ).
ENDMETHOD.
All is well 🙂
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
31 | |
10 | |
8 | |
7 | |
6 | |
6 | |
6 | |
5 | |
5 | |
4 |
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.