Direct messages
A direct message is text in a space that only some of its members can read: the people it is for, and whoever wrote it. Use it for a private word inside a shared space. Today it needs a private space.
Sending and reading
const people = await node.direct.reachable(space.id); // who you can write to
await node.direct.send(space.id, people.slice(0, 1), 'See you at eight?');
const messages = await node.direct.list(space.id);
messages[0]?.text; // null when this node can't open itThe methods are on
NodeDirect, and each
message is a
DirectMessage.
A message is a record, so it syncs and is kept like any other. Delete its
key to take it back.
A message is for 1–16 people besides you, and you can only write to someone who has a member key in the space.
Who can read them
node.direct needs the account's member key for the space. An agent isn't
given one, so it may only call reachable.
That is the library refusing, not the sealing. An app or program granted the whole account holds the key every member key comes from. One granted a private space holds its member key. Either can open the account's direct messages there with code of its own (threat model §3.6).
A bot is an account of its own, with its own member key, so it reads and
sends them like anyone. The actions direct_list and direct_send
(actions) are offered to bots, not to agents. An automation set
off by a direct message hands the bot the message already opened.
What others see
In a private space the whole body is also encrypted with the space key, so peers outside the space see only the envelope. Other members see who wrote to whom, and when, but not what.
to is a topic, so "direct messages to me" can be an automation of the
account's own, which a keeper can match without reading the message
(automations).
A reader can't tell that the others read the same text. A writer can seal each reader's copy to open to a different text (#214).
How it works
One record serves every reader and every one of their devices. A fresh message key seals the text once. That key is then sealed to each reader's member key, in a box of their own.
This is a convention on the protocol, not part of it. Peers that have never
heard of std.direct sync, store and judge it like any other record. Only
apps that read each other's direct messages must agree on the format below.
It uses member keys
(spec 03 §9.1),
the space key's derivations (spec 03 §8.1), and the envelope and HPKE sealing
(spec 01 §9).
std.direct
std.direct (topics: to; no rules; who may write one is the space's grants)
{
to: [<account DID>, …], // 1–16, sorted, once each, not the writer
data: string ≤ 60000, // the text, sealed with the message key
boxes: [{ to: <account DID>, sealed: string ≤ 1000 }, …] // ≤ 17: one per reader
}The writer, from, is the root of the record's first version. The
readers are every DID in to, and from.
Sealing
bound = { space: <spaceId>, from: <from>, to: <to joined by ","> }
k = 32 fresh random bytes, a space key (spec 03 §8.1)
data = base64url(envelope(sealKey(k), "minca/direct-text/v1", bound, JSON { text }))
box(r) = HPKE(memberKey(r), "minca/direct-box/v1", { ...bound, reader: r }, JSON { key: base64url(k) })
boxes = [ box(r) for r in to, then box(from) ]sealKey(k) is HKDF(k, "minca/space-seal/v1", 32), as for any space key.
envelope and HPKE are spec 01 §9.1 and §9.2.
memberKey(r) is the key r published in sys.memberkey. The writer may
seal its own box to its member key as it holds it, rather than as published,
so its other devices can read what it sent before that key is published. A
writer must not write a direct message for someone with no member key in the
space.
Example: space space-1, from did:a, to did:c and did:b. The text is
bound to {"from":"did:a","space":"space-1","to":"did:b,did:c"}, and the box
for did:b to {"from":"did:a","reader":"did:b","space":"space-1","to":"did:b,did:c"}.
Opening
A reader r opens a direct message only when the record verifies and stands,
its collection is std.direct, and r is from or is in to. It builds the
bound from the space the record is in, and from and to as the record gives
them, sorted. Then it:
- takes the box whose
toisr; - opens it with its own member key, label
minca/direct-box/v1and the bound withreader: r, giving a value whosekeyis a base64url string of 32 bytes; - opens
datawith that key's seal key, labelminca/direct-text/v1and the bound, giving an object whosetextis a string.
If any step fails, the message is unreadable to r. A chat must show it as
unreadable, not drop it, the same as a body a reader can't open.
Binding the text to the space, the writer and the readers means a copy
re-posted by another member, moved to another space, or with a reader added
to to opens nothing. Binding each box to its reader means a box can't be
moved into another reader's slot.
Planned: direct messages in public spaces
Members publish member keys only in private spaces, so a public space has none
to seal to. Publishing them in every space would let members of a public
space write to each other. The text would then be protected only by the
sealing above, with to visible to anyone.