12 MultiDeviceAnnouncementsPOC
emdee edited this page 2024-02-02 15:58:07 +00:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Previous: Home

We need to solve the usage of Tox with multiple devices.

This proof-of-concept (POC) is based on the premise that solving the usage of Tox with multiple devices is an urgent problem, as anyone with mobiles runs into it, and all the competition software deals with it. MultiDevice is much easier in centralized systems and our decentalization is why it's hard for us; if we make it, we make be the first ever!

Woke Warning: gender specific, non-android/furry friendly grammar is used throughout.

The idea here is to address one simple usecase: a person can have multiple devices and he wants one device to be "active" at any given time. So he pushes so kind of announcement of what device he is currently using so that others can communicate to the device he is currently using.

The POC ignores the problem of history sync that this creates. Others can address that later perhaps with tools like rsync.

Background

When it comes to multi-device support, at the present time we are lagging behind well-known centralized messenger systems. A few examples are Google Hangouts, Facebook Messenger, and Skype. Every one of these syncs messages to every device which connects to their network. If you need an address, number, or entire friend list its a simple login away - because they're centralized.

Multi-Device support would solve some of the issues sharing a ToxID: it would allow the clients to connect with mobile devices (NFC, QR codes, other) and be available anywhere.

First Steps

The idea is to firstly define a PersonaID, much like a ToxPk that stays permanent across his devices, and maps the Person to the active ToxPk. So we distinguish between a Person, and a Friend who is one ToxID in a Person.

So the first step is to extend all library code and all clients to work with PersonaIDs as well as ToxPks. The PersonaIDs would be client facing, a nd the ToxPk code would be for internal access. A simple implementation would be duplicating all code calls that deals with ToxPks to have a layer of calls on top that take the PersonaID and consult a table that contains the active ToxPk, and then calls the ToxPk function.

For the sake of argument, let's say that the table is initially populated with all the existing ToxPks by deriving a default PersonaID ToxID by a simple method: the default table contains a PersonaID that maps to the first the ToxPk of the Person that it sees (identity table).

So we can add this layer of PersonaID->ToxPk mapping to the library and all clients right away with no breakage. In time, the clients will move to showing the PersonaID not the ToxPk as the primary client-facing ID, and extend the clients with the ability to see the table of Personae -> ToxPks (but maybe not change them), and the active ToxPk for each PersonaID.

More concretely, the PersonaID mapping table would be like a dictionary with 2 (or more) types of entries; before the arrival of a blob update, the table is by default (assuming JSON)

{"Pk1": "Pk1",
 "Pk2": "Pk2",
 ...}

So we could get the whole network switched over to accommodating PersonaIDs with no breakage. After the arrival of a blob to update the table, the PersonaID is an ordered dictionary mapping to a list of device PKs:

{"Pk1": "Pe1",
 "Pe1": ["Pe1pk1", "Pe1pk2", ...],
 "Pk2": "Pk2",
 ...}

or better still, associated ordered dictionary mapping to a dictionary with devices entry with a dictionary using nicknames, (leaving room for other fields in the Person dictionary such as groups Persona keys). :

{"Pk1": "Pe1",
 "Pe1": {"devices": {
          "Device1Nick": "Pe1k1",
          "Device2Nick": "Pe1k2", ...}
        },
 "Pk2": "Pk2",
 ...}

where:

  • Pk1 is the PK of user1,
  • Pe1 is the PersonaID of user1, which could be the public signing key of user1.
  • ["Pe1pk1", "Pe1pk2", ...] are the PKs of each of the devices of user1, delivered in a blob signed by ```Pe1``

Richer formats for the table are obviously possible, but we want to maintain a structure that foresees this table being managed by a keyring manager like keepassxc. For instance, this assumes each "device" is only in Tox profile, whereas there could be more than one device, hence keypair in use. We leave that aside for now.

MultiDevice Announcements Push

So if we modify the library and clients as described above, how do we keep it up to date when a friend changes his devices? If we know the PersonaID, how is the library code table of PersonaID to ToxPk mapping updated? We want this to be mainly automatic without bothering the user.

We would need to modify the client code to save a copy if the mapping table, so that when we start up a client, the mapping table is filled with last good values from the Persona storage. For the sake of this POC:

  1. the storage file could be JSON, possibly encrypted with ToxESave, or

  2. it could be a files in a Personae directory with files saved in JSON or YAML, like we do with avatars, with the filename being named PersonaID.json.

Again, we want to maintain a structure that foresees this table being managed by a keyring manager like keepassxc. It would not be saved in a profile, as this table spans multiple profiles.

Let's say we have a friend, and we know his ToxPk. We initially derive his PersonaID by setting it to the friend's public key - the entry in the table is a string.

We will call "a blob" the information that will update our PersonaID mapping to his active ToxID that is pushed to clients. We want to watch the blob flow for changes. So what is the blob flow of information?

  1. If a blob is small we can base64 encode it and stick it in his status message,

  2. We can do a blob push mechanism like we do for avatars, and push a blob into a file in a sub-directory called Personae, and handle updates the way we do avatars.

  3. We might push it as a DHT announcement.

Some clients may let the user decide whether of not to accept a blob update when it arrives.

MultiDevice Announcements Blob

So what is the blob of information that is pushed?

It has these characteristics:

  1. It has 3 things - a name and a payload, and a NaCl signature.

  2. It is small - 1K to 4K in size.

  1. As we will need a NaCl signing keypair in order to sign the payload, the name, (PersonaID) can be the public key of the signing keypair - 32 bytes or 64 hex chars.
  1. If we push the blob info by DHT announcements, it pushed regularly so people can get the PersonaID relatively soon - say hourly for now. Any longer means a person who has not connected to that friend before has to wait longer if they only have a PersonaID. If we push it like avatars, then it can be pushed once at start and subsequently on any changes.
  1. It might be encrypted so that the contents can not be seen in transit, but if the push mechanism is within Tox, it would be unnecessary.

  2. It must be signed so that it can be verified to be from the PersonaID, so we will assume every person generates a NaCl signing keypair and uses the same keypair for all of his devices. The public key of the signing keypair can be used as the PersonaID. If a secret is used as the seed of the signing keypair, then the generated keypair is reproducible.

If we use DHT announcements, each client would watch the stream for blobs named the PersonaID that they are interested in, which they know from the public signing key being in the status message of the friends. Pushing the blob through DHT announcements would mean that the PersonaID to ToxPk mapping could be accessed even by people who were not yet your Friends - helping discovery. This may not be a good idea, depending on if it's encrypted. This set of ToxPks could be keys that open the encrypted blob. Anyone who already has a ToxPk of the Persona could open the blob to get the update. Hence this encryption is different from most Tox encryption in that multiple keys can open it - like a multi-key Vera/True/dmwrapper container. But it might be enough for the blob to be signed, and that would be a good starting point - add encryption later if needed.

The public signing key could be put into the status message field of each Person, and if each user put the same signature public key as the status message on each of his devices, then that signing PK can become the PersonaID. The mapping table can be automatically scanned and any entries for which the PersonaID == ToxPk could be automatically updated to use the signing key as the PersonaId.

I had assumed we would have to use the DHT to deal with decentralization. But this POC makes me think we can use the toxcore status_message and avatar_files code to do it without the DHT, at least to Friends. So let's say for now to focus this discussion, we will ignore DHT announcements.

  1. Set your status message to be the signing public key and push the blob by some means like avatars to Friends, or

  2. It can be pushed as a status message. If we compress and base64 encode the blob, sign it and attach the signature in hex, and keep it under 1007 chars, then it can be set as your status message on all of your devices. The status message is visible to all of your friends so they all have a copy of the latest blob all of the time.

  3. If the blog.gz is less than 1000 chars, could it fit in a QR code?

In fact we could do #2 first to get things going, and then do #1, which is more changes to the code, although a lot of the code could be morphed from the avatar code. Or you could push a blob as a file if it got too big for a status message.

The same blob would be on each of a Person's devices.

Use the status message if less than 1007 bytes

With the signature and the public key, this should leave room for at least 12 devices in less than 1007 bytes, compressed and base64 encoded. With 1007 bytes with the signature and signing key, and nicknames of 12 bytes it will take about 16 devices: 1007-128-64-2/(32+6+12) = 16

One problem with this is that only your Friends could verify the blob, which means users you have never invited will not be able to verify, unless they had the signing public key independently (perhaps using /sendfile). But perhaps that's good. Clients could watch status message changes easily using the status_message callback.

It can be pushed as files like avatars amongst Friends

All the avatar code could be repurposed to also handle blobs, with the signing public key put into the Person's status message. Avatars are simple file transfers; there is a WHOLE byte dedicated for "file_kind", which means we can "just" add a new file_kind and use the exising file transfer code. In the beginning the clients would not need to be able to generate the blob - a simple Python script could do it outside the clients.

Unknown for now is does the client have to send a Friend add request to any new ToxPks found in the blob (new devices) to make them active if the client has never seen the PK before? I assume yes. If so then it complicates things a little as the blob might need to contain ToxIDs. The code could accept either ToxPks or ToxIds, distinguishing by length. Or you could just require that the Person already be a Friend with that device before the information in the table was useable.

(The blob might be a signed way of associating nicknames with Pks if we use associated ordered dictionary mapping to a dictionary with nicknames.)

The clients would manage this seamlessly to aggregate the ToxPks together under one Persona that is shown to the user. The library does most of this by accepting the update from the blob to update the table pointing from PersonaID to the new list of ToxPks (devices) with the first one being the active one.

The generation and signing verification of the blobs could be done outside of the clients in a utilty common to all clients. The clients let a user push a blob whenever he wants - like when he changes devices.

MultiDevice Profiles

In order for MultiDevice to work, we need to bootstrap a new devices from an existing profile. If you just copy a profile from one device to the next it will not work as you cannot have 2 profiles with the same keys online at the same time. We could have a simple utility that rekeys a profile and creates a new ToxID, so that we could copy a profile and rekey it. Then the new client would push a new blob with the new device's PK, signed by the signing keypair which is independent of profiles.

The profile rekey utility could also do other things at the same time, like edit the TCP_RELAYS section of the profile if desired. See https://git.plastiras.org/emdee/tox_profile

MultiDevice Groups

Previously, we described how we simply copy a profile to a new devices and change the public/private keys as a starting point.

But we are left with as special problem with NGC groups.

The code is assumed to have been modified to handle Personas and, at first glance, NGC groups become groups of Personas. Each member of the group is a Persona, and uses the table lookup to get the active ToxPk.

But the group structure in the profile has a copy of the founder's shared_state.founder_public_key (group_pack.c#L293) So if the profile being copied onto a new device is the profile of the founder, then this key would need to be updated too.

Besides the group keypair in #L351-2

  • bin_pack_bin(bp, chat->chat_public_key, EXT_PUBLIC_KEY_SIZE); // 1
  • bin_pack_bin(bp, chat->chat_secret_key, EXT_SECRET_KEY_SIZE); // 2

The chat->chat_secret_key is all zeros, I assume in everyone but the founder's profile.

It seems also that self's group keypair are saved to the group:

  • self_public_key #L353
  • self_secret_key #L354

Do these keys have to be updated with the new keypair of the new profile? Or do you only copy the keys from the profile that was in use when the Person first joined the group as a user - wrinkle. In which case, the notion of the blob needs extending from just the ToxPks and nicknames of each of the devices, to include the secret keys of that Persona for each NGC group the Persona belongs to.

In other words if I have 3 profiles with 3 nicks and 3 keypairs, and the first profile had a GROUP section where I have a group_nick, could I copy the GROUP section entirely into the other 2 profiles, and then be a member of the NGC group known as group_nick when I used those profiles? If so, the Persona should have dictionarys for each group, with group_nick, self_public_key and self_secret_key in each.

( Aside: I don't like the idea of keeping secret keys in profiles; they should be managed by a keymanager. In a corporate setting, this will be a requirement. )

There is a Python script to parse profiles that could be modified to rekey group self entries at: https://git.plastiras.org/emdee/tox_profile

Future Directions

If Personae identify which are your devices, it opens the possibility that clients could treat incoming messages from one of your devices differently. Instead of simply displaying the message in a window, the client might examine the message to see if it is a command. So clients could build in, for example a filebot, that responds to a set of commands if it's coming from one of your devices. Automated file transfer, DoorSpy, history sync; the possibilities are endless.

Summary

The changes to the core are not large:

  1. Change the status message callback to look for public signing keys
  2. Change the status message callback to look for blobs as a first implementation, and then later extend the avatar file transfer mechanism to transfer blobs
  3. Use the public signing keys to verify the blobs. This a NaCl call.
  4. Store the blobs in a subdirectory, with their signature.
  5. Use the blobs to populate the table, and designate one entry as active.
  6. Wrap all the calls that use a PK to look in the table for the active PK.
  7. Update the tables, if files are deleted from the Persona directory.

All existing unmodified users would continue to work: no testnet required.

Assuming the blobs were created by an independent stand alone utility, the changes to the clients are not large:

  1. Consult the mapping table and show it to the users.
  2. Accept or reject new blob updates.
  3. Redefine the notion of contacts to point to the PersonaID.
  4. Add the ability to push a new blob to Friends, or perhaps selected Friends.

Also we would want the Python script to keep profiles within a Persona in sync. We need to resolve the questions about having the same groups Personae.

This is also a security issue as without a keyring and user acceptance of a displayname, there is always a risk of impersonation in NGC groups: https://github.com/JFreegman/toxic/issues/622