syncular-runtime 0.1.0

Shared Rust runtime for Syncular SQLite-backed native and browser clients.
Documentation
use crate::error::{Result, SyncularError};
use crate::protocol::{
    BlobRef, PullResponse, PushBatchRequest, PushCommitRequest, PushCommitResponse, SyncChange,
    SyncOperation,
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::sync::Arc;

pub const DEFAULT_FIELD_ENCRYPTION_PREFIX: &str = "dgsync:e2ee:1:";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum FieldDecryptionErrorMode {
    Throw,
    KeepCiphertext,
}

impl Default for FieldDecryptionErrorMode {
    fn default() -> Self {
        Self::Throw
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FieldEncryptionRule {
    pub scope: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub table: Option<String>,
    pub fields: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub row_id_field: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldEncryptionContext {
    pub actor_id: String,
    pub client_id: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldEncryptionTarget {
    pub scope: String,
    pub table: String,
    pub row_id: String,
    pub field: String,
}

pub trait FieldEncryptionKeyProvider: Send + Sync {
    fn get_key(&self, _kid: &str) -> Result<Vec<u8>> {
        Err(e2ee_feature_disabled())
    }

    fn encryption_kid(
        &self,
        _ctx: &FieldEncryptionContext,
        _target: &FieldEncryptionTarget,
    ) -> Result<String> {
        Err(e2ee_feature_disabled())
    }
}

#[derive(Debug, Clone)]
pub struct StaticFieldEncryptionKeys;

impl StaticFieldEncryptionKeys {
    pub fn new(
        _keys: impl IntoIterator<Item = (impl Into<String>, impl Into<Vec<u8>>)>,
        _encryption_kid: Option<String>,
    ) -> Result<Self> {
        Err(e2ee_feature_disabled())
    }

    pub fn from_key_material(
        _keys: BTreeMap<String, String>,
        _encryption_kid: Option<String>,
    ) -> Result<Self> {
        Err(e2ee_feature_disabled())
    }
}

impl FieldEncryptionKeyProvider for StaticFieldEncryptionKeys {}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StaticFieldEncryptionConfig {
    pub rules: Vec<FieldEncryptionRule>,
    pub keys: BTreeMap<String, String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub encryption_kid: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub decryption_error_mode: Option<FieldDecryptionErrorMode>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub envelope_prefix: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StaticBlobEncryptionConfig {
    pub keys: BTreeMap<String, String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub encryption_kid: Option<String>,
}

#[derive(Debug, Clone)]
pub struct EncryptedBlobBody {
    pub blob: BlobRef,
    pub body: Vec<u8>,
}

#[derive(Debug, Clone)]
pub struct BlobEncryption;

impl BlobEncryption {
    pub fn from_static_config(_config: StaticBlobEncryptionConfig) -> Result<Self> {
        Err(e2ee_feature_disabled())
    }

    pub fn from_static_config_json(config_json: &str) -> Result<Option<Self>> {
        let trimmed = config_json.trim();
        if trimmed.is_empty() || trimmed == "null" {
            return Ok(None);
        }
        Err(e2ee_feature_disabled())
    }

    pub fn encrypt_blob(&self, _plaintext: &[u8], _mime_type: &str) -> Result<EncryptedBlobBody> {
        Err(e2ee_feature_disabled())
    }

    pub fn decrypt_blob(&self, _blob: &BlobRef, _body: &[u8]) -> Result<Vec<u8>> {
        Err(e2ee_feature_disabled())
    }

    pub fn ensure_can_decrypt(&self, _blob: &BlobRef) -> Result<()> {
        Err(e2ee_feature_disabled())
    }
}

#[derive(Clone)]
pub struct FieldEncryption {
    rules: Vec<FieldEncryptionRule>,
}

impl FieldEncryption {
    pub fn new(
        _rules: Vec<FieldEncryptionRule>,
        _keys: Arc<dyn FieldEncryptionKeyProvider>,
    ) -> Result<Self> {
        Err(e2ee_feature_disabled())
    }

    pub fn with_options(
        _rules: Vec<FieldEncryptionRule>,
        _keys: Arc<dyn FieldEncryptionKeyProvider>,
        _envelope_prefix: Option<String>,
        _decryption_error_mode: FieldDecryptionErrorMode,
    ) -> Result<Self> {
        Err(e2ee_feature_disabled())
    }

    pub fn from_static_config(_config: StaticFieldEncryptionConfig) -> Result<Self> {
        Err(e2ee_feature_disabled())
    }

    pub fn from_static_config_json(config_json: &str) -> Result<Option<Self>> {
        let trimmed = config_json.trim();
        if trimmed.is_empty() || trimmed == "null" {
            return Ok(None);
        }
        Err(e2ee_feature_disabled())
    }

    pub fn rules(&self) -> &[FieldEncryptionRule] {
        &self.rules
    }

    pub fn transform_push_batch_request(
        &self,
        _ctx: &FieldEncryptionContext,
        _request: PushBatchRequest,
    ) -> Result<PushBatchRequest> {
        Err(e2ee_feature_disabled())
    }

    pub fn transform_push_commit_request(
        &self,
        _ctx: &FieldEncryptionContext,
        _request: PushCommitRequest,
    ) -> Result<PushCommitRequest> {
        Err(e2ee_feature_disabled())
    }

    pub fn transform_operations_for_push(
        &self,
        _ctx: &FieldEncryptionContext,
        _operations: Vec<SyncOperation>,
    ) -> Result<Vec<SyncOperation>> {
        Err(e2ee_feature_disabled())
    }

    pub fn transform_push_response(
        &self,
        _ctx: &FieldEncryptionContext,
        _outbox_operations: &[SyncOperation],
        _response: PushCommitResponse,
    ) -> Result<PushCommitResponse> {
        Err(e2ee_feature_disabled())
    }

    pub fn transform_pull_response(
        &self,
        _ctx: &FieldEncryptionContext,
        _response: PullResponse,
    ) -> Result<PullResponse> {
        Err(e2ee_feature_disabled())
    }

    pub fn transform_snapshot_row(
        &self,
        _ctx: &FieldEncryptionContext,
        _snapshot_table: &str,
        _row: serde_json::Value,
    ) -> Result<serde_json::Value> {
        Err(e2ee_feature_disabled())
    }

    pub fn transform_change(
        &self,
        _ctx: &FieldEncryptionContext,
        _change: SyncChange,
    ) -> Result<SyncChange> {
        Err(e2ee_feature_disabled())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct X25519KeyPair {
    pub public_key: String,
    pub private_key: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WrappedKey {
    pub ephemeral_public: Vec<u8>,
    pub ciphertext: Vec<u8>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WrappedKeyJson {
    pub ephemeral_public: String,
    pub ciphertext: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum ParsedKeyShare {
    #[serde(rename = "symmetric")]
    Symmetric { key: String, kid: Option<String> },
    #[serde(rename = "publicKey")]
    PublicKey { public_key: String },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Argon2idKeyDerivationParams {
    pub memory_kib: u32,
    pub iterations: u32,
    pub parallelism: u32,
}

impl Default for Argon2idKeyDerivationParams {
    fn default() -> Self {
        Self {
            memory_kib: 19 * 1024,
            iterations: 2,
            parallelism: 1,
        }
    }
}

pub fn generate_symmetric_key() -> Result<Vec<u8>> {
    Err(e2ee_feature_disabled())
}

pub fn key_to_base64url(_key: &[u8]) -> Result<String> {
    Err(e2ee_feature_disabled())
}

pub fn base64url_to_key(_encoded: &str) -> Result<Vec<u8>> {
    Err(e2ee_feature_disabled())
}

pub fn key_to_mnemonic(_key: &[u8]) -> Result<String> {
    Err(e2ee_feature_disabled())
}

pub fn mnemonic_to_key(_phrase: &str) -> Result<Vec<u8>> {
    Err(e2ee_feature_disabled())
}

pub fn generate_x25519_keypair() -> Result<X25519KeyPair> {
    Err(e2ee_feature_disabled())
}

pub fn public_key_to_mnemonic(_public_key: &[u8]) -> Result<String> {
    Err(e2ee_feature_disabled())
}

pub fn mnemonic_to_public_key(_phrase: &str) -> Result<Vec<u8>> {
    Err(e2ee_feature_disabled())
}

pub fn wrap_key_for_recipient(
    _recipient_public_key: &[u8],
    _symmetric_key: &[u8],
) -> Result<WrappedKey> {
    Err(e2ee_feature_disabled())
}

pub fn unwrap_key(_my_private_key: &[u8], _wrapped: &WrappedKey) -> Result<Vec<u8>> {
    Err(e2ee_feature_disabled())
}

pub fn encode_wrapped_key(_wrapped: &WrappedKey) -> String {
    String::new()
}

pub fn decode_wrapped_key(_encoded: &str) -> Result<WrappedKey> {
    Err(e2ee_feature_disabled())
}

pub fn wrapped_key_to_json(wrapped: &WrappedKey) -> WrappedKeyJson {
    WrappedKeyJson {
        ephemeral_public: String::from_utf8_lossy(&wrapped.ephemeral_public).into_owned(),
        ciphertext: String::from_utf8_lossy(&wrapped.ciphertext).into_owned(),
    }
}

pub fn wrapped_key_from_json(_json: &WrappedKeyJson) -> Result<WrappedKey> {
    Err(e2ee_feature_disabled())
}

pub fn key_to_share_url(_key: &[u8], _kid: Option<&str>) -> Result<String> {
    Err(e2ee_feature_disabled())
}

pub fn public_key_to_share_url(_public_key: &[u8]) -> Result<String> {
    Err(e2ee_feature_disabled())
}

pub fn parse_share_url(_url: &str) -> Result<ParsedKeyShare> {
    Err(e2ee_feature_disabled())
}

pub fn derive_scoped_passphrase_key_pbkdf2(
    _passphrase: &str,
    _scope: &str,
    _iterations: u32,
) -> Result<Vec<u8>> {
    Err(e2ee_feature_disabled())
}

pub fn derive_passphrase_key_argon2id(
    _passphrase: &str,
    _salt: &[u8],
    _params: Argon2idKeyDerivationParams,
) -> Result<Vec<u8>> {
    Err(e2ee_feature_disabled())
}

pub fn encryption_helpers_json(_method: &str, _args_json: &str) -> Result<String> {
    Err(e2ee_feature_disabled())
}

fn e2ee_feature_disabled() -> SyncularError {
    SyncularError::config("E2EE support is not enabled in this Syncular runtime build")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn non_null_config_reports_disabled_feature() {
        let err = match FieldEncryption::from_static_config_json("{}") {
            Ok(_) => panic!("non-null E2EE config should require e2ee feature"),
            Err(err) => err,
        };
        assert!(err.to_string().contains("E2EE support is not enabled"));
    }
}