CH-J Toolspengurusan pelayan melalui SSH
Menu
Sumber yang diterbitkan

CH-J Tools

Layari direktori dan fail untuk keluaran aplikasi tertentu.

Muat turun ZIP sumber
CH-J / Server Manager 1704 fail
0.1.2-beta Ditambah: · Dibina:
passwords.rs Ditambah: Tarikh tidak direkodkan 6.6 KB · 136 baris
Kod Muat turun fail
1//! General passwords stored in the encrypted vault (`secrets/passwords`), shown in the Keychain view.
2//! Listing never returns the password; only `reveal` does, and the command guards it with
3//! `require_sensitive_action`.
5use crate::core::error::{CoreError, CoreResult};
6use crate::core::vault::VaultStore;
7use serde_json::{json, Value};
9const MAX_ITEMS: usize = 2000;
11fn invalid(message: &str) -> CoreError {
12 CoreError::new("PASSWORD_ENTRY_INVALID", message)
15fn field(input: &Value, key: &str, limit: usize, multiline: bool) -> CoreResult<String> {
16 let value = input.get(key).and_then(Value::as_str).unwrap_or("");
17 let value = if multiline { value.to_string() } else { value.trim().to_string() };
18 if value.chars().count() > limit || value.contains('\0') || (!multiline && value.chars().any(char::is_control)) {
19 return Err(invalid("A password entry field is invalid or too long."));
20 }
21 Ok(value)
24fn describe(entry: &Value) -> Value {
25 json!({"id":entry["id"],"label":entry["label"],"username":entry["username"],"url":entry["url"],"note":entry["note"],
26 "createdAt":entry["createdAt"],"updatedAt":entry["updatedAt"]})
29pub fn list_at_generation(vault: &VaultStore, generation: u64) -> CoreResult<Value> {
30 vault.with_data_at_generation(generation, |data| {
31 Ok(Value::Array(data.pointer("/secrets/passwords").and_then(Value::as_array).map(|items| items.iter().map(describe).collect()).unwrap_or_default()))
32 })
35/// Create (no `id`) or update an entry. An empty password on update keeps the stored one.
36pub fn save_at_generation(vault: &VaultStore, input: &Value, generation: u64) -> CoreResult<Value> {
37 let label = field(input, "label", 200, false)?;
38 if label.is_empty() {
39 return Err(invalid("Enter a name for the password."));
40 }
41 let username = field(input, "username", 256, false)?;
42 let url = field(input, "url", 2048, false)?;
43 let note = field(input, "note", 4000, true)?;
44 let password = field(input, "password", 4096, true)?;
45 let id = input.get("id").and_then(Value::as_str).unwrap_or("").to_string();
46 vault.update_at_generation(generation, |data| {
47 if !data.get("secrets").is_some_and(Value::is_object) {
48 data["secrets"] = json!({});
49 }
50 let items = data["secrets"].as_object_mut().ok_or_else(|| invalid("Vault secrets are invalid."))?
51 .entry("passwords").or_insert_with(|| json!([])).as_array_mut().ok_or_else(|| invalid("Vault passwords are invalid."))?;
52 let now = chrono::Utc::now().to_rfc3339();
53 if id.is_empty() {
54 if password.is_empty() {
55 return Err(invalid("Enter the password to store."));
56 }
57 if items.len() >= MAX_ITEMS {
58 return Err(invalid("Too many stored passwords."));
59 }
60 let entry = json!({"id":uuid::Uuid::new_v4().to_string(),"label":label,"username":username,"url":url,"note":note,"password":password,"createdAt":now,"updatedAt":now});
61 items.push(entry.clone());
62 return Ok(describe(&entry));
63 }
64 let entry = items.iter_mut().find(|item| item["id"].as_str() == Some(id.as_str()))
65 .ok_or_else(|| CoreError::new("PASSWORD_ENTRY_NOT_FOUND", "The stored password was not found."))?;
66 entry["label"] = json!(label);
67 entry["username"] = json!(username);
68 entry["url"] = json!(url);
69 entry["note"] = json!(note);
70 if !password.is_empty() {
71 entry["password"] = json!(password);
72 }
73 entry["updatedAt"] = json!(now);
74 Ok(describe(entry))
75 })
78pub fn reveal_at_generation(vault: &VaultStore, id: &str, generation: u64) -> CoreResult<String> {
79 vault.with_data_at_generation(generation, |data| {
80 data.pointer("/secrets/passwords").and_then(Value::as_array)
81 .and_then(|items| items.iter().find(|item| item["id"].as_str() == Some(id)))
82 .and_then(|item| item["password"].as_str().map(str::to_string))
83 .ok_or_else(|| CoreError::new("PASSWORD_ENTRY_NOT_FOUND", "The stored password was not found."))
84 })
87pub fn remove_at_generation(vault: &VaultStore, id: &str, generation: u64) -> CoreResult<Value> {
88 vault.update_at_generation(generation, |data| {
89 let mut removed = false;
90 if let Some(items) = data.pointer_mut("/secrets/passwords").and_then(Value::as_array_mut) {
91 items.retain(|item| {
92 let keep = item["id"].as_str() != Some(id);
93 removed |= !keep;
94 keep
95 });
96 }
97 Ok(json!({"removed":removed}))
98 })
101#[cfg(test)]
102mod tests {
103 use super::*;
105 #[tokio::test]
106 async fn listing_hides_passwords_and_update_keeps_the_old_one() {
107 let root = tempfile::tempdir().unwrap();
108 let vault = VaultStore::new(root.path());
109 vault.create("synthetic-vault-password").await.unwrap();
110 let generation = vault.unlocked_generation().unwrap();
111 let saved = save_at_generation(&vault, &json!({"label":"Router","username":"admin","password":"synthetic-secret"}), generation).unwrap();
112 let id = saved["id"].as_str().unwrap().to_string();
113 let listed = list_at_generation(&vault, generation).unwrap();
114 assert_eq!(listed[0]["label"], "Router");
115 assert!(!listed.to_string().contains("synthetic-secret"));
116 save_at_generation(&vault, &json!({"id":id,"label":"Router 2","username":"admin","password":""}), generation).unwrap();
117 assert_eq!(reveal_at_generation(&vault, &id, generation).unwrap(), "synthetic-secret");
118 save_at_generation(&vault, &json!({"id":id,"label":"Router 2","password":"synthetic-new"}), generation).unwrap();
119 assert_eq!(reveal_at_generation(&vault, &id, generation).unwrap(), "synthetic-new");
120 assert!(save_at_generation(&vault, &json!({"label":"","password":"x"}), generation).is_err());
121 assert!(save_at_generation(&vault, &json!({"label":"No password"}), generation).is_err());
122 assert_eq!(remove_at_generation(&vault, &id, generation).unwrap()["removed"], true);
123 assert!(reveal_at_generation(&vault, &id, generation).is_err());
124 }
126 #[tokio::test]
127 async fn locked_vault_refuses_every_operation() {
128 let root = tempfile::tempdir().unwrap();
129 let vault = VaultStore::new(root.path());
130 vault.create("synthetic-vault-password").await.unwrap();
131 let generation = vault.unlocked_generation().unwrap();
132 vault.lock();
133 assert!(list_at_generation(&vault, generation).is_err());
134 assert!(save_at_generation(&vault, &json!({"label":"x","password":"y"}), generation).is_err());
135 }
SHA-256facc8aa9e9538b9b90448d444b6972f3a92955f0f97b8dee67ff9ed529f7c996

CH-J Proprietary Software License 1.14 · Kod sumber disediakan di bawah CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah terma lesen atau memberikan hak tambahan.

SHA-256 arkibac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Fail