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
21pub 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 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 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, ¶ms, &mut key_data)
51 .map_err(|_| SpectreError::KeyDerivationFailed)?;
52
53 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
67pub 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 let site_key = spectre_site_key(
79 user_key,
80 site_name,
81 key_counter,
82 key_purpose,
83 key_context,
84 )?;
85
86 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 let templates = result_type.template();
96 if templates.is_empty() {
97 return Err(SpectreError::PasswordGenerationFailed);
98 }
99
100 let template_index = site_key[0] as usize % templates.len();
102 let template = templates[template_index];
103
104 let mut password = String::new();
106 let mut seed_index = 1; 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
121fn 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 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
162pub 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
172pub 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
188pub 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 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 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 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