# GroupMember Lifecycle

The `GroupMember` struct in `quicproquo-core` is the core MLS state machine
that manages a single client's membership in an MLS group. It wraps an openmls
`MlsGroup`, a persistent crypto backend, and the long-term Ed25519 identity
keypair. Every MLS operation -- key package generation, group creation, member
addition, joining, sending, and receiving -- flows through this struct.

**Source:** `crates/quicproquo-core/src/group.rs`

---

## Struct Fields

```rust
pub struct GroupMember {
    backend:  StoreCrypto,          // persistent crypto backend (key store + RustCrypto)
    identity: Arc<IdentityKeypair>, // long-term Ed25519 signing keypair
    group:    Option<MlsGroup>,     // active MLS group (None before create/join)
    config:   MlsGroupConfig,       // shared group configuration
}
```

| Field      | Type                    | Purpose |
|------------|-------------------------|---------|
| `backend`  | `StoreCrypto`           | Implements `OpenMlsCryptoProvider`. Couples a `RustCrypto` engine with a `DiskKeyStore` that holds HPKE init private keys. The backend is **persistent** -- the same instance must be used from `generate_key_package()` through `join_group()`. See [Storage Backend](storage-backend.md) for details on `DiskKeyStore`. |
| `identity` | `Arc<IdentityKeypair>`  | The client's long-term Ed25519 keypair. Used as the MLS `Signer` for all group operations (signing Commits, KeyPackages, credentials). Also used to build the MLS `BasicCredential`. See [Ed25519 Identity Keys](../cryptography/identity-keys.md). |
| `group`    | `Option<MlsGroup>`      | `None` until the client creates or joins a group. Once set, all message operations (`send_message`, `receive_message`) operate on this group. |
| `config`   | `MlsGroupConfig`        | Shared configuration for all groups created by this member. Built once in the constructor. |

### MlsGroupConfig

The configuration is constructed as:

```rust
MlsGroupConfig::builder()
    .use_ratchet_tree_extension(true)
    .build()
```

Setting `use_ratchet_tree_extension = true` embeds the ratchet tree inside
Welcome messages (in the `GroupInfo` extension). This means `new_from_welcome`
can be called with `ratchet_tree = None` -- openmls extracts the tree from the
Welcome itself. This simplifies the protocol by eliminating the need for a
separate ratchet tree distribution mechanism.

---

## State Transition Diagram

```text
GroupMember::new(identity) -----> [No Group]
      |                             group = None
      |
      +-- generate_key_package() --> [Has KeyPackage, waiting for Welcome]
      |     Returns TLS-encoded        HPKE init key stored in backend
      |     KeyPackage bytes
      |
      +-- create_group(group_id) --> [Group Creator, epoch 0]
      |     group = Some(MlsGroup)     Sole member of the group
      |     |
      |     +-- add_member(kp_bytes) --> [epoch N+1]
      |           Returns (commit_bytes, welcome_bytes)
      |           Pending commit merged locally
      |           Creator ready to encrypt immediately
      |
      +-- join_group(welcome_bytes) --> [Group Member, epoch N]
            group = Some(MlsGroup)       Joined via Welcome
            |
            +-- send_message(plaintext) --> encrypted PrivateMessage bytes
            |
            +-- receive_message(bytes) --> Some(plaintext)  [ApplicationMessage]
                                        | None             [Commit or Proposal]
```

### Transitions in Detail

1. **`new(identity)`** -- Creates a `GroupMember` with an ephemeral
   `DiskKeyStore` and no active group. The `StoreCrypto` backend is initialized
   fresh. An alternative constructor, `new_with_state`, accepts a pre-existing
   `DiskKeyStore` and optional serialized `MlsGroup` for session resumption.

2. **`generate_key_package()`** -- Generates a fresh single-use MLS KeyPackage.
   The HPKE init private key is stored in `self.backend`'s key store. Returns
   TLS-encoded KeyPackage bytes suitable for upload to the
   [Authentication Service](authentication-service.md).

3. **`create_group(group_id)`** -- Creates a new MLS group where the caller
   becomes the sole member at epoch 0. The `group_id` can be any non-empty byte
   string (SHA-256 of a human-readable name is recommended).

4. **`add_member(key_package_bytes)`** -- Adds a peer using their TLS-encoded
   KeyPackage. Produces a Commit and a Welcome. The Commit is merged locally
   (advancing the epoch), so the creator is immediately ready to encrypt. The
   caller is responsible for distributing the Welcome to the new member via the
   [Delivery Service](delivery-service.md).

5. **`join_group(welcome_bytes)`** -- Joins an existing group from a TLS-encoded
   Welcome message. The caller must have previously called
   `generate_key_package()` on **this same instance** so the HPKE init private
   key is available in the backend.

6. **`send_message(plaintext)`** -- Encrypts plaintext as an MLS Application
   message (PrivateMessage variant). Returns TLS-encoded bytes for delivery.

7. **`receive_message(bytes)`** -- Processes an incoming MLS message. Returns
   `Some(plaintext)` for application messages, `None` for Commits (which advance
   the group epoch) and Proposals (which are stored for a future Commit).

---

## Critical Invariant: Backend Identity

The same `GroupMember` instance must be used from `generate_key_package()`
through `join_group()`. This is the most important invariant in the system.

**Why:** When `generate_key_package()` runs, openmls creates an HPKE key pair
and stores the private key in the `StoreCrypto` backend's in-memory key store
(the `DiskKeyStore`). When `join_group()` later processes the Welcome, openmls
calls `new_from_welcome`, which reads the HPKE init private key from the key
store to decrypt the Welcome's encrypted group secrets. If a different backend
instance is used, the private key will not be found, and `new_from_welcome` will
fail with a key-not-found error.

```text
generate_key_package()                   join_group(welcome)
        |                                       |
        v                                       v
   KeyPackage::builder().build()          MlsGroup::new_from_welcome()
        |                                       |
        v                                       v
   backend.key_store().store(             backend.key_store().read(
       init_key_ref, hpke_private_key)        init_key_ref) -> hpke_private_key
        |                                       |
        +----------- MUST BE SAME BACKEND ------+
```

For persistent clients, the `DiskKeyStore::persistent(path)` constructor is used
so that the HPKE init keys survive process restarts. The client state file
stores the path alongside the identity seed and serialized group, and
`new_with_state` reconstructs the `GroupMember` with the persisted key store.

---

## Credential Construction

The `make_credential_with_key` helper builds the MLS `CredentialWithKey` used
for KeyPackage generation and group creation:

```rust
fn make_credential_with_key(&self) -> Result<CredentialWithKey, CoreError> {
    let credential = Credential::new(
        self.identity.public_key_bytes().to_vec(),
        CredentialType::Basic,
    )?;

    Ok(CredentialWithKey {
        credential,
        signature_key: self.identity.public_key_bytes().to_vec().into(),
    })
}
```

Key points:

- **Credential type:** `CredentialType::Basic` -- the simplest MLS credential
  form, containing only the raw public key bytes.
- **Credential identity:** The raw 32-byte Ed25519 public key. This is what
  peers use to identify the member within the group.
- **Signature key:** The same Ed25519 public key bytes, wrapped in the openmls
  `SignaturePublicKey` type.
- **Signer:** The `IdentityKeypair` struct implements the openmls `Signer`
  trait directly, so it can be passed to `KeyPackage::builder().build()` and
  `MlsGroup::new_with_group_id()` without the external
  `openmls_basic_credential` crate.

---

## MLS Ciphersuite

All operations use a single ciphersuite:

```text
MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519
```

This provides:

| Component     | Algorithm          | Security Level |
|---------------|--------------------|----------------|
| HPKE KEM      | DHKEM(X25519)      | 128-bit classical |
| AEAD          | AES-128-GCM        | 128-bit |
| KDF / Hash    | SHA-256            | 128-bit collision resistance |
| Signature     | Ed25519            | 128-bit classical |

See [Cryptography Overview](../cryptography/overview.md) for the full algorithm
inventory across all protocol layers.

---

## KeyPackage Deserialization (openmls 0.5)

openmls 0.5 separates serializable and deserializable types. `KeyPackage`
derives `TlsSerialize` but not `TlsDeserialize`. To deserialize an incoming
KeyPackage:

```rust
let key_package: KeyPackage =
    KeyPackageIn::tls_deserialize(&mut bytes.as_ref())?
        .validate(backend.crypto(), ProtocolVersion::Mls10)?;
```

The `KeyPackageIn` type derives `TlsDeserialize` and provides `validate()`,
which verifies the KeyPackage's signature and returns a trusted `KeyPackage`.

Similarly, `MlsMessageIn` is used to deserialize incoming MLS messages, and its
`extract()` method returns the inner message body (`MlsMessageInBody`). The
`into_welcome()` and `into_protocol_message()` methods that existed in earlier
openmls versions are feature-gated in 0.5; `extract()` with pattern matching is
the public API:

```rust
let msg_in = MlsMessageIn::tls_deserialize(&mut bytes.as_ref())?;
match msg_in.extract() {
    MlsMessageInBody::Welcome(w) => { /* join_group path */ }
    MlsMessageInBody::PrivateMessage(m) => ProtocolMessage::PrivateMessage(m),
    MlsMessageInBody::PublicMessage(m) => ProtocolMessage::PublicMessage(m),
    _ => { /* error: unexpected message type */ }
}
```

---

## Message Processing

`receive_message` handles four variants of `ProcessedMessageContent`:

| Variant                          | Action                                     | Return Value |
|----------------------------------|--------------------------------------------|--------------|
| `ApplicationMessage`             | Extract plaintext bytes                    | `Some(plaintext)` |
| `StagedCommitMessage`            | `merge_staged_commit()` -- epoch advances  | `None` |
| `ProposalMessage`                | `store_pending_proposal()` -- cached       | `None` |
| `ExternalJoinProposalMessage`    | `store_pending_proposal()` -- cached       | `None` |

For Commit messages, `merge_staged_commit` advances the group's epoch and
updates the ratchet tree. Proposals are stored for inclusion in a future Commit;
this allows the group to accumulate multiple proposals before committing them as
a batch.

---

## Error Handling

All `GroupMember` methods return `Result<_, CoreError>`. The MLS-specific error
variant is:

```rust
#[error("MLS error: {0}")]
Mls(String)
```

The inner string is the debug representation of the openmls error. This is a
deliberate design choice: openmls error types are complex enums with many
variants, and wrapping the debug output provides sufficient diagnostic
information without coupling `CoreError` to openmls's internal error hierarchy.

Common error scenarios:

| Operation              | Failure Mode                                    |
|------------------------|-------------------------------------------------|
| `generate_key_package` | Backend RNG failure (extremely unlikely)         |
| `create_group`         | Group already exists in state                    |
| `add_member`           | Malformed KeyPackage, no active group            |
| `join_group`           | Welcome does not match any stored init key       |
| `send_message`         | No active group                                  |
| `receive_message`      | Malformed message, decryption failure, wrong epoch |

---

## Accessors

| Method            | Returns                               | Purpose |
|-------------------|---------------------------------------|---------|
| `group_id()`      | `Option<Vec<u8>>`                     | MLS group ID bytes, or `None` if no group is active |
| `identity()`      | `&IdentityKeypair`                    | Reference to the long-term Ed25519 keypair |
| `identity_seed()` | `[u8; 32]`                            | Private seed bytes for state persistence |
| `backend()`       | `&StoreCrypto`                        | Reference to the crypto provider |
| `group_ref()`     | `Option<&MlsGroup>`                   | Reference to the MLS group for serialization |

---

## Unit Tests

The `two_party_mls_round_trip` test exercises the complete lifecycle:

1. Alice and Bob each create a `GroupMember` with fresh identities.
2. Bob generates a KeyPackage (stored in his backend).
3. Alice creates a group and adds Bob using his KeyPackage.
4. Bob joins via the Welcome message.
5. Alice sends "hello bob" -- Bob decrypts and verifies.
6. Bob sends "hello alice" -- Alice decrypts and verifies.

This test runs entirely in-memory (no server) and validates that the HPKE init
key invariant is maintained when the same `GroupMember` instance is used
throughout.

---

## Related Pages

- [KeyPackage Exchange Flow](keypackage-exchange.md) -- upload and fetch of KeyPackages via the server
- [Delivery Service Internals](delivery-service.md) -- how Commits and Welcomes are relayed
- [Authentication Service Internals](authentication-service.md) -- server-side KeyPackage storage
- [Storage Backend](storage-backend.md) -- `DiskKeyStore` and `FileBackedStore` persistence
- [Cryptography Overview](../cryptography/overview.md) -- algorithm inventory
- [Ed25519 Identity Keys](../cryptography/identity-keys.md) -- the `IdentityKeypair` struct
