Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: establish 1on1 mls conversation with new removal key [WPB-10744] #6502

Merged
merged 22 commits into from
Sep 16, 2024

Conversation

V-Gira
Copy link
Contributor

@V-Gira V-Gira commented Sep 3, 2024

Description

See https://wearezeta.atlassian.net/browse/WPB-10737 for a more in depth description of the issue.

In a nutshell, it's possible for a federated user to create a 1:1 conversation even if it's not hosted on their own backend, in which case it can introduce a host of issues because they're lacking the correct removal key.

to address that issue, the response from the endpoint GET /conversations/one2one/{domain}/{userId} was changed to include that removal key, from

{ <conversation metadata> }

to

{
  "conversation": { <conversation metadata> },
  "public_keys": {
    "removal": {
      "<sig-algo>": "string",
      ...
    }
  }
}

Checklist

  • mentions the JIRA issue in the PR name (Ex. WPB-423)
  • PR has been self reviewed by the author;
  • Hard-to-understand areas of the code have been commented;
  • If it is a core feature, unit tests have been added;

@V-Gira V-Gira changed the title feat: establish 1on1 mls conversation with new removal key [10744] feat!: establish 1on1 mls conversation with new removal key [10744] Sep 11, 2024
@V-Gira V-Gira changed the title feat!: establish 1on1 mls conversation with new removal key [10744] feat!: establish 1on1 mls conversation with new removal key [WPB-10744] Sep 11, 2024
@echoes-hq echoes-hq bot added the echoes: unplanned Any work item that isn’t part of the product or technical roadmap. label Sep 11, 2024
Comment on lines -154 to -193
/**
* Short term solution to an issues with 1:1 MLS call, see
* https://wearezeta.atlassian.net/wiki/spaces/PAD/pages/1314750477/2024-07-29+1+1+calls+over+SFT#Do-not-leave-the-subconversation
* Will delete a conference subconversation when hanging up on a 1:1 call instead of leaving.
*
* @param conversationId Id of the parent conversation which subconversation we want to leave
*/
public async leave1on1ConferenceSubconversation(conversationId: QualifiedId): Promise<void> {
const subconversationGroupId = await this.getSubconversationGroupId(conversationId, SUBCONVERSATION_ID.CONFERENCE);

if (!subconversationGroupId) {
return;
}

const doesGroupExistLocally = await this.mlsService.conversationExists(subconversationGroupId);
if (!doesGroupExistLocally) {
// If the subconversation was known by a client but is does not exist locally, we can remove it from the store.
return this.clearSubconversationGroupId(conversationId, SUBCONVERSATION_ID.CONFERENCE);
}

try {
const epochInfo = await this.getSubconversationEpochInfo(conversationId, subconversationGroupId);

if (!epochInfo) {
throw new Error('Failed to get epoch info for conference subconversation');
}
await this.apiClient.api.conversation.deleteSubconversation(conversationId, SUBCONVERSATION_ID.CONFERENCE, {
groupId: subconversationGroupId,
epoch: epochInfo.epoch,
});
} catch (error) {
this.logger.error(`Failed to delete conference subconversation:`, error);
}

await this.mlsService.wipeConversation(subconversationGroupId);

// once we've deleted the subconversation, we can remove it from the store
await this.clearSubconversationGroupId(conversationId, SUBCONVERSATION_ID.CONFERENCE);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code block was a previous attempt to solve a related issue without requiring a change to the backend response, by calling the deleteSubconversation endpoint instead of deleteSubconversationSelf.

see #6434

This is no longer required

@V-Gira V-Gira marked this pull request as ready for review September 11, 2024 14:12
}

// If group is not established on backend,
// we wipe the it locally (in case it exsits in the local store) and try to register it.
await this.mlsService.wipeConversation(groupId);

try {
await this.mlsService.register1to1Conversation(groupId, otherUserId, selfUser);
await this.mlsService.register1to1Conversation(groupId, otherUserId, selfUser, public_keys?.removal);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main change, we can register a conversation with a removal key that comes from a different endpoint

@@ -481,7 +485,8 @@ export class MLSService extends TypedEventEmitter<Events> {
} else {
const mlsKeys = (await this.apiClient.api.client.getPublicKeys()).removal;
const ciphersuiteSignature = getSignatureAlgorithmForCiphersuite(this.config.defaultCiphersuite);
const removalKeyForSignature = mlsKeys[ciphersuiteSignature];
const removalKeyForSignature =
removalKeyFor1to1Signature?.[ciphersuiteSignature] ?? mlsKeys[ciphersuiteSignature];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the removal key is provided by the conversation endpoint, we use that one, else, we use a removal key provided by getPublicKey (our own backend removal key)

@V-Gira V-Gira changed the title feat!: establish 1on1 mls conversation with new removal key [WPB-10744] feat: establish 1on1 mls conversation with new removal key [WPB-10744] Sep 13, 2024
Comment on lines 542 to 545
if (isMLS1to1Conversation(response)) {
return response;
}
return {conversation: response};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we return response.conversation and response? 🤔
Code will be more readable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And nice to have passed type for fn what we returning there ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we return response.conversation and response? 🤔 Code will be more readable.

This function returns

export interface MLS1to1Conversation {
  conversation: MLSConversation;
  public_keys?: {
    removal: MLSPublicKeyRecord;
  };
}

the optional public_keys is needed in some cases so we can't only return MLSConversation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@przemvs see if you like @e-maad 's suggestion I applied

Copy link
Contributor

@e-maad e-maad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added few suggestions.

Comment on lines 120 to 130
if (typeof response === 'object' && response !== null) {
const conversation = response as MLS1to1Conversation;

if (!!conversation.conversation && !!conversation.public_keys) {
return true;
}

return false;
}

return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to normalize the logic?
Can we return early with false value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this?

export function isMLS1to1Conversation(response: unknown): response is MLS1to1Conversation {
  if (typeof response === 'object' && response !== null && 'conversation' in response && 'public_keys' in response) {
    return true;
  }

  return false;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but I think in is an alternative for hasOwnProperty().
If you are sure that these property cannot contain null/undefined, then we are good to go.

// for example
const response = {conversation: null, public_keys: undefined};
// 'conversation' in response will be true
// 'public_keys' in response will be true

@@ -580,19 +587,19 @@ export class ConversationService extends TypedEventEmitter<Events> {
);

await this.joinByExternalCommit(mlsConversation.qualified_id);
return this.getMLS1to1Conversation(otherUserId);
return (await this.getMLS1to1Conversation(otherUserId)).conversation;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we go with smth like this? Code will look more readable :)

Suggested change
return (await this.getMLS1to1Conversation(otherUserId)).conversation;
const updatedMLSConversation = await this.getMLS1to1Conversation(otherUserId);
return updatedMLSConversation.conversation;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with this, WDYT?

      const {conversation: updatedMLSConversation} = await this.getMLS1to1Conversation(otherUserId);
      return updatedMLSConversation;

Copy link

Copy link
Contributor

@przemvs przemvs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@V-Gira V-Gira merged commit 5ff3cf7 into main Sep 16, 2024
9 checks passed
@V-Gira V-Gira deleted the v/establish-mls-1on1 branch September 16, 2024 13:55
V-Gira added a commit that referenced this pull request Sep 16, 2024
#6502)

* feat: establish 1on1 mls conversation with new removal key [10744]

* remove federated SFT 1:1 call temporary fix

* type data structure for 1:1 MLS conversations

* remove public_keys from NewConversation type

* use nullish coalescing instead of ternary for removalKey logic

* implement typeguard for the specific MLS1to1Conversation response

* use union types for api-client getMLS1to1Conversation return value

* use type guard and return MLS1to1Conversation for getMLS1to1Conversation in core

* address tests

* Update packages/api-client/src/conversation/Conversation.ts

Co-authored-by: Przemysław Jóźwik <[email protected]>

* rename variable in type guard

* Update packages/core/src/conversation/ConversationService/ConversationService.ts

Co-authored-by: Immad Abdul Jabbar <[email protected]>

* address CR issue

* simplify typeguard's logic

* address CR

---------

Co-authored-by: Przemysław Jóźwik <[email protected]>
Co-authored-by: Immad Abdul Jabbar <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
echoes: unplanned Any work item that isn’t part of the product or technical roadmap. type: feature / request ✨
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants