Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "vaultify"
version = "0.2.7"
version = "0.3.0"
edition = "2021"
authors = ["vaultify contributors"]
description = "A secure, file-based secrets vault with interactive CLI"
Expand Down Expand Up @@ -42,6 +42,7 @@ atty = "0.2"
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = { version = "0.8", features = ["preserve_order"] }

# Error handling
thiserror = "1.0"
Expand Down
37 changes: 20 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A secure, file-based password manager with hierarchical organization. Written in

- 🔐 **Per-item encryption** with Argon2id + AES-256-GCM
- 📁 **Hierarchical organization** of secrets
- 📝 **Markdown-based** vault format for easy versioning
- 📝 **TOML-based** vault format for easy editing and versioning
- 🔍 **Fast filtering** of entries
- 📋 **Clipboard integration** with automatic clearing
- 🚀 **Interactive and CLI modes**
Expand Down Expand Up @@ -58,7 +58,7 @@ cargo build --release
vaultify init
```

This creates a `vault.md` file in the current directory.
This creates a `vault.toml` file in the current directory.

### Add a secret

Expand Down Expand Up @@ -134,26 +134,29 @@ vaultify> exit

## Vault Format

Vaults are stored as markdown files with encrypted content:
Vaults are stored as TOML files with encrypted content:

```markdown
# root <!-- vaultify v1 -->
```toml
version = "v0.3"
modified = "2025-01-17T10:00:00Z"

## personal
<description/>
Personal accounts
</description>
<encrypted></encrypted>
[personal]
description = "Personal accounts"
encrypted = ""
salt = ""

### personal/email
<description/>
Email accounts
</description>
<encrypted salt="base64-encoded-salt">
base64-encoded-encrypted-content
</encrypted>
[personal.email]
description = "Email accounts"
encrypted = "base64-encoded-encrypted-content"
salt = "base64-encoded-salt"
```

### Key Features:
- **Insertion order preserved**: Entries maintain their original order
- **Smart group insertion**: New entries are added at the end of their group
- **Native TOML format**: Clean, readable structure with dotted key notation
- **Flexible parsing**: Parent entries are created automatically

## Security

- **Encryption**: Each entry is encrypted with Argon2id + AES-256-GCM
Expand Down
84 changes: 41 additions & 43 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub struct Cli {
long,
global = true,
env = "VAULT_FILE",
help = "Path to vault file (default: searches for vault.md)"
help = "Path to vault file (default: searches for vault.toml)"
)]
pub file: Option<PathBuf>,

Expand Down Expand Up @@ -56,7 +56,7 @@ pub enum Commands {

/// Add a new secret to the vault
Add {
/// Secret scope (e.g., personal/email/gmail)
/// Secret scope (e.g., personal.email.gmail)
scope: String,

/// Description of the secret
Expand Down Expand Up @@ -158,11 +158,11 @@ pub enum Commands {

/// Decrypt GPG-encrypted vault file
GpgDecrypt {
/// Input file (default: vault.md.gpg or vault.md.asc)
/// Input file (default: vault.toml.gpg or vault.toml.asc)
#[arg(short, long)]
input: Option<PathBuf>,

/// Output file (default: vault.md)
/// Output file (default: vault.toml)
#[arg(short = 'O', long = "output-file")]
output_file: Option<PathBuf>,
},
Expand Down Expand Up @@ -241,7 +241,7 @@ impl Cli {
let vault_path = if let Some(path) = &self.file {
path.clone()
} else {
PathBuf::from("vault.md")
PathBuf::from("vault.toml")
};

// Check if vault already exists
Expand All @@ -259,9 +259,10 @@ impl Cli {
}
}

// Create vault file
let content = "# root <!-- vaultify v1 -->\n";
fs::write(&vault_path, content)?;
// Create vault file with TOML format
let content = "version = \"v0.3\"\ncreated = \"{}\"\n";
let now = chrono::Utc::now().to_rfc3339();
fs::write(&vault_path, content.replace("{}", &now))?;

// Set proper permissions on Unix
#[cfg(unix)]
Expand Down Expand Up @@ -334,36 +335,31 @@ impl Cli {
let scopes: Vec<String> = result.entries.iter().map(|e| e.scope.clone()).collect();
let tree_lines = utils::format_tree(&scopes);

for line in tree_lines.iter() {
// Find the corresponding entry by checking if the line ends with the last part of the scope
if let Some(entry) = result.entries.iter().find(|e| {
let scope_parts: Vec<&str> = e.scope.split('/').collect();
if let Some(last_part) = scope_parts.last() {
line.ends_with(last_part)
} else {
false
}
}) {
let desc_lines: Vec<&str> = entry.description.lines().collect();
let first_line = desc_lines.first().copied().unwrap_or("");
for (i, line) in tree_lines.iter().enumerate() {
// Find the corresponding entry by matching the scope from the original scopes list
if i < scopes.len() {
if let Some(entry) = result.entries.iter().find(|e| e.scope == scopes[i]) {
let desc_lines: Vec<&str> = entry.description.lines().collect();
let first_line = desc_lines.first().copied().unwrap_or("");

if !entry.has_content {
println!("{} {} - {}", line, "[empty]".yellow(), first_line);
} else {
println!("{} - {}", line, first_line);
}
if !entry.has_content {
println!("{} {} - {}", line, "[empty]".yellow(), first_line);
} else {
println!("{line} - {first_line}");
}

// Print additional description lines with appropriate indentation
if desc_lines.len() > 1 {
// Calculate indentation based on tree line
let indent_len = line.len() + 3; // +3 for " - "
let indent = " ".repeat(indent_len);
for desc_line in desc_lines.iter().skip(1) {
println!("{}{}", indent, desc_line);
// Print additional description lines with appropriate indentation
if desc_lines.len() > 1 {
// Calculate indentation based on tree line
let indent_len = line.len() + 3; // +3 for " - "
let indent = " ".repeat(indent_len);
for desc_line in desc_lines.iter().skip(1) {
println!("{indent}{desc_line}");
}
}
} else {
println!("{line}");
}
} else {
println!("{line}");
}
}
} else {
Expand Down Expand Up @@ -394,7 +390,7 @@ impl Cli {

// Print additional description lines indented
for line in desc_lines.iter().skip(1) {
println!(" {}", line);
println!(" {line}");
}
}
}
Expand Down Expand Up @@ -579,16 +575,16 @@ impl Cli {
} else {
// Try to find encrypted vault
let vault_path = self.get_vault_file()?;
let gpg_path = vault_path.with_extension("md.gpg");
let asc_path = vault_path.with_extension("md.asc");
let gpg_path = vault_path.with_extension("toml.gpg");
let asc_path = vault_path.with_extension("toml.asc");

if gpg_path.exists() {
gpg_path
} else if asc_path.exists() {
asc_path
} else {
return Err(VaultError::Other(
"No encrypted vault file found (vault.md.gpg or vault.md.asc)".to_string(),
"No encrypted vault file found (vault.toml.gpg or vault.toml.asc)".to_string(),
));
}
};
Expand Down Expand Up @@ -680,16 +676,18 @@ mod tests {

#[test]
fn test_validate_scope_name() {
assert!(utils::validate_scope_name("personal/email"));
assert!(utils::validate_scope_name("work/vpn"));
assert!(utils::validate_scope_name("personal.email"));
assert!(utils::validate_scope_name("work.vpn"));
assert!(!utils::validate_scope_name(""));
assert!(!utils::validate_scope_name("personal/."));
assert!(!utils::validate_scope_name("personal/.."));
assert!(!utils::validate_scope_name("personal.."));
assert!(!utils::validate_scope_name("personal..."));
assert!(!utils::validate_scope_name(".personal"));
assert!(!utils::validate_scope_name("personal."));
}

#[test]
fn test_parse_scope_path() {
let parts = utils::parse_scope_path("personal/email/gmail");
let parts = utils::parse_scope_path("personal.email.gmail");
assert_eq!(parts, vec!["personal", "email", "gmail"]);
}
}
3 changes: 0 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ pub enum VaultError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),

#[error("Parse error: {0}")]
Parse(#[from] crate::parser::ParseError),

#[error("Crypto error: {0}")]
Crypto(#[from] crate::crypto::CryptoError),

Expand Down
Loading