-
Notifications
You must be signed in to change notification settings - Fork 249
Whisper Push Notifications
It is crucial to understand what is Whisper Notification Service is designed for:
- to augment Whisper/5 functionality, by allowing clients to subscribe to arbitrary node that has service enabled
- once subscribed, client receives randomly generated symmetric key (
SubscriptionKey
), which is used to control subscription (say, querying for server id or generating chat keys). - using
SubscriptionKey
, new chat sessions can be initiated (yielding yet another SymKey -ChatKey
). By sharing that key with others, you will have a common secret that can be used to trigger notifications. - everywhere possible, Whisper protocol itself was used. For example, discovery mechanism is nothing more but Whisper message exchanges between parties. The same goes for notification sending. The aim was to keep the Service as slim as possible.
The crux here is how to allow DeviceB
to trigger notifications on DeviceA
, all this w/o knowing much of which wnode is used, or details of DeviceA
. And Chat SymKey
solves this issue elegantly (the beauty is that it works for both one-on-one and group chats, all you need to ensure is share a secret, namely Chat SymKey
, so that newcomers can add their devices to subscribers list).
Important Notes:
-
Device A
can start the protocol by broadcasting using Discovery Protocol SymKey. However,statusd wnode
command (that is used to start notification-enabled wnodes) also supports using asymmetric keys to kick off the process. For further details, see examples below. - It is important to understand that notification request messages go along the normal communication i.e.
- on
DeviceA
you send encrypted message toDeviceB
(so, onlyDeviceB
can open envelope) - once done, you need to send a broadcast message encrypting it with
Chat SymKey
- some node over there, will be able to decrypt your broadcast (the only one that has SymKey)
- once decrypted, wnode will go over list of device ids (that were previously registered with given Chat SymKey) and send notifications requests to FCM on behalf of them to them (only 1 request for one-on-one chats,
n-1
requests for group ofn
chats - that's we do not notify ourselves)
- on
- So, messages are never exposed, and they are routed in parallel. Notifications are also encrypted (by Chat SymKey), so are not exposed to eavesdropper. This means we are not compromising on darkness. What gets exposed, however? Well, by registering device id's with a given chat session we expose it to
wnode
.
In Whisper/5 all messages are encrypted, either with symmetric/topic key or asymmetric public key. That means, when broadcasting we need to have some SymKey that wnodes capable of Notification Services can decrypt and respond to.
var keyname = 'NOTIFICATION_PROTOCOL_KEY';
var keypass = 'asdfasdf';
// make sure that notification protocol key exists in whisper
if (!web3.shh.hasSymKey(keyname)) {
console.log("protocol key not found, adding..");
web3.shh.addSymKey(keyname, web3.fromAscii(keypass));
}
It is important to understand that this key is publicly known (we will use different password on our cluster, you may use your own password, see statusd wnode --password
option). So, encryption here doesn't provide privacy - instead, it allows Notification Service capable nodes to filter requests to them (as they will be able to decrypt those messages).
Assume that we have two nodes (Device A
and Device B
) which have different underlying nodes:
var web3 = new Web3();
web3.setProvider(new web3.providers.HttpProvider('http://localhost:8645')); // Device A
web3.setProvider(new web3.providers.HttpProvider('http://localhost:8745')); // Device B
Both nodes have Notification Protocol SymKey added, so can participate in discovery and subscription to Notification Services.
To support those two different apps, two Status nodes are started:
statusd --datadir app1 --http --httpport 8645 --shh
statusd --datadir app2 --http --httpport 8745 --shh
Original workflow for discovery mechanism was introduced in:
Everything starts with Device A
willing to register with some Notification Service Provider. To do so, it sends/broadcasts discovery request:
var discoverServerTopic = '0x268302f3'; // DISCOVER_NOTIFICATION_SERVER
var sendDiscoveryRequest = function (identity) {
// notification server discovery request is a signed (sent from us) broadcast,
// encrypted with Notification Protocol Symmetric Key (which is publicly known)
var err = web3.shh.post({
from: identity,
topics: [discoverServerTopic],
ttl: 20,
keyname: 'NOTIFICATION_PROTOCOL_KEY'
});
if (err !== null) {
console.log("message NOT sent")
} else {
console.log("message sent OK")
}
};
var identity = web3.shh.newIdentity();
sendDiscoveryRequest(identity);
Once that request is sent all capable wnodes respond back, so we need to watch for their responses, and select one provider (generally FIFO). To watch:
var proposeServerTopic = '0x08e3d8c0'; // PROPOSE_NOTIFICATION_SERVER
var watchProposeServerResponses = function (identity) {
// some notification servers will be able to serve, so they will send encrypted (to you)
// message, with a PROPOSE_NOTIFICATION_SERVER topic (for which we wait)
var filter = web3.shh.filter({
to: identity, // wait for anon. messages to ourselves
topics: [proposeServerTopic]
});
filter.watch(function (error, result) {
if (!error) {
console.log("Server proposal received: ", result);
// response will be in JSON, e.g. {"server": "0x81f34abd0df038e01a8f9c04bee7ce92925b7240e334dc8f2400dea7a2a6d829678be8b40e1d9b9988e25960552eafe2df7f928188e4143ba657a699519c438d"}
// which will give you serverID
var payload = JSON.parse(web3.toAscii(result.payload));
// no need to watch for the filter any more
filter.stopWatching();
// accept (in FIFO order) the server
// we need to make sure that only a single server is selected,
// as you will receive multiple proposals for different servers,
// and may accept more that one of those proposals (which will
// result in duplicate notifications)
sendAcceptServerRequest(identity, payload.server);
}
});
};
watchProposeServerResponses(identity);
Now, whenever we receive server proposal and willing to accept it, sendAcceptServerRequest()
message needs to be broadcasted (so that server knows we have selected it):
var acceptServerTopic = '0x04f7dea6'; // ACCEPT_NOTIFICATION_SERVER
var sendAcceptServerRequest = function (identity, serverID) {
// whenever we are ready to accept server, having a given serverID, we need
// to notify it by sending signed (from us) acceptance message.
var err = web3.shh.post({
from: identity, // it is absolutely important to identify the client, or your acceptance will be dropped
topics: [acceptServerTopic],
payload: '{"server": "' + serverID + '"}',
ttl: 20,
keyname: keyname
});
if (err !== null) {
console.log("message NOT sent")
} else {
console.log("message sent OK")
}
};
To complete discovery protocol, we need to receive ACK message from the Notification Server we selected. Server's ACK message will also include SymKey, which we will use to communicate securely with server down the line:
var ackClientSubscriptionTopic = '0x93dafe28'; // ACK_NOTIFICATION_SERVER_SUBSCRIPTION
var watchServerAckResponses = function (identity) {
// if server we accepted is ok, it will send encrypted (to you)
// message, with a ACK_NOTIFICATION_SERVER_SUBSCRIPTION topic (for which we wait)
// This message completes the subscription process. At this point you should
// have topic and symkey necessary to manage your subscription.
var filter = web3.shh.filter({
to: identity, // wait for anon. messages to ourselves
topics: [ackClientSubscriptionTopic]
});
filter.watch(function (error, result) {
if (!error) {
console.log("Server ACK received: ", result);
// response will be in JSON, e.g. {"server": "0xdeadbeef", "key": "0xdeadbeef"}
// which will give you serverID
var payload = JSON.parse(web3.toAscii(result.payload));
console.log(payload);
// no need to watch for the filter any more
filter.stopWatching();
// this concludes discovery, and we can use obtained key to invoke chat sessions
createChatSession(payload.key)
}
});
};
So, at this point we have a client session key (payload.key
passed into createChatSession()
in previous code example), which we are ready to use it to create chat session.
Now, why do we have distinct client and chat keys? The reason is simple: client session key is cryptographic connection between client and Notification Service, it is to be known on host device only. However, in order to be able to trigger notification on each other, devices need to share some secret. That secret is chat session key. For a device it is enough to know chat session key, to be able to:
- register itself as notifications receiver
- trigger notifications on all the other registered devices
- all this in secure/dark way
Here is how you create chat session key might look like (remember we are still using web3
object connected to Device A
node):
var newChatSessionTopic = '0x509579a2'; // NEW_CHAT_SESSION
var ackNewChatSessionTopic = '0xd012aae8'; // ACK_NEW_CHAT_SESSION
var createChatSession = function (subscriptionKey) {
var chatId = '0xdeadbeef';
// subscriptionKey is key shared by server that allows us to communicate with server privately
var keyname = 'SUBSCRIPTION_KEY'; // you might want to be tad more creative
web3.shh.deleteSymKey(keyname);
web3.shh.addSymKey(keyname, subscriptionKey);
console.log("subscription key: ", subscriptionKey);
// before sending new chat request, let's start waiting for response
var filter = web3.shh.filter({
to: identity,
topics: [ackNewChatSessionTopic]
});
filter.watch(function (error, result) {
if (!error) {
console.log("Chat Creation ACK received: ", result);
// response will be in JSON, e.g. {"server": "0xdeadbeef", "key": "0xdeadbeef"}
// which will give you serverID
var payload = JSON.parse(web3.toAscii(result.payload));
// no need to watch for the filter any more
filter.stopWatching();
// ok, at this point we have Chat Session SymKey, and we can:
// 1. register our device with that chat
// 2. share that key with others, so that they can register themselves
// 3. use chat key to trigger notifications
registerDevice(chatId, payload.key);
shareChatKey(chatId, payload.key);
}
});
var err = web3.shh.post({
from: identity,
topics: [newChatSessionTopic],
payload: '{"chat": "' + chatId + '"}', // globally unique chat ID
ttl: 20,
keyname: keyname // we use subscription key, so connection is NOT public
});
if (err !== null) {
console.log("message NOT sent")
} else {
console.log("message sent OK")
}
};
Now, we have Chat Session Key, which we can use to register our device id (to be used as target by FCM), and allow others to register themselves.
Let's see how registerDevice()
might look like:
var newDeviceRegistrationTopic = '0x14621a51'; // NEW_DEVICE_REGISTRATION
var ackDeviceRegistrationTopic = '0x424358d6'; // ACK_DEVICE_REGISTRATION
var registerDevice = function (chatId, chatKey) {
console.log('chat session key: ', chatKey)
// this obtained from https://status-sandbox-c1b34.firebaseapp.com/
var deviceID = 'ca5pRJc6L8s:APA91bHpYFtpxvXx6uOayGmnNVnktA4PEEZdquCCt3fWR5ldLzSy1A37Tsbzk5Gavlmk1d_fvHRVnK7xPAhFFl-erF7O87DnIEstW6DEyhyiKZYA4dXFh6uy323f9A3uw5hEtT_kQVhT';
// make sure that chat key is loaded
var keyname = chatId + 'chatkey'; // there might be many chat keys
web3.shh.deleteSymKey(keyname);
web3.shh.addSymKey(keyname, chatKey);
// before sending request, let's start waiting for response
var filter = web3.shh.filter({
to: identity,
topics: [ackDeviceRegistrationTopic]
});
filter.watch(function (error, result) {
if (!error) {
console.log("Device Registration ACK received: ", result);
// response will be in JSON, e.g. {"server": "0xdeadbeef"}
var payload = JSON.parse(web3.toAscii(result.payload));
// no need to watch for the filter any more
filter.stopWatching();
}
});
var err = web3.shh.post({
from: identity,
topics: [newDeviceRegistrationTopic],
payload: '{"device": "' + deviceID + '"}',
ttl: 20,
keyname: keyname
});
if (err !== null) {
console.log("message NOT sent")
} else {
console.log("message sent OK")
}
};
So, on Device A
side, we have Chat Session Key, and we registered the device with that session. Now, we need to send/share the SymKey:
var shareChatKey = function (chatId, chatKey) {
console.log('chat session key: ', chatKey)
// pre-defined test identity (it gets injected automatically by statusd)
var deviceBIdentity = '0x04eedbaafd6adf4a9233a13e7b1c3c14461fffeba2e9054b8d456ce5f6ebeafadcbf3dce3716253fbc391277fa5a086b60b283daf61fb5b1f26895f456c2f31ae3';
// it is up to you how you share secret among participants, here is sample
var err = web3.shh.post({
from: identity,
to: deviceBIdentity,
topics: ["chatKeySharing"],
payload: '{"chat": "' + chatId + '", "key": "' + chatKey + '"}',
ttl: 20
});
if (err !== null) {
console.log("message NOT sent")
} else {
console.log("message sent OK")
}
};
You are free to use your own topic key, and format. What is important is the fact that Chat Session Key gets to the counter-party.
On Device B
side, we need to wait for shared key, and then, once we get it, register Device B
with the chat session.
var web3 = new Web3();
web3.setProvider(new web3.providers.HttpProvider('http://localhost:8745'));
// pre-defined test identity (it gets injected automatically by statusd)
var identity = '0x04eedbaafd6adf4a9233a13e7b1c3c14461fffeba2e9054b8d456ce5f6ebeafadcbf3dce3716253fbc391277fa5a086b60b283daf61fb5b1f26895f456c2f31ae3';
if (!web3.shh.hasIdentity(identity)) {
throw 'idenitity "0x04eedbaafd6adf4a9233a13e7b1c3c14461fffeba2e9054b8d456ce5f6ebeafadcbf3dce3716253fbc391277fa5a086b60b283daf61fb5b1f26895f456c2f31ae3" not found in whisper';
}
// for for key sharing, it is up to you how you implement it (which topic to use etc)
var filter = web3.shh.filter({
to: identity, // wait for anon. messages to ourselves
topics: ['chatKeySharing']
});
filter.watch(function (error, result) {
if (!error) {
console.log("Chat key received: ", result);
// response will be in JSON, e.g. {chat: "0xdeadbeef", key: "0x04e68e37433baf55ddc2fe9f7533e4e722bcdad4239c98df92f3522907ced72d"}
var payload = JSON.parse(web3.toAscii(result.payload));
console.log(payload);
// no need to watch for the filter any more
filter.stopWatching();
// let's save incoming key
var keyname = payload.chat + '-chatkey';
web3.shh.deleteSymKey(keyname);
web3.shh.addSymKey(keyname, payload.key);
// now register ourselves
var deviceId = 'some-device-id'; // you obtain this from FCM
registerDevice(web3, identity, payload.chat, payload.key, deviceId);
// finally, trigger the notifications on all registered device (except for yourself)
// at this point it is really trivial, use the key + specific topic:
var err = web3.shh.post({
from: identity,
topics: [sendNotificationTopic],
payload: "Hello, you've got mail!!", // yes, arbitrary data!
ttl: 20,
keyname: keyname
});
if (err !== null) {
console.log("message NOT sent")
} else {
console.log("message sent OK")
}
}
});
We provide all the code for Device B
in a single chunk. Basically, all that Device B
does, is: wait for shared key, register device, trigger notification.
Full sample page used for testing can be found here: https://gist.github.com/farazdagi/5ac082ddf006d00de422385f07d41ad3
In order to use that page you need to start the following:
statusd --datadir app1 --http --httpport 8645 wnode # as Device A
statusd --datadir app2 --http --httpport 8745 wnode # as Device B
statusd --datadir wnode2 wnode --notify --password asdfasdf --injectaccounts=false --firebaseauth=XXX # as notification server, you need to setup your app with FCM and use your own authorization key
If you run that page in gist, you should see sth like this:
If Discovery Protocol's SymKey is publicly known, what will happen if some attacker just forks status-go
repo, and responds to discovery request?
It is crucial to understand what might be exposed:
- clients' public keys
- clients' device IDs
- some extra info which you pass in notifications
So, public keys exposure shouldn't be a huge problem. When it comes to device IDs, can attacker use them to send unwanted/spam notifications? Well, since those device IDs were generated for a given FCM app, they are useless to attacker unless authorization key to that app is known (and it isn't). Finally, to make sure we do not over-expose ourselves, make sure that you do not include too much of internal information in notifications - then even if attacker can get those, not much will be gained (although, we must warn users that by enabling notifications they potentially enable 3rd party to track the fact of communication - not the content of it of course).
Now, on incentives: we must make sure that economic rewards are such that it is more interesting for 3rd party to provide proper service than to involve in malicious acts.
Project Information
Getting started
Developers
Support
Internal