gun/sea/
work.rs

1//! Proof of Work / Content Hashing
2//! Based on Gun.js sea/work.js
3//! Provides PBKDF2 key derivation and SHA-256 hashing for proof-of-work and content addressing
4
5use super::SeaError;
6use base64::{engine::general_purpose, Engine as _};
7use pbkdf2::pbkdf2_hmac;
8use rand::RngCore;
9use sha2::{Digest, Sha256};
10use std::sync::Arc;
11use tokio::task;
12
13/// Options for SEA.work()
14#[derive(Clone, Debug)]
15pub struct WorkOptions {
16    /// Algorithm name: "PBKDF2" (default) or "SHA-256" (when name starts with "sha")
17    pub name: Option<String>,
18    /// Number of iterations for PBKDF2 (default: 100000)
19    pub iterations: Option<u32>,
20    /// Salt for PBKDF2 (auto-generated if not provided)
21    pub salt: Option<Vec<u8>>,
22    /// Hash algorithm for PBKDF2 (default: SHA-256)
23    pub hash: Option<String>,
24    /// Output length in bits (default: 512 bits = 64 bytes)
25    pub length: Option<usize>,
26    /// Output encoding (default: "base64")
27    pub encode: Option<String>,
28}
29
30impl Default for WorkOptions {
31    fn default() -> Self {
32        Self {
33            name: Some("PBKDF2".to_string()),
34            iterations: Some(100_000),
35            salt: None,
36            hash: Some("SHA-256".to_string()),
37            length: Some(512), // 64 bytes * 8 bits
38            encode: Some("base64".to_string()),
39        }
40    }
41}
42
43/// Compute proof-of-work or content hash
44/// Based on Gun.js SEA.work()
45///
46/// # Arguments
47/// * `data` - Data to hash/derive (can be string or bytes)
48/// * `salt_or_pair` - Optional salt (Vec<u8>) or KeyPair (for epub extraction)
49/// * `opt` - Work options
50///
51/// # Returns
52/// Base64-encoded hash or derived key
53///
54/// # Examples
55/// ```rust
56/// use gun::sea::{work, WorkOptions};
57///
58/// // SHA-256 hash for content addressing
59/// let hash = work(b"hello world", None, WorkOptions {
60///     name: Some("SHA-256".to_string()),
61///     ..Default::default()
62/// }).await?;
63///
64/// // PBKDF2 key derivation
65/// let key = work(b"password", Some(vec![1,2,3,4,5,6,7,8,9]), WorkOptions::default()).await?;
66/// ```
67pub async fn work(
68    data: &[u8],
69    salt_or_pair: Option<Vec<u8>>,
70    opt: WorkOptions,
71) -> Result<String, SeaError> {
72    let opt = Arc::new(opt);
73    let data = data.to_vec();
74
75    // Check if SHA-256 mode (when name starts with "sha")
76    let name_lower = opt
77        .name
78        .as_ref()
79        .map(|n| n.to_lowercase())
80        .unwrap_or_else(|| "pbkdf2".to_string());
81
82    if name_lower.starts_with("sha") {
83        // SHA-256 hashing mode
84        return task::spawn_blocking(move || {
85            let mut hasher = Sha256::new();
86            hasher.update(&data);
87            let hash = hasher.finalize();
88
89            let encoded = match opt.encode.as_deref().unwrap_or("base64") {
90                "base64" => general_purpose::STANDARD_NO_PAD.encode(hash),
91                "hex" => hex::encode(hash),
92                _ => general_purpose::STANDARD_NO_PAD.encode(hash),
93            };
94
95            Ok(encoded)
96        })
97        .await
98        .map_err(|e| SeaError::Crypto(format!("Task join error: {}", e)))?
99        .map_err(|e: SeaError| e);
100    }
101
102    // PBKDF2 key derivation mode (default)
103    let salt = if let Some(ref salt) = salt_or_pair {
104        salt.clone()
105    } else if let Some(ref opt_salt) = opt.salt {
106        opt_salt.clone()
107    } else {
108        // Generate random 9-byte salt (matching Gun.js)
109        let mut salt_bytes = vec![0u8; 9];
110        rand::thread_rng().fill_bytes(&mut salt_bytes);
111        salt_bytes
112    };
113
114    let iterations = opt.iterations.unwrap_or(100_000);
115    let length_bits = opt.length.unwrap_or(512);
116    let length_bytes = length_bits / 8;
117
118    // Perform PBKDF2 in blocking task (CPU-intensive)
119    let result = task::spawn_blocking(move || {
120        let mut output = vec![0u8; length_bytes];
121        pbkdf2_hmac::<Sha256>(&data, &salt, iterations, &mut output);
122        output
123    })
124    .await
125    .map_err(|e| SeaError::Crypto(format!("Task join error: {}", e)))?;
126
127    // Encode result
128    let encoded = match opt.encode.as_deref().unwrap_or("base64") {
129        "base64" => general_purpose::STANDARD_NO_PAD.encode(&result),
130        "hex" => hex::encode(&result),
131        _ => general_purpose::STANDARD_NO_PAD.encode(&result),
132    };
133
134    Ok(encoded)
135}
136
137/// Convenience function: work with string data
138pub async fn work_string(
139    data: &str,
140    salt_or_pair: Option<Vec<u8>>,
141    opt: WorkOptions,
142) -> Result<String, SeaError> {
143    work(data.as_bytes(), salt_or_pair, opt).await
144}
145
146/// Convenience function: work with JSON-serializable data
147pub async fn work_json<T: serde::Serialize>(
148    data: &T,
149    salt_or_pair: Option<Vec<u8>>,
150    opt: WorkOptions,
151) -> Result<String, SeaError> {
152    let json_str = serde_json::to_string(data)
153        .map_err(|e| SeaError::Crypto(format!("JSON serialization error: {}", e)))?;
154    work(json_str.as_bytes(), salt_or_pair, opt).await
155}