Skip to main content

ling_crypto/
geo.rs

1//! Geometric post-quantum crypto suite — knot identities, holographic fingerprints,
2//! and all-or-nothing "holographic" encoding.
3//!
4//! Designed in the spirit of China's SM2/SM3/SM4 national suite (an asymmetric
5//! scheme, a hash, and a block cipher), but **post-quantum** and **geometric**:
6//!
7//! | SM analogue | This module | What it is |
8//! |-------------|-------------|------------|
9//! | SM2 (ECC)   | [`KnotIdentity`] | Hybrid X25519+ML-KEM-768 KEM whose public key is a hyperbolic knot |
10//! | SM3 (hash)  | [`holo_hash`] + [`KnotShape::from_bytes`] | SHA3-256 with a 3-D knot **visual fingerprint** |
11//! | SM4 (cipher)| [`holo_seal`]/[`holo_open`] + [`scatter`]/[`gather`] | XChaCha20-Poly1305 + a 4-D holographic all-or-nothing transform |
12//!
13//! # ⚠️ Security model — read this
14//!
15//! **All cryptographic hardness comes from vetted primitives**: ML-KEM-768
16//! (FIPS 203), X25519, BLAKE3, SHA3-256, XChaCha20-Poly1305, and Rivest's
17//! all-or-nothing package transform. The hyperbolic-knot and 4-D holographic
18//! constructions are **deterministic encodings and visual fingerprints** — they
19//! are *not* a new hardness assumption, and the geometry is never the secret.
20//!
21//! What the geometry genuinely buys you:
22//! - A **human-verifiable 3-D fingerprint** of a public key (think SSH randomart
23//!   / the drunken-bishop algorithm, but a renderable knot). Its security as a
24//!   fingerprint reduces exactly to the collision/preimage resistance of SHA3.
25//! - A themed, visual **identity** for a real post-quantum KEM.
26//! - A "you need every shard" **holographic split** that is a sound AONT.
27
28use crate::hybrid::{self, HybridKeypair};
29use crate::symmetric::XChaCha20;
30use sha3::{Digest, Sha3_256};
31
32// ══════════════════════════════════════════════════════════════════════════
33// SM3-analogue: holo_hash + a 3-D hyperbolic-knot visual fingerprint
34// ══════════════════════════════════════════════════════════════════════════
35
36/// Domain-separated geometric hash (SHA3-256). The cryptographic core of the
37/// suite — everything visual is derived deterministically from this digest.
38pub fn holo_hash(data: &[u8]) -> [u8; 32] {
39    let mut h = Sha3_256::new();
40    h.update(b"ling-geo-holo-hash-v1");
41    h.update(data);
42    h.finalize().into()
43}
44
45fn gcd(mut a: u32, mut b: u32) -> u32 {
46    while b != 0 {
47        let t = b;
48        b = a % b;
49        a = t;
50    }
51    a
52}
53
54/// A deterministic 3-D **(p, q) torus knot** acting as a visual fingerprint of a
55/// 32-byte digest. Identical digests → identical knots; a single bit flip
56/// reshapes the whole curve. Render `points` as a tube/polyline to *see* a key.
57#[derive(Clone, Debug, PartialEq)]
58pub struct KnotShape {
59    /// Longitudinal winding (coprime to `q`) — the knot is non-trivial for p,q ≥ 2.
60    pub p: u32,
61    /// Meridional winding.
62    pub q: u32,
63    /// Major torus radius.
64    pub major_r: f32,
65    /// Minor torus radius.
66    pub minor_r: f32,
67    /// A flavor-only "hyperbolic volume" label derived from the digest (decorative).
68    pub volume: f32,
69    /// Sampled 3-D points along the knot (closed curve).
70    pub points: Vec<[f32; 3]>,
71}
72
73impl KnotShape {
74    /// Number of samples along the knot curve.
75    pub const SAMPLES: usize = 256;
76
77    /// Derive a knot directly from arbitrary bytes (hashes them first).
78    pub fn from_bytes(data: &[u8]) -> Self {
79        Self::from_digest(&holo_hash(data))
80    }
81
82    /// Derive a knot from a 32-byte digest.
83    pub fn from_digest(d: &[u8; 32]) -> Self {
84        // p, q ∈ [2,17], forced coprime & distinct → a genuine torus knot.
85        let mut p = 2 + (d[0] as u32 % 16);
86        let mut q = 2 + (d[1] as u32 % 16);
87        if p == q {
88            q = 2 + ((q) % 16) + 1;
89        }
90        while gcd(p, q) != 1 {
91            q += 1;
92            if q > 18 {
93                q = 2;
94                p += 1;
95                if p > 18 {
96                    p = 2;
97                }
98            }
99        }
100
101        // Radii and a decorative "volume" from later digest bytes.
102        let major_r = 2.0 + (d[2] as f32 / 255.0) * 1.5;
103        let minor_r = 0.4 + (d[3] as f32 / 255.0) * 0.8;
104        let phase = (u16::from_le_bytes([d[4], d[5]]) as f32 / 65535.0) * std::f32::consts::TAU;
105        let volume =
106            1.0 + (u32::from_le_bytes([d[6], d[7], d[8], d[9]]) as f32 / u32::MAX as f32) * 11.0;
107
108        let mut points = Vec::with_capacity(Self::SAMPLES);
109        for i in 0..Self::SAMPLES {
110            let t = (i as f32 / Self::SAMPLES as f32) * std::f32::consts::TAU + phase;
111            let qc = (q as f32 * t).cos();
112            let r = major_r + minor_r * qc;
113            let x = r * (p as f32 * t).cos();
114            let y = r * (p as f32 * t).sin();
115            let z = minor_r * (q as f32 * t).sin();
116            points.push([x, y, z]);
117        }
118        Self { p, q, major_r, minor_r, volume, points }
119    }
120
121    /// Short human-readable fingerprint, e.g. `"knot-7_5-v8.2"`.
122    pub fn label(&self) -> String {
123        format!("knot-{}_{}-v{:.1}", self.p, self.q, self.volume)
124    }
125}
126
127// ══════════════════════════════════════════════════════════════════════════
128// SM2-analogue: KnotIdentity — a hybrid PQ KEM with a knot-shaped public key
129// ══════════════════════════════════════════════════════════════════════════
130
131/// A post-quantum identity. Under the hood it is a hybrid **X25519 + ML-KEM-768**
132/// keypair (secure if either leg holds); its public key also renders as a unique
133/// hyperbolic knot you can show a human to verify out-of-band.
134pub struct KnotIdentity {
135    inner: HybridKeypair,
136    public: Vec<u8>,
137}
138
139impl KnotIdentity {
140    /// Generate a fresh identity from the system CSPRNG.
141    pub fn generate() -> Self {
142        let inner = HybridKeypair::generate();
143        let public = inner.public_key();
144        Self { inner, public }
145    }
146
147    /// Raw hybrid public key bytes (X25519 pk ‖ ML-KEM ek) — share these.
148    pub fn public_key(&self) -> &[u8] {
149        &self.public
150    }
151
152    /// The public key's knot fingerprint — a renderable 3-D identity badge.
153    pub fn public_knot(&self) -> KnotShape {
154        KnotShape::from_bytes(&self.public)
155    }
156
157    /// Decapsulate a ciphertext to recover the shared secret.
158    pub fn decapsulate(&self, ciphertext: &[u8]) -> Result<[u8; 32], &'static str> {
159        self.inner.decapsulate(ciphertext)
160    }
161}
162
163/// Encapsulate a shared secret to a knot identity's public key.
164/// Returns `(ciphertext, shared_secret)`.
165pub fn knot_encapsulate(public_key: &[u8]) -> Result<(Vec<u8>, [u8; 32]), &'static str> {
166    hybrid::encapsulate(public_key)
167}
168
169/// The knot a peer's public key *should* produce — compare against the knot you
170/// were shown to detect a man-in-the-middle (geometric key confirmation).
171pub fn knot_for_public_key(public_key: &[u8]) -> KnotShape {
172    KnotShape::from_bytes(public_key)
173}
174
175// ══════════════════════════════════════════════════════════════════════════
176// SM4-analogue: holo_seal AEAD + a 4-D holographic all-or-nothing transform
177// ══════════════════════════════════════════════════════════════════════════
178
179/// Authenticated encryption (XChaCha20-Poly1305) under a 32-byte key — e.g. a
180/// shared secret from [`knot_encapsulate`] or a key from [`KnotShape`]/mandala.
181pub fn holo_seal(key: [u8; 32], plaintext: &[u8]) -> Result<Vec<u8>, &'static str> {
182    XChaCha20::new(key).encrypt(plaintext)
183}
184
185/// Inverse of [`holo_seal`].
186pub fn holo_open(key: [u8; 32], ciphertext: &[u8]) -> Result<Vec<u8>, &'static str> {
187    XChaCha20::new(key).decrypt(ciphertext)
188}
189
190/// One "hologram" fragment of an all-or-nothing transform. Like a real hologram,
191/// no single fragment reveals any part of the original — you need every one.
192#[derive(Clone, Debug, PartialEq)]
193pub struct HoloFragment {
194    /// Fragment index (0-based).
195    pub index: u32,
196    /// A 4-D coordinate on the unit 3-sphere for visualization (decorative).
197    pub coord: [f32; 4],
198    /// The 32-byte transformed block.
199    pub block: [u8; 32],
200}
201
202const HOLO_BLOCK: usize = 32;
203
204/// Derive a keystream block index `i` from the random session key `k`.
205fn ks_block(k: &[u8; 32], i: u32) -> [u8; 32] {
206    let mut h = blake3::Hasher::new_keyed(k);
207    h.update(b"ling-holo-aont-v1");
208    h.update(&i.to_le_bytes());
209    *h.finalize().as_bytes()
210}
211
212/// **Holographic scatter** — Rivest's all-or-nothing package transform.
213///
214/// Splits `data` into fragments such that **all** fragments are required to
215/// recover even one byte of plaintext. This is a sound AONT: a fresh random key
216/// `k` masks every block via a BLAKE3 keystream, and a final *anchor* block
217/// stores `k ⊕ H(all masked blocks)`. Lose any fragment and `H(...)` changes, so
218/// `k` — and therefore everything — is unrecoverable.
219pub fn scatter(data: &[u8]) -> Vec<HoloFragment> {
220    use rand::RngCore;
221    let mut k = [0u8; 32];
222    rand::rngs::OsRng.fill_bytes(&mut k);
223
224    // Length-prefix so we can trim padding on the way back.
225    let mut msg = (data.len() as u64).to_le_bytes().to_vec();
226    msg.extend_from_slice(data);
227    while !msg.len().is_multiple_of(HOLO_BLOCK) {
228        msg.push(0);
229    }
230    let n = (msg.len() / HOLO_BLOCK) as u32;
231
232    // Masked data blocks: c_i = m_i ⊕ KS_k(i)
233    let mut blocks: Vec<[u8; 32]> = Vec::with_capacity(n as usize + 1);
234    for i in 0..n {
235        let ks = ks_block(&k, i);
236        let mut c = [0u8; 32];
237        for j in 0..HOLO_BLOCK {
238            c[j] = msg[i as usize * HOLO_BLOCK + j] ^ ks[j];
239        }
240        blocks.push(c);
241    }
242
243    // Anchor block: c_n = k ⊕ H(c_0 ‖ … ‖ c_{n-1})
244    let mut h = blake3::Hasher::new();
245    h.update(b"ling-holo-anchor-v1");
246    for c in &blocks {
247        h.update(c);
248    }
249    let digest = *h.finalize().as_bytes();
250    let mut anchor = [0u8; 32];
251    for j in 0..HOLO_BLOCK {
252        anchor[j] = k[j] ^ digest[j];
253    }
254    blocks.push(anchor);
255
256    blocks
257        .into_iter()
258        .enumerate()
259        .map(|(i, block)| HoloFragment {
260            index: i as u32,
261            coord: sphere4_point(i as u32, &block),
262            block,
263        })
264        .collect()
265}
266
267/// **Holographic gather** — invert [`scatter`]. Returns `None` if any fragment is
268/// missing/corrupt (the anchor hash won't match → no key → no plaintext).
269pub fn gather(fragments: &[HoloFragment]) -> Option<Vec<u8>> {
270    if fragments.len() < 2 {
271        return None;
272    }
273    let mut frags = fragments.to_vec();
274    frags.sort_by_key(|f| f.index);
275    // Indices must be exactly 0..len with no gaps.
276    for (i, f) in frags.iter().enumerate() {
277        if f.index as usize != i {
278            return None;
279        }
280    }
281    let n = frags.len() - 1; // data blocks; last is the anchor
282
283    // Recover k = anchor ⊕ H(c_0 ‖ … ‖ c_{n-1})
284    let mut h = blake3::Hasher::new();
285    h.update(b"ling-holo-anchor-v1");
286    for f in &frags[..n] {
287        h.update(&f.block);
288    }
289    let digest = *h.finalize().as_bytes();
290    let mut k = [0u8; 32];
291    for j in 0..HOLO_BLOCK {
292        k[j] = frags[n].block[j] ^ digest[j];
293    }
294
295    // Unmask: m_i = c_i ⊕ KS_k(i)
296    let mut msg = Vec::with_capacity(n * HOLO_BLOCK);
297    for (i, f) in frags[..n].iter().enumerate() {
298        let ks = ks_block(&k, i as u32);
299        for (j, c) in ks.iter().enumerate() {
300            msg.push(f.block[j] ^ c);
301        }
302    }
303    if msg.len() < 8 {
304        return None;
305    }
306    let len = u64::from_le_bytes(msg[..8].try_into().ok()?) as usize;
307    if 8 + len > msg.len() {
308        return None;
309    }
310    Some(msg[8..8 + len].to_vec())
311}
312
313/// Map a fragment to a point on the unit 3-sphere in 4-D (visualization only).
314fn sphere4_point(index: u32, block: &[u8; 32]) -> [f32; 4] {
315    let a = (u16::from_le_bytes([block[0], block[1]]) as f32 / 65535.0) * std::f32::consts::PI;
316    let b = (u16::from_le_bytes([block[2], block[3]]) as f32 / 65535.0) * std::f32::consts::TAU;
317    let c = ((index as f32) * 0.618_034).fract() * std::f32::consts::TAU;
318    [
319        a.sin() * b.cos(),
320        a.sin() * b.sin(),
321        a.cos() * c.cos(),
322        a.cos() * c.sin(),
323    ]
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn knot_is_deterministic_and_avalanches() {
332        let k1 = KnotShape::from_bytes(b"alice-public-key");
333        let k2 = KnotShape::from_bytes(b"alice-public-key");
334        let k3 = KnotShape::from_bytes(b"alice-public-keyX");
335        assert_eq!(k1, k2, "same input → same knot");
336        assert_ne!(k1.points, k3.points, "one byte change reshapes the knot");
337        assert_eq!(k1.points.len(), KnotShape::SAMPLES);
338        assert_eq!(gcd(k1.p, k1.q), 1, "p,q coprime → genuine torus knot");
339    }
340
341    #[test]
342    fn knot_identity_kem_round_trip() {
343        let id = KnotIdentity::generate();
344        let pk = id.public_key().to_vec();
345        // The knot a sender renders must match the recipient's own knot.
346        assert_eq!(knot_for_public_key(&pk), id.public_knot());
347        let (ct, ss_send) = knot_encapsulate(&pk).expect("encapsulate");
348        let ss_recv = id.decapsulate(&ct).expect("decapsulate");
349        assert_eq!(ss_send, ss_recv);
350    }
351
352    #[test]
353    fn seal_open_round_trip() {
354        let id = KnotIdentity::generate();
355        let (ct, key) = knot_encapsulate(id.public_key()).unwrap();
356        let sealed = holo_seal(key, b"meet at the temple at dusk").unwrap();
357        let key2 = id.decapsulate(&ct).unwrap();
358        let opened = holo_open(key2, &sealed).unwrap();
359        assert_eq!(opened, b"meet at the temple at dusk");
360    }
361
362    #[test]
363    fn holographic_aont_needs_every_fragment() {
364        let secret = b"all-or-nothing holographic payload \x00\xff";
365        let frags = scatter(secret);
366        assert!(frags.len() >= 2);
367        // Full set reconstructs.
368        assert_eq!(gather(&frags).as_deref(), Some(&secret[..]));
369        // Drop any one fragment → unrecoverable.
370        for drop in 0..frags.len() {
371            let partial: Vec<_> = frags
372                .iter()
373                .filter(|f| f.index as usize != drop)
374                .cloned()
375                .collect();
376            assert!(
377                gather(&partial).is_none(),
378                "missing fragment {drop} must break recovery"
379            );
380        }
381    }
382
383    #[test]
384    fn holographic_fragments_leak_nothing_individually() {
385        // A single fragment's block must not equal any plaintext block.
386        let secret = [0x41u8; 64]; // 'AAAA...'
387        let frags = scatter(&secret);
388        for f in &frags {
389            assert_ne!(
390                f.block, [0x41u8; 32],
391                "a lone hologram fragment reveals plaintext"
392            );
393        }
394    }
395}