diff --git a/Whisper-Push-Notifications.md b/Whisper-Push-Notifications.md index 5b1d0d9..d9115d1 100644 --- a/Whisper-Push-Notifications.md +++ b/Whisper-Push-Notifications.md @@ -1,773 +1 @@ -# Overview - -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. - -## High-Level Overview of the process - -[![image](https://cloud.githubusercontent.com/assets/188194/25035829/b3cf02aa-20f8-11e7-882b-1a4958bcda13.png)](https://cloud.githubusercontent.com/assets/188194/25035829/b3cf02aa-20f8-11e7-882b-1a4958bcda13.png) - - -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` starts the protocol by sending encrypted message using Discovery Protocol PubKey (Status test cluster will have a single PubKey installed on all the nodes, allowing any of them to respond to discovery requests). -- While discovery happens using asymmetric cryptography, once you obtain `Subscription SymKey` and `Chat SymKey` you rely on symmetric encryption. -- It is important to understand that notification request messages go *along* the normal communication i.e. - - on `DeviceA` you send encrypted message to `DeviceB` (so, only `DeviceB` 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 `Chat SymKey`) - - once decrypted, wnode will go over list of device IDs previously registered for this Chat, and send notifications requests to FCM using those IDs (only 1 request for one-on-one chats, `n-1` requests for group of `n` chats - that's we do not notify ourselves) -- So, messages are never exposed, and they are routed in parallel. Notifications are also encrypted (by Chat SymKey), so are not exposed to eavesdropper either. This means we are not compromising on darkness. The following info gets exposed though: - - by registering device ID with a given chat session we expose it to `wnode` - - we expose our PubKeys (wnodes need message to be signed, so that you PubKey can be filtered out, and notification by you towards yourself is avoided) - - we may expose some critical information in body of notification message (highly not advised!). - -# Communication Protocol - -⭐️ ⭐️ ⭐️ The code below is **outdated** (it was written for Geth 1.5.9, and with Geth 1.6.0 Whisper API changed drastically). It is still advised to skim over the code and comments, to get better understanding of the idea. And at the end of this document, we include full-fledged test suite (to be run with [Mocha](https://mochajs.org/)). - -Suppose you started service node(s) using `statusd --datadir wnode1 wnode --notify -nodekey ./wnodekey -.shh.firebaseauth ./firebaseuathkey`, where `wnodekey` and `firebaseauthkey` files contain node's private key and FCM authorization key respectively. Now, your server is ready to receive requests encrypted with its PubKey, which for our cluster is `0x040edb0d71a3dbe928e154fcb696ffbda359b153a90efc2b46f0043ce9f5dbe55b77b9328fd841a1db5273758624afadd5b39638d4c35b36b3a96e1a586c1b4c2a`. - -On client side, we need to use protocol PubKey and pre-defined topic: - -```js -var protocolKey = '0x040edb0d71a3dbe928e154fcb696ffbda359b153a90efc2b46f0043ce9f5dbe55b77b9328fd841a1db5273758624afadd5b39638d4c35b36b3a96e1a586c1b4c2a'; -var sendDiscoveryRequest = function (identity) { - // notification server discovery request is a signed (sent from us), - // encrypted with Notification Protocol Asymmetric (Public) Key - var err = web3.shh.post({ - from: identity, - to: protocolKey, - topics: [discoverServerTopic], - ttl: 20 - }); - if (err !== null) { - console.log("message NOT sent") - } else { - console.log("message sent OK") - } -}; -``` - -Here is the list of topics (see the full sample file, where they are all used): -```js -var discoverServerTopic = '0x268302f3'; // DISCOVER_NOTIFICATION_SERVER -var proposeServerTopic = '0x08e3d8c0'; // PROPOSE_NOTIFICATION_SERVER -var acceptServerTopic = '0x04f7dea6'; // ACCEPT_NOTIFICATION_SERVER -var ackClientSubscriptionTopic = '0x93dafe28'; // ACK_NOTIFICATION_SERVER_SUBSCRIPTION -var sendNotificationTopic = '0x69915296'; // SEND_NOTIFICATION -var newChatSessionTopic = '0x509579a2'; // NEW_CHAT_SESSION -var ackNewChatSessionTopic = '0xd012aae8'; // ACK_NEW_CHAT_SESSION -var newDeviceRegistrationTopic = '0x14621a51'; // NEW_DEVICE_REGISTRATION -var ackDeviceRegistrationTopic = '0x424358d6'; // ACK_DEVICE_REGISTRATION -var checkClientSessionTopic = '0x8745d931'; // CHECK_CLIENT_SESSION -var confirmClientSessionTopic = '0xd3202c5f'; // CONFIRM_CLIENT_SESSION -var dropClientSessionTopic = '0x3a6656bb'; // DROP_CLIENT_SESSION -``` - -Finally, in order to test everything out, you'd better use two different clients not one. So, assume that we have two nodes (`Device A` and `Device B`) which have different underlying Status daemons: -```js -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 -``` -To support those two different apps, two Status nodes are started: -```bash -statusd --datadir app1 --http --httpport 8645 --shh -statusd --datadir app2 --http --httpport 8745 --shh -``` - -# Workflow: Discovery, Subscription, Chat Management, Notification Triggering - -### 1. Discovery request and Notification Server selection - -Everything starts with `Device A` willing to register with some Notification Service Provider. To do so, it sends discovery request *to some known cluster* (as you need to specify PubKey used on wnode side): - -```js -var protocolKey = '0x040edb0d71a3dbe928e154fcb696ffbda359b153a90efc2b46f0043ce9f5dbe55b77b9328fd841a1db5273758624afadd5b39638d4c35b36b3a96e1a586c1b4c2a'; -var discoverServerTopic = '0x268302f3'; // DISCOVER_NOTIFICATION_SERVER -var sendDiscoveryRequest = function (identity) { - // notification server discovery request is a signed (sent from us), - // encrypted with Notification Protocol Asymmetric (Public) Key - var err = web3.shh.post({ - from: identity, - to: protocolKey, - topics: [discoverServerTopic], - ttl: 20 - }); - 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: - -```js -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): - -```js -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) and encrypted (using protocol key) - // acceptance message. - var err = web3.shh.post({ - from: identity, // it is absolutely important to identify the client, or your acceptance will be dropped - to: protocolKey, - topics: [acceptServerTopic], - payload: '{"server": "' + serverId + '"}', - ttl: 20 - }); - 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: - -```js -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) - } - }); -}; -``` - -### 2. Creating Chat session and registering device - -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 what creating chat session key might look like (remember we are still using `web3` object connected to `Device A` node): - -```js -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: -```js -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") - } -}; -``` - -### 3. Sharing Chat session key, and allowing notifications - -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: - -```js -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. - -```js -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 'identity "' + identity + '" not found in whisper'; -} - -// 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: '{' // see https://firebase.google.com/docs/cloud-messaging/http-server-ref - + '"notification": {' - + '"title": "status.im notification",' - + '"body": "Hello this is test notification!",' - + '"icon": "https://status.im/img/logo.png",' - + '"click_action": "https://status.im"' - + '},' - + '"to": "{{ ID }}"' // this gets replaced by device id you've registered - + '}', - 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. - -### 4. Checking subscription status and unsubscribing - -```js -var checkClientSessionTopic = '0x8745d931'; // CHECK_CLIENT_SESSION -var confirmClientSessionTopic = '0xd3202c5f'; // CONFIRM_CLIENT_SESSION -var dropClientSessionTopic = '0x3a6656bb'; // DROP_CLIENT_SESSION -var removeClientSession = function () { - var checkClientSession = function (callback) { - // before sending request, let's start waiting for response - var filter = web3.shh.filter({ - to: identity, - topics: [confirmClientSessionTopic] - }); - filter.watch(function (error, result) { - if (!error) { - // response will be in JSON, e.g. {"server": "0xdeadbeef", "key": "0xdeadbeef"} - var payload = JSON.parse(web3.toAscii(result.payload)); - console.log("Client session confirmation received: ", result, payload); - - // no need to watch for the filter any more - filter.stopWatching(); - - callback(payload) - } - }); - - // send enquiry to all servers, to learn whether we have a client session with any of them - var err = web3.shh.post({ - from: identity, - to: protocolKey, - topics: [checkClientSessionTopic], - ttl: 20 - }); - if (err !== null) { - console.log("message NOT sent") - } else { - console.log("message sent OK") - } - }; - - checkClientSession(function (payload) { - console.log("time to cleanup: ", payload.server); - console.log("session key: ", payload.key); - - setTimeout(function () { - // notify server that you want to unsubscribe - var err = web3.shh.post({ - from: identity, - to: protocolKey, - topics: [dropClientSessionTopic], - ttl: 20 - }); - if (err !== null) { - console.log("message NOT sent") - } else { - console.log("message sent OK") - } - }, 5000); // let's all other concurrent tasks to wrap up - }); -}; -``` - -# Complete Sample - -We rely on Mocha.js test, to make sure that all aspects of notification sending is covered in reproducible and easy-to-follow manner. - -In order to use the test you need: -- to clone `status-go` repo and `npm install` there (so that Mocha.js and other node modules are installed) -- to run `make` (to build the `statusd` daemon) -- to start the following: -```bash -statusd --datadir app1 --http --httpport 8645 wnode # as Device A -statusd --datadir app2 --http --httpport 8745 wnode # as Device B -statusd --datadir wnode1 wnode --notify -nodekey ./wnodekey --injectaccounts=false -shh.firebaseauth ./firebaseauthkey - -``` - -At this point, if you run `npm test`, you should see something like this: -![image](https://cloud.githubusercontent.com/assets/188194/25640962/6a09b66e-2f9a-11e7-9a33-e86f2b9a11d1.png) - -Here is full source-code of the test (the most up to date version of test is available as [status-go/static/tests/whisper.js](https://github.com/status-im/status-go/blob/develop/static/tests/whisper.js)) - -```js -context('push notifications', function () { - this.timeout(5000); - var discoveryPubKey = '0x040edb0d71a3dbe928e154fcb696ffbda359b153a90efc2b46f0043ce9f5dbe55b77b9328fd841a1db5273758624afadd5b39638d4c35b36b3a96e1a586c1b4c2a'; - var discoverServerTopic = '0x268302f3'; // DISCOVER_NOTIFICATION_SERVER - var proposeServerTopic = '0x08e3d8c0'; // PROPOSE_NOTIFICATION_SERVER - var acceptServerTopic = '0x04f7dea6'; // ACCEPT_NOTIFICATION_SERVER - var ackClientSubscriptionTopic = '0x93dafe28'; // ACK_NOTIFICATION_SERVER_SUBSCRIPTION - var sendNotificationTopic = '0x69915296'; // SEND_NOTIFICATION - var newChatSessionTopic = '0x509579a2'; // NEW_CHAT_SESSION - var ackNewChatSessionTopic = '0xd012aae8'; // ACK_NEW_CHAT_SESSION - var newDeviceRegistrationTopic = '0x14621a51'; // NEW_DEVICE_REGISTRATION - var ackDeviceRegistrationTopic = '0x424358d6'; // ACK_DEVICE_REGISTRATION - var checkClientSessionTopic = '0x8745d931'; // CHECK_CLIENT_SESSION - var confirmClientSessionTopic = '0xd3202c5f'; // CONFIRM_CLIENT_SESSION - var dropClientSessionTopic = '0x3a6656bb'; // DROP_CLIENT_SESSION - - // ensures that message had payload (which is HEX-encoded JSON) - var extractPayload = function (message) { - expect(message).to.have.property('payload'); - return JSON.parse(web3.toAscii(message.payload)); - }; - - var identity1 = ''; // pub key of device 1 - var identity2 = ''; // pub key of device 2 - var chatKeySharingTopic = makeTopic(); // topic used by device1 to send chat key to device 2 - - context('prepare devices', function () { - it('create key pair to be used as main identity on device1', function () { - var keyId = node1.shh.newKeyPair(); - assert.lengthOf(keyId, 64); - - identity1 = node1.shh.getPublicKey(keyId); - assert.lengthOf(identity1, 132); - - expect(node1.shh.hasKeyPair(identity1)).to.equal(true); - expect(node1.shh.hasKeyPair(identity2)).to.equal(false); - }); - - it('create key pair to be used as main identity on device2', function () { - var keyId = node2.shh.newKeyPair(); - assert.lengthOf(keyId, 64); - - identity2 = node2.shh.getPublicKey(keyId); - assert.lengthOf(identity1, 132); - - expect(node2.shh.hasKeyPair(identity1)).to.equal(false); - expect(node2.shh.hasKeyPair(identity2)).to.equal(true); - }); - }); - - context('run device1', function () { - var serverId = ''; // accepted/selected server id - var subscriptionKeyId = ''; // symkey provided by server, and used to configure client-server subscription - var chatKeyId = ''; // symkey provided by server, and shared among clients so that they can trigger notifications - var appChatId = ''; // chat id that identifies device1-device2 interaction session on RN app level - - - it('start discovery by sending discovery request', function () { - var message = { - type: "asym", - sig: identity1, - key: discoveryPubKey, - topic: discoverServerTopic, - ttl: 20 - }; - expect(node1.shh.post(message)).to.equal(null); - }); - - it('watch for server proposals', function (done) { - watchFilter(node1.shh.filter({ - type: "asym", - sig: discoveryPubKey, - key: identity1, - topics: [proposeServerTopic] - }), function (err, message) { - if (err) return done(err); - - // process payload - var payload = extractPayload(message); - expect(payload).to.have.property('server'); - serverId = payload.server; - - done(); - }); - }); - - it('client accepts server', function () { - var message = { - type: "asym", - sig: identity1, - key: discoveryPubKey, - topic: acceptServerTopic, - payload: '{"server": "' + serverId + '"}', - ttl: 20 - }; - expect(node1.shh.post(message)).to.equal(null); - }); - - it('watch for server ACK response and save provided subscription key', function (done) { - watchFilter(node1.shh.filter({ - type: "asym", - key: identity1, - topics: [ackClientSubscriptionTopic] - }), function (err, message) { - if (err) return done(err); - - // process payload - var payload = extractPayload(message); - expect(payload).to.have.property('server'); - expect(payload).to.have.property('key'); - - // save subscription key - subscriptionKeyId = node1.shh.addSymmetricKeyDirect(payload.key); - assert.lengthOf(subscriptionKeyId, 64); - expect(node1.shh.hasSymmetricKey(subscriptionKeyId)).to.equal(true); - - done(); - }); - }); - - it('create chat session', function () { - appChatId = makeTopic(); // globally unique chat id - var message = { - type: "sym", - sig: identity1, - key: subscriptionKeyId, - topic: newChatSessionTopic, - payload: '{"chat": "' + appChatId + '"}', - ttl: 20 - }; - expect(node1.shh.post(message)).to.equal(null); - }); - - it('watch for server to respond with chat key', function (done) { - watchFilter(node1.shh.filter({ - type: "asym", - key: identity1, - topics: [ackNewChatSessionTopic] - }), function (err, message) { - if (err) return done(err); - - // process payload - var payload = extractPayload(message); - expect(payload).to.have.property('server'); - expect(payload).to.have.property('key'); - - // save subscription key - chatKeyId = node1.shh.addSymmetricKeyDirect(payload.key); - assert.lengthOf(chatKeyId, 64); - expect(node1.shh.hasSymmetricKey(chatKeyId)).to.equal(true); - - done(); - }); - }); - - it('register device with a given chat', function (done) { - // this obtained from https://status-sandbox-c1b34.firebaseapp.com/ - var deviceId = 'ca5pRJc6L8s:APA91bHpYFtpxvXx6uOayGmnNVnktA4PEEZdquCCt3fWR5ldLzSy1A37Tsbzk5Gavlmk1d_fvHRVnK7xPAhFFl-erF7O87DnIEstW6DEyhyiKZYA4dXFh6uy323f9A3uw5hEtT_kQVhT'; - var message = { - type: "sym", - sig: identity1, - key: chatKeyId, - topic: newDeviceRegistrationTopic, - payload: '{"device": "' + deviceId + '"}', - ttl: 20 - }; - expect(node1.shh.post(message)).to.equal(null); - - // watch for server server ACK - watchFilter(node1.shh.filter({ - type: "asym", - key: identity1, - topics: [ackDeviceRegistrationTopic] - }), function (err, message) { - if (err) return done(err); - - // process payload - var payload = extractPayload(message); - expect(payload).to.have.property('server'); - - done(); - }); - }); - - it('share chat key, so that another device can send us notifications', function () { - var chatKey = node1.shh.getSymmetricKey(chatKeyId); - assert.lengthOf(chatKey, 66); - var message = { - type: "asym", - sig: identity1, - key: identity2, - topic: chatKeySharingTopic, - payload: '{"chat": "' + appChatId + '", "key": "' + chatKey + '"}', - ttl: 20 - }; - expect(node1.shh.post(message)).to.equal(null); - }); - }); - - context('run device2', function () { - var chatKeyId = ''; - - it('watch for device1 to send us chat key', function (done) { - watchFilter(node2.shh.filter({ - type: "asym", - key: identity2, - topics: [chatKeySharingTopic] - }), function (err, message) { - if (err) return done(err); - - // process payload - var payload = extractPayload(message); - expect(payload).to.have.property('chat'); - expect(payload).to.have.property('key'); - - // persist chat key - chatKeyId = node2.shh.addSymmetricKeyDirect(payload.key); - assert.lengthOf(chatKeyId, 64); - expect(node2.shh.hasSymmetricKey(chatKeyId)).to.equal(true); - - done(); - }); - }); - - it('trigger notification (from device2, on device1)', function () { - var message = { - type: "sym", - sig: identity2, - key: chatKeyId, - topic: sendNotificationTopic, - payload: '{' // see https://firebase.google.com/docs/cloud-messaging/http-server-ref - + '"notification": {' - + '"title": "status.im notification",' - + '"body": "Hello this is test notification!",' - + '"icon": "https://status.im/img/logo.png",' - + '"click_action": "https://status.im"' - + '},' - + '"to": "{{ ID }}"' // this get replaced by device id your've registered - + '}', - ttl: 20 - }; - expect(node2.shh.post(message)).to.equal(null); - }); - }); - - context('misc methods and cleanup', function () { - - it('check client session', function (done) { - // request status - var message = { - type: "asym", - sig: identity1, - key: discoveryPubKey, - topic: checkClientSessionTopic, - ttl: 20 - }; - expect(node1.shh.post(message)).to.equal(null); - - // process server's response - watchFilter(node1.shh.filter({ - type: "asym", - key: identity1, - topics: [confirmClientSessionTopic] - }), function (err, message) { - if (err) return done(err); - - // process payload - var payload = extractPayload(message); - expect(payload).to.have.property('server'); - expect(payload).to.have.property('key'); - - done(); - }); - }); - - it('remove client session', function () { - var message = { - type: "asym", - sig: identity1, - key: discoveryPubKey, - topic: dropClientSessionTopic, - ttl: 20 - }; - expect(node1.shh.post(message)).to.equal(null); - }); - }); -}); -``` - -Provided you've updated deviceId (to point to your device), once you run the test, you should get sth like this: - -![image](https://cloud.githubusercontent.com/assets/188194/24966636/34a226ea-1fb0-11e7-9b18-b3595ed108e6.png) - -# Limitations -- the main limitation of this release is the fact that all the data is in-memory, i.e. no persistent store, and once node goes down subscriptions, chats, devices - all that info is gone. This issue will be addressed with high priority (and it is actually not hard to implement) \ No newline at end of file +The next iteration of Push Notifications (v2) is [documented](https://github.com/status-im/ideas/tree/master/ideas/086-push-notif-v2) in the Ideas repository. It is currently waiting for prioritization before work can resume. \ No newline at end of file