krypteia-arcana 0.1.0

Pure-Rust classical cryptographic primitives: RSA (PKCS#1 v1.5, OAEP), ECC (NIST P-256/384/521, secp256k1), ECDSA, EdDSA (Ed25519), X25519, AES (128/192/256, GCM/CBC), DES/3DES, SHA-1/2/3, HMAC. Side-channel-aware (Montgomery ladder, branchless point_add_ct). Targets embedded (no_std), STM32 M0/M4/M33, ESP32-C3 RISC-V. Zero runtime dependencies.
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
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
//! AES-XTS tweakable block cipher (IEEE 1619, NIST SP 800-38E).
//!
//! XTS = XEX-based Tweaked codebook with ciphertext Stealing.
//!
//! XTS is the **disk encryption** mode of AES. Unlike GCM and CCM,
//! it is **not** an AEAD: there is no authentication tag, no
//! associated data, and no nonce-uniqueness assumption. Instead it
//! is a *length-preserving, deterministic, tweakable* cipher
//! designed for storage scenarios where:
//!
//! * Each storage unit (typically a 512-byte or 4096-byte sector)
//!   has a stable identifier — its **sector number** — which is
//!   used as the tweak.
//! * Encrypted sectors are written in place at the same byte
//!   position they had in plaintext (length-preserving).
//! * The ciphertext for sector `n` is independent of every other
//!   sector, so single-sector reads / writes are possible.
//!
//! Used by **LUKS2** (Linux full-disk encryption), **BitLocker**
//! (Windows since Vista), **FileVault** (macOS), **VeraCrypt**, and
//! the embedded SSD self-encryption layers (Opal SED, eDrive).
//!
//! # Construction (IEEE 1619 §5.3)
//!
//! Two AES keys: `K = K1 || K2` (so 32 bytes for XTS-AES-128 and
//! 64 bytes for XTS-AES-256). For each 16-byte block `j` of a
//! sector with sequence number `i`:
//!
//! ```text
//!     T = AES_K2(i)             ; encrypt the tweak (once per sector)
//!     for j in 0..n_blocks:
//!         C_j = AES_K1(P_j XOR T) XOR T
//!         T   = T * α  in GF(2^128)    ; α = 0x02
//! ```
//!
//! `i` is the data unit sequence number, **encoded as 16
//! little-endian bytes**. The multiplication by `α = 0x02` in
//! `GF(2^128)` reduces by `x^128 + x^7 + x^2 + x + 1` (the
//! standard XEX polynomial; `0x87` byte for the high-bit reduction).
//!
//! When the sector length is **not** a multiple of 16, the last
//! block uses **ciphertext stealing**: the last full block and the
//! short tail block are processed jointly so that the output is
//! the same length as the input. See [`AesXts::encrypt_sector`]
//! for the details.
//!
//! # API
//!
//! ```rust,ignore
//! use arcana::cipher::xts::AesXts;
//!
//! let key:    [u8; 32]   = /* K1 || K2, 32 bytes for XTS-AES-128 */;
//! let tweak:  [u8; 16]   = sector_number_le_padded;
//! let mut buf            = sector_plaintext.to_vec();
//!
//! let xts = AesXts::new(&key).unwrap();
//! xts.encrypt_sector(&tweak, &mut buf);
//! // ... write `buf` to disk ...
//! xts.decrypt_sector(&tweak, &mut buf);
//! ```
//!
//! # Key reuse warning
//!
//! XTS is **not** an AEAD. It does not detect tampering — flipping
//! one ciphertext bit just flips the same bit in plaintext (within
//! a single 16-byte block). Disk encryption tools deal with this
//! at a higher layer (file checksums, file system journals,
//! application-level MACs).
//!
//! XTS *does* offer per-sector independence: rewriting sector 100
//! does not affect sector 101. But within a sector, an attacker
//! who can corrupt ciphertext bytes can corrupt plaintext bytes
//! one-for-one. Don't use XTS for anything that needs end-to-end
//! integrity.
//!
//! # Tests
//!
//! Pinned against the standard IEEE 1619 test vectors (the same
//! ones used by OpenSSL, Crypto++, and BoringSSL).

use super::aes::Aes;
use crate::BlockCipher;

// ============================================================================
// GF(2^128) tweak multiplication by α (= 0x02), little-endian byte layout
// ============================================================================

/// Multiply a 16-byte tweak by `α = 0x02` in `GF(2^128)`, where the
/// reduction polynomial is `x^128 + x^7 + x^2 + x + 1` (the IEEE
/// 1619 XEX polynomial — note: this is the same poly as GCM but
/// with a *little-endian* byte ordering, which inverts the
/// semantics relative to GHASH).
///
/// In bit terms: shift the 128-bit integer left by 1 (so each byte
/// is shifted left by 1 with the carry coming from the previous
/// byte), and if the high bit overflowed, XOR `0x87` into byte 0.
fn gf128_mul_alpha(t: &mut [u8; 16]) {
    let mut carry: u8 = 0;
    for byte in t.iter_mut() {
        let new_carry = *byte >> 7;
        *byte = (*byte << 1) | carry;
        carry = new_carry;
    }
    if carry != 0 {
        t[0] ^= 0x87;
    }
}

// ============================================================================
// Public API
// ============================================================================

/// AES-XTS state. Holds the two AES key schedules `K1` (for the
/// data block encryption) and `K2` (for the tweak encryption).
///
/// Construction is the bottleneck — both key schedules are
/// expanded once at `new` and reused for every sector.
pub struct AesXts {
    /// AES instance with key `K1`, used for the data-path
    /// `AES_K1(P_j XOR T)` step.
    k1: Aes,
    /// AES instance with key `K2`, used for the tweak-path
    /// `T = AES_K2(i)` step (called exactly once per sector).
    k2: Aes,
}

impl AesXts {
    /// Initialise XTS with a concatenated key `K = K1 || K2`.
    ///
    /// Accepts:
    /// * 32 bytes -- XTS-AES-128 (each half is a 16-byte AES-128 key)
    /// * 64 bytes -- XTS-AES-256 (each half is a 32-byte AES-256 key)
    ///
    /// Returns `None` if the key length is invalid or if `K1 == K2`
    /// (the IEEE 1619 spec mandates the two halves be distinct, since
    /// `K1 == K2` collapses XTS to a degenerate variant of XEX with
    /// a tweak that's just `AES_K(i)` and exposes a known-plaintext
    /// distinguishing attack).
    pub fn new(key: &[u8]) -> Option<Self> {
        let half = match key.len() {
            32 => 16,
            64 => 32,
            _ => return None,
        };
        let (k1_bytes, k2_bytes) = key.split_at(half);
        // IEEE 1619 §5.1: K1 != K2.
        if k1_bytes == k2_bytes {
            return None;
        }
        Some(Self {
            k1: <Aes as BlockCipher>::new(k1_bytes),
            k2: <Aes as BlockCipher>::new(k2_bytes),
        })
    }

    /// Encrypt one sector in place.
    ///
    /// `tweak` is 16 bytes (little-endian encoding of the sector
    /// sequence number; pad with zeros for sequence numbers smaller
    /// than 128 bits, which is the common case).
    ///
    /// `data` may be any length **>= 16 bytes** (XTS is not defined
    /// for < 16 bytes — fewer than one block has nothing to "steal"
    /// from). Returns silently with the data unchanged if the
    /// length is below 16. For lengths that are multiples of 16, it
    /// is plain XEX. Otherwise the last full block and the partial
    /// tail are joined via ciphertext stealing per IEEE 1619 §5.3.2.
    pub fn encrypt_sector(&self, tweak: &[u8; 16], data: &mut [u8]) {
        if data.len() < 16 {
            return;
        }

        // Step 1: encrypt the tweak with K2.
        let mut t = *tweak;
        self.k2.encrypt_block(&mut t);

        // Step 2: process all but possibly the last two blocks
        // straightforwardly. If the data length is a multiple of
        // 16, we'll process all blocks in this loop and skip the
        // ciphertext-stealing step. Otherwise we'll stop one block
        // early and feed the trailing two blocks (one full + one
        // short) into the stealing path.
        let n = data.len();
        let full_blocks = n / 16;
        let tail = n % 16;
        let blocks_in_main = if tail == 0 { full_blocks } else { full_blocks - 1 };

        for j in 0..blocks_in_main {
            let off = j * 16;
            let mut block = [0u8; 16];
            block.copy_from_slice(&data[off..off + 16]);
            // P XOR T
            for i in 0..16 {
                block[i] ^= t[i];
            }
            // AES_K1(...)
            self.k1.encrypt_block(&mut block);
            // ... XOR T
            for i in 0..16 {
                block[i] ^= t[i];
            }
            data[off..off + 16].copy_from_slice(&block);

            // Advance the tweak.
            gf128_mul_alpha(&mut t);
        }

        // Step 3: ciphertext stealing for the last 1.x blocks (if any).
        if tail > 0 {
            // We are at the second-to-last full block (offset
            // `(full_blocks - 1) * 16`). Encrypt it with the
            // current tweak `t`.
            let last_full_off = (full_blocks - 1) * 16;
            let mut block = [0u8; 16];
            block.copy_from_slice(&data[last_full_off..last_full_off + 16]);
            for i in 0..16 {
                block[i] ^= t[i];
            }
            self.k1.encrypt_block(&mut block);
            for i in 0..16 {
                block[i] ^= t[i];
            }
            // `block` is now the encrypted "next-to-last" block;
            // it will become the *last* output block after stealing.

            // The tail block (length `tail`) takes the first `tail`
            // bytes of `block` to round itself out to 16 bytes;
            // the original tail bytes XOR-replace the high `tail`
            // bytes of `block`. (See IEEE 1619 §5.3.2 figure for
            // a graphical view.)
            let tail_off = full_blocks * 16;
            let mut cc = [0u8; 16];
            // High `16 - tail` bytes of `cc` come from the encrypted
            // last full block (the "stolen" bytes).
            cc[tail..].copy_from_slice(&block[tail..]);
            // Low `tail` bytes of `cc` come from the original tail
            // plaintext.
            cc[..tail].copy_from_slice(&data[tail_off..tail_off + tail]);

            // Advance the tweak (one more α-multiply for the
            // "stolen" final block).
            gf128_mul_alpha(&mut t);

            // Encrypt `cc` with the advanced tweak.
            for i in 0..16 {
                cc[i] ^= t[i];
            }
            self.k1.encrypt_block(&mut cc);
            for i in 0..16 {
                cc[i] ^= t[i];
            }

            // Write outputs:
            // - The "encrypted-tweaked-stolen" block `cc` is the
            //   new last full block at offset `last_full_off`.
            // - The first `tail` bytes of `block` (the original
            //   penultimate-block ciphertext) become the tail of
            //   the output, at offset `tail_off`.
            data[last_full_off..last_full_off + 16].copy_from_slice(&cc);
            data[tail_off..tail_off + tail].copy_from_slice(&block[..tail]);
        }
    }

    /// Decrypt one sector in place.
    ///
    /// Inverse of [`Self::encrypt_sector`]. Same length / tweak conventions.
    pub fn decrypt_sector(&self, tweak: &[u8; 16], data: &mut [u8]) {
        if data.len() < 16 {
            return;
        }

        // Encrypt the tweak with K2 (same as on encrypt -- the
        // tweak path is symmetric).
        let mut t = *tweak;
        self.k2.encrypt_block(&mut t);

        let n = data.len();
        let full_blocks = n / 16;
        let tail = n % 16;
        let blocks_in_main = if tail == 0 { full_blocks } else { full_blocks - 1 };

        for j in 0..blocks_in_main {
            let off = j * 16;
            let mut block = [0u8; 16];
            block.copy_from_slice(&data[off..off + 16]);
            for i in 0..16 {
                block[i] ^= t[i];
            }
            self.k1.decrypt_block(&mut block);
            for i in 0..16 {
                block[i] ^= t[i];
            }
            data[off..off + 16].copy_from_slice(&block);

            gf128_mul_alpha(&mut t);
        }

        // Ciphertext stealing on decrypt: same shape, but the
        // tweak for the second-to-last full block is **the
        // ADVANCED tweak**, not the current one. We compute the
        // advanced tweak first, decrypt the last full block with
        // it, recover the stolen bytes, then decrypt the
        // (penultimate) block with the un-advanced tweak.
        if tail > 0 {
            let last_full_off = (full_blocks - 1) * 16;
            let tail_off = full_blocks * 16;

            // Save the current tweak; we'll need it after the
            // advance for the second-to-last block.
            let mut t_advanced = t;
            gf128_mul_alpha(&mut t_advanced);

            // Decrypt the last full block (the one at last_full_off,
            // which currently holds the "encrypted-stolen" block)
            // with the *advanced* tweak.
            let mut block = [0u8; 16];
            block.copy_from_slice(&data[last_full_off..last_full_off + 16]);
            for i in 0..16 {
                block[i] ^= t_advanced[i];
            }
            self.k1.decrypt_block(&mut block);
            for i in 0..16 {
                block[i] ^= t_advanced[i];
            }
            // `block` now contains: low `tail` bytes = original
            // penultimate plaintext, high `16 - tail` bytes = the
            // bytes that were "stolen" from the encrypted second-
            // to-last block.

            // Reconstruct the second-to-last ciphertext block by
            // taking the low `tail` bytes from the on-disk tail
            // followed by the high `16 - tail` bytes of `block`.
            let mut cc = [0u8; 16];
            cc[..tail].copy_from_slice(&data[tail_off..tail_off + tail]);
            cc[tail..].copy_from_slice(&block[tail..]);

            // Decrypt `cc` with the un-advanced tweak.
            for i in 0..16 {
                cc[i] ^= t[i];
            }
            self.k1.decrypt_block(&mut cc);
            for i in 0..16 {
                cc[i] ^= t[i];
            }

            // Write outputs.
            data[last_full_off..last_full_off + 16].copy_from_slice(&cc);
            data[tail_off..tail_off + tail].copy_from_slice(&block[..tail]);
        }
    }
}

// ============================================================================
// Tests (IEEE 1619 / Crypto++ pinned vectors)
// ============================================================================

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

    fn hex(s: &str) -> Vec<u8> {
        let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
        assert!(s.len() % 2 == 0);
        (0..s.len())
            .step_by(2)
            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
            .collect()
    }

    fn hex_arr<const N: usize>(s: &str) -> [u8; N] {
        let v = hex(s);
        assert_eq!(v.len(), N);
        let mut out = [0u8; N];
        out.copy_from_slice(&v);
        out
    }

    /// **IEEE 1619 / Crypto++ test vector 1** -- the all-zero canary.
    ///
    /// Key:        00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    ///             00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    /// Tweak (i):  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    /// Plain:      00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    ///             00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    /// Cipher:     91 7c f6 9e bd 68 b2 ec 9b 9f e9 a3 ea dd a6 92
    ///             cd 43 d2 f5 95 98 ed 85 8c 02 c2 65 2f bf 92 2e
    ///
    /// This vector is a sharp test for two classes of bugs:
    /// (1) any state contamination from a non-zero K1, K2, or
    /// initial tweak would change the output; (2) the K1 == K2
    /// rejection: this vector intentionally uses K1 == K2 == 0,
    /// which our `new` rightly rejects -- so we have to construct
    /// the AES instances directly to exercise it. We do that via
    /// the test-only path below.
    ///
    /// Note: for the public API, the K1 == K2 == 0 case is
    /// (rightly) refused. We bypass that here only to validate the
    /// raw construction against the canonical reference.
    #[test]
    fn ieee1619_vector_1_all_zero() {
        let zero = [0u8; 16];
        let xts = AesXts {
            k1: <Aes as BlockCipher>::new(&zero),
            k2: <Aes as BlockCipher>::new(&zero),
        };
        let tweak: [u8; 16] = [0u8; 16];
        let mut data = [0u8; 32];
        xts.encrypt_sector(&tweak, &mut data);

        let expected = hex("917cf69ebd68b2ec9b9fe9a3eadda692\
             cd43d2f59598ed858c02c2652fbf922e");
        assert_eq!(data.to_vec(), expected);

        // Round-trip
        xts.decrypt_sector(&tweak, &mut data);
        assert_eq!(data, [0u8; 32]);
    }

    /// **IEEE 1619 / Crypto++ test vector 2.**
    ///
    /// Key:    11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11
    ///         22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22
    /// Tweak:  33 33 33 33 33 00 00 00 00 00 00 00 00 00 00 00
    /// Plain:  44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44
    ///         44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44
    /// Cipher: c4 54 18 5e 6a 16 93 6e 39 33 40 38 ac ef 83 8b
    ///         fb 18 6f ff 74 80 ad c4 28 93 82 ec d6 d3 94 f0
    #[test]
    fn ieee1619_vector_2() {
        let key = hex("11111111111111111111111111111111\
             22222222222222222222222222222222");
        let tweak: [u8; 16] = hex_arr("33333333330000000000000000000000");
        let mut data = hex("44444444444444444444444444444444\
             44444444444444444444444444444444");

        let xts = AesXts::new(&key).unwrap();
        xts.encrypt_sector(&tweak, &mut data);

        let expected = hex("c454185e6a16936e39334038acef838b\
             fb186fff7480adc4289382ecd6d394f0");
        assert_eq!(data, expected);

        // Round-trip
        xts.decrypt_sector(&tweak, &mut data);
        let original = hex("44444444444444444444444444444444\
             44444444444444444444444444444444");
        assert_eq!(data, original);
    }

    /// **IEEE 1619 / Crypto++ test vector 3** -- different K1, same
    /// K2 / tweak / plaintext as vector 2 to cross-check that K1
    /// is actually consumed in the data path.
    ///
    /// Key:    ff fe fd fc fb fa f9 f8 f7 f6 f5 f4 f3 f2 f1 f0
    ///         22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22
    /// Tweak:  33 33 33 33 33 00 00 00 00 00 00 00 00 00 00 00
    /// Plain:  44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44
    ///         44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44
    /// Cipher: af 85 33 6b 59 7a fc 1a 90 0b 2e b2 1e c9 49 d2
    ///         92 df 4c 04 7e 0b 21 53 21 86 a5 97 1a 22 7a 89
    #[test]
    fn ieee1619_vector_3() {
        let key = hex("fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0\
             22222222222222222222222222222222");
        let tweak: [u8; 16] = hex_arr("33333333330000000000000000000000");
        let mut data = hex("44444444444444444444444444444444\
             44444444444444444444444444444444");

        let xts = AesXts::new(&key).unwrap();
        xts.encrypt_sector(&tweak, &mut data);

        let expected = hex("af85336b597afc1a900b2eb21ec949d2\
             92df4c047e0b21532186a5971a227a89");
        assert_eq!(data, expected);
    }

    /// Build a 32-byte XTS key with K1 != K2 (the IEEE 1619 spec
    /// requires the two halves to be distinct, and our `new` enforces
    /// it). Used by every property test below.
    fn distinct_key_32() -> [u8; 32] {
        let mut k = [0u8; 32];
        for i in 0..16 {
            k[i] = i as u8 ^ 0x42;
        }
        for i in 16..32 {
            k[i] = i as u8 ^ 0xa5;
        }
        k
    }

    /// Round-trip on a multi-block sector (4 full blocks = 64 bytes).
    /// Tests the tweak advance / `gf128_mul_alpha` path across more
    /// than two blocks.
    #[test]
    fn xts_multi_block_roundtrip() {
        let key = distinct_key_32();
        let tweak = [0xa5u8; 16];
        let original: Vec<u8> = (0..64).map(|i| i as u8).collect();
        let mut data = original.clone();

        let xts = AesXts::new(&key).unwrap();
        xts.encrypt_sector(&tweak, &mut data);
        assert_ne!(data, original);

        xts.decrypt_sector(&tweak, &mut data);
        assert_eq!(data, original);
    }

    /// Round-trip with ciphertext stealing: 17 bytes (1 full + 1 byte).
    #[test]
    fn xts_ciphertext_stealing_17_bytes() {
        let key = distinct_key_32();
        let tweak = [0xa5u8; 16];
        let original: Vec<u8> = (0..17).map(|i| (i * 11) as u8).collect();
        let mut data = original.clone();

        let xts = AesXts::new(&key).unwrap();
        xts.encrypt_sector(&tweak, &mut data);
        assert_eq!(data.len(), 17, "XTS must be length-preserving");
        assert_ne!(data, original);

        xts.decrypt_sector(&tweak, &mut data);
        assert_eq!(data, original);
    }

    /// Round-trip with ciphertext stealing: 31 bytes (1 full + 15 bytes).
    /// Exercises the maximum tail length.
    #[test]
    fn xts_ciphertext_stealing_31_bytes() {
        let key = distinct_key_32();
        let tweak = [0xa5u8; 16];
        let original: Vec<u8> = (0..31).map(|i| (i * 7) as u8).collect();
        let mut data = original.clone();

        let xts = AesXts::new(&key).unwrap();
        xts.encrypt_sector(&tweak, &mut data);
        assert_eq!(data.len(), 31);

        xts.decrypt_sector(&tweak, &mut data);
        assert_eq!(data, original);
    }

    /// Round-trip with ciphertext stealing: 100 bytes (6 full + 4 bytes).
    /// Tests stealing combined with multiple full blocks.
    #[test]
    fn xts_ciphertext_stealing_100_bytes() {
        let key = distinct_key_32();
        let tweak = [0xa5u8; 16];
        let original: Vec<u8> = (0..100).map(|i| (i ^ 0x5a) as u8).collect();
        let mut data = original.clone();

        let xts = AesXts::new(&key).unwrap();
        xts.encrypt_sector(&tweak, &mut data);
        assert_eq!(data.len(), 100);

        xts.decrypt_sector(&tweak, &mut data);
        assert_eq!(data, original);
    }

    /// Different tweaks must produce different ciphertexts for the
    /// same plaintext + key. This is the *whole point* of XTS:
    /// per-sector independence.
    #[test]
    fn xts_different_tweaks_differ() {
        let key = distinct_key_32();
        let tweak1 = [0u8; 16];
        let mut tweak2 = [0u8; 16];
        tweak2[0] = 1;
        let original: Vec<u8> = (0..32).map(|i| i as u8).collect();

        let xts = AesXts::new(&key).unwrap();

        let mut data1 = original.clone();
        xts.encrypt_sector(&tweak1, &mut data1);

        let mut data2 = original.clone();
        xts.encrypt_sector(&tweak2, &mut data2);

        assert_ne!(data1, data2);
    }

    /// XTS-AES-256 round-trip (64-byte concatenated key).
    #[test]
    fn xts_aes256_roundtrip() {
        let mut key = [0u8; 64];
        for i in 0..32 {
            key[i] = i as u8;
        }
        for i in 32..64 {
            key[i] = (i + 0x80) as u8;
        }
        let tweak = [0x11u8; 16];
        let original: Vec<u8> = (0..48).map(|i| i as u8).collect();
        let mut data = original.clone();

        let xts = AesXts::new(&key).unwrap();
        xts.encrypt_sector(&tweak, &mut data);
        xts.decrypt_sector(&tweak, &mut data);
        assert_eq!(data, original);
    }

    /// Parameter validation: invalid key lengths are rejected.
    #[test]
    fn xts_rejects_invalid_key_lengths() {
        for bad_len in [0, 1, 15, 16, 17, 24, 31, 33, 48, 63, 65, 128] {
            let key = vec![0u8; bad_len];
            assert!(AesXts::new(&key).is_none(), "key length {} should be rejected", bad_len);
        }
        // Valid lengths.
        // We need K1 != K2 to actually construct, so use distinct halves.
        let mut k32 = [0u8; 32];
        k32[16] = 1;
        assert!(AesXts::new(&k32).is_some());
        let mut k64 = [0u8; 64];
        k64[32] = 1;
        assert!(AesXts::new(&k64).is_some());
    }

    /// Parameter validation: K1 == K2 is rejected per IEEE 1619 §5.1.
    #[test]
    fn xts_rejects_k1_eq_k2() {
        // 32-byte key with K1 == K2 == all 1's
        let k32 = [0x11u8; 32];
        assert!(AesXts::new(&k32).is_none());
        // 64-byte key with K1 == K2 == all 1's
        let k64 = [0x11u8; 64];
        assert!(AesXts::new(&k64).is_none());
    }

    /// gf128_mul_alpha sanity test: shifting `0x01 || 0...0` should
    /// give `0x02 || 0...0`. Shifting `0x80 || 0...0` (highest byte
    /// of byte 0) should give `0x00 || 0...0` with no carry, so we
    /// also test `0...0 || 0x80` which sets the high bit overall.
    #[test]
    fn gf128_mul_alpha_basic() {
        // Test 1: shift 0x01 -> 0x02
        let mut t = [0u8; 16];
        t[0] = 0x01;
        gf128_mul_alpha(&mut t);
        let mut expected = [0u8; 16];
        expected[0] = 0x02;
        assert_eq!(t, expected);

        // Test 2: shift the highest bit of the last byte -- this
        // is the bit that overflows out of the 128-bit register and
        // triggers the `0x87` reduction XOR on byte 0.
        let mut t = [0u8; 16];
        t[15] = 0x80;
        gf128_mul_alpha(&mut t);
        let mut expected = [0u8; 16];
        expected[0] = 0x87;
        assert_eq!(t, expected);
    }
}