Skip to main content
Markdown

Encryption and the site's users

What your plugin sends through WordPlus Cloud is sealed on the way: the realtime servers carry it without being able to read it or change it. You don't write any encryption code. The site holds the keys, gives each user the ones they may have, and the browser part opens everything before your listener sees it.

What is sealed​

WhatSealed withWho can open it
Events your server publishes to a private or presence channelthe channel's keywhoever the channel's owner lets in
Client events between browsers on those channelsthe channel's keythe same
A presence member's infothe channel's keythe same
What a call's and a room's people see of each other (info)the day's keyeveryone the site knows
The site's users in the directory (name, avatar, profile link)the day's keyeveryone the site knows

Public channels are not sealed. Anyone may subscribe to them, so nothing secret belongs there anyway.

The realtime servers see user ids, which key a value needs, and the sealed values. They never hold a key.

How it works​

  • One master key on the site. The package makes it the first time it's needed and keeps it in the option wordplus_realtime_master_key. It never leaves WordPress: not to WordPlus Cloud, not to a browser.
  • Every other key is derived from it, so every plugin on the site, whichever copy of the package it carries, has the same keys. What one plugin seals, another plugin's code opens.
  • The browser gets only the keys its user may have:
    • a signed-in user's page carries their own key and the day's keys;
    • a channel's key comes with the channel's token, from your channel's callback;
    • anything else, the page asks the site for (POST /wp-json/wordplus/v1/realtime/keys), and the site checks again.
  • Keys never come from the realtime servers. A key a server handed out could open what the browser seals next, so the browser takes keys from the site only.
  • XChaCha20-Poly1305: a value changed on the way doesn't open. PHP seals with sodium, which every WordPress site has (WordPress carries a copy for a PHP without the extension).

Who gets which key​

KeyLabelGiven to
The day'sd:{day}everyone the site knows: a signed-in user, or a visitor a plugin names (wordplus_realtime_visitor_id)
A user's ownu:{id}:{generation}that user
A channel'sch:{channel}:{generation}whoever the channel's callback lets in
A plugin's own kind{kind}:{…}whoever that plugin's callback says

A user the site cut off gets none (the filter wordplus_realtime_user_has_keys).

When someone loses access​

// A customer no longer belongs to the order: what the channel carries from now on is sealed from them.
wordplus_realtime()->keys()->rotate_channel( 'private-orders-42' );

// A user's access is revoked: their own key moves on.
wordplus_realtime()->keys()->rotate_user( $user_id );

Those still let in get the new key from the site when its first event comes. Nothing else changes in your code.

The site's users​

Every plugin on the site shares one directory of its users on WordPlus Cloud: each user's name, avatar and profile link, sealed with the day's key. Read it in the browser:

const users = await rt.users( [ 7, 12 ] );
// { 7: { user_id: 7, name: 'Ann', avatar: 'https://…', url: 'https://…/author/ann/' }, 12: { … } }
  • A signed-in user is in it from their page on: the page tells the server who they are, as the site signed it, and their entry is kept for a day after they were last seen.
  • Only a page whose user the site knows may read it. A visitor's page asks the site first; one the site doesn't know gets no_identity.
  • Add to an entry with the filter wordplus_realtime_profile. Every text is sealed, however deep in arrays; numbers and true/false stay as they are, so put nothing private in them.
add_filter( 'wordplus_realtime_profile', function ( $profile, $user_id ) {
$profile['badge'] = my_shop_badge( $user_id ); // text: sealed

return $profile;
}, 10, 2 );

Sealing your own data​

The keys are yours to use for anything else your plugin sends through WordPlus Cloud:

$keys   = wordplus_realtime()->keys();
$sealed = $keys->seal_json( $order, $keys->user_label( $customer_id ) ); // only the customer opens it
$again = json_decode( $keys->open( $sealed ), true ); // the site opens anything it sealed
const ring = wordplusRealtimeClient.keyring();
await ring.ensure( ring.missing( sealed ) ); // asks the site for a key the page lacks
const order = ring.openJson( sealed ); // undefined when no key it holds opens it

A plugin with things of its own to seal, conversations or tickets, registers a kind and decides who may have its keys:

wordplus_realtime()->keys()->kind( 'ticket', function ( $rest, $user_id ) {
return my_desk_may_read( (int) $rest, $user_id ); // the key ticket:42 for those who may read ticket 42
} );

Turning it off​

add_filter( 'wordplus_realtime_seal', '__return_false' ) sends everything in plain, for a site that has to look at what goes by. publish( …, array( 'seal' => false ) ) does it for one event. The browser opens what is sealed and passes on what isn't, so both work with the same code.

The Pusher-compatible API​

Plugins built for Pusher keep working as they are, and send what they send: sealing is this SDK's.