Search code examples
delphidelphi-xe5cryptoapihmacsha1

CryptoAPI returns incorrect result for HMAC_SHA1


I'm using the code below with the Crypto API and I'm not getting the results I would expect based on testing with other API's and libraries.

I'm using the key, "key" and the data is "message"

For example, using Indy's TidHMACSHA1, I get 2088df74d5f2146b48146caf4965377e9d0be3a4

I get the same result using online generators (such as http://www.freeformatter.com/hmac-generator.html, for example).

With the code I've written (see below) I get 4a52c3c0abc0a06049d1ab648bb4057e3ff5f359

The code is below, I'm using the JEDI wcrypt2.pas header

function Hashhmacsha1(const Key, Value: AnsiString): AnsiString;
var
  hCryptProvider: HCRYPTPROV;
  hHash: HCRYPTHASH;
  hKey: HCRYPTKEY;
  bHash: array[0..$7F] of Byte;
  dwHashLen: dWord;
  i: Integer;


  hHmacHash: HCRYPTHASH;
  bHmacHash: array[0..$7F] of Byte;
  dwHmacHashLen: dWord;
  hmac_info : Wcrypt2.HMAC_INFO;
begin
  dwHashLen := 32;
  dwHmacHashLen := 32;
  {get context for crypt default provider}
  if CryptAcquireContext(@hCryptProvider, nil, nil, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT or CRYPT_MACHINE_KEYSET) then
  begin
    {create hash-object }
    if CryptCreateHash(hCryptProvider, CALG_SHA1, 0, 0, @hHash) then
    begin

      {get hash from password}
      if CryptHashData(hHash, @Key[1], Length(Key), 0) then
      begin

        // hHash is now a hash of the provided key, (SHA1)
        // Now we derive a key for it
        if CryptDeriveKey(hCryptProvider, CALG_RC4, hHash, 0, @hKey) then
        begin

          //hkey now holds our key. So we have do the whole thing over again
          //ZeroMemory( hmac_info, SizeOf(hmac_info) );
          hmac_info.HashAlgid := CALG_SHA1;
          if CryptCreateHash(hCryptProvider, CALG_HMAC, hKey, 0, @hHmacHash) then
          begin

            {get hash from password}

              if CryptSetHashParam( hHmacHash, HP_HMAC_INFO, @hmac_info, 0) then
              begin

                if CryptHashData(hHmacHash, @Value[1], Length(Value), 0) then
                begin
                  if CryptGetHashParam(hHmacHash, HP_HASHVAL, @bHmacHash[0], @dwHmacHashLen, 0) then
                  begin
                    for i := 0 to dwHmacHashLen-1 do
                      Result := Result + IntToHex(bHmacHash[i], 2);
                  end
                  else
                   WriteLn( 'CryptGetHashParam ERROR --> ' + SysErrorMessage(GetLastError)) ;
                end
                else
                  WriteLn( 'CryptHashData ERROR --> ' + SysErrorMessage(GetLastError)) ;
                {destroy hash-object}
                CryptDestroyHash(hHmacHash);
                CryptDestroyKey(hKey);
              end
              else
                WriteLn( 'CryptSetHashParam ERROR --> ' + SysErrorMessage(GetLastError)) ;

          end
          else
            WriteLn( 'CryptCreateHash ERROR --> ' + SysErrorMessage(GetLastError)) ;
        end
        else
          WriteLn( 'CryptDeriveKey ERROR --> ' + SysErrorMessage(GetLastError)) ;

      end;
      {destroy hash-object}
      CryptDestroyHash(hHash);
    end;
    {release the context for crypt default provider}
    CryptReleaseContext(hCryptProvider, 0);
  end;
  Result := AnsiLowerCase(Result);
end;

I'm obviously doing something incorrectly, but I'm no idea what ??


Solution

  • So I found a solution, which when generating an HMAC_SHA1 for the data "message" with the key "key" generates the expected hash of 2088df74d5f2146b48146caf4965377e9d0be3a4

    As you can see, this code uses CryptImportKey instead of CryptDeriveKey, which seems to resolve the issue. It seems that using CryptDeriveKey is actually generating an HMAC_SHA1 hash using the data "message" and the SHA1 Hash of the key "key" encoded as RC4 instead of the plaintext key as initially thought.

    The code works for keys upto 16 characters in length, any larger and only uses the first 16 characters anyway. I'm posting a second quesiton to enquire about that!!

    Code is posted below.

    function Hashhmacsha1(const Key, Value: AnsiString): AnsiString;
    const
      KEY_LEN_MAX = 16;
    var
      hCryptProvider: HCRYPTPROV;
      hHash: HCRYPTHASH;
      hKey: HCRYPTKEY;
      bHash: array[0..$7F] of Byte;
      dwHashLen: dWord;
      i: Integer;
    
      hPubKey : HCRYPTKey;
      hHmacHash: HCRYPTHASH;
      bHmacHash: array[0..$7F] of Byte;
      dwHmacHashLen: dWord;
      hmac_info : Wcrypt2.HMAC_INFO;
    
      keyBlob: record
        keyHeader: BLOBHEADER;
        keySize: DWORD;
        keyData: array[0..KEY_LEN_MAX-1] of Byte;
      end;
      keyLen : INTEGER;
    begin
      dwHashLen := 32;
      dwHmacHashLen := 32;
      {get context for crypt default provider}
      if CryptAcquireContext(@hCryptProvider, nil, nil, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT) then
      begin
        {create hash-object MD5}
        if CryptCreateHash(hCryptProvider, CALG_SHA1, 0, 0, @hHash) then
        begin
    
          {get hash from password}
          if CryptHashData(hHash, PByte(Key), Length(Key), 0) then
          begin
    
            // hHash is now a hash of the provided key, (SHA1)
            // Now we derive a key for it
            hPubKey := 0;
    
            FillChar(keyBlob, SizeOf(keyBlob), 0);
            keyBlob.keyHeader.bType := PLAINTEXTKEYBLOB;
            keyBlob.keyHeader.bVersion := CUR_BLOB_VERSION;
            keyBlob.keyHeader.aiKeyAlg := CALG_RC4;
            KeyBlob.keySize := KEY_LEN_MAX;
    
            if(Length(key) < (KEY_LEN_MAX))then
              KeyLen := Length(key)
            else
              KeyLen := KEY_LEN_MAX;
            Move(Key[1], KeyBlob.keyData[0], KeyLen );
    
            if CryptImportKey(hCryptProvider, @keyBlob, SizeOf(KeyBlob), hPubKey, 0, @hKey) then
            begin
    
              //hkey now holds our key. So we have do the whole thing over again
              ZeroMemory( @hmac_info, SizeOf(hmac_info) );
              hmac_info.HashAlgid := CALG_SHA1;
              if CryptCreateHash(hCryptProvider, CALG_HMAC, hKey, 0, @hHmacHash) then
              begin
                  if CryptSetHashParam( hHmacHash, HP_HMAC_INFO, @hmac_info, 0) then
                  begin
    
                    if CryptHashData(hHmacHash, @Value[1], Length(Value), 0) then
                    begin
                      if CryptGetHashParam(hHmacHash, HP_HASHVAL, @bHmacHash[0], @dwHmacHashLen, 0) then
                      begin
                        for i := 0 to dwHmacHashLen-1 do
                          Result := Result + IntToHex(bHmacHash[i], 2);
                      end
                      else
                       WriteLn( 'CryptGetHashParam ERROR --> ' + SysErrorMessage(GetLastError)) ;
                    end
                    else
                      WriteLn( 'CryptHashData ERROR --> ' + SysErrorMessage(GetLastError)) ;
                    {destroy hash-object}
                    CryptDestroyHash(hHmacHash);
                    CryptDestroyKey(hKey);
                  end
                  else
                    WriteLn( 'CryptSetHashParam ERROR --> ' + SysErrorMessage(GetLastError)) ;
    
              end
              else
                WriteLn( 'CryptCreateHash ERROR --> ' + SysErrorMessage(GetLastError)) ;
            end
            else
              WriteLn( 'CryptDeriveKey ERROR --> ' + SysErrorMessage(GetLastError)) ;
    
          end;
          {destroy hash-object}
          CryptDestroyHash(hHash);
        end;
        {release the context for crypt default provider}
        CryptReleaseContext(hCryptProvider, 0);
      end;
      Result := AnsiLowerCase(Result);
    end;