diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java index 7828b58159..4112b98e09 100644 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java @@ -528,6 +528,22 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL StatusThreadPoolExecutor.getInstance().execute(r); } + @ReactMethod + public void verifyDatabasePassword(final String keyUID, final String password, final Callback callback) { + Log.d(TAG, "verifyDatabasePassword"); + + Runnable r = new Runnable() { + @Override + public void run() { + String result = Statusgo.verifyDatabasePassword(keyUID, password); + + callback.invoke(result); + } + }; + + StatusThreadPoolExecutor.getInstance().execute(r); + } + public String getKeyStorePath(String keyUID) { final String commonKeydir = pathCombine(this.getNoBackupDirectory(), "/keystore"); final String keydir = pathCombine(commonKeydir, keyUID); @@ -1478,6 +1494,20 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL StatusThreadPoolExecutor.getInstance().execute(r); } + @ReactMethod + public void convertToKeycardAccount(final String keyUID, final String accountData, final String options, final String password, final String newPassword, final Callback callback) { + Log.d(TAG, "convertToKeycardAccount"); + final String keyStoreDir = this.getKeyStorePath(keyUID); + Runnable r = new Runnable() { + @Override + public void run() { + String result = Statusgo.convertToKeycardAccount(keyStoreDir, accountData, options, password, newPassword); + callback.invoke(result); + } + }; + + StatusThreadPoolExecutor.getInstance().execute(r); + } } diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m index 6423118849..109c940d7c 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m @@ -570,6 +570,17 @@ RCT_EXPORT_METHOD(verify:(NSString *)address callback(@[result]); } +//////////////////////////////////////////////////////////////////// verifyDatabasePassword +RCT_EXPORT_METHOD(verifyDatabasePassword:(NSString *)keyUID + password:(NSString *)password + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"VerifyDatabasePassword() method called"); +#endif + NSString *result = StatusgoVerifyDatabasePassword(keyUID, password); + callback(@[result]); +} + //////////////////////////////////////////////////////////////////// changeDatabasePassword RCT_EXPORT_METHOD(reEncryptDbAndKeystore:(NSString *)keyUID currentPassword:(NSString *)currentPassword @@ -583,6 +594,21 @@ RCT_EXPORT_METHOD(reEncryptDbAndKeystore:(NSString *)keyUID callback(@[result]); } +//////////////////////////////////////////////////////////////////// convertToKeycardAccount +RCT_EXPORT_METHOD(convertToKeycardAccount:(NSString *)keyUID + accountData:(NSString *)accountData + settings:(NSString *)settings + currentPassword:(NSString *)currentPassword + newPassword:(NSString *)newPassword + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"convertToKeycardAccount() method called"); +#endif + NSURL *multiaccountKeystoreDir = [self getKeyStoreDir:keyUID]; + NSString *result = StatusgoConvertToKeycardAccount(multiaccountKeystoreDir.path, accountData, settings, currentPassword, newPassword); + callback(@[result]); +} + //////////////////////////////////////////////////////////////////// #pragma mark - SendTransaction //////////////////////////////////////////////////////////////////// sendTransaction diff --git a/src/status_im/keycard/recovery.cljs b/src/status_im/keycard/recovery.cljs index 161032deb7..7250040af7 100644 --- a/src/status_im/keycard/recovery.cljs +++ b/src/status_im/keycard/recovery.cljs @@ -5,6 +5,7 @@ [status-im.multiaccounts.model :as multiaccounts.model] [status-im.utils.fx :as fx] [re-frame.core :as re-frame] + [clojure.string :as string] [status-im.i18n.i18n :as i18n] [taoensso.timbre :as log] [status-im.keycard.common :as common] @@ -13,6 +14,10 @@ [status-im.ethereum.eip55 :as eip55] [status-im.ethereum.core :as ethereum] [status-im.bottom-sheet.core :as bottom-sheet] + [status-im.native-module.core :as status] + [status-im.utils.types :as types] + [status-im.utils.security :as security] + [status-im.utils.keychain.core :as keychain] [status-im.utils.platform :as platform])) (fx/defn pair* [_ password] @@ -217,13 +222,54 @@ (navigation/set-stack-root :profile-stack [:my-profile :keycard-settings]) (return-to-keycard-login)))) +(re-frame/reg-fx + ::finish-migration + (fn [[account settings password encryption-pass login-params]] + (status/convert-to-keycard-account + account + settings + password + encryption-pass + #(let [{:keys [error]} (types/json->clj %)] + (if (string/blank? error) + (status/login-with-keycard login-params) + (throw (js/Error. "Please shake the phone to report this error and restart the app. Migration failed unexpectedly."))))))) + +(fx/defn migrate-account + [{:keys [db] :as cofx}] + (let [pairing (get-in db [:keycard :secrets :pairing]) + paired-on (get-in db [:keycard :secrets :paired-on]) + instance-uid (get-in db [:keycard :multiaccount :instance-uid]) + account (-> db + :multiaccounts/login + (assoc :keycard-pairing pairing) + (assoc :save-password? false)) + key-uid (-> account :key-uid) + settings {:keycard-instance-uid instance-uid + :keycard-paired-on paired-on + :keycard-pairing pairing} + password (ethereum/sha3 (security/safe-unmask-data (get-in db [:keycard :migration-password]))) + encryption-pass (get-in db [:keycard :multiaccount :encryption-public-key]) + login-params {:key-uid key-uid + :multiaccount-data (types/clj->json account) + :password encryption-pass + :chat-key (get-in db [:keycard :multiaccount :whisper-private-key])}] + {:db (-> db + (assoc-in [:multiaccounts/multiaccounts key-uid :keycard-pairing] pairing) + (assoc :multiaccounts/login account) + (assoc :auth-method keychain/auth-method-none) + (update :keycard dissoc :flow :migration-password) + (dissoc :recovered-account?)) + ::finish-migration [account settings password encryption-pass login-params]})) + (fx/defn on-generate-and-load-key-success {:events [:keycard.callback/on-generate-and-load-key-success] :interceptors [(re-frame/inject-cofx :random-guid-generator) (re-frame/inject-cofx ::multiaccounts.create/get-signing-phrase)]} [{:keys [db random-guid-generator] :as cofx} data] (let [account-data (js->clj data :keywordize-keys true) - backup? (get-in db [:keycard :creating-backup?])] + backup? (get-in db [:keycard :creating-backup?]) + migration? (get-in db [:keycard :converting-account?])] (fx/merge cofx {:db (-> db (assoc-in [:keycard :multiaccount] @@ -242,14 +288,14 @@ (assoc-in [:keycard :pin :status] nil) (assoc-in [:keycard :application-info :key-uid] (ethereum/normalized-hex (:key-uid account-data))) - (update :keycard dissoc :recovery-phrase) - (update :keycard dissoc :creating-backup?) - (update-in [:keycard :secrets] dissoc :pin :puk :password) - (assoc :multiaccounts/new-installation-id (random-guid-generator)) - (update-in [:keycard :secrets] dissoc :mnemonic))} + (update :keycard dissoc :recovery-phrase :creating-backup? :converting-account?) + (update-in [:keycard :secrets] dissoc :pin :puk :password :mnemonic) + (assoc :multiaccounts/new-installation-id (random-guid-generator)))} (common/remove-listener-to-hardware-back-button) (common/hide-connection-sheet) - (if backup? (on-backup-success backup?) (create-keycard-multiaccount))))) + (cond backup? (on-backup-success backup?) + migration? (migrate-account) + :else (create-keycard-multiaccount))))) (fx/defn on-generate-and-load-key-error {:events [:keycard.callback/on-generate-and-load-key-error]} diff --git a/src/status_im/multiaccounts/key_storage/core.cljs b/src/status_im/multiaccounts/key_storage/core.cljs index a434a71aaf..a95a0d12f5 100644 --- a/src/status_im/multiaccounts/key_storage/core.cljs +++ b/src/status_im/multiaccounts/key_storage/core.cljs @@ -10,8 +10,11 @@ [status-im.popover.core :as popover] [status-im.utils.fx :as fx] [status-im.utils.security :as security] + [status-im.ethereum.core :as ethereum] + [status-im.i18n.i18n :as i18n] [status-im.utils.types :as types] - [status-im.keycard.backup-key :as keycard.backup])) + [status-im.keycard.backup-key :as keycard.backup] + [status-im.bottom-sheet.core :as bottom-sheet])) (fx/defn key-and-storage-management-pressed "This event can be dispatched before login and from profile and needs to redirect accordingly" @@ -29,6 +32,11 @@ [{:keys [db] :as cofx} checked?] {:db (assoc-in db [:multiaccounts/key-storage :move-keystore-checked?] checked?)}) +(fx/defn reset-db-checked + {:events [::reset-db-checked]} + [{:keys [db] :as cofx} checked?] + {:db (assoc-in db [:multiaccounts/key-storage :reset-db-checked?] checked?)}) + (fx/defn navigate-back {:events [::navigate-back]} [{:keys [db] :as cofx}] @@ -120,11 +128,6 @@ [{:keys [db]} selected?] {:db (assoc-in db [:multiaccounts/key-storage :keycard-storage-selected?] selected?)}) -(fx/defn warning-popup - {:events [::show-transfer-warning-popup]} - [cofx] - (popover/show-popover cofx {:view :transfer-multiaccount-to-keycard-warning})) - (re-frame/reg-fx ::delete-multiaccount (fn [{:keys [key-uid on-success on-error]}] @@ -158,13 +161,54 @@ The exact events dispatched for this flow if consumed from the UI are: We don't need to take the exact steps, just set the required state and redirect to correct screen " -(fx/defn handle-delete-multiaccount-success +(fx/defn import-multiaccount {:events [::delete-multiaccount-success]} - [{:keys [db] :as cofx} _] - {::multiaccounts.recover/import-multiaccount {:passphrase (get-in db [:multiaccounts/key-storage :seed-phrase]) + [{:keys [db] :as cofx}] + {:dispatch [:bottom-sheet/hide] + ::multiaccounts.recover/import-multiaccount {:passphrase (get-in db [:multiaccounts/key-storage :seed-phrase]) :password nil :success-event ::import-multiaccount-success}}) +(fx/defn storage-selected + {:events [::storage-selected]} + [{:keys [db] :as cofx}] + (if (get-in db [:multiaccounts/key-storage :reset-db-checked?]) + (popover/show-popover cofx {:view :transfer-multiaccount-to-keycard-warning}) + (bottom-sheet/show-bottom-sheet cofx {:view :migrate-account-password}))) + +(fx/defn skip-password-pressed + {:events [::skip-password-pressed]} + [cofx] + (popover/show-popover cofx {:view :transfer-multiaccount-to-keycard-warning})) + +(fx/defn password-changed + {:events [::password-changed]} + [{db :db} password] + (let [unmasked-pass (security/safe-unmask-data password)] + {:db (update db :keycard assoc + :migration-password password + :migration-password-error nil + :migration-password-valid? (and unmasked-pass (> (count unmasked-pass) 5)))})) + +(fx/defn verify-password-result + {:events [::verify-password-result]} + [{:keys [db] :as cofx} result] + (let [{:keys [error]} (types/json->clj result)] + (if (string/blank? error) + (fx/merge + cofx + {:db (update db :keycard dissoc :migration-password-error :migration-password-valid?)} + (import-multiaccount)) + {:db (assoc-in db [:keycard :migration-password-error] (i18n/label :t/wrong-password))}))) + +(fx/defn verify-password + {:events [::verify-password]} + [{:keys [db] :as cofx}] + (native-module/verify-database-password + (get-in db [:multiaccounts/login :key-uid]) + (ethereum/sha3 (security/safe-unmask-data (get-in db [:keycard :migration-password]))) + #(re-frame/dispatch [::verify-password-result %]))) + (fx/defn handle-multiaccount-import {:events [::import-multiaccount-success]} [{:keys [db] :as cofx} root-data derived-data] @@ -178,6 +222,7 @@ We don't need to take the exact steps, just set the required state and redirect :selected-storage-type :advanced) (assoc-in [:keycard :flow] :recovery) (assoc-in [:keycard :from-key-storage-and-migration?] true) + (assoc-in [:keycard :converting-account?] (not (get-in db [:multiaccounts/key-storage :reset-db-checked?]))) (dissoc :multiaccounts/key-storage))} (popover/hide-popover) (navigation/navigate-to-cofx :keycard-onboarding-intro nil))) diff --git a/src/status_im/multiaccounts/login/core.cljs b/src/status_im/multiaccounts/login/core.cljs index bca922194e..eef63568b6 100644 --- a/src/status_im/multiaccounts/login/core.cljs +++ b/src/status_im/multiaccounts/login/core.cljs @@ -337,12 +337,15 @@ (fx/defn login-only-events [{:keys [db] :as cofx} key-uid password save-password?] (let [auth-method (:auth-method db) - new-auth-method (get-new-auth-method auth-method save-password?)] + new-auth-method (get-new-auth-method auth-method save-password?) + from-migration? (get-in db [:keycard :from-key-storage-and-migration?])] (log/debug "[login] login-only-events" "auth-method" auth-method "new-auth-method" new-auth-method) (fx/merge cofx - {:db (assoc db :chats/loading? true) + {:db (-> db + (assoc :chats/loading? true) + (update :keycard dissoc :from-key-storage-and-migration?)) ::json-rpc/call [{:method "browsers_getBrowsers" :on-success #(re-frame/dispatch [::initialize-browsers %])} @@ -354,6 +357,8 @@ :on-success #(do (re-frame/dispatch [::get-settings-callback %]) (redirect-to-root db))}]} (notifications/load-notification-preferences) + (when from-migration? + (utils/show-popup (i18n/label :t/migration-successful) (i18n/label :t/migration-successful-text))) (when save-password? (keychain/save-user-password key-uid password)) (keychain/save-auth-method key-uid (or new-auth-method auth-method keychain/auth-method-none))))) diff --git a/src/status_im/native_module/core.cljs b/src/status_im/native_module/core.cljs index 4f84ca1154..0f60130200 100644 --- a/src/status_im/native_module/core.cljs +++ b/src/status_im/native_module/core.cljs @@ -192,6 +192,12 @@ (log/debug "[native-module] verify") (.verify ^js (status) address hashed-password callback)) +(defn verify-database-password + "NOTE: beware, the password has to be sha3 hashed" + [key-uid hashed-password callback] + (log/debug "[native-module] verify-database-password") + (.verifyDatabasePassword ^js (status) key-uid hashed-password callback)) + (defn login-with-keycard [{:keys [key-uid multiaccount-data password chat-key]}] (log/debug "[native-module] login-with-keycard") @@ -414,3 +420,14 @@ (init-keystore key-uid #(.reEncryptDbAndKeystore ^js (status) key-uid current-password# new-password# callback))) + +(defn convert-to-keycard-account + [{:keys [key-uid] :as multiaccount-data} settings current-password# new-password callback] + (log/debug "[native-module] convert-to-keycard-account") + (.convertToKeycardAccount ^js (status) + key-uid + (types/clj->json multiaccount-data) + (types/clj->json settings) + current-password# + new-password + callback)) diff --git a/src/status_im/ui/screens/bottom_sheets/views.cljs b/src/status_im/ui/screens/bottom_sheets/views.cljs index 577f5b0148..55067a7876 100644 --- a/src/status_im/ui/screens/bottom_sheets/views.cljs +++ b/src/status_im/ui/screens/bottom_sheets/views.cljs @@ -3,6 +3,7 @@ [re-frame.core :as re-frame] [status-im.ui.screens.home.sheet.views :as home.sheet] [status-im.ui.screens.keycard.views :as keycard] + [status-im.ui.screens.multiaccounts.key-storage.views :as key-storage] [status-im.ui.screens.about-app.views :as about-app] [status-im.ui.screens.multiaccounts.recover.views :as recover.views] [quo.core :as quo])) @@ -33,7 +34,10 @@ (merge about-app/learn-more) (= view :recover-sheet) - (merge recover.views/bottom-sheet))] + (merge recover.views/bottom-sheet) + + (= view :migrate-account-password) + (merge key-storage/migrate-account-password))] [quo/bottom-sheet opts (when content [content])])) diff --git a/src/status_im/ui/screens/multiaccounts/key_storage/styles.cljs b/src/status_im/ui/screens/multiaccounts/key_storage/styles.cljs index 010accbf88..4f6e58add9 100644 --- a/src/status_im/ui/screens/multiaccounts/key_storage/styles.cljs +++ b/src/status_im/ui/screens/multiaccounts/key_storage/styles.cljs @@ -24,3 +24,11 @@ {:color colors/gray :text-align :center :line-height 22}) + +(def header + {:flex-direction :row + :align-items :center + :justify-content :space-between + :padding-top 16 + :padding-left 16 + :margin-bottom 11}) \ No newline at end of file diff --git a/src/status_im/ui/screens/multiaccounts/key_storage/views.cljs b/src/status_im/ui/screens/multiaccounts/key_storage/views.cljs index 5bd564b2d5..7ef31588cf 100644 --- a/src/status_im/ui/screens/multiaccounts/key_storage/views.cljs +++ b/src/status_im/ui/screens/multiaccounts/key_storage/views.cljs @@ -55,7 +55,7 @@ ;; Component to render Key and Storage management screen (defview actions-base [{:keys [next-title next-event]}] (letsubs [{:keys [name] :as multiaccount} [:multiaccounts/login] - {:keys [move-keystore-checked?]} [:multiaccounts/key-storage]] + {:keys [move-keystore-checked? reset-db-checked?]} [:multiaccounts/key-storage]] [react/view {:flex 1} [local-topbar (i18n/label :t/choose-actions)] [accordion/section {:title name @@ -77,8 +77,8 @@ [quo/list-item {:title (i18n/label :t/reset-database) :subtitle (i18n/label :t/reset-database-warning) :subtitle-max-lines 4 - :disabled true - :active move-keystore-checked? + :active reset-db-checked? + :on-press #(re-frame/dispatch [::multiaccounts.key-storage/reset-db-checked (not reset-db-checked?)]) :accessory :checkbox}]] (when (and next-title next-event) [toolbar/toolbar {:show-border? true @@ -195,9 +195,37 @@ :right [quo/button {:type :secondary :disabled (not keycard-storage-selected?) - :on-press #(re-frame/dispatch [::multiaccounts.key-storage/show-transfer-warning-popup])} + :on-press #(re-frame/dispatch [::multiaccounts.key-storage/storage-selected])} (i18n/label :t/confirm)]}]]])) +(defview migrate-account-password-view [] + (letsubs [{:keys [migration-password-error migration-password-valid?]} [:keycard]] + [react/view {:flex 1} + [react/view styles/header + [react/text {:style {:typography :title-bold}} (i18n/label :t/enter-password)] + [react/view {:padding-horizontal 24} + [quo/button {:type :secondary + :on-press #(re-frame/dispatch [::multiaccounts.key-storage/skip-password-pressed])} + (i18n/label :t/skip)]]] + [quo/separator] + [react/view {:padding-horizontal 16 :padding-vertical 12} + [react/text {:style {:margin-top 6 :margin-bottom 12 :color colors/gray}} (i18n/label :t/enter-password-migration-prompt)] + [quo/text-input + {:secure-text-entry true + :placeholder (i18n/label :t/current-password) + :on-change-text #(re-frame/dispatch [::multiaccounts.key-storage/password-changed (status-im.utils.security/mask-data %)]) + :accessibility-label :enter-password-input + :auto-capitalize :none + :error migration-password-error + :show-cancel false}]] + [react/view {:padding-horizontal 16 :padding-vertical 12} + [quo/button {:on-press #(re-frame/dispatch [::multiaccounts.key-storage/verify-password]) + :disabled (not migration-password-valid?)} + (i18n/label :t/confirm)]]])) + +(def migrate-account-password + {:content migrate-account-password-view}) + (defview seed-key-uid-mismatch-popover [] (letsubs [{:keys [name]} [:multiaccounts/login] {logged-in-name :name} [:multiaccount]] diff --git a/status-go-version.json b/status-go-version.json index 1665acafb2..100a018cc1 100644 --- a/status-go-version.json +++ b/status-go-version.json @@ -2,7 +2,7 @@ "_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh ' instead", "owner": "status-im", "repo": "status-go", - "version": "v0.82.0", - "commit-sha1": "2f6b32b1f54de5802882c0177bb0b16924d04271", - "src-sha256": "1nlslsbibwnb7s6mbxqsijxrsw3lrs1dqvqy6bhfxsvwi5x97a28" + "version": "v0.83.0", + "commit-sha1": "6c2e9652d0e77a732af8649a1a75961ac9ac820f", + "src-sha256": "14fhj54d46xv3vh5qkbryxq7hdjrp13pabz8012qa592i0lf72fb" } diff --git a/translations/en.json b/translations/en.json index 29f2268317..3a192c957b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -521,6 +521,10 @@ "pair-code-placeholder": "Pair code...", "enter-pair-code-description": "Pairing code can be set from an already paired Status client", "enter-password": "Enter password", + "enter-password-migration-prompt": "Enter your password to move contacts, chats and settings along with your keys", + "migration-successful": "Migration successful", + "migration-successful-text": "Account succesfully migrated to Keycard", + "skip": "Skip", "password-placeholder":"Password...", "confirm-password-placeholder": "Confirm your password...", "enter-pin": "Enter 6-digit passcode",