1const DOMAIN: &str = "ling-mandala-v1";
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct MandalaParams {
20 pub n_rings: u8, pub n_spokes: u8, pub n_petals: u8, pub n_spiral: u8, pub inner_r: f32, pub outer_r: f32, pub twist: f32, pub hue_offset: f32, pub seed: u64, }
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 pub fn key(&self) -> [u8; 32] {
75 self.base
76 }
77
78 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 pub fn subkey(&self, context: &str) -> [u8; 32] {
89 *blake3::keyed_hash(&self.base, context.as_bytes()).as_bytes()
90 }
91
92 pub fn encryption_key(&self) -> [u8; 32] {
94 self.subkey("ling:encryption:v1")
95 }
96
97 pub fn signing_seed(&self) -> [u8; 32] {
99 self.subkey("ling:signing:v1")
100 }
101
102 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}