docs: add comprehensive doc comments to all Rust source files
Document every public function, struct, field, constant, and non-trivial private function across idfoto-core and idfoto-cli. Module-level docs explain each module's role in the architecture. Comments explain the "why" (crypto choices, algorithm design, data model rationale) not just the "what". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,48 @@
|
||||
//! Vault data model: entries, manifest entries, and the manifest index.
|
||||
//!
|
||||
//! The vault stores credentials in two tiers:
|
||||
//!
|
||||
//! 1. **Individual entries** (`entries/<id>.enc`): each file contains a single
|
||||
//! [`Entry`] encrypted with the master key. Only decrypted when the user
|
||||
//! needs to read or edit a specific credential.
|
||||
//!
|
||||
//! 2. **Manifest** (`manifest.enc`): a single encrypted file containing a
|
||||
//! [`Manifest`] -- a map from entry IDs to [`ManifestEntry`] summaries.
|
||||
//! This lets the CLI list and search entries by decrypting only one file,
|
||||
//! rather than decrypting every entry in the vault.
|
||||
//!
|
||||
//! ## Entry IDs
|
||||
//!
|
||||
//! Entry IDs are random 8-character lowercase hex strings (4 bytes of entropy,
|
||||
//! ~4 billion possible values). This is sufficient for family-scale vaults while
|
||||
//! keeping filenames short and filesystem-friendly.
|
||||
//!
|
||||
//! ## Serialization strategy
|
||||
//!
|
||||
//! All structs derive `Serialize`/`Deserialize` for JSON encoding. Optional fields
|
||||
//! use `#[serde(skip_serializing_if = "Option::is_none")]` to keep the JSON compact
|
||||
//! -- omitting null fields reduces ciphertext size and avoids leaking structural
|
||||
//! information about which optional fields a credential uses.
|
||||
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A single password entry (stored encrypted in entries/<id>.enc).
|
||||
/// A full credential entry stored encrypted in `entries/<id>.enc`.
|
||||
///
|
||||
/// Contains all sensitive data for a single credential. Each entry is encrypted
|
||||
/// independently, so accessing one entry does not require decrypting others.
|
||||
///
|
||||
/// ## Fields
|
||||
///
|
||||
/// - `name`: human-readable label (e.g., "GitHub", "Work Email"). Required.
|
||||
/// - `url`: the login URL. Optional; used for autofill matching in the browser extension.
|
||||
/// - `username`: the account username or email. Optional.
|
||||
/// - `password`: the credential password. Required (this is the core secret).
|
||||
/// - `notes`: free-form text (e.g., security questions, recovery codes). Optional.
|
||||
/// - `totp_secret`: base32-encoded TOTP secret for 2FA. Optional.
|
||||
/// - `created_at`: ISO 8601 timestamp (or Unix seconds) when the entry was created.
|
||||
/// - `updated_at`: ISO 8601 timestamp (or Unix seconds) of the last modification.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Entry {
|
||||
pub name: String,
|
||||
@@ -19,25 +59,46 @@ pub struct Entry {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// Summary info about an entry (stored in the manifest).
|
||||
/// Summary metadata for a single entry, stored in the manifest.
|
||||
///
|
||||
/// This is a lightweight projection of [`Entry`] that contains only the
|
||||
/// non-sensitive fields needed for listing and searching. The password,
|
||||
/// notes, and TOTP secret are intentionally excluded so that listing
|
||||
/// entries requires decrypting only the manifest, not every individual entry.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ManifestEntry {
|
||||
/// Human-readable label for display and search matching.
|
||||
pub name: String,
|
||||
/// Login URL for search matching and browser extension autofill.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
/// Account username for display in entry listings.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub username: Option<String>,
|
||||
/// Timestamp of last modification, used for sorting and display.
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// The vault manifest — maps entry IDs to their metadata.
|
||||
/// The vault manifest -- an encrypted index mapping entry IDs to their metadata.
|
||||
///
|
||||
/// The manifest serves two purposes:
|
||||
///
|
||||
/// 1. **Efficient listing**: decrypting the single manifest file is enough to show
|
||||
/// all entry names, URLs, and usernames without touching individual entry files.
|
||||
/// 2. **Search**: the [`search`](Manifest::search) method performs case-insensitive
|
||||
/// substring matching against entry names and URLs.
|
||||
///
|
||||
/// The `version` field allows future schema migrations if the manifest format evolves.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Manifest {
|
||||
/// Map from entry ID (8-char hex string) to entry metadata.
|
||||
pub entries: HashMap<String, ManifestEntry>,
|
||||
/// Schema version. Currently always `1`.
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
impl Manifest {
|
||||
/// Create a new empty manifest with version 1.
|
||||
pub fn new() -> Self {
|
||||
Manifest {
|
||||
entries: HashMap::new(),
|
||||
@@ -45,14 +106,23 @@ impl Manifest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert or update an entry in the manifest.
|
||||
///
|
||||
/// If an entry with the same ID already exists, it is overwritten.
|
||||
/// This is used both for `add` (new entry) and `edit` (update existing).
|
||||
pub fn add_entry(&mut self, id: String, entry: ManifestEntry) {
|
||||
self.entries.insert(id, entry);
|
||||
}
|
||||
|
||||
/// Remove an entry from the manifest by ID, returning its metadata if it existed.
|
||||
pub fn remove_entry(&mut self, id: &str) -> Option<ManifestEntry> {
|
||||
self.entries.remove(id)
|
||||
}
|
||||
|
||||
/// Search entries by case-insensitive substring match against name and URL.
|
||||
///
|
||||
/// Returns a vector of `(id, entry)` pairs for all matching entries. An entry
|
||||
/// matches if the query appears in its name or URL (case-insensitive).
|
||||
pub fn search(&self, query: &str) -> Vec<(&String, &ManifestEntry)> {
|
||||
let q = query.to_lowercase();
|
||||
self.entries
|
||||
@@ -75,6 +145,10 @@ impl Default for Manifest {
|
||||
}
|
||||
|
||||
/// Generate a random 8-character hex string to use as an entry ID.
|
||||
///
|
||||
/// Uses 4 random bytes (32 bits of entropy), producing IDs like `"a1b2c3d4"`.
|
||||
/// This gives ~4 billion possible values, which is more than sufficient for
|
||||
/// a family-scale vault (typically < 1000 entries).
|
||||
pub fn generate_entry_id() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
let bytes: [u8; 4] = rng.gen();
|
||||
|
||||
Reference in New Issue
Block a user