origin-crypto-sdk 0.6.3

Standalone cryptographic SDK with classical (Ed25519) and post-quantum (Falcon, SLH-DSA, ML-DSA, NTRU Prime, Curve41417) primitives. Hybrid signing by default.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
// SPDX-License-Identifier: Apache-2.0

//! ChaCha40 — post-quantum hardened stream cipher with 512-bit key and 40 rounds.
//!
//! # Motivation
//!
//! Standard ChaCha20 uses a 256-bit key and 20 rounds. Against a quantum
//! adversary armed with Grover's algorithm, a 256-bit key provides 128-bit
//! post-quantum security — sufficient per NIST guidance. This module offers
//! a **conservative hardening** for applications that want a larger safety
//! margin against future quantum cryptanalysis:
//!
//! - **512-bit key** (two 256-bit halves, run as two parallel ChaCha states)
//!   → 256-bit post-quantum security against Grover's algorithm
//! - **40 rounds** per state (20 double-rounds) → security margin doubled
//!   over ChaCha20's already-conservative 20 rounds
//!
//! # Design: Dual-state parallel ChaCha
//!
//! This is **not** a single cipher with a modified 32-word state. Instead,
//! it runs two standard ChaCha20 states in parallel, each with a 256-bit
//! key half derived from the 512-bit master key. Both states share the same
//! counter and nonce but produce independent keystream blocks. The outputs
//! are interleaved:
//!
//! ```text
//! Combined block (64 bytes) = State_A[0..32] || State_B[0..32]
//! ```
//!
//! This design was chosen because:
//!
//! 1. **Reuses existing cryptanalysis.** Each state is standard ChaCha with
//!    a modified round count. All security proofs for the ChaCha quarter-round
//!    ARX structure apply unchanged.
//! 2. **No novel state layout.** No need to redesign the 16-word state to fit
//!    a 512-bit key — the key is split naturally across two standard states.
//! 3. **Interleaved binding.** An attacker cannot attack one 256-bit key half
//!    independently because a known-plaintext pair reveals only 32 bytes of
//!    each state's output per combined block — insufficient to recover either
//!    half-key individually without solving two separate Grover searches.
//! 4. **Minimal code.** Reuses `quarter_round`, `init_state`, and the
//!    standard ChaCha block function — only the round count and output
//!    combining differ.
//!
//! # Security notes
//!
//! - **256-bit PQ security** against Grover's algorithm. Grover's provides a
//!   quadratic speedup, so searching a 512-bit space requires 2²⁵⁶ operations
//!   — physically impossible under known physics.
//! - **40 rounds** (2× ChaCha20) provide double the standard security margin.
//!   No cryptanalytic attack on standard ChaCha20 exists beyond 7–8 rounds;
//!   40 rounds is vastly conservative.
//! - **Two-state attack surface.** The two states are independent, so a break
//!   of one ChaCha half (e.g., a novel cryptanalytic attack on 20-round ChaCha)
//!   still leaves the other half secure. The cipher degrades gracefully to
//!   ChaCha20-level security.
//!
//! # Performance
//!
//! Approximately 2× slower than standard ChaCha20 (two states × 40/20 rounds
//! = 4× the quarter-rounds, but some overhead is shared). On a modern x86_64
//! CPU, expect ~2–3 GB/s vs ~8–10 GB/s for ChaCha20. This is the cost of
//! conservative PQ hardening.

use crate::primitives::chacha20::{init_state, quarter_round, CHACHA20_KEY_SIZE};

/// ChaCha40 key size in bytes (512 bits)
pub const CHACHA40_KEY_SIZE: usize = 64;

/// ChaCha40 nonce size in bytes (96 bits, same as IETF ChaCha20)
pub const CHACHA40_NONCE_SIZE: usize = 12;

/// ChaCha40 combined block size in bytes (64 bytes = 32 from each state)
pub const CHACHA40_BLOCK_SIZE: usize = 64;

/// The ChaCha constant "expand 32-byte k" as little-endian u32s
const CONSTANTS: [u32; 4] = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574];

/// Run the ChaCha block function with a custom number of double-rounds.
///
/// - `initial`: the initialized state (constants, key, counter, nonce)
/// - `double_rounds`: number of double-rounds to apply (e.g., 10 for ChaCha20,
///   20 for ChaCha40)
///
/// Returns the 64-byte keystream block.
fn chacha_block_custom_rounds(initial: &[u32; 16], double_rounds: usize) -> [u8; 64] {
    let mut state = *initial;

    for _ in 0..double_rounds {
        // Column rounds
        quarter_round(&mut state, 0, 4, 8, 12);
        quarter_round(&mut state, 1, 5, 9, 13);
        quarter_round(&mut state, 2, 6, 10, 14);
        quarter_round(&mut state, 3, 7, 11, 15);
        // Diagonal rounds
        quarter_round(&mut state, 0, 5, 10, 15);
        quarter_round(&mut state, 1, 6, 11, 12);
        quarter_round(&mut state, 2, 7, 8, 13);
        quarter_round(&mut state, 3, 4, 9, 14);
    }

    // Add initial state
    for i in 0..16 {
        state[i] = state[i].wrapping_add(initial[i]);
    }

    // Serialize to bytes
    let mut out = [0u8; 64];
    for i in 0..16 {
        out[i * 4..(i + 1) * 4].copy_from_slice(&state[i].to_le_bytes());
    }

    out
}

/// ChaCha40 block function — produces a 64-byte keystream block.
///
/// Splits the 512-bit key into two 256-bit halves, runs 40 rounds (20
/// double-rounds) on each half, and interleaves the outputs:
///
/// ```text
/// output[0..32]  = ChaCha40(state_A, counter, nonce)[0..32]
/// output[32..64] = ChaCha40(state_B, counter, nonce)[0..32]
/// ```
///
/// # Arguments
///
/// * `key` — 64-byte (512-bit) master key
/// * `counter` — 32-bit block counter
/// * `nonce` — 12-byte (96-bit) nonce
///
/// # Returns
///
/// A 64-byte combined keystream block.
pub fn chacha40_block(
    key: &[u8; CHACHA40_KEY_SIZE],
    counter: u32,
    nonce: &[u8; CHACHA40_NONCE_SIZE],
) -> [u8; CHACHA40_BLOCK_SIZE] {
    // Split the 512-bit key into two 256-bit halves
    let key_a: [u8; CHACHA20_KEY_SIZE] = {
        let mut k = [0u8; 32];
        k.copy_from_slice(&key[..32]);
        k
    };
    let key_b: [u8; CHACHA20_KEY_SIZE] = {
        let mut k = [0u8; 32];
        k.copy_from_slice(&key[32..]);
        k
    };

    // Initialize both states with the same counter and nonce
    let state_a = init_state(&key_a, counter, nonce);
    let state_b = init_state(&key_b, counter, nonce);

    // Run 40 rounds (20 double-rounds) on each state
    let block_a = chacha_block_custom_rounds(&state_a, 20);
    let block_b = chacha_block_custom_rounds(&state_b, 20);

    // Interleave: first 32 bytes from state A, last 32 from state B
    let mut combined = [0u8; CHACHA40_BLOCK_SIZE];
    combined[..32].copy_from_slice(&block_a[..32]);
    combined[32..].copy_from_slice(&block_b[..32]);

    combined
}

/// ChaCha40 encryption/decryption.
///
/// XORs plaintext/ciphertext with the ChaCha40 keystream.
/// Encryption and decryption are the same operation (XOR is its own inverse).
///
/// # Arguments
///
/// * `key` — 64-byte (512-bit) master key
/// * `counter` — starting 32-bit block counter (usually 0 or 1)
/// * `nonce` — 12-byte (96-bit) nonce
/// * `data` — plaintext or ciphertext bytes
///
/// # Returns
///
/// The XOR'd output (ciphertext if input was plaintext, vice versa).
pub fn chacha40_encrypt(
    key: &[u8; CHACHA40_KEY_SIZE],
    counter: u32,
    nonce: &[u8; CHACHA40_NONCE_SIZE],
    data: &[u8],
) -> Vec<u8> {
    let mut output = Vec::with_capacity(data.len());
    let mut block_counter = counter;

    for chunk in data.chunks(CHACHA40_BLOCK_SIZE) {
        let keystream = chacha40_block(key, block_counter, nonce);
        for (i, &byte) in chunk.iter().enumerate() {
            output.push(byte ^ keystream[i]);
        }
        block_counter = block_counter.wrapping_add(1);
    }

    output
}

/// HChaCha40 — key derivation for XChaCha40 (extended nonce).
///
/// Takes a 512-bit key and a 128-bit nonce (first 16 bytes of a 24-byte nonce)
/// and produces a 512-bit subkey.
///
/// The derivation runs HChaCha20 on each 256-bit key half independently, using
/// the same 128-bit nonce, then concatenates the two 256-bit subkeys:
///
/// ```text
/// subkey = HChaCha20(key[0..32], nonce) || HChaCha20(key[32..64], nonce)
/// ```
pub fn hchacha40(
    key: &[u8; CHACHA40_KEY_SIZE],
    nonce: &[u8; 16],
) -> [u8; CHACHA40_KEY_SIZE] {
    // Split the 512-bit key into two 256-bit halves
    let key_a: [u8; CHACHA20_KEY_SIZE] = {
        let mut k = [0u8; 32];
        k.copy_from_slice(&key[..32]);
        k
    };
    let key_b: [u8; CHACHA20_KEY_SIZE] = {
        let mut k = [0u8; 32];
        k.copy_from_slice(&key[32..]);
        k
    };

    // HChaCha40 on each half with the same nonce
    let subkey_a = hchacha40_internal(&key_a, nonce);
    let subkey_b = hchacha40_internal(&key_b, nonce);

    // Concatenate
    let mut full_subkey = [0u8; CHACHA40_KEY_SIZE];
    full_subkey[..32].copy_from_slice(&subkey_a);
    full_subkey[32..].copy_from_slice(&subkey_b);

    full_subkey
}

/// Internal HChaCha with 40 rounds (20 double-rounds).
///
/// Uses standard HChaCha20 state layout (16-byte nonce fills state[12..15])
/// but runs 40 rounds instead of 20. Cannot reuse `init_state` because
/// HChaCha's state layout differs from ChaCha20's block function layout
/// (no counter word — all 4 state[12..15] slots hold nonce bytes).
fn hchacha40_internal(
    key: &[u8; CHACHA20_KEY_SIZE],
    nonce: &[u8; 16],
) -> [u8; CHACHA20_KEY_SIZE] {
    let mut state = [0u32; 16];

    // Constants
    state[0] = CONSTANTS[0];
    state[1] = CONSTANTS[1];
    state[2] = CONSTANTS[2];
    state[3] = CONSTANTS[3];

    // Key (8 words)
    for i in 0..8 {
        state[4 + i] =
            u32::from_le_bytes([key[i * 4], key[i * 4 + 1], key[i * 4 + 2], key[i * 4 + 3]]);
    }

    // Nonce (4 words from 16-byte nonce, filling state[12..15] — NO counter)
    for i in 0..4 {
        state[12 + i] = u32::from_le_bytes([
            nonce[i * 4],
            nonce[i * 4 + 1],
            nonce[i * 4 + 2],
            nonce[i * 4 + 3],
        ]);
    }

    // 40 rounds = 20 double-rounds for HChaCha40
    for _ in 0..20 {
        quarter_round(&mut state, 0, 4, 8, 12);
        quarter_round(&mut state, 1, 5, 9, 13);
        quarter_round(&mut state, 2, 6, 10, 14);
        quarter_round(&mut state, 3, 7, 11, 15);
        quarter_round(&mut state, 0, 5, 10, 15);
        quarter_round(&mut state, 1, 6, 11, 12);
        quarter_round(&mut state, 2, 7, 8, 13);
        quarter_round(&mut state, 3, 4, 9, 14);
    }

    let mut subkey = [0u8; CHACHA20_KEY_SIZE];
    subkey[0..4].copy_from_slice(&state[0].to_le_bytes());
    subkey[4..8].copy_from_slice(&state[1].to_le_bytes());
    subkey[8..12].copy_from_slice(&state[2].to_le_bytes());
    subkey[12..16].copy_from_slice(&state[3].to_le_bytes());
    subkey[16..20].copy_from_slice(&state[12].to_le_bytes());
    subkey[20..24].copy_from_slice(&state[13].to_le_bytes());
    subkey[24..28].copy_from_slice(&state[14].to_le_bytes());
    subkey[28..32].copy_from_slice(&state[15].to_le_bytes());

    subkey
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Basic test: ChaCha40 block produces different output than ChaCha20
    /// for the same key (truncated to first 256 bits).
    #[test]
    fn test_chacha40_differs_from_chacha20() {
        let key_20 = [42u8; 32];
        let nonce = [0u8; 12];

        // ChaCha20 block (20 rounds) with standard 256-bit key
        let standard_block = crate::primitives::chacha20::chacha20_block(&key_20, 0, &nonce);

        // ChaCha40 block with key = key_20 || zeros (512-bit key)
        let mut chacha40_key = [0u8; 64];
        chacha40_key[..32].copy_from_slice(&key_20);
        let chacha40_block_out = chacha40_block(&chacha40_key, 0, &nonce);

        // Must differ — different round count and interleaving structure
        assert_ne!(chacha40_block_out[..32], standard_block[..32]);
    }

    /// Encryption/decryption roundtrip
    #[test]
    fn test_chacha40_roundtrip() {
        let key = [42u8; 64];
        let nonce = [0u8; 12];
        let plaintext = b"ChaCha40 post-quantum hardened encryption roundtrip test.";

        let ciphertext = chacha40_encrypt(&key, 0, &nonce, plaintext);
        assert_ne!(ciphertext, plaintext);

        let decrypted = chacha40_encrypt(&key, 0, &nonce, &ciphertext);
        assert_eq!(decrypted, plaintext);
    }

    /// Multi-block encryption (data > 64 bytes)
    #[test]
    fn test_chacha40_multi_block() {
        let key = [1u8; 64];
        let nonce = [2u8; 12];
        let plaintext: Vec<u8> = (0..512).map(|i| i as u8).collect();

        let ciphertext = chacha40_encrypt(&key, 0, &nonce, &plaintext);
        assert_eq!(ciphertext.len(), plaintext.len());

        let decrypted = chacha40_encrypt(&key, 0, &nonce, &ciphertext);
        assert_eq!(decrypted, plaintext);
    }

    /// Determinism: same key + nonce + counter → same output
    #[test]
    fn test_chacha40_deterministic() {
        let key = [99u8; 64];
        let nonce = [7u8; 12];

        let block1 = chacha40_block(&key, 1, &nonce);
        let block2 = chacha40_block(&key, 1, &nonce);

        assert_eq!(block1, block2);
    }

    /// Different counter → different output
    #[test]
    fn test_chacha40_counter_distinction() {
        let key = [42u8; 64];
        let nonce = [0u8; 12];

        let block0 = chacha40_block(&key, 0, &nonce);
        let block1 = chacha40_block(&key, 1, &nonce);

        assert_ne!(block0, block1);
    }

    /// Different nonce → different output
    #[test]
    fn test_chacha40_nonce_distinction() {
        let key = [42u8; 64];
        let nonce_a = [0u8; 12];
        let mut nonce_b = [0u8; 12];
        nonce_b[0] = 1;

        let block_a = chacha40_block(&key, 0, &nonce_a);
        let block_b = chacha40_block(&key, 0, &nonce_b);

        assert_ne!(block_a, block_b);
    }

    /// Different key → different output
    #[test]
    fn test_chacha40_key_distinction() {
        let mut key_a = [0u8; 64];
        let mut key_b = [0u8; 64];
        key_b[63] = 1;

        let nonce = [0u8; 12];

        let block_a = chacha40_block(&key_a, 0, &nonce);
        let block_b = chacha40_block(&key_b, 0, &nonce);

        assert_ne!(block_a, block_b);
    }

    /// Both key halves contribute to the output
    #[test]
    fn test_chacha40_both_halves_matter() {
        let nonce = [0u8; 12];

        let mut key_original = [0u8; 64];
        key_original[0] = 1;
        key_original[32] = 2;

        let block_original = chacha40_block(&key_original, 0, &nonce);

        // Same key, but second half zeroed
        let mut key_b_zero = [0u8; 64];
        key_b_zero[0] = 1;
        let block_b_zero = chacha40_block(&key_b_zero, 0, &nonce);

        // Must differ — the second key half contributes to the output
        assert_ne!(block_original, block_b_zero);
    }

    /// HChaCha40 roundtrip (key derivation produces deterministic subkeys)
    #[test]
    fn test_hchacha40_deterministic() {
        let key = [42u8; 64];
        let nonce = [7u8; 16];

        let subkey1 = hchacha40(&key, &nonce);
        let subkey2 = hchacha40(&key, &nonce);

        assert_eq!(subkey1, subkey2);
    }

    /// HChaCha40 subkey is 64 bytes (512 bits)
    #[test]
    fn test_hchacha40_key_size() {
        let key = [42u8; 64];
        let nonce = [7u8; 16];

        let subkey = hchacha40(&key, &nonce);
        assert_eq!(subkey.len(), 64);
    }

    /// HChaCha40: different key → different subkey
    #[test]
    fn test_hchacha40_key_distinction() {
        let mut key_a = [0u8; 64];
        let mut key_b = [0u8; 64];
        key_b[63] = 1;

        let nonce = [0u8; 16];

        assert_ne!(hchacha40(&key_a, &nonce), hchacha40(&key_b, &nonce));
    }

    /// Verify that encrypt/counter ties back to block correctly
    #[test]
    fn test_chacha40_encrypt_matches_block_xor() {
        let key = [77u8; 64];
        let nonce = [3u8; 12];

        // Encrypt a 64-byte message
        let msg: Vec<u8> = (0..64).map(|i| i as u8).collect();
        let ct = chacha40_encrypt(&key, 0, &nonce, &msg);

        // Manually XOR with block output
        let block = chacha40_block(&key, 0, &nonce);
        let expected: Vec<u8> = msg.iter().zip(block.iter()).map(|(m, k)| m ^ k).collect();

        assert_eq!(ct, expected);
    }
}