diff --git a/compiler/src/test/pascal/cp.test.e2e.crypto.pas b/compiler/src/test/pascal/cp.test.e2e.crypto.pas new file mode 100644 index 0000000..7ea7ba7 --- /dev/null +++ b/compiler/src/test/pascal/cp.test.e2e.crypto.pas @@ -0,0 +1,55 @@ +{ + Blaise - An Object Pascal Compiler + Copyright (c) 2026 Graeme Geldenhuys + SPDX-License-Identifier: Apache-2.0 WITH Swift-exception + Licensed under the Apache License v2.0 with Runtime Library Exception. + See LICENSE file in the project root for full license terms. +} + +unit cp.test.e2e.crypto; + +{ E2E smoke test for the Security.Crypto stdlib unit. + Guards against native-backend miscompilation of the hash + implementations (the Ord(S[I]) precedent). } + +interface + +uses + blaise.testing, cp.test.e2e.base; + +type + [Threaded] + TE2ECryptoTests = class(TE2ETestCase) + protected + procedure SetUp; override; + published + procedure TestRun_Sha256Hex_Empty; + end; + +implementation + +procedure TE2ECryptoTests.SetUp; +begin + inherited SetUp(); + SetUpScratch('compiler/target/test-e2e-crypto'); +end; + +procedure TE2ECryptoTests.TestRun_Sha256Hex_Empty; +begin + if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end; + AssertRTLRunsOnAll( + ''' + program P; + uses Security.Crypto; + begin + WriteLn(Sha256Hex('')); + end. + ''', + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + LineEnding, + 0); +end; + +initialization + RegisterTest(TE2ECryptoTests); + +end. diff --git a/stdlib/src/main/pascal/security.crypto.pas b/stdlib/src/main/pascal/security.crypto.pas index 79a49fb..2052c58 100644 --- a/stdlib/src/main/pascal/security.crypto.pas +++ b/stdlib/src/main/pascal/security.crypto.pas @@ -16,6 +16,10 @@ decisions. It remains required for interop where a protocol mandates it (e.g. the WebSocket opening handshake, Git object ids). + NB: MD5 is cryptographically broken and must not be used for new security + decisions. It remains required for legacy interop where a protocol or + existing system mandates it (e.g. content checksums, legacy API signatures). + Base64 lives in Encoding.Base64, not here: it is a text encoding, not crypto. Compose them at the call site, e.g. Base64Encode(Sha1(Key + GUID)). @@ -33,10 +37,33 @@ function Sha1(const AData: string): string; { SHA-1 digest as a 40-character lower-case hex string. } function Sha1Hex(const AData: string): string; +{ Raw 32-byte SHA-256 digest of AData (treated as raw bytes). } +function Sha256(const AData: string): string; + +{ SHA-256 digest as a 64-character lower-case hex string. } +function Sha256Hex(const AData: string): string; + +{ Raw 16-byte MD5 digest of AData (treated as raw bytes). } +function Md5(const AData: string): string; + +{ MD5 digest as a 32-character lower-case hex string. } +function Md5Hex(const AData: string): string; + +{ HMAC-SHA256 (RFC 2104 / RFC 4231). Returns raw 32-byte MAC. + Keys longer than 64 bytes (SHA-256 block size) are hashed first. } +function HmacSha256(const AKey, AData: string): string; + +{ HMAC-SHA256 as a 64-character lower-case hex string. } +function HmacSha256Hex(const AKey, AData: string): string; + +{ Timing-safe comparison. XOR-accumulates all bytes up to the shorter + length, then ORs in a length-mismatch flag. Never short-circuits. } +function ConstantTimeEqual(const A, B: string): Boolean; + implementation uses - Classes, StrUtils; + StrUtils; const MASK32 = $FFFFFFFF; @@ -46,6 +73,29 @@ begin Result := ((V shl ABits) or (V shr (32 - ABits))) and MASK32; end; +function Rotr32(V: UInt32; ABits: Integer): UInt32; +begin + Result := ((V shr ABits) or (V shl (32 - ABits))) and MASK32; +end; + +function DigestToHex(const ARaw: string): string; +var + SB: TStringBuilder; + I, B: Integer; +const + Hex = '0123456789abcdef'; +begin + SB := TStringBuilder.Create(); + for I := 0 to Length(ARaw) - 1 do + begin + B := Byte(ARaw[I]); + SB.AppendByte(Byte(Hex[B div 16])); + SB.AppendByte(Byte(Hex[B mod 16])); + end; + Result := SB.ToString(); + SB.Free(); +end; + function Sha1(const AData: string): string; var H0, H1, H2, H3, H4: UInt32; @@ -155,23 +205,326 @@ begin end; function Sha1Hex(const AData: string): string; -var - Raw: string; - SB: TStringBuilder; - I, B: Integer; -const - Hex = '0123456789abcdef'; begin - Raw := Sha1(AData); + Result := DigestToHex(Sha1(AData)); +end; + +function Sha256(const AData: string): string; +const + K: array[0..63] of UInt32 = ( + $428a2f98, $71374491, $b5c0fbcf, $e9b5dba5, + $3956c25b, $59f111f1, $923f82a4, $ab1c5ed5, + $d807aa98, $12835b01, $243185be, $550c7dc3, + $72be5d74, $80deb1fe, $9bdc06a7, $c19bf174, + $e49b69c1, $efbe4786, $0fc19dc6, $240ca1cc, + $2de92c6f, $4a7484aa, $5cb0a9dc, $76f988da, + $983e5152, $a831c66d, $b00327c8, $bf597fc7, + $c6e00bf3, $d5a79147, $06ca6351, $14292967, + $27b70a85, $2e1b2138, $4d2c6dfc, $53380d13, + $650a7354, $766a0abb, $81c2c92e, $92722c85, + $a2bfe8a1, $a81a664b, $c24b8b70, $c76c51a3, + $d192e819, $d6990624, $f40e3585, $106aa070, + $19a4c116, $1e376c08, $2748774c, $34b0bcb5, + $391c0cb3, $4ed8aa4a, $5b9cca4f, $682e6ff3, + $748f82ee, $78a5636f, $84c87814, $8cc70208, + $90befffa, $a4506ceb, $bef9a3f7, $c67178f2 + ); +var + H0, H1, H2, H3, H4, H5, H6, H7: UInt32; + MsgLen, TotalBits: Int64; + PadLen, I, T, ChunkStart, NumChunks, C: Integer; + Msg: array[0..63] of Byte; + W: array[0..63] of UInt32; + A, B, Cc, D, E, F, G, Hh: UInt32; + S0, S1, Ch, Maj, Temp1, Temp2: UInt32; + PData: string; + SB, OutSB: TStringBuilder; +begin + MsgLen := Length(AData); + TotalBits := MsgLen * 8; + + PadLen := 56 - ((MsgLen + 1) mod 64); + if PadLen < 0 then + PadLen := PadLen + 64; + SB := TStringBuilder.Create(); - for I := 0 to Length(Raw) - 1 do - begin - B := Byte(Raw[I]); - SB.AppendByte(Byte(Hex[B div 16])); { Hex is 0-based in Blaise } - SB.AppendByte(Byte(Hex[B mod 16])); - end; - Result := SB.ToString(); + SB.Append(AData); + SB.AppendByte(128); + for I := 1 to PadLen do + SB.AppendByte(0); + for I := 7 downto 0 do + SB.AppendByte((TotalBits shr (I * 8)) and $FF); + PData := SB.ToString(); SB.Free(); + + H0 := $6a09e667; + H1 := $bb67ae85; + H2 := $3c6ef372; + H3 := $a54ff53a; + H4 := $510e527f; + H5 := $9b05688c; + H6 := $1f83d9ab; + H7 := $5be0cd19; + + NumChunks := Length(PData) div 64; + for C := 0 to NumChunks - 1 do + begin + ChunkStart := C * 64; + for I := 0 to 63 do + Msg[I] := Byte(PData[ChunkStart + I]); + + for T := 0 to 15 do + W[T] := ((UInt32(Msg[T * 4]) shl 24) or + (UInt32(Msg[T * 4 + 1]) shl 16) or + (UInt32(Msg[T * 4 + 2]) shl 8) or + UInt32(Msg[T * 4 + 3])) and MASK32; + + for T := 16 to 63 do + begin + S0 := (Rotr32(W[T-15], 7) xor Rotr32(W[T-15], 18) xor (W[T-15] shr 3)) and MASK32; + S1 := (Rotr32(W[T-2], 17) xor Rotr32(W[T-2], 19) xor (W[T-2] shr 10)) and MASK32; + W[T] := (W[T-16] + S0 + W[T-7] + S1) and MASK32; + end; + + A := H0; B := H1; Cc := H2; D := H3; + E := H4; F := H5; G := H6; Hh := H7; + + for T := 0 to 63 do + begin + S1 := (Rotr32(E, 6) xor Rotr32(E, 11) xor Rotr32(E, 25)) and MASK32; + Ch := ((E and F) xor (((not E) and MASK32) and G)) and MASK32; + Temp1 := (Hh + S1 + Ch + K[T] + W[T]) and MASK32; + S0 := (Rotr32(A, 2) xor Rotr32(A, 13) xor Rotr32(A, 22)) and MASK32; + Maj := ((A and B) xor (A and Cc) xor (B and Cc)) and MASK32; + Temp2 := (S0 + Maj) and MASK32; + + Hh := G; + G := F; + F := E; + E := (D + Temp1) and MASK32; + D := Cc; + Cc := B; + B := A; + A := (Temp1 + Temp2) and MASK32; + end; + + H0 := (H0 + A) and MASK32; + H1 := (H1 + B) and MASK32; + H2 := (H2 + Cc) and MASK32; + H3 := (H3 + D) and MASK32; + H4 := (H4 + E) and MASK32; + H5 := (H5 + F) and MASK32; + H6 := (H6 + G) and MASK32; + H7 := (H7 + Hh) and MASK32; + end; + + OutSB := TStringBuilder.Create(); + OutSB.AppendByte((H0 shr 24) and $FF); OutSB.AppendByte((H0 shr 16) and $FF); + OutSB.AppendByte((H0 shr 8) and $FF); OutSB.AppendByte(H0 and $FF); + OutSB.AppendByte((H1 shr 24) and $FF); OutSB.AppendByte((H1 shr 16) and $FF); + OutSB.AppendByte((H1 shr 8) and $FF); OutSB.AppendByte(H1 and $FF); + OutSB.AppendByte((H2 shr 24) and $FF); OutSB.AppendByte((H2 shr 16) and $FF); + OutSB.AppendByte((H2 shr 8) and $FF); OutSB.AppendByte(H2 and $FF); + OutSB.AppendByte((H3 shr 24) and $FF); OutSB.AppendByte((H3 shr 16) and $FF); + OutSB.AppendByte((H3 shr 8) and $FF); OutSB.AppendByte(H3 and $FF); + OutSB.AppendByte((H4 shr 24) and $FF); OutSB.AppendByte((H4 shr 16) and $FF); + OutSB.AppendByte((H4 shr 8) and $FF); OutSB.AppendByte(H4 and $FF); + OutSB.AppendByte((H5 shr 24) and $FF); OutSB.AppendByte((H5 shr 16) and $FF); + OutSB.AppendByte((H5 shr 8) and $FF); OutSB.AppendByte(H5 and $FF); + OutSB.AppendByte((H6 shr 24) and $FF); OutSB.AppendByte((H6 shr 16) and $FF); + OutSB.AppendByte((H6 shr 8) and $FF); OutSB.AppendByte(H6 and $FF); + OutSB.AppendByte((H7 shr 24) and $FF); OutSB.AppendByte((H7 shr 16) and $FF); + OutSB.AppendByte((H7 shr 8) and $FF); OutSB.AppendByte(H7 and $FF); + Result := OutSB.ToString(); + OutSB.Free(); +end; + +function Sha256Hex(const AData: string): string; +begin + Result := DigestToHex(Sha256(AData)); +end; + +function Md5(const AData: string): string; +const + T: array[0..63] of UInt32 = ( + $d76aa478, $e8c7b756, $242070db, $c1bdceee, + $f57c0faf, $4787c62a, $a8304613, $fd469501, + $698098d8, $8b44f7af, $ffff5bb1, $895cd7be, + $6b901122, $fd987193, $a679438e, $49b40821, + $f61e2562, $c040b340, $265e5a51, $e9b6c7aa, + $d62f105d, $02441453, $d8a1e681, $e7d3fbc8, + $21e1cde6, $c33707d6, $f4d50d87, $455a14ed, + $a9e3e905, $fcefa3f8, $676f02d9, $8d2a4c8a, + $fffa3942, $8771f681, $6d9d6122, $fde5380c, + $a4beea44, $4bdecfa9, $f6bb4b60, $bebfbc70, + $289b7ec6, $eaa127fa, $d4ef3085, $04881d05, + $d9d4d039, $e6db99e5, $1fa27cf8, $c4ac5665, + $f4292244, $432aff97, $ab9423a7, $fc93a039, + $655b59c3, $8f0ccc92, $ffeff47d, $85845dd1, + $6fa87e4f, $fe2ce6e0, $a3014314, $4e0811a1, + $f7537e82, $bd3af235, $2ad7d2bb, $eb86d391 + ); + S: array[0..63] of Integer = ( + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21 + ); +var + H0, H1, H2, H3: UInt32; + MsgLen, TotalBits: Int64; + PadLen, I, J, Chunk, ChunkStart, NumChunks, G: Integer; + M: array[0..15] of UInt32; + A, B, Cc, D, F, Tmp: UInt32; + PData: string; + SB, OutSB: TStringBuilder; +begin + MsgLen := Length(AData); + TotalBits := MsgLen * 8; + + PadLen := 56 - ((MsgLen + 1) mod 64); + if PadLen < 0 then + PadLen := PadLen + 64; + + SB := TStringBuilder.Create(); + SB.Append(AData); + SB.AppendByte(128); + for I := 1 to PadLen do + SB.AppendByte(0); + { MD5 uses LITTLE-ENDIAN 64-bit bit length. } + for I := 0 to 7 do + SB.AppendByte((TotalBits shr (I * 8)) and $FF); + PData := SB.ToString(); + SB.Free(); + + H0 := $67452301; + H1 := $efcdab89; + H2 := $98badcfe; + H3 := $10325476; + + NumChunks := Length(PData) div 64; + for Chunk := 0 to NumChunks - 1 do + begin + ChunkStart := Chunk * 64; + { Read 16 words in LITTLE-ENDIAN order. } + for J := 0 to 15 do + M[J] := (UInt32(Byte(PData[ChunkStart + J * 4 + 3])) shl 24) or + (UInt32(Byte(PData[ChunkStart + J * 4 + 2])) shl 16) or + (UInt32(Byte(PData[ChunkStart + J * 4 + 1])) shl 8) or + UInt32(Byte(PData[ChunkStart + J * 4])); + + A := H0; B := H1; Cc := H2; D := H3; + + for I := 0 to 63 do + begin + if I < 16 then + begin + F := (B and Cc) or (((not B) and MASK32) and D); + G := I; + end + else if I < 32 then + begin + F := (D and B) or (((not D) and MASK32) and Cc); + G := (5 * I + 1) mod 16; + end + else if I < 48 then + begin + F := B xor Cc xor D; + G := (3 * I + 5) mod 16; + end + else + begin + F := (Cc xor (B or ((not D) and MASK32))) and MASK32; + G := (7 * I) mod 16; + end; + F := F and MASK32; + Tmp := D; + D := Cc; + Cc := B; + B := (B + Rotl32((A + F + T[I] + M[G]) and MASK32, S[I])) and MASK32; + A := Tmp; + end; + + H0 := (H0 + A) and MASK32; + H1 := (H1 + B) and MASK32; + H2 := (H2 + Cc) and MASK32; + H3 := (H3 + D) and MASK32; + end; + + { Emit 16 raw bytes, LITTLE-ENDIAN per word. } + OutSB := TStringBuilder.Create(); + OutSB.AppendByte(H0 and $FF); OutSB.AppendByte((H0 shr 8) and $FF); + OutSB.AppendByte((H0 shr 16) and $FF); OutSB.AppendByte((H0 shr 24) and $FF); + OutSB.AppendByte(H1 and $FF); OutSB.AppendByte((H1 shr 8) and $FF); + OutSB.AppendByte((H1 shr 16) and $FF); OutSB.AppendByte((H1 shr 24) and $FF); + OutSB.AppendByte(H2 and $FF); OutSB.AppendByte((H2 shr 8) and $FF); + OutSB.AppendByte((H2 shr 16) and $FF); OutSB.AppendByte((H2 shr 24) and $FF); + OutSB.AppendByte(H3 and $FF); OutSB.AppendByte((H3 shr 8) and $FF); + OutSB.AppendByte((H3 shr 16) and $FF); OutSB.AppendByte((H3 shr 24) and $FF); + Result := OutSB.ToString(); + OutSB.Free(); +end; + +function Md5Hex(const AData: string): string; +begin + Result := DigestToHex(Md5(AData)); +end; + +function HmacSha256(const AKey, AData: string): string; +const + BLOCK_SIZE = 64; { SHA-256 block size; SHA-512 uses 128 } +var + KeyBlock: string; + IPad, OPad: TStringBuilder; + I: Integer; + InnerHash: string; +begin + if Length(AKey) > BLOCK_SIZE then + KeyBlock := Sha256(AKey) + else + KeyBlock := AKey; + + IPad := TStringBuilder.Create(); + OPad := TStringBuilder.Create(); + for I := 0 to BLOCK_SIZE - 1 do + begin + if I < Length(KeyBlock) then + begin + IPad.AppendByte(Byte(KeyBlock[I]) xor $36); + OPad.AppendByte(Byte(KeyBlock[I]) xor $5C); + end + else + begin + IPad.AppendByte($36); + OPad.AppendByte($5C); + end; + end; + + IPad.Append(AData); + InnerHash := Sha256(IPad.ToString()); + IPad.Free(); + + OPad.Append(InnerHash); + Result := Sha256(OPad.ToString()); + OPad.Free(); +end; + +function HmacSha256Hex(const AKey, AData: string): string; +begin + Result := DigestToHex(HmacSha256(AKey, AData)); +end; + +function ConstantTimeEqual(const A, B: string): Boolean; +var + Diff, I, MinLen: Integer; +begin + Diff := Length(A) xor Length(B); + MinLen := Length(A); + if Length(B) < MinLen then + MinLen := Length(B); + for I := 0 to MinLen - 1 do + Diff := Diff or (Byte(A[I]) xor Byte(B[I])); + Result := Diff = 0; end; end. diff --git a/stdlib/src/test/pascal/crypto.tests.pas b/stdlib/src/test/pascal/crypto.tests.pas index 759425e..dc57c03 100644 --- a/stdlib/src/test/pascal/crypto.tests.pas +++ b/stdlib/src/test/pascal/crypto.tests.pas @@ -14,7 +14,7 @@ unit Crypto.Tests; interface uses - blaise.testing, Security.Crypto, Encoding.Base64; + blaise.testing, Security.Crypto, Encoding.Base64, StrUtils; type TCryptoTests = class(TTestCase) @@ -22,6 +22,15 @@ type procedure TestSha1Hex_KnownVectors; procedure TestSha1_DigestLength; procedure TestSha1Base64_WebSocketHandshake; + procedure TestSha256Hex_KnownVectors; + procedure TestSha256_DigestLength; + procedure TestSha256Hex_BoundaryVectors; + procedure TestMd5Hex_KnownVectors; + procedure TestMd5_DigestLength; + procedure TestHmacSha256Hex_KnownVectors; + procedure TestHmacSha256_DigestLength; + procedure TestHmacSha256Hex_LongKey; + procedure TestConstantTimeEqual; end; implementation @@ -51,6 +60,148 @@ begin Base64Encode(Sha1('dGhlIHNhbXBsZSBub25jZQ==' + GUID))); end; +procedure TCryptoTests.TestSha256Hex_KnownVectors; +begin + { NIST CAVP / FIPS 180-4 SHA-256 test vectors. } + AssertEquals('empty', + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + Sha256Hex('')); + AssertEquals('abc', + 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', + Sha256Hex('abc')); + AssertEquals('448-bit (56 bytes, 2-block)', + '248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1', + Sha256Hex('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')); + AssertEquals('896-bit (112 bytes, multi-block)', + 'cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1', + Sha256Hex('abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu')); +end; + +procedure TCryptoTests.TestSha256_DigestLength; +begin + AssertEquals('raw digest is 32 bytes', 32, Length(Sha256('anything'))); +end; + +procedure TCryptoTests.TestSha256Hex_BoundaryVectors; +begin + { 55 bytes: padding (0x80 + 8-byte length = 9 bytes) fits in one 64-byte block. } + AssertEquals('55-byte single-block boundary', + '9f4390f8d30c2dd92ec9f095b65e2b9ae9b0a925a5258e241c9f1e910f734318', + Sha256Hex('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')); +end; + +procedure TCryptoTests.TestMd5Hex_KnownVectors; +begin + { RFC 1321 Appendix A.5 — all 7 test vectors. } + AssertEquals('empty', + 'd41d8cd98f00b204e9800998ecf8427e', Md5Hex('')); + AssertEquals('a', + '0cc175b9c0f1b6a831c399e269772661', Md5Hex('a')); + AssertEquals('abc', + '900150983cd24fb0d6963f7d28e17f72', Md5Hex('abc')); + AssertEquals('message digest', + 'f96b697d7cb7938d525a2f31aaf161d0', Md5Hex('message digest')); + AssertEquals('a..z', + 'c3fcd3d76192e4007dfb496cca67e13b', Md5Hex('abcdefghijklmnopqrstuvwxyz')); + AssertEquals('A..Za..z0..9', + 'd174ab98d277d9f5a5611c2c9f419d9f', + Md5Hex('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')); + AssertEquals('8x 1234567890', + '57edf4a22be3c955ac49da2e2107b67a', + Md5Hex('12345678901234567890123456789012345678901234567890123456789012345678901234567890')); +end; + +procedure TCryptoTests.TestMd5_DigestLength; +begin + AssertEquals('raw digest is 16 bytes', 16, Length(Md5('anything'))); +end; + +procedure TCryptoTests.TestHmacSha256Hex_KnownVectors; +var + KeySB, DataSB: TStringBuilder; + I: Integer; + Key1, Data1, Key3, Data3: string; +begin + { RFC 4231 Test Case 1: 20-byte key of 0x0b, data = "Hi There" } + KeySB := TStringBuilder.Create(); + for I := 0 to 19 do + KeySB.AppendByte($0b); + Key1 := KeySB.ToString(); + KeySB.Free(); + AssertEquals('TC1', + 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7', + HmacSha256Hex(Key1, 'Hi There')); + + { RFC 4231 Test Case 2: key = "Jefe", data = "what do ya want for nothing?" } + AssertEquals('TC2', + '5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843', + HmacSha256Hex('Jefe', 'what do ya want for nothing?')); + + { RFC 4231 Test Case 3: 20-byte key of 0xaa, 50-byte data of 0xdd } + KeySB := TStringBuilder.Create(); + for I := 0 to 19 do + KeySB.AppendByte($aa); + Key3 := KeySB.ToString(); + KeySB.Free(); + DataSB := TStringBuilder.Create(); + for I := 0 to 49 do + DataSB.AppendByte($dd); + Data3 := DataSB.ToString(); + DataSB.Free(); + AssertEquals('TC3', + '773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe', + HmacSha256Hex(Key3, Data3)); +end; + +procedure TCryptoTests.TestHmacSha256_DigestLength; +begin + AssertEquals('raw HMAC is 32 bytes', 32, + Length(HmacSha256('key', 'data'))); +end; + +procedure TCryptoTests.TestHmacSha256Hex_LongKey; +var + KeySB, DataSB: TStringBuilder; + I: Integer; + LongKey, Data: string; +begin + { RFC 4231 Test Case 5: 131-byte key of 0xaa (> 64-byte block size), + data = "Test Using Larger Than Block-Size Key - Hash Key First" } + KeySB := TStringBuilder.Create(); + for I := 0 to 130 do + KeySB.AppendByte($aa); + LongKey := KeySB.ToString(); + KeySB.Free(); + AssertEquals('TC5 long key', + '60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54', + HmacSha256Hex(LongKey, 'Test Using Larger Than Block-Size Key - Hash Key First')); + + { RFC 4231 Test Case 6: same 131-byte key, + data = "This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm." } + AssertEquals('TC6 long key+data', + '9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2', + HmacSha256Hex(LongKey, + 'This is a test using a larger than block-size key and a ' + + 'larger than block-size data. The key needs to be hashed ' + + 'before being used by the HMAC algorithm.')); +end; + +procedure TCryptoTests.TestConstantTimeEqual; +begin + AssertTrue('equal strings', ConstantTimeEqual('abc', 'abc')); + AssertTrue('both empty', ConstantTimeEqual('', '')); + AssertFalse('different content', ConstantTimeEqual('abc', 'abd')); + AssertFalse('different lengths', ConstantTimeEqual('abc', 'ab')); + AssertFalse('one empty', ConstantTimeEqual('abc', '')); + AssertFalse('other empty', ConstantTimeEqual('', 'abc')); + AssertTrue('long equal', ConstantTimeEqual( + 'The quick brown fox jumps over the lazy dog', + 'The quick brown fox jumps over the lazy dog')); + AssertFalse('long differ last byte', ConstantTimeEqual( + 'The quick brown fox jumps over the lazy dog', + 'The quick brown fox jumps over the lazy doh')); +end; + initialization RegisterTest(TCryptoTests);