DEV Community

Sui Gn
Sui Gn

Posted on

Cryptographic set-chemistry for union and intersection audiences over encrypted data — what is that?

Union and Intersection Audiences Over Encrypted Data — What Is That?

Encrypting a value for exactly one recipient is a solved problem — public-key encryption has done that for decades. Encrypting a value so that any one of these five people can read it, or so that all three of these people, together, and only together, can read it — that's a different, less commonly implemented problem. Call it audience set algebra: treating "who can read this" as a real set expression, not a single key.

The two operations that matter
Union audience — A = K₁ ∪ K₂ ∪ ... ∪ Kₙ. Any single member can decrypt independently. This is how you'd share a family photo album: you, your spouse, maybe a sibling — any one of you should be able to open it without needing the others online.

Algebra Of Encrypted Audiences

Intersection audience — A = K₁ ∩ K₂ ∩ ... ∩ Kₙ. All members must cooperate to decrypt. This is the harder, more interesting case: a value that requires quorum, not just membership.

Both are constructible from ordinary primitives, and both have real, different failure modes worth knowing before you reach for either.

Union, concretely
S ← random(32)                                  // one symmetric key
ciphertext = encryptSymmetric(S, value)
∀ Kᵢ ∈ A:  Wᵢ = wrapSecretV1(P256_pub(Kᵢ), S)    // wrap S for each member
publish { ciphertext, wraps: [(identityHash(Kᵢ), Wᵢ)] }
Enter fullscreen mode Exit fullscreen mode

Decryption: any member finds their wrap, unwraps S with their own private key, decrypts. O(|A|) wrapped keys, one ciphertext. The real cost shows up on removal — dropping a member from the union means re-encrypting S for everyone who remains, or the removed member can still decrypt anything encrypted before rotation. Backward secrecy isn't automatic; you have to actively rotate.

Intersection, concretely

S₁, ..., Sₙ₋₁ ← random(32) each
Sₙ = S ⊕ S₁ ⊕ ... ⊕ Sₙ₋₁                          // XOR completion
∀ Kᵢ ∈ A:  Wᵢ = wrapSecretV1(P256_pub(Kᵢ), Sᵢ)    // wrap one share each
publish { ciphertext, shares: [(identityHash(Kᵢ), Wᵢ)] }
Enter fullscreen mode Exit fullscreen mode

Every member unwraps their own share; only XOR-ing all shares together reconstructs S. Miss one member, and the ciphertext is permanently opaque — there's no partial decryption. This needs real out-of-band coordination to reassemble (or Shamir's Secret Sharing instead of XOR splitting, if you want a threshold-k version rather than requiring literally everyone).

The part that's easy to get wrong
None of this works if you can't get the recipient's public key in the first place. wrapSecretV1 needs P256_pub(K_recipient) before you can wrap anything for them — and publishing that key discoverably, tied to a real identity, without a trusted broker in the middle, is its own separate problem.

It's tempting to assume some existing piece of infrastructure already solves it. Worth checking directly rather than assuming — in more than one real system, the "obvious" discovery surface turns out to publish a different kind of key, for a different purpose, than the one this actually needs.

Where this is implemented and verified
The full spec — key type, kernel binding, axiom compatibility (A0/A2/A3/A7 preserved, A8 extended), and the honest state of the key-distribution gap — source-verified against a real kernel, not just described:

Cryptographic Set-Chemistry on Audiences →

Composable version, treating the whole thing as an "Island" whose audience is a set expression: Encrypted Semantic Island

Top comments (0)