remem-ai 0.6.35

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use std::io::Read;
use std::path::Path;

use anyhow::{bail, Context, Result};
use rusqlite::{Connection, OpenFlags};

use crate::db;

pub(super) enum ExistingKeyDatabaseState {
    Encrypted,
    Missing(db::CipherKey),
}

pub(super) fn inspect_existing_key_database(
    key_path: &Path,
    db_path: &Path,
) -> Result<ExistingKeyDatabaseState> {
    let key_text = std::fs::read_to_string(key_path)
        .with_context(|| format!("read SQLCipher key file {}", key_path.display()))?;
    let key = db::parse_cipher_key(&key_text)
        .with_context(|| format!("parse SQLCipher key file {}", key_path.display()))?
        .with_context(|| format!("SQLCipher key file is empty: {}", key_path.display()))?;

    if !db_path.exists() {
        return Ok(ExistingKeyDatabaseState::Missing(key));
    }

    let conn = Connection::open_with_flags(db_path, OpenFlags::SQLITE_OPEN_READ_ONLY)
        .with_context(|| format!("open existing remem database {}", db_path.display()))?;
    match db::configure_cipher(&conn, Some(&key)) {
        Ok(true) => Ok(ExistingKeyDatabaseState::Encrypted),
        Ok(false) => bail!(
            "SQLCipher key was not applied while validating {}",
            db_path.display()
        ),
        Err(error) => {
            if sqlite_file_has_plaintext_header(db_path)? {
                bail!(
                    "SQLCipher key file exists at {} but {} is still plaintext SQLite; move the stale key aside and rerun `remem encrypt`, or restore the encrypted database that matches the key",
                    key_path.display(),
                    db_path.display()
                );
            }
            bail!(
                "SQLCipher key file exists at {} but does not unlock {}; key/DB mismatch or encrypted database corruption: {error:#}",
                key_path.display(),
                db_path.display()
            );
        }
    }
}

pub(super) fn initialize_missing_database_with_key(
    db_path: &Path,
    key: &db::CipherKey,
) -> Result<()> {
    if let Some(parent) = db_path.parent() {
        std::fs::create_dir_all(parent)?;
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let perms = std::fs::Permissions::from_mode(0o700);
            if let Err(error) = std::fs::set_permissions(parent, perms) {
                crate::log::warn("db", &format!("cannot set data dir permissions: {error}"));
            }
        }
    }

    let conn = db::open_configured_connection(db_path, Some(key))?;
    crate::retrieval::vector::load_vec_extension(&conn)?;
    crate::migrate::run_migrations(&conn)?;
    crate::retrieval::vector::ensure_vec_table(&conn)?;
    Ok(())
}

fn sqlite_file_has_plaintext_header(path: &Path) -> Result<bool> {
    let mut file =
        std::fs::File::open(path).with_context(|| format!("inspect {}", path.display()))?;
    let mut header = [0_u8; 16];
    match file.read_exact(&mut header) {
        Ok(()) => Ok(&header == b"SQLite format 3\0"),
        Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => Ok(false),
        Err(error) => Err(error).with_context(|| format!("read {}", path.display())),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::db::test_support::ScopedTestDataDir;

    #[test]
    fn run_encrypt_with_existing_key_verifies_encrypted_database() -> Result<()> {
        let test_dir = ScopedTestDataDir::new("encrypt-existing-key-encrypted-db");
        std::env::remove_var("REMEM_ALLOW_PLAINTEXT_DB");
        std::env::remove_var("REMEM_CIPHER_KEY");
        std::fs::create_dir_all(&test_dir.path)?;
        let raw_hex = "3".repeat(64);
        std::fs::write(test_dir.path.join(".key"), format!("v2:{raw_hex}"))?;
        {
            let conn = Connection::open(test_dir.db_path())?;
            db::configure_cipher(&conn, Some(&db::CipherKey::Raw(raw_hex.clone())))?;
            conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)", [])?;
        }

        super::super::maintenance::run_encrypt(false)?;

        let conn = Connection::open(test_dir.db_path())?;
        db::configure_cipher(&conn, Some(&db::CipherKey::Raw(raw_hex)))?;
        let count: i64 =
            conn.query_row("SELECT COUNT(*) FROM sqlite_master", [], |row| row.get(0))?;
        assert!(count > 0);
        Ok(())
    }

    #[test]
    fn run_encrypt_with_existing_key_rejects_plaintext_database() -> Result<()> {
        let test_dir = ScopedTestDataDir::new("encrypt-existing-key-plaintext-db");
        std::env::remove_var("REMEM_ALLOW_PLAINTEXT_DB");
        std::env::remove_var("REMEM_CIPHER_KEY");
        std::fs::create_dir_all(&test_dir.path)?;
        std::fs::write(test_dir.path.join(".key"), format!("v2:{}", "4".repeat(64)))?;
        {
            let conn = Connection::open(test_dir.db_path())?;
            conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)", [])?;
        }

        let error = super::super::maintenance::run_encrypt(false)
            .expect_err("plaintext DB with stale key must fail");

        let message = error.to_string();
        assert!(message.contains("plaintext SQLite"), "got: {message}");
        let header = std::fs::read(test_dir.db_path())?;
        assert_eq!(&header[..16], b"SQLite format 3\0");
        assert!(!test_dir.path.join("remem.db.bak").exists());
        Ok(())
    }

    #[test]
    fn run_encrypt_with_existing_key_rejects_mismatched_encrypted_database() -> Result<()> {
        let test_dir = ScopedTestDataDir::new("encrypt-existing-key-mismatch");
        std::env::remove_var("REMEM_ALLOW_PLAINTEXT_DB");
        std::env::remove_var("REMEM_CIPHER_KEY");
        std::fs::create_dir_all(&test_dir.path)?;
        let database_key = "5".repeat(64);
        let wrong_key = "6".repeat(64);
        std::fs::write(test_dir.path.join(".key"), format!("v2:{wrong_key}"))?;
        {
            let conn = Connection::open(test_dir.db_path())?;
            db::configure_cipher(&conn, Some(&db::CipherKey::Raw(database_key)))?;
            conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)", [])?;
        }

        let error = super::super::maintenance::run_encrypt(false)
            .expect_err("wrong key for encrypted DB must fail");

        let message = error.to_string();
        assert!(message.contains("does not unlock"), "got: {message}");
        assert!(!test_dir.path.join("remem.db.bak").exists());
        Ok(())
    }

    #[test]
    fn run_encrypt_with_existing_key_initializes_missing_database_from_key_file() -> Result<()> {
        let test_dir = ScopedTestDataDir::new("encrypt-existing-key-missing-db");
        std::env::remove_var("REMEM_ALLOW_PLAINTEXT_DB");
        std::fs::create_dir_all(&test_dir.path)?;
        let persisted_key = "7".repeat(64);
        let env_key = "8".repeat(64);
        std::fs::write(test_dir.path.join(".key"), format!("v2:{persisted_key}"))?;
        std::env::set_var("REMEM_CIPHER_KEY", format!("v2:{env_key}"));
        test_dir.remove_db_files();

        let result = super::super::maintenance::run_encrypt(false);
        std::env::remove_var("REMEM_CIPHER_KEY");
        result?;

        assert!(test_dir.db_path().exists());
        let header = std::fs::read(test_dir.db_path())?;
        assert_ne!(&header[..16], b"SQLite format 3\0");

        let conn = Connection::open(test_dir.db_path())?;
        db::configure_cipher(&conn, Some(&db::CipherKey::Raw(persisted_key)))?;
        let count: i64 =
            conn.query_row("SELECT COUNT(*) FROM sqlite_master", [], |row| row.get(0))?;
        assert!(count > 0);

        let wrong_conn = Connection::open(test_dir.db_path())?;
        let error = db::configure_cipher(&wrong_conn, Some(&db::CipherKey::Raw(env_key)))
            .expect_err("env key must not unlock database initialized from .key");
        assert!(
            error.to_string().contains("schema is unreadable"),
            "got: {error:#}"
        );
        Ok(())
    }

    #[test]
    fn run_encrypt_with_existing_key_rejects_invalid_key_file_for_missing_database() -> Result<()> {
        let test_dir = ScopedTestDataDir::new("encrypt-existing-invalid-key-missing-db");
        std::env::remove_var("REMEM_ALLOW_PLAINTEXT_DB");
        std::fs::create_dir_all(&test_dir.path)?;
        std::fs::write(test_dir.path.join(".key"), "v2:not-hex")?;
        std::env::set_var("REMEM_CIPHER_KEY", format!("v2:{}", "9".repeat(64)));
        test_dir.remove_db_files();

        let result = super::super::maintenance::run_encrypt(false);
        std::env::remove_var("REMEM_CIPHER_KEY");
        let error = result.expect_err("invalid persisted key must fail before DB creation");

        let message = format!("{error:#}");
        assert!(
            message.contains("parse SQLCipher key file"),
            "got: {message}"
        );
        assert!(
            !test_dir.db_path().exists(),
            "invalid persisted key must not create a database"
        );
        Ok(())
    }
}