Skip to main content

ling_crypto/
mandala.rs

1//! Mandala Hash — Ling's custom geometric key derivation.
2//!
3//! A MandalaHash derives cryptographic key material from a set of visual
4//! geometric parameters (rings, spokes, petals, spiral arms, scale).
5//! The same mandala pattern always yields the same key, making it possible
6//! to use visual/geometric patterns as cryptographic identities.
7//!
8//! Construction:
9//!   1. Serialize parameters deterministically with domain label
10//!   2. Hash with BLAKE3 (collision-resistant, fast, XOF-capable)
11//!   3. Expand to requested length via BLAKE3 XOF
12//!
13//! This is a one-way function — you cannot recover the parameters from the key.
14
15const DOMAIN: &str = "ling-mandala-v1";
16
17/// Parameters describing a mandala visual pattern.
18#[derive(Debug, Clone, PartialEq)]
19pub struct MandalaParams {
20    pub n_rings: u8,     // number of concentric rings (1–255)
21    pub n_spokes: u8,    // chakra spokes (1–255)
22    pub n_petals: u8,    // lotus petals (1–255)
23    pub n_spiral: u8,    // spiral arms (0–255)
24    pub inner_r: f32,    // inner radius (normalized 0.0–1.0)
25    pub outer_r: f32,    // outer radius (normalized 0.0–1.0)
26    pub twist: f32,      // spiral twist factor
27    pub hue_offset: f32, // starting hue (0.0–1.0, visual only — included for uniqueness)
28    pub seed: u64,       // extra entropy / version
29}
30
31impl MandalaParams {
32    pub fn new(n_rings: u8, n_spokes: u8, n_petals: u8) -> Self {
33        Self {
34            n_rings,
35            n_spokes,
36            n_petals,
37            n_spiral: 4,
38            inner_r: 0.1,
39            outer_r: 0.9,
40            twist: 0.0,
41            hue_offset: 0.0,
42            seed: 0,
43        }
44    }
45
46    fn serialize(&self) -> Vec<u8> {
47        let mut v = DOMAIN.as_bytes().to_vec();
48        v.push(self.n_rings);
49        v.push(self.n_spokes);
50        v.push(self.n_petals);
51        v.push(self.n_spiral);
52        v.extend_from_slice(&self.inner_r.to_bits().to_le_bytes());
53        v.extend_from_slice(&self.outer_r.to_bits().to_le_bytes());
54        v.extend_from_slice(&self.twist.to_bits().to_le_bytes());
55        v.extend_from_slice(&self.hue_offset.to_bits().to_le_bytes());
56        v.extend_from_slice(&self.seed.to_le_bytes());
57        v
58    }
59}
60
61pub struct MandalaHash {
62    params: MandalaParams,
63    base: [u8; 32],
64}
65
66impl MandalaHash {
67    pub fn new(params: MandalaParams) -> Self {
68        let serialized = params.serialize();
69        let base = *blake3::hash(&serialized).as_bytes();
70        Self { params, base }
71    }
72
73    /// 32-byte key derived from this mandala.
74    pub fn key(&self) -> [u8; 32] {
75        self.base
76    }
77
78    /// Derive `len` bytes of key material (XOF expansion).
79    pub fn expand(&self, len: usize) -> Vec<u8> {
80        let mut hasher = blake3::Hasher::new_keyed(&self.base);
81        hasher.update(b"expand");
82        let mut out = vec![0u8; len];
83        hasher.finalize_xof().fill(&mut out);
84        out
85    }
86
87    /// Derive a named subkey (different contexts → independent keys).
88    pub fn subkey(&self, context: &str) -> [u8; 32] {
89        *blake3::keyed_hash(&self.base, context.as_bytes()).as_bytes()
90    }
91
92    /// Derive an encryption key (AES-GCM or XChaCha20 ready).
93    pub fn encryption_key(&self) -> [u8; 32] {
94        self.subkey("ling:encryption:v1")
95    }
96
97    /// Derive a signing seed (Ed25519 ready).
98    pub fn signing_seed(&self) -> [u8; 32] {
99        self.subkey("ling:signing:v1")
100    }
101
102    /// Derive a KEM seed (ML-KEM ready).
103    pub fn kem_seed(&self) -> [u8; 32] {
104        self.subkey("ling:kem:v1")
105    }
106
107    pub fn params(&self) -> &MandalaParams {
108        &self.params
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn deterministic() {
118        let p = MandalaParams::new(8, 12, 8);
119        let h1 = MandalaHash::new(p.clone());
120        let h2 = MandalaHash::new(p);
121        assert_eq!(h1.key(), h2.key());
122    }
123
124    #[test]
125    fn different_params_different_keys() {
126        let h1 = MandalaHash::new(MandalaParams::new(8, 12, 8));
127        let h2 = MandalaHash::new(MandalaParams::new(9, 12, 8));
128        assert_ne!(h1.key(), h2.key());
129    }
130
131    #[test]
132    fn subkeys_independent() {
133        let h = MandalaHash::new(MandalaParams::new(8, 12, 8));
134        assert_ne!(h.encryption_key(), h.signing_seed());
135        assert_ne!(h.signing_seed(), h.kem_seed());
136    }
137}