[Fixes #4998 ] Handle decryption failures in realm

Signed-off-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
This commit is contained in:
Andrea Maria Piana 2018-07-09 09:19:40 +02:00
parent 1ad6f40f57
commit a2ee06bc9d
No known key found for this signature in database
GPG Key ID: AA6CCA6DE0E06424
10 changed files with 173 additions and 182 deletions

View File

@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Fixed ### Fixed
- Fixed mailservers connectivity issue - Fixed mailservers connectivity issue
- Clear chat action correctly clear the unread messages counter - Clear chat action correctly clear the unread messages counter
- Gracefully handle realm decryption failures by showing a pop up asking the user to reset the data
### Changed ### Changed
- Downgraded React Native to 0.53.3 for improved performance and decreased battery consumption - Downgraded React Native to 0.53.3 for improved performance and decreased battery consumption

View File

@ -1,6 +1,7 @@
(ns status-im.data-store.core (ns status-im.data-store.core
(:require [cljs.core.async :as async] (:require [cljs.core.async :as async]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[taoensso.timbre :as log]
[status-im.data-store.realm.core :as data-source] [status-im.data-store.realm.core :as data-source]
status-im.data-store.chats status-im.data-store.chats
status-im.data-store.messages status-im.data-store.messages
@ -14,11 +15,11 @@
(defn init [encryption-key] (defn init [encryption-key]
(when-not @data-source/base-realm (when-not @data-source/base-realm
(data-source/open-base-realm encryption-key)) (data-source/open-base-realm encryption-key)))
(data-source/reset-account-realm encryption-key))
(defn change-account [address new-account? encryption-key handler] (defn change-account [address encryption-key]
(data-source/change-account address new-account? encryption-key handler)) (log/debug "changing account to: " address)
(data-source/change-account address encryption-key))
(defn- perform-transactions [raw-transactions realm] (defn- perform-transactions [raw-transactions realm]
(let [success-events (keep :success-event raw-transactions) (let [success-events (keep :success-event raw-transactions)

View File

@ -11,8 +11,6 @@
[status-im.react-native.js-dependencies :as rn-dependencies] [status-im.react-native.js-dependencies :as rn-dependencies]
[status-im.utils.utils :as utils])) [status-im.utils.utils :as utils]))
(def new-account-filename "new-account")
(defn to-buffer [key] (defn to-buffer [key]
(when key (when key
(let [length (.-length key) (let [length (.-length key)
@ -31,9 +29,8 @@
[options file-name encryption-key] [options file-name encryption-key]
(log/debug "Opening realm at " file-name "...") (log/debug "Opening realm at " file-name "...")
(let [options-js (clj->js (assoc options :path file-name))] (let [options-js (clj->js (assoc options :path file-name))]
(when encryption-key
(log/debug "Using encryption key...") (log/debug "Using encryption key...")
(set! (.-encryptionKey options-js) (to-buffer encryption-key))) (set! (.-encryptionKey options-js) (to-buffer encryption-key))
(when (exists? js/window) (when (exists? js/window)
(rn-dependencies/realm. options-js)))) (rn-dependencies/realm. options-js))))
@ -62,12 +59,6 @@
(when realm (when realm
(.close realm))) (.close realm)))
(defn reset-realm
"Delete realm & open a new database using encryption key"
[file-name schemas encryption-key]
(delete-realm file-name)
(open-realm (last schemas) file-name encryption-key))
(defn- migrate-schemas (defn- migrate-schemas
"Apply migrations in sequence and open database with the last schema" "Apply migrations in sequence and open database with the last schema"
[file-name schemas encryption-key current-version] [file-name schemas encryption-key current-version]
@ -110,33 +101,9 @@
(reset! base-realm (open-migrated-realm (.-defaultPath rn-dependencies/realm) base/schemas encryption-key)) (reset! base-realm (open-migrated-realm (.-defaultPath rn-dependencies/realm) base/schemas encryption-key))
(log/debug "Created @base-realm")) (log/debug "Created @base-realm"))
(defn reset-account-realm [encryption-key] (defn change-account [address encryption-key]
(log/debug "Resetting account realm...")
(when @account-realm
(close @account-realm))
(reset! account-realm (open-migrated-realm new-account-filename account/schemas encryption-key))
(.write @account-realm #(.deleteAll @account-realm))
(log/debug "Created @account-realm"))
(defn move-file-handler [address encryption-key err handler]
(log/debug "Moved file with error: " err address)
(if err
(log/error "Error moving account realm: " (.-message err))
(reset! account-realm (open-migrated-realm address account/schemas encryption-key)))
(handler err))
(defn change-account [address new-account? encryption-key handler]
(let [path (.-path @account-realm)]
(log/debug "closing account realm: " path)
(close-account-realm) (close-account-realm)
(log/debug "is new account? " new-account?) (reset! account-realm (open-migrated-realm address account/schemas encryption-key)))
(if new-account?
(let [new-path (string/replace path new-account-filename address)]
(log/debug "Moving file " path " to " new-path)
(fs/move-file path new-path #(move-file-handler address encryption-key % handler)))
(do
(reset! account-realm (open-migrated-realm address account/schemas encryption-key))
(handler nil)))))
(declare realm-obj->clj) (declare realm-obj->clj)
@ -153,19 +120,6 @@
[schema-name :properties])))] [schema-name :properties])))]
(.create realm (name schema-name) (clj->js obj-to-save) update?)))) (.create realm (name schema-name) (clj->js obj-to-save) update?))))
(defn save
([realm schema-name obj]
(save realm schema-name obj false))
([realm schema-name obj update?]
(write realm #(create realm schema-name obj update?))))
(defn save-all
([realm schema-name objs]
(save-all realm schema-name objs false))
([realm schema-name objs update?]
(write realm (fn []
(mapv #(save realm schema-name % update?) objs)))))
(defn delete [realm obj] (defn delete [realm obj]
(.delete realm obj)) (.delete realm obj))

View File

@ -686,6 +686,12 @@
:invalid-key-content "To protect yourself, you need to create new account and erase your old data by tapping “Apply”. If you have an existing account and would like to save your seed phrase then choose “Cancel”, back it up, and restart the app. We strongly recommend creating new account because the old one is stored unencrypted." :invalid-key-content "To protect yourself, you need to create new account and erase your old data by tapping “Apply”. If you have an existing account and would like to save your seed phrase then choose “Cancel”, back it up, and restart the app. We strongly recommend creating new account because the old one is stored unencrypted."
:invalid-key-confirm "Apply" :invalid-key-confirm "Apply"
;; decryption-failed
:decryption-failed-title "We were not able to decrypt your data"
:decryption-failed-content "We were not able to decrypt your data, you might need to create new account and erase your old data by tapping “Apply”. Clicking on “Cancel”, will try again"
:decryption-failed-confirm "Apply"
;; browser ;; browser
:browser "Browser" :browser "Browser"
:enter-dapp-url "Enter a ÐApp URL" :enter-dapp-url "Enter a ÐApp URL"

View File

@ -88,28 +88,12 @@
(-> (add-account db account) (-> (add-account db account)
(assoc :dispatch [:login-account normalized-address password])))))) (assoc :dispatch [:login-account normalized-address password]))))))
(handlers/register-handler-fx (defn load-accounts [{:keys [db all-accounts]}]
:load-accounts
[(re-frame/inject-cofx :data-store/get-all-accounts)]
(fn [{:keys [db all-accounts]} _]
(let [accounts (->> all-accounts (let [accounts (->> all-accounts
(map (fn [{:keys [address] :as account}] (map (fn [{:keys [address] :as account}]
[address account])) [address account]))
(into {})) (into {}))]
;;workaround for realm bug, migrating account v4 {:db (assoc db :accounts/accounts accounts)}))
events (mapv #(when (empty? (:networks %)) [:account-update-networks (:address %)]) (vals accounts))]
(merge
{:db (assoc db :accounts/accounts accounts)}
(when-not (empty? events)
{:dispatch-n events})))))
(handlers/register-handler-fx
:account-update-networks
(fn [{{:accounts/keys [accounts] :networks/keys [networks] :as db} :db} [_ id]]
(let [current-account (get accounts id)
new-account (assoc current-account :networks networks)]
{:db (assoc-in db [:accounts/accounts id] new-account)
:data-store/base-tx [(accounts-store/save-account-tx new-account)]})))
(defn update-settings (defn update-settings
([settings cofx] (update-settings settings nil cofx)) ([settings cofx] (update-settings settings nil cofx))

View File

@ -1,6 +1,6 @@
(ns status-im.ui.screens.accounts.login.events (ns status-im.ui.screens.accounts.login.events
(:require status-im.ui.screens.accounts.login.navigation (:require status-im.ui.screens.accounts.login.navigation
[re-frame.core :refer [dispatch reg-fx]] [re-frame.core :as re-frame]
[status-im.utils.handlers :refer [register-handler-db register-handler-fx]] [status-im.utils.handlers :refer [register-handler-db register-handler-fx]]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[status-im.utils.types :refer [json->clj]] [status-im.utils.types :refer [json->clj]]
@ -12,39 +12,34 @@
;;;; FX ;;;; FX
(reg-fx ::stop-node (fn [] (status/stop-node))) (re-frame/reg-fx ::stop-node (fn [] (status/stop-node)))
(reg-fx (re-frame/reg-fx
::login ::login
(fn [[address password]] (fn [[address password]]
(status/login address password #(dispatch [:login-handler % address])))) (status/login address password #(re-frame/dispatch [:login-handler % address]))))
(reg-fx (re-frame/reg-fx
::clear-web-data ::clear-web-data
(fn [] (fn []
(status/clear-web-data))) (status/clear-web-data)))
(defn change-account [address encryption-key] (defn change-account! [address encryption-key]
(let [change-account-fn (fn [] (data-store/change-account address (data-store/change-account address encryption-key)
false (re-frame/dispatch [:change-account-handler address]))
encryption-key
#(dispatch [:change-account-handler % address])))]
(if config/stub-status-go?
(utils/set-timeout change-account-fn
300)
(change-account-fn))))
(reg-fx (defn handle-change-account [address]
;; No matter what is the keychain we use, as checks are done on decrypting base
(.. (keychain/safe-get-encryption-key)
(then (partial change-account! address))
(catch (fn [error]
;; If all else fails we fallback to showing initial error
(re-frame/dispatch [:initialize-app "" :decryption-failed])))))
(re-frame/reg-fx
::change-account ::change-account
(fn [[address]] (fn [[address]]
;; if we don't add delay when running app without status-go (handle-change-account address)))
;; "null is not an object (evaluating 'realm.schema')" error appears
(.. (keychain/get-encryption-key)
(then (partial change-account address))
(catch (fn [{:keys [error key]}]
;; no need of further error handling as already taken care
;; when starting the app
(change-account address (or key "")))))))
;;;; Handlers ;;;; Handlers
@ -154,12 +149,10 @@
(register-handler-fx (register-handler-fx
:change-account-handler :change-account-handler
(fn [{{:keys [view-id] :as db} :db} [_ error address]] (fn [{{:keys [view-id] :as db} :db} [_ address]]
(if (nil? error)
{:db (cond-> (dissoc db :accounts/login) {:db (cond-> (dissoc db :accounts/login)
(= view-id :create-account) (= view-id :create-account)
(assoc-in [:accounts/create :step] :enter-name)) (assoc-in [:accounts/create :step] :enter-name))
:dispatch [:initialize-account address :dispatch [:initialize-account address
(when (not= view-id :create-account) (when (not= view-id :create-account)
[[:navigate-to-clean :home]])]} [[:navigate-to-clean :home]])]}))
(log/debug "Error changing acount: " error))))

View File

@ -6,7 +6,7 @@
status-im.network.events status-im.network.events
[status-im.transport.handlers :as transport.handlers] [status-im.transport.handlers :as transport.handlers]
status-im.protocol.handlers status-im.protocol.handlers
status-im.ui.screens.accounts.events [status-im.ui.screens.accounts.events :as accounts.events]
status-im.ui.screens.accounts.login.events status-im.ui.screens.accounts.login.events
status-im.ui.screens.accounts.recover.events status-im.ui.screens.accounts.recover.events
[status-im.ui.screens.contacts.events :as contacts] [status-im.ui.screens.contacts.events :as contacts]
@ -139,25 +139,23 @@
(doseq [call calls] (doseq [call calls]
(http-get call)))) (http-get call))))
;; Try to decrypt the database, move on if successful otherwise go back to
;; initial state
(re-frame/reg-fx (re-frame/reg-fx
::init-store ::init-store
(fn [encryption-key] (fn [encryption-key]
(data-store/init encryption-key))) (try
(do
(defn move-to-internal-storage [config] (data-store/init encryption-key)
(status/move-to-internal-storage (re-frame/dispatch [:after-decryption]))
#(status/start-node config))) (catch js/Error error
(log/warn "Could not decrypt database" error)
(re-frame/dispatch [:initialize-app encryption-key :decryption-failed])))))
(re-frame/reg-fx (re-frame/reg-fx
:initialize-geth-fx :initialize-geth-fx
(fn [config] (fn [config]
;;TODO get rid of this, because we don't need this anymore (status/start-node (types/clj->json config))))
(status/should-move-to-internal-storage?
(fn [should-move?]
(if should-move?
(re-frame/dispatch [:request-permissions {:permissions [:read-external-storage]
:on-allowed #(move-to-internal-storage config)}])
(status/start-node (types/clj->json config)))))))
(re-frame/reg-fx (re-frame/reg-fx
::status-module-initialized-fx ::status-module-initialized-fx
@ -224,16 +222,16 @@
(fn [db [_ path v]] (fn [db [_ path v]]
(assoc-in db path v))) (assoc-in db path v)))
(handlers/register-handler-fx
:initialize-keychain
(fn [_ _]
{:get-encryption-key [:initialize-app]}))
(defn- reset-keychain [] (defn- reset-keychain []
(.. (keychain/reset) (.. (keychain/reset)
(then (then
#(re-frame/dispatch [:initialize-keychain])))) #(re-frame/dispatch [:initialize-keychain]))))
(defn- handle-reset-data []
(.. (realm/delete-realms)
(then reset-keychain)
(catch reset-keychain)))
(defn handle-invalid-key-parameters [encryption-key] (defn handle-invalid-key-parameters [encryption-key]
{:title (i18n/label :invalid-key-title) {:title (i18n/label :invalid-key-title)
:content (i18n/label :invalid-key-content) :content (i18n/label :invalid-key-content)
@ -243,45 +241,38 @@
:on-cancel #(do :on-cancel #(do
(log/warn "initializing app with invalid key") (log/warn "initializing app with invalid key")
(re-frame/dispatch [:initialize-app encryption-key])) (re-frame/dispatch [:initialize-app encryption-key]))
:on-accept (fn [] :on-accept handle-reset-data})
(.. (realm/delete-realms)
(then reset-keychain)
(catch reset-keychain)))})
(handlers/register-handler-fx (defn handle-decryption-failed-parameters [encryption-key]
:initialize-app {:title (i18n/label :decryption-failed-title)
(fn [_ [_ encryption-key error]] :content (i18n/label :decryption-failed-content)
(if (= :invalid-key error) :confirm-button-text (i18n/label :decryption-failed-confirm)
{:show-confirmation (handle-invalid-key-parameters encryption-key)} ;; On cancel we initialize the app with the same key, in case the error was
{::init-device-UUID nil ;; not related/fs error
::testfairy-alert nil :on-cancel #(do
:dispatch-n [[:initialize-db encryption-key] (log/warn "initializing app with same key after decryption failed")
[:load-accounts] (re-frame/dispatch [:initialize-app encryption-key]))
[:initialize-views] :on-accept handle-reset-data})
[:listen-to-network-status]
[:initialize-geth]]})))
(handlers/register-handler-fx (defn initialize-views [{{:accounts/keys [accounts] :as db} :db}]
:logout {:db (if (empty? accounts)
(fn [{:keys [db] :as cofx} [this-event encryption-key]] (assoc db :view-id :intro :navigation-stack (list :intro))
(if encryption-key (let [{:keys [address photo-path name]} (first (sort-by :last-sign-in > (vals accounts)))]
(let [{:transport/keys [chats]} db] (-> db
(handlers-macro/merge-fx cofx (assoc :view-id :login
{:dispatch-n [[:initialize-db encryption-key] :navigation-stack (list :login))
[:load-accounts] (update :accounts/login assoc
[:listen-to-network-status] :address address
[:navigate-to :accounts]]} :photo-path photo-path
(navigation/navigate-to-clean nil) :name name))))})
(transport/stop-whisper)))
{:get-encryption-key [this-event]})))
(defn initialize-db [encryption-key (defn initialize-db
{{:universal-links/keys [url] "Initialize db to the initial state"
[{{:universal-links/keys [url]
:keys [status-module-initialized? status-node-started? :keys [status-module-initialized? status-node-started?
network-status network peers-count peers-summary device-UUID] network-status network peers-count peers-summary device-UUID]
:or {network (get app-db :network)}} :db}] :or {network (get app-db :network)}} :db}]
{::init-store encryption-key {:db (assoc app-db
:db (assoc app-db
:contacts/contacts {} :contacts/contacts {}
:network-status network-status :network-status network-status
:peers-count (or peers-count 0) :peers-count (or peers-count 0)
@ -292,10 +283,51 @@
:universal-links/url url :universal-links/url url
:device-UUID device-UUID)}) :device-UUID device-UUID)})
;; Entrypoint, fetches the key from the keychain and initialize the app
(handlers/register-handler-fx (handlers/register-handler-fx
:initialize-db :initialize-keychain
(fn [cofx [_ encryption-key]] (fn [_ _]
(initialize-db encryption-key cofx))) {:get-encryption-key [:initialize-app]}))
;; Check the key is valid, shows options if not, otherwise continues loading
;; the database
(handlers/register-handler-fx
:initialize-app
(fn [cofx [_ encryption-key error]]
(cond
(= :invalid-key error)
{:show-confirmation (handle-invalid-key-parameters encryption-key)}
(= :decryption-failed error)
{:show-confirmation (handle-decryption-failed-parameters encryption-key)}
:else
(handlers-macro/merge-fx cofx
{::init-device-UUID nil
::init-store encryption-key
::testfairy-alert nil}
(initialize-db)))))
;; DB has been decrypted, load accounts, initialize geth, etc
(handlers/register-handler-fx
:after-decryption
[(re-frame/inject-cofx :data-store/get-all-accounts)]
(fn [cofx _]
(handlers-macro/merge-fx cofx
{:dispatch-n
[[:listen-to-network-status]
[:initialize-geth]]}
(accounts.events/load-accounts)
(initialize-views))))
(handlers/register-handler-fx
:logout
(fn [{:keys [db] :as cofx} _]
(let [{:transport/keys [chats]} db]
(handlers-macro/merge-fx cofx
{:dispatch [:initialize-keychain]}
(navigation/navigate-to-clean nil)
(transport/stop-whisper)))))
(handlers/register-handler-db (handlers/register-handler-db
:initialize-account-db :initialize-account-db
@ -344,20 +376,6 @@
(universal-links/stored-url-event cofx)] (universal-links/stored-url-event cofx)]
(seq events-after) (into events-after))})) (seq events-after) (into events-after))}))
(handlers/register-handler-fx
:initialize-views
(fn [{{:accounts/keys [accounts] :as db} :db} _]
{:db (if (empty? accounts)
(assoc db :view-id :intro :navigation-stack (list :intro))
(let [{:keys [address photo-path name]} (first (sort-by :last-sign-in > (vals accounts)))]
(-> db
(assoc :view-id :login
:navigation-stack (list :login))
(update :accounts/login assoc
:address address
:photo-path photo-path
:name name))))}))
(handlers/register-handler-fx (handlers/register-handler-fx
:initialize-geth :initialize-geth
(fn [{db :db} _] (fn [{db :db} _]

View File

@ -61,6 +61,15 @@
(handle-found res) (handle-found res)
(handle-not-found)))))) (handle-not-found))))))
(defn safe-get-encryption-key
"Return encryption key or empty string in case invalid/empty"
[]
(log/debug "initializing realm encryption key...")
(.. (get-encryption-key)
(catch (fn [{:keys [_ key]}]
(log/warn "key is invalid, continuing")
(or key "")))))
(defn reset [] (defn reset []
(log/debug "resetting key...") (log/debug "resetting key...")
(.resetGenericPassword rn/keychain)) (.resetGenericPassword rn/keychain))

View File

@ -4,7 +4,6 @@
(deftest initialize-db (deftest initialize-db
(testing "it preserves universal-links/url" (testing "it preserves universal-links/url"
(is (= "some-url" (get-in (events/initialize-db "blah" (is (= "some-url" (get-in (events/initialize-db {:db
{:db
{:universal-links/url "some-url"}}) {:universal-links/url "some-url"}})
[:db :universal-links/url]))))) [:db :universal-links/url])))))

View File

@ -50,3 +50,29 @@
(is (= :weak-key error)) (is (= :weak-key error))
(is (= weak-key (js->clj key))) (is (= weak-key (js->clj key)))
(done)))))))) (done))))))))
(deftest safe-key-is-not-valid
(async
done
(with-redefs [rn/keychain #js {:getGenericPassword (constantly (.resolve js/Promise #js {:password (key->json weak-key)}))}]
(testing "it returns a valid key"
(.. (keychain/safe-get-encryption-key)
(then (fn [k]
(is (= weak-key (js->clj k)))
(done)))
(catch (fn [err]
(is (not err))
(done))))))))
(deftest safe-key-is-nil
(async
done
(with-redefs [rn/keychain #js {:getGenericPassword (constantly (.resolve js/Promise #js {:password nil}))}]
(testing "it returns a valid key"
(.. (keychain/safe-get-encryption-key)
(then (fn [k]
(is (= "" (js->clj k)))
(done)))
(catch (fn [err]
(is (not err))
(done))))))))