How-to
Encrypt to multiple recipients
Goal
Send a message that must pass through several parties in sequence — each peels off one layer of encryption before passing the remainder onward.
Common use cases
- Approval workflows (HR approves, then Legal, then the message reaches the user)
- Sealed-bid auctions (commit chain)
- Audit trails (each participant records their step)
The three calls
Multi-party messaging on Bridge uses three methods, one per role:
MultiEncryptMessageUsingSharedKeyAsync— originator encrypts once for the whole chainDecryptMessageUsingSharedKeyPreAsync— each intermediate hop peels one layerMultiDecryptMessageUsingSharedKeyAsync— final recipient unwraps the innermost layer
The chain string is semicolon-separated and lists every hop including the final recipient.
Code — originator
// Encrypt for chain: HR → Legal → alice
string recipients = "hr.acme.com;legal.acme.com;[email protected]";
// multiAsk is fetched from the platform for this chain at the current kgt
string ct = await client.MultiEncryptMessageUsingSharedKeyAsync(
recipients: recipients,
message: "approve project X",
sharedKey: multiAsk); Code — intermediate peel (order does not matter)
Every intermediate fetches its own (self, originator) ASK and calls Pre exactly once. The chain is still sequential — each peel takes the previous peel's output as its input — but which intermediate goes first, second, third, etc. is the application's choice. HR-then-Legal and Legal-then-HR produce final ciphertexts that the final recipient's MultiDecrypt resolves to the same plaintext.
// Each intermediate fetches its own (self, originator) ASK
string hrAsk = await hrClient.FetchAskAsync(originator: "[email protected]", kgt);
string legalAsk = await legalClient.FetchAskAsync(originator: "[email protected]", kgt);
// Option A — HR peels first, Legal peels HR's output
string afterHr = await hrClient.DecryptMessageUsingSharedKeyPreAsync(ct, hrAsk);
string afterBoth1 = await legalClient.DecryptMessageUsingSharedKeyPreAsync(afterHr, legalAsk);
// Option B — Legal peels first, HR peels Legal's output
string afterLegal = await legalClient.DecryptMessageUsingSharedKeyPreAsync(ct, legalAsk);
string afterBoth2 = await hrClient.DecryptMessageUsingSharedKeyPreAsync(afterLegal, hrAsk);
// afterBoth1 and afterBoth2 both decrypt to the same plaintext via
// alice's MultiDecrypt — Pre is commutative across peelers.
// (Note: each Pre call still needs the previous peel's output as its input;
// you cannot peel the original ct in parallel and join the results.) Code — final recipient (alice)
// alice fetches a decrypt-mode ASK via FETCHKGT with the ~D~ prefix
string aliceAsk = await aliceClient.FetchDestinationAskAsync(
logintoken: aliceToken, kgt, destination: "[email protected]");
// afterBoth (either afterBoth1 or afterBoth2 from above) is the fully-peeled
// ciphertext after BOTH intermediates have done their Pre.
string plaintext = await aliceClient.MultiDecryptMessageUsingSharedKeyAsync(
afterBoth1, aliceAsk);
// plaintext envelope: {"SENDER":"[email protected]",
// "RECEIVER":"hr.acme.com;legal.acme.com;[email protected]",
// "BODY":"approve project X", ...} Scaling beyond two recipients
The chain is N-deep and Bridge does not impose a maximum. A 5-hop chain looks exactly like a 2-hop chain at every call site:
// 5 recipients (originator picks the set at encrypt time)
string recipients = "hr.acme.com;legal.acme.com;cfo.acme.com;ceo.acme.com;[email protected]";
string multiAsk = await client.FetchDestinationAskAsync(
logintoken: originatorToken, kgt, destination: recipients);
string ct = await client.MultiEncryptMessageUsingSharedKeyAsync(
recipients, "approve project X", multiAsk);
// Every intermediate (HR, Legal, CFO, CEO) peels exactly ONCE with its own
// (self, originator) ASK. Each peel feeds the next; the application chooses
// the order of intermediates, but the peels themselves must be serialized.
string p1 = await hr.DecryptMessageUsingSharedKeyPreAsync(ct, hrAsk);
string p2 = await legal.DecryptMessageUsingSharedKeyPreAsync(p1, legalAsk);
string p3 = await cfo.DecryptMessageUsingSharedKeyPreAsync(p2, cfoAsk);
string p4 = await ceo.DecryptMessageUsingSharedKeyPreAsync(p3, ceoAsk);
// alice is the final recipient — only she calls MultiDecrypt
string plaintext = await alice.MultiDecryptMessageUsingSharedKeyAsync(p4, aliceAsk); Commutative peeling — why this matters
Pre is commutative across peelers. Unlike Tor onion routing (where each peeler must be invoked in a strict outer-to-inner sequence on a predetermined path), the HexaEight intermediates can be applied in any order. The chain is still strictly sequential — each Pre call takes the previous peel's output as input — but which intermediate goes first, which goes second, etc. is the application's choice. Every permutation of the same N intermediates produces a final ciphertext that the recipient's MultiDecrypt resolves to the same plaintext.
Concrete advantages over strict-order onion routing:
- No predetermined routing path. The originator picks the set of approvers; the application picks the order at runtime. You can defer the decision until each approver becomes available.
- Failover-friendly. If the approver you intended to route to next is offline, route to any other intermediate in the set first and circle back later — the recipient still recovers the same plaintext.
- No need for peelers to know the chain. Each intermediate only needs its own
(self, originator)ASK and the current ciphertext. It does not need to know who else is in the set or where to send the result; the application orchestrates. - Order-independent auditability. Each peeler can log its peel timestamp; reviewers can reason about who saw what when, regardless of physical message sequence.
- Dynamic re-routing. If a hop fails after peeling but before forwarding, the peeled output can be picked up by any other application node and forwarded to the next intermediate — no need to redo the upstream peel.
Transport is the application's problem
Bridge provides only the three crypto primitives above. Carrying the ciphertext between hops — HTTP, message queue, file drop, fan-out broadcast, custom protocol — and orchestrating each intermediate's Pre call is the application's responsibility. The same is true for delivering the innermost ciphertext to the final recipient. As long as each intermediate peels exactly once with the correct (self, originator) ASK and the final recipient runs MultiDecryptMessageUsingSharedKeyAsync after all intermediates have peeled, the chain decrypts. Bridge does not care about the order or topology.
From a ClientSession
When you have an authenticated ClientSession, the same three calls are exposed as EncryptToMultiAsync, DecryptMultiAsync (intermediate) and DecryptMultiFinalAsync (final) — they wrap the methods above and run on a worker thread.
See also
- Set up an approval chain rule How-to
- Verify the multi-party chain How-to