1use crate::hybrid::{self, HybridKeypair};
29use crate::symmetric::XChaCha20;
30use sha3::{Digest, Sha3_256};
31
32pub 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#[derive(Clone, Debug, PartialEq)]
58pub struct KnotShape {
59 pub p: u32,
61 pub q: u32,
63 pub major_r: f32,
65 pub minor_r: f32,
67 pub volume: f32,
69 pub points: Vec<[f32; 3]>,
71}
72
73impl KnotShape {
74 pub const SAMPLES: usize = 256;
76
77 pub fn from_bytes(data: &[u8]) -> Self {
79 Self::from_digest(&holo_hash(data))
80 }
81
82 pub fn from_digest(d: &[u8; 32]) -> Self {
84 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 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 pub fn label(&self) -> String {
123 format!("knot-{}_{}-v{:.1}", self.p, self.q, self.volume)
124 }
125}
126
127pub struct KnotIdentity {
135 inner: HybridKeypair,
136 public: Vec<u8>,
137}
138
139impl KnotIdentity {
140 pub fn generate() -> Self {
142 let inner = HybridKeypair::generate();
143 let public = inner.public_key();
144 Self { inner, public }
145 }
146
147 pub fn public_key(&self) -> &[u8] {
149 &self.public
150 }
151
152 pub fn public_knot(&self) -> KnotShape {
154 KnotShape::from_bytes(&self.public)
155 }
156
157 pub fn decapsulate(&self, ciphertext: &[u8]) -> Result<[u8; 32], &'static str> {
159 self.inner.decapsulate(ciphertext)
160 }
161}
162
163pub fn knot_encapsulate(public_key: &[u8]) -> Result<(Vec<u8>, [u8; 32]), &'static str> {
166 hybrid::encapsulate(public_key)
167}
168
169pub fn knot_for_public_key(public_key: &[u8]) -> KnotShape {
172 KnotShape::from_bytes(public_key)
173}
174
175pub fn holo_seal(key: [u8; 32], plaintext: &[u8]) -> Result<Vec<u8>, &'static str> {
182 XChaCha20::new(key).encrypt(plaintext)
183}
184
185pub fn holo_open(key: [u8; 32], ciphertext: &[u8]) -> Result<Vec<u8>, &'static str> {
187 XChaCha20::new(key).decrypt(ciphertext)
188}
189
190#[derive(Clone, Debug, PartialEq)]
193pub struct HoloFragment {
194 pub index: u32,
196 pub coord: [f32; 4],
198 pub block: [u8; 32],
200}
201
202const HOLO_BLOCK: usize = 32;
203
204fn 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
212pub 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 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 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 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
267pub 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 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; 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 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
313fn 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 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 assert_eq!(gather(&frags).as_deref(), Some(&secret[..]));
369 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 let secret = [0x41u8; 64]; 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}