CH-J Toolsszerverkezelés SSH felett
Menü
Közzétett forrás

CH-J Tools

Könyvtárak és fájlok böngészése egy adott alkalmazás kiadásához.

Letöltés forrás ZIP
CH-J / Server Manager 1704 fájlok
0.1.2-beta Hozzáadva: · Beépített:
vault.rs Hozzáadva: Nem rögzített dátum 23,4 KB · 558 sorok
Kód Fájl letöltése
1//! Encrypted local vault — byte-compatible port of `security/vaultStore.js`.
2//!
3//! Format (unchanged, so the 0.1.0 master password keeps working):
4//! * `vault/core-v1.meta.json`: scrypt parameters (N=32768, r=8, p=1, 32 B key) and a 16 B salt.
5//! * `vault/core-v1.data.json`: AES-256-GCM, 12 B IV, 16 B tag, AAD `CHJ_CORE_VAULT_V1`,
6//! plaintext = compact JSON `{schemaVersion:1, profiles, hostKeys, secrets}`.
7//! The key exists only in Core memory and is zeroized on lock.
9use crate::core::error::{CoreError, CoreResult};
10use crate::core::storage::atomic_write_json;
11use aes_gcm::aead::{Aead, KeyInit, Payload};
12use aes_gcm::{Aes256Gcm, Nonce};
13use base64::engine::general_purpose::STANDARD as B64;
14use base64::Engine;
15use parking_lot::Mutex;
16use rand::RngCore;
17use serde_json::{json, Value};
18use sha2::{Digest, Sha256};
19use std::fs;
20use std::path::{Path, PathBuf};
21use zeroize::{Zeroize, Zeroizing};
23pub const KEY_LENGTH: usize = 32;
24const SALT_LENGTH: usize = 16;
25const IV_LENGTH: usize = 12;
26const TAG_LENGTH: usize = 16;
27const AAD: &[u8] = b"CHJ_CORE_VAULT_V1";
28const SCRYPT_N: u64 = 1 << 15;
29const SCRYPT_R: u32 = 8;
30const SCRYPT_P: u32 = 1;
31pub const MIN_PASSWORD_LENGTH: usize = 4;
32pub const MAX_PASSWORD_LENGTH: usize = 64;
34pub type VaultKey = Zeroizing<[u8; KEY_LENGTH]>;
36pub fn initial_vault() -> Value {
37 json!({ "schemaVersion": 1, "profiles": [], "hostKeys": {}, "secrets": {} })
40pub fn validate_password(password: &str) -> CoreResult<String> {
41 if password.contains('\0') {
42 return Err(CoreError::plain("Password contains an invalid character."));
43 }
44 let length = password.chars().count();
45 if !(MIN_PASSWORD_LENGTH..=MAX_PASSWORD_LENGTH).contains(&length) {
46 return Err(CoreError::plain(format!(
47 "Vault password must contain {MIN_PASSWORD_LENGTH} to {MAX_PASSWORD_LENGTH} characters."
48 )));
49 }
50 Ok(password.to_string())
53pub fn validate_vault_data(mut value: Value) -> CoreResult<Value> {
54 let object = value.as_object_mut().ok_or_else(|| CoreError::plain("Vault data is invalid."))?;
55 if object.get("schemaVersion").and_then(Value::as_f64) != Some(1.0) {
56 return Err(CoreError::plain("Unsupported vault data schema."));
57 }
58 if !object.get("profiles").map(Value::is_array).unwrap_or(false) {
59 return Err(CoreError::plain("Vault profiles are invalid."));
60 }
61 if !object.get("hostKeys").map(Value::is_object).unwrap_or(false) {
62 return Err(CoreError::plain("Vault host keys are invalid."));
63 }
64 if !object.get("secrets").map(Value::is_object).unwrap_or(false) {
65 object.insert("secrets".into(), json!({}));
66 }
67 Ok(value)
70fn derive_key(password: &str, salt: &[u8], n: u64, r: u32, p: u32) -> CoreResult<VaultKey> {
71 if n < 2 || !n.is_power_of_two() || n > (1 << 20) || r == 0 || r > 32 || p == 0 || p > 16 {
72 return Err(CoreError::plain("Unsupported vault KDF parameters."));
73 }
74 let log_n = n.trailing_zeros() as u8;
75 let params = scrypt::Params::new(log_n, r, p, KEY_LENGTH).map_err(|_| CoreError::plain("Unsupported vault KDF parameters."))?;
76 let mut key: VaultKey = Zeroizing::new([0u8; KEY_LENGTH]);
77 scrypt::scrypt(password.as_bytes(), salt, &params, key.as_mut()).map_err(|_| CoreError::plain("Key derivation failed."))?;
78 Ok(key)
81#[derive(Default)]
82struct Session {
83 generation: u64,
84 // Authenticated host trust is retained in RAM across lock, never written outside the vault.
85 host_keys: serde_json::Map<String, Value>,
86 key: Option<VaultKey>,
87 data: Option<Value>,
90pub struct VaultStore {
91 root: PathBuf,
92 meta_path: PathBuf,
93 data_path: PathBuf,
94 session: Mutex<Session>,
95 /// Serializes read-modify-write updates: two concurrent SSH connects must not lose a trusted host key.
96 update_lock: Mutex<()>,
99#[derive(serde::Serialize, Clone, Debug)]
100pub struct VaultStatus {
101 pub initialized: bool,
102 #[serde(rename = "needsSetup")]
103 pub needs_setup: bool,
104 pub damaged: bool,
105 pub unlocked: bool,
108fn locked_error() -> CoreError {
109 CoreError::new("VAULT_LOCKED", "Vault is locked.")
112impl VaultStore {
113 pub fn new(storage_root: &Path) -> Self {
114 let root = storage_root.join("vault");
115 Self {
116 update_lock: Mutex::new(()),
117 meta_path: root.join("core-v1.meta.json"),
118 data_path: root.join("core-v1.data.json"),
119 root,
120 session: Mutex::new(Session::default()),
121 }
122 }
124 pub fn generation(&self) -> u64 {
125 self.session.lock().generation
126 }
128 pub fn status(&self) -> VaultStatus {
129 let meta = self.meta_path.exists();
130 let data = self.data_path.exists();
131 let session = self.session.lock();
132 VaultStatus {
133 initialized: meta && data,
134 needs_setup: !meta && !data,
135 damaged: meta != data,
136 unlocked: session.key.is_some() && session.data.is_some(),
137 }
138 }
140 pub fn is_unlocked(&self) -> bool {
141 let session = self.session.lock();
142 session.key.is_some() && session.data.is_some()
143 }
145 fn read_meta(&self) -> CoreResult<Value> {
146 Ok(serde_json::from_str(&fs::read_to_string(&self.meta_path)?)?)
147 }
149 /// Stable identity of this vault instance (same formula as 0.1.0, used by biometric enrollment).
150 pub fn instance_id(&self) -> Option<String> {
151 if !self.status().initialized {
152 return None;
153 }
154 let meta = self.read_meta().ok()?;
155 Self::meta_identity(&meta)
156 }
158 fn meta_identity(meta: &Value) -> Option<String> {
159 let identity = json!([meta.get("formatVersion"), meta.get("createdAt"), meta.get("salt")]);
160 let text = serde_json::to_string(&identity).ok()?;
161 Some(hex::encode(Sha256::digest(text.as_bytes())))
162 }
164 pub async fn create(&self, password: &str) -> CoreResult<VaultStatus> {
165 let generation = self.generation();
166 let status = self.status();
167 if status.initialized {
168 return Err(CoreError::plain("Vault is already initialized."));
169 }
170 if status.damaged {
171 return Err(CoreError::plain("Vault files are incomplete. Restore or remove them before setup."));
172 }
173 let normalized = validate_password(password)?;
174 let mut salt = [0u8; SALT_LENGTH];
175 rand::rngs::OsRng.fill_bytes(&mut salt);
176 let key = tokio::task::spawn_blocking(move || derive_key(&normalized, &salt, SCRYPT_N, SCRYPT_R, SCRYPT_P))
177 .await
178 .map_err(|_| CoreError::plain("Key derivation failed."))??;
179 let mut session = self.session.lock();
180 if generation != session.generation || self.meta_path.exists() || self.data_path.exists() {
181 return Err(CoreError::plain("Vault operation cancelled."));
182 }
183 let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
184 let data = initial_vault();
185 let result = (|| -> CoreResult<()> {
186 atomic_write_json(
187 &self.meta_path,
188 &json!({
189 "formatVersion": 1,
190 "dataSchemaVersion": 1,
191 "createdAt": now,
192 "updatedAt": now,
193 "cipher": "aes-256-gcm",
194 "kdf": { "name": "scrypt", "N": SCRYPT_N, "r": SCRYPT_R, "p": SCRYPT_P, "keyLength": KEY_LENGTH },
195 "salt": B64.encode(salt)
196 }),
197 )?;
198 self.write_encrypted(&data, &key)
199 })();
200 if let Err(error) = result {
201 let _ = fs::remove_file(&self.meta_path);
202 let _ = fs::remove_file(&self.data_path);
203 return Err(error);
204 }
205 Self::set_session(&mut session, key, data);
206 drop(session);
207 Ok(self.status())
208 }
210 fn kdf_from_meta(meta: &Value) -> CoreResult<(Vec<u8>, u64, u32, u32)> {
211 if meta.get("formatVersion").and_then(Value::as_f64) != Some(1.0)
212 || meta.get("cipher").and_then(Value::as_str) != Some("aes-256-gcm")
213 || meta.pointer("/kdf/name").and_then(Value::as_str) != Some("scrypt")
214 {
215 return Err(CoreError::plain("Unsupported vault format."));
216 }
217 let salt = B64.decode(meta.get("salt").and_then(Value::as_str).unwrap_or("")).map_err(|_| CoreError::plain("Vault salt is invalid."))?;
218 if salt.len() != SALT_LENGTH {
219 return Err(CoreError::plain("Vault salt is invalid."));
220 }
221 let num = |key: &str, fallback: u64| meta.pointer(&format!("/kdf/{key}")).and_then(Value::as_u64).filter(|v| *v > 0).unwrap_or(fallback);
222 Ok((salt, num("N", SCRYPT_N), num("r", SCRYPT_R as u64) as u32, num("p", SCRYPT_P as u64) as u32))
223 }
225 pub async fn unlock(&self, password: &str) -> CoreResult<VaultStatus> {
226 let generation = self.generation();
227 let instance = self.instance_id();
228 if !self.status().initialized {
229 return Err(CoreError::plain("Vault setup is required or its files are incomplete."));
230 }
231 let normalized = validate_password(password)?;
232 let failure = || CoreError::new("VAULT_UNLOCK_FAILED", "Vault password is incorrect or the vault is damaged.");
233 let meta = self.read_meta().map_err(|_| failure())?;
234 let (salt, n, r, p) = Self::kdf_from_meta(&meta).map_err(|_| failure())?;
235 let key = tokio::task::spawn_blocking(move || derive_key(&normalized, &salt, n, r, p))
236 .await
237 .map_err(|_| failure())?
238 .map_err(|_| failure())?;
239 self.unlock_key_at_generation(key, generation, instance).map_err(|_| failure())
240 }
242 /// Copy of the unlock key for a Core-internal operation (biometric enrollment). Never exposed over IPC.
243 pub fn unlock_key_copy(&self) -> CoreResult<VaultKey> {
244 let session = self.session.lock();
245 match (&session.key, &session.data) {
246 (Some(key), Some(_)) => Ok(Zeroizing::new(**key)),
247 _ => Err(locked_error()),
248 }
249 }
251 pub async fn verify_password(&self, password: &str) -> CoreResult<bool> {
252 self.assert_unlocked()?;
253 let generation = self.generation();
254 let required = || CoreError::new("BIOMETRIC_MASTER_PASSWORD_REQUIRED", "Master password required.");
255 let normalized = validate_password(password)?;
256 let meta = self.read_meta()?;
257 let (salt, n, r, p) = Self::kdf_from_meta(&meta)?;
258 let key = tokio::task::spawn_blocking(move || derive_key(&normalized, &salt, n, r, p))
259 .await
260 .map_err(|_| required())??;
261 let session = self.session.lock();
262 let current = session.key.as_ref().filter(|_| session.data.is_some()).ok_or_else(required)?;
263 if generation != session.generation || !bool::from(subtle::ConstantTimeEq::ct_eq(&key[..], &current[..])) {
264 return Err(required());
265 }
266 self.read_encrypted(&key)?;
267 Ok(true)
268 }
270 pub fn unlock_with_key(&self, key: &[u8]) -> CoreResult<VaultStatus> {
271 self.unlock_with_key_at_generation(key, self.generation())
272 }
274 pub fn unlock_with_key_at_generation(&self, key: &[u8], generation: u64) -> CoreResult<VaultStatus> {
275 if !self.status().initialized || key.len() != KEY_LENGTH {
276 return Err(CoreError::new("BIOMETRIC_INVALID_KEY", "Invalid protected unlocking key."));
277 }
278 let instance = self.instance_id();
279 let mut copy: VaultKey = Zeroizing::new([0u8; KEY_LENGTH]);
280 copy.copy_from_slice(key);
281 self.unlock_key_at_generation(copy, generation, instance)
282 }
284 fn unlock_key_at_generation(&self, key: VaultKey, generation: u64, instance: Option<String>) -> CoreResult<VaultStatus> {
285 // Check and decrypt/commit under the same mutex used by lock/reset. A completed
286 // lock cannot be undone by a KDF or biometric retrieval that started earlier.
287 let mut session = self.session.lock();
288 let current_instance = self.read_meta().ok().and_then(|meta| Self::meta_identity(&meta));
289 if generation != session.generation || instance.is_none() || instance != current_instance {
290 return Err(locked_error());
291 }
292 let data = self.read_encrypted(&key).map_err(|_| CoreError::new(
293 "BIOMETRIC_ENROLLMENT_INVALIDATED", "Biometric enrollment is no longer valid. Use the master password.",
294 ))?;
295 Self::set_session(&mut session, key, data);
296 drop(session);
297 Ok(self.status())
298 }
300 fn clear_session(session: &mut Session) {
301 session.generation += 1;
302 session.key = None;
303 session.data = None;
304 }
306 pub fn lock(&self) -> VaultStatus {
307 Self::clear_session(&mut self.session.lock());
308 self.status()
309 }
311 pub fn reset(&self, confirmation: &str) -> CoreResult<VaultStatus> {
312 if confirmation != "SMAZAT" {
313 return Err(CoreError::new("VAULT_RESET_CONFIRMATION_REQUIRED", "Vault reset confirmation is invalid."));
314 }
315 let mut session = self.session.lock();
316 Self::clear_session(&mut session);
317 session.host_keys.clear();
318 if self.root.exists() {
319 fs::remove_dir_all(&self.root)?;
320 }
321 drop(session);
322 let status = self.status();
323 if !status.needs_setup {
324 return Err(CoreError::new("VAULT_RESET_FAILED", "Vault data could not be removed completely."));
325 }
326 Ok(status)
327 }
329 pub fn assert_unlocked(&self) -> CoreResult<()> {
330 if self.is_unlocked() {
331 Ok(())
332 } else {
333 Err(locked_error())
334 }
335 }
337 pub fn get_data(&self) -> CoreResult<Value> {
338 let session = self.session.lock();
339 match (&session.key, &session.data) {
340 (Some(_), Some(data)) => Ok(data.clone()),
341 _ => Err(locked_error()),
342 }
343 }
345 pub fn replace_data(&self, next: Value) -> CoreResult<Value> {
346 self.replace_data_at_generation(next, self.generation())
347 }
349 fn replace_data_at_generation(&self, next: Value, generation: u64) -> CoreResult<Value> {
350 let normalized = validate_vault_data(next)?;
351 let mut session = self.session.lock();
352 if session.generation != generation { return Err(locked_error()); }
353 let key = match (&session.key, &session.data) {
354 (Some(key), Some(_)) => Zeroizing::new(**key),
355 _ => return Err(locked_error()),
356 };
357 self.write_encrypted(&normalized, &key)?;
358 session.host_keys = normalized["hostKeys"].as_object().cloned().unwrap_or_default();
359 session.data = Some(normalized.clone());
360 Ok(normalized)
361 }
363 /// Mutator → encrypt → commit bound to a single unlocking epoch.
364 pub fn update<R>(&self, mutator: impl FnOnce(&mut Value) -> CoreResult<R>) -> CoreResult<R> {
365 self.update_at_generation(self.unlocked_generation()?, mutator)
366 }
368 pub fn unlocked_generation(&self) -> CoreResult<u64> {
369 let session = self.session.lock();
370 if session.key.is_none() || session.data.is_none() { return Err(locked_error()); }
371 Ok(session.generation)
372 }
374 pub fn assert_generation(&self, generation: u64) -> CoreResult<()> {
375 self.with_generation(generation, || Ok(()))
376 }
378 /// Runs a small synchronous final effect atomically with respect to lock/reset.
379 /// The callback must not reenter VaultStore.
380 pub fn with_generation<R>(&self, generation: u64, operation: impl FnOnce() -> CoreResult<R>) -> CoreResult<R> {
381 let session = self.session.lock();
382 if session.generation != generation || session.key.is_none() || session.data.is_none() { return Err(locked_error()); }
383 operation()
384 }
386 /// A final local effect bound to an immutable snapshot. Must not reenter VaultStore.
387 pub fn with_data_at_generation<R>(&self,generation:u64,operation:impl FnOnce(&Value)->CoreResult<R>)->CoreResult<R>{
388 let session=self.session.lock();
389 if session.generation!=generation || session.key.is_none(){return Err(locked_error())}
390 operation(session.data.as_ref().ok_or_else(locked_error)?)
391 }
393 pub fn get_data_at_generation(&self, generation: u64) -> CoreResult<Value> {
394 let session = self.session.lock();
395 if session.generation != generation || session.key.is_none() { return Err(locked_error()); }
396 session.data.clone().ok_or_else(locked_error)
397 }
399 pub fn update_at_generation<R>(&self, generation: u64, mutator: impl FnOnce(&mut Value) -> CoreResult<R>) -> CoreResult<R> {
400 let _update = self.update_lock.lock();
401 let mut draft = self.get_data_at_generation(generation)?;
402 let result = mutator(&mut draft)?;
403 self.replace_data_at_generation(draft, generation)?;
404 Ok(result)
405 }
407 fn write_encrypted(&self, data: &Value, key: &[u8; KEY_LENGTH]) -> CoreResult<()> {
408 let data = validate_vault_data(data.clone())?;
409 let mut plaintext = Zeroizing::new(serde_json::to_vec(&data)?);
410 let mut iv = [0u8; IV_LENGTH];
411 rand::rngs::OsRng.fill_bytes(&mut iv);
412 let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| CoreError::plain("Invalid vault key."))?;
413 let mut sealed = cipher
414 .encrypt(Nonce::from_slice(&iv), Payload { msg: &plaintext, aad: AAD })
415 .map_err(|_| CoreError::plain("Vault encryption failed."))?;
416 plaintext.zeroize();
417 let tag = sealed.split_off(sealed.len() - TAG_LENGTH);
418 let result = atomic_write_json(
419 &self.data_path,
420 &json!({
421 "formatVersion": 1,
422 "updatedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
423 "iv": B64.encode(iv),
424 "authTag": B64.encode(&tag),
425 "ciphertext": B64.encode(&sealed)
426 }),
427 );
428 sealed.zeroize();
429 result
430 }
432 fn read_encrypted(&self, key: &[u8; KEY_LENGTH]) -> CoreResult<Value> {
433 let blob: Value = serde_json::from_str(&fs::read_to_string(&self.data_path)?)?;
434 if blob.get("formatVersion").and_then(Value::as_f64) != Some(1.0) {
435 return Err(CoreError::plain("Unsupported encrypted vault data."));
436 }
437 let field = |name: &str| B64.decode(blob.get(name).and_then(Value::as_str).unwrap_or("")).unwrap_or_default();
438 let iv = field("iv");
439 let tag = field("authTag");
440 let mut ciphertext = field("ciphertext");
441 if iv.len() != IV_LENGTH || tag.len() != TAG_LENGTH || ciphertext.is_empty() {
442 return Err(CoreError::plain("Encrypted vault data is invalid."));
443 }
444 ciphertext.extend_from_slice(&tag);
445 let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| CoreError::plain("Invalid vault key."))?;
446 let plaintext = Zeroizing::new(
447 cipher
448 .decrypt(Nonce::from_slice(&iv), Payload { msg: &ciphertext, aad: AAD })
449 .map_err(|_| CoreError::plain("Vault authentication failed."))?,
450 );
451 ciphertext.zeroize();
452 validate_vault_data(serde_json::from_slice(&plaintext)?)
453 }
455 fn set_session(session: &mut Session, key: VaultKey, data: Value) {
456 session.generation += 1;
457 session.host_keys = data["hostKeys"].as_object().cloned().unwrap_or_default();
458 session.key = Some(key);
459 session.data = Some(data);
460 }
462 pub fn trusted_host_key(&self, id: &str) -> Option<String> {
463 self.session.lock().host_keys.get(id).and_then(Value::as_str).map(str::to_string)
464 }
466 /// Outer None is for a deliberate Core-internal write; Some binds a user's
467 /// confirmation to exactly the previously observed key, including unknown.
468 pub fn trust_host_key(&self, id: &str, fingerprint: &str, expected: Option<Option<&str>>) -> CoreResult<bool> {
469 let _update = self.update_lock.lock();
470 let mut session = self.session.lock();
471 if let Some(expected) = expected {
472 if session.host_keys.get(id).and_then(Value::as_str) != expected {
473 return Err(CoreError::new("HOST_KEY_REPLACEMENT_STALE", "The trusted host key changed while confirmation was pending."));
474 }
475 }
476 let persistent = session.key.is_some() && session.data.is_some();
477 if persistent {
478 let mut data = session.data.as_ref().unwrap().clone();
479 data["hostKeys"][id] = json!(fingerprint);
480 self.write_encrypted(&data, session.key.as_ref().unwrap())?;
481 session.data = Some(data);
482 }
483 session.host_keys.insert(id.to_string(), json!(fingerprint));
484 Ok(persistent)
485 }
488#[cfg(test)]
489mod tests {
490 use super::*;
492 #[tokio::test]
493 async fn create_lock_unlock_roundtrip() {
494 let dir = tempfile::tempdir().unwrap();
495 let vault = VaultStore::new(dir.path());
496 assert!(vault.status().needs_setup);
497 vault.create("heslo123").await.unwrap();
498 vault.update(|data| {
499 data["profiles"].as_array_mut().unwrap().push(json!({"id": "p1"}));
500 Ok(())
501 })
502 .unwrap();
503 vault.lock();
504 assert!(vault.get_data().is_err());
505 assert_eq!(vault.unlock("wrong-pass").await.unwrap_err().code, "VAULT_UNLOCK_FAILED");
506 vault.unlock("heslo123").await.unwrap();
507 assert_eq!(vault.get_data().unwrap()["profiles"][0]["id"], "p1");
508 assert!(vault.verify_password("heslo123").await.unwrap());
509 assert!(vault.verify_password("jine-heslo").await.is_err());
510 }
512 #[tokio::test]
513 async fn password_rules_count_code_points() {
514 assert!(validate_password("ččč").is_err());
515 assert!(validate_password("čččč").is_ok());
516 assert!(validate_password(&"a".repeat(65)).is_err());
517 assert!(validate_password("ab\0c").is_err());
518 }
520 #[tokio::test]
521 async fn late_unlock_and_biometric_commit_cannot_undo_lock() {
522 let root = tempfile::tempdir().unwrap(); let vault = VaultStore::new(root.path());
523 vault.create("fixture-password").await.unwrap();
524 let generation = vault.generation(); let key = vault.unlock_key_copy().unwrap(); let instance = vault.instance_id();
525 vault.lock();
526 assert!(vault.unlock_key_at_generation(key.clone(), generation, instance).is_err());
527 assert!(vault.unlock_with_key_at_generation(key.as_ref(), generation).is_err());
528 assert!(!vault.is_unlocked());
529 }
531 #[tokio::test]
532 async fn stale_mutator_cannot_overwrite_a_new_unlock_or_recreated_vault() {
533 let root = tempfile::tempdir().unwrap(); let vault = VaultStore::new(root.path());
534 vault.create("fixture-password").await.unwrap();
535 let key = vault.unlock_key_copy().unwrap(); let stale_generation = vault.generation();
536 let stale_data = vault.get_data().unwrap();
537 let result = vault.update(|data| {
538 data["secrets"]["stale"] = json!(true);
539 vault.lock(); vault.unlock_with_key(key.as_ref())?;
540 Ok(())
541 });
542 assert_eq!(result.unwrap_err().code, "VAULT_LOCKED");
543 assert!(vault.get_data().unwrap()["secrets"]["stale"].is_null());
544 vault.reset("SMAZAT").unwrap(); vault.create("new-fixture-password").await.unwrap();
545 assert!(vault.replace_data_at_generation(stale_data, stale_generation).is_err());
546 assert!(vault.update_at_generation(stale_generation, |_| Ok(())).is_err());
547 assert!(vault.with_generation::<()>(stale_generation, || panic!("stale final effect ran")).is_err());
548 }
550 #[tokio::test]
551 async fn reset_requires_confirmation() {
552 let dir = tempfile::tempdir().unwrap();
553 let vault = VaultStore::new(dir.path());
554 vault.create("heslo123").await.unwrap();
555 assert!(vault.reset("DELETE").is_err());
556 assert!(vault.reset("SMAZAT").unwrap().needs_setup);
557 }
SHA-25609bb7e4e67c97c6404873fdb23e54e7fb4eb87507dc89abe3758ce26f26f9abb

CH-J Proprietary Software License 1.14 · A forrás a CH-J Proprietary Software License 1.14 alatt található. Rendelkezése nem változtatja meg a licenc feltételeit, és nem ad további jogokat.

Archive SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Fájlok