unit PBKDF2;

{
  Inno Setup
  Copyright (C) 1997-2024 Jordan Russell
  Portions by Martijn Laan
  For conditions of distribution and use, see LICENSE.TXT.

  PBKDF2-HMAC-SHA256 password-based key derivation

  modified for compilation with Delphi 10 by J. Rathlev, January 2025
}

interface

uses
  Compatibility, SysUtils;

function PBKDF2SHA256(Password: TBytes; const Salt: TBytes; const Iterations, KeyLength: Integer): TBytes; overload;
function PBKDF2SHA256(const Password: String; const Salt: TBytes; const Iterations, KeyLength: Integer): TBytes; overload;

implementation

uses
  SHA256, Math;

const
  SHA256BlockSize = 64;

function DigestToBytes(const Digest: TSHA256Digest): TBytes;
begin
  SetLength(Result, Length(Digest));
  Move(Digest[0], Result[0], Length(Digest));
end;

function GetHMACAsBytes(const AData, AKey: TBytes): TBytes;
const
  CInnerPad : Byte = $36;
  COuterPad : Byte = $5C;
var
  TempBuffer1: TBytes;
  TempBuffer2: TBytes;
  FKey: TBytes;
  LKey: TBytes;
  I: Integer;
  LBuffer: TBytes;
  LHashSize: Integer;
  LBlockSize: Integer;
begin
  LBlockSize := SHA256BlockSize;

  LBuffer := AData;

  FKey := AKey;
  if Length(FKey) > LBlockSize then
    FKey := DigestToBytes(SHA256Buf(FKey[0], Length(FKey)));

  LKey := Copy(FKey, 0, MaxInt);
  SetLength(LKey, LBlockSize);
  SetLength(TempBuffer1, LBlockSize + Length(LBuffer));
  for I := Low(LKey) to High(LKey) do
    TempBuffer1[I] := LKey[I] xor CInnerPad;

  if Length(LBuffer) > 0 then
    Move(LBuffer[0], TempBuffer1[Length(LKey)], Length(LBuffer));

  TempBuffer2 := DigestToBytes(SHA256Buf(TempBuffer1[0], Length(TempBuffer1)));

  LHashSize := Length(TempBuffer2);

  SetLength(TempBuffer1, LBlockSize + LHashSize);
  for I := Low(LKey) to High(LKey) do
    TempBuffer1[I] := LKey[I] xor COuterPad;

  Move(TempBuffer2[0], TempBuffer1[Length(LKey)], Length(TempBuffer2));

  Result := DigestToBytes(SHA256Buf(TempBuffer1[0], Length(TempBuffer1)));
end;


function PBKDF2SHA256(Password: TBytes; const Salt: TBytes; const Iterations, KeyLength: Integer): TBytes;
var
  I,J,L,Block,
  BytesDone,
  BytesLeft,
  BytesToCopy,
  HashSize : integer;
  SaltAndBlock,F,U : TBytes;
begin
  HashSize := Sizeof(TSHA256Digest);

  if Length(Password) > SHA256BlockSize then begin
    { Pre-hash password so THashSHA2.GetHMACAsBytes wont do this over and over again }
    Password := DigestToBytes(SHA256Buf(Password[0], Length(Password)));
  end;

  SetLength(Result, KeyLength);
  BytesDone := 0;

  L := Ceil(KeyLength / HashSize);

  for Block := 1 to L do begin
    SetLength(SaltAndBlock, Length(Salt) + 4);
    Move(Salt[0], SaltAndBlock[0], Length(Salt));
    SaltAndBlock[Length(Salt)] := Byte(Block shr 24);
    SaltAndBlock[Length(Salt)+1] := Byte(Block shr 16);
    SaltAndBlock[Length(Salt)+2] := Byte(Block shr 8);
    SaltAndBlock[Length(Salt)+3] := Byte(Block);
    U := GetHMACAsBytes(SaltAndBlock, Password);
    F := U;

    for I := 2 to Iterations do begin
      U := GetHMACAsBytes(U, Password);
      for J := 0 to High(F) do
        F[J] := F[J] xor U[J];
    end;

    BytesLeft := KeyLength - BytesDone;
    BytesToCopy := Min(BytesLeft, Length(F));
    Move(F[0], Result[BytesDone], BytesToCopy);
    Inc(BytesDone, BytesToCopy);
  end;
end;

function PBKDF2SHA256(const Password: String; const Salt: TBytes; const Iterations, KeyLength: Integer): TBytes;

  function StringToBytes(const S: String): TBytes;
  var
    N : integer;
  begin
    N := Length(S)*SizeOf(S[1]);
    SetLength(Result, N);
    if N > 0 then
      Move(S[1], Result[0], N);
  end;

begin
  Result := PBKDF2SHA256(StringToBytes(Password), Salt, Iterations, KeyLength);
end;

{.$DEFINE TEST}

{$IFDEF TEST}

{$C+}

procedure TestPBKDF2SHA256;
var
  Password,Salt : TBytes;

  function AnsiStringToBytes(const S: AnsiString): TBytes;
  var
    N : integer;
  begin
    N := Length(S)*SizeOf(S[1]);
    SetLength(Result, N);
    if N > 0 then
      Move(S[1], Result[0], N);
  end;

  procedure Test(const Key1, Key2: TBytes);
  var
    I : integer;
  begin
    Assert(Length(Key1) = Length(Key2));
    for I := 0 to High(Key1) do
      Assert(Key1[I] = Key2[I]);
  end;

begin
  //https://stackoverflow.com/a/5136918/301485
  Password := AnsiStringToBytes('password');
  Salt := AnsiStringToBytes('salt');
  Test(PBKDF2SHA256(Password, Salt, 1, 32),
       [$12, $0f, $b6, $cf, $fc, $f8, $b3, $2c, $43, $e7, $22, $52, $56, $c4, $f8, $37, $a8, $65, $48, $c9, $2c, $cc, $35, $48, $08, $05, $98, $7c, $b7, $0b, $e1, $7b]);
  Test(PBKDF2SHA256(Password, Salt, 4096, 32),
       [$c5, $e4, $78, $d5, $92, $88, $c8, $41, $aa, $53, $0d, $b6, $84, $5c, $4c, $8d, $96, $28, $93, $a0, $01, $ce, $4e, $11, $a4, $96, $38, $73, $aa, $98, $13, $4a]);

  Password := AnsiStringToBytes('passwordPASSWORDpassword');
  Salt := AnsiStringToBytes('saltSALTsaltSALTsaltSALTsaltSALTsalt');
  Test(PBKDF2SHA256(Password, Salt, 4096, 40),
       [$34, $8c, $89, $db, $cb, $d3, $2b, $2f, $32, $d8, $14, $b8, $11, $6e, $84, $cf, $2b, $17, $34, $7e, $bc, $18, $00, $18, $1c, $4e, $2a, $1f, $b8, $dd, $53, $e1, $c6, $35, $51, $8c, $7d, $ac, $47, $e9]);

  Password := AnsiStringToBytes('pass'#0'word');
  Salt := AnsiStringToBytes('sa'#0'lt');
  Test(PBKDF2SHA256(Password, Salt, 4096, 16),
       [$89, $b6, $9d, $05, $16, $f8, $29, $89, $3c, $69, $62, $26, $65, $0a, $86, $87]);

  //https://en.wikipedia.org/wiki/PBKDF2 - but with SHA256 instead of SHA1 - first tested using SHA1
  Password := AnsiStringToBytes('plnlrtfpijpuhqylxbgqiiyipieyxvfsavzgxbbcfusqkozwpngsyejqlmjsytrmd');
  Salt := [$a0, $09, $c1, $a4, $85, $91, $2c, $6a, $e6, $30, $d3, $e7, $44, $24, $0b, $04];
  Test(PBKDF2SHA256(Password, Salt, 1000, 16),
       [$28, $86, $9b, $5f, $31, $ae, $29, $23, $6f, $16, $4c, $5c, $b3, $3e, $2e, $3b]);
end;

initialization
  TestPBKDF2SHA256;

{$ENDIF}

end.