diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/NetworkManager.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/NetworkManager.kt index ac46fce241..2d7ddc6158 100644 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/NetworkManager.kt +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/NetworkManager.kt @@ -75,4 +75,17 @@ class NetworkManager(private val reactContext: ReactApplicationContext) : ReactC utils.executeRunnableStatusGoMethod({ Statusgo.recover(rpcParams) }, callback) } + @ReactMethod + fun getConnectionStringForExportingKeypairsKeystores(configJSON: String, callback: Callback) { + val jsonConfig = JSONObject(configJSON) + val senderConfig = jsonConfig.getJSONObject("senderConfig") + val keyUID = senderConfig.getString("loggedInKeyUid") + val keyStorePath = utils.getKeyStorePath(keyUID) + senderConfig.put("keystorePath", keyStorePath) + + utils.executeRunnableStatusGoMethod( + { Statusgo.getConnectionStringForExportingKeypairsKeystores(jsonConfig.toString()) }, + callback) + } + } diff --git a/modules/react-native-status/ios/RCTStatus/NetworkManager.m b/modules/react-native-status/ios/RCTStatus/NetworkManager.m index c82448be72..3701ec3acb 100644 --- a/modules/react-native-status/ios/RCTStatus/NetworkManager.m +++ b/modules/react-native-status/ios/RCTStatus/NetworkManager.m @@ -106,4 +106,22 @@ RCT_EXPORT_METHOD(recover:(NSString *)message callback(@[result]); } +RCT_EXPORT_METHOD(getConnectionStringForExportingKeypairsKeystores:(NSString *)configJSON + callback:(RCTResponseSenderBlock)callback) { + + NSData *configData = [configJSON dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSMutableDictionary *configDict = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingMutableContainers error:&error]; + NSMutableDictionary *senderConfig = configDict[@"senderConfig"]; + NSString *keyUID = senderConfig[@"loggedInKeyUid"]; + NSURL *multiaccountKeystoreDir = [Utils getKeyStoreDirForKeyUID:keyUID]; + NSString *keystoreDir = multiaccountKeystoreDir.path; + + [senderConfig setValue:keystoreDir forKey:@"keystorePath"]; + NSString *modifiedConfigJSON = [Utils jsonStringWithPrettyPrint:NO fromDictionary:configDict]; + + NSString *result = StatusgoGetConnectionStringForExportingKeypairsKeystores(modifiedConfigJSON); + callback(@[result]); +} + @end diff --git a/src/native_module/core.cljs b/src/native_module/core.cljs index c69bf28d32..a0f0f93df1 100644 --- a/src/native_module/core.cljs +++ b/src/native_module/core.cljs @@ -601,3 +601,11 @@ (.createAccountFromMnemonicAndDeriveAccountsForPaths ^js (account-manager) (types/clj->json mnemonic) #(callback (types/json->clj %)))) + +(defn get-connection-string-for-exporting-keypairs-keystores + "Generates connection string form status-go for the purpose of exporting keypairs and keystores on sender side" + [config-json callback] + (log/info "[native-module] Fetching Export Keypairs Connection String" + {:fn :get-connection-string-for-exporting-keypairs-keystores + :config-json config-json}) + (.getConnectionStringForExportingKeypairsKeystores ^js (network) config-json callback)) diff --git a/src/status_im/contexts/settings/wallet/events.cljs b/src/status_im/contexts/settings/wallet/events.cljs index 234f97a727..08b171eb13 100644 --- a/src/status_im/contexts/settings/wallet/events.cljs +++ b/src/status_im/contexts/settings/wallet/events.cljs @@ -1,8 +1,12 @@ (ns status-im.contexts.settings.wallet.events (:require + [native-module.core :as native-module] + [status-im.contexts.syncing.utils :as sync-utils] [taoensso.timbre :as log] [utils.i18n :as i18n] - [utils.re-frame :as rf])) + [utils.re-frame :as rf] + [utils.security.core :as security] + [utils.transforms :as transforms])) (rf/reg-event-fx :wallet/rename-keypair-success @@ -30,3 +34,22 @@ :on-error #(log/info "failed to rename keypair " %)}]]]}) (rf/reg-event-fx :wallet/rename-keypair rename-keypair) + +(defn get-key-pair-export-connection + [{:keys [db]} [{:keys [sha3-pwd keypair-key-uid callback]}]] + (let [key-uid (get-in db [:profile/profile :key-uid]) + config-map (transforms/clj->json {:senderConfig {:loggedInKeyUid key-uid + :keystorePath "" + :keypairsToExport [keypair-key-uid] + :password (security/safe-unmask-data + sha3-pwd)} + :serverConfig {:timeout 0}}) + handle-connection (fn [response] + (when (sync-utils/valid-connection-string? response) + (callback response) + (rf/dispatch [:hide-bottom-sheet])))] + (native-module/get-connection-string-for-exporting-keypairs-keystores + config-map + handle-connection))) + +(rf/reg-event-fx :wallet/get-key-pair-export-connection get-key-pair-export-connection) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/actions/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/actions/view.cljs index 39421e225c..1222dfc6bb 100644 --- a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/actions/view.cljs +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/actions/view.cljs @@ -7,13 +7,23 @@ [data] (rf/dispatch [:open-modal :screen/settings.rename-keypair data])) +(defn on-show-qr + [data] + (rf/dispatch [:open-modal :screen/settings.encrypted-key-pair-qr data])) + (defn view [props data] - [:<> - [quo/drawer-top props] - [quo/action-drawer - [(when (= (:type props) :keypair) - [{:icon :i/edit - :accessibility-label :rename-key-pair - :label (i18n/label :t/rename-key-pair) - :on-press #(on-rename-request data)}])]]]) + (let [has-paired-device (rf/sub [:pairing/has-paired-devices])] + [:<> + [quo/drawer-top props] + [quo/action-drawer + [(when has-paired-device + [{:icon :i/qr-code + :accessibility-label :show-key-pr-qr + :label (i18n/label :t/show-encrypted-qr-of-key-pairs) + :on-press #(on-show-qr data)}]) + (when (= (:type props) :keypair) + [{:icon :i/edit + :accessibility-label :rename-key-pair + :label (i18n/label :t/rename-key-pair) + :on-press #(on-rename-request data)}])]]])) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/encrypted_qr/countdown/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/encrypted_qr/countdown/view.cljs new file mode 100644 index 0000000000..cce451e9c1 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/encrypted_qr/countdown/view.cljs @@ -0,0 +1,38 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.countdown.view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.hooks :as hooks] + [utils.datetime :as datetime] + [utils.i18n :as i18n])) + +(def code-valid-for-ms 120000) +(def one-min-ms 60000) + +(defn current-ms + [] + (* 1000 (js/Math.ceil (/ (datetime/timestamp) 1000)))) + +(defn view + [on-clear] + (let [[valid-for-ms set-valid-for-ms] (rn/use-state code-valid-for-ms) + [timestamp set-timestamp] (rn/use-state current-ms) + clock (rn/use-callback (fn [] + (let [remaining (- code-valid-for-ms + (- (current-ms) + timestamp))] + (when (pos? remaining) + (set-valid-for-ms remaining)) + (when (zero? remaining) + (set-timestamp (current-ms)) + (set-valid-for-ms code-valid-for-ms) + (on-clear)))) + [code-valid-for-ms])] + (hooks/use-interval clock on-clear 1000) + [quo/text + {:size :paragraph-2 + :style {:color (if (< valid-for-ms one-min-ms) + colors/danger-60 + colors/white-opa-40)}} + (i18n/label :t/valid-for-time {:valid-for (datetime/ms-to-duration valid-for-ms)})])) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/encrypted_qr/style.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/encrypted_qr/style.cljs new file mode 100644 index 0000000000..5598fbce21 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/encrypted_qr/style.cljs @@ -0,0 +1,46 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.style + (:require + [quo.foundations.colors :as colors] + [react-native.safe-area :as safe-area])) + +(defn container-main + [] + {:background-color colors/neutral-95 + :padding-top (safe-area/get-top) + :flex 1}) + +(def page-container + {:margin-top 14 + :margin-horizontal 20}) + +(def title-container + {:flex-direction :row + :align-items :center + :justify-content :space-between}) + +(def standard-auth + {:margin-top 12 + :flex 1}) + +(def qr-container + {:margin-top 12 + :background-color colors/white-opa-5 + :border-radius 20 + :flex 1 + :padding 12}) + +(def sub-text-container + {:margin-bottom 8 + :justify-content :space-between + :align-items :center + :flex-direction :row}) + +(def valid-cs-container + {:flex 1 + :margin 12}) + +(def warning-text + {:margin-horizontal 16 + :margin-top 20 + :text-align :center + :color colors/white-opa-70}) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/encrypted_qr/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/encrypted_qr/view.cljs new file mode 100644 index 0000000000..a54d4ff340 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/encrypted_qr/view.cljs @@ -0,0 +1,98 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [status-im.common.qr-codes.view :as qr-codes] + [status-im.common.resources :as resources] + [status-im.common.standard-authentication.core :as standard-auth] + [status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.countdown.view :as countdown] + [status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.style :as style] + [status-im.contexts.syncing.utils :as sync-utils] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn navigate-back [] (rf/dispatch [:navigate-back])) + +(defn view + [] + (let [{:keys [key-uid]} (rf/sub [:get-screen-params]) + {:keys [customization-color]} (rf/sub [:profile/profile-with-image]) + [code set-code] (rn/use-state nil) + valid-connection-string? (rn/use-memo #(sync-utils/valid-connection-string? code) [code]) + validate-and-set-code (rn/use-callback (fn [connection-string] + (when (sync-utils/valid-connection-string? + connection-string) + (set-code connection-string)))) + cleanup-clock (rn/use-callback #(set-code nil)) + on-auth-success (rn/use-callback (fn [entered-password] + (rf/dispatch + [:wallet/get-key-pair-export-connection + {:sha3-pwd entered-password + :keypair-key-uid key-uid + :callback validate-and-set-code}])) + [key-uid])] + [rn/view {:style (style/container-main)} + [rn/scroll-view + [quo/page-nav + {:type :no-title + :icon-name :i/close + :background :blur + :on-press navigate-back}] + [rn/view {:style style/page-container} + [rn/view {:style style/title-container} + [quo/text + {:size :heading-1 + :weight :semi-bold + :style {:color colors/white}} + (i18n/label :t/encrypted-key-pairs)]] + [rn/view {:style style/qr-container} + (if valid-connection-string? + [qr-codes/qr-code {:url code}] + [rn/view {:style {:flex-direction :row}} + [rn/image + {:source (resources/get-image :qr-code) + :style {:width "100%" + :background-color colors/white-opa-70 + :border-radius 12 + :aspect-ratio 1}}]]) + (when valid-connection-string? + [rn/view {:style style/valid-cs-container} + [rn/view {:style style/sub-text-container} + [quo/text + {:size :paragraph-2 + :style {:color colors/white-opa-40}} + (i18n/label :t/encrypted-key-pairs-code)] + [countdown/view cleanup-clock]] + [quo/input + {:default-value code + :type :password + :default-shown? true + :editable false}] + [quo/button + {:on-press (fn [] + (clipboard/set-string code) + (rf/dispatch [:toasts/upsert + {:type :positive + :text (i18n/label + :t/sharing-copied-to-clipboard)}])) + :type :grey + :container-style {:margin-top 12} + :icon-left :i/copy} + (i18n/label :t/copy-qr)]]) + (when-not valid-connection-string? + [rn/view {:style style/standard-auth} + [standard-auth/slide-button + {:blur? true + :size :size-40 + :track-text (i18n/label :t/slide-to-reveal-qr-code) + :customization-color customization-color + :on-auth-success on-auth-success + :auth-button-label (i18n/label :t/reveal-qr-code) + :auth-button-icon-left :i/reveal}]])]] + (when-not valid-connection-string? + [quo/text + {:size :paragraph-2 + :style style/warning-text} + (i18n/label :t/make-sure-no-camera-warning)])]])) diff --git a/src/status_im/navigation/screens.cljs b/src/status_im/navigation/screens.cljs index 10edaf96dd..2885067b95 100644 --- a/src/status_im/navigation/screens.cljs +++ b/src/status_im/navigation/screens.cljs @@ -57,6 +57,8 @@ [status-im.contexts.profile.settings.screens.password.change-password.view :as change-password] [status-im.contexts.profile.settings.screens.password.view :as settings-password] [status-im.contexts.profile.settings.view :as settings] + [status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.view :as + encrypted-key-pair-qr] [status-im.contexts.settings.wallet.keypairs-and-accounts.rename.view :as keypair-rename] [status-im.contexts.settings.wallet.keypairs-and-accounts.view :as keypairs-and-accounts] [status-im.contexts.settings.wallet.network-settings.view :as network-settings] @@ -509,6 +511,10 @@ :options (assoc options/dark-screen :sheet? true) :component keypair-rename/view} + {:name :screen/settings.encrypted-key-pair-qr + :options options/transparent-screen-options + :component encrypted-key-pair-qr/view} + {:name :screen/settings.saved-addresses :options options/transparent-modal-screen-options :component saved-addresses-settings/view} diff --git a/src/status_im/subs/pairing.cljs b/src/status_im/subs/pairing.cljs index 7c79db255d..1b32811ffb 100644 --- a/src/status_im/subs/pairing.cljs +++ b/src/status_im/subs/pairing.cljs @@ -28,3 +28,9 @@ :<- [:syncing] (fn [syncing] (:pairing-status syncing))) + +(re-frame/reg-sub + :pairing/has-paired-devices + :<- [:pairing/enabled-installations] + (fn [installations] + (> (count installations) 1))) diff --git a/translations/en.json b/translations/en.json index e21b0de81d..c1aed2d552 100644 --- a/translations/en.json +++ b/translations/en.json @@ -524,6 +524,8 @@ "enable": "Enable", "enable-notifications-sub-title": "Receive notifications about your new messages or wallet transactions", "encrypt-with-password": "Encrypt with password", + "encrypted-key-pairs": "Encrypted key pairs", + "encrypted-key-pairs-code": "Encrypted key pairs code", "ending-not-allowed": "{{ending}} ending is not allowed", "ends-with-space": "Cannot end with space", "ens-10-SNT": "10 SNT", @@ -939,6 +941,7 @@ "main-wallet": "Main Wallet", "make-admin": "Make admin", "make-moderator": "Make moderator", + "make-sure-no-camera-warning": "Make sure no camera or person can see this screen before revealing", "manage-keys-and-storage": "Manage keys and storage", "mark-as-read": "Mark as read", "mark-all-read": "Mark all read", @@ -1272,6 +1275,7 @@ "reset-card-description": "This operation will reset card to initial state. It will erase all card data including private keys. Operation is not reversible.", "retry": "Retry", "reveal-sync-code": "Reveal sync code", + "reveal-qr-code": "Reveal QR code", "revoke-access": "Revoke access", "save": "Save", "save-address": "Save address", @@ -1337,6 +1341,7 @@ "show-more": "Show more", "show-qr": "Show QR code", "show-transaction-data": "Show transaction data", + "show-encrypted-qr-of-key-pairs": "Show encrypted QR of key pairs on device", "sign-and-send": "Sign and send", "sign-in": "Sign in", "sign-message": "Sign Message", @@ -1970,6 +1975,7 @@ "slide-to-request-to-join": "Slide to request to join", "slide-to-reveal-code": "Slide to reveal code", "slide-to-create-account": "Slide to create account", + "slide-to-reveal-qr-code": "Slide to reveal QR code", "minimum-received": "Minimum received", "powered-by-paraswap": "Powered by Paraswap", "priority": "Priority",