Skip to content

Commit

Permalink
Anonymize PN pubkeys. Part of #6772
Browse files Browse the repository at this point in the history
  • Loading branch information
Pedro Pombeiro committed Jan 9, 2019
1 parent a814369 commit 42c72e7
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 66 deletions.
8 changes: 5 additions & 3 deletions src/status_im/chat/models/message.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -375,11 +375,13 @@
(add-own-status chat-id message-id :sending)
(send chat-id message-id wrapped-record))))

(fx/defn send-push-notification [cofx fcm-token status]
(fx/defn send-push-notification [cofx message-id fcm-token status]
(log/debug "#6772 - send-push-notification" message-id fcm-token)
(when (and fcm-token (= status :sent))
(let [payload {:from (accounts.db/current-public-key cofx)
:to (get-in cofx [:db :current-chat-id])}]
{:send-notification {:data-payload (notifications/create-notification-payload payload)
:to (get-in cofx [:db :current-chat-id])
:id message-id}]
{:send-notification {:data-payload (notifications/encode-notification-payload payload)
:tokens [fcm-token]}})))

(fx/defn update-message-status [{:keys [db]} chat-id message-id status]
Expand Down
8 changes: 4 additions & 4 deletions src/status_im/events.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -821,8 +821,8 @@

(handlers/register-handler-fx
:notifications/notification-open-event-received
(fn [cofx [_ event]]
(notifications/handle-push-notification-open cofx event)))
(fn [cofx [_ decoded-payload ctx]]
(notifications/handle-push-notification-open cofx decoded-payload ctx)))

(handlers/register-handler-fx
:notifications.callback/get-fcm-token-success
Expand All @@ -841,8 +841,8 @@

(handlers/register-handler-fx
:notifications.callback/on-message
(fn [cofx [_ from to]]
(notifications/handle-on-message cofx from to)))
(fn [cofx [_ decoded-payload]]
(notifications/handle-on-message cofx decoded-payload)))

;; hardwallet module

Expand Down
8 changes: 4 additions & 4 deletions src/status_im/notifications/background.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
(try
(when message-js
(log/debug "message-handler-fn called" (js/JSON.stringify message-js))
(let [payload (notifications/get-notification-payload message-js)]
(when payload
(log/debug "dispatching :notifications.callback/on-message to display background message" payload)
(re-frame/dispatch [:notifications.callback/on-message (:from payload) (:to payload)]))))
(let [decoded-payload (notifications/decode-notification-payload message-js)]
(when decoded-payload
(log/debug "dispatching :notifications.callback/on-message to display background message" decoded-payload)
(re-frame/dispatch [:notifications.callback/on-message decoded-payload]))))
(on-success)
(catch :default e
(log/warn "failed to handle background message" e)
Expand Down
200 changes: 146 additions & 54 deletions src/status_im/notifications/core.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
(:require [goog.object :as object]
[re-frame.core :as re-frame]
[status-im.react-native.js-dependencies :as rn]
[status-im.js-dependencies :as dependencies]
[taoensso.timbre :as log]
[status-im.i18n :as i18n]
[status-im.accounts.db :as accounts.db]
Expand All @@ -12,6 +13,10 @@
;; Work in progress namespace responsible for push notifications and interacting
;; with Firebase Cloud Messaging.

(def ^:private pn-message-id-hash-length 10)
(def ^:private pn-pubkey-hash-length 10)
(def ^:private pn-pubkey-length 132)

(when-not platform/desktop?

(def firebase (object/get rn/react-native-firebase "default")))
Expand All @@ -31,13 +36,31 @@

(defn valid-notification-payload?
[{:keys [from to] :as payload}]
(and from to))
(and from to
(or
;; is it full pubkey?
(and (= (.-length from) pn-pubkey-length)
(= (.-length to) pn-pubkey-length))
;; or is it an anonymized pubkey hash (v2 payload)?
(and (= (.-length from) pn-pubkey-hash-length)
(= (.-length to) pn-pubkey-hash-length)))))

(defn create-notification-payload
[{:keys [from to] :as payload}]
(defn sha3 [s]
(.sha3 dependencies/Web3.prototype s))

(defn anonymize-pubkey
[pubkey]
"Anonymize a public key, if needed, by hashing it and taking the first 4 bytes"
(if (= (.-length pubkey) pn-pubkey-hash-length)
pubkey
(apply str (take pn-pubkey-hash-length (sha3 pubkey)))))

(defn encode-notification-payload
[{:keys [from to id] :as payload}]
(if (valid-notification-payload? payload)
{:msg (js/JSON.stringify #js {:from from
:to to})}
{:msg-v2 (js/JSON.stringify #js {:from (anonymize-pubkey from)
:to (anonymize-pubkey to)
:id (apply str (take pn-message-id-hash-length id))})}
(throw (str "Invalid push notification payload" payload))))

(when platform/desktop?
Expand All @@ -51,30 +74,73 @@
(def group-id "im.status.ethereum.MESSAGE")
(def icon "ic_stat_status_notification")

(defn get-notification-payload [message-js]
(defn lookup-contact-pubkey-from-hash
[{:keys [db] :as cofx} contact-pubkey-or-hash]
"Tries to deanonymize a given contact pubkey hash by looking up the full pubkey"
"(if db is unlocked) in :contacts/contacts."
"Returns original value if not a hash (e.g. already a public key)."
(if (and contact-pubkey-or-hash
(= (.-length contact-pubkey-or-hash) pn-pubkey-hash-length))
;; Do easy (and most common) lookup first, the current account public key
(let [current-account-pubkey (get-in db [:account/account :public-key])]
(if (= (anonymize-pubkey current-account-pubkey) contact-pubkey-or-hash)
current-account-pubkey
(if (accounts.db/logged-in? cofx)
;; TODO: for simplicity we're doing a linear lookup of the contacts,
;; but we might want to build a map of hashed pubkeys to pubkeys
;; for this purpose
(:public-key (first
(filter #(= (anonymize-pubkey (:public-key %)) contact-pubkey-or-hash)
(vals (:contacts/contacts db)))))
(log/warn "failed to lookup contact from hash, not logged in"))))
contact-pubkey-or-hash))

(defn parse-notification-v1-payload [msg-json]
(let [msg (js/JSON.parse msg-json)]
{:from (object/get msg "from")
:to (object/get msg "to")}))

(defn parse-notification-v2-payload [msg-v2-json]
(let [msg (js/JSON.parse msg-v2-json)]
{:from (object/get msg "from")
:to (object/get msg "to")
:id (object/get msg "id")}))

(defn decode-notification-payload [message-js]
;; message-js.-data is Notification.data():
;; https://github.com/invertase/react-native-firebase/blob/adcbeac3d11585dd63922ef178ff6fd886d5aa9b/src/modules/notifications/Notification.js#L79
(let [data (.. message-js -data)
msg-json (object/get data "msg")]
(let [data-js (.. message-js -data)
msg-v2-json (object/get data-js "msg-v2")]
(try
(let [msg (js/JSON.parse msg-json)
from (object/get msg "from")
to (object/get msg "to")
payload {:from from
:to to}]
(let [payload (if msg-v2-json
(parse-notification-v2-payload msg-v2-json)
(parse-notification-v1-payload (object/get data-js "msg")))]
(if (valid-notification-payload? payload)
payload
(log/warn "failed to retrieve notification payload from" (js/JSON.stringify data))))
(catch :default _
(log/debug (str "Failed to parse " msg-json))))))
(log/warn "failed to retrieve notification payload from" (js/JSON.stringify data-js))))
(catch :default e
(log/debug "failed to parse" (js/JSON.stringify data-js) "exception:" e)))))

(defn display-notification [{:keys [title body from to]}]
(let [notification (firebase.notifications.Notification.)]
(defn rehydrate-payload
[cofx {:keys [from to id] :as decoded-payload}]
"Takes a payload with hashed pubkeys and returns a payload with the real (matched) pubkeys"
(if (accounts.db/logged-in? cofx)
{:from (lookup-contact-pubkey-from-hash cofx from)
:to (lookup-contact-pubkey-from-hash cofx to)
;; TODO: Rehydrate message id
:id id}
decoded-payload))

(defn display-notification [{:keys [title body decoded-payload]}]
(let [notification (firebase.notifications.Notification.
nil (.notifications firebase))
msg-id (:id decoded-payload)]
(when msg-id
(.. notification (setNotificationId (str "hash:" msg-id)))) ;; We must prefix the notification ID, otherwise it will cause a crash in iOS
(.. notification
(setTitle title)
(setBody body)
(setData (clj->js (create-notification-payload {:from from
:to to})))
(setData (clj->js (encode-notification-payload decoded-payload)))
(setSound sound-name))
(when platform/android?
(.. notification
Expand Down Expand Up @@ -111,38 +177,58 @@
(then #(log/debug "Notification channel created:" channel-id)
#(log/error "Notification channel creation error:" channel-id %)))))

(defn- show-notification?
"Ignore push notifications from unknown contacts or removed chats"
[{:keys [db] :as cofx} {:keys [from] :as rehydrated-payload}]
(and (valid-notification-payload? rehydrated-payload)
(accounts.db/logged-in? cofx)
(some #(= (:public-key %) from)
(vals (:contacts/contacts db)))
(some #(= (:chat-id %) from)
(vals (:chats db)))))

(fx/defn handle-on-message
[cofx from to]
(let [view-id (get-in cofx [:db :view-id])
current-chat-id (get-in cofx [:db :current-chat-id])
app-state (get-in cofx [:db :app-state])]
(log/debug "handle-on-message" "app-state:" app-state "view-id:" view-id "current-chat-id:" current-chat-id "from:" from "to:" to)
[{:keys [db] :as cofx} decoded-payload]
(let [view-id (:view-id db)
current-chat-id (:current-chat-id db)
app-state (:app-state db)
rehydrated-payload (rehydrate-payload cofx decoded-payload)
from (:from rehydrated-payload)]
(log/debug "handle-on-message" "app-state:" app-state "view-id:"
view-id "current-chat-id:" current-chat-id "from:" from)
(when-not (and (= app-state "active")
(= :chat view-id)
(= current-chat-id from))
{:notifications/display-notification {:title (i18n/label :notifications-new-message-title)
:body (i18n/label :notifications-new-message-body)
:to to
:from from}})))
(when (show-notification? cofx rehydrated-payload)
{:notifications/display-notification {:title (i18n/label :notifications-new-message-title)
:body (i18n/label :notifications-new-message-body)
:decoded-payload rehydrated-payload}}))))

(fx/defn handle-push-notification-open
[{:keys [db] :as cofx} {:keys [from to stored?] :as event}]
[{:keys [db] :as cofx} decoded-payload {:keys [stored?] :as ctx}]
(let [current-public-key (accounts.db/current-public-key cofx)
nav-opts (when stored? {:navigation-reset? true})]
nav-opts (when stored? {:navigation-reset? true})
rehydrated-payload (rehydrate-payload cofx decoded-payload)
from (:from rehydrated-payload)
to (:to rehydrated-payload)]
(log/debug "handle-push-notification-open"
"current-public-key:" current-public-key
"rehydrated-payload:" rehydrated-payload "stored?:" stored?)
(if (= to current-public-key)
(fx/merge cofx
{:db (update db :push-notifications/stored dissoc to)}
(chat-model/navigate-to-chat from nav-opts))
{:db (assoc-in db [:push-notifications/stored to] from)})))
{:db (assoc-in db [:push-notifications/stored to] (js/JSON.stringify (clj->js rehydrated-payload)))})))

(defn handle-notification-open-event [event] ;; https://github.com/invertase/react-native-firebase/blob/adcbeac3d11585dd63922ef178ff6fd886d5aa9b/src/modules/notifications/Notification.js#L13
(let [payload (get-notification-payload (.. event -notification))]
(when payload
(re-frame/dispatch [:notifications/notification-open-event-received payload]))))
(let [decoded-payload (decode-notification-payload (.. event -notification))]
(when decoded-payload
(re-frame/dispatch [:notifications/notification-open-event-received decoded-payload nil]))))

(defn handle-initial-push-notification
"This method handles pending push notifications. It is only needed to handle PNs from legacy clients (which use firebase.notifications API)"
[]
(defn handle-initial-push-notification []
"This method handles pending push notifications."
"It is only needed to handle PNs from legacy clients"
"(which use firebase.notifications API)"
(.. firebase
notifications
getInitialNotification
Expand All @@ -165,20 +251,20 @@
(.onNotification (.notifications firebase)
(fn [message-js]
(log/debug "handle-on-notification-callback called")
(let [payload (get-notification-payload message-js)]
(log/debug "handle-on-notification-callback payload:" payload)
(when payload
(re-frame/dispatch [:notifications.callback/on-message (:from payload) (:to payload)]))))))
(let [decoded-payload (decode-notification-payload message-js)]
(log/debug "handle-on-notification-callback payload:" decoded-payload)
(when decoded-payload
(re-frame/dispatch [:notifications.callback/on-message decoded-payload]))))))

(defn setup-on-message-callback []
(log/debug "calling onMessage")
(.onMessage (.messaging firebase)
(fn [message-js]
(log/debug "handle-on-message-callback called")
(let [payload (get-notification-payload message-js)]
(log/debug "handle-on-message-callback payload:" payload)
(when payload
(re-frame/dispatch [:notifications.callback/on-message (:from payload) (:to payload)]))))))
(let [decoded-payload (decode-notification-payload message-js)]
(log/debug "handle-on-message-callback decoded-payload:" decoded-payload)
(when decoded-payload
(re-frame/dispatch [:notifications.callback/on-message decoded-payload]))))))

(defn setup-on-notification-opened-callback []
(.. firebase
Expand All @@ -196,17 +282,23 @@
(fx/defn process-stored-event [cofx address stored-pns]
(when-not platform/desktop?
(if (accounts.db/logged-in? cofx)
(let [current-account (get-in cofx [:db :account/account])
current-address (:address current-account)
to (:public-key current-account)
from (get stored-pns to)]
(log/debug "process-stored-event" "address" address "from" from "to" to)
(let [current-account (get-in cofx [:db :account/account])
current-address (:address current-account)
current-account-pubkey (:public-key current-account)
stored-pn-val-json (or (get stored-pns current-account-pubkey)
(get stored-pns (anonymize-pubkey current-account-pubkey)))
stored-pn-payload (if (= (first stored-pn-val-json) \{)
(js->clj (js/JSON.parse stored-pn-val-json) :keywordize-keys true)
{:from stored-pn-val-json
:to current-account-pubkey})
from (lookup-contact-pubkey-from-hash cofx (:from stored-pn-payload))
to (lookup-contact-pubkey-from-hash cofx (:to stored-pn-payload))]
(when (and from
(= address current-address))
(log/debug "process-stored-event" "address" address "from" from "to" to)
(handle-push-notification-open cofx
{:from from
:to to
:stored? true})))
stored-pn-payload
{:stored? true})))
(log/error "process-stored-event called without user being logged in!"))))

(re-frame/reg-fx
Expand Down
2 changes: 1 addition & 1 deletion src/status_im/transport/message/core.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
(fx/merge cofx
(remove-hash envelope-hash)
(check-confirmations status chat-id message-id)
(models.message/send-push-notification fcm-token status)))))))
(models.message/send-push-notification message-id fcm-token status)))))))

(fx/defn set-contact-message-envelope-hash
[{:keys [db] :as cofx} chat-id envelope-hash]
Expand Down

0 comments on commit 42c72e7

Please sign in to comment.