> ## Documentation Index
> Fetch the complete documentation index at: https://personal-9eca1d6c-claude-nifty-bohr-567oke.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Signal

> Low-level Signal protocol operations for encryption, decryption, and session management

The `Signal` struct provides direct access to Signal protocol operations including message encryption/decryption for both 1:1 and group conversations, session management, and participant node creation.

<Warning>
  These are low-level APIs that bypass the high-level message sending pipeline. Most users should use [`client.send_message()`](/api/client#send_message) which handles encryption automatically. Use these methods only when you need direct control over the Signal protocol layer.
</Warning>

## Access

Access Signal protocol operations through the client:

```rust theme={null}
let signal = client.signal();
```

## Methods

### encrypt\_message

Encrypt plaintext for a single recipient using the Signal protocol.

```rust theme={null}
pub async fn encrypt_message(
    &self,
    jid: &Jid,
    plaintext: &[u8],
) -> Result<(EncType, Vec<u8>), SignalError>
```

**Parameters:**

* `jid` - Recipient JID. PN JIDs are resolved to LID and `Hosted` JIDs to `HostedLid` when a mapping exists, matching WA Web's `SignalAddress.toString()` and the internal send path. See [Signal address resolution](/advanced/signal-protocol#signal-address-resolution).
* `plaintext` - Raw bytes to encrypt. The caller is responsible for padding if needed.

**Returns:**

* `(EncType, Vec<u8>)` - The encryption type and ciphertext bytes

**EncType variants:**

* `EncType::PreKeyMessage` - Session was just established (includes prekey bundle)
* `EncType::Message` - Standard encrypted message

**Example:**

```rust theme={null}
use wacore::message_processing::EncType;

let plaintext = b"Hello, world!";
let (enc_type, ciphertext) = client.signal().encrypt_message(&jid, plaintext).await?;

match enc_type {
    EncType::PreKeyMessage => println!("New session established"),
    EncType::Message => println!("Existing session used"),
    _ => {}
}
```

### decrypt\_message

Decrypt a Signal protocol message from a sender.

```rust theme={null}
pub async fn decrypt_message(
    &self,
    jid: &Jid,
    enc_type: EncType,
    ciphertext: &[u8],
) -> Result<Vec<u8>, SignalError>
```

**Parameters:**

* `jid` - Sender JID. PN JIDs are resolved to LID and `Hosted` JIDs to `HostedLid` when a mapping exists.
* `enc_type` - The encryption type (`EncType::PreKeyMessage` or `EncType::Message`)
* `ciphertext` - Encrypted bytes to decrypt

**Returns:**

* `Vec<u8>` - Raw padded plaintext. Use `MessageUtils::unpad_message_ref` with the stanza's `v` attribute if WhatsApp message unpadding is needed.

<Note>
  Passing `EncType::SenderKey` returns an error — use [`decrypt_group_message`](#decrypt_group_message) for sender-key encrypted group messages.
</Note>

**Example:**

```rust theme={null}
let plaintext = client.signal().decrypt_message(
    &sender_jid,
    EncType::Message,
    &ciphertext,
).await?;
```

### encrypt\_group\_message

Encrypt plaintext for a group using sender keys.

```rust theme={null}
pub async fn encrypt_group_message(
    &self,
    group_jid: &Jid,
    plaintext: &[u8],
) -> Result<(Option<Vec<u8>>, Vec<u8>), SignalError>
```

**Parameters:**

* `group_jid` - Group JID (`@g.us`)
* `plaintext` - Raw bytes to encrypt

**Returns:**

* `(Option<Vec<u8>>, Vec<u8>)` - A tuple of optional SKDM bytes and ciphertext bytes. The SKDM is `Some` only when a new sender key was created (first encrypt for this group or after key rotation). You must distribute the SKDM to all group participants when present.

<Warning>
  Not safe to call concurrently with `decrypt_group_message` for the same group — sender key state is not internally locked.
</Warning>

**Example:**

```rust theme={null}
let (skdm, ciphertext) = client.signal().encrypt_group_message(
    &group_jid,
    &plaintext,
).await?;

if let Some(skdm_bytes) = skdm {
    // Distribute SKDM to all group participants
    println!("New sender key created, SKDM must be distributed");
}
```

### decrypt\_group\_message

Decrypt a group (sender-key) message.

```rust theme={null}
pub async fn decrypt_group_message(
    &self,
    group_jid: &Jid,
    sender_jid: &Jid,
    ciphertext: &[u8],
) -> Result<Vec<u8>, SignalError>
```

**Parameters:**

* `group_jid` - Group JID
* `sender_jid` - Sender's JID within the group
* `ciphertext` - Encrypted bytes to decrypt

**Returns:**

* `Vec<u8>` - Raw padded plaintext. Use `MessageUtils::unpad_message_ref` with the stanza's `v` attribute if WhatsApp message unpadding is needed.

<Warning>
  Not safe to call concurrently with `encrypt_group_message` for the same group — sender key state is not internally locked.
</Warning>

**Example:**

```rust theme={null}
let plaintext = client.signal().decrypt_group_message(
    &group_jid,
    &sender_jid,
    &ciphertext,
).await?;
```

### validate\_session

Check whether a Signal session exists for a JID.

```rust theme={null}
pub async fn validate_session(&self, jid: &Jid) -> Result<bool, SignalError>
```

**Parameters:**

* `jid` - JID to check. PN JIDs are resolved to LID and `Hosted` JIDs to `HostedLid` when a mapping exists.

**Returns:**

* `bool` - `true` if a session exists, `false` otherwise

**Example:**

```rust theme={null}
if client.signal().validate_session(&jid).await? {
    println!("Session exists for {}", jid);
} else {
    println!("No session — need to establish one first");
}
```

### delete\_sessions

Delete Signal sessions and identity keys for the given JIDs.

```rust theme={null}
pub async fn delete_sessions(&self, jids: &[Jid]) -> Result<(), SignalError>
```

**Parameters:**

* `jids` - JIDs whose sessions and identity keys should be deleted. PN JIDs are resolved to LID and `Hosted` JIDs to `HostedLid` when a mapping exists.

This matches WhatsApp Web's `deleteRemoteSession` behavior, which removes both the session and identity key as a paired operation. Changes are flushed to the persistent backend before returning.

**Example:**

```rust theme={null}
// Delete sessions for specific contacts
client.signal().delete_sessions(&[jid1, jid2]).await?;
```

### create\_participant\_nodes

Create encrypted participant `<to>` nodes for the given recipient JIDs.

```rust theme={null}
pub async fn create_participant_nodes(
    &self,
    recipient_jids: &[Jid],
    message: &waproto::whatsapp::Message,
) -> Result<(Vec<Node>, bool), SignalError>
```

**Parameters:**

* `recipient_jids` - JIDs to encrypt for
* `message` - Protobuf message to encrypt

**Returns:**

* `(Vec<Node>, bool)` - The encrypted participant XML nodes and a boolean indicating whether a device identity node should be included in the stanza (true when any participant received a PreKey message).

This method resolves devices, ensures Signal sessions exist, encrypts the message for each device, and returns the resulting XML nodes. It acquires session locks matching the DM send path via `session_mutexes_for()` (bare recipient JID for the recipient, per-device for own companion devices).

**Example:**

```rust theme={null}
use waproto::whatsapp as wa;

let message = wa::Message {
    conversation: Some("Hello!".to_string()),
    ..Default::default()
};

let (nodes, include_identity) = client.signal().create_participant_nodes(
    &[recipient_jid],
    &message,
).await?;
```

### assert\_sessions

Ensure E2E sessions exist for the given JIDs.

```rust theme={null}
pub async fn assert_sessions(&self, jids: &[Jid]) -> Result<(), SignalError>
```

**Parameters:**

* `jids` - JIDs to ensure sessions for

If sessions do not exist, this method fetches prekey bundles from the server and establishes new sessions.

**Example:**

```rust theme={null}
// Ensure sessions exist before manual encryption
client.signal().assert_sessions(&[jid1, jid2]).await?;
```

### get\_user\_devices

Get all known device JIDs for the given user JIDs via usync.

```rust theme={null}
pub async fn get_user_devices(&self, jids: &[Jid]) -> Result<Vec<Jid>, SignalError>
```

**Parameters:**

* `jids` - User JIDs to query

**Returns:**

* `Vec<Jid>` - All device JIDs for the given users

**Example:**

```rust theme={null}
let devices = client.signal().get_user_devices(&[user_jid]).await?;
println!("User has {} devices", devices.len());
```

## EncType

The `EncType` enum represents the Signal protocol encryption type used for a message:

```rust theme={null}
pub enum EncType {
    /// Standard Signal message (existing session)
    Message,
    /// PreKey Signal message (new session establishment)
    PreKeyMessage,
    /// Sender key message (group encryption)
    SenderKey,
    /// Bot message secret (`<enc type="msmsg">`) — Meta AI / fbid bot
    /// replies. Decrypted with the outbound `messageSecret`, not a Signal
    /// session. See [Bot message decryption](#bot-message-decryption-msmsg).
    MessageSecret,
}
```

`EncType` exposes two predicate helpers: `is_session()` (true for `Message` / `PreKeyMessage`, **excludes** `MessageSecret`) and `is_bot_secret()` (true only for `MessageSecret`).

## Bot message decryption (msmsg)

When you message Meta AI or another `@bot` account, the bot's replies arrive as `<enc type="msmsg">` stanzas. These are **not** Signal-session encrypted — they use a dual-HKDF derivation over the 32-byte `messageSecret` from the prompt you sent, then AES-256-GCM.

The client handles this end to end and **transparently**:

1. **On send to a bot**, the outbound `MessageContextInfo.messageSecret` is persisted (keyed by `(chat, sender, msg_id)`) so the reply can be decrypted later.
2. **On receive**, an `msmsg` stanza is decrypted and decoded into a `wa::Message`, then dispatched as a normal [`Event::Messages`](/concepts/events#messages) — there is no separate bot event. The sender is the bot JID (e.g. `…@bot`) and `MsgMetaInfo.target_id` points back at your original prompt.
3. **On failure** (missing secret, GCM tag mismatch, malformed proto) the client nacks with reason `495` (`MissingMessageSecret`) instead of silently dropping, and group bot replies are acked with a bare `<ack class="message">` matching WA Web.

You don't need to call anything — receiving bot replies works as soon as you've sent a message to the bot from the same client. The low-level primitive is `wacore::bot_message::decrypt_bot_message(message_secret, enc_iv, enc_payload, ctx)`, and persistence is backed by the [`MsgSecretStore`](/api/store#msgsecretstore) trait.

## Usage examples

### Manual 1:1 encryption round-trip

```rust theme={null}
// Ensure a session exists
client.signal().assert_sessions(&[recipient_jid.clone()]).await?;

// Encrypt
let plaintext = b"Secret message";
let (enc_type, ciphertext) = client.signal().encrypt_message(
    &recipient_jid,
    plaintext,
).await?;

// The recipient would decrypt with:
// let decrypted = client.signal().decrypt_message(&sender_jid, enc_type, &ciphertext).await?;
```

### Check session before sending

```rust theme={null}
let has_session = client.signal().validate_session(&jid).await?;

if !has_session {
    // Establish session first
    client.signal().assert_sessions(&[jid.clone()]).await?;
}

let (enc_type, ciphertext) = client.signal().encrypt_message(&jid, plaintext).await?;
```

### Group encryption with SKDM handling

```rust theme={null}
let (skdm, ciphertext) = client.signal().encrypt_group_message(
    &group_jid,
    &plaintext,
).await?;

if skdm.is_some() {
    // First message in this group or after key rotation.
    // The SKDM must be distributed to all participants
    // so they can decrypt future messages.
}
```

### Reset a broken session

```rust theme={null}
// Delete the corrupted session
client.signal().delete_sessions(&[jid.clone()]).await?;

// Re-establish
client.signal().assert_sessions(&[jid.clone()]).await?;

// Now encryption should work again
let (enc_type, ciphertext) = client.signal().encrypt_message(&jid, plaintext).await?;
```

## Error types

### `SignalError`

All signal methods return `Result<T, SignalError>`:

```rust theme={null}
#[non_exhaustive]
pub enum SignalError {
    #[error(transparent)]
    Protocol(#[from] SignalProtocolError),
    #[error("unsupported signal operation: {0}")]
    Unsupported(String),
    #[error(transparent)]
    Internal(#[from] anyhow::Error),
}
```

**Variants:**

* `Protocol` — Signal protocol error (session mismatch, decode failure, etc.)
* `Unsupported` — Operation not supported for the given parameters
* `Internal` — Catch-all for other errors

## See also

* [Signal Protocol implementation](/advanced/signal-protocol) - Deep dive into the protocol internals
* [Client](/api/client) - Core client API
* [Send](/api/send) - High-level message sending (handles encryption automatically)
