Statement

An audience is a monotone Boolean formula over identities. It compiles to a tree of key envelopes. Encryption can only grant; it can never take away.

The Island in Digital Space Algebra is I = (path, ciphertext, T, A, C). SpaceStructure defines path. This page defines A.

Definitions

universe
U — finite set of kernel identities k = (identityHash, P256_pub), as in SetChemistry
audience
A ::= k | OR(A₁, …, Aₙ) | AND(A₁, …, Aₙ), with k ∈ U
coalition
S ⊆ U — the identities whose private keys are present
opens
opens(S, A) — the formula A is true when exactly the members of S are set true

Below, ∨ is OR and ∧ is AND. SetChemistry's union audience K₁ ∪ … ∪ Kₙ is OR over leaves; its intersection audience K₁ ∩ … ∩ Kₙ is AND over leaves. There is no NOT: that is what makes it monotone.

Canonical Form

Every A reduces to min(A), its set of minimal coalitions. It is an antichain: no coalition in it contains another.

(alice AND bob) OR carol   =   { {alice, bob}, {carol} }

Operations on canonical form, where ⌊·⌋ drops every coalition that is a superset of another:

min(k)       = { {k} }
min(A ∨ B)   = ⌊ min(A) ∪ min(B) ⌋
min(A ∧ B)   = ⌊ { X ∪ Y | X ∈ min(A), Y ∈ min(B) } ⌋

id(A)        = hash(sorted min(A))

The id of an audience is the hash of its canonical form, so how it was written doesn't matter. carol OR (bob AND alice) and (alice AND bob) OR carol OR (carol AND bob) have the same id.

Monotone formulas over a finite U, up to equivalence, form a finite distributive lattice: ∨ and ∧ always give back an audience, and every audience has exactly one canonical form. Closed and well defined.

Laws

These are the test contract.

L4, written out:
A ∨ B = B ∨ A                A ∧ B = B ∧ A
(A ∨ B) ∨ C = A ∨ (B ∨ C)    (A ∧ B) ∧ C = A ∧ (B ∧ C)
A ∨ A = A                    A ∧ A = A
A ∨ (A ∧ B) = A              A ∧ (A ∨ B) = A
A ∧ (B ∨ C) = (A ∧ B) ∨ (A ∧ C)
A ∨ (B ∧ C) = (A ∨ B) ∧ (A ∨ C)

A \ x can come out empty — alice AND x minus x leaves no coalition. An empty audience opens for no one, so seal refuses it.

Construction

Seal walks the formula tree. Each node carries a key. An OR node hands the same key to every child. An AND node splits its key into XOR shares, K = s₁ ⊕ … ⊕ sₙ, one per child. A leaf wraps whatever key it receives to that identity with wrapSecretV1; the identity opens it with its P256 private key.

seal(V, A):
  K ← random(32)
  ciphertext = encryptSymmetric(K, V)
  publish { ciphertext, tree: wrap(A, K) }

wrap(k, K):
  leaf(identityHash(k), wrapSecretV1(P256_pub(k), K))
wrap(OR(A₁…Aₙ), K):
  or [ wrap(Aᵢ, K) ]ᵢ
wrap(AND(A₁…Aₙ), K):
  s₁, ..., sₙ₋₁ ← random(32) each
  sₙ = K ⊕ s₁ ⊕ ... ⊕ sₙ₋₁
  and [ wrap(Aᵢ, sᵢ) ]ᵢ

open(node, keys):
  leaf(h, W) → k ∈ keys with identityHash(k) = h ?
               unwrapSecretV1(P256_priv(k), W) : ⊥
  or  [...]  → the first child that opens, else ⊥
  and [...]  → every child opens → s₁ ⊕ ... ⊕ sₙ, else ⊥

V = decryptSymmetric(open(tree, keys), ciphertext)

Open walks the tree with whatever keys it holds. It is recursive, so nesting costs nothing extra. SetChemistry's Union and Intersection blocks are this tree at depth one. The ciphertext is authenticated, so shares that combine to a wrong K end in a decrypt failure (L8), not garbage.

Excluded, and Why

Groups

A group is an immutable value identified by its set: id(G) is the hash of its canonical form. Same members in any order, same group — the same property as the README's audienceSeed, where frank + ana = ana + frank.

G₁ = { alice, bob }
G₂ = G₁ ∪ { carol }    // new id, new key

team → G₁              // "to team" at t₀ = G₁
team → G₂              // "to team" at t₁ = G₂

history(team) = [ G₁, G₂ ]

Adding a member creates a new group with a new id and a new key. What was sealed to G₁ remains exactly for G₁. What changes is a name in the tree: team points to G₁, then to G₂. Sealing "to team" uses the group it points to at that moment, and the name's history is the sequence of groups. Join and leave move the pointer, so revocation needs no extra mechanism (L7).

A member may itself be a group. That is just nesting.

Limits of the Model

Implementation Status

Checked against neurons-me/.me source, Typescript/src.

AudienceBlobV1 in SetChemistry is a spec, not kernel code, and it is flat: a list of members. It will need to grow into a tree. The README's audienceSeed is an example in the README, not a kernel export. The real wrapSecretV1 takes { secret, recipientPublicKey, kid, class }; the pseudo-code above uses SetChemistry's short form.

The laws above become the test contract tests/Security/audience-algebra.test.ts in neurons-me/.me. Planned, not written. OR cases can run today by composition; AND cases are marked todo.