car-sync 0.50.0

Multi-device sync core for Common Agent Runtime — replica-tagged append-only oplog + deterministic CRDT fold
Documentation
//! End-to-end proof of the org-scope shared-brain path — the whole chain, no
//! shortcuts: a granter SIGNS and PUBLISHES wraps into a directory; each member
//! RESOLVES their `K_org` from that directory (never handed the key); a member
//! authors a `Scope::Shared { org }` op that TRAVELS a shared relay; another
//! member reads it. Mutual readability = decryptable AND delivered — this test
//! proves both, and proves the fail-closed negatives.
//!
//! This is the executable proof that the crypto + resolver + provider + delivery
//! all compose. It stays a TEST: the production daemon does not yet open a shared
//! `Scope::Shared { org }` relay scope (see the org-scope activation wiring), so
//! nothing here activates org scope for a real tenant.

use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;

use car_sync::crypto::{ed25519_verifying, wrap_org_key, x25519_public};
use car_sync::org_key_directory::{InMemoryOrgKeyDirectory, OrgKeyDirectory};
use car_sync::{
    resolve_org_root, DerivedKeyProvider, InMemoryRelay, OrgAwareKeyProvider, OrgSigningKey,
    OrgVerifyingKey, RelayConfig, Scope, StretchedMaster, Surface, SyncKeyProvider, SyncSession,
};
use serde_json::json;

const ORG: &str = "testorg";
const K_ORG: [u8; 32] = [0x5a; 32];

// Issued-high-entropy masters (skip Argon2id) so this e2e stays fast — it proves
// the org-key sharing path, not the passphrase stretch (that's a crypto unit test).
fn x25519_id(secret: &[u8], user: &str) -> x25519_dalek::StaticSecret {
    car_sync::derive_x25519_identity(
        &StretchedMaster::from_issued_high_entropy(secret, user),
        user,
    )
}
fn ed25519_id(secret: &[u8], user: &str) -> OrgSigningKey {
    car_sync::derive_ed25519_identity(
        &StretchedMaster::from_issued_high_entropy(secret, user),
        user,
    )
}

fn granter() -> OrgSigningKey {
    ed25519_id(b"granter-login-secret", "acc_granter")
}

/// Build a member's key provider by RESOLVING their root from `dir` (the real
/// path — never pass `K_org` in). `trusted` is the org's granter policy. Returns
/// the provider and whether a root resolved (for the fail-closed assertions).
fn member_provider(
    dir: &dyn OrgKeyDirectory,
    passphrase: &str,
    account_id: &str,
    trusted: &[OrgVerifyingKey],
) -> (Arc<dyn SyncKeyProvider>, bool) {
    let my_secret = x25519_id(passphrase.as_bytes(), account_id);
    let resolved =
        resolve_org_root(dir, ORG, &my_secret, account_id, trusted).expect("directory reachable");
    let mut roots = HashMap::new();
    let granted = resolved.is_some();
    if let Some(r) = resolved {
        assert_eq!(*r.root, K_ORG, "resolver recovered the true org key");
        roots.insert(ORG.to_string(), BTreeMap::from([(r.epoch, r.root)]));
    }
    // Personal delegate keyed on the member's OWN passphrase (so personal ops stay
    // private per member). Org ops key on the shared resolved root.
    let personal: Arc<dyn SyncKeyProvider> = Arc::new(DerivedKeyProvider::from_login_secret(
        passphrase.as_bytes(),
        account_id,
    ));
    (Arc::new(OrgAwareKeyProvider::new(personal, roots)), granted)
}

/// Granter wraps `K_org` for `account_id`'s published identity key and publishes.
fn grant(
    dir: &mut InMemoryOrgKeyDirectory,
    passphrase: &str,
    account_id: &str,
    signer: &OrgSigningKey,
    publisher: &str,
) {
    let recipient_pub = x25519_public(&x25519_id(passphrase.as_bytes(), account_id));
    let w = wrap_org_key(
        &K_ORG,
        ORG,
        1,
        account_id,
        &recipient_pub,
        publisher,
        signer,
    )
    .unwrap();
    dir.publish_wrapped(&w).unwrap();
}

fn session(
    device: &str,
    dir: &tempfile::TempDir,
    provider: Arc<dyn SyncKeyProvider>,
) -> SyncSession {
    SyncSession::open(
        device,
        &dir.path().join("oplog.jsonl"),
        &dir.path().join("ckpts"),
        car_sync::system_clock(),
    )
    .unwrap()
    .with_key_provider(provider)
}

/// The org op's decrypted body as seen in a session's folded state, or None if it
/// stayed an opaque envelope (member could not decrypt).
fn read_body(sess: &SyncSession, id: &str) -> Option<serde_json::Value> {
    sess.state()
        .log_entries(&Surface::Knowledge.tag())
        .iter()
        .find(|e| e.payload.get("id").and_then(|v| v.as_str()) == Some(id))
        .map(|e| e.payload.get("body").cloned().unwrap_or(json!(null)))
}

#[test]
fn two_granted_members_share_an_org_op_end_to_end() {
    // --- setup: granter publishes wraps for alice and bob into the directory ---
    let mut dir = InMemoryOrgKeyDirectory::new();
    grant(
        &mut dir,
        "alice-pass",
        "acc_alice",
        &granter(),
        "acc_granter",
    );
    grant(&mut dir, "bob-pass", "acc_bob", &granter(), "acc_granter");

    let trusted = [ed25519_verifying(&granter())];
    let (alice_kp, a_granted) = member_provider(&dir, "alice-pass", "acc_alice", &trusted);
    let (bob_kp, b_granted) = member_provider(&dir, "bob-pass", "acc_bob", &trusted);
    assert!(a_granted && b_granted, "both members resolved K_org");

    // --- shared relay = the org delivery channel ---
    let mut relay = InMemoryRelay::new(RelayConfig::default(), car_sync::system_clock());
    let (da, db) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
    let mut alice = session("alice-dev", &da, alice_kp);
    let mut bob = session("bob-dev", &db, bob_kp);

    // Alice authors an ORG-scoped op + a PERSONAL op.
    let org_body = "the shared org brain remembers this";
    alice
        .append(
            Scope::Shared { org: ORG.into() },
            Surface::Knowledge,
            json!({"id": "org-note", "body": org_body}),
        )
        .unwrap();
    alice
        .append(
            Scope::Personal,
            Surface::Knowledge,
            json!({"id": "alice-note", "body": "private"}),
        )
        .unwrap();

    // Deliver: alice pushes ciphertext, bob pulls.
    alice.pump(&mut relay).unwrap();
    bob.pump(&mut relay).unwrap();

    // MUTUAL READABILITY: bob, a different member, decrypts alice's org op — with a
    // key he RESOLVED from the directory, over an op that TRAVELED the relay.
    assert_eq!(
        read_body(&bob, "org-note")
            .as_ref()
            .and_then(|v| v.as_str()),
        Some(org_body),
        "bob reads alice's org op via the shared K_org"
    );
    // PERSONAL ISOLATION: bob received alice's personal op (it traveled) but cannot
    // read it — different personal keys → stays an opaque envelope.
    assert!(
        read_body(&bob, "alice-note").is_none(),
        "bob must NOT read alice's personal op — the provider swap didn't collapse personal into shared"
    );
}

#[test]
fn ungranted_member_receives_but_cannot_read_the_org_op() {
    // Alice is granted; Mallory is NOT (no wrap published for her). She still syncs
    // the op over the shared relay — proving delivery — but fails closed.
    let mut dir = InMemoryOrgKeyDirectory::new();
    grant(
        &mut dir,
        "alice-pass",
        "acc_alice",
        &granter(),
        "acc_granter",
    );

    let trusted = [ed25519_verifying(&granter())];
    let (alice_kp, _) = member_provider(&dir, "alice-pass", "acc_alice", &trusted);
    let (mallory_kp, m_granted) = member_provider(&dir, "mallory-pass", "acc_mallory", &trusted);
    assert!(
        !m_granted,
        "mallory has no trusted grant → Ok(None) → DenyCipher"
    );

    let mut relay = InMemoryRelay::new(RelayConfig::default(), car_sync::system_clock());
    let (da, dm) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
    let mut alice = session("alice-dev", &da, alice_kp);
    let mut mallory = session("mallory-dev", &dm, mallory_kp);

    alice
        .append(
            Scope::Shared { org: ORG.into() },
            Surface::Knowledge,
            json!({"id": "org-note", "body": "secret"}),
        )
        .unwrap();
    alice.pump(&mut relay).unwrap();
    let report = mallory.pump(&mut relay).unwrap();

    assert!(
        report.folded >= 1,
        "mallory DID receive the op over the relay (delivery happened)"
    );
    assert!(
        read_body(&mallory, "org-note").is_none(),
        "but mallory cannot decrypt it — fail-closed, opaque envelope"
    );
}

#[test]
fn wrap_from_an_untrusted_publisher_does_not_grant_access() {
    // Mallory (a member) publishes a wrap of the REAL K_org to Bob's key, signed
    // with HER key. Bob trusts only the granter → the resolver skips it → Bob gets
    // no root. Proves verify-before-decrypt gates the resolver, not just unwrap.
    let mut dir = InMemoryOrgKeyDirectory::new();
    let mallory_signer = ed25519_id(b"mallory-login", "acc_mallory");
    grant(
        &mut dir,
        "bob-pass",
        "acc_bob",
        &mallory_signer,
        "acc_mallory",
    ); // untrusted signer

    let trusted = [ed25519_verifying(&granter())]; // bob trusts only the granter
    let (_, b_granted) = member_provider(&dir, "bob-pass", "acc_bob", &trusted);
    assert!(!b_granted, "an untrusted-signed wrap grants nothing");
}