From c506521778161e10d6d154dfc2ed975426711c82 Mon Sep 17 00:00:00 2001 From: Roman Volosovskyi Date: Fri, 23 Nov 2018 22:10:01 +0200 Subject: [PATCH] [slow sign in] Better handling of migration failures and db encryption problems. Migration failures are handled separately from other errors which might appear during opening account's realm DB. In case if user chooses to erase the account's database, only this database will be removed and other accounts will not be touched. --- src/status_im/accounts/login/core.cljs | 2 +- src/status_im/data_store/core.cljs | 14 ++- src/status_im/data_store/realm/core.cljs | 111 ++++++++++++++--------- src/status_im/events.cljs | 10 ++ src/status_im/init/core.cljs | 53 +++++++++-- translations/en.json | 7 +- 6 files changed, 142 insertions(+), 55 deletions(-) diff --git a/src/status_im/accounts/login/core.cljs b/src/status_im/accounts/login/core.cljs index 3f363624f0..d2e5eb35fc 100644 --- a/src/status_im/accounts/login/core.cljs +++ b/src/status_im/accounts/login/core.cljs @@ -33,7 +33,7 @@ (catch (fn [error] (log/warn "Could not change account" error) ;; If all else fails we fallback to showing initial error - (re-frame/dispatch [:init.callback/account-change-error (str error)]))))) + (re-frame/dispatch [:init.callback/account-change-error error]))))) ;;;; Handlers (fx/defn login [cofx] diff --git a/src/status_im/data_store/core.cljs b/src/status_im/data_store/core.cljs index 10b317bb7e..819921b8a5 100644 --- a/src/status_im/data_store/core.cljs +++ b/src/status_im/data_store/core.cljs @@ -27,7 +27,19 @@ (defn change-account [address password encryption-key] (log/debug "changing account to: " address) - (data-source/change-account address password encryption-key)) + (.. + (js/Promise. + (fn [on-success on-error] + (try + (data-source/close-account-realm) + (on-success) + (catch :default e + (on-error {:message (str e) + :error :closing-account-failed}))))) + (then + #(data-source/check-db-encryption address password encryption-key)) + (then + #(data-source/open-account address password encryption-key)))) (defn- perform-transactions [raw-transactions realm] (let [success-events (keep :success-event raw-transactions) diff --git a/src/status_im/data_store/realm/core.cljs b/src/status_im/data_store/realm/core.cljs index 736a6e7a98..7db27f1fd7 100644 --- a/src/status_im/data_store/realm/core.cljs +++ b/src/status_im/data_store/realm/core.cljs @@ -30,6 +30,17 @@ (.schemaVersion rn-dependencies/realm file-name (to-buffer encryption-key)) (.schemaVersion rn-dependencies/realm file-name))) +(defn encrypted-realm-version-promise + [file-name encryption-key] + (js/Promise. + (fn [on-success on-error] + (try + (encrypted-realm-version file-name encryption-key) + (on-success) + (catch :default e + (on-error {:message (str e) + :error :decryption-failed})))))) + (defn open-realm [options file-name encryption-key] (log/debug "Opening realm at " file-name "...") @@ -73,6 +84,15 @@ (log/warn "realm: deleting all realms") (fs/unlink realm-dir)) +(defn delete-account-realm + [address] + (log/warn "realm: deleting account db " (utils.ethereum/sha3 address)) + (let [file (str accounts-realm-dir (utils.ethereum/sha3 address))] + (.. (fs/unlink file) + (then #(fs/unlink (str file ".lock"))) + (then #(fs/unlink (str file ".management"))) + (then #(fs/unlink (str file ".note")))))) + (defn ensure-directories [] (.. (fs/mkdir realm-dir) @@ -104,11 +124,12 @@ (defn- migrate-schemas "Apply migrations in sequence and open database with the last schema" [file-name schemas encryption-key current-version] - (log/info "migrate schemas") - (doseq [schema schemas - :when (> (:schemaVersion schema) current-version) - :let [migrated-realm (open-realm schema file-name encryption-key)]] - (close migrated-realm)) + (log/info "migrate schemas" current-version) + (when (pos? current-version) + (doseq [schema schemas + :when (> (:schemaVersion schema) current-version) + :let [migrated-realm (open-realm schema file-name encryption-key)]] + (close migrated-realm))) (open-realm (last schemas) file-name encryption-key)) (defn keccak512-array [key] @@ -136,10 +157,6 @@ file-name encryption-key))) -(defn open-migrated-realm - [file-name schemas encryption-key] - (migrate-realm file-name schemas encryption-key)) - (defn- index-entity-schemas [all-schemas] (into {} (map (juxt :name identity)) (-> all-schemas last :schema))) @@ -152,6 +169,7 @@ (def realm-queue (utils.async/task-queue 2000)) (defn close-account-realm [] + (log/debug "closing account realm") (close @account-realm) (reset! account-realm nil)) @@ -159,7 +177,7 @@ (log/debug "Opening base realm... (first run)") (when @base-realm (close @base-realm)) - (reset! base-realm (open-migrated-realm base-realm-path base/schemas encryption-key)) + (reset! base-realm (migrate-realm base-realm-path base/schemas encryption-key)) (log/debug "Created @base-realm")) (defn re-encrypt-realm @@ -172,11 +190,12 @@ (catch (fn [e] (let [message (str "can't move old database " (str e) " " file-name)] (log/debug message) - (on-error {:error message})))) + (on-error {:message message + :error :removing-old-db-failed})))) (then (fn [] - (let [old-account-db (open-migrated-realm old-file-name - account/schemas - old-key)] + (let [old-account-db (migrate-realm old-file-name + account/schemas + old-key)] (log/info "copy old database") (.writeCopyTo old-account-db file-name (to-buffer new-key)) (log/info "old database copied") @@ -190,38 +209,48 @@ (catch :default _)) (let [message (str "something went wrong " (str e) " " file-name)] (log/info message) - (on-error {:error message}))))))) + (on-error {:error :write-copy-to-failed + :message message}))))))) + +(defn get-account-db-path + [address] + (str accounts-realm-dir (utils.ethereum/sha3 address))) (defn check-db-encryption - [file-name old-key new-key] - (js/Promise. - (fn [on-success on-error] - (try - (do - (log/info "try to encrypt with password") - (encrypted-realm-version file-name new-key) - (log/info "try to encrypt with password success") - (on-success)) - (catch :default e + [address password old-key] + (let [file-name (get-account-db-path address) + new-key (db-encryption-key password old-key)] + (js/Promise. + (fn [on-success on-error] + (try (do - (log/warn "failed checking db encryption with" e) - (log/info "try to encrypt with old key") - (encrypted-realm-version file-name old-key) - (log/info "try to encrypt with old key success") - (re-encrypt-realm file-name old-key new-key on-success on-error))))))) + (log/info "try to encrypt with password") + (encrypted-realm-version file-name new-key) + (log/info "try to encrypt with password success") + (on-success)) + (catch :default e + (do + (log/warn "failed checking db encryption with" e) + (log/info "try to encrypt with old key") + (.. (encrypted-realm-version-promise file-name old-key) + (then + #(re-encrypt-realm file-name old-key new-key on-success on-error)) + (catch on-error))))))))) -(defn change-account [address password encryption-key] - (let [path (str accounts-realm-dir (utils.ethereum/sha3 address)) +(defn open-account [address password encryption-key] + (let [path (get-account-db-path address) account-db-key (db-encryption-key password encryption-key)] - (close-account-realm) - (.. - (check-db-encryption path encryption-key account-db-key) - (then - (fn [] - (log/info "change-account done" (nil? @account-realm)) - (reset! account-realm - (open-migrated-realm path account/schemas account-db-key)) - (log/info "account-realm " (nil? @account-realm))))))) + (js/Promise. + (fn [on-success on-error] + (try + (log/info "open-account") + (reset! account-realm + (migrate-realm path account/schemas account-db-key)) + (log/info "account-realm " (nil? @account-realm)) + (on-success) + (catch :default e + (on-error {:message (str e) + :error :migrations-failed}))))))) (declare realm-obj->clj) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index 753881e74e..ee2e77411a 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -54,6 +54,11 @@ (fn [cofx _] {:init/reset-data nil})) +(handlers/register-handler-fx + :init.ui/account-data-reset-accepted + (fn [_ [_ address]] + {:init/reset-account-data address})) + (handlers/register-handler-fx :init.ui/data-reset-cancelled (fn [cofx [_ encryption-key]] @@ -128,6 +133,11 @@ (fn [cofx _] (init/initialize-keychain cofx))) +(handlers/register-handler-fx + :init.callback/account-db-removed + (fn [{:keys [db]} _] + {:db (assoc-in db [:accounts/login :processing] false)})) + ;; home screen (handlers/register-handler-fx diff --git a/src/status_im/init/core.cljs b/src/status_im/init/core.cljs index d5f8f0d5c1..92bbca456e 100644 --- a/src/status_im/init/core.cljs +++ b/src/status_im/init/core.cljs @@ -29,7 +29,8 @@ [status-im.utils.utils :as utils] [taoensso.timbre :as log] [status-im.utils.fx :as fx] - [status-im.chat.models :as chat-model])) + [status-im.chat.models :as chat-model] + [status-im.accounts.db :as accounts.db])) (defn init-store! "Try to decrypt the database, move on if successful otherwise go back to @@ -52,6 +53,12 @@ (then reset-keychain!) (catch reset-keychain!))) +(defn reset-account-data! [address] + (let [callback #(re-frame/dispatch [:init.callback/account-db-removed])] + (.. (realm/delete-account-realm address) + (then callback) + (catch callback)))) + (fx/defn initialize-keychain "Entrypoint, fetches the key from the keychain and initialize the app" [cofx] @@ -98,15 +105,37 @@ {:db (assoc db :device-UUID device-uuid)}) (fx/defn handle-change-account-error - [cofx error] - {:ui/show-confirmation - {:title (i18n/label :invalid-key-title) - :content (str error "\n" (i18n/label :invalid-key-content)) - :confirm-button-text (i18n/label :invalid-key-confirm) - ;; On cancel we initialize the app with the invalid key, to allow the user - ;; to recover the seed phrase - :on-cancel #(re-frame/dispatch [:init.ui/data-reset-cancelled ""]) - :on-accept #(re-frame/dispatch [:init.ui/data-reset-accepted])}}) + [{:keys [db]} error] + (let [{:keys [error message]} + (if (map? error) + error + {:message (str error)}) + address (get-in db [:accounts/login :address]) + erase-button-text (i18n/label :migrations-erase-accounts-data-button)] + (case error + :migrations-failed + {:ui/show-confirmation + {:title (i18n/label :migrations-failed-title) + :content (i18n/label + :migrations-failed-content + {:message message + :erase-accounts-data-button-text erase-button-text}) + :confirm-button-text erase-button-text + :on-cancel #(re-frame/dispatch [:init.ui/data-reset-cancelled ""]) + :on-accept #(re-frame/dispatch [:init.ui/account-data-reset-accepted address])}} + + ;; TODO(rasom): handle different errors as separate cases + ;; Right now it might be corrupted db file, wrong password, + ;; problem with permissions, etc. + {:ui/show-confirmation + {:title (i18n/label :invalid-key-title) + :content (i18n/label + :invalid-key-content + {:message message + :erase-accounts-data-button-text erase-button-text}) + :confirm-button-text (i18n/label :invalid-key-confirm) + :on-cancel #(re-frame/dispatch [:init.ui/data-reset-cancelled ""]) + :on-accept #(re-frame/dispatch [:init.ui/account-data-reset-accepted address])}}))) (fx/defn handle-init-store-error [encryption-key cofx] @@ -238,3 +267,7 @@ (re-frame/reg-fx :init/reset-data reset-data!) + +(re-frame/reg-fx + :init/reset-account-data + reset-account-data!) diff --git a/translations/en.json b/translations/en.json index 310399a310..1ee6355661 100644 --- a/translations/en.json +++ b/translations/en.json @@ -380,7 +380,7 @@ "recover-access": "Recover access", "currency-display-name-ron": "Romania Leu", "log-level-settings": "Log level settings", - "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 recovery 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": "{{message}}\n\nAccount's database can't be encrypted because file is corrupted. There is no way to restore it. If you press \"Cancel\" button, nothing will happen. If you press \"{{erase-accounts-data-button-text}}\" button, account's db will be removed and you will be able to unlock account. All account's data will be lost.", "advanced-settings": "Advanced settings", "group-info": "Group info", "currency-display-name-nio": "Nicaragua Cordoba", @@ -782,5 +782,8 @@ "network-invalid-status-code": "Invalid status code: {{code}}", "extension-is-already-added": "The extension is already added", "extension-uninstalled": "The extension was uninstalled", - "extension-hooks-cannot-be-added": "The following hooks from this extension cannot be added: {{hooks}}" + "extension-hooks-cannot-be-added": "The following hooks from this extension cannot be added: {{hooks}}", + "migrations-failed-title": "Migration failed", + "migrations-failed-content": "{{message}}\n\nPlease let us know about this problem at #status public chat. If you press \"Cancel\" button, nothing will happen. If you press \"{{erase-accounts-data-button-text}}\" button, account's db will be removed and you will be able to unlock account. All account's data will be lost.", + "migrations-erase-accounts-data-button": "Erase account's db" }