Cursor: Apply local changes for cloud agent

This commit is contained in:
2026-02-22 22:29:52 +01:00
parent 6b8b61c6ae
commit 41c57a1181
21 changed files with 616 additions and 142 deletions

View File

@@ -221,11 +221,11 @@ mod tests {
fn roundtrip_chat() {
let body = b"hello";
let encoded = serialize_chat(body, None);
let (t, msg) = parse(&encoded).unwrap();
let (t, msg) = parse(&encoded).expect("serialize_chat output is valid");
assert_eq!(t, MessageType::Chat);
match &msg {
AppMessage::Chat { message_id: _, body: b } => assert_eq!(b.as_slice(), body),
_ => panic!("expected Chat"),
assert!(matches!(&msg, AppMessage::Chat { .. }), "expected Chat, got {:?}", msg);
if let AppMessage::Chat { body: b, .. } = &msg {
assert_eq!(b.as_slice(), body);
}
}
@@ -234,25 +234,23 @@ mod tests {
let ref_id = [1u8; 16];
let body = b"reply text";
let encoded = serialize_reply(ref_id, body);
let (t, msg) = parse(&encoded).unwrap();
let (t, msg) = parse(&encoded).expect("serialize_reply output is valid");
assert_eq!(t, MessageType::Reply);
match &msg {
AppMessage::Reply { ref_msg_id, body: b } => {
assert_eq!(ref_msg_id, &ref_id);
assert_eq!(b.as_slice(), body);
}
_ => panic!("expected Reply"),
assert!(matches!(&msg, AppMessage::Reply { .. }), "expected Reply, got {:?}", msg);
if let AppMessage::Reply { ref_msg_id, body: b } = &msg {
assert_eq!(ref_msg_id, &ref_id);
assert_eq!(b.as_slice(), body);
}
}
#[test]
fn roundtrip_typing() {
let encoded = serialize_typing(1);
let (t, msg) = parse(&encoded).unwrap();
let (t, msg) = parse(&encoded).expect("serialize_typing output is valid");
assert_eq!(t, MessageType::Typing);
match &msg {
AppMessage::Typing { active } => assert_eq!(*active, 1),
_ => panic!("expected Typing"),
assert!(matches!(&msg, AppMessage::Typing { .. }), "expected Typing, got {:?}", msg);
if let AppMessage::Typing { active } = &msg {
assert_eq!(*active, 1);
}
}
}

View File

@@ -2,9 +2,10 @@
//!
//! # Design
//!
//! [`GroupMember`] wraps an openmls [`MlsGroup`] plus the per-client
//! [`StoreCrypto`] backend. The backend is **persistent** — it holds the
//! in-memory key store that maps init-key references to HPKE private keys.
//! [`GroupMember`] wraps an openmls [`MlsGroup`] plus a per-client crypto
//! backend ([`StoreCrypto`] or [`HybridCryptoProvider`] for M7). The backend
//! is **persistent** — it holds the key store that maps init-key references
//! to HPKE private keys (classical or hybrid).
//! openmls's `new_from_welcome` reads those private keys from the key store to
//! decrypt the Welcome, so the same backend instance must be used from
//! `generate_key_package` through `join_group`.
@@ -37,6 +38,7 @@ use openmls_traits::OpenMlsCryptoProvider;
use crate::{
error::CoreError,
hybrid_crypto::HybridCryptoProvider,
identity::IdentityKeypair,
keystore::{DiskKeyStore, StoreCrypto},
};
@@ -49,6 +51,9 @@ const CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA2
/// Per-client MLS state: identity keypair, crypto backend, and optional group.
///
/// Generic over the crypto provider `P`: [`StoreCrypto`] (default, classical)
/// or [`HybridCryptoProvider`] (M7, post-quantum hybrid KEM).
///
/// # Lifecycle
///
/// ```text
@@ -60,10 +65,10 @@ const CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA2
/// ├─ send_message(msg) → encrypt application data
/// └─ receive_message(b) → decrypt; returns Some(plaintext) or None
/// ```
pub struct GroupMember {
/// Persistent crypto backend. Holds the in-memory key store with HPKE
pub struct GroupMember<P: OpenMlsCryptoProvider = StoreCrypto> {
/// Crypto backend (classical or hybrid). Holds the key store with HPKE
/// private keys created during `generate_key_package`.
backend: StoreCrypto,
backend: P,
/// Long-term Ed25519 identity keypair. Also used as the MLS `Signer`.
identity: Arc<IdentityKeypair>,
/// Active MLS group, if any.
@@ -72,8 +77,8 @@ pub struct GroupMember {
config: MlsGroupConfig,
}
impl GroupMember {
/// Create a new `GroupMember` with a fresh crypto backend.
impl GroupMember<StoreCrypto> {
/// Create a new `GroupMember` with a fresh crypto backend (classical X25519).
pub fn new(identity: Arc<IdentityKeypair>) -> Self {
Self::new_with_state(identity, DiskKeyStore::ephemeral(), None)
}
@@ -105,6 +110,41 @@ impl GroupMember {
config,
}
}
}
impl GroupMember<HybridCryptoProvider> {
/// Create a `GroupMember` that uses post-quantum hybrid KEM (X25519 + ML-KEM-768) for HPKE.
///
/// All members of a group must use the same provider type: if the creator uses
/// `new_with_hybrid`, KeyPackages will have hybrid init keys and joiners must
/// also use `new_with_hybrid` to decrypt the Welcome.
pub fn new_with_hybrid(
identity: Arc<IdentityKeypair>,
key_store: DiskKeyStore,
) -> Self {
Self::new_with_state_hybrid(identity, key_store, None)
}
/// Create a PQ `GroupMember` from persisted state (identity, key store, optional group).
pub fn new_with_state_hybrid(
identity: Arc<IdentityKeypair>,
key_store: DiskKeyStore,
group: Option<MlsGroup>,
) -> Self {
let config = MlsGroupConfig::builder()
.use_ratchet_tree_extension(true)
.build();
Self {
backend: HybridCryptoProvider::new(key_store),
identity,
group,
config,
}
}
}
impl<P: OpenMlsCryptoProvider> GroupMember<P> {
// ── KeyPackage ────────────────────────────────────────────────────────────
@@ -414,7 +454,7 @@ impl GroupMember {
}
/// Return a reference to the underlying crypto backend.
pub fn backend(&self) -> &StoreCrypto {
pub fn backend(&self) -> &P {
&self.backend
}
@@ -498,6 +538,48 @@ mod tests {
assert_eq!(pt_creator, b"hello back");
}
/// M7: Full two-party MLS round-trip with post-quantum hybrid KEM (HybridCryptoProvider).
#[test]
fn two_party_mls_round_trip_hybrid() {
let creator_id = Arc::new(IdentityKeypair::generate());
let joiner_id = Arc::new(IdentityKeypair::generate());
let key_store_creator = DiskKeyStore::ephemeral();
let key_store_joiner = DiskKeyStore::ephemeral();
let mut creator =
GroupMember::<HybridCryptoProvider>::new_with_hybrid(Arc::clone(&creator_id), key_store_creator);
let mut joiner =
GroupMember::<HybridCryptoProvider>::new_with_hybrid(Arc::clone(&joiner_id), key_store_joiner);
let joiner_kp = joiner
.generate_key_package()
.expect("joiner KeyPackage (hybrid)");
creator
.create_group(b"test-group-m7-hybrid")
.expect("creator create group");
let (_, welcome) = creator
.add_member(&joiner_kp)
.expect("creator add joiner");
joiner.join_group(&welcome).expect("joiner join group");
let ct_creator = creator.send_message(b"hello pq").expect("creator send");
let pt_joiner = joiner
.receive_message(&ct_creator)
.expect("joiner recv")
.expect("application message");
assert_eq!(pt_joiner, b"hello pq");
let ct_joiner = joiner.send_message(b"hello back pq").expect("joiner send");
let pt_creator = creator
.receive_message(&ct_joiner)
.expect("creator recv")
.expect("application message");
assert_eq!(pt_creator, b"hello back pq");
}
/// `group_id()` returns None before create_group, Some afterwards.
#[test]
fn group_id_lifecycle() {

View File

@@ -38,4 +38,5 @@ pub use hybrid_kem::{
pub use hybrid_crypto::{HybridCrypto, HybridCryptoProvider};
pub use identity::IdentityKeypair;
pub use keypackage::{generate_key_package, validate_keypackage_ciphersuite};
pub use keystore::DiskKeyStore;
pub use keystore::{DiskKeyStore, StoreCrypto};
pub use openmls::prelude::MlsGroup;