spectre/
algorithm.rs

1use hmac::{Hmac, Mac};
2use sha2::{Sha256, Digest};
3use scrypt::{scrypt, Params};
4use crate::error::{Result, SpectreError};
5use crate::types::*;
6
7type HmacSha256 = Hmac<Sha256>;
8
9const SPECTRE_N: u32 = 32768;
10const SPECTRE_R: u32 = 8;
11const SPECTRE_P: u32 = 2;
12const SPECTRE_DK_LEN: usize = 64;
13
14#[derive(Clone)]
15pub struct SpectreUserKey {
16    pub key_id: [u8; 32],
17    pub key_data: Vec<u8>,
18    pub algorithm: SpectreAlgorithm,
19}
20
21/// Derive a user key from the user's name and secret
22pub fn spectre_user_key(
23    user_name: &str,
24    user_secret: &str,
25    algorithm: SpectreAlgorithm,
26) -> Result<SpectreUserKey> {
27    if !(SPECTRE_ALGORITHM_FIRST..=SPECTRE_ALGORITHM_LAST).contains(&algorithm) {
28        return Err(SpectreError::InvalidAlgorithm(algorithm));
29    }
30
31    // Create salt: "com.lyndir.masterpassword" + user_name length + user_name
32    let scope = b"com.lyndir.masterpassword";
33    let name_bytes = user_name.as_bytes();
34    let name_len = (name_bytes.len() as u32).to_be_bytes();
35    
36    let mut salt = Vec::with_capacity(scope.len() + 4 + name_bytes.len());
37    salt.extend_from_slice(scope);
38    salt.extend_from_slice(&name_len);
39    salt.extend_from_slice(name_bytes);
40
41    // Derive key using scrypt
42    let mut key_data = vec![0u8; SPECTRE_DK_LEN];
43    let params = Params::new(
44        (SPECTRE_N as f64).log2() as u8,
45        SPECTRE_R,
46        SPECTRE_P,
47        SPECTRE_DK_LEN,
48    ).map_err(|_| SpectreError::KeyDerivationFailed)?;
49    
50    scrypt(user_secret.as_bytes(), &salt, &params, &mut key_data)
51        .map_err(|_| SpectreError::KeyDerivationFailed)?;
52
53    // Generate key ID (SHA256 of the key)
54    let mut hasher = Sha256::new();
55    hasher.update(&key_data);
56    let key_id_vec = hasher.finalize();
57    let mut key_id = [0u8; 32];
58    key_id.copy_from_slice(&key_id_vec);
59
60    Ok(SpectreUserKey {
61        key_id,
62        key_data,
63        algorithm,
64    })
65}
66
67/// Generate a site password
68pub fn spectre_site_result(
69    user_key: &SpectreUserKey,
70    site_name: &str,
71    result_type: SpectreResultType,
72    result_param: Option<&str>,
73    key_counter: SpectreCounter,
74    key_purpose: SpectreKeyPurpose,
75    key_context: Option<&str>,
76) -> Result<String> {
77    // Derive site key
78    let site_key = spectre_site_key(
79        user_key,
80        site_name,
81        key_counter,
82        key_purpose,
83        key_context,
84    )?;
85
86    // For stateful types, handle differently
87    if result_type.is_stateful() {
88        if let Some(state) = result_param {
89            return Ok(state.to_string());
90        }
91        return Err(SpectreError::PasswordGenerationFailed);
92    }
93
94    // Generate password from template
95    let templates = result_type.template();
96    if templates.is_empty() {
97        return Err(SpectreError::PasswordGenerationFailed);
98    }
99
100    // Select template based on seed
101    let template_index = site_key[0] as usize % templates.len();
102    let template = templates[template_index];
103
104    // Generate password from template
105    let mut password = String::new();
106    let mut seed_index = 1; // Start at 1 since 0 was used for template selection
107    for template_char in template.chars() {
108        let char_class = char_class_for_template(template_char);
109        if char_class.is_empty() {
110            password.push(template_char);
111        } else {
112            let char_index = site_key[seed_index % site_key.len()] as usize % char_class.len();
113            password.push(char_class[char_index]);
114            seed_index += 1;
115        }
116    }
117
118    Ok(password)
119}
120
121/// Derive a site-specific key
122fn spectre_site_key(
123    user_key: &SpectreUserKey,
124    site_name: &str,
125    key_counter: SpectreCounter,
126    key_purpose: SpectreKeyPurpose,
127    key_context: Option<&str>,
128) -> Result<Vec<u8>> {
129    let scope: &[u8] = match key_purpose {
130        SpectreKeyPurpose::Authentication => b"com.lyndir.masterpassword",
131        SpectreKeyPurpose::Identification => b"com.lyndir.masterpassword.login",
132        SpectreKeyPurpose::Recovery => b"com.lyndir.masterpassword.answer",
133    };
134
135    let site_bytes = site_name.as_bytes();
136    let site_len = (site_bytes.len() as u32).to_be_bytes();
137
138    let mut salt = Vec::new();
139    salt.extend_from_slice(scope);
140    salt.extend_from_slice(&site_len);
141    salt.extend_from_slice(site_bytes);
142    salt.extend_from_slice(&key_counter.to_be_bytes());
143
144    if let Some(context) = key_context {
145        if !context.is_empty() {
146            let context_bytes = context.as_bytes();
147            let context_len = (context_bytes.len() as u32).to_be_bytes();
148            salt.extend_from_slice(&context_len);
149            salt.extend_from_slice(context_bytes);
150        }
151    }
152
153    // Use HMAC-SHA256 to derive site key
154    let mut mac = HmacSha256::new_from_slice(&user_key.key_data)
155        .map_err(|_| SpectreError::KeyDerivationFailed)?;
156    mac.update(&salt);
157    let result = mac.finalize();
158    
159    Ok(result.into_bytes().to_vec())
160}
161
162/// Generate an identicon for a user
163pub fn spectre_identicon(user_name: &str, user_secret: &str) -> Result<[u8; 4]> {
164    let user_key = spectre_user_key(user_name, user_secret, SPECTRE_ALGORITHM_CURRENT)?;
165    
166    let mut identicon = [0u8; 4];
167    identicon.copy_from_slice(&user_key.key_data[0..4]);
168    
169    Ok(identicon)
170}
171
172/// Render an identicon as a visual string
173pub fn spectre_identicon_render(identicon: [u8; 4]) -> String {
174    const COLORS: &[&str] = &[
175        "🔴", "🟠", "🟡", "🟢", "🔵", "🟣", "🟤", "⚫",
176        "🔴", "🟠", "🟡", "🟢", "🔵", "🟣", "🟤", "⚪",
177    ];
178    
179    let mut result = String::new();
180    for &byte in &identicon {
181        let index = (byte % 16) as usize;
182        result.push_str(COLORS[index]);
183    }
184    
185    result
186}
187
188/// Encrypt a personal password (stateful)
189pub fn spectre_site_state(
190    user_key: &SpectreUserKey,
191    site_name: &str,
192    _result_type: SpectreResultType,
193    plaintext: &str,
194    key_counter: SpectreCounter,
195    key_purpose: SpectreKeyPurpose,
196    key_context: Option<&str>,
197) -> Result<String> {
198    // For now, just base64 encode (in production, should use proper encryption)
199    // This is a simplified version - the C implementation uses proper AES encryption
200    use std::fmt::Write;
201    
202    let site_key = spectre_site_key(
203        user_key,
204        site_name,
205        key_counter,
206        key_purpose,
207        key_context,
208    )?;
209    
210    // XOR encrypt (simplified - should use AES in production)
211    let mut encrypted = Vec::new();
212    for (i, &byte) in plaintext.as_bytes().iter().enumerate() {
213        encrypted.push(byte ^ site_key[i % site_key.len()]);
214    }
215    
216    // Hex encode
217    let mut result = String::new();
218    for byte in encrypted {
219        write!(&mut result, "{:02x}", byte).unwrap();
220    }
221    
222    Ok(result)
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_user_key_derivation() {
231        let result = spectre_user_key("Abdulrhman A", "nice work mate", SPECTRE_ALGORITHM_CURRENT);
232        assert!(result.is_ok());
233    }
234
235    #[test]
236    fn test_password_generation() {
237        let user_key = spectre_user_key("Abdulrhman A", "nice work mate", SPECTRE_ALGORITHM_CURRENT).unwrap();
238        let password = spectre_site_result(
239            &user_key,
240            "masterpasswordapp.com",
241            SpectreResultType::LongPassword,
242            None,
243            1,
244            SpectreKeyPurpose::Authentication,
245            None,
246        );
247        assert!(password.is_ok());
248    }
249}
250