holographic_memory/core/
mask.rs1use anyhow::{anyhow, ensure, Result};
23use sha2::{Digest, Sha256};
24use zeroize::Zeroize;
25
26use super::entangled::EntangledHVec;
27
28const DOMAIN: &[u8] = b"hms-vector-mask-v1";
29
30pub const MAX_MASK_DIMENSIONS: usize = 1 << 20;
32
33pub const MIN_SALT_LEN: usize = 16;
35
36pub struct VectorMask {
38 forward: Vec<u32>,
39}
40
41impl VectorMask {
42 pub fn derive(passphrase: &[u8], salt: &[u8], dim: usize) -> Result<Self> {
47 ensure!(!passphrase.is_empty(), "mask passphrase must not be empty");
48 ensure!(
49 salt.len() >= MIN_SALT_LEN,
50 "mask salt must be at least {MIN_SALT_LEN} bytes"
51 );
52 ensure!(
53 (1..=MAX_MASK_DIMENSIONS).contains(&dim),
54 "mask dimensions must be in 1..={MAX_MASK_DIMENSIONS}"
55 );
56
57 let mut key = [0u8; 32];
58 argon2::Argon2::default()
59 .hash_password_into(passphrase, salt, &mut key)
60 .map_err(|e| anyhow!("Argon2 key derivation failed: {e}"))?;
61
62 let mut prefix = Sha256::new();
64 prefix.update(DOMAIN);
65 prefix.update(key.as_slice());
66 prefix.update((dim as u64).to_le_bytes());
67 key.zeroize();
68
69 let mut ranked: Vec<([u8; 32], u32)> = (0..dim as u32)
70 .map(|idx| {
71 let mut h = prefix.clone();
72 h.update(idx.to_le_bytes());
73 (h.finalize().into(), idx)
74 })
75 .collect();
76 ranked.sort_unstable();
77
78 let mut forward = vec![0u32; dim];
79 for (rank, (_, idx)) in ranked.iter().enumerate() {
80 forward[*idx as usize] = rank as u32;
81 }
82 for (tag, _) in ranked.iter_mut() {
83 tag.zeroize();
84 }
85 Ok(Self { forward })
86 }
87
88 pub fn dimensions(&self) -> usize {
89 self.forward.len()
90 }
91
92 pub fn apply(&self, vector: &EntangledHVec) -> Result<EntangledHVec> {
94 self.map(vector, &self.forward)
95 }
96
97 pub fn invert(&self, masked: &EntangledHVec) -> Result<EntangledHVec> {
99 let mut inverse = vec![0u32; self.forward.len()];
100 for (idx, &target) in self.forward.iter().enumerate() {
101 inverse[target as usize] = idx as u32;
102 }
103 let result = self.map(masked, &inverse);
104 inverse.zeroize();
105 result
106 }
107
108 fn map(&self, vector: &EntangledHVec, table: &[u32]) -> Result<EntangledHVec> {
109 ensure!(
110 vector.dim == table.len(),
111 "vector dimension {} does not match mask dimension {}",
112 vector.dim,
113 table.len()
114 );
115 let mut indices = Vec::with_capacity(vector.indices.len());
116 for &idx in &vector.indices {
117 let mapped = table
118 .get(idx as usize)
119 .ok_or_else(|| anyhow!("vector index {idx} is out of range"))?;
120 indices.push(*mapped);
121 }
122 indices.sort_unstable();
123 Ok(EntangledHVec::from_indices(indices, vector.dim))
124 }
125}
126
127impl Drop for VectorMask {
128 fn drop(&mut self) {
129 self.forward.zeroize();
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 const DIM: usize = 16384;
138 const SALT: &[u8] = b"0123456789abcdef";
139
140 fn mask(passphrase: &str) -> VectorMask {
141 VectorMask::derive(passphrase.as_bytes(), SALT, DIM).unwrap()
142 }
143
144 #[test]
145 fn forward_table_is_a_permutation() {
146 let m = mask("correct horse");
147 let mut seen = m.forward.clone();
148 seen.sort_unstable();
149 assert!(seen.iter().enumerate().all(|(i, &v)| v == i as u32));
150 }
151
152 #[test]
153 fn similarity_and_binding_are_preserved_exactly() {
154 let m = mask("correct horse");
155 let a = EntangledHVec::new_deterministic(DIM, 1);
156 let b = EntangledHVec::new_deterministic(DIM, 2);
157 let c = EntangledHVec::bundle(&[a.clone(), b.clone()]);
159 let (ma, mb, mc) = (
160 m.apply(&a).unwrap(),
161 m.apply(&b).unwrap(),
162 m.apply(&c).unwrap(),
163 );
164
165 assert!(a.similarity(&c) > 0.1);
166 assert_eq!(a.similarity(&c), ma.similarity(&mc));
167 assert_eq!(a.similarity(&b), ma.similarity(&mb));
168 assert_eq!(ma.indices.len(), a.indices.len());
169 assert_eq!(m.apply(&a.bind(&b)).unwrap().indices, ma.bind(&mb).indices);
170 }
171
172 #[test]
173 fn round_trips_and_hides_original_coordinates() {
174 let m = mask("correct horse");
175 let a = EntangledHVec::new_deterministic(DIM, 7);
176 let masked = m.apply(&a).unwrap();
177 assert_eq!(m.invert(&masked).unwrap().indices, a.indices);
178 assert!(a.similarity(&masked) < 0.1);
179 }
180
181 #[test]
182 fn derivation_is_deterministic_and_key_and_salt_dependent() {
183 let a = EntangledHVec::new_deterministic(DIM, 7);
184 let base = mask("correct horse").apply(&a).unwrap();
185 assert_eq!(
186 mask("correct horse").apply(&a).unwrap().indices,
187 base.indices
188 );
189 assert_ne!(mask("wrong horse").apply(&a).unwrap().indices, base.indices);
190 let other_salt = VectorMask::derive(b"correct horse", b"fedcba9876543210", DIM).unwrap();
191 assert_ne!(other_salt.apply(&a).unwrap().indices, base.indices);
192 }
193
194 #[test]
195 fn rejects_invalid_parameters() {
196 assert!(VectorMask::derive(b"", SALT, DIM).is_err());
197 assert!(VectorMask::derive(b"k", &SALT[..MIN_SALT_LEN - 1], DIM).is_err());
198 assert!(VectorMask::derive(b"k", SALT, 0).is_err());
199 assert!(VectorMask::derive(b"k", SALT, MAX_MASK_DIMENSIONS + 1).is_err());
200 assert!(VectorMask::derive(b"k", SALT, MAX_MASK_DIMENSIONS).is_ok());
201
202 let wrong_dim = EntangledHVec::new_deterministic(DIM / 2, 1);
203 assert!(mask("k").apply(&wrong_dim).is_err());
204 }
205}