NevTon
NevTon

Reputation: 289

Delphi & CryptoAPI - how to calculate HMAC-SHA512 hash?

Does anybody know how to calculate a HMAC-SHA512 hash in Delphi 2010+ using MS CryptoAPI ?

The example from MS website, https://learn.microsoft.com/en-us/windows/win32/seccrypto/example-c-program--creating-an-hmac generates incorrect results.

I have found this answer https://stackoverflow.com/a/41387095/2111514 to be somehow usefull (because it is manual rewrite from https://en.wikipedia.org/wiki/HMAC), but it is not in Pascal and my attempt to refactor it to Pascal was without luck. It works, but still calculates wrong results.

Can anybody help me, please?

Edit:: This is my code that I have problem with:

uses
  Windows,
  JwaWinCrypt,
  JwaWinError;

const
  BLOCK_SIZE = 64;

type
  EHMACError = class(Exception);

function WinError(const RetVal: BOOL; const FuncName: String): BOOL;
var
  dwResult: Integer;
begin
  Result:=RetVal;
  if not RetVal then begin
    dwResult:=GetLastError();
    raise EHMACError.CreateFmt('Error [x%x]: %s failed.'#13#10'%s', [dwResult, FuncName, SysErrorMessage(dwResult)]);
  end;
end;

function TBytesToHex(const Value: TBytes): String;
const
  dictionary: Array[0..15] of Char = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f');
var
  i: Integer;
begin
  Result:='';
  for i:=0 to High(Value) do
    Result:=Result + dictionary[Value[i] shr 4] + dictionary[Value[i] and $0F];
end;

function hmac(AKey, AMessage: TBytes; Algid: ALG_ID): TBytes;

  function hash(const hProv: HCRYPTPROV; hData: TBytes): TBytes;
  var
    len, cb: DWORD;
    hHash: HCRYPTHASH;
  begin
    SetLength(Result, 0);
    WinError(CryptCreateHash(hProv, Algid, 0, 0, hHash), 'CryptCreateHash');
    try
      len:=Length(hData);
      cb:=SizeOf(len);
      WinError(CryptHashData(hHash, @hData[0], len, 0), 'CryptHashData');
      WinError(CryptGetHashParam(hHash, HP_HASHSIZE, @len, cb, 0), 'CryptGetHashParam(HP_HASHSIZE)');
      SetLength(Result, len);
      WinError(CryptGetHashParam(hHash, HP_HASHVAL, @Result[0], len, 0), 'CryptGetHashParam(HP_HASHVAL)');
    finally
      WinError(CryptDestroyHash(hHash), 'CryptDestroyHash');
    end;
  end;

  function double_hash(const hProv: HCRYPTPROV; hData1, hData2: TBytes): TBytes;
  var
    len, len1, len2, cb: DWORD;
    hHash: HCRYPTHASH;
  begin
    SetLength(Result, 0);
    WinError(CryptCreateHash(hProv, Algid, 0, 0, hHash), 'DH_CryptCreateHash');
    try
      len1:=Length(hData1);
      len2:=Length(hData2);
      cb:=SizeOf(DWORD);
      WinError(CryptHashData(hHash, @hData1[0], len1, 0), 'DH_CryptHashData(hData1)');
      WinError(CryptHashData(hHash, @hData2[0], len2, 0), 'DH_CryptHashData(hData1)');
      WinError(CryptGetHashParam(hHash, HP_HASHSIZE, @len, cb, 0), 'DH_CryptGetHashParam(HP_HASHSIZE)');
      SetLength(Result, len);
      WinError(CryptGetHashParam(hHash, HP_HASHVAL, @Result[0], len, 0), 'DH_CryptGetHashParam(HP_HASHVAL)');
    finally
      WinError(CryptDestroyHash(hHash), 'DH_CryptDestroyHash');
    end;
  end;

var
  hProv: HCRYPTPROV;
  hHash: HCRYPTHASH;
  i_key_pad, o_key_pad: TBytes;
  data, ret: TBytes;
  len, i: Integer;
  c: Byte;
  ifree: Boolean;
begin
  ifree:=False;
  SetLength(Result, 0);
  SetLength(i_key_pad, BLOCK_SIZE);
  SetLength(o_key_pad, BLOCK_SIZE);
  WinError(CryptAcquireContext(hProv, Nil, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_VERIFYCONTEXT), 'CryptAcquireContext');
  try
    data:=AKey;
    len:=Length(data);
    if len > BLOCK_SIZE then begin
      data:=hash(hProv, data);
      ifree:=True;
    end;
    //
    i:=BLOCK_SIZE-1;
    while i >= 0 do begin
      if i < len then
        c:=data[i]
      else
        c:=0;
      i_key_pad[i]:=$36 xor c;
      o_key_pad[i]:=$5c xor c;
      Dec(i);
    end;
    data:=double_hash(hProv, i_key_pad, AMessage);
    Result:=double_hash(hProv, o_key_pad, data);
    SetLength(data, 0);
  finally
    if ifree then
      SetLength(data, 0);
    SetLength(i_key_pad, 0);
    SetLength(o_key_pad, 0);
    WinError(CryptReleaseContext(hProv, 0), 'CryptReleaseContext');
  end;
end;

...and it is called by:

Result:=hmac(Password, InString, CALG_SHA_512);

Example:

TBytesToHex(hmac('pass', 'test', CALG_SHA_512)); produces (HEX encoded)

1319bb7baefc3fbaf07824261c240cecd04a54cd83cdf0deb68e56cadff20e7c644e2e956660ab9df47a19502173090df5ec3d0b9236d59917afc4f3607cf980

whereas online HMAC calculator produces

46beca277a5fec10beba65b0c2fb3917115f352eb8b2560e9ada0a3dbafb6c7a3fc456b1e13a07c4a9c856b633b70b2403907ca89894021772393e3f97e78684

for the same input

Upvotes: 1

Views: 1897

Answers (1)

NevTon
NevTon

Reputation: 289

The whole working solution to my question, thanks to @whosrdaddy for helping.

//
// HMAC-SHA512 - cryptoapi hash generation
//
// based on:
//   https://en.wikipedia.org/wiki/HMAC
//   https://github.com/ogay/hmac
//
// refactored from:
//   https://stackoverflow.com/questions/41384395/wrong-result-for-base64-string-of-hmac-sha1-using-crypto-api/41387095#41387095
//
unit CryptoAPI_HMAC_SHA512;

interface

uses
  SysUtils,
  Classes;

function CryptoAPI_Hash_HmacSHA512(const InString, Password: TBytes): TBytes; overload;
function CryptoAPI_Hash_HmacSHA512(const InString, Password: String): String; overload;

implementation

uses
  Windows,
  JwaWinCrypt,
  JwaWinError;

const
  BLOCK_SIZE  = 128; // bytes for SHA512

type
  EHMACError = class(Exception);

function WinError(const RetVal: BOOL; const FuncName: String): BOOL;
var
  dwResult: Integer;
begin
  Result:=RetVal;
  if not RetVal then begin
    dwResult:=GetLastError();
    raise EHMACError.CreateFmt('Error [x%x]: %s failed.'#13#10'%s', [dwResult, FuncName, SysErrorMessage(dwResult)]);
  end;
end;

function hmac(AKey, AMessage: TBytes; Algid: ALG_ID): TBytes;

  function hash(const hProv: HCRYPTPROV; hData1, hData2: TBytes): TBytes;
  var
    len, len1, len2, cb: DWORD;
    hHash: HCRYPTHASH;
  begin
    SetLength(Result, 0);
    WinError(CryptCreateHash(hProv, Algid, 0, 0, hHash), 'CryptCreateHash');
    try
      len:=0;
      len1:=Length(hData1);
      len2:=Length(hData2);
      cb:=SizeOf(DWORD);
      WinError(CryptHashData(hHash, @hData1[0], len1, 0), 'CryptHashData(hData1)');
      if len2 > 0 then
        WinError(CryptHashData(hHash, @hData2[0], len2, 0), 'CryptHashData(hData1)');
      WinError(CryptGetHashParam(hHash, HP_HASHSIZE, @len, cb, 0), 'CryptGetHashParam(HP_HASHSIZE)');
      SetLength(Result, len);
      WinError(CryptGetHashParam(hHash, HP_HASHVAL, @Result[0], len, 0), 'CryptGetHashParam(HP_HASHVAL)');
    finally
      WinError(CryptDestroyHash(hHash), 'CryptDestroyHash');
    end;
  end;

var
  hProv: HCRYPTPROV;
  i_key_pad, o_key_pad: TBytes;
  data: TBytes;
  emptyArray: TBytes;
  len, i: Integer;
  c: Byte;
  ifree: Boolean;
begin
  ifree:=False;
  SetLength(Result, 0);
  SetLength(emptyArray, 0);
  SetLength(i_key_pad, BLOCK_SIZE);
  SetLength(o_key_pad, BLOCK_SIZE);
  WinError(CryptAcquireContext(hProv, Nil, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_VERIFYCONTEXT), 'CryptAcquireContext');
  try
    data:=AKey;
    len:=Length(data);
    if len > BLOCK_SIZE then begin
      data:=hash(hProv, data, emptyArray);
      len:=Length(data);
      ifree:=True;
    end;
    //
    i:=BLOCK_SIZE-1;
    while i >= 0 do begin
      c:=0;
      if i < len then
        c:=data[i];
      i_key_pad[i]:=$36 xor c;
      o_key_pad[i]:=$5c xor c;
      Dec(i);
    end;
    if ifree then
      SetLength(data, 0);
    data:=hash(hProv, i_key_pad, AMessage);
    Result:=hash(hProv, o_key_pad, data);
    SetLength(data, 0);
  finally
    SetLength(i_key_pad, 0);
    SetLength(o_key_pad, 0);
    WinError(CryptReleaseContext(hProv, 0), 'CryptReleaseContext');
  end;
end;

function TBytesToHex(const Value: TBytes): String;
const
  dictionary: Array[0..15] of Char = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f');
var
  i: Integer;
begin
  Result:='';
  for i:=0 to High(Value) do
    Result:=Result + dictionary[Value[i] shr 4] + dictionary[Value[i] and $0F];
end;

// source: https://stackoverflow.com/a/26892830/2111514
function MBCSString(const s: UnicodeString; CodePage: Word): RawByteString;
var
  enc: TEncoding;
  bytes: TBytes;
begin
  enc:=TEncoding.GetEncoding(CodePage);
  try
    bytes:=enc.GetBytes(s);
    SetLength(Result, Length(bytes));
    Move(Pointer(bytes)^, Pointer(Result)^, Length(bytes));
    SetCodePage(Result, CodePage, False);
  finally
    enc.Free;
  end;
end;

function UnicodeStringToTBytes(const Value: String): TBytes;
var
  ansi: AnsiString;
begin
  ansi:=MBCSString(Value, 65001); // Unicode (UTF-8) codepage
  Result:=BytesOf(ansi);
  ansi:='';
end;

function CryptoAPI_Hash_HmacSHA512(const InString, Password: TBytes): TBytes;
begin
  SetLength(Result, 0);
  if Length(Password) = 0 then
    raise EHMACError.Create('Error: Password length must be greater then 0!');

  Result:=hmac(Password, InString, CALG_SHA_512);
end;

function CryptoAPI_Hash_HmacSHA512(const InString, Password: String): String;
var
  input_bytes, input_password: TBytes;
begin
  input_bytes:=UnicodeStringToTBytes(InString);
  input_password:=UnicodeStringToTBytes(Password);
  try
    Result:=TBytesToHex(CryptoAPI_Hash_HmacSHA512(input_bytes, input_password));
  finally
    SetLength(input_password, 0);
    SetLength(input_bytes, 0);
  end;
end;

end.

Upvotes: 1

Related Questions