Who can open a value, as a closed algebra.
The Island in Digital Space Algebra
is I = (path, ciphertext, T, A, C).
SpaceStructure defines path.
This page defines A.
U — finite set of kernel identities k = (identityHash, P256_pub), as in SetChemistryA ::= k | OR(A₁, …, Aₙ) | AND(A₁, …, Aₙ), with k ∈ US ⊆ U — the identities whose private keys are presentopens(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.
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.
These are the test contract.
opens(S, A) iff S contains some coalition in min(A).opens(S, A) ∧ S ⊆ S′ ⟹ opens(S′, A).min(A) opens nothing and derives nothing.A \ x drops every coalition containing x. Resolved at seal time only. x cannot open that version.OR any member may publish; under AND only the full coalition. Declaring a public formula is the owner's standing disclosure.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.
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.
NOT — "everyone except x" as a standing rule. Not monotone: adding people can never remove someone's ability to open. Exclusion exists only as A \ x, applied once, at seal time (L6).XOR — "a or b but not both." Not monotone either. ⊕ appears only inside, as the share split that implements AND.monad, not an audience.k-of-n — expressible today as OR of ANDs. A native operator (Shamir) may come later without breaking any law above.
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.
{a, b, c} means ORAND is declared explicitlyAND can always combine their shares. That is what AND is.P256_pub of each recipient — is not solved by the kernel. See SetChemistry's Key Exchange Prerequisite.Checked against neurons-me/.me source, Typescript/src.
wrapSecretV1 / unwrapSecretV1 in src/crypto.ts, exposed as statics on ME. One recipient per envelope — ECDH-ES P-256 / HKDF-SHA-256 / AES-256-GCM. Wrong key, tampered ciphertext and tampered tag are rejected in tests/contracts/wrapped-secret-negative.contract.test.mjs.grep -i audience src/ finds nothing.
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.
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.