Import 'mls-rs' crate

Request Document: go/android-rust-importing-crates
For CL Reviewers: go/android3p#cl-review
For Build Team: go/ab-third-party-imports
Bug: http://b/330708876
Test: m libmls_rs

Change-Id: Ib0a891a4d7bf582ebea9ba7a1447ea959e42e0d3
diff --git a/src/group/external_commit.rs b/src/group/external_commit.rs
new file mode 100644
index 0000000..34b1042
--- /dev/null
+++ b/src/group/external_commit.rs
@@ -0,0 +1,266 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use mls_rs_core::{crypto::SignatureSecretKey, identity::SigningIdentity};
+
+use crate::{
+    client_config::ClientConfig,
+    group::{
+        cipher_suite_provider,
+        epoch::SenderDataSecret,
+        key_schedule::{InitSecret, KeySchedule},
+        proposal::{ExternalInit, Proposal, RemoveProposal},
+        EpochSecrets, ExternalPubExt, LeafIndex, LeafNode, MlsError, TreeKemPrivate,
+    },
+    Group, MlsMessage,
+};
+
+#[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
+use crate::group::secret_tree::SecretTree;
+
+#[cfg(feature = "custom_proposal")]
+use crate::group::{
+    framing::MlsMessagePayload,
+    message_processor::{EventOrContent, MessageProcessor},
+    message_signature::AuthenticatedContent,
+    message_verifier::verify_plaintext_authentication,
+    CustomProposal,
+};
+
+use alloc::vec;
+use alloc::vec::Vec;
+
+#[cfg(feature = "psk")]
+use mls_rs_core::psk::{ExternalPskId, PreSharedKey};
+
+#[cfg(feature = "psk")]
+use crate::group::{
+    PreSharedKeyProposal, {JustPreSharedKeyID, PreSharedKeyID},
+};
+
+use super::{validate_group_info_joiner, ExportedTree};
+
+/// A builder that aids with the construction of an external commit.
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type(opaque))]
+pub struct ExternalCommitBuilder<C: ClientConfig> {
+    signer: SignatureSecretKey,
+    signing_identity: SigningIdentity,
+    config: C,
+    tree_data: Option<ExportedTree<'static>>,
+    to_remove: Option<u32>,
+    #[cfg(feature = "psk")]
+    external_psks: Vec<ExternalPskId>,
+    authenticated_data: Vec<u8>,
+    #[cfg(feature = "custom_proposal")]
+    custom_proposals: Vec<Proposal>,
+    #[cfg(feature = "custom_proposal")]
+    received_custom_proposals: Vec<MlsMessage>,
+}
+
+impl<C: ClientConfig> ExternalCommitBuilder<C> {
+    pub(crate) fn new(
+        signer: SignatureSecretKey,
+        signing_identity: SigningIdentity,
+        config: C,
+    ) -> Self {
+        Self {
+            tree_data: None,
+            to_remove: None,
+            authenticated_data: Vec::new(),
+            signer,
+            signing_identity,
+            config,
+            #[cfg(feature = "psk")]
+            external_psks: Vec::new(),
+            #[cfg(feature = "custom_proposal")]
+            custom_proposals: Vec::new(),
+            #[cfg(feature = "custom_proposal")]
+            received_custom_proposals: Vec::new(),
+        }
+    }
+
+    #[must_use]
+    /// Use external tree data if the GroupInfo message does not contain a
+    /// [`RatchetTreeExt`](crate::extension::built_in::RatchetTreeExt)
+    pub fn with_tree_data(self, tree_data: ExportedTree<'static>) -> Self {
+        Self {
+            tree_data: Some(tree_data),
+            ..self
+        }
+    }
+
+    #[must_use]
+    /// Propose the removal of an old version of the client as part of the external commit.
+    /// Only one such proposal is allowed.
+    pub fn with_removal(self, to_remove: u32) -> Self {
+        Self {
+            to_remove: Some(to_remove),
+            ..self
+        }
+    }
+
+    #[must_use]
+    /// Add plaintext authenticated data to the resulting commit message.
+    pub fn with_authenticated_data(self, data: Vec<u8>) -> Self {
+        Self {
+            authenticated_data: data,
+            ..self
+        }
+    }
+
+    #[cfg(feature = "psk")]
+    #[must_use]
+    /// Add an external psk to the group as part of the external commit.
+    pub fn with_external_psk(mut self, psk: ExternalPskId) -> Self {
+        self.external_psks.push(psk);
+        self
+    }
+
+    #[cfg(feature = "custom_proposal")]
+    #[must_use]
+    /// Insert a [`CustomProposal`] into the current commit that is being built.
+    pub fn with_custom_proposal(mut self, proposal: CustomProposal) -> Self {
+        self.custom_proposals.push(Proposal::Custom(proposal));
+        self
+    }
+
+    #[cfg(all(feature = "custom_proposal", feature = "by_ref_proposal"))]
+    #[must_use]
+    /// Insert a [`CustomProposal`] received from a current group member into the current
+    /// commit that is being built.
+    ///
+    /// # Warning
+    ///
+    /// The authenticity of the proposal is NOT fully verified. It is only verified the
+    /// same way as by [`ExternalGroup`](`crate::external_client::ExternalGroup`).
+    /// The proposal MUST be an MlsPlaintext, else the [`Self::build`] function will fail.
+    pub fn with_received_custom_proposal(mut self, proposal: MlsMessage) -> Self {
+        self.received_custom_proposals.push(proposal);
+        self
+    }
+
+    /// Build the external commit using a GroupInfo message provided by an existing group member.
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    pub async fn build(self, group_info: MlsMessage) -> Result<(Group<C>, MlsMessage), MlsError> {
+        let protocol_version = group_info.version;
+
+        if !self.config.version_supported(protocol_version) {
+            return Err(MlsError::UnsupportedProtocolVersion(protocol_version));
+        }
+
+        let group_info = group_info
+            .into_group_info()
+            .ok_or(MlsError::UnexpectedMessageType)?;
+
+        let cipher_suite = cipher_suite_provider(
+            self.config.crypto_provider(),
+            group_info.group_context.cipher_suite,
+        )?;
+
+        let external_pub_ext = group_info
+            .extensions
+            .get_as::<ExternalPubExt>()?
+            .ok_or(MlsError::MissingExternalPubExtension)?;
+
+        let public_tree = validate_group_info_joiner(
+            protocol_version,
+            &group_info,
+            self.tree_data,
+            &self.config.identity_provider(),
+            &cipher_suite,
+        )
+        .await?;
+
+        let (leaf_node, _) = LeafNode::generate(
+            &cipher_suite,
+            self.config.leaf_properties(),
+            self.signing_identity,
+            &self.signer,
+            self.config.lifetime(),
+        )
+        .await?;
+
+        let (init_secret, kem_output) =
+            InitSecret::encode_for_external(&cipher_suite, &external_pub_ext.external_pub).await?;
+
+        let epoch_secrets = EpochSecrets {
+            #[cfg(feature = "psk")]
+            resumption_secret: PreSharedKey::new(vec![]),
+            sender_data_secret: SenderDataSecret::from(vec![]),
+            #[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
+            secret_tree: SecretTree::empty(),
+        };
+
+        let (mut group, _) = Group::join_with(
+            self.config,
+            group_info,
+            public_tree,
+            KeySchedule::new(init_secret),
+            epoch_secrets,
+            TreeKemPrivate::new_for_external(),
+            None,
+            self.signer,
+        )
+        .await?;
+
+        #[cfg(feature = "psk")]
+        let psk_ids = self
+            .external_psks
+            .into_iter()
+            .map(|psk_id| PreSharedKeyID::new(JustPreSharedKeyID::External(psk_id), &cipher_suite))
+            .collect::<Result<Vec<_>, MlsError>>()?;
+
+        let mut proposals = vec![Proposal::ExternalInit(ExternalInit { kem_output })];
+
+        #[cfg(feature = "psk")]
+        proposals.extend(
+            psk_ids
+                .into_iter()
+                .map(|psk| Proposal::Psk(PreSharedKeyProposal { psk })),
+        );
+
+        #[cfg(feature = "custom_proposal")]
+        {
+            let mut custom_proposals = self.custom_proposals;
+            proposals.append(&mut custom_proposals);
+        }
+
+        #[cfg(all(feature = "custom_proposal", feature = "by_ref_proposal"))]
+        for message in self.received_custom_proposals {
+            let MlsMessagePayload::Plain(plaintext) = message.payload else {
+                return Err(MlsError::UnexpectedMessageType);
+            };
+
+            let auth_content = AuthenticatedContent::from(plaintext.clone());
+
+            verify_plaintext_authentication(&cipher_suite, plaintext, None, None, &group.state)
+                .await?;
+
+            group
+                .process_event_or_content(EventOrContent::Content(auth_content), true, None)
+                .await?;
+        }
+
+        if let Some(r) = self.to_remove {
+            proposals.push(Proposal::Remove(RemoveProposal {
+                to_remove: LeafIndex(r),
+            }));
+        }
+
+        let commit_output = group
+            .commit_internal(
+                proposals,
+                Some(&leaf_node),
+                self.authenticated_data,
+                Default::default(),
+                None,
+                None,
+            )
+            .await?;
+
+        group.apply_pending_commit().await?;
+
+        Ok((group, commit_output.commit_message))
+    }
+}