feat: upgrade OpenMLS 0.5 → 0.8 for security patches and GREASE support
Migrates all MLS code in quicprochat-core from OpenMLS 0.5 to 0.8: - StorageProvider replaces OpenMlsKeyStore (keystore.rs full rewrite) - HybridCryptoProvider updated for new OpenMlsProvider trait - Group operations updated for new API signatures - MLS state persistence via MemoryStorage serialization - tls_codec 0.3 → 0.4, openmls_traits/rust_crypto 0.2 → 0.5
This commit is contained in:
@@ -1449,10 +1449,8 @@ pub(crate) async fn cmd_dm(
|
||||
},
|
||||
display_name: format!("@{username}"),
|
||||
mls_group_blob: member
|
||||
.group_ref()
|
||||
.map(bincode::serialize)
|
||||
.transpose()
|
||||
.context("serialize group")?,
|
||||
.serialize_mls_state()
|
||||
.context("serialize MLS state")?,
|
||||
keystore_blob: None,
|
||||
member_keys,
|
||||
unread_count: 0,
|
||||
@@ -1493,10 +1491,8 @@ pub(crate) fn cmd_create_group(session: &mut SessionState, name: &str) -> anyhow
|
||||
kind: ConversationKind::Group { name: name.to_string() },
|
||||
display_name: format!("#{name}"),
|
||||
mls_group_blob: member
|
||||
.group_ref()
|
||||
.map(bincode::serialize)
|
||||
.transpose()
|
||||
.context("serialize group")?,
|
||||
.serialize_mls_state()
|
||||
.context("serialize MLS state")?,
|
||||
keystore_blob: None,
|
||||
member_keys,
|
||||
unread_count: 0,
|
||||
@@ -1780,9 +1776,7 @@ pub(crate) async fn cmd_join(
|
||||
kind: ConversationKind::Group { name: display.clone() },
|
||||
display_name: format!("#{display}"),
|
||||
mls_group_blob: new_member
|
||||
.group_ref()
|
||||
.map(bincode::serialize)
|
||||
.transpose()
|
||||
.serialize_mls_state()
|
||||
.context("serialize joined group")?,
|
||||
keystore_blob: None,
|
||||
member_keys,
|
||||
@@ -3186,8 +3180,9 @@ async fn try_auto_join(
|
||||
};
|
||||
|
||||
let mls_blob = member
|
||||
.group_ref()
|
||||
.and_then(|g| bincode::serialize(g).ok());
|
||||
.serialize_mls_state()
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let conv = Conversation {
|
||||
id: conv_id.clone(),
|
||||
|
||||
@@ -16,7 +16,7 @@ use quicprochat_core::{DiskKeyStore, GroupMember, HybridKeypair, IdentityKeypair
|
||||
use super::conversation::{
|
||||
now_ms, Conversation, ConversationId, ConversationKind, ConversationStore,
|
||||
};
|
||||
use super::state::{load_or_init_state, keystore_path};
|
||||
use super::state::load_or_init_state;
|
||||
|
||||
/// Runtime state for an interactive REPL session.
|
||||
pub struct SessionState {
|
||||
@@ -109,7 +109,7 @@ impl SessionState {
|
||||
/// Migrate the legacy single-group from StoredState into the conversation DB.
|
||||
fn migrate_legacy_group(
|
||||
&mut self,
|
||||
state_path: &Path,
|
||||
_state_path: &Path,
|
||||
group_blob: &Option<Vec<u8>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let blob = match group_blob {
|
||||
@@ -117,16 +117,22 @@ impl SessionState {
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// Reconstruct GroupMember using the legacy keystore and group blob.
|
||||
let ks_path = keystore_path(state_path);
|
||||
let ks = DiskKeyStore::persistent(&ks_path)?;
|
||||
let group = bincode::deserialize(blob).context("decode legacy group")?;
|
||||
let member = GroupMember::new_with_state(
|
||||
// Legacy group blobs used openmls 0.5 serde format. After the 0.8
|
||||
// upgrade the blob format changed to storage-provider state. Attempt
|
||||
// to load from the new format; if that fails, skip the legacy group.
|
||||
let group_id_guess = &blob[..blob.len().min(16)];
|
||||
let member = match GroupMember::new_from_storage_bytes(
|
||||
Arc::clone(&self.identity),
|
||||
ks,
|
||||
Some(group),
|
||||
blob,
|
||||
group_id_guess,
|
||||
false, // legacy groups are classical
|
||||
);
|
||||
) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "skipping incompatible legacy group blob (openmls version mismatch)");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let group_id_bytes = member.group_id().unwrap_or_default();
|
||||
|
||||
@@ -182,26 +188,31 @@ impl SessionState {
|
||||
|
||||
/// Create a GroupMember from a stored conversation.
|
||||
fn create_member_from_conv(&self, conv: &Conversation) -> anyhow::Result<GroupMember> {
|
||||
let ks_path = self.keystore_path_for(&conv.id);
|
||||
let ks = DiskKeyStore::persistent(&ks_path)
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::warn!(path = %ks_path.display(), error = %e, "DiskKeyStore open failed, falling back to ephemeral");
|
||||
DiskKeyStore::ephemeral()
|
||||
});
|
||||
|
||||
let group = conv
|
||||
.mls_group_blob
|
||||
.as_ref()
|
||||
.map(|b| bincode::deserialize(b))
|
||||
.transpose()
|
||||
.context("decode MLS group from conversation db")?;
|
||||
|
||||
Ok(GroupMember::new_with_state(
|
||||
Arc::clone(&self.identity),
|
||||
ks,
|
||||
group,
|
||||
conv.is_hybrid,
|
||||
))
|
||||
if let Some(blob) = conv.mls_group_blob.as_ref() {
|
||||
let group_id = conv.id.0.as_slice();
|
||||
let member = GroupMember::new_from_storage_bytes(
|
||||
Arc::clone(&self.identity),
|
||||
blob,
|
||||
group_id,
|
||||
conv.is_hybrid,
|
||||
)
|
||||
.context("restore MLS state from conversation db")?;
|
||||
Ok(member)
|
||||
} else {
|
||||
// No MLS state — create an empty member.
|
||||
let ks_path = self.keystore_path_for(&conv.id);
|
||||
let ks = DiskKeyStore::persistent(&ks_path)
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::warn!(path = %ks_path.display(), error = %e, "DiskKeyStore open failed, falling back to ephemeral");
|
||||
DiskKeyStore::ephemeral()
|
||||
});
|
||||
Ok(GroupMember::new_with_state(
|
||||
Arc::clone(&self.identity),
|
||||
ks,
|
||||
None,
|
||||
conv.is_hybrid,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Path for a per-conversation keystore file.
|
||||
@@ -214,10 +225,8 @@ impl SessionState {
|
||||
pub fn save_member(&self, conv_id: &ConversationId) -> anyhow::Result<()> {
|
||||
let member = self.members.get(conv_id).context("no such conversation")?;
|
||||
let blob = member
|
||||
.group_ref()
|
||||
.map(bincode::serialize)
|
||||
.transpose()
|
||||
.context("serialize MLS group")?;
|
||||
.serialize_mls_state()
|
||||
.context("serialize MLS state")?;
|
||||
|
||||
let member_keys = member.member_identities();
|
||||
|
||||
|
||||
@@ -27,18 +27,31 @@ pub struct StoredState {
|
||||
/// Cached member public keys for group participants.
|
||||
#[serde(default)]
|
||||
pub member_keys: Vec<Vec<u8>>,
|
||||
/// MLS group ID bytes, needed to reload the group from StorageProvider state.
|
||||
#[serde(default)]
|
||||
pub group_id: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl StoredState {
|
||||
pub fn into_parts(self, state_path: &Path) -> anyhow::Result<(GroupMember, Option<HybridKeypair>)> {
|
||||
let identity = Arc::new(IdentityKeypair::from_seed(self.identity_seed));
|
||||
let group = self
|
||||
.group
|
||||
.map(|bytes| bincode::deserialize(&bytes).context("decode group"))
|
||||
.transpose()?;
|
||||
let key_store = DiskKeyStore::persistent(keystore_path(state_path))?;
|
||||
let hybrid = self.hybrid_key.is_some();
|
||||
let member = GroupMember::new_with_state(identity, key_store, group, hybrid);
|
||||
|
||||
let member = match (self.group.as_ref(), self.group_id.as_ref()) {
|
||||
(Some(storage_bytes), Some(gid)) => {
|
||||
GroupMember::new_from_storage_bytes(
|
||||
identity,
|
||||
storage_bytes,
|
||||
gid,
|
||||
hybrid,
|
||||
)
|
||||
.context("restore MLS state from stored state")?
|
||||
}
|
||||
_ => {
|
||||
let key_store = DiskKeyStore::persistent(keystore_path(state_path))?;
|
||||
GroupMember::new_with_state(identity, key_store, None, hybrid)
|
||||
}
|
||||
};
|
||||
|
||||
let hybrid_kp = self
|
||||
.hybrid_key
|
||||
@@ -50,15 +63,15 @@ impl StoredState {
|
||||
|
||||
pub fn from_parts(member: &GroupMember, hybrid_kp: Option<&HybridKeypair>) -> anyhow::Result<Self> {
|
||||
let group = member
|
||||
.group_ref()
|
||||
.map(|g| bincode::serialize(g).context("serialize group"))
|
||||
.transpose()?;
|
||||
.serialize_mls_state()
|
||||
.context("serialize MLS state")?;
|
||||
|
||||
Ok(Self {
|
||||
identity_seed: *member.identity_seed(),
|
||||
group,
|
||||
hybrid_key: hybrid_kp.map(|kp| kp.to_bytes()),
|
||||
member_keys: Vec::new(),
|
||||
group_id: member.group_id(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -245,6 +258,7 @@ mod tests {
|
||||
hybrid_key: None,
|
||||
group: None,
|
||||
member_keys: Vec::new(),
|
||||
group_id: None,
|
||||
};
|
||||
let password = "test-password";
|
||||
let plaintext = bincode::serialize(&state).unwrap();
|
||||
@@ -268,6 +282,7 @@ mod tests {
|
||||
}),
|
||||
group: None,
|
||||
member_keys: Vec::new(),
|
||||
group_id: None,
|
||||
};
|
||||
let password = "another-password";
|
||||
let plaintext = bincode::serialize(&state).unwrap();
|
||||
@@ -285,6 +300,7 @@ mod tests {
|
||||
hybrid_key: None,
|
||||
group: None,
|
||||
member_keys: Vec::new(),
|
||||
group_id: None,
|
||||
};
|
||||
let plaintext = bincode::serialize(&state).unwrap();
|
||||
let encrypted = encrypt_state("correct", &plaintext).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user