unit Webauthn.Crypto;

interface

uses
  System.SysUtils, System.NetEncoding, System.Hash;

function Base64UrlEncode(const ABytes: TBytes): string;
function Base64UrlDecode(const AStr: string): TBytes;
function SHA256Bytes(const AData: TBytes): TBytes;
function HMACSHA256Bytes(const AKey, AData: TBytes): TBytes;
function RandomBytes(ACount: Integer): TBytes;
// Convert DER-encoded ECDSA signature (from WebAuthn) to raw 64-byte r||s
function DerSigToRaw(const ADer: TBytes): TBytes;
// Verify ES256 ECDSA-P256 signature over AMessage (raw bytes, hashed internally)
// APubKeyX, APubKeyY: raw 32-byte big-endian coordinates
// ASignatureDer: DER-encoded signature bytes from WebAuthn assertion
function VerifyECDSAP256(const APubKeyX, APubKeyY, AMessage, ASignatureDer: TBytes): Boolean;

implementation

uses
  Winapi.Windows;

const
  BCRYPT_ECDSA_PUBLIC_P256_MAGIC: DWORD = $31534345;
  BCRYPT_ECC_PUBLIC_BLOB = 'ECCPUBLICBLOB';
  STATUS_SUCCESS = LongInt(0);
  BCRYPT_USE_SYSTEM_PREFERRED_RNG: DWORD = 2;

type
  NTSTATUS = LongInt;
  BCRYPT_ALG_HANDLE = THandle;
  BCRYPT_KEY_HANDLE = THandle;
  BCRYPT_ECCKEY_BLOB = packed record
    dwMagic: DWORD;
    cbKey: DWORD;
  end;

function BCryptOpenAlgorithmProvider(out phAlgorithm: BCRYPT_ALG_HANDLE;
  pszAlgId, pszImplementation: PWideChar; dwFlags: DWORD): NTSTATUS;
  stdcall; external 'bcrypt.dll';

function BCryptCloseAlgorithmProvider(hAlgorithm: BCRYPT_ALG_HANDLE;
  dwFlags: DWORD): NTSTATUS;
  stdcall; external 'bcrypt.dll';

function BCryptImportKeyPair(hAlgorithm: BCRYPT_ALG_HANDLE;
  hImportKey: BCRYPT_KEY_HANDLE; pszBlobType: PWideChar;
  out phKey: BCRYPT_KEY_HANDLE; pbInput: PByte; cbInput: DWORD;
  dwFlags: DWORD): NTSTATUS;
  stdcall; external 'bcrypt.dll';

function BCryptDestroyKey(hKey: BCRYPT_KEY_HANDLE): NTSTATUS;
  stdcall; external 'bcrypt.dll';

function BCryptVerifySignature(hKey: BCRYPT_KEY_HANDLE; pPaddingInfo: Pointer;
  pbHash: PByte; cbHash: DWORD; pbSignature: PByte; cbSignature: DWORD;
  dwFlags: DWORD): NTSTATUS;
  stdcall; external 'bcrypt.dll';

function BCryptGenRandom(hAlgorithm: BCRYPT_ALG_HANDLE; pbBuffer: PByte;
  cbBuffer: DWORD; dwFlags: DWORD): NTSTATUS;
  stdcall; external 'bcrypt.dll';

// ---------------------------------------------------------------------------

function Base64UrlEncode(const ABytes: TBytes): string;
begin
  Result := TNetEncoding.Base64.EncodeBytesToString(ABytes);
  Result := Result.Replace('+', '-').Replace('/', '_').TrimRight(['=']);
end;

function Base64UrlDecode(const AStr: string): TBytes;
var
  s: string;
  padLen: Integer;
begin
  s := AStr.Replace('-', '+').Replace('_', '/');
  padLen := (4 - (Length(s) mod 4)) mod 4;
  if padLen > 0 then
    s := s + StringOfChar('=', padLen);
  Result := TNetEncoding.Base64.DecodeStringToBytes(s);
end;

function SHA256Bytes(const AData: TBytes): TBytes;
begin
  Result := THashSHA2.GetHashBytes(AData);
end;

function HMACSHA256Bytes(const AKey, AData: TBytes): TBytes;
const
  BlockSize = 64;
var
  normKey: TBytes;
  ipadKey, opadKey: TBytes;
  innerData, outerData: TBytes;
  innerHash: TBytes;
  i: Integer;
begin
  // Normalize key to block size
  SetLength(normKey, BlockSize);
  FillChar(normKey[0], BlockSize, 0);
  if Length(AKey) > BlockSize then
  begin
    innerHash := SHA256Bytes(AKey);
    Move(innerHash[0], normKey[0], Length(innerHash));
  end
  else if Length(AKey) > 0 then
    Move(AKey[0], normKey[0], Length(AKey));

  SetLength(ipadKey, BlockSize);
  SetLength(opadKey, BlockSize);
  for i := 0 to BlockSize - 1 do
  begin
    ipadKey[i] := normKey[i] xor $36;
    opadKey[i] := normKey[i] xor $5C;
  end;

  // inner = SHA256(ipadKey || data)
  SetLength(innerData, BlockSize + Length(AData));
  Move(ipadKey[0], innerData[0], BlockSize);
  if Length(AData) > 0 then
    Move(AData[0], innerData[BlockSize], Length(AData));
  innerHash := SHA256Bytes(innerData);

  // result = SHA256(opadKey || innerHash)
  SetLength(outerData, BlockSize + 32);
  Move(opadKey[0], outerData[0], BlockSize);
  Move(innerHash[0], outerData[BlockSize], 32);
  Result := SHA256Bytes(outerData);
end;

function RandomBytes(ACount: Integer): TBytes;
begin
  SetLength(Result, ACount);
  if ACount > 0 then
    BCryptGenRandom(0, @Result[0], ACount, BCRYPT_USE_SYSTEM_PREFERRED_RNG);
end;

function DerSigToRaw(const ADer: TBytes): TBytes;
var
  pos, rLen, sLen, rStart, sStart: Integer;
begin
  SetLength(Result, 64);
  FillChar(Result[0], 64, 0);

  pos := 0;
  if (Length(ADer) < 8) or (ADer[pos] <> $30) then Exit;
  Inc(pos);

  // Skip sequence length (handle 1-byte and 2-byte forms)
  if ADer[pos] = $81 then Inc(pos);
  Inc(pos);

  // r integer
  if (pos >= Length(ADer)) or (ADer[pos] <> $02) then Exit;
  Inc(pos);
  rLen   := ADer[pos]; Inc(pos);
  rStart := pos;
  Inc(pos, rLen);

  // s integer
  if (pos >= Length(ADer)) or (ADer[pos] <> $02) then Exit;
  Inc(pos);
  sLen   := ADer[pos]; Inc(pos);
  sStart := pos;

  // Copy r right-justified into Result[0..31], stripping leading 0x00
  if (rLen > 0) and (ADer[rStart] = $00) then begin Inc(rStart); Dec(rLen); end;
  if rLen > 32 then begin Inc(rStart, rLen - 32); rLen := 32; end;
  if rLen > 0 then
    Move(ADer[rStart], Result[32 - rLen], rLen);

  // Copy s right-justified into Result[32..63], stripping leading 0x00
  if (sLen > 0) and (ADer[sStart] = $00) then begin Inc(sStart); Dec(sLen); end;
  if sLen > 32 then begin Inc(sStart, sLen - 32); sLen := 32; end;
  if sLen > 0 then
    Move(ADer[sStart], Result[64 - sLen], sLen);
end;

function VerifyECDSAP256(const APubKeyX, APubKeyY, AMessage, ASignatureDer: TBytes): Boolean;
var
  algHandle: BCRYPT_ALG_HANDLE;
  keyHandle: BCRYPT_KEY_HANDLE;
  blob: TBytes;
  header: BCRYPT_ECCKEY_BLOB;
  msgHash, rawSig: TBytes;
  status: NTSTATUS;
begin
  Result := False;

  if (Length(APubKeyX) <> 32) or (Length(APubKeyY) <> 32) then Exit;

  header.dwMagic := BCRYPT_ECDSA_PUBLIC_P256_MAGIC;
  header.cbKey   := 32;

  SetLength(blob, SizeOf(BCRYPT_ECCKEY_BLOB) + 64);
  Move(header,       blob[0],                               SizeOf(BCRYPT_ECCKEY_BLOB));
  Move(APubKeyX[0],  blob[SizeOf(BCRYPT_ECCKEY_BLOB)],      32);
  Move(APubKeyY[0],  blob[SizeOf(BCRYPT_ECCKEY_BLOB) + 32], 32);

  // ES256 signs SHA-256(message)
  msgHash := SHA256Bytes(AMessage);
  rawSig  := DerSigToRaw(ASignatureDer);

  if Length(rawSig) <> 64 then Exit;

  status := BCryptOpenAlgorithmProvider(algHandle, 'ECDSA_P256', nil, 0);
  if status <> STATUS_SUCCESS then Exit;
  try
    status := BCryptImportKeyPair(algHandle, 0, BCRYPT_ECC_PUBLIC_BLOB,
                                  keyHandle, @blob[0], Length(blob), 0);
    if status <> STATUS_SUCCESS then Exit;
    try
      status := BCryptVerifySignature(keyHandle, nil,
                                      @msgHash[0], Length(msgHash),
                                      @rawSig[0],  Length(rawSig), 0);
      Result := (status = STATUS_SUCCESS);
    finally
      BCryptDestroyKey(keyHandle);
    end;
  finally
    BCryptCloseAlgorithmProvider(algHandle, 0);
  end;
end;

end.
