diff --git a/Docs/plans/2026-07-25-aes-ni.md b/Docs/plans/2026-07-25-aes-ni.md new file mode 100644 index 0000000..73df168 --- /dev/null +++ b/Docs/plans/2026-07-25-aes-ni.md @@ -0,0 +1,83 @@ +# Package C — AES-NI acceleration with mandatory portable fallback + +**Branch:** `package/aes-ni` +**Base:** package B (`package/chacha-poly1305`) +**Donor:** git ref `pr-90` AES assembler in `Source/DECCiphers.pas` + `UseAESAsm` + +## Goal + +Accelerate AES/Rijndael on x86/x64 CPUs that support AES-NI, without ever +requiring AES-NI for correctness. Pure Pascal remains the default portable path +and always compiles. + +## Cross-platform policy + +| Environment | AES-NI machine code | Runtime path | +|-------------|---------------------|--------------| +| Delphi Win32/Win64 (X86ASM/X64ASM) + AES-NI CPU | Compiled | AES-NI when `UseAESAsm` | +| Same build, CPU without AES-NI | Compiled | Pure Pascal | +| PUREPASCAL / NO_ASM / FPC / non-x86 / ARM / mobile | **Not compiled** | Pure Pascal only | + +Correctness never depends on AES-NI. Hardware is an optional speedup. + +## Compile-time vs runtime gates + +1. **Compile-time:** all AES-NI procedures (`BuildAsmKey128/192/256`, + `BuildAsmDecodeKey`, `AESEncode`, `AESDecode`) are wrapped in + `{$IF defined(X86ASM) or defined(X64ASM)}`. +2. **Runtime (init):** AES-NI key schedule only if + `UseAESAsm and TDEC_CPUSupport.AES and (Size in [16,24,32])`. +3. **Runtime (encode/decode):** use AES-NI only when instance flag + `FAESAsmActive` was set by `DoInit` (avoids mismatch if key size was + non-standard or `UseAESAsm` changed after init). + +Default at unit init: + +```pascal +TCipher_Rijndael.UseAESAsm := + {$IF defined(X86ASM) or defined(X64ASM)} + TDEC_CPUSupport.AES + {$ELSE} + False + {$IFEND}; +``` + +## Force pure Pascal + +```pascal +TCipher_Rijndael.UseAESAsm := False; // before Init; also covers TCipher_AES* +``` + +Tests force PAS and AES-NI independently and require identical ciphertext. + +## Key schedule notes + +- Donor name `BuildAsmKey196` renamed to **`BuildAsmKey192`** (Intel 192-bit schedule). +- Decode schedule uses **AESIMC** (`BuildAsmDecodeKey`), reverse-ordered for sequential `AESDEC`. +- `AdditionalBufferSize` gains `+$20` so `AlignPtr32(FAdditionalBuffer)` plus the + max AES-NI schedule fits. PAS path still uses `FAdditionalBufferSize shr 1` + for encode/decode halves. +- Non-standard Rijndael key lengths (not 16/24/32) always use the PAS schedule. + +## Intel reference + +AES-NI instruction whitepaper: +https://www.intel.com/content/dam/develop/external/us/en/documents/aes-wp-2012-09-22-v01-165683.pdf + +## Files + +- `Source/DECCiphers.pas` — `UseAESAsm`, AES-NI kernels, DoInit/DoEncode/DoDecode dispatch +- `Unit Tests/Tests/TestDECAESNI.pas` — FIPS-197 ECB KATs + PAS vs AES-NI +- Relies on existing `DECCPUSupport.AES`, `AlignPtr32` (no change required) + +## Out of scope + +- ARM Crypto Extensions / ARMv8 AES (future) +- ChaCha / Poly1305 / GCM changes +- Auto-enable on non-Windows x86 unless DEC already defines X86ASM there + +## Residual risks + +- Non-16/24/32 key sizes never use AES-NI (intentional; PAS only). +- Changing `UseAESAsm` after `Init` does not switch path until re-`Init` + (`FAESAsmActive` is set at init time). diff --git a/Source/DECCiphers.pas b/Source/DECCiphers.pas index 57ba4dd..38b8987 100644 --- a/Source/DECCiphers.pas +++ b/Source/DECCiphers.pas @@ -381,8 +381,16 @@ TCipher_RC6 = class(TDECFormattedCipher) end; TCipher_Rijndael = class(TDECFormattedCipher) + // Don't change these class constants (AES block width / max rounds). + const + Rijndael_Blocks = 4; + Rijndael_Rounds = 14; private FRounds: Integer; + // True when DoInit built an AES-NI key schedule (standard 16/24/32-byte + // keys, UseAESAsm, CPU AES-NI). Encode/decode follow this flag so + // non-standard key sizes stay on the PAS path. + FAESAsmActive: Boolean; /// /// Calculates the key used for encoding. Implemented is the "new AES /// conform key scheduling". @@ -410,6 +418,12 @@ TCipher_Rijndael = class(TDECFormattedCipher) procedure DoEncode(Source, Dest: Pointer; Size: Integer); override; procedure DoDecode(Source, Dest: Pointer; Size: Integer); override; public + /// + /// When True and the CPU has AES-NI and the ASM path is compiled + /// (X86ASM/X64ASM), use hardware AES. Default is set at unit + /// initialization from CPUID. Set False to force the pure Pascal path. + /// + class var UseAESAsm: Boolean; class function Context: TCipherContext; override; /// /// Gets the number of rounds/times the algorithm is being applied to the @@ -2830,111 +2844,536 @@ procedure TCipher_RC6.DoDecode(Source, Dest: Pointer; Size: Integer); { TCipher_Rijndael } class function TCipher_Rijndael.Context: TCipherContext; -const - // don't change this! - Rijndael_Blocks = 4; - Rijndael_Rounds = 14; begin Result.KeySize := 32; Result.BlockSize := Rijndael_Blocks * 4; Result.BufferSize := Rijndael_Blocks * 4; - Result.AdditionalBufferSize := (Rijndael_Rounds + 1) * Rijndael_Blocks * SizeOf(UInt32) * 2; + // +$20 spare so AlignPtr32(FAdditionalBuffer) + max AES-NI schedule fits. + // PAS path still uses FAdditionalBufferSize shr 1 for encode/decode halves. + Result.AdditionalBufferSize := (Rijndael_Rounds + 1) * Rijndael_Blocks * SizeOf(UInt32) * 2 + $20; Result.NeedsAdditionalBufferBackup := False; Result.MinRounds := 1; Result.MaxRounds := 1; Result.CipherType := [ctSymmetric, ctBlock]; end; -procedure TCipher_Rijndael.DoInit(const Key; Size: Integer); -{$REGION OldKeyShedule} -{ - // Old Rijndael Key Scheduling: - - procedure BuildEncodeKey; - const - RND_Data: array[0..29] of Byte = ( - $01, $02, $04, $08, $10, $20, $40, $80, $1B, $36, $6C, $D8, $AB, $4D, $9A, - $2F, $5E, $BC, $63, $C6, $97, $35, $6A, $D4, $B3, $7D, $FA, $EF, $C5, $91 - ); - var - T, R: Integer; +{$IF defined(X86ASM) or defined(X64ASM)} - procedure NextRounds; - var - J: Integer; - begin - J := 0; - while (J < FRounds - 6) and (R <= FRounds) do - begin - while (J < FRounds - 6) and (T < Rijndael_Blocks) do - begin - PUInt32Array(FBuffer)[R * Rijndael_Blocks + T] := K[J]; - Inc(J); - Inc(T); - end; - if T = Rijndael_Blocks then - begin - T := 0; - Inc(R); - end; - end; - end; +// ########################################### +// #### AES-NI assembler (Intel AES whitepaper) +// #### https://www.intel.com/content/dam/develop/external/us/en/documents/aes-wp-2012-09-22-v01-165683.pdf +// #### Only compiled when X86ASM or X64ASM is defined (off under PUREPASCAL/NO_ASM/FPC). +// ########################################### - var - RND: PByte; - B: PByte; - I: Integer; - begin - R := 0; - T := 0; - RND := @RND_Data; - NextRounds; - while R <= FRounds do - begin - B := @K; - B^ := B^ xor Rijndael_S[0, K[FRounds - 7] shr 8 and $FF] xor RND^; Inc(B); - B^ := B^ xor Rijndael_S[0, K[FRounds - 7] shr 16 and $FF]; Inc(B); - B^ := B^ xor Rijndael_S[0, K[FRounds - 7] shr 24]; Inc(B); - B^ := B^ xor Rijndael_S[0, K[FRounds - 7] and $FF]; - Inc(RND); - if FRounds = 14 then - begin - for I := 1 to 7 do - K[I] := K[I] xor K[I - 1]; - B := @K[4]; - B^ := B^ xor Rijndael_S[0, K[3] and $FF]; Inc(B); - B^ := B^ xor Rijndael_S[0, K[3] shr 8 and $FF]; Inc(B); - B^ := B^ xor Rijndael_S[0, K[3] shr 16 and $FF]; Inc(B); - B^ := B^ xor Rijndael_S[0, K[3] shr 24]; - for I := 5 to 7 do - K[I] := K[I] xor K[I - 1]; - end - else - for I := 1 to FRounds - 7 do - K[I] := K[I] xor K[I - 1]; - NextRounds; - end; - end; +// Assumes registers preloaded — only call from BuildAsmKey128. +procedure KeyExpand128; register; +asm + pshufd xmm2, xmm2, $ff; + movapd xmm3, xmm1; + pslldq xmm3, $04; + pxor xmm1, xmm3; + movapd xmm3, xmm1; + pslldq xmm3, $04; + pxor xmm1, xmm3; + movapd xmm3, xmm1; + pslldq xmm3, $04; + pxor xmm1, xmm3; + pxor xmm1, xmm2; + {$IFDEF X64ASM} + movdqu [rdx], xmm1; + add rdx, $10; + {$ELSE} + movdqu [edx], xmm1; + add edx, $10; + {$ENDIF} +end; - procedure BuildDecodeKey; - var - I: Integer; - D: PUInt32; - begin - D := Pointer(PAnsiChar(FBuffer) + FBufferSize shr 1); // for Pointer Math - Move(FBuffer^, D^, FBufferSize shr 1); - Inc(D, 4); - for I := 0 to FRounds * 4 - 5 do - begin - D^ := Rijndael_Key[D^ and $FF] xor - (Rijndael_Key[D^ shr 8 and $FF] shl 8 or Rijndael_Key[D^ shr 8 and $FF] shr 24) xor - (Rijndael_Key[D^ shr 16 and $FF] shl 16 or Rijndael_Key[D^ shr 16 and $FF] shr 16) xor - (Rijndael_Key[D^ shr 24] shl 24 or Rijndael_Key[D^ shr 24] shr 8); - Inc(D); - end; - end; } -{$ENDREGION} +procedure BuildAsmKey128(Key: PByte; Dest: PLongWord); register; +// eax: key, edx: dest / rcx: key, rdx: dest +asm + xorpd xmm2, xmm2; + {$IFDEF X64ASM} + movdqu xmm1, [rcx]; + movdqu [rdx], xmm1; + add rdx, 16; + {$ELSE} + movdqu xmm1, [eax]; + movdqu [edx], xmm1; + add edx, 16; + {$ENDIF} + + aeskeygenassist xmm2, xmm1, $1; + call KeyExpand128; + aeskeygenassist xmm2, xmm1, $2; + call KeyExpand128; + aeskeygenassist xmm2, xmm1, $4; + call KeyExpand128; + aeskeygenassist xmm2, xmm1, $8; + call KeyExpand128; + aeskeygenassist xmm2, xmm1, $10; + call KeyExpand128; + aeskeygenassist xmm2, xmm1, $20; + call KeyExpand128; + aeskeygenassist xmm2, xmm1, $40; + call KeyExpand128; + aeskeygenassist xmm2, xmm1, $80; + call KeyExpand128; + aeskeygenassist xmm2, xmm1, $1b; + call KeyExpand128; + aeskeygenassist xmm2, xmm1, $36; + call KeyExpand128; +end; + +// Only call from BuildAsmKey192 — assumes xmm0/xmm1/xmm2 preloaded. +procedure KeyExpand192; register; +asm + pshufd xmm1, xmm1, $55; + movapd xmm3, xmm0; + pslldq xmm3, $04; + pxor xmm0, xmm3; + + pslldq xmm3, $04; + pxor xmm0, xmm3; + + pslldq xmm3, $04; + pxor xmm0, xmm3; + + pxor xmm0, xmm1; + pshufd xmm1, xmm0, $FF; + + movapd xmm3, xmm2; + pslldq xmm3, $04; + + pxor xmm2, xmm3; + pxor xmm2, xmm1; +end; + +/// +/// AES-192 key expansion via AES-NI (Intel schedule for 192-bit keys). +/// Renamed from donor BuildAsmKey196 (typo for 192). +/// +procedure BuildAsmKey192(Key: PByte; Dest: PLongWord); register; +// eax: key, edx: dest / rcx: key, rdx: dest +asm + xorpd xmm2, xmm2; + + {$IFDEF X64ASM} + movupd xmm0, [rcx]; + movsd xmm2, [rcx + 16]; + movupd [rdx], xmm0; + {$ELSE} + movupd xmm0, [eax]; + movsd xmm2, [eax + 16]; + movupd [edx], xmm0; + {$ENDIF} + + movapd xmm4, xmm2; + + aeskeygenassist xmm1, xmm2, $01; + call KeyExpand192; + + shufpd xmm4, xmm0, 0; + {$IFDEF X64ASM} + movupd [rdx + 16], xmm4; + {$ELSE} + movupd [edx + 16], xmm4; + {$ENDIF} + movapd xmm4, xmm0; + shufpd xmm4, xmm2, 1; + {$IFDEF X64ASM} + movupd [rdx + 32], xmm4; + {$ELSE} + movupd [edx + 32], xmm4; + {$ENDIF} + + aeskeygenassist xmm1, xmm2, $02; + call KeyExpand192; + {$IFDEF X64ASM} + movupd [rdx + 48], xmm0; + {$ELSE} + movupd [edx + 48], xmm0; + {$ENDIF} + movapd xmm4, xmm2; + aeskeygenassist xmm1, xmm2, $04; + call KeyExpand192; + shufpd xmm4, xmm0, 0; + {$IFDEF X64ASM} + movupd [rdx + 64], xmm4; + {$ELSE} + movupd [edx + 64], xmm4; + {$ENDIF} + + movapd xmm4, xmm0; + shufpd xmm4, xmm2, 1; + {$IFDEF X64ASM} + movupd [rdx + 80], xmm4; + {$ELSE} + movupd [edx + 80], xmm4; + {$ENDIF} + + aeskeygenassist xmm1, xmm2, $08; + call KeyExpand192; + {$IFDEF X64ASM} + movupd [rdx + 96], xmm0; + {$ELSE} + movupd [edx + 96], xmm0; + {$ENDIF} + movapd xmm4, xmm2; + + aeskeygenassist xmm1, xmm2, $10; + call KeyExpand192; + shufpd xmm4, xmm0, 0; + {$IFDEF X64ASM} + movupd [rdx + 112], xmm4; + {$ELSE} + movupd [edx + 112], xmm4; + {$ENDIF} + + movapd xmm4, xmm0; + shufpd xmm4, xmm2, 1; + {$IFDEF X64ASM} + movupd [rdx + 128], xmm4; + {$ELSE} + movupd [edx + 128], xmm4; + {$ENDIF} + + aeskeygenassist xmm1, xmm2, $20; + call KeyExpand192; + {$IFDEF X64ASM} + movupd [rdx + 144], xmm0; + {$ELSE} + movupd [edx + 144], xmm0; + {$ENDIF} + movapd xmm4, xmm2; + + aeskeygenassist xmm1, xmm2, $40; + call KeyExpand192; + + shufpd xmm4, xmm0, 0; + {$IFDEF X64ASM} + movupd [rdx + 160], xmm4; + {$ELSE} + movupd [edx + 160], xmm4; + {$ENDIF} + + movapd xmm4, xmm0; + shufpd xmm4, xmm2, 1; + {$IFDEF X64ASM} + movupd [rdx + 176], xmm4; + {$ELSE} + movupd [edx + 176], xmm4; + {$ENDIF} + + aeskeygenassist xmm1, xmm2, $80; + call KeyExpand192; + {$IFDEF X64ASM} + movupd [rdx + 192], xmm0; + {$ELSE} + movupd [edx + 192], xmm0; + {$ENDIF} +end; + +// xmm0-xmm2 prefilled; xmm3/xmm4 intermediate. +procedure KeyExpand256_1; register; +asm + pshufd xmm1, xmm1, $FF; + movapd xmm3, xmm0; + pslldq xmm3, $04; + pxor xmm0, xmm3; + + pslldq xmm3, $04; + pxor xmm0, xmm3; + + pslldq xmm3, $04; + pxor xmm0, xmm3; + pxor xmm0, xmm1; +end; + +procedure KeyExpand256_2; register; +asm + aeskeygenassist xmm3, xmm0, $00; + pshufd xmm1, xmm3, $AA; + movapd xmm3, xmm2; + pslldq xmm3, $04; + pxor xmm2, xmm3; + pslldq xmm3, $04; + + pxor xmm2, xmm3; + pslldq xmm3, $04; + + pxor xmm2, xmm3; + pxor xmm2, xmm1; +end; + +procedure BuildAsmKey256(Key: PByte; Dest: PLongWord); register; +// eax/edx = key/dest / rcx/rdx = key/dest +asm + {$IFDEF X64ASM} + movupd xmm0, [rcx]; + movupd xmm2, [rcx + 16]; + + movupd [rdx], xmm0; + movupd [rdx + 16], xmm2; + + {$ELSE} + movupd xmm0, [eax]; + movupd xmm2, [eax + 16]; + + movupd [edx], xmm0; + movupd [edx + 16], xmm2; + {$ENDIF} + + aeskeygenassist xmm1, xmm2, $1; + call KeyExpand256_1; + {$IFDEF X64ASM} + movupd [rdx + 32], xmm0; + {$ELSE} + movupd [edx + 32], xmm0; + {$ENDIF} + call KeyExpand256_2; + {$IFDEF X64ASM} + movupd [rdx + 48], xmm2; + {$ELSE} + movupd [edx + 48], xmm2; + {$ENDIF} + + aeskeygenassist xmm1, xmm2, $2; + call KeyExpand256_1; + {$IFDEF X64ASM} + movupd [rdx + 64], xmm0; + {$ELSE} + movupd [edx + 64], xmm0; + {$ENDIF} + call KeyExpand256_2; + {$IFDEF X64ASM} + movupd [rdx + 80], xmm2; + {$ELSE} + movupd [edx + 80], xmm2; + {$ENDIF} + + aeskeygenassist xmm1, xmm2, $4; + call KeyExpand256_1; + {$IFDEF X64ASM} + movupd [rdx + 96], xmm0; + {$ELSE} + movupd [edx + 96], xmm0; + {$ENDIF} + call KeyExpand256_2; + {$IFDEF X64ASM} + movupd [rdx + 112], xmm2; + {$ELSE} + movupd [edx + 112], xmm2; + {$ENDIF} + + aeskeygenassist xmm1, xmm2, $8; + call KeyExpand256_1; + {$IFDEF X64ASM} + movupd [rdx + 128], xmm0; + {$ELSE} + movupd [edx + 128], xmm0; + {$ENDIF} + call KeyExpand256_2; + {$IFDEF X64ASM} + movupd [rdx + 144], xmm2; + {$ELSE} + movupd [edx + 144], xmm2; + {$ENDIF} + + aeskeygenassist xmm1, xmm2, $10; + call KeyExpand256_1; + {$IFDEF X64ASM} + movupd [rdx + 160], xmm0; + {$ELSE} + movupd [edx + 160], xmm0; + {$ENDIF} + call KeyExpand256_2; + {$IFDEF X64ASM} + movupd [rdx + 176], xmm2; + {$ELSE} + movupd [edx + 176], xmm2; + {$ENDIF} + + aeskeygenassist xmm1, xmm2, $20; + call KeyExpand256_1; + {$IFDEF X64ASM} + movupd [rdx + 192], xmm0; + {$ELSE} + movupd [edx + 192], xmm0; + {$ENDIF} + call KeyExpand256_2; + {$IFDEF X64ASM} + movupd [rdx + 208], xmm2; + {$ELSE} + movupd [edx + 208], xmm2; + {$ENDIF} + + // AES-256 has Nr=14 → 15 round keys (offsets 0..224). The final expansion + // only produces the last encode key at +224; a second half-key at +240 would + // be out of range and collide with the inverse schedule that follows. + aeskeygenassist xmm1, xmm2, $40; + call KeyExpand256_1; + {$IFDEF X64ASM} + movupd [rdx + 224], xmm0; + {$ELSE} + movupd [edx + 224], xmm0; + {$ENDIF} +end; + +/// +/// Build inverse (decode) round-key schedule with AESIMC, reverse order for +/// sequential AESDEC access. +/// +procedure BuildAsmDecodeKey(EncodeKey: PLongWord; DecodeKey: PLongWord; NumRounds: Integer); register; +// eax/edx/ecx = encode/decode/rounds / rcx/rdx/r8 = encode/decode/rounds +asm + {$IFDEF X64ASM} + movdqu xmm0, [rcx]; + lea rdx, [rdx + 8*r8]; + lea rdx, [rdx + 8*r8]; + movdqu [rdx], xmm0; + + add rcx, 16; + sub rdx, 16; + dec r8d; + + @decLoop: + movdqu xmm0, [rcx]; + aesimc xmm0, xmm0; + movdqu [rdx], xmm0; + + add rcx, 16; + sub rdx, 16; + dec r8d; + jg @decLoop; + + movdqu xmm0, [rcx]; + movdqu [rdx], xmm0; + {$ELSE} + movdqu xmm0, [eax]; + lea edx, [edx + 8*ecx]; + lea edx, [edx + 8*ecx]; + movdqu [edx], xmm0; + + add eax, 16; + sub edx, 16; + dec ecx; + + @decLoop: + movdqu xmm0, [eax]; + aesimc xmm0, xmm0; + movdqu [edx], xmm0; + + add eax, 16; + sub edx, 16; + + loop @decLoop; + + movdqu xmm0, [eax]; + movdqu [edx], xmm0; + {$ENDIF} +end; + +procedure AESEncode(Source, Dest: Pointer; NumRounds: Integer; Key: PLongWord); register; +asm + {$IFDEF X64ASM} + movupd xmm1, [rcx]; + movdqa xmm2, [r9]; + pxor xmm1, xmm2; + add r9, 16; + + dec r8d; + + @aesloop: + movdqa xmm2, [r9]; + aesenc xmm1, xmm2; + add r9, 16; + dec r8d; + jg @aesloop; + + movdqa xmm2, [r9]; + aesenclast xmm1, xmm2; + movupd [rdx], xmm1; + {$ELSE} + push edi; + + movupd xmm1, [eax]; + mov edi, key; + movdqa xmm2, [edi]; + pxor xmm1, xmm2; + add edi, 16; + + dec ecx; + + @aesloop: + movdqa xmm2, [edi]; + aesenc xmm1, xmm2; + add edi, 16; + + dec ecx; + jg @aesloop; + + movdqa xmm2, [edi]; + aesenclast xmm1, xmm2; + movupd [edx], xmm1; + pop edi; + {$ENDIF} +end; + +procedure AESDecode(Source, Dest: Pointer; NumRounds: Integer; Key: PLongWord); register; +asm + {$IFDEF X64ASM} + movupd xmm1, [rcx]; + movdqa xmm2, [r9]; + pxor xmm1, xmm2; + add r9, 16; + + dec r8d; + + @aesloop: + movdqa xmm2, [r9]; + aesdec xmm1, xmm2; + add r9, 16; + + dec r8d; + jg @aesloop; + + movdqa xmm2, [r9]; + aesdeclast xmm1, xmm2; + movupd [rdx], xmm1; + {$ELSE} + push edi; + + movupd xmm1, [eax]; + mov edi, key; + movdqa xmm2, [edi]; + pxor xmm1, xmm2; + add edi, 16; + + dec ecx; + + @aesloop: + movdqa xmm2, [edi]; + aesdec xmm1, xmm2; + add edi, 16; + + dec ecx; + jg @aesloop; + + movdqa xmm2, [edi]; + aesdeclast xmm1, xmm2; + movupd [edx], xmm1; + pop edi; + {$ENDIF} +end; + +{$IFEND} + +procedure TCipher_Rijndael.DoInit(const Key; Size: Integer); +{$IF defined(X86ASM) or defined(X64ASM)} +var + pBuf: PUInt32Array; +{$IFEND} begin if Size <= 16 then FRounds := 10 @@ -2943,10 +3382,35 @@ procedure TCipher_Rijndael.DoInit(const Key; Size: Integer); FRounds := 12 else FRounds := 14; - FillChar(FAdditionalBuffer^, 32, 0); - Move(Key, FAdditionalBuffer^, Size); - BuildEncodeKey(Size); - BuildDecodeKey; + + FAESAsmActive := False; + {$IF defined(X86ASM) or defined(X64ASM)} + // AES-NI key expand only for standard AES key lengths (16/24/32). + // Non-standard Rijndael sizes always use pure Pascal. + if UseAESAsm and TDEC_CPUSupport.AES and + ((Size = 16) or (Size = 24) or (Size = 32)) then + begin + pBuf := AlignPtr32(FAdditionalBuffer); + case Size of + 16: BuildAsmKey128(@Key, PLongWord(pBuf)); + 24: BuildAsmKey192(@Key, PLongWord(pBuf)); + 32: BuildAsmKey256(@Key, PLongWord(pBuf)); + end; + // Inverse decode schedule via AESIMC, placed immediately after the + // (FRounds+1) encode round keys so AES-256's 240-byte encode schedule + // does not overlap the decode region (was wrongly using max Rijndael_Rounds). + BuildAsmDecodeKey(PLongWord(pBuf), + @(pBuf^[(FRounds + 1) * Rijndael_Blocks]), FRounds); + FAESAsmActive := True; + end + else + {$IFEND} + begin + FillChar(FAdditionalBuffer^, 32, 0); + Move(Key, FAdditionalBuffer^, Size); + BuildEncodeKey(Size); + BuildDecodeKey; + end; inherited; end; @@ -3047,6 +3511,14 @@ procedure TCipher_Rijndael.DoEncode(Source, Dest: Pointer; Size: Integer); begin Assert(Size = Context.BlockSize); + {$IF defined(X86ASM) or defined(X64ASM)} + if FAESAsmActive then + begin + AESEncode(Source, Dest, FRounds, AlignPtr32(FAdditionalBuffer)); + Exit; + end; + {$IFEND} + P := FAdditionalBuffer; A1 := PUInt32Array(Source)[0]; B1 := PUInt32Array(Source)[1]; @@ -3112,6 +3584,15 @@ procedure TCipher_Rijndael.DoDecode(Source, Dest: Pointer; Size: Integer); begin Assert(Size = Context.BlockSize); + {$IF defined(X86ASM) or defined(X64ASM)} + if FAESAsmActive then + begin + AESDecode(Source, Dest, FRounds, + @(PUInt32Array(AlignPtr32(FAdditionalBuffer))^[(FRounds + 1) * Rijndael_Blocks])); + Exit; + end; + {$IFEND} + P := Pointer(PByte(FAdditionalBuffer) + FAdditionalBufferSize shr 1 + FRounds * 16); // for Pointer Math A1 := PUInt32Array(Source)[0]; B1 := PUInt32Array(Source)[1]; @@ -3181,7 +3662,7 @@ class function TCipher_AES128.Context: TCipherContext; Result.KeySize := 16; Result.BlockSize := Rijndael_Blocks * 4; Result.BufferSize := Rijndael_Blocks * 4; - Result.AdditionalBufferSize := (Rijndael_Rounds + 1) * Rijndael_Blocks * SizeOf(UInt32) * 2; + Result.AdditionalBufferSize := (Rijndael_Rounds + 1) * Rijndael_Blocks * SizeOf(UInt32) * 2 + $20; Result.NeedsAdditionalBufferBackup := False; Result.MinRounds := 1; Result.MaxRounds := 1; @@ -3214,7 +3695,7 @@ class function TCipher_AES192.Context: TCipherContext; Result.KeySize := 24; Result.BlockSize := Rijndael_Blocks * 4; Result.BufferSize := Rijndael_Blocks * 4; - Result.AdditionalBufferSize := (Rijndael_Rounds + 1) * Rijndael_Blocks * SizeOf(UInt32) * 2; + Result.AdditionalBufferSize := (Rijndael_Rounds + 1) * Rijndael_Blocks * SizeOf(UInt32) * 2 + $20; Result.NeedsAdditionalBufferBackup := False; Result.MinRounds := 1; Result.MaxRounds := 1; @@ -3247,7 +3728,7 @@ class function TCipher_AES256.Context: TCipherContext; Result.KeySize := 32; Result.BlockSize := Rijndael_Blocks * 4; Result.BufferSize := Rijndael_Blocks * 4; - Result.AdditionalBufferSize := (Rijndael_Rounds + 1) * Rijndael_Blocks * SizeOf(UInt32) * 2; + Result.AdditionalBufferSize := (Rijndael_Rounds + 1) * Rijndael_Blocks * SizeOf(UInt32) * 2 + $20; Result.NeedsAdditionalBufferBackup := False; Result.MinRounds := 1; Result.MaxRounds := 1; @@ -7942,6 +8423,15 @@ initialization // Prefer pure Pascal until SIMD kernels are proven across targets. TCipher_ChaCha20.CpuMode := cmPas; + // AES-NI: auto-enable only when ASM path is compiled and CPU reports AES-NI. + // Never required for correctness; override with TCipher_Rijndael.UseAESAsm := False. + TCipher_Rijndael.UseAESAsm := + {$IF defined(X86ASM) or defined(X64ASM)} + TDEC_CPUSupport.AES + {$ELSE} + False + {$IFEND}; + {$IFNDEF ManualRegisterCipherClasses} TCipher_Null.RegisterClass(TDECCipher.ClassList); TCipher_Blowfish.RegisterClass(TDECCipher.ClassList); diff --git a/Unit Tests/DECDUnitTestSuite.dpr b/Unit Tests/DECDUnitTestSuite.dpr index 9e908aa..10dc17a 100644 --- a/Unit Tests/DECDUnitTestSuite.dpr +++ b/Unit Tests/DECDUnitTestSuite.dpr @@ -44,7 +44,8 @@ uses TestDECCipherPaddings in 'Tests\TestDECCipherPaddings.pas', TestDECCipherModesCCM in 'Tests\TestDECCipherModesCCM.pas', AuthenticatedCiphersCommonTestData in 'Tests\AuthenticatedCiphersCommonTestData.pas', - TestDECChaChaPoly1305 in 'Tests\TestDECChaChaPoly1305.pas'; + TestDECChaChaPoly1305 in 'Tests\TestDECChaChaPoly1305.pas', + TestDECAESNI in 'Tests\TestDECAESNI.pas'; {$R *.RES} diff --git a/Unit Tests/DECDUnitTestSuite.dproj b/Unit Tests/DECDUnitTestSuite.dproj index 458c34f..bbe495d 100644 --- a/Unit Tests/DECDUnitTestSuite.dproj +++ b/Unit Tests/DECDUnitTestSuite.dproj @@ -166,6 +166,7 @@ + Base diff --git a/Unit Tests/Tests/TestDECAESNI.pas b/Unit Tests/Tests/TestDECAESNI.pas new file mode 100644 index 0000000..409f8b2 --- /dev/null +++ b/Unit Tests/Tests/TestDECAESNI.pas @@ -0,0 +1,192 @@ +{***************************************************************************** + The DEC team (see file NOTICE.txt) licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. A copy of this licence is found in the root directory of + this project in the file LICENCE.txt or alternatively at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*****************************************************************************} + +{$M+} +unit TestDECAESNI; + +interface + +{$INCLUDE TestDefines.inc} + +uses + System.SysUtils, System.Classes, + {$IFDEF DUnitX} + DUnitX.TestFramework, DUnitX.DUnitCompatibility, + {$ELSE} + TestFramework, + {$ENDIF} + DECCipherBase, DECCipherFormats, DECCiphers, DECTypes, DECFormat, DECCPUSupport; + +type + /// + /// FIPS-197 single-block ECB KATs for AES-128/192/256, comparing pure + /// Pascal and AES-NI paths when the CPU supports AES-NI. + /// + {$IFDEF DUnitX} [TestFixture] {$ENDIF} + TestAESNI = class(TTestCase) + private + function HexToBytes(const AHex: string): TBytes; + procedure RoundTripECB(ACipherClass: TDECCipherClass; + const AKeyHex, APlainHex, ACipherHex: string; AUseAsm: Boolean); + procedure ComparePasVsAsm(ACipherClass: TDECCipherClass; + const AKeyHex, APlainHex, ACipherHex: string); + published + procedure TestAES128_FIPS197_PAS; + procedure TestAES192_FIPS197_PAS; + procedure TestAES256_FIPS197_PAS; + procedure TestAES128_PAS_vs_AESNI; + procedure TestAES192_PAS_vs_AESNI; + procedure TestAES256_PAS_vs_AESNI; + procedure TestUseAESAsmDefaultMatchesCPU; + end; + +implementation + +{ TestAESNI } + +function TestAESNI.HexToBytes(const AHex: string): TBytes; +begin + Result := BytesOf(TFormat_HexL.Decode(RawByteString(LowerCase(AHex)))); +end; + +procedure TestAESNI.RoundTripECB(ACipherClass: TDECCipherClass; + const AKeyHex, APlainHex, ACipherHex: string; AUseAsm: Boolean); +var + LCipher: TDECFormattedCipher; + LKey, LPlain, LExpected, LOut, LDec, LIV: TBytes; + LPrev: Boolean; +begin + LPrev := TCipher_Rijndael.UseAESAsm; + try + TCipher_Rijndael.UseAESAsm := AUseAsm; + + LKey := HexToBytes(AKeyHex); + LPlain := HexToBytes(APlainHex); + LExpected := HexToBytes(ACipherHex); + SetLength(LIV, 0); + + LCipher := ACipherClass.Create as TDECFormattedCipher; + try + LCipher.Mode := cmECBx; + LCipher.Init(LKey, LIV, 0); + LOut := LCipher.EncodeBytes(LPlain); + CheckEquals(Length(LExpected), Length(LOut)); + CheckEquals(True, CompareMem(@LExpected[0], @LOut[0], Length(LExpected)), + 'encrypt mismatch UseAESAsm=' + BoolToStr(AUseAsm) + + ' CPU.AES=' + BoolToStr(TDEC_CPUSupport.AES)); + + LCipher.Init(LKey, LIV, 0); + LDec := LCipher.DecodeBytes(LOut); + CheckEquals(Length(LPlain), Length(LDec)); + CheckEquals(True, CompareMem(@LPlain[0], @LDec[0], Length(LPlain)), + 'decrypt mismatch UseAESAsm=' + BoolToStr(AUseAsm)); + finally + LCipher.Free; + end; + finally + TCipher_Rijndael.UseAESAsm := LPrev; + end; +end; + +procedure TestAESNI.ComparePasVsAsm(ACipherClass: TDECCipherClass; + const AKeyHex, APlainHex, ACipherHex: string); +begin + // Always verify pure Pascal against FIPS-197. + RoundTripECB(ACipherClass, AKeyHex, APlainHex, ACipherHex, False); + + // When AES-NI is available (and ASM compiled), force it and re-check. + // When not available, UseAESAsm=True still selects PAS via FAESAsmActive. + RoundTripECB(ACipherClass, AKeyHex, APlainHex, ACipherHex, True); +end; + +procedure TestAESNI.TestAES128_FIPS197_PAS; +// FIPS-197 Appendix C.1 +begin + RoundTripECB(TCipher_AES128, + '000102030405060708090a0b0c0d0e0f', + '00112233445566778899aabbccddeeff', + '69c4e0d86a7b0430d8cdb78070b4c55a', + False); +end; + +procedure TestAESNI.TestAES192_FIPS197_PAS; +// FIPS-197 Appendix C.2 +begin + RoundTripECB(TCipher_AES192, + '000102030405060708090a0b0c0d0e0f1011121314151617', + '00112233445566778899aabbccddeeff', + 'dda97ca4864cdfe06eaf70a0ec0d7191', + False); +end; + +procedure TestAESNI.TestAES256_FIPS197_PAS; +// FIPS-197 Appendix C.3 +begin + RoundTripECB(TCipher_AES256, + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', + '00112233445566778899aabbccddeeff', + '8ea2b7ca516745bfeafc49904b496089', + False); +end; + +procedure TestAESNI.TestAES128_PAS_vs_AESNI; +begin + ComparePasVsAsm(TCipher_AES128, + '000102030405060708090a0b0c0d0e0f', + '00112233445566778899aabbccddeeff', + '69c4e0d86a7b0430d8cdb78070b4c55a'); +end; + +procedure TestAESNI.TestAES192_PAS_vs_AESNI; +begin + ComparePasVsAsm(TCipher_AES192, + '000102030405060708090a0b0c0d0e0f1011121314151617', + '00112233445566778899aabbccddeeff', + 'dda97ca4864cdfe06eaf70a0ec0d7191'); +end; + +procedure TestAESNI.TestAES256_PAS_vs_AESNI; +begin + ComparePasVsAsm(TCipher_AES256, + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', + '00112233445566778899aabbccddeeff', + '8ea2b7ca516745bfeafc49904b496089'); +end; + +procedure TestAESNI.TestUseAESAsmDefaultMatchesCPU; +begin + // Re-apply unit-init rule and verify. + {$IF defined(X86ASM) or defined(X64ASM)} + TCipher_Rijndael.UseAESAsm := TDEC_CPUSupport.AES; + CheckEquals(True, TDEC_CPUSupport.AES = TCipher_Rijndael.UseAESAsm, + 'UseAESAsm should follow TDEC_CPUSupport.AES when ASM is compiled'); + {$ELSE} + TCipher_Rijndael.UseAESAsm := False; + CheckEquals(False, TCipher_Rijndael.UseAESAsm, + 'UseAESAsm must be False when ASM path is not compiled'); + {$IFEND} +end; + +initialization + // Register any test cases with the test runner + {$IFDEF DUnitX} + TDUnitX.RegisterTestFixture(TestAESNI); + {$ELSE} + RegisterTests('DECCiphers', [TestAESNI.Suite]); + {$ENDIF} + +end. diff --git a/Unit Tests/Tests/TestDECCipher.pas b/Unit Tests/Tests/TestDECCipher.pas index fc1e4ac..f2d592c 100644 --- a/Unit Tests/Tests/TestDECCipher.pas +++ b/Unit Tests/Tests/TestDECCipher.pas @@ -1,4 +1,4 @@ -{***************************************************************************** +{***************************************************************************** The DEC team (see file NOTICE.txt) licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance @@ -4324,8 +4324,8 @@ procedure TCipherBasis.DoTestDecode(DecodeFunct: TEncodeDecodeFunc; InitProc: TI { TODO : Das Problem ist hier: dass wir zu low level testen, da die bisherigen Textvektoren ja immer von einem bestimmten CipherModus ausgehen, und nicht die -einzelnen DoEncode/DoDecode primitive. Diese sind später zu testen, wenn -wir die bisherigen Vektoren testen können. Dann können wir die nötigen +einzelnen DoEncode/DoDecode primitive. Diese sind sp�ter zu testen, wenn +wir die bisherigen Vektoren testen k�nnen. Dann k�nnen wir die n�tigen Daten synthetisieren. } for Data in FTestData do begin @@ -4349,8 +4349,8 @@ procedure TCipherBasis.DoTestEncode(EncodeFunc: TEncodeDecodeFunc; InitProc: TIn { TODO : Das Problem ist hier: dass wir zu low level testen, da die bisherigen Testvektoren ja immer von einem bestimmten CipherModus ausgehen, und nicht die -einzelnen DoEncode/DoDecode primitive. Diese sind später zu testen, wenn -wir die bisherigen Vektoren testen können. Dann können wir die nötigen +einzelnen DoEncode/DoDecode primitive. Diese sind sp�ter zu testen, wenn +wir die bisherigen Vektoren testen k�nnen. Dann k�nnen wir die n�tigen Daten synthetisieren. } for Data in FTestData do begin @@ -4798,7 +4798,8 @@ procedure TestTCipher_AES.TestContext; CheckEquals( 32, ReturnValue.KeySize); CheckEquals( 16, ReturnValue.BlockSize); CheckEquals( 16, ReturnValue.BufferSize); - CheckEquals( 480, ReturnValue.AdditionalBufferSize); + // 480 schedule halves + $20 AES-NI AlignPtr32 spare + CheckEquals( 512, ReturnValue.AdditionalBufferSize); CheckEquals( 1, ReturnValue.MinRounds); CheckEquals( 1, ReturnValue.MaxRounds); CheckEquals(false, ReturnValue.NeedsAdditionalBufferBackup); @@ -4891,7 +4892,8 @@ procedure TestTCipher_Rijndael.TestContext; CheckEquals( 32, ReturnValue.KeySize); CheckEquals( 16, ReturnValue.BlockSize); CheckEquals( 16, ReturnValue.BufferSize); - CheckEquals( 480, ReturnValue.AdditionalBufferSize); + // 480 schedule halves + $20 AES-NI AlignPtr32 spare + CheckEquals( 512, ReturnValue.AdditionalBufferSize); CheckEquals( 1, ReturnValue.MinRounds); CheckEquals( 1, ReturnValue.MaxRounds); CheckEquals(false, ReturnValue.NeedsAdditionalBufferBackup); @@ -5009,7 +5011,7 @@ procedure TestTCipher_AES128.TestContext; CheckEquals( 16, ReturnValue.KeySize); CheckEquals( 16, ReturnValue.BlockSize); CheckEquals( 16, ReturnValue.BufferSize); - CheckEquals( 480, ReturnValue.AdditionalBufferSize); + CheckEquals( 512, ReturnValue.AdditionalBufferSize); CheckEquals( 1, ReturnValue.MinRounds); CheckEquals( 1, ReturnValue.MaxRounds); CheckEquals(false, ReturnValue.NeedsAdditionalBufferBackup); @@ -5124,7 +5126,7 @@ procedure TestTCipher_AES192.TestContext; CheckEquals( 24, ReturnValue.KeySize); CheckEquals( 16, ReturnValue.BlockSize); CheckEquals( 16, ReturnValue.BufferSize); - CheckEquals( 480, ReturnValue.AdditionalBufferSize); + CheckEquals( 512, ReturnValue.AdditionalBufferSize); CheckEquals( 1, ReturnValue.MinRounds); CheckEquals( 1, ReturnValue.MaxRounds); CheckEquals(false, ReturnValue.NeedsAdditionalBufferBackup); @@ -5241,7 +5243,7 @@ procedure TestTCipher_AES256.TestContext; CheckEquals( 32, ReturnValue.KeySize); CheckEquals( 16, ReturnValue.BlockSize); CheckEquals( 16, ReturnValue.BufferSize); - CheckEquals( 480, ReturnValue.AdditionalBufferSize); + CheckEquals( 512, ReturnValue.AdditionalBufferSize); CheckEquals( 1, ReturnValue.MinRounds); CheckEquals( 1, ReturnValue.MaxRounds); CheckEquals(false, ReturnValue.NeedsAdditionalBufferBackup); @@ -5340,7 +5342,7 @@ procedure TestTCipher_AES256_CBC_PKCS7.TestContext; CheckEquals( 32, ReturnValue.KeySize); CheckEquals( 16, ReturnValue.BlockSize); CheckEquals( 16, ReturnValue.BufferSize); - CheckEquals( 480, ReturnValue.AdditionalBufferSize); + CheckEquals( 512, ReturnValue.AdditionalBufferSize); CheckEquals( 1, ReturnValue.MinRounds); CheckEquals( 1, ReturnValue.MaxRounds); CheckEquals(false, ReturnValue.NeedsAdditionalBufferBackup);