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
9 changes: 7 additions & 2 deletions components/CustomTitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
DropdownMenuSubTrigger,
} from "@/components/ui/dropdown-menu";
import { openSettingsWindow } from "@/lib/window";
import { getRecentDatabases } from "@/lib/storage";
import { getValidatedRecentDatabases } from "@/lib/storage";
import { openDatabaseInNewInstance } from "@/lib/tauri";

interface CustomTitleBarProps {
Expand Down Expand Up @@ -63,7 +63,12 @@ export function CustomTitleBar({
const [recentDatabases, setRecentDatabases] = useState<string[]>([]);

useEffect(() => {
setRecentDatabases(getRecentDatabases());
// Load and validate recent databases on mount
const loadRecentDatabases = async () => {
const validated = await getValidatedRecentDatabases();
setRecentDatabases(validated);
};
loadRecentDatabases();
}, []);

const handleOpenDatabase = async (dbPath: string) => {
Expand Down
46 changes: 46 additions & 0 deletions lib/storage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { invoke } from "@tauri-apps/api/core";
import { validateDatabaseFile } from "./tauri";

const LAST_DATABASE_KEY = "lastDatabasePath";
const RECENT_DATABASES_KEY = "recentDatabases";
Expand Down Expand Up @@ -50,6 +51,51 @@ export function getRecentDatabases(): string[] {
return [];
}

/**
* Validates all recent database paths and returns only valid ones.
* This function checks if each path exists and is a valid KDBX file.
* Invalid paths are automatically removed from localStorage.
*
* Note: Validates paths concurrently for better performance. Since the recent
* databases list is limited to 10 items (see addRecentDatabase), this should
* not cause file system issues.
*/
export async function getValidatedRecentDatabases(): Promise<string[]> {
const recent = getRecentDatabases();
if (recent.length === 0) {
return [];
}

// Validate each path concurrently (limited to 10 max by addRecentDatabase)
// Note: File system operations are fast (checking existence + reading 8 bytes),
// and Tauri invoke calls have built-in timeouts, so explicit timeout handling
// is not necessary here.
const validationResults = await Promise.all(
recent.map(async (path) => {
try {
const isValid = await validateDatabaseFile(path);
return { path, isValid };
} catch (error) {
// If validation fails (e.g., file system error), consider it invalid
console.warn(`Failed to validate database path: ${path}`, error);
return { path, isValid: false };
}
})
);

// Filter to only valid paths
const validPaths = validationResults
.filter(result => result.isValid)
.map(result => result.path);

// If some paths were invalid, update localStorage to remove them
if (validPaths.length !== recent.length && typeof window !== "undefined") {
localStorage.setItem(RECENT_DATABASES_KEY, JSON.stringify(validPaths));
}

return validPaths;
}

export function clearRecentDatabase(path: string): void {
if (typeof window !== "undefined") {
const recent = getRecentDatabases();
Expand Down
4 changes: 4 additions & 0 deletions lib/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,7 @@ export async function checkBreachedPasswords(): Promise<BreachedEntry[]> {
export async function openDatabaseInNewInstance(dbPath: string): Promise<void> {
return await invoke<void>("open_database_in_new_instance", { dbPath });
}

export async function validateDatabaseFile(path: string): Promise<boolean> {
return await invoke<boolean>("validate_database_file", { path });
}
17 changes: 0 additions & 17 deletions package-lock.json

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

46 changes: 46 additions & 0 deletions src-tauri/src/commands/database.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::kdbx::{Database, GroupData, KdfInfo};
use crate::state::AppState;
use std::io::Read;
use std::path::PathBuf;
use tauri::State;
use std::process::Command;
use std::fs::File;

#[tauri::command]
pub fn get_initial_file_path(state: State<AppState>) -> Option<String> {
Expand Down Expand Up @@ -136,6 +138,50 @@ pub fn open_database_in_new_instance(db_path: String) -> Result<(), String> {
Ok(())
}

#[tauri::command]
pub fn validate_database_file(path: String) -> Result<bool, String> {
let path_buf = PathBuf::from(&path);

// Check if file exists
if !path_buf.exists() {
return Ok(false);
}

// Check if it's a file (not a directory)
if !path_buf.is_file() {
return Ok(false);
}

// Check .kdbx extension
if let Some(ext) = path_buf.extension() {
if ext.to_string_lossy().to_lowercase() != "kdbx" {
return Ok(false);
}
} else {
return Ok(false);
}

// Validate KDBX magic bytes (0x03D9A29A)
// KDBX format starts with these 4 bytes after the base signature
let mut file = File::open(&path_buf)
.map_err(|_| "Failed to open file for validation".to_string())?;

let mut magic_bytes = [0u8; 8];
if file.read_exact(&mut magic_bytes).is_err() {
// File is too small to be a valid KDBX file
return Ok(false);
}

// Check for KDBX signature: first 4 bytes should be 0x03, 0xD9, 0xA2, 0x9A
// followed by version bytes
let valid = magic_bytes[0] == 0x03
&& magic_bytes[1] == 0xD9
&& magic_bytes[2] == 0xA2
&& magic_bytes[3] == 0x9A;

Ok(valid)
}

#[tauri::command]
pub fn merge_database(state: State<AppState>) -> Result<(), String> {
let mut database_lock = state.database.lock()
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fn main() {
commands::database::check_database_changes,
commands::database::merge_database,
commands::database::open_database_in_new_instance,
commands::database::validate_database_file,
commands::database::get_groups,
commands::entry::get_entries,
commands::entry::get_favorite_entries,
Expand Down