age-vault 0.1.0

A secure vault for managing age-encrypted accounts and data.
Documentation
//! Database persistence layer for accounts and metadata.
//!
//! This module handles low-level storage using the neuxdb embedded database.
//! It provides functions to initialize tables, save/load accounts, and manage
//! the salt used for key derivation.

use crate::account::Account;
use crate::error::{Error, Result};
use base64::Engine;
use neuxdb::ColumnType;
use neuxdb::Database;

const ACCOUNTS_TABLE: &str = "accounts";
const METADATA_TABLE: &str = "_metadata";

/// Initializes the `accounts` table in the database.
///
/// If the table already exists, this function does nothing.
///
/// # Parameters
///
/// * `db` - Mutable reference to the database.
///
/// # Errors
///
/// Returns [`Error::Db`] if creating the table fails.
///
/// # Panics
///
/// This function does not panic.
///
/// # Examples
///
/// ```no_run
/// use age_vault::store::init_accounts_table;
/// use neuxdb::Database;
/// # use age_vault::Error;
/// # fn main() -> Result<(), Error> {
/// let mut db = Database::create("path/to/db.ndbx", "password")?;
/// init_accounts_table(&mut db)?;
/// # Ok(())
/// # }
/// ```
pub fn init_accounts_table(db: &mut Database) -> Result<()> {
    if db.list_tables().contains(&ACCOUNTS_TABLE.to_string()) {
        return Ok(());
    }
    db.create_table(
        ACCOUNTS_TABLE,
        vec![
            ("id", ColumnType::Text),
            ("name", ColumnType::Text),
            ("role", ColumnType::Text),
            ("public_key", ColumnType::Text),
            ("encrypted_secret_key", ColumnType::Text),
            ("enabled", ColumnType::Bool),
        ],
    )?;
    Ok(())
}

/// Saves an account to the database.
///
/// The account's role is serialized to JSON, and the encrypted secret key is
/// base64-encoded before storage.
///
/// # Parameters
///
/// * `db` - Mutable reference to the database.
/// * `account` - The account to save.
///
/// # Errors
///
/// Returns [`Error::Serde`] if serialization of the role fails.
/// Returns [`Error::Db`] if the database insert or commit fails.
///
/// # Panics
///
/// This function does not panic.
///
/// # Examples
///
/// ```no_run
/// use age_vault::{Account, Role, store::save_account};
/// # use age_vault::Error;
/// # use neuxdb::Database;
/// # fn main() -> Result<(), Error> {
/// # let mut db = Database::create("path.ndbx", "pw")?;
/// let account = Account {
///     id: "123".to_string(),
///     name: "alice".to_string(),
///     role: Role::Admin,
///     public_key: "age1...".to_string(),
///     encrypted_secret_key: vec![0u8; 64],
///     enabled: true,
/// };
/// save_account(&mut db, &account)?;
/// # Ok(())
/// # }
/// ```
pub fn save_account(db: &mut Database, account: &Account) -> Result<()> {
    let enc_key_b64 =
        base64::engine::general_purpose::STANDARD.encode(&account.encrypted_secret_key);
    let role_str = serde_json::to_string(&account.role)?;
    let row = vec![
        account.id.clone().into(),
        account.name.clone().into(),
        role_str.into(),
        account.public_key.clone().into(),
        enc_key_b64.into(),
        account.enabled.into(),
    ];
    db.insert(ACCOUNTS_TABLE, row)?;
    db.commit()?;
    Ok(())
}

/// Loads all accounts from the database.
///
/// Reads every row from the `accounts` table, decodes base64 fields, and
/// deserializes the role from JSON.
///
/// # Parameters
///
/// * `db` - Shared reference to the database.
///
/// # Returns
///
/// A vector of `Account` structs.
///
/// # Errors
///
/// Returns [`Error::Db`] if the database query fails.
/// Returns [`Error::Serde`] if role JSON deserialization fails.
/// Returns [`Error::DecryptionFailed`] if base64 decoding fails or boolean parsing fails.
///
/// # Panics
///
/// This function may panic if any database row contains fewer than 6 columns.
/// (This should not happen with a consistent database.)
///
/// # Examples
///
/// ```no_run
/// use age_vault::store::load_accounts;
/// # use age_vault::Error;
/// # use neuxdb::Database;
/// # fn main() -> Result<(), Error> {
/// # let db = Database::open("path.ndbx", "pw")?;
/// let accounts = load_accounts(&db)?;
/// for acc in accounts {
///     println!("{}: {}", acc.id, acc.name);
/// }
/// # Ok(())
/// # }
/// ```
pub fn load_accounts(db: &Database) -> Result<Vec<Account>> {
    let rows = db.select(ACCOUNTS_TABLE, None, None)?;
    let mut accounts = Vec::new();
    for row in rows {
        let id = row[0].to_string();
        let name = row[1].to_string();
        let role: crate::account::Role = serde_json::from_str(&row[2].to_string())?;
        let public_key = row[3].to_string();
        let enc_key_b64 = row[4].to_string();
        let enabled = row[5].to_string().parse::<bool>().map_err(|e| {
            Error::DecryptionFailed(format!("Invalid bool for account {}: {}", id, e))
        })?;
        let encrypted_secret_key = base64::engine::general_purpose::STANDARD
            .decode(&enc_key_b64)
            .map_err(|e| Error::DecryptionFailed(e.to_string()))?;
        accounts.push(Account {
            id,
            name,
            role,
            public_key,
            encrypted_secret_key,
            enabled,
        });
    }
    Ok(accounts)
}

/// Initializes the `_metadata` table in the database.
///
/// This table stores key-value pairs (e.g., cryptographic salt).
/// If the table already exists, this function does nothing.
///
/// # Parameters
///
/// * `db` - Mutable reference to the database.
///
/// # Errors
///
/// Returns [`Error::Db`] if creating the table fails.
///
/// # Panics
///
/// This function does not panic.
///
/// # Examples
///
/// ```no_run
/// use age_vault::store::init_metadata_table;
/// # use neuxdb::Database;
/// # use age_vault::Error;
/// # fn main() -> Result<(), Error> {
/// # let mut db = Database::create("path.ndbx", "pw")?;
/// init_metadata_table(&mut db)?;
/// # Ok(())
/// # }
/// ```
pub fn init_metadata_table(db: &mut Database) -> Result<()> {
    if db.list_tables().contains(&METADATA_TABLE.to_string()) {
        return Ok(());
    }
    db.create_table(
        METADATA_TABLE,
        vec![("key", ColumnType::Text), ("value", ColumnType::Text)],
    )?;
    Ok(())
}

/// Saves the cryptographic salt to the metadata table.
///
/// The salt is stored under the key `"salt"` as a base64-encoded string.
///
/// # Parameters
///
/// * `db` - Mutable reference to the database.
/// * `salt` - Salt bytes (typically 16 bytes).
///
/// # Errors
///
/// Returns [`Error::Db`] if the insert or commit fails.
///
/// # Panics
///
/// This function does not panic.
///
/// # Examples
///
/// ```no_run
/// use age_vault::store::save_metadata_salt;
/// # use neuxdb::Database;
/// # use age_vault::Error;
/// # fn main() -> Result<(), Error> {
/// # let mut db = Database::create("path.ndbx", "pw")?;
/// let salt = b"0123456789abcdef";
/// save_metadata_salt(&mut db, salt)?;
/// # Ok(())
/// # }
/// ```
pub fn save_metadata_salt(db: &mut Database, salt: &[u8]) -> Result<()> {
    let salt_b64 = base64::engine::general_purpose::STANDARD.encode(salt);
    let row = vec!["salt".into(), salt_b64.into()];
    db.insert(METADATA_TABLE, row)?;
    Ok(())
}

/// Loads the cryptographic salt from the metadata table.
///
/// Retrieves the value for key `"salt"` and decodes it from base64.
///
/// # Parameters
///
/// * `db` - Shared reference to the database.
///
/// # Returns
///
/// The salt as `Vec<u8>`.
///
/// # Errors
///
/// Returns [`Error::DecryptionFailed`] if the salt is missing or base64 decoding fails.
/// Returns [`Error::Db`] if the database query fails.
///
/// # Panics
///
/// This function panics if the retrieved row does not contain at least 2 columns.
///
/// # Examples
///
/// ```no_run
/// use age_vault::store::load_metadata_salt;
/// # use neuxdb::Database;
/// # use age_vault::Error;
/// # fn main() -> Result<(), Error> {
/// # let db = Database::open("path.ndbx", "pw")?;
/// let salt = load_metadata_salt(&db)?;
/// assert_eq!(salt.len(), 16);
/// # Ok(())
/// # }
/// ```
pub fn load_metadata_salt(db: &Database) -> Result<Vec<u8>> {
    let rows = db.select(
        METADATA_TABLE,
        None,
        Some(&|row| row[0].to_string() == "salt"),
    )?;
    if rows.is_empty() {
        return Err(Error::DecryptionFailed(
            "Salt metadata not found in vault".into(),
        ));
    }
    let salt_b64 = rows[0][1].to_string();
    let salt = base64::engine::general_purpose::STANDARD
        .decode(salt_b64)
        .map_err(|e| Error::DecryptionFailed(format!("Invalid salt base64: {}", e)))?;
    Ok(salt)
}