Sumber yang diterbitkan
Unduh ZIP sumber
CH-J Tools
Jelajahi direktori dan file untuk rilis aplikasi tertentu.
CH-J / Server Manager
1704 file
0.1.2-beta
Ditambahkan: · Dibuat:
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 with3
//! `require_sensitive_action`.5
use crate::core::error::{CoreError, CoreResult};6
use crate::core::vault::VaultStore;7
use serde_json::{json, Value};9
const MAX_ITEMS: usize = 2000;11
fn invalid(message: &str) -> CoreError {12
CoreError::new("PASSWORD_ENTRY_INVALID", message)13
}15
fn 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)22
}24
fn 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"]})27
}29
pub 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
})33
}35
/// Create (no `id`) or update an entry. An empty password on update keeps the stored one.36
pub 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
})76
}78
pub 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
})85
}87
pub 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
keep95
});96
}97
Ok(json!({"removed":removed}))98
})99
}101
#[cfg(test)]102
mod 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
}136
}SHA-256
facc8aa9e9538b9b90448d444b6972f3a92955f0f97b8dee67ff9ed529f7c996CH-J Proprietary Software License 1.14 · Kode sumber disediakan berdasarkan CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah ketentuan lisensi atau memberikan hak tambahan.
SHA-256 arsip
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f