A direct message is a messge encrypted to a feed_id
When you send a direct message you can have up to 16 DM recipients.
When sending a DM message, it's expected that you include your own feed_id
as a recpient,
this is so that anyone replying to your message can confidently copies the recps
from your message,
to the recps
field of thier message and be confident you will receive it!
You can also combine DM with group_id
or pobox_id
, in which case whether you need to include
your own feed_id
depends on whether you're part of the group or pobox
One example of these combined recps is when adding a new member to a group with a group/add-member
msg.
Here you have recps = [group_id, feed_id, feed_id, ...]
(NOTE group_id
must be in first slot, and can
be followed by up to 15 other feed_id
- the people you're adding).
Such a message can simultaneously:
- give the new person the info needed to access the group (decrypt with a DM shared key)
- reveal to the group that this action happened (decrypt with the group key)
When we see a feed_id
in content.recps
we need to map this to a key we will used in a recp_key
.
How we do that depends on whether the feed_id
is ours (the authors).
The two cases:
- A. Foreign case - mapping some elses
feed_id
torecp_key
- B. Self case - mapping our own
feed_id
torecp_key
We define a shared key that the sender (us) + recipient can both derive:
const hash = 'SHA256'
const length = 32
const salt = SHA256('envelope-dm-v1-extract-salt')
function directMessageSlotKey(
my_dh_secret,
my_dh_public,
my_id_bfe,
your_dh_public,
your_id_bfe
) {
var input_keying_material = scalarmult(my_dh_secret, your_dh_public)
var info_context = Buffer.from('envelope-ssb-dm-v1/key', 'utf8')
var info_keys = sort([
bfe.encode(3, 0, my_dh_public) || my_id_bfe,
bfe.encode(3, 0, your_dh_public) || your_id_bfe
])
var info = slp.encode([info_context, ...info_keys])
return hkdf(input_keying_material, length, { salt, info, hash })
}
(dh_secret, dh_public)
is some Diffie-Hellman compatible keypair to be used for encryption- currently we take feed keys (
ed25519
signing keys) and convert these to keys compatible with diffie-hellman (dh) shared key encryption (curve25119
keys) - in the future we plan to generate dh encryption keys seperately
- currently we take feed keys (
id_bfe
is the "id" of a feed, namely the public part of that feed's signing keypair, encoded in "type-format-key" format (see BFE)||
means Buffer concatsort
means sort these 2 buffers bytewise so that the smallest is firstbfe.encode
is bfe encoding of the encryption key, here withtype: 3, format 0
(see BFE)slp.encode
is "shallow length-prefixed encode" (see SLP)
The complete key-and-scheme you try with a slot is of form
js { key: Buffer, // the directMessageSlotKey scheme: "envelope-id-based-dm-converted-ed25519" }
A problem with sending a direct message to another feed is that once the message is enveloped, we don't have a record of who the remote recipients were, so guessing which shared-key to derive (1) is hard.
A solution is to include our own feed_id
as a recipient, and to always try a key we would have used on such messages.
We could in theory apply the same scheme as in (1), but this would involve us doing a scalar multiplaction of the public and private parts
of our key, which should be safe, but we've decided not to do it.
Instead, when you see your own feed_id
as a recipient, you're expected to map that to a private symmetric key, own_key
.
Major advantages of this approach are:
- replies to original messages can use the same recps - makes coding more consistent
- this is compatible with multi-device identities - later I can send a copy of keys to other devices so they have access to the same DMs
Use a key of 32 bytes
Notes:
- the full key you try with a slot is of form
{ key, scheme: "envelope-symmetric-key-for-self" }
- we considered using a "personal group", but this has the disadvantages:
- makes replies hard - remote feeds do not know how to map your personal
group_id
to a symmetric key, so...? - includes un-cloaked tangle info about the group, which may leak private info (e.g. feeds / devices you don't want known)
- makes replies hard - remote feeds do not know how to map your personal
The complete key-and-scheme for the envelope key_recp
slot is of form
js { key: Buffer, // the directMessageSlotKey scheme: "envelope-id-based-dm-converted-ed25519" }
// TODO
If the encrypted message is one I sent, I can decrypt it by:
- trying my
own_key
on all slots of the envelope - you could technically try each DM-key derived from
you_dh_sk
+friend_dh_pk
...- DON'T do this, it's a super expensive brute force approach
- with the principle that "content.recps is conserved across messages" we don't need to do this!
- BUT, this is why
feed_id
based DMs is never truly one-way, even if you "omit yourself" from recps
If the encrypted message is from someone else, I can try decrypting it by:
- trying a key derived from
my_dh_secret
andauthor_dh_public
etc.
It's safe to use feed_id
anywhere in public (as these are already public).
In the context of content.recps
we use feed_id
so that other recipients can see who
was included in the message, but for the envelope recp_key
we map the feed_id
to a shared_dm_key
using the above definition
var content = {
//...
recps: [
'ssb:identity/group/g_JTmMEjG4JP2aQAO0LM8tIoRtNkTq07Se6h1qwnQKb=',
'ssb:feed/classic/YjoQc7sLF_ye-QM09iPcDJdzQo3lQD6YvQIFhmNbEqg=' // << a feed_id
]
}