How-to
Build a multi-hop relay
Goal
Build a chain where a message passes through several intermediate agents (A, B, C, D) before reaching the final recipient (alice). Bridge handles the crypto for arbitrarily deep chains; the application owns the transport and the orchestration. The chain is sequential — each peel feeds the next — but which intermediate peels first, second, third, etc. is the application's choice.
Not Tor — peel order is the application's choice
Unlike onion routing where each layer must be stripped in a strict outer-to-inner sequence on a predetermined path, HexaEight's intermediate peel (DecryptMessageUsingSharedKeyPreAsync) is commutative across peelers. The N intermediates can be applied in any permutation; each Pre still takes the previous peel's output as its input (you cannot peel the original ciphertext in parallel and join the outputs), but whichever order you choose, the final recipient's MultiDecrypt recovers the same plaintext.
- No predetermined path. The originator picks the set of intermediates at encrypt time; the application picks the order at runtime — and can defer the choice until each intermediate becomes available.
- Failover-friendly. If the intermediate you intended to route to next is offline, route to any other unpeeled intermediate first and come back later.
- Peelers don't need to know who else is in the chain or where the result goes next — they just peel their layer and hand the result back to the application.
- Order-independent auditability. Each peeler logs its own peel timestamp; reviewers can reason about who saw what when, regardless of physical message sequence.
Functions involved
Client.MultiEncryptMessageUsingSharedKeyAsync(recipients, message, sharedKey)— originator only, encrypts once for the whole chainClient.FetchAskAsync(recipient, kgt)— each intermediate fetches its (self, originator) ASKClient.DecryptMessageUsingSharedKeyPreAsync(ciphertext, sharedKey)— each intermediate peels one layerClient.FetchDestinationAskAsync(logintoken, kgt, "~D~" + originator)— final recipient fetches its decrypt-mode ASKClient.MultiDecryptMessageUsingSharedKeyAsync(ciphertext, sharedKey)— final recipient unwraps the innermost layer
Originator (one call)
// Chain: A → B → C → D → alice
string chain = "A.example.com;B.example.com;C.example.com;D.example.com;[email protected]";
// Fetch the multi-shared key for the whole chain at this kgt
string multiAsk = await originator.FetchDestinationAskAsync(
logintoken: originatorToken, kgt, destination: chain);
string ct0 = await originator.MultiEncryptMessageUsingSharedKeyAsync(
chain, "payload", multiAsk);
// Send ct0 to A via your transport, alongside (kgt, original_sender = self). Each intermediate hop (identical code, different keys, order-agnostic)
// On any intermediate (A, B, C, or D) — they can run this in any order.
string askSelf = await selfClient.FetchAskAsync(originalSender, kgt);
string ctOut = await selfClient.DecryptMessageUsingSharedKeyPreAsync(ctIn, askSelf);
// (optional) Casbin policy check before forwarding
var decision = await selfClient.Authorization.AuthorizeAsync(
sender: originalSender, destination: finalRecipient,
direction: "outbound", sessionId: selfClient.Name);
if (!decision.Allowed) return;
// Hand off (ctOut, kgt, original_sender, sender_agent = self) to wherever
// makes sense for your workflow — the next sequential hop, a join
// aggregator, or directly to alice once all intermediates have peeled. Topology choices the application owns
Because peel order is freely chosen, the same N-intermediate set works under several sequential routings:
- Fixed pipeline: originator → A → B → C → D → alice. Static path, easiest to operate.
- Dynamic next-hop: after each peel, the application picks any unpeeled intermediate as the next hop based on availability or policy. Useful when peelers come online unpredictably.
- Quorum collection: hand the current ciphertext to whichever intermediate responds first; loop until all N have peeled in some order; then forward to alice.
What is not possible: peeling the original ciphertext at multiple intermediates in parallel and joining their outputs. Each Pre consumes the output of the previous Pre, so the application must serialize the peel calls — it just gets to pick the order.
Final recipient (one call)
// On alice
string aliceAsk = await alice.FetchDestinationAskAsync(
logintoken: aliceToken, kgt,
destination: "~D~" + originalSender);
string plaintextEnvelope = await alice.MultiDecryptMessageUsingSharedKeyAsync(
ctIn, aliceAsk);
// plaintextEnvelope.RECEIVER carries the full chain that was bound at encrypt time:
// "A.example.com;B.example.com;C.example.com;D.example.com;[email protected]"
// — a missing or substituted hop fails the peel cryptographically. Transport and orchestration are the application's job
Bridge does not orchestrate the hops. The application chooses how to move (ciphertext, kgt, original_sender, sender_agent) from one peeler to the next — HTTP, MQ, file drop, whatever. The crypto chain is correct as long as each intermediate calls DecryptMessageUsingSharedKeyPreAsync with the right (self, originator) ASK exactly once (in any order), and the final recipient calls MultiDecryptMessageUsingSharedKeyAsync with its own (self, originator) ASK after every intermediate has peeled.
Content signatures and chain-of-custody
If the original sender signs the body with the HexaEight signing library before calling MultiEncryptMessageUsingSharedKeyAsync, the signed-body bytes are part of the encrypted payload and are recovered intact only at the final recipient's MultiDecrypt. Intermediates never see the signed body — they only strip their layer of the wrapper. The final recipient can verify the signature for cryptographic proof of origin without trusting any intermediate.