Some checks failed
Build and Release / build-and-release (push) Has been cancelled
- Adopt standard versioning: 1.0, 1.1, 1.1-1 (major.minor-revision) - Archive naming: comet-1.1-1-linux-x64.tar.gz (professional format) - Move test sources from src/ to test/ directory - Update Makefile, release script, and CI workflow to match
248 lines
6.3 KiB
ObjectPascal
248 lines
6.3 KiB
ObjectPascal
{
|
|
test_crypt.pas - Test X25519 key exchange and ChaCha20 encryption
|
|
}
|
|
program test_crypt;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
uses
|
|
SysUtils, cometcrypt, cometed25519;
|
|
|
|
var
|
|
TestCount, PassCount: Integer;
|
|
|
|
procedure Check(const Name: string; Condition: Boolean);
|
|
begin
|
|
Inc(TestCount);
|
|
if Condition then
|
|
begin
|
|
Inc(PassCount);
|
|
WriteLn(Name, ' OK');
|
|
end
|
|
else
|
|
WriteLn(Name, ' FAIL');
|
|
end;
|
|
|
|
procedure TestChaCha20;
|
|
var
|
|
S1, S2: TChacha20State;
|
|
Key: array[0..31] of Byte;
|
|
Original, Data: array[0..999] of Byte;
|
|
Cipher1: array[0..999] of Byte;
|
|
BigData: array[0..65535] of Byte;
|
|
BigOrig: array[0..65535] of Byte;
|
|
I: Integer;
|
|
AllMatch: Boolean;
|
|
begin
|
|
WriteLn('--- ChaCha20 ---');
|
|
|
|
{ Create a test key }
|
|
for I := 0 to 31 do
|
|
Key[I] := Byte(I * 7 + 3);
|
|
|
|
{ Create test data }
|
|
for I := 0 to 999 do
|
|
Original[I] := Byte(I * 13 + 42);
|
|
Move(Original, Data, 1000);
|
|
|
|
{ Encrypt }
|
|
ChaCha20Init(S1, Key);
|
|
ChaCha20Crypt(S1, @Data[0], 1000);
|
|
|
|
{ Verify ciphertext differs from plaintext }
|
|
AllMatch := True;
|
|
for I := 0 to 999 do
|
|
if Data[I] <> Original[I] then AllMatch := False;
|
|
Check('ChaCha20: ciphertext differs', not AllMatch);
|
|
|
|
{ Decrypt with same key }
|
|
ChaCha20Init(S2, Key);
|
|
ChaCha20Crypt(S2, @Data[0], 1000);
|
|
|
|
{ Verify plaintext recovered }
|
|
AllMatch := True;
|
|
for I := 0 to 999 do
|
|
if Data[I] <> Original[I] then AllMatch := False;
|
|
Check('ChaCha20: decrypt recovers plaintext', AllMatch);
|
|
|
|
{ Test incremental: encrypt in chunks matches encrypt all-at-once }
|
|
Move(Original, Data, 1000);
|
|
ChaCha20Init(S1, Key);
|
|
ChaCha20Crypt(S1, @Data[0], 1000);
|
|
Move(Data, Cipher1, 1000);
|
|
|
|
{ Encrypt same data in small chunks }
|
|
Move(Original, Data, 1000);
|
|
ChaCha20Init(S2, Key);
|
|
ChaCha20Crypt(S2, @Data[0], 100);
|
|
ChaCha20Crypt(S2, @Data[100], 300);
|
|
ChaCha20Crypt(S2, @Data[400], 600);
|
|
|
|
AllMatch := True;
|
|
for I := 0 to 999 do
|
|
if Data[I] <> Cipher1[I] then AllMatch := False;
|
|
Check('ChaCha20: chunked matches whole', AllMatch);
|
|
|
|
{ Large block test - 64KB }
|
|
for I := 0 to 65535 do
|
|
BigOrig[I] := Byte(I xor (I shr 8));
|
|
Move(BigOrig, BigData, 65536);
|
|
|
|
ChaCha20Init(S1, Key);
|
|
ChaCha20Crypt(S1, @BigData[0], 65536);
|
|
|
|
ChaCha20Init(S2, Key);
|
|
ChaCha20Crypt(S2, @BigData[0], 65536);
|
|
|
|
AllMatch := True;
|
|
for I := 0 to 65535 do
|
|
if BigData[I] <> BigOrig[I] then AllMatch := False;
|
|
Check('ChaCha20: 64KB round-trip', AllMatch);
|
|
end;
|
|
|
|
procedure TestX25519;
|
|
var
|
|
PrivA, PubA, PrivB, PubB: TX25519Key;
|
|
SecretA, SecretB: TX25519Key;
|
|
I: Integer;
|
|
Match, NonZeroA, NonZeroB, Differ, NonZeroS: Boolean;
|
|
begin
|
|
WriteLn('--- X25519 ---');
|
|
|
|
{ Generate two keypairs }
|
|
X25519Keypair(PrivA, PubA);
|
|
X25519Keypair(PrivB, PubB);
|
|
|
|
{ Keys should not be all zeros }
|
|
NonZeroA := False;
|
|
NonZeroB := False;
|
|
for I := 0 to 31 do
|
|
begin
|
|
if PubA[I] <> 0 then NonZeroA := True;
|
|
if PubB[I] <> 0 then NonZeroB := True;
|
|
end;
|
|
Check('X25519: pubkey A non-zero', NonZeroA);
|
|
Check('X25519: pubkey B non-zero', NonZeroB);
|
|
|
|
{ Keys should differ }
|
|
Differ := False;
|
|
for I := 0 to 31 do
|
|
if PubA[I] <> PubB[I] then Differ := True;
|
|
Check('X25519: pubkeys differ', Differ);
|
|
|
|
{ Diffie-Hellman: both sides compute same shared secret }
|
|
Check('X25519: DH A succeeds', X25519SharedSecret(PrivA, PubB, SecretA));
|
|
Check('X25519: DH B succeeds', X25519SharedSecret(PrivB, PubA, SecretB));
|
|
|
|
Match := True;
|
|
for I := 0 to 31 do
|
|
if SecretA[I] <> SecretB[I] then Match := False;
|
|
Check('X25519: shared secrets match', Match);
|
|
|
|
{ Shared secret should not be all zeros }
|
|
NonZeroS := False;
|
|
for I := 0 to 31 do
|
|
if SecretA[I] <> 0 then NonZeroS := True;
|
|
Check('X25519: shared secret non-zero', NonZeroS);
|
|
end;
|
|
|
|
procedure TestSessionCrypt;
|
|
var
|
|
PrivA, PubA, PrivB, PubB, SecretA, SecretB: TX25519Key;
|
|
CryptOrig, CryptAnsw: TCometCrypt;
|
|
Data, Original: array[0..511] of Byte;
|
|
I: Integer;
|
|
AllMatch: Boolean;
|
|
begin
|
|
WriteLn('--- Session Encryption ---');
|
|
|
|
{ Simulate key exchange }
|
|
X25519Keypair(PrivA, PubA);
|
|
X25519Keypair(PrivB, PubB);
|
|
X25519SharedSecret(PrivA, PubB, SecretA);
|
|
X25519SharedSecret(PrivB, PubA, SecretB);
|
|
|
|
{ Initialize both sides }
|
|
CometCryptInit(CryptOrig, SecretA, PubA, PubB, True); { originator }
|
|
CometCryptInit(CryptAnsw, SecretB, PubB, PubA, False); { answerer }
|
|
|
|
Check('Session: originator active', CryptOrig.Active);
|
|
Check('Session: answerer active', CryptAnsw.Active);
|
|
|
|
{ Originator sends frame to answerer }
|
|
for I := 0 to 511 do
|
|
Original[I] := Byte(I * 3 + 17);
|
|
Move(Original, Data, 512);
|
|
|
|
CometCryptEncrypt(CryptOrig, @Data[0], 512);
|
|
|
|
{ Verify encrypted }
|
|
AllMatch := True;
|
|
for I := 0 to 511 do
|
|
if Data[I] <> Original[I] then AllMatch := False;
|
|
Check('Session: originator->answerer encrypted', not AllMatch);
|
|
|
|
{ Answerer decrypts }
|
|
CometCryptDecrypt(CryptAnsw, @Data[0], 512);
|
|
|
|
AllMatch := True;
|
|
for I := 0 to 511 do
|
|
if Data[I] <> Original[I] then AllMatch := False;
|
|
Check('Session: answerer decrypts correctly', AllMatch);
|
|
|
|
{ Answerer sends frame to originator }
|
|
for I := 0 to 511 do
|
|
Original[I] := Byte(I * 11 + 99);
|
|
Move(Original, Data, 512);
|
|
|
|
CometCryptEncrypt(CryptAnsw, @Data[0], 512);
|
|
CometCryptDecrypt(CryptOrig, @Data[0], 512);
|
|
|
|
AllMatch := True;
|
|
for I := 0 to 511 do
|
|
if Data[I] <> Original[I] then AllMatch := False;
|
|
Check('Session: answerer->originator round-trip', AllMatch);
|
|
|
|
{ Multiple frames in sequence - counters advance correctly }
|
|
for I := 0 to 511 do
|
|
Original[I] := Byte(I);
|
|
Move(Original, Data, 512);
|
|
|
|
CometCryptEncrypt(CryptOrig, @Data[0], 512);
|
|
CometCryptDecrypt(CryptAnsw, @Data[0], 512);
|
|
|
|
AllMatch := True;
|
|
for I := 0 to 511 do
|
|
if Data[I] <> Original[I] then AllMatch := False;
|
|
Check('Session: second frame round-trip', AllMatch);
|
|
|
|
{ Third frame }
|
|
Move(Original, Data, 512);
|
|
CometCryptEncrypt(CryptOrig, @Data[0], 512);
|
|
CometCryptDecrypt(CryptAnsw, @Data[0], 512);
|
|
|
|
AllMatch := True;
|
|
for I := 0 to 511 do
|
|
if Data[I] <> Original[I] then AllMatch := False;
|
|
Check('Session: third frame round-trip', AllMatch);
|
|
end;
|
|
|
|
begin
|
|
TestCount := 0;
|
|
PassCount := 0;
|
|
|
|
TestChaCha20;
|
|
TestX25519;
|
|
TestSessionCrypt;
|
|
|
|
WriteLn;
|
|
WriteLn(PassCount, '/', TestCount, ' tests passed.');
|
|
if PassCount = TestCount then
|
|
WriteLn('All crypto tests passed.')
|
|
else
|
|
begin
|
|
WriteLn('SOME TESTS FAILED');
|
|
Halt(1);
|
|
end;
|
|
end.
|