Skip to main content

origin_crypto_sdk/seed/
gen.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Multi-hash seed generation with hash diversity.
4//!
5//! Generates cryptographic seeds by chaining multiple hash families,
6//! ported from the cryp Python prototype. Each construction uses two
7//! independent hash primitives so that a break in one family does not
8//! compromise the seed.
9//!
10//! # Constructions
11//!
12//! | Variant             | Chain                              | Output    |
13//! |---------------------|------------------------------------|-----------|
14//! | `Blake2bShake256`   | keyed-BLAKE2b → SHAKE-256          | 256 bits  |
15//! | `Blake2bSha3_256`   | keyed-BLAKE2b → SHA3-256           | 256 bits  |
16//! | `Blake2bSha512`     | keyed-BLAKE2b → SHA-512            | 512 bits  |
17//! | `Shake256Sha3_256`  | SHAKE-256 → SHA3-256               | 256 bits  |
18//!
19//! # Quick start
20//!
21//! ```
22//! use origin_crypto_sdk::seed::gen::{generate, SeedVariant};
23//!
24//! let result = generate(SeedVariant::Blake2bShake256);
25//! assert_eq!(result.seed.len(), 32);
26//! assert_eq!(result.salt.len(), 16);
27//! ```
28
29use crate::internal::getrandom;
30use crate::internal::zeroize::Zeroize;
31use crate::primitives::sha3::{sha3_256, Shake256};
32use sha3::digest::{ExtendableOutput, Update, XofReader};
33
34// ---------------------------------------------------------------------------
35// Public types
36// ---------------------------------------------------------------------------
37
38/// Seed generation variant — selects the hash chain.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum SeedVariant {
41    /// keyed-BLAKE2b (32 bytes) → SHAKE-256 (32 bytes)
42    Blake2bShake256,
43    /// keyed-BLAKE2b (32 bytes) → SHA3-256 (32 bytes)
44    Blake2bSha3_256,
45    /// keyed-BLAKE2b (32 bytes) → SHA-512 truncated to 32 bytes
46    Blake2bSha512,
47    /// SHAKE-256 (32 bytes) → SHA3-256 (32 bytes)
48    Shake256Sha3_256,
49}
50
51/// Output of [`generate`].
52#[derive(Debug, Clone)]
53pub struct GeneratedSeed {
54    /// The derived seed bytes.
55    pub seed: Vec<u8>,
56    /// Randomly generated salt used in derivation.
57    pub salt: Vec<u8>,
58    /// Which variant produced this seed.
59    pub variant: SeedVariant,
60}
61
62impl Drop for GeneratedSeed {
63    fn drop(&mut self) {
64        self.seed.zeroize();
65    }
66}
67
68// ---------------------------------------------------------------------------
69// Generation
70// ---------------------------------------------------------------------------
71
72/// Generate a seed using the chosen hash chain.
73///
74/// Returns `(seed, salt)` where `seed` is 32 bytes and `salt` is 16 bytes
75/// unless the variant specifies otherwise.
76pub fn generate(variant: SeedVariant) -> GeneratedSeed {
77    generate_with_entropy(variant, None)
78}
79
80/// Generate a seed, optionally using caller-supplied entropy (for testing).
81///
82/// When `entropy` is `None`, fresh random bytes are drawn from the OS RNG.
83pub fn generate_with_entropy(variant: SeedVariant, entropy: Option<&[u8]>) -> GeneratedSeed {
84    // Key material: 32 bytes of randomness (or caller-supplied)
85    let key_bytes = entropy.map(|e| e.to_vec()).unwrap_or_else(|| {
86        let mut buf = vec![0u8; 32];
87        getrandom::fill(&mut buf).expect("OS RNG failed");
88        buf
89    });
90
91    // Message material: 32 bytes of randomness
92    let msg_bytes = if let Some(e) = entropy {
93        // Derive a second pool via SHA3-256 to avoid reusing the same bytes
94        let mut derived = [0u8; 32];
95        derived.copy_from_slice(&sha3_256(e));
96        derived.to_vec()
97    } else {
98        let mut buf = vec![0u8; 32];
99        getrandom::fill(&mut buf).expect("OS RNG failed");
100        buf
101    };
102
103    // Salt: 16 bytes
104    let mut salt = [0u8; 16];
105    if let Some(e) = entropy {
106        // Derive salt from entropy too for deterministic testing
107        let salt_input = [0xDEu8; 16]; // domain separator
108        let hash = sha3_256(&[e, &salt_input[..]].concat());
109        salt.copy_from_slice(&hash[..16]);
110    } else {
111        getrandom::fill(&mut salt).expect("OS RNG failed");
112    }
113
114    let seed = match variant {
115        SeedVariant::Blake2bShake256 => {
116            // Round 1: keyed-BLAKE2b
117            let blake_key: [u8; 32] = key_bytes.as_slice().try_into().unwrap_or([0u8; 32]);
118            let blake_hash = blake3::keyed_hash(&blake_key, &msg_bytes);
119
120            // Round 2: SHAKE-256
121            let mut hasher = Shake256::default();
122            hasher.update(blake_hash.as_bytes());
123            hasher.update(&salt);
124            let mut reader = hasher.finalize_xof();
125            let mut out = [0u8; 32];
126            reader.read(&mut out);
127            out.to_vec()
128        }
129
130        SeedVariant::Blake2bSha3_256 => {
131            // Round 1: keyed-BLAKE2b
132            let blake_key: [u8; 32] = key_bytes.as_slice().try_into().unwrap_or([0u8; 32]);
133            let blake_hash = blake3::keyed_hash(&blake_key, &msg_bytes);
134
135            // Round 2: SHA3-256
136            let mut input = Vec::with_capacity(32 + 16);
137            input.extend_from_slice(blake_hash.as_bytes());
138            input.extend_from_slice(&salt);
139            sha3_256(&input).to_vec()
140        }
141
142        SeedVariant::Blake2bSha512 => {
143            // Round 1: keyed-BLAKE2b
144            let blake_key: [u8; 32] = key_bytes.as_slice().try_into().unwrap_or([0u8; 32]);
145            let blake_hash = blake3::keyed_hash(&blake_key, &msg_bytes);
146
147            // Round 2: SHA-512, truncate to 32 bytes (256 bits)
148            use sha2::{Digest, Sha512};
149            let mut hasher = Sha512::new();
150            hasher.update(blake_hash.as_bytes());
151            hasher.update(&salt);
152            let hash = hasher.finalize();
153            hash[..32].to_vec()
154        }
155
156        SeedVariant::Shake256Sha3_256 => {
157            // Round 1: SHAKE-256
158            let mut hasher = Shake256::default();
159            hasher.update(&key_bytes);
160            hasher.update(&msg_bytes);
161            let mut reader = hasher.finalize_xof();
162            let mut shake_out = [0u8; 32];
163            reader.read(&mut shake_out);
164
165            // Round 2: SHA3-256
166            let mut input = Vec::with_capacity(32 + 16);
167            input.extend_from_slice(&shake_out);
168            input.extend_from_slice(&salt);
169            sha3_256(&input).to_vec()
170        }
171    };
172
173    // Wipe intermediate material
174    drop(key_bytes);
175    drop(msg_bytes);
176
177    GeneratedSeed {
178        seed,
179        salt: salt.to_vec(),
180        variant,
181    }
182}
183
184// ---------------------------------------------------------------------------
185// Tests
186// ---------------------------------------------------------------------------
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn determinism() {
194        let entropy = [0xABu8; 64];
195        for &variant in &[
196            SeedVariant::Blake2bShake256,
197            SeedVariant::Blake2bSha3_256,
198            SeedVariant::Blake2bSha512,
199            SeedVariant::Shake256Sha3_256,
200        ] {
201            let a = generate_with_entropy(variant, Some(&entropy));
202            let b = generate_with_entropy(variant, Some(&entropy));
203            assert_eq!(a.seed, b.seed, "determinism failed for {variant:?}");
204        }
205    }
206
207    #[test]
208    fn all_variants_produce_32_bytes() {
209        for &variant in &[
210            SeedVariant::Blake2bShake256,
211            SeedVariant::Blake2bSha3_256,
212            SeedVariant::Blake2bSha512,
213            SeedVariant::Shake256Sha3_256,
214        ] {
215            let gen = generate(variant);
216            assert_eq!(gen.seed.len(), 32, "seed length for {variant:?}");
217            assert_eq!(gen.salt.len(), 16, "salt length for {variant:?}");
218        }
219    }
220
221    #[test]
222    fn different_variants_produce_different_seeds() {
223        let entropy = [0x42u8; 64];
224        let variants = [
225            SeedVariant::Blake2bShake256,
226            SeedVariant::Blake2bSha3_256,
227            SeedVariant::Blake2bSha512,
228            SeedVariant::Shake256Sha3_256,
229        ];
230        let seeds: Vec<Vec<u8>> = variants
231            .iter()
232            .map(|v| generate_with_entropy(*v, Some(&entropy)).seed.clone())
233            .collect();
234
235        for i in 0..seeds.len() {
236            for j in (i + 1)..seeds.len() {
237                assert_ne!(
238                    seeds[i], seeds[j],
239                    "variants {i} and {j} produced same seed"
240                );
241            }
242        }
243    }
244
245    #[test]
246    fn randomness() {
247        // Two random seeds must differ (probability of collision ~2^{-256})
248        let a = generate(SeedVariant::Blake2bShake256);
249        let b = generate(SeedVariant::Blake2bShake256);
250        assert_ne!(a.seed, b.seed);
251    }
252}