Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92dfea7329 | |||
| 27f08e92e5 |
2 changed files with 24 additions and 331 deletions
|
|
@ -1,325 +0,0 @@
|
|||
# Call PQ Secret Envelope Migration
|
||||
|
||||
## What Changed
|
||||
|
||||
The backend no longer treats `CallSecret` as a plain ECDH/base64 string. It now parses, stores, forwards, and returns a typed call secret envelope containing `SecretId`, `VersionNumber`, `EncryptedSecret`, `KemCiphertext`, and `WrappingScheme`.
|
||||
|
||||
Call secret storage is now recipient-specific. `CallGroup.secrets` is keyed by recipient user ID and stores a `CallSecretEnvelope`, so reconnect/session payloads return only the envelope intended for the current user.
|
||||
|
||||
Both authenticated and anonymous `call_invite` handlers now reject a plain-string `CallSecret` with `BadRequest`. The forwarded `CallInvite` push includes the envelope object, and anonymous invite forwarding now includes `CallSecret` as well.
|
||||
|
||||
## Why
|
||||
|
||||
The call client is migrating from an old ECDH shared-secret protocol to a recipient-specific PQ KEM wrapping protocol. The backend must not decrypt, decapsulate, derive keys, or inspect the encrypted secret material. It only validates that the envelope shape is present and forwards/stores the opaque bytes unchanged.
|
||||
|
||||
The previous pairwise string storage mirrored one string for both `(inviter, invitee)` and `(invitee, inviter)`, which is incompatible with recipient-specific KEM ciphertexts. Each recipient needs their own `EncryptedSecret` and `KemCiphertext` envelope.
|
||||
|
||||
## Raw Diff
|
||||
|
||||
```diff
|
||||
diff --git a/src/anonymous_clients/anonymous_client_connection.rs b/src/anonymous_clients/anonymous_client_connection.rs
|
||||
index b2c90e7..ddf69b8 100644
|
||||
--- a/src/anonymous_clients/anonymous_client_connection.rs
|
||||
+++ b/src/anonymous_clients/anonymous_client_connection.rs
|
||||
@@ -7,7 +7,7 @@ use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::anonymous_clients::anonymous_manager::{self, generate_username};
|
||||
-use crate::calls::call_manager;
|
||||
+use crate::calls::{call_group::CallSecretEnvelope, call_manager};
|
||||
use crate::data::user::UserStatus;
|
||||
use crate::omega::omega_connection::{OmegaConnection, get_omega_connection};
|
||||
use crate::rho::connection::GeneralConnection;
|
||||
@@ -367,12 +367,21 @@ impl AnonymousClientConnection {
|
||||
}
|
||||
};
|
||||
|
||||
- let secret = cv
|
||||
- .get_data(DataType::CallSecret)
|
||||
- .as_str()
|
||||
- .map(|s| s.to_string());
|
||||
- let invited =
|
||||
- call_manager::add_invite(call_id, self.user_id, receiver_id as u64, secret).await;
|
||||
+ let secret = match CallSecretEnvelope::from_data_value(cv.get_data(DataType::CallSecret)) {
|
||||
+ Some(secret) => secret,
|
||||
+ None => {
|
||||
+ self.send_error_response(&cv.get_id(), CommunicationType::BadRequest)
|
||||
+ .await;
|
||||
+ return;
|
||||
+ }
|
||||
+ };
|
||||
+ let invited = call_manager::add_invite(
|
||||
+ call_id,
|
||||
+ self.user_id,
|
||||
+ receiver_id as u64,
|
||||
+ Some(secret.clone()),
|
||||
+ )
|
||||
+ .await;
|
||||
if !invited {
|
||||
self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidCallId)
|
||||
.await;
|
||||
@@ -423,6 +432,7 @@ impl AnonymousClientConnection {
|
||||
let forward = CommunicationValue::new(CommunicationType::CallInvite)
|
||||
.with_receiver(receiver_id as u64)
|
||||
.with_sender(sender_id)
|
||||
+ .add_typed_default(DataType::CallSecret, secret.to_data_value())
|
||||
.add_typed_default(DataType::CallId, DataValue::Str(call_id.to_string()))
|
||||
.add_typed_default(
|
||||
DataType::ReceiverId,
|
||||
diff --git a/src/calls/call_group.rs b/src/calls/call_group.rs
|
||||
index d751802..a5c76ea 100755
|
||||
--- a/src/calls/call_group.rs
|
||||
+++ b/src/calls/call_group.rs
|
||||
@@ -1,4 +1,4 @@
|
||||
-use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||
+use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue, TypeMap};
|
||||
use serde_json::Map;
|
||||
|
||||
use std::{collections::BTreeMap, env, sync::Arc, time::Duration};
|
||||
@@ -16,7 +16,80 @@ pub struct CallGroup {
|
||||
pub show: RwLock<bool>,
|
||||
pub anonymous_joining: RwLock<bool>,
|
||||
pub short_link: RwLock<Option<String>>,
|
||||
- pub secrets: RwLock<BTreeMap<(u64, u64), String>>,
|
||||
+ pub secrets: RwLock<BTreeMap<u64, CallSecretEnvelope>>,
|
||||
+}
|
||||
+
|
||||
+#[derive(Clone, Debug)]
|
||||
+pub struct CallSecretEnvelope {
|
||||
+ pub secret_id: String,
|
||||
+ pub version_number: i64,
|
||||
+ pub encrypted_secret: Vec<u8>,
|
||||
+ pub kem_ciphertext: Vec<u8>,
|
||||
+ pub wrapping_scheme: String,
|
||||
+}
|
||||
+
|
||||
+impl CallSecretEnvelope {
|
||||
+ pub fn from_data_value(value: &DataValue) -> Option<Self> {
|
||||
+ let tm = TypeMap::latest();
|
||||
+ let secret_id = value
|
||||
+ .get_field(DataType::SecretId.to_id(&tm))?
|
||||
+ .as_str()?
|
||||
+ .to_string();
|
||||
+ let version_number = value
|
||||
+ .get_field(DataType::VersionNumber.to_id(&tm))?
|
||||
+ .as_signed_number()
|
||||
+ .and_then(|n| i64::try_from(n).ok())
|
||||
+ .or_else(|| {
|
||||
+ value
|
||||
+ .get_field(DataType::VersionNumber.to_id(&tm))?
|
||||
+ .as_number()
|
||||
+ .and_then(|n| i64::try_from(n).ok())
|
||||
+ })?;
|
||||
+ let encrypted_secret = value
|
||||
+ .get_field(DataType::EncryptedSecret.to_id(&tm))?
|
||||
+ .as_bytes()?;
|
||||
+ let kem_ciphertext = value
|
||||
+ .get_field(DataType::KemCiphertext.to_id(&tm))?
|
||||
+ .as_bytes()?;
|
||||
+ let wrapping_scheme = value
|
||||
+ .get_field(DataType::WrappingScheme.to_id(&tm))?
|
||||
+ .as_str()?
|
||||
+ .to_string();
|
||||
+
|
||||
+ Some(Self {
|
||||
+ secret_id,
|
||||
+ version_number,
|
||||
+ encrypted_secret,
|
||||
+ kem_ciphertext,
|
||||
+ wrapping_scheme,
|
||||
+ })
|
||||
+ }
|
||||
+
|
||||
+ pub fn to_data_value(&self) -> DataValue {
|
||||
+ let tm = TypeMap::latest();
|
||||
+ let mut map: BTreeMap<DataTypeId, DataValue> = BTreeMap::new();
|
||||
+ map.insert(
|
||||
+ DataType::SecretId.to_id(&tm),
|
||||
+ DataValue::Str(self.secret_id.clone()),
|
||||
+ );
|
||||
+ map.insert(
|
||||
+ DataType::VersionNumber.to_id(&tm),
|
||||
+ DataValue::SignedNumber(self.version_number.into()),
|
||||
+ );
|
||||
+ map.insert(
|
||||
+ DataType::EncryptedSecret.to_id(&tm),
|
||||
+ DataValue::Bytes(self.encrypted_secret.clone()),
|
||||
+ );
|
||||
+ map.insert(
|
||||
+ DataType::KemCiphertext.to_id(&tm),
|
||||
+ DataValue::Bytes(self.kem_ciphertext.clone()),
|
||||
+ );
|
||||
+ map.insert(
|
||||
+ DataType::WrappingScheme.to_id(&tm),
|
||||
+ DataValue::Str(self.wrapping_scheme.clone()),
|
||||
+ );
|
||||
+ DataValue::container_from_map(&map)
|
||||
+ }
|
||||
}
|
||||
|
||||
impl CallGroup {
|
||||
diff --git a/src/calls/call_manager.rs b/src/calls/call_manager.rs
|
||||
index 6c33fd6..718f5f1 100644
|
||||
--- a/src/calls/call_manager.rs
|
||||
+++ b/src/calls/call_manager.rs
|
||||
@@ -3,7 +3,11 @@ use once_cell::sync::Lazy;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
-use crate::calls::{call_group::CallGroup, call_util, caller::Caller};
|
||||
+use crate::calls::{
|
||||
+ call_group::{CallGroup, CallSecretEnvelope},
|
||||
+ call_util,
|
||||
+ caller::Caller,
|
||||
+};
|
||||
|
||||
pub static CALL_GROUPS: Lazy<DashMap<Uuid, Arc<CallGroup>>> = Lazy::new(|| DashMap::new());
|
||||
#[allow(dead_code)]
|
||||
@@ -74,7 +78,7 @@ pub async fn add_invite(
|
||||
call_id: Uuid,
|
||||
inviter_id: u64,
|
||||
invitee_id: u64,
|
||||
- secret: Option<String>,
|
||||
+ secret: Option<CallSecretEnvelope>,
|
||||
) -> bool {
|
||||
if let Some(cg) = CALL_GROUPS.get(&call_id) {
|
||||
let mut members = cg.members.write().await;
|
||||
@@ -88,8 +92,7 @@ pub async fn add_invite(
|
||||
|
||||
if let Some(secret) = secret {
|
||||
let mut secrets = cg.secrets.write().await;
|
||||
- secrets.insert((inviter_id, invitee_id), secret.clone());
|
||||
- secrets.insert((invitee_id, inviter_id), secret);
|
||||
+ secrets.insert(invitee_id, secret);
|
||||
}
|
||||
|
||||
return true;
|
||||
diff --git a/src/rho/client_connection.rs b/src/rho/client_connection.rs
|
||||
index 2f0ca96..56a6cba 100644
|
||||
--- a/src/rho/client_connection.rs
|
||||
+++ b/src/rho/client_connection.rs
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::anonymous_clients::anonymous_manager;
|
||||
-use crate::calls::{call_manager, call_util};
|
||||
+use crate::calls::{call_group::CallSecretEnvelope, call_manager, call_util};
|
||||
use crate::omega::omega_connection::get_omega_connection;
|
||||
use crate::rho::connection::GeneralConnection;
|
||||
use crate::rho::{rho_connection::RhoConnection, rho_manager};
|
||||
@@ -354,12 +354,21 @@ impl ClientConnection {
|
||||
}
|
||||
};
|
||||
|
||||
- let secret = cv
|
||||
- .get_data(DataType::CallSecret)
|
||||
- .as_str()
|
||||
- .map(|s| s.to_string());
|
||||
- let invited =
|
||||
- call_manager::add_invite(call_id, self.user_id, receiver_id as u64, secret).await;
|
||||
+ let secret = match CallSecretEnvelope::from_data_value(cv.get_data(DataType::CallSecret)) {
|
||||
+ Some(secret) => secret,
|
||||
+ None => {
|
||||
+ self.send_error_response(cv.get_id(), CommunicationType::BadRequest)
|
||||
+ .await;
|
||||
+ return;
|
||||
+ }
|
||||
+ };
|
||||
+ let invited = call_manager::add_invite(
|
||||
+ call_id,
|
||||
+ self.user_id,
|
||||
+ receiver_id as u64,
|
||||
+ Some(secret.clone()),
|
||||
+ )
|
||||
+ .await;
|
||||
if !invited {
|
||||
self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidCallId)
|
||||
.await;
|
||||
@@ -410,10 +419,7 @@ impl ClientConnection {
|
||||
let forward = CommunicationValue::new(CommunicationType::CallInvite)
|
||||
.with_receiver(receiver_id as u64)
|
||||
.with_sender(sender_id as u64)
|
||||
- .add_typed_default(
|
||||
- DataType::CallSecret,
|
||||
- cv.get_data(DataType::CallSecret).clone(),
|
||||
- )
|
||||
+ .add_typed_default(DataType::CallSecret, secret.to_data_value())
|
||||
.add_typed_default(DataType::CallId, DataValue::Str(call_id.to_string()))
|
||||
.add_typed_default(
|
||||
DataType::ReceiverId,
|
||||
diff --git a/src/rho/connection.rs b/src/rho/connection.rs
|
||||
index 90abac0..2e97711 100755
|
||||
--- a/src/rho/connection.rs
|
||||
+++ b/src/rho/connection.rs
|
||||
@@ -207,7 +207,16 @@ impl GeneralConnection {
|
||||
);
|
||||
}
|
||||
|
||||
- // Add to global calls (without contact-specific secret)
|
||||
+ if let Some(secret) =
|
||||
+ call.secrets.read().await.get(&(user_id as u64))
|
||||
+ {
|
||||
+ base_call_map.insert(
|
||||
+ DataType::CallSecret.to_id(&tm),
|
||||
+ secret.to_data_value(),
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
+ // Add to global calls with only this user's recipient-specific secret.
|
||||
global_calls.push(DataValue::container_from_map(
|
||||
&base_call_map,
|
||||
));
|
||||
@@ -218,19 +227,7 @@ impl GeneralConnection {
|
||||
continue;
|
||||
}
|
||||
|
||||
- let mut contact_call_map = base_call_map.clone();
|
||||
-
|
||||
- if let Some(secret) = call
|
||||
- .secrets
|
||||
- .read()
|
||||
- .await
|
||||
- .get(&(member_id, user_id as u64))
|
||||
- {
|
||||
- contact_call_map.insert(
|
||||
- DataType::CallSecret.to_id(&tm),
|
||||
- DataValue::Str(secret.clone()),
|
||||
- );
|
||||
- }
|
||||
+ let contact_call_map = base_call_map.clone();
|
||||
|
||||
invites
|
||||
.entry(member_id as i64)
|
||||
diff --git a/src/rho/iota_connection.rs b/src/rho/iota_connection.rs
|
||||
index 95956a4..b499875 100755
|
||||
--- a/src/rho/iota_connection.rs
|
||||
+++ b/src/rho/iota_connection.rs
|
||||
@@ -433,7 +433,11 @@ impl IotaConnection {
|
||||
base_call_map.insert(DataType::HasAdmin.to_id(&tm), DataValue::Bool(true));
|
||||
}
|
||||
|
||||
- // Add to global calls (without contact-specific secret)
|
||||
+ if let Some(secret) = call.secrets.read().await.get(&user_id) {
|
||||
+ base_call_map.insert(DataType::CallSecret.to_id(&tm), secret.to_data_value());
|
||||
+ }
|
||||
+
|
||||
+ // Add to global calls with only this user's recipient-specific secret.
|
||||
global_calls.push(DataValue::container_from_map(&base_call_map));
|
||||
|
||||
// Attach this call to EVERY member of the call (other than ourselves)
|
||||
@@ -443,15 +447,7 @@ impl IotaConnection {
|
||||
continue;
|
||||
}
|
||||
|
||||
- let mut contact_call_map = base_call_map.clone();
|
||||
-
|
||||
- // Add secret if it exists for this pairing
|
||||
- if let Some(secret) = call.secrets.read().await.get(&(member_id, user_id)) {
|
||||
- contact_call_map.insert(
|
||||
- DataType::CallSecret.to_id(&tm),
|
||||
- DataValue::Str(secret.clone()),
|
||||
- );
|
||||
- }
|
||||
+ let contact_call_map = base_call_map.clone();
|
||||
|
||||
invites
|
||||
.entry(member_id as i64)
|
||||
```
|
||||
|
|
@ -109,6 +109,7 @@ type CallStore = {
|
|||
pendingWatchedParticipantIds: number[];
|
||||
activeScreenShareParticipantIds: number[];
|
||||
isEncrypted: boolean;
|
||||
ownCallSecretInvitePending: boolean;
|
||||
callIsFullscreen: boolean;
|
||||
callIsPopout: boolean;
|
||||
layoutVersion: number;
|
||||
|
|
@ -897,6 +898,7 @@ export async function disconnect() {
|
|||
watchedStreamParticipantIds: [],
|
||||
pendingWatchedParticipantIds: [],
|
||||
activeScreenShareParticipantIds: [],
|
||||
ownCallSecretInvitePending: false,
|
||||
callIsFullscreen: false,
|
||||
lastFocusedParticipantId: null,
|
||||
});
|
||||
|
|
@ -930,9 +932,11 @@ export async function joinCall(
|
|||
}
|
||||
|
||||
log(2, "call", "purple", "Call creation initialised");
|
||||
const isNewCall = !callSecret && !existingCallId;
|
||||
useCall.setState({
|
||||
state: "encrypting",
|
||||
invitedUserId: sendInvite && !existingCallId ? userId : null,
|
||||
ownCallSecretInvitePending: isNewCall,
|
||||
});
|
||||
|
||||
if (callSecret) {
|
||||
|
|
@ -1119,6 +1123,7 @@ export const useCall = create<CallStore>(() => ({
|
|||
pendingWatchedParticipantIds: [],
|
||||
activeScreenShareParticipantIds: [],
|
||||
isEncrypted: false,
|
||||
ownCallSecretInvitePending: false,
|
||||
callIsFullscreen: false,
|
||||
callIsPopout: false,
|
||||
layoutVersion: 0,
|
||||
|
|
@ -1226,13 +1231,9 @@ export function useInitializeCall() {
|
|||
SenderId: number;
|
||||
};
|
||||
|
||||
showCallingScreen(
|
||||
CallId,
|
||||
normalizeWrappedCallSecret(CallSecret),
|
||||
SenderId,
|
||||
);
|
||||
showCallingScreen(CallId, CallSecret, SenderId);
|
||||
});
|
||||
}, [subscribePush, showCallingScreen]);
|
||||
}, [load, subscribePush, showCallingScreen]);
|
||||
|
||||
// get callId from url
|
||||
useEffect(() => {
|
||||
|
|
@ -1333,6 +1334,23 @@ export function useInitializeCall() {
|
|||
}
|
||||
|
||||
const invitedUserId = useCall.getState().invitedUserId;
|
||||
const ownCallSecretInvitePending =
|
||||
useCall.getState().ownCallSecretInvitePending;
|
||||
|
||||
if (ownCallSecretInvitePending) {
|
||||
useCall.setState({ ownCallSecretInvitePending: false });
|
||||
void load("user_id")
|
||||
.then((ownUserId) => sendCallInvite(Number(ownUserId)))
|
||||
.catch((error) => {
|
||||
log(
|
||||
1,
|
||||
"call",
|
||||
"red",
|
||||
"Failed to send own call secret invite",
|
||||
error,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (invitedUserId != null) {
|
||||
setTimeout(async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue