ping-openmls-sdk-mls-store 0.6.13

Persistent OpenMLS provider — SQLite (native) / memory backends ([CR-4])
Documentation
//! SQLite I/O for the persistent provider. Native targets only.
//!
//! Schema is intentionally trivial — single-row table holding one CBOR-encoded blob.
//! See `src/lib.rs` for the rationale (v0.1 chooses simplicity; future revisions can
//! upgrade to per-row writes behind the same `StorageBackend` API).

use std::path::Path;

use parking_lot::Mutex;
use rusqlite::{params, Connection, OpenFlags};
use zeroize::Zeroizing;

use crate::{Error, Result};

const SCHEMA_USER_VERSION: i64 = 1;
const BLOB_ROW_ID: i64 = 1;

#[derive(Debug)]
pub(crate) struct SqliteFile {
    conn: Mutex<Connection>,
}

impl SqliteFile {
    pub(crate) fn open(path: &Path, encryption_key: Option<&Zeroizing<[u8; 32]>>) -> Result<Self> {
        let conn = Connection::open_with_flags(
            path,
            OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
        )
        .map_err(|e| Error::Io(format!("open {}: {e}", path.display())))?;

        // SQLCipher key, if supplied. PRAGMA key MUST come before any other statement
        // on a fresh connection; otherwise the file is treated as plaintext and any
        // subsequent reads will fail with a binary-garbage error.
        if let Some(key) = encryption_key {
            // Use lower-hex `x'…'` form so we can pass raw bytes without dealing with
            // string-quoting edge cases (NUL bytes, etc.).
            let hex_key: String = key.iter().map(|b| format!("{:02x}", b)).collect();
            conn.execute_batch(&format!("PRAGMA key = \"x'{hex_key}'\";"))
                .map_err(|e| Error::Io(format!("pragma key: {e}")))?;

            // Touch the schema to confirm the key worked. On a wrong key, this errors
            // with "file is not a database" — translate to our specific variant.
            if let Err(e) = conn.execute_batch("SELECT count(*) FROM sqlite_master;") {
                let msg = format!("{e}");
                if msg.contains("file is not a database") || msg.contains("file is encrypted") {
                    return Err(Error::EncryptionKeyMismatch);
                }
                return Err(Error::Io(format!("verify key: {e}")));
            }
        }

        // Sensible pragmas.
        conn.execute_batch(
            r#"
            PRAGMA journal_mode = WAL;
            PRAGMA synchronous = NORMAL;
            PRAGMA foreign_keys = OFF;
            "#,
        )
        .map_err(|e| Error::Io(format!("pragma init: {e}")))?;

        // Migrate.
        let v: i64 = conn
            .query_row("PRAGMA user_version", [], |r| r.get(0))
            .map_err(|e| Error::Io(format!("user_version: {e}")))?;
        match v {
            0 => {
                conn.execute_batch(&format!(
                    r#"
                    CREATE TABLE IF NOT EXISTS mls_blob (
                        id    INTEGER PRIMARY KEY CHECK(id = {BLOB_ROW_ID}),
                        value BLOB NOT NULL
                    );
                    PRAGMA user_version = {SCHEMA_USER_VERSION};
                    "#
                ))
                .map_err(|e| Error::Io(format!("migrate v0->v1: {e}")))?;
            }
            v if v == SCHEMA_USER_VERSION => { /* current */ }
            other => return Err(Error::UnsupportedSchema(other)),
        }

        Ok(Self {
            conn: Mutex::new(conn),
        })
    }

    /// Read the persisted MLS-state blob. Returns `None` on a freshly-created file
    /// (no checkpoint has happened yet).
    pub(crate) fn read_blob(&self) -> Result<Option<Vec<u8>>> {
        let conn = self.conn.lock();
        let mut stmt = conn
            .prepare_cached("SELECT value FROM mls_blob WHERE id = ?")
            .map_err(|e| Error::Io(format!("prepare read_blob: {e}")))?;
        let row: Option<Vec<u8>> = stmt
            .query_row(params![BLOB_ROW_ID], |r| r.get::<_, Vec<u8>>(0))
            .map(Some)
            .or_else(|e| match e {
                rusqlite::Error::QueryReturnedNoRows => Ok(None),
                other => Err(other),
            })
            .map_err(|e| Error::Io(format!("read_blob: {e}")))?;
        Ok(row)
    }

    /// Write the persisted MLS-state blob, replacing whatever was there. Single-row
    /// table — UPSERT on `id = BLOB_ROW_ID`.
    pub(crate) fn write_blob(&self, value: &[u8]) -> Result<()> {
        let conn = self.conn.lock();
        conn.execute(
            "INSERT INTO mls_blob (id, value) VALUES (?, ?) \
             ON CONFLICT(id) DO UPDATE SET value = excluded.value",
            params![BLOB_ROW_ID, value],
        )
        .map_err(|e| Error::Io(format!("write_blob: {e}")))?;
        Ok(())
    }
}