From b56fd2e29fae97c47a8274449adaa70526665e3b Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Thu, 27 Dec 2018 13:12:54 +0100 Subject: [PATCH] Show popup when device is not targeted. When someone is sending a pfs message to us but did not include our own device, a pop up is shown propmting the user to connect with the user. The reason for receiving messages that are not targeting our devices are various: 1) The account was just recovered (which means it is a new installation id) 2) More than 3 devices are in use (we only keep max 3 devices in sync) 3) The sender has used an old bundle which does not include the current device Eventually we will reduce the likelihood of this scenario happening, but we can't dismiss it completely. It's only enabled when PFS is enabled. Signed-off-by: Andrea Maria Piana --- src/status_im/contact_recovery/core.cljs | 63 ++++++++++++ .../data_store/contact_recovery.cljs | 14 +++ .../schemas/account/contact_recovery.cljs | 7 ++ .../realm/schemas/account/core.cljs | 18 ++++ src/status_im/events.cljs | 23 +++++ src/status_im/signals/core.cljs | 2 + src/status_im/transport/message/protocol.cljs | 96 +++++++++---------- src/status_im/ui/screens/db.cljs | 1 + .../status_im/test/contact-recovery/core.cljs | 38 ++++++++ test/cljs/status_im/test/runner.cljs | 2 + translations/en.json | 2 + 11 files changed, 215 insertions(+), 51 deletions(-) create mode 100644 src/status_im/contact_recovery/core.cljs create mode 100644 src/status_im/data_store/contact_recovery.cljs create mode 100644 src/status_im/data_store/realm/schemas/account/contact_recovery.cljs create mode 100644 test/cljs/status_im/test/contact-recovery/core.cljs diff --git a/src/status_im/contact_recovery/core.cljs b/src/status_im/contact_recovery/core.cljs new file mode 100644 index 0000000000..4b2d1747e7 --- /dev/null +++ b/src/status_im/contact_recovery/core.cljs @@ -0,0 +1,63 @@ +(ns status-im.contact-recovery.core + (:require + [status-im.i18n :as i18n] + [re-frame.core :as re-frame] + [status-im.data-store.contact-recovery :as data-store.contact-recovery] + [status-im.utils.fx :as fx] + [status-im.accounts.db :as accounts.db] + [status-im.contact.core :as models.contact])) + +(defn prompt-dismissed! [public-key] + (re-frame/dispatch [:contact-recovery.ui/prompt-dismissed public-key])) + +(defn prompt-accepted! [public-key] + (re-frame/dispatch [:contact-recovery.ui/prompt-accepted public-key])) + +(defn show-contact-recovery-fx + "Check that a pop up for that given user is not already shown, if not proceed fetching from the db whether we should be showing it" + [{:keys [db] :as cofx} public-key] + (let [my-public-key (accounts.db/current-public-key cofx) + pfs? (get-in db [:account/account :settings :pfs?])] + (when (and (not= public-key my-public-key) + pfs? + (not (get-in db [:contact-recovery/pop-up public-key]))) + {:db (update db :contact-recovery/pop-up conj public-key) + :contact-recovery/show-contact-recovery-message public-key}))) + +(fx/defn prompt-dismissed [{:keys [db]} public-key] + {:db (update db :contact-recovery/pop-up disj public-key)}) + +(defn show-contact-recovery-message? [public-key] + (not (data-store.contact-recovery/get-contact-recovery-by-id public-key))) + +(defn show-contact-recovery-message-fx [public-key] + (when (show-contact-recovery-message? public-key) + (re-frame/dispatch [:contact-recovery.callback/show-contact-recovery-message public-key]))) + +(re-frame/reg-fx + :contact-recovery/show-contact-recovery-message + show-contact-recovery-message-fx) + +(fx/defn save-contact-recovery [{:keys [now]} public-key] + {:data-store/tx [(data-store.contact-recovery/save-contact-recovery-tx {:timestamp now + :id public-key})]}) + +(fx/defn prompt-accepted [cofx public-key] + (fx/merge + cofx + (prompt-dismissed public-key) + (save-contact-recovery public-key))) + +(fx/defn show-contact-recovery-message [{:keys [db] :as cofx} public-key] + (let [pfs? (get-in db [:account/account :settings :pfs?]) + contact (models.contact/build-contact cofx public-key) + popup {:ui/show-confirmation {:title (i18n/label :t/contact-recovery-title {:name (:name contact)}) + :content (i18n/label :t/contact-recovery-content {:name (:name contact)}) + :confirm-button-text (i18n/label :t/add-to-contacts) + :cancel-button-text (i18n/label :t/cancel) + :on-cancel #(prompt-dismissed! public-key) + :on-accept #(prompt-accepted! public-key)}}] + + (when pfs? + (fx/merge cofx + popup)))) diff --git a/src/status_im/data_store/contact_recovery.cljs b/src/status_im/data_store/contact_recovery.cljs new file mode 100644 index 0000000000..743b69ec3b --- /dev/null +++ b/src/status_im/data_store/contact_recovery.cljs @@ -0,0 +1,14 @@ +(ns status-im.data-store.contact-recovery + (:require [status-im.data-store.realm.core :as core])) + +(defn get-contact-recovery-by-id [public-key] + (core/single (core/get-by-field @core/account-realm :contact-recovery :id public-key))) + +(defn save-contact-recovery-tx + "Returns tx function for saving a contact-recovery" + [contact-recovery] + (fn [realm] + (core/create realm + :contact-recovery + contact-recovery + true))) diff --git a/src/status_im/data_store/realm/schemas/account/contact_recovery.cljs b/src/status_im/data_store/realm/schemas/account/contact_recovery.cljs new file mode 100644 index 0000000000..b31546c10b --- /dev/null +++ b/src/status_im/data_store/realm/schemas/account/contact_recovery.cljs @@ -0,0 +1,7 @@ +(ns status-im.data-store.realm.schemas.account.contact-recovery) + +(def v1 {:name :contact-recovery + :primaryKey :id + :properties {:id :string + :timestamp {:type :int + :optional true}}}) diff --git a/src/status_im/data_store/realm/schemas/account/core.cljs b/src/status_im/data_store/realm/schemas/account/core.cljs index de37a794c6..f008dddd55 100644 --- a/src/status_im/data_store/realm/schemas/account/core.cljs +++ b/src/status_im/data_store/realm/schemas/account/core.cljs @@ -13,6 +13,7 @@ [status-im.data-store.realm.schemas.account.request :as request] [status-im.data-store.realm.schemas.account.membership-update :as membership-update] [status-im.data-store.realm.schemas.account.installation :as installation] + [status-im.data-store.realm.schemas.account.contact-recovery :as contact-recovery] [status-im.data-store.realm.schemas.account.migrations :as migrations] [taoensso.timbre :as log])) @@ -343,6 +344,20 @@ browser/v8 dapp-permissions/v9]) +(def v32 [chat/v13 + transport/v7 + contact/v3 + message/v9 + mailserver/v11 + mailserver-topic/v1 + user-status/v2 + membership-update/v1 + installation/v2 + local-storage/v1 + browser/v8 + dapp-permissions/v9 + contact-recovery/v1]) + ;; put schemas ordered by version (def schemas [{:schema v1 :schemaVersion 1 @@ -436,4 +451,7 @@ :migration migrations/v30} {:schema v31 :schemaVersion 31 + :migration (constantly nil)} + {:schema v32 + :schemaVersion 32 :migration (constantly nil)}]) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index 1a83b1c23f..937bd0a611 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -16,6 +16,7 @@ [status-im.chat.models.loading :as chat.loading] [status-im.chat.models.message :as chat.message] [status-im.contact.core :as contact] + [status-im.contact-recovery.core :as contact-recovery] [status-im.data-store.core :as data-store] [status-im.extensions.core :as extensions] [status-im.extensions.registry :as extensions.registry] @@ -1404,3 +1405,25 @@ :pairing.callback/disable-installation-success (fn [cofx [_ installation-id]] (pairing/disable cofx installation-id))) + +;; Contact recovery module + +(handlers/register-handler-fx + :contact-recovery.ui/prompt-accepted + [(re-frame/inject-cofx :random-id-generator)] + (fn [cofx [_ public-key]] + (fx/merge + cofx + (contact/add-contact public-key) + (contact-recovery/prompt-accepted public-key)))) + +(handlers/register-handler-fx + :contact-recovery.ui/prompt-dismissed + (fn [cofx [_ public-key]] + (contact-recovery/prompt-dismissed cofx public-key))) + +(handlers/register-handler-fx + :contact-recovery.callback/show-contact-recovery-message + [(re-frame/inject-cofx :random-id-generator)] + (fn [cofx [_ public-key]] + (contact-recovery/show-contact-recovery-message cofx public-key))) diff --git a/src/status_im/signals/core.cljs b/src/status_im/signals/core.cljs index 92aecd898a..9aadd0d1bf 100644 --- a/src/status_im/signals/core.cljs +++ b/src/status_im/signals/core.cljs @@ -4,6 +4,7 @@ [status-im.init.core :as init] [status-im.node.core :as node] [status-im.pairing.core :as pairing] + [status-im.contact-recovery.core :as contact-recovery] [status-im.mailserver.core :as mailserver] [status-im.transport.message.core :as transport.message] [status-im.utils.fx :as fx] @@ -75,5 +76,6 @@ "mailserver.request.completed" (mailserver/handle-request-completed cofx event) "mailserver.request.expired" (when (accounts.db/logged-in? cofx) (mailserver/resend-request cofx {:request-id (:hash event)})) + "messages.decrypt.failed" (contact-recovery/show-contact-recovery-fx cofx (:sender event)) "discovery.summary" (summary cofx event) (log/debug "Event " type " not handled")))) diff --git a/src/status_im/transport/message/protocol.cljs b/src/status_im/transport/message/protocol.cljs index 1266be424b..2cc9a67df1 100644 --- a/src/status_im/transport/message/protocol.cljs +++ b/src/status_im/transport/message/protocol.cljs @@ -35,19 +35,36 @@ :resend? resend? :now now}))}) +(defn send-public-message + "Sends the payload to topic" + [{:keys [db] :as cofx} chat-id success-event payload] + (let [{:keys [web3]} db] + {:shh/send-public-message [{:web3 web3 + :success-event success-event + :src (accounts.db/current-public-key cofx) + :chat chat-id + :payload payload}]})) + (fx/defn send-with-sym-key "Sends the payload using symetric key and topic from db (looked up by `chat-id`)" [{:keys [db] :as cofx} {:keys [payload chat-id success-event]}] ;; we assume that the chat contains the contact public-key (let [{:keys [web3]} db - {:keys [sym-key-id topic]} (get-in db [:transport/chats chat-id])] - {:shh/post [{:web3 web3 - :success-event success-event - :message (merge {:sig (accounts.db/current-public-key cofx) - :symKeyID sym-key-id - :payload payload - :topic topic} - whisper-opts)}]})) + {:keys [sym-key-id topic]} (get-in db [:transport/chats chat-id]) + pfs? (get-in db [:account/account :settings :pfs?])] + (if pfs? + (send-public-message + cofx + chat-id + success-event + payload) + {:shh/post [{:web3 web3 + :success-event success-event + :message (merge {:sig (accounts.db/current-public-key cofx) + :symKeyID sym-key-id + :payload payload + :topic topic} + whisper-opts)}]}))) (fx/defn send-direct-message "Sends the payload using to dst" @@ -59,33 +76,28 @@ :dst dst :payload payload}]})) -(defn send-public-message - "Sends the payload to topic" - [{:keys [db] :as cofx} chat-id success-event payload] - (let [{:keys [web3]} db] - {:shh/send-public-message [{:web3 web3 - :success-event success-event - :src (accounts.db/current-public-key cofx) - :chat chat-id - :payload payload}]})) - (fx/defn send-with-pubkey "Sends the payload using asymetric key (account `:public-key` in db) and fixed discovery topic" [{:keys [db] :as cofx} {:keys [payload chat-id success-event]}] (let [{:keys [web3]} db] - {:shh/post [{:web3 web3 - :success-event success-event - :message (merge {:sig (accounts.db/current-public-key cofx) - :pubKey chat-id - :payload payload - :topic (transport.utils/get-topic constants/contact-discovery)} - whisper-opts)}]})) + (let [pfs? (get-in db [:account/account :settings :pfs?])] + (if pfs? + (send-direct-message cofx + chat-id + success-event + payload) + {:shh/post [{:web3 web3 + :success-event success-event + :message (merge {:sig (accounts.db/current-public-key cofx) + :pubKey chat-id + :payload payload + :topic (transport.utils/get-topic constants/contact-discovery)} + whisper-opts)}]})))) (defrecord Message [content content-type message-type clock-value timestamp] StatusMessage (send [this chat-id {:keys [message-id] :as cofx}] (let [dev-mode? (get-in cofx [:db :account/account :dev-mode?]) - pfs? (get-in cofx [:db :account/account :settings :pfs?]) current-public-key (accounts.db/current-public-key cofx) params {:chat-id chat-id :payload this @@ -95,25 +107,13 @@ message-type]}] (case message-type :public-group-user-message - (if pfs? - (send-public-message - cofx - chat-id - (:success-event params) - this) - (send-with-sym-key cofx params)) + (send-with-sym-key cofx params) :user-message - (if pfs? - (send-direct-message - cofx - chat-id - (:success-event params) - this) - (fx/merge cofx - #(when (config/pairing-enabled? dev-mode?) - (send-direct-message % current-public-key nil this)) - (send-with-pubkey params)))))) + (fx/merge cofx + #(when (config/pairing-enabled? dev-mode?) + (send-direct-message % current-public-key nil this)) + (send-with-pubkey params))))) (receive [this chat-id signature _ cofx] {:chat-received-message/add-fx [(assoc (into {} this) @@ -134,14 +134,8 @@ (defrecord MessagesSeen [message-ids] StatusMessage (send [this chat-id cofx] - (let [pfs? (get-in cofx [:db :account/account :settings :pfs?])] - (if pfs? - (send-direct-message cofx - chat-id - nil - this) - (send-with-pubkey cofx {:chat-id chat-id - :payload this})))) + (send-with-pubkey cofx {:chat-id chat-id + :payload this})) (receive [this chat-id signature _ cofx] (chat/receive-seen cofx chat-id signature this)) (validate [this] diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index dee8cb0e0e..a722efd3f6 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -24,6 +24,7 @@ :navigation-stack '() :contacts/contacts {} :pairing/installations {} + :contact-recovery/pop-up #{} :qr-codes {} :group/selected-contacts #{} :chats {} diff --git a/test/cljs/status_im/test/contact-recovery/core.cljs b/test/cljs/status_im/test/contact-recovery/core.cljs new file mode 100644 index 0000000000..76e1c9667a --- /dev/null +++ b/test/cljs/status_im/test/contact-recovery/core.cljs @@ -0,0 +1,38 @@ +(ns status-im.test.contact-recovery.core + (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.contact-recovery.core :as contact-recovery])) + +(deftest show-contact-recovery-fx + (let [public-key "pk"] + (testing "pfs is not enabled" + (testing "no pop up is displayed" + (let [actual (contact-recovery/show-contact-recovery-fx {:db {:contact-recovery/pop-up #{}}} public-key)] + (testing "it does nothing" + (is (not (:db actual))) + (is (not (:contact-recovery/show-contact-recovery-message actual))))))) + (testing "no pop up is displayed" + (let [cofx {:db {:contact-recovery/pop-up #{} + :account/account {:settings {:pfs? true}}}} + actual (contact-recovery/show-contact-recovery-fx cofx public-key)] + (testing "it sets the pop up as displayed" + (is (get-in actual [:db :contact-recovery/pop-up public-key]))) + (testing "it adds an fx for fetching the contact" + (is (= public-key (:contact-recovery/show-contact-recovery-message actual)))))) + (testing "pop up is already displayed" + (let [actual (contact-recovery/show-contact-recovery-fx {:db {:contact-recovery/pop-up #{public-key}}} public-key)] + (testing "it does nothing" + (is (not (:db actual))) + (is (not (:contact-recovery/show-contact-recovery-message actual)))))))) + +(deftest show-contact-recovery-message + (let [public-key "pk"] + (testing "pfs is enabled" + (let [cofx {:db {:account/account {:settings {:pfs? true}}}} + actual (contact-recovery/show-contact-recovery-message cofx public-key)] + (testing "it shows a pop up" + (is (:ui/show-confirmation actual))))) + (testing "pfs is not enabled" + (let [cofx {:db {:account/account {:settings {}}}} + actual (contact-recovery/show-contact-recovery-message cofx public-key)] + (testing "it shows a pop up" + (is (not (:ui/show-confirmation actual)))))))) diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index 4ff314dd0b..c6505102fd 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -56,6 +56,7 @@ [status-im.test.utils.fx] [status-im.test.accounts.recover.core] [status-im.test.hardwallet.core] + [status-im.test.contact-recovery.core] [status-im.test.ui.screens.currency-settings.models] [status-im.test.ui.screens.wallet.db])) @@ -124,5 +125,6 @@ 'status-im.test.ui.screens.currency-settings.models 'status-im.test.ui.screens.wallet.db 'status-im.test.browser.core + 'status-im.test.contact-recovery.core 'status-im.test.extensions.ethereum 'status-im.test.browser.permissions) diff --git a/translations/en.json b/translations/en.json index a11574344b..263a58de6b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -15,6 +15,8 @@ "sync-all-devices": "Sync all devices", "syncing-devices": "Syncing...", "paired-devices": "Paired devices", + "contact-recovery-title": "{{name}} has sent you a message", + "contact-recovery-content": "{{name}} has sent you a message but did not include this device.\nThis might happen if you have more than 3 devices, you haven't paired your devices correctly or you just recovered your account.\nPlease make sure your devices are paired correctly and click Add to contacts to notify the user of this device.", "pairing-maximum-number-reached-title": "Max number of devices reached", "pairing-maximum-number-reached-content": "Please disable one of your devices before enabling a new one.", "pairing-new-installation-detected-title": "New device detected",