1 Commits

Author SHA1 Message Date
1408d94d99 v0.3.0: vendor cr.x25519 + cr.chacha20 from fpc-crypto v0.2.0
cm.crypto.pas drops its in-tree X25519 Montgomery ladder and
ChaCha20 RFC 8439 implementation, keeping only the Comet-
specific session glue on top: SHA-256 key derivation,
tx/rx ChaCha20 pair, CometCryptInit / Encrypt / Decrypt.

Primitives now live in fpc-crypto 0.2.0:
  TX25519Key + X25519Keypair/ScalarMult/SharedSecret  -> cr.x25519
  TChacha20State + ChaCha20Init/Crypt                 -> cr.chacha20

cm.driver.pas imports cr.x25519 explicitly (no re-export
gymnastics through cm.crypto) so its dependency graph
matches reality.  Same rationale as v0.2.0's treatment of
cr.ed25519 callers.

12/12 local tests PASS.  6-target cross-build (incl.
i386-go32v2) green.  Wire protocol unchanged -- the session-
layer key-derivation and frame encryption produce
byte-identical output to v0.2.0.
2026-04-24 12:08:19 -07:00
8 changed files with 428 additions and 319 deletions

View File

@@ -10,6 +10,40 @@ Semver intent:
- **minor** — additive features, new hooks, new capability flags
- **patch** — bug fixes, security hardening, internal perf
## 0.3.0 — 2026-04-24
### Changed
- **X25519 + ChaCha20 primitives moved out to fpc-crypto
v0.2.0.** `cm.crypto.pas` thins down to the
Comet-specific session glue (SHA-256 key derivation +
paired tx/rx ChaCha20 streams keyed per direction).
Pure crypto primitives now live upstream:
- `TX25519Key`, `X25519Keypair`, `X25519ScalarMult`,
`X25519SharedSecret``cr.x25519`
- `TChacha20State`, `ChaCha20Init`, `ChaCha20Crypt`
`cr.chacha20`
`cm.driver.pas` gains `cr.x25519` in its uses-clause so
`TX25519Key` + `X25519Keypair` / `X25519SharedSecret`
resolve directly from the primitive library instead of
being re-exported through `cm.crypto`.
- Rationale: X25519 and ChaCha20 are crypto; they belong
in the crypto library regardless of consumer count.
The "wait for a second consumer" rule that delayed their
carve in v0.1.0 / v0.2.0 was appropriate for MD5 (which
was duplicated code across three libraries) but doesn't
serve primitives like X25519 / ChaCha20 where upstream
ownership is the obviously-correct home.
### Not changed
- `TCometCrypt` record still owns the paired ChaCha20
states and the SHA-256-chained key derivation -- that's
Comet session-layer behaviour, not a primitive.
- Wire protocol is identical to 0.2.0. Same test vectors,
same interop behaviour. 12/12 local test suite green
pre- and post-migration; 6-target cross-build clean.
## 0.2.0 — 2026-04-23
### Changed

View File

@@ -35,6 +35,8 @@ UNITS=(
src/cr.ed25519.sc.pas
src/cr.ed25519.bp.pas
src/cr.ed25519.ge.pas
src/cr.x25519.pas
src/cr.chacha20.pas
src/cm.crypto.pas
src/cm.zlib.pas
src/cm.cram.pas

View File

@@ -1,26 +1,30 @@
{
Comet - Direct TCP File Transfer for FidoNet
cometcrypt.pas - Session encryption: X25519 key exchange + ChaCha20
cm.crypto.pas - Session encryption: Comet-specific glue on
top of cr.x25519 + cr.chacha20 + cr.sha
Provides full session encryption for the Comet native protocol using
keys already configured for ED25519 authentication. No certificates,
no external libraries, no CA infrastructure required.
Key exchange:
Both sides generate ephemeral X25519 keypairs and exchange public
keys. The shared secret is computed via Diffie-Hellman. Ephemeral
keys provide forward secrecy -- compromising long-term keys does
not reveal past session content.
Encryption:
ChaCha20 stream cipher (RFC 8439) with per-direction keys and
counters. Each frame body (TYPE+SEQ+PAYLOAD+CRC32) is encrypted
after CRC computation and decrypted before CRC verification.
This unit is the session-layer wrapper that:
1. Takes an X25519 shared secret + both ephemeral
public keys (produced by cr.x25519) and derives a
pair of SHA-256-chained directional keys,
2. Keys two cr.chacha20 streams (send / receive), one
per direction, into a TCometCrypt record, and
3. Exposes CometCryptEncrypt / CometCryptDecrypt so
the frame encoder / decoder can XOR frame bodies
on the wire.
Key derivation:
session_key = SHA-256(shared_secret || ephemeral_pub_A || ephemeral_pub_B)
send_key = SHA-256(session_key || "comet-send-" || direction_byte)
recv_key = SHA-256(session_key || "comet-recv-" || direction_byte)
session_key = SHA-256(shared_secret || pub_A || pub_B)
(pub_A = originator's, pub_B = answerer's;
both sides hash in the same order)
Key-A = SHA-256(session_key || 'A')
Key-B = SHA-256(session_key || 'B')
Originator sends with Key-A, receives with Key-B.
Answerer sends with Key-B, receives with Key-A.
The X25519 key-exchange + ChaCha20 stream primitives
themselves live in fpc-crypto (cr.x25519 / cr.chacha20)
as of fpc-comet 0.3.0 / fpc-crypto 0.2.0.
Copyright (C) 2026 Ken Johnson
License: GPL-2.0
@@ -33,62 +37,21 @@ unit cm.crypto;
interface
uses
SysUtils, cr.ed25519, cr.sha;
SysUtils, cr.sha, cr.x25519, cr.chacha20;
type
{ X25519 key types }
TX25519Key = array[0..31] of Byte;
{ ChaCha20 cipher state }
TChacha20State = record
Key: array[0..31] of Byte; { 256-bit key }
Nonce: array[0..11] of Byte; { 96-bit nonce (zeroed, counter-based) }
Counter: QWord; { Block counter }
Block: array[0..63] of Byte; { Current keystream block }
Used: Integer; { Bytes used in current block }
end;
{ Session encryption context }
{ Session encryption context. One TCometCrypt per
session. Holds both directional ChaCha20 states plus
an activation flag. }
TCometCrypt = record
Active: Boolean; { Encryption is active }
TxState: TChacha20State; { Transmit cipher state }
RxState: TChacha20State; { Receive cipher state }
Active: Boolean; { Encryption is active }
TxState: cr.chacha20.TChacha20State; { Transmit cipher state }
RxState: cr.chacha20.TChacha20State; { Receive cipher state }
end;
{ ---- X25519 Key Exchange ---- }
{ Generate a random X25519 keypair.
PrivKey is the clamped 32-byte scalar.
PubKey is the 32-byte Montgomery u-coordinate. }
procedure X25519Keypair(out PrivKey, PubKey: TX25519Key);
{ Scalar multiplication on Curve25519 (Montgomery form).
R = K * U. Exported for testing. }
procedure X25519ScalarMult(const K, U: TX25519Key; out R: TX25519Key);
{ Compute shared secret = X25519(our_private, their_public).
Returns False if the result is all-zero (bad public key). }
function X25519SharedSecret(const OurPriv, TheirPub: TX25519Key;
out Secret: TX25519Key): Boolean;
{ ---- ChaCha20 Stream Cipher ---- }
{ Initialize a ChaCha20 cipher state with a 32-byte key. }
procedure ChaCha20Init(var State: TChacha20State;
const Key: array of Byte);
{ Encrypt/decrypt data in place using ChaCha20 (XOR with keystream). }
procedure ChaCha20Crypt(var State: TChacha20State;
Data: PByte; Len: LongWord);
{ ---- Session Encryption ---- }
{ Derive send/recv keys from shared secret and ephemeral public keys,
and initialize the encryption context. IsOriginator determines
key direction (originator's send key = answerer's recv key). }
and initialize the encryption context. IsOriginator determines key
direction (originator's send key = answerer's recv key). }
procedure CometCryptInit(var Crypt: TCometCrypt;
const SharedSecret, EphPubLocal, EphPubRemote: TX25519Key;
IsOriginator: Boolean);
@@ -110,250 +73,6 @@ implementation
session engine logs activation events at its own layer. }
{ ---- X25519 Montgomery Ladder ---- }
{ Clamp a 32-byte scalar for X25519 per RFC 7748. }
procedure X25519Clamp(var K: TX25519Key);
begin
K[0] := K[0] and 248;
K[31] := (K[31] and 127) or 64;
end;
{ Montgomery ladder scalar multiplication on Curve25519.
Computes Result = k * u where u is a Montgomery u-coordinate.
Uses field element operations from cr.ed25519. }
procedure X25519ScalarMult(const K, U: TX25519Key; out R: TX25519Key);
{ Montgomery ladder per RFC 7748 Section 5.
Clamps scalar and decodes u-coordinate per spec.
All temporaries are distinct to avoid aliasing bugs. }
var
Scalar: TX25519Key;
UCopy: TX25519Key;
U_fe, X2, Z2, X3, Z3: TFieldElement;
tA, tAA, tB, tBB, tE: TFieldElement;
tC, tD, tDA, tCB: TFieldElement;
tDAp, tDAm, tT: TFieldElement;
Swap, KT: LongInt;
I: Integer;
begin
{ decodeScalar25519: clamp scalar per RFC 7748 }
Move(K, Scalar, 32);
X25519Clamp(Scalar);
{ decodeUCoordinate: clear high bit per RFC 7748 }
Move(U, UCopy, 32);
UCopy[31] := UCopy[31] and $7F;
FE_FromBytes(U_fe, @UCopy[0]);
FE_1(X2);
FE_0(Z2);
FE_Copy(X3, U_fe);
FE_1(Z3);
Swap := 0;
for I := 254 downto 0 do
begin
KT := (Scalar[I shr 3] shr (I and 7)) and 1;
Swap := Swap xor KT;
FE_CSwap(X2, X3, Swap);
FE_CSwap(Z2, Z3, Swap);
Swap := KT;
FE_Add(tA, X2, Z2);
FE_Sq(tAA, tA);
FE_Sub(tB, X2, Z2);
FE_Sq(tBB, tB);
FE_Sub(tE, tAA, tBB);
FE_Add(tC, X3, Z3);
FE_Sub(tD, X3, Z3);
FE_Mul(tDA, tD, tA);
FE_Mul(tCB, tC, tB);
FE_Add(tDAp, tDA, tCB);
FE_Sq(X3, tDAp);
FE_Sub(tDAm, tDA, tCB);
FE_Sq(tDAm, tDAm);
FE_Mul(Z3, U_fe, tDAm);
FE_Mul(X2, tAA, tBB);
FE_Mul121666(tT, tE); { 121666 * E }
FE_Sub(tT, tT, tE); { 121665 * E (a24 for Curve25519) }
FE_Add(tT, tAA, tT); { AA + a24 * E }
FE_Mul(Z2, tE, tT); { z_2 = E * (AA + a24 * E) }
end;
FE_CSwap(X2, X3, Swap);
FE_CSwap(Z2, Z3, Swap);
FE_Invert(Z2, Z2);
FE_Mul(X2, X2, Z2);
FE_ToBytes(TED25519PublicKey(R), X2);
end;
{ The X25519 base point (u=9). }
const
X25519_BASEPOINT: TX25519Key = (
9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
);
procedure X25519Keypair(out PrivKey, PubKey: TX25519Key);
var
Seed: TED25519Seed;
begin
{ Generate 32 random bytes }
ED25519RandomSeed(Seed);
Move(Seed, PrivKey, 32);
X25519Clamp(PrivKey);
{ Public key = PrivKey * basepoint }
X25519ScalarMult(PrivKey, X25519_BASEPOINT, PubKey);
end;
function X25519SharedSecret(const OurPriv, TheirPub: TX25519Key;
out Secret: TX25519Key): Boolean;
var
I: Integer;
Check: Byte;
begin
X25519ScalarMult(OurPriv, TheirPub, Secret);
{ Check for all-zero result (invalid public key) }
Check := 0;
for I := 0 to 31 do
Check := Check or Secret[I];
Result := Check <> 0;
end;
{ ---- ChaCha20 Stream Cipher (RFC 8439) ---- }
type
TChaCha20Block = array[0..15] of LongWord;
function RotL32(X: LongWord; N: Integer): LongWord; inline;
begin
Result := (X shl N) or (X shr (32 - N));
end;
procedure QuarterRound(var A, B, C, D: LongWord); inline;
begin
A := A + B; D := D xor A; D := RotL32(D, 16);
C := C + D; B := B xor C; B := RotL32(B, 12);
A := A + B; D := D xor A; D := RotL32(D, 8);
C := C + D; B := B xor C; B := RotL32(B, 7);
end;
procedure ChaCha20Block(const Key: array of Byte;
Counter: LongWord; const Nonce: array of Byte;
out Output: array of Byte);
var
State, Working: TChaCha20Block;
I: Integer;
begin
{ Initialize state }
State[0] := $61707865; { "expa" }
State[1] := $3320646e; { "nd 3" }
State[2] := $79622d32; { "2-by" }
State[3] := $6b206574; { "te k" }
{ Key (little-endian LongWords) }
for I := 0 to 7 do
State[4 + I] := LongWord(Key[I*4]) or (LongWord(Key[I*4+1]) shl 8) or
(LongWord(Key[I*4+2]) shl 16) or (LongWord(Key[I*4+3]) shl 24);
{ Counter }
State[12] := Counter;
{ Nonce (little-endian LongWords) }
for I := 0 to 2 do
State[13 + I] := LongWord(Nonce[I*4]) or (LongWord(Nonce[I*4+1]) shl 8) or
(LongWord(Nonce[I*4+2]) shl 16) or (LongWord(Nonce[I*4+3]) shl 24);
{ Copy state to working }
Working := State;
{ 20 rounds (10 double-rounds) }
for I := 1 to 10 do
begin
{ Column rounds }
QuarterRound(Working[0], Working[4], Working[8], Working[12]);
QuarterRound(Working[1], Working[5], Working[9], Working[13]);
QuarterRound(Working[2], Working[6], Working[10], Working[14]);
QuarterRound(Working[3], Working[7], Working[11], Working[15]);
{ Diagonal rounds }
QuarterRound(Working[0], Working[5], Working[10], Working[15]);
QuarterRound(Working[1], Working[6], Working[11], Working[12]);
QuarterRound(Working[2], Working[7], Working[8], Working[13]);
QuarterRound(Working[3], Working[4], Working[9], Working[14]);
end;
{ Add original state }
for I := 0 to 15 do
Working[I] := Working[I] + State[I];
{ Serialize to bytes (little-endian) }
for I := 0 to 15 do
begin
Output[I*4] := Byte(Working[I]);
Output[I*4 + 1] := Byte(Working[I] shr 8);
Output[I*4 + 2] := Byte(Working[I] shr 16);
Output[I*4 + 3] := Byte(Working[I] shr 24);
end;
end;
procedure ChaCha20Init(var State: TChacha20State;
const Key: array of Byte);
begin
FillChar(State, SizeOf(State), 0);
Move(Key[0], State.Key[0], 32);
State.Counter := 0;
State.Used := 64; { Force new block generation on first use }
end;
procedure ChaCha20Crypt(var State: TChacha20State;
Data: PByte; Len: LongWord);
var
I: LongWord;
Avail: Integer;
begin
I := 0;
while I < Len do
begin
{ Generate new keystream block if needed }
if State.Used >= 64 then
begin
ChaCha20Block(State.Key, LongWord(State.Counter),
State.Nonce, State.Block);
Inc(State.Counter);
State.Used := 0;
end;
{ XOR data with keystream }
Avail := 64 - State.Used;
if Avail > Integer(Len - I) then
Avail := Integer(Len - I);
while Avail > 0 do
begin
Data[I] := Data[I] xor State.Block[State.Used];
Inc(I);
Inc(State.Used);
Dec(Avail);
end;
end;
end;
{ ---- Session Encryption Setup ---- }
procedure CometCryptInit(var Crypt: TCometCrypt;
const SharedSecret, EphPubLocal, EphPubRemote: TX25519Key;
IsOriginator: Boolean);
@@ -388,8 +107,8 @@ begin
SHA256Final(Ctx, SessionKey);
{ Derive directional keys:
Key-A = SHA-256(session_key || "comet-keystream-A")
Key-B = SHA-256(session_key || "comet-keystream-B")
Key-A = SHA-256(session_key || "A")
Key-B = SHA-256(session_key || "B")
Originator sends with Key-A, receives with Key-B.
Answerer sends with Key-B, receives with Key-A. }
DirByte := Ord('A');

View File

@@ -39,7 +39,7 @@ interface
uses
Classes, SysUtils,
log.types, mb.address,
cm.types, cm.frame, cm.cram, cr.ed25519, cm.crypto,
cm.types, cm.frame, cm.cram, cr.ed25519, cr.x25519, cm.crypto,
cm.session, cm.events, cm.transport, cm.provider, cm.config;
type

View File

@@ -21,9 +21,9 @@ interface
const
CM_VERSION_MAJOR = 0;
CM_VERSION_MINOR = 2;
CM_VERSION_MINOR = 3;
CM_VERSION_PATCH = 0;
CM_VERSION = '0.2.0';
CM_VERSION = '0.3.0';
{ Oldest version a consumer pinned to CM_VERSION is
guaranteed to remain ABI/API-compatible with. Bumped

181
src/cr.chacha20.pas Normal file
View File

@@ -0,0 +1,181 @@
{ cr.chacha20 -- ChaCha20 stream cipher (RFC 8439).
Incremental ChaCha20 keystream generator with per-state
buffering so arbitrary-length plaintext can be streamed
across multiple ChaCha20Crypt calls without needing to
align on 64-byte blocks.
Public API:
ChaCha20Init Install a 32-byte key, reset counter/nonce/block
ChaCha20Crypt XOR Len bytes of Data with keystream (in place)
Nonce is 96 bits (RFC 8439 §2.3); caller pre-fills
State.Nonce before the first ChaCha20Crypt. Counter is
incremented per 64-byte block. Same ChaCha20 is used for
both encrypt and decrypt (symmetric stream cipher).
Carved from Comet's cm.crypto 2026-04-24. }
unit cr.chacha20;
{$mode objfpc}{$H+}
{$R-}{$Q-} { Range + overflow checks OFF for crypto arithmetic }
interface
uses
SysUtils;
type
{ ChaCha20 cipher state -- 256-bit key, 96-bit nonce,
64-bit block counter, 64-byte keystream buffer. Caller
populates Nonce before first ChaCha20Crypt; Key comes
from ChaCha20Init; Counter + Block + Used are managed
internally. }
TChacha20State = record
Key: array[0..31] of Byte; { 256-bit key }
Nonce: array[0..11] of Byte; { 96-bit nonce (zeroed, counter-based) }
Counter: QWord; { Block counter }
Block: array[0..63] of Byte; { Current keystream block }
Used: Integer; { Bytes used in current block }
end;
{ Initialize a ChaCha20 cipher state with a 32-byte key.
Clears nonce + counter; caller sets nonce bytes before
calling ChaCha20Crypt. Used=64 forces first call to
generate a fresh block. }
procedure ChaCha20Init(var State: TChacha20State;
const Key: array of Byte);
{ Encrypt/decrypt Len bytes of Data in place. Operation is
symmetric: XOR with keystream. Safe to call repeatedly
on different-length chunks; internal Used/Counter track
position. }
procedure ChaCha20Crypt(var State: TChacha20State;
Data: PByte; Len: LongWord);
implementation
type
TChaCha20Block = array[0..15] of LongWord;
function RotL32(X: LongWord; N: Integer): LongWord; inline;
begin
Result := (X shl N) or (X shr (32 - N));
end;
procedure QuarterRound(var A, B, C, D: LongWord); inline;
begin
A := A + B; D := D xor A; D := RotL32(D, 16);
C := C + D; B := B xor C; B := RotL32(B, 12);
A := A + B; D := D xor A; D := RotL32(D, 8);
C := C + D; B := B xor C; B := RotL32(B, 7);
end;
procedure ChaCha20Block(const Key: array of Byte;
Counter: LongWord; const Nonce: array of Byte;
out Output: array of Byte);
var
State, Working: TChaCha20Block;
I: Integer;
begin
{ Initialize state }
State[0] := $61707865; { "expa" }
State[1] := $3320646e; { "nd 3" }
State[2] := $79622d32; { "2-by" }
State[3] := $6b206574; { "te k" }
{ Key (little-endian LongWords) }
for I := 0 to 7 do
State[4 + I] := LongWord(Key[I*4]) or (LongWord(Key[I*4+1]) shl 8) or
(LongWord(Key[I*4+2]) shl 16) or (LongWord(Key[I*4+3]) shl 24);
{ Counter }
State[12] := Counter;
{ Nonce (little-endian LongWords) }
for I := 0 to 2 do
State[13 + I] := LongWord(Nonce[I*4]) or (LongWord(Nonce[I*4+1]) shl 8) or
(LongWord(Nonce[I*4+2]) shl 16) or (LongWord(Nonce[I*4+3]) shl 24);
{ Copy state to working }
Working := State;
{ 20 rounds (10 double-rounds) }
for I := 1 to 10 do
begin
{ Column rounds }
QuarterRound(Working[0], Working[4], Working[8], Working[12]);
QuarterRound(Working[1], Working[5], Working[9], Working[13]);
QuarterRound(Working[2], Working[6], Working[10], Working[14]);
QuarterRound(Working[3], Working[7], Working[11], Working[15]);
{ Diagonal rounds }
QuarterRound(Working[0], Working[5], Working[10], Working[15]);
QuarterRound(Working[1], Working[6], Working[11], Working[12]);
QuarterRound(Working[2], Working[7], Working[8], Working[13]);
QuarterRound(Working[3], Working[4], Working[9], Working[14]);
end;
{ Add original state }
for I := 0 to 15 do
Working[I] := Working[I] + State[I];
{ Serialize to bytes (little-endian) }
for I := 0 to 15 do
begin
Output[I*4] := Byte(Working[I]);
Output[I*4 + 1] := Byte(Working[I] shr 8);
Output[I*4 + 2] := Byte(Working[I] shr 16);
Output[I*4 + 3] := Byte(Working[I] shr 24);
end;
end;
procedure ChaCha20Init(var State: TChacha20State;
const Key: array of Byte);
begin
FillChar(State, SizeOf(State), 0);
Move(Key[0], State.Key[0], 32);
State.Counter := 0;
State.Used := 64; { Force new block generation on first use }
end;
procedure ChaCha20Crypt(var State: TChacha20State;
Data: PByte; Len: LongWord);
var
I: LongWord;
Avail: Integer;
begin
I := 0;
while I < Len do
begin
{ Generate new keystream block if needed }
if State.Used >= 64 then
begin
ChaCha20Block(State.Key, LongWord(State.Counter),
State.Nonce, State.Block);
Inc(State.Counter);
State.Used := 0;
end;
{ XOR data with keystream }
Avail := 64 - State.Used;
if Avail > Integer(Len - I) then
Avail := Integer(Len - I);
while Avail > 0 do
begin
Data[I] := Data[I] xor State.Block[State.Used];
Inc(I);
Inc(State.Used);
Dec(Avail);
end;
end;
end;
end.

View File

@@ -19,9 +19,9 @@ interface
const
CRYPTO_VERSION_MAJOR = 0;
CRYPTO_VERSION_MINOR = 1;
CRYPTO_VERSION_MINOR = 2;
CRYPTO_VERSION_PATCH = 0;
CRYPTO_VERSION = '0.1.0';
CRYPTO_VERSION = '0.2.0';
CRYPTO_MIN_COMPATIBLE_VERSION = '0.1.0';

173
src/cr.x25519.pas Normal file
View File

@@ -0,0 +1,173 @@
{ cr.x25519 -- Curve25519 scalar multiplication (RFC 7748).
X25519 Diffie-Hellman key exchange over Curve25519 in
Montgomery form. Public API:
X25519Keypair random clamped scalar + matching u-coord
X25519ScalarMult R = k * u (exported for test vectors)
X25519SharedSecret DH shared secret with zero-check
Shares field arithmetic (GF(2^255-19)) with cr.ed25519 --
Ed25519 and X25519 are the same curve in different
coordinate forms, so the FE_* ops from cr.ed25519 are
reused rather than duplicated.
Carved from Comet's cm.crypto 2026-04-24. }
unit cr.x25519;
{$mode objfpc}{$H+}
{$R-}{$Q-} { Range + overflow checks OFF for crypto arithmetic }
interface
uses
SysUtils, cr.ed25519;
type
{ 32-byte Curve25519 u-coordinate / scalar. Same wire
layout as TED25519PublicKey / TED25519Seed but kept
distinct for type discipline. }
TX25519Key = array[0..31] of Byte;
{ Generate a random X25519 keypair.
PrivKey is the clamped 32-byte scalar.
PubKey is the 32-byte Montgomery u-coordinate (PrivKey * basepoint). }
procedure X25519Keypair(out PrivKey, PubKey: TX25519Key);
{ Scalar multiplication on Curve25519 (Montgomery form).
R = K * U. Exported for testability; consumers usually
call X25519Keypair + X25519SharedSecret instead. }
procedure X25519ScalarMult(const K, U: TX25519Key; out R: TX25519Key);
{ Compute shared secret = X25519(our_private, their_public).
Returns False if the result is all-zero (bad public key --
low-order point attack). }
function X25519SharedSecret(const OurPriv, TheirPub: TX25519Key;
out Secret: TX25519Key): Boolean;
implementation
{ Clamp a 32-byte scalar for X25519 per RFC 7748. }
procedure X25519Clamp(var K: TX25519Key);
begin
K[0] := K[0] and 248;
K[31] := (K[31] and 127) or 64;
end;
{ Montgomery ladder scalar multiplication on Curve25519.
Computes Result = k * u where u is a Montgomery u-coordinate.
Uses field element operations from cr.ed25519. }
procedure X25519ScalarMult(const K, U: TX25519Key; out R: TX25519Key);
{ Montgomery ladder per RFC 7748 Section 5.
Clamps scalar and decodes u-coordinate per spec.
All temporaries are distinct to avoid aliasing bugs. }
var
Scalar: TX25519Key;
UCopy: TX25519Key;
U_fe, X2, Z2, X3, Z3: TFieldElement;
tA, tAA, tB, tBB, tE: TFieldElement;
tC, tD, tDA, tCB: TFieldElement;
tDAp, tDAm, tT: TFieldElement;
Swap, KT: LongInt;
I: Integer;
begin
{ decodeScalar25519: clamp scalar per RFC 7748 }
Move(K, Scalar, 32);
X25519Clamp(Scalar);
{ decodeUCoordinate: clear high bit per RFC 7748 }
Move(U, UCopy, 32);
UCopy[31] := UCopy[31] and $7F;
FE_FromBytes(U_fe, @UCopy[0]);
FE_1(X2);
FE_0(Z2);
FE_Copy(X3, U_fe);
FE_1(Z3);
Swap := 0;
for I := 254 downto 0 do
begin
KT := (Scalar[I shr 3] shr (I and 7)) and 1;
Swap := Swap xor KT;
FE_CSwap(X2, X3, Swap);
FE_CSwap(Z2, Z3, Swap);
Swap := KT;
FE_Add(tA, X2, Z2);
FE_Sq(tAA, tA);
FE_Sub(tB, X2, Z2);
FE_Sq(tBB, tB);
FE_Sub(tE, tAA, tBB);
FE_Add(tC, X3, Z3);
FE_Sub(tD, X3, Z3);
FE_Mul(tDA, tD, tA);
FE_Mul(tCB, tC, tB);
FE_Add(tDAp, tDA, tCB);
FE_Sq(X3, tDAp);
FE_Sub(tDAm, tDA, tCB);
FE_Sq(tDAm, tDAm);
FE_Mul(Z3, U_fe, tDAm);
FE_Mul(X2, tAA, tBB);
FE_Mul121666(tT, tE); { 121666 * E }
FE_Sub(tT, tT, tE); { 121665 * E (a24 for Curve25519) }
FE_Add(tT, tAA, tT); { AA + a24 * E }
FE_Mul(Z2, tE, tT); { z_2 = E * (AA + a24 * E) }
end;
FE_CSwap(X2, X3, Swap);
FE_CSwap(Z2, Z3, Swap);
FE_Invert(Z2, Z2);
FE_Mul(X2, X2, Z2);
FE_ToBytes(TED25519PublicKey(R), X2);
end;
{ The X25519 base point (u=9). }
const
X25519_BASEPOINT: TX25519Key = (
9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
);
procedure X25519Keypair(out PrivKey, PubKey: TX25519Key);
var
Seed: TED25519Seed;
begin
{ Generate 32 random bytes }
ED25519RandomSeed(Seed);
Move(Seed, PrivKey, 32);
X25519Clamp(PrivKey);
{ Public key = PrivKey * basepoint }
X25519ScalarMult(PrivKey, X25519_BASEPOINT, PubKey);
end;
function X25519SharedSecret(const OurPriv, TheirPub: TX25519Key;
out Secret: TX25519Key): Boolean;
var
I: Integer;
Check: Byte;
begin
X25519ScalarMult(OurPriv, TheirPub, Secret);
{ Check for all-zero result (invalid public key -- low-order
point attack; RFC 7748 §6.1 mandates rejection). }
Check := 0;
for I := 0 to 31 do
Check := Check or Secret[I];
Result := Check <> 0;
end;
end.