use crate::error::PrivacyError;
use crate::keyhop::{aes_gcm_encrypt, aes_gcm_decrypt, hkdf_derive};
use crate::types::{PrivacyProof, ProofScheme};
use sha2::{Digest, Sha256};
use serde::{Deserialize, Serialize};
use reed_solomon_erasure::galois_8::ReedSolomon;
extern crate alloc;
use alloc::{format, collections::BTreeMap, string::{String, ToString}, vec, vec::Vec};
pub const DEFAULT_K: usize = 10;
pub const DEFAULT_N: usize = 15;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultFile {
pub file_id: String,
pub owner_did: String,
pub shards: Vec<Vec<u8>>,
pub tuple_ids: Vec<String>,
pub expiry_ms: u64,
pub proof: PrivacyProof,
pub enc_len: usize,
}
pub struct SanctuaryVault {
files: BTreeMap<String, VaultFile>,
k: usize,
n: usize,
}
impl SanctuaryVault {
pub fn new() -> Self {
Self { files: BTreeMap::new(), k: DEFAULT_K, n: DEFAULT_N }
}
pub fn with_threshold(k: usize, n: usize) -> Self {
assert!(k <= n, "k must be <= n");
assert!(k >= 1, "k must be >= 1");
Self { files: BTreeMap::new(), k, n }
}
pub fn store(
&mut self,
file_id: impl Into<String>,
owner_did: impl Into<String>,
plaintext: &[u8],
chaos_seed: &[u8; 32],
expiry_ms: u64,
) -> Result<String, PrivacyError> {
let file_id = file_id.into();
let owner_did = owner_did.into();
let enc_key = self.derive_enc_key(chaos_seed, &file_id);
let encrypted = aes_gcm_encrypt(&enc_key, plaintext)?;
let enc_len = encrypted.len();
let shards = self.shard(&encrypted)?;
let tuple_ids: Vec<String> = shards.iter().enumerate().map(|(i, shard)| {
let mut hasher = Sha256::new();
hasher.update(file_id.as_bytes());
hasher.update((i as u64).to_le_bytes());
hasher.update(shard);
hex::encode(hasher.finalize())[..16].to_string()
}).collect();
let proof = self.prove_ownership(&file_id, &owner_did, chaos_seed);
let file = VaultFile {
file_id: file_id.clone(),
owner_did,
shards,
tuple_ids,
expiry_ms,
proof,
enc_len,
};
self.files.insert(file_id.clone(), file);
Ok(file_id)
}
pub fn access(
&self,
file_id: &str,
owner_did: &str,
chaos_seed: &[u8; 32],
now_ms: u64,
) -> Result<Vec<u8>, PrivacyError> {
let file = self.files.get(file_id)
.ok_or_else(|| PrivacyError::TupleNotFound(file_id.into()))?;
if file.expiry_ms > 0 && now_ms > file.expiry_ms {
return Err(PrivacyError::TupleExpired(now_ms));
}
if file.owner_did != owner_did {
return Err(PrivacyError::VaultAccessDenied(
format!("DID mismatch: expected {}", file.owner_did)
));
}
if file.shards.len() < self.k {
return Err(PrivacyError::ShardReconstructionFailed {
got: file.shards.len(),
needed: self.k,
});
}
let reconstructed = self.reconstruct(&file.shards)?;
let enc_data = if file.enc_len > 0 && file.enc_len <= reconstructed.len() {
&reconstructed[..file.enc_len]
} else {
&reconstructed[..]
};
let enc_key = self.derive_enc_key(chaos_seed, file_id);
let decrypted = aes_gcm_decrypt(&enc_key, enc_data)?;
Ok(decrypted)
}
pub fn homomorphic_search(
&self,
file_id: &str,
keyword_hash: &[u8; 32],
) -> Result<bool, PrivacyError> {
let file = self.files.get(file_id)
.ok_or_else(|| PrivacyError::TupleNotFound(file_id.into()))?;
let found = file.shards.iter().any(|shard| {
shard.windows(32).any(|w| w == keyword_hash)
});
Ok(found)
}
pub fn file_count(&self) -> usize {
self.files.len()
}
fn derive_enc_key(&self, chaos_seed: &[u8; 32], file_id: &str) -> [u8; 32] {
let mut key = [0u8; 32];
hkdf_derive(chaos_seed, file_id.as_bytes(), b"vault-enc-key-v1", &mut key)
.expect("HKDF derive failed");
key
}
fn shard(&self, data: &[u8]) -> Result<Vec<Vec<u8>>, PrivacyError> {
let k = self.k;
let parity = self.n - k;
let r = ReedSolomon::new(k, parity)
.map_err(|e| PrivacyError::Internal(alloc::format!("RS init failed: {:?}", e)))?;
let shard_size = data.len().div_ceil(k);
let padded_len = shard_size * k;
let mut padded = data.to_vec();
padded.resize(padded_len, 0u8);
let mut shards: Vec<Vec<u8>> = (0..k)
.map(|i| padded[i * shard_size..(i + 1) * shard_size].to_vec())
.collect();
for _ in 0..parity {
shards.push(vec![0u8; shard_size]);
}
r.encode(&mut shards)
.map_err(|e| PrivacyError::Internal(alloc::format!("RS encode failed: {:?}", e)))?;
Ok(shards)
}
fn reconstruct(&self, shards: &[Vec<u8>]) -> Result<Vec<u8>, PrivacyError> {
let k = self.k;
let parity = self.n - k;
let r = ReedSolomon::new(k, parity)
.map_err(|e| PrivacyError::Internal(alloc::format!("RS init failed: {:?}", e)))?;
let total = self.n;
let mut shard_opts: Vec<Option<Vec<u8>>> = (0..total)
.map(|i| {
if i < shards.len() {
Some(shards[i].clone())
} else {
None
}
})
.collect();
r.reconstruct(&mut shard_opts)
.map_err(|_| PrivacyError::ShardReconstructionFailed {
got: shards.len(),
needed: k,
})?;
let mut result = Vec::new();
for s in shard_opts[..k].iter().flatten() {
result.extend_from_slice(s);
}
Ok(result)
}
fn prove_ownership(&self, file_id: &str, owner_did: &str, chaos_seed: &[u8; 32]) -> PrivacyProof {
let mut hasher = Sha256::new();
hasher.update(file_id.as_bytes());
hasher.update(owner_did.as_bytes());
hasher.update(chaos_seed);
hasher.update(b"vault-ownership-v1");
let commitment: [u8; 32] = hasher.finalize().into();
PrivacyProof {
proof_bytes: hex::encode(commitment),
public_inputs: hex::encode(Sha256::digest(owner_did.as_bytes())),
scheme: ProofScheme::Snark,
security_bits: 128,
proof_size: 64,
chsh_value: 0.0,
lyapunov: 4.5,
}
}
}
impl Default for SanctuaryVault {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_store_access() {
let mut vault = SanctuaryVault::with_threshold(2, 3);
let seed = [0u8; 32];
vault.store("file1", "did:wyqcc:alice", b"secret data", &seed, 9_999_999_999).unwrap();
let data = vault.access("file1", "did:wyqcc:alice", &seed, 0).unwrap();
assert_eq!(data, b"secret data");
}
#[test]
fn test_wrong_owner() {
let mut vault = SanctuaryVault::with_threshold(2, 3);
let seed = [0u8; 32];
vault.store("file2", "did:wyqcc:alice", b"data", &seed, 9_999_999_999).unwrap();
assert!(vault.access("file2", "did:wyqcc:bob", &seed, 0).is_err());
}
#[test]
fn test_store_access_larger_data() {
let mut vault = SanctuaryVault::with_threshold(3, 5);
let seed = [0xabu8; 32];
let plaintext = b"This is a longer test payload for Reed-Solomon sharding verification.";
vault.store("file3", "did:wyqcc:carol", plaintext, &seed, 9_999_999_999).unwrap();
let data = vault.access("file3", "did:wyqcc:carol", &seed, 0).unwrap();
assert_eq!(data, plaintext);
}
}