Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1408d94d99 |
34
CHANGELOG.md
34
CHANGELOG.md
@@ -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
|
||||
|
||||
2
build.sh
2
build.sh
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
181
src/cr.chacha20.pas
Normal 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.
|
||||
@@ -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
173
src/cr.x25519.pas
Normal 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.
|
||||
Reference in New Issue
Block a user