feat(recovery): implement header backup/restore, BIP-39 recovery key, and integrity verification

This commit is contained in:
2026-09-08 09:49:08 +02:00
parent 2d14c64c3e
commit 1c8a860184
11 changed files with 1451 additions and 57 deletions
+124
View File
@@ -507,6 +507,130 @@ impl Database {
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?;
Ok(())
}
/// Schreibt oder stellt die Metadaten in der `meta`-Tabelle wieder her (z. B. nach Restore oder Header-Neugenerierung).
pub fn restore_meta(&self, meta: &ContainerMeta) -> Result<()> {
let conn = self.conn.lock().unwrap();
let params_json = serde_json::to_string(&meta.kdf_params)?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS meta (
magic BLOB NOT NULL,
version INTEGER NOT NULL,
kdf_salt BLOB NOT NULL,
kdf_params TEXT NOT NULL,
wrapped_dek BLOB NOT NULL,
header_nonce BLOB NOT NULL,
header_tag BLOB NOT NULL
);",
)?;
conn.execute("DELETE FROM meta", [])?;
conn.execute(
"INSERT INTO meta (magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![
MAGIC_BYTES.as_slice(),
meta.version,
meta.kdf_salt.as_slice(),
params_json,
meta.wrapped_dek,
meta.header_nonce.as_slice(),
meta.header_tag.as_slice(),
],
)?;
Ok(())
}
/// Führt SQLite-eigene Integritäts- und Foreign-Key-Prüfungen aus.
pub fn run_sqlite_integrity_check(&self) -> Result<Vec<String>> {
let conn = self.conn.lock().unwrap();
let mut issues = Vec::new();
// 1. PRAGMA integrity_check
let mut stmt = conn.prepare("PRAGMA integrity_check;")?;
let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
for r in rows {
let msg = r?;
if msg.to_lowercase() != "ok" {
issues.push(format!("SQLite integrity error: {msg}"));
}
}
// 2. PRAGMA foreign_key_check
let mut fk_stmt = conn.prepare("PRAGMA foreign_key_check;")?;
let fk_rows = fk_stmt.query_map([], |row| {
let table: String = row.get(0)?;
let rowid: i64 = row.get(1)?;
let parent: String = row.get(2)?;
let fkid: i64 = row.get(3)?;
Ok(format!(
"Foreign Key Verletzung in Tabelle '{table}', RowId {rowid}, Ziel '{parent}', FK #{fkid}"
))
})?;
for r in fk_rows {
issues.push(r?);
}
Ok(issues)
}
/// Zählt die Anzahl von Verzeichnissen, Dateien und Daten-Chunks im Container.
pub fn count_nodes_and_chunks(&self) -> Result<(usize, usize, usize)> {
let conn = self.conn.lock().unwrap();
let dirs: i64 =
conn.query_row("SELECT COUNT(*) FROM nodes WHERE is_dir = 1", [], |r| {
r.get(0)
})?;
let files: i64 =
conn.query_row("SELECT COUNT(*) FROM nodes WHERE is_dir = 0", [], |r| {
r.get(0)
})?;
let chunks: i64 = conn.query_row("SELECT COUNT(*) FROM chunks", [], |r| r.get(0))?;
Ok((dirs as usize, files as usize, chunks as usize))
}
/// Listet alle Knoten (Dateien und Ordner) im gesamten Baum auf.
pub fn list_all_nodes(&self) -> Result<Vec<NodeRecord>> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT id, parent_id, name, is_dir, size, created_at, modified_at FROM nodes ORDER BY id ASC",
)?;
let rows = stmt.query_map([], |row| {
Ok(NodeRecord {
id: row.get(0)?,
parent_id: row.get(1)?,
name: row.get(2)?,
is_dir: row.get::<_, i32>(3)? != 0,
size: row.get::<_, i64>(4)? as u64,
created_at: row.get::<_, i64>(5)? as u64,
modified_at: row.get::<_, i64>(6)? as u64,
})
})?;
let mut result = Vec::new();
for r in rows {
result.push(r?);
}
Ok(result)
}
/// Liefert alle vorhandenen Chunk-Identifikatoren (node_id, chunk_index).
pub fn list_all_chunk_headers(&self) -> Result<Vec<(i64, u32)>> {
let conn = self.conn.lock().unwrap();
let mut stmt =
conn.prepare("SELECT node_id, chunk_index FROM chunks ORDER BY node_id, chunk_index")?;
let rows = stmt.query_map([], |row| {
let nid: i64 = row.get(0)?;
let cidx: u32 = row.get(1)?;
Ok((nid, cidx))
})?;
let mut result = Vec::new();
for r in rows {
result.push(r?);
}
Ok(result)
}
}
#[cfg(test)]