implement account conversion

Signed-off-by: Michele Balistreri <michele@bitgamma.com>
This commit is contained in:
Michele Balistreri 2021-07-13 15:41:45 +02:00
parent baa96ed22e
commit e8f7ae8f27
No known key found for this signature in database
GPG Key ID: E9567DA33A4F791A
11 changed files with 239 additions and 26 deletions

View File

@ -528,6 +528,22 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
StatusThreadPoolExecutor.getInstance().execute(r); 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) { public String getKeyStorePath(String keyUID) {
final String commonKeydir = pathCombine(this.getNoBackupDirectory(), "/keystore"); final String commonKeydir = pathCombine(this.getNoBackupDirectory(), "/keystore");
final String keydir = pathCombine(commonKeydir, keyUID); final String keydir = pathCombine(commonKeydir, keyUID);
@ -1478,6 +1494,20 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
StatusThreadPoolExecutor.getInstance().execute(r); 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);
}
} }

View File

@ -570,6 +570,17 @@ RCT_EXPORT_METHOD(verify:(NSString *)address
callback(@[result]); 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 //////////////////////////////////////////////////////////////////// changeDatabasePassword
RCT_EXPORT_METHOD(reEncryptDbAndKeystore:(NSString *)keyUID RCT_EXPORT_METHOD(reEncryptDbAndKeystore:(NSString *)keyUID
currentPassword:(NSString *)currentPassword currentPassword:(NSString *)currentPassword
@ -583,6 +594,21 @@ RCT_EXPORT_METHOD(reEncryptDbAndKeystore:(NSString *)keyUID
callback(@[result]); 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 #pragma mark - SendTransaction
//////////////////////////////////////////////////////////////////// sendTransaction //////////////////////////////////////////////////////////////////// sendTransaction

View File

@ -5,6 +5,7 @@
[status-im.multiaccounts.model :as multiaccounts.model] [status-im.multiaccounts.model :as multiaccounts.model]
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[clojure.string :as string]
[status-im.i18n.i18n :as i18n] [status-im.i18n.i18n :as i18n]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[status-im.keycard.common :as common] [status-im.keycard.common :as common]
@ -13,6 +14,10 @@
[status-im.ethereum.eip55 :as eip55] [status-im.ethereum.eip55 :as eip55]
[status-im.ethereum.core :as ethereum] [status-im.ethereum.core :as ethereum]
[status-im.bottom-sheet.core :as bottom-sheet] [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])) [status-im.utils.platform :as platform]))
(fx/defn pair* [_ password] (fx/defn pair* [_ password]
@ -217,13 +222,54 @@
(navigation/set-stack-root :profile-stack [:my-profile :keycard-settings]) (navigation/set-stack-root :profile-stack [:my-profile :keycard-settings])
(return-to-keycard-login)))) (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 (fx/defn on-generate-and-load-key-success
{:events [:keycard.callback/on-generate-and-load-key-success] {:events [:keycard.callback/on-generate-and-load-key-success]
:interceptors [(re-frame/inject-cofx :random-guid-generator) :interceptors [(re-frame/inject-cofx :random-guid-generator)
(re-frame/inject-cofx ::multiaccounts.create/get-signing-phrase)]} (re-frame/inject-cofx ::multiaccounts.create/get-signing-phrase)]}
[{:keys [db random-guid-generator] :as cofx} data] [{:keys [db random-guid-generator] :as cofx} data]
(let [account-data (js->clj data :keywordize-keys true) (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 (fx/merge cofx
{:db (-> db {:db (-> db
(assoc-in [:keycard :multiaccount] (assoc-in [:keycard :multiaccount]
@ -242,14 +288,14 @@
(assoc-in [:keycard :pin :status] nil) (assoc-in [:keycard :pin :status] nil)
(assoc-in [:keycard :application-info :key-uid] (assoc-in [:keycard :application-info :key-uid]
(ethereum/normalized-hex (:key-uid account-data))) (ethereum/normalized-hex (:key-uid account-data)))
(update :keycard dissoc :recovery-phrase) (update :keycard dissoc :recovery-phrase :creating-backup? :converting-account?)
(update :keycard dissoc :creating-backup?) (update-in [:keycard :secrets] dissoc :pin :puk :password :mnemonic)
(update-in [:keycard :secrets] dissoc :pin :puk :password) (assoc :multiaccounts/new-installation-id (random-guid-generator)))}
(assoc :multiaccounts/new-installation-id (random-guid-generator))
(update-in [:keycard :secrets] dissoc :mnemonic))}
(common/remove-listener-to-hardware-back-button) (common/remove-listener-to-hardware-back-button)
(common/hide-connection-sheet) (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 (fx/defn on-generate-and-load-key-error
{:events [:keycard.callback/on-generate-and-load-key-error]} {:events [:keycard.callback/on-generate-and-load-key-error]}

View File

@ -10,8 +10,11 @@
[status-im.popover.core :as popover] [status-im.popover.core :as popover]
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
[status-im.utils.security :as security] [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.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 (fx/defn key-and-storage-management-pressed
"This event can be dispatched before login and from profile and needs to redirect accordingly" "This event can be dispatched before login and from profile and needs to redirect accordingly"
@ -29,6 +32,11 @@
[{:keys [db] :as cofx} checked?] [{:keys [db] :as cofx} checked?]
{:db (assoc-in db [:multiaccounts/key-storage :move-keystore-checked?] 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 (fx/defn navigate-back
{:events [::navigate-back]} {:events [::navigate-back]}
[{:keys [db] :as cofx}] [{:keys [db] :as cofx}]
@ -120,11 +128,6 @@
[{:keys [db]} selected?] [{:keys [db]} selected?]
{:db (assoc-in db [:multiaccounts/key-storage :keycard-storage-selected?] 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 (re-frame/reg-fx
::delete-multiaccount ::delete-multiaccount
(fn [{:keys [key-uid on-success on-error]}] (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 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]} {:events [::delete-multiaccount-success]}
[{:keys [db] :as cofx} _] [{:keys [db] :as cofx}]
{::multiaccounts.recover/import-multiaccount {:passphrase (get-in db [:multiaccounts/key-storage :seed-phrase]) {:dispatch [:bottom-sheet/hide]
::multiaccounts.recover/import-multiaccount {:passphrase (get-in db [:multiaccounts/key-storage :seed-phrase])
:password nil :password nil
:success-event ::import-multiaccount-success}}) :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 (fx/defn handle-multiaccount-import
{:events [::import-multiaccount-success]} {:events [::import-multiaccount-success]}
[{:keys [db] :as cofx} root-data derived-data] [{: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) :selected-storage-type :advanced)
(assoc-in [:keycard :flow] :recovery) (assoc-in [:keycard :flow] :recovery)
(assoc-in [:keycard :from-key-storage-and-migration?] true) (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))} (dissoc :multiaccounts/key-storage))}
(popover/hide-popover) (popover/hide-popover)
(navigation/navigate-to-cofx :keycard-onboarding-intro nil))) (navigation/navigate-to-cofx :keycard-onboarding-intro nil)))

View File

@ -337,12 +337,15 @@
(fx/defn login-only-events (fx/defn login-only-events
[{:keys [db] :as cofx} key-uid password save-password?] [{:keys [db] :as cofx} key-uid password save-password?]
(let [auth-method (:auth-method db) (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" (log/debug "[login] login-only-events"
"auth-method" auth-method "auth-method" auth-method
"new-auth-method" new-auth-method) "new-auth-method" new-auth-method)
(fx/merge cofx (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 ::json-rpc/call
[{:method "browsers_getBrowsers" [{:method "browsers_getBrowsers"
:on-success #(re-frame/dispatch [::initialize-browsers %])} :on-success #(re-frame/dispatch [::initialize-browsers %])}
@ -354,6 +357,8 @@
:on-success #(do (re-frame/dispatch [::get-settings-callback %]) :on-success #(do (re-frame/dispatch [::get-settings-callback %])
(redirect-to-root db))}]} (redirect-to-root db))}]}
(notifications/load-notification-preferences) (notifications/load-notification-preferences)
(when from-migration?
(utils/show-popup (i18n/label :t/migration-successful) (i18n/label :t/migration-successful-text)))
(when save-password? (when save-password?
(keychain/save-user-password key-uid password)) (keychain/save-user-password key-uid password))
(keychain/save-auth-method key-uid (or new-auth-method auth-method keychain/auth-method-none))))) (keychain/save-auth-method key-uid (or new-auth-method auth-method keychain/auth-method-none)))))

View File

@ -192,6 +192,12 @@
(log/debug "[native-module] verify") (log/debug "[native-module] verify")
(.verify ^js (status) address hashed-password callback)) (.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 (defn login-with-keycard
[{:keys [key-uid multiaccount-data password chat-key]}] [{:keys [key-uid multiaccount-data password chat-key]}]
(log/debug "[native-module] login-with-keycard") (log/debug "[native-module] login-with-keycard")
@ -414,3 +420,14 @@
(init-keystore (init-keystore
key-uid key-uid
#(.reEncryptDbAndKeystore ^js (status) key-uid current-password# new-password# callback))) #(.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))

View File

@ -3,6 +3,7 @@
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[status-im.ui.screens.home.sheet.views :as home.sheet] [status-im.ui.screens.home.sheet.views :as home.sheet]
[status-im.ui.screens.keycard.views :as keycard] [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.about-app.views :as about-app]
[status-im.ui.screens.multiaccounts.recover.views :as recover.views] [status-im.ui.screens.multiaccounts.recover.views :as recover.views]
[quo.core :as quo])) [quo.core :as quo]))
@ -33,7 +34,10 @@
(merge about-app/learn-more) (merge about-app/learn-more)
(= view :recover-sheet) (= 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 [quo/bottom-sheet opts
(when content (when content
[content])])) [content])]))

View File

@ -24,3 +24,11 @@
{:color colors/gray {:color colors/gray
:text-align :center :text-align :center
:line-height 22}) :line-height 22})
(def header
{:flex-direction :row
:align-items :center
:justify-content :space-between
:padding-top 16
:padding-left 16
:margin-bottom 11})

View File

@ -55,7 +55,7 @@
;; Component to render Key and Storage management screen ;; Component to render Key and Storage management screen
(defview actions-base [{:keys [next-title next-event]}] (defview actions-base [{:keys [next-title next-event]}]
(letsubs [{:keys [name] :as multiaccount} [:multiaccounts/login] (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} [react/view {:flex 1}
[local-topbar (i18n/label :t/choose-actions)] [local-topbar (i18n/label :t/choose-actions)]
[accordion/section {:title name [accordion/section {:title name
@ -77,8 +77,8 @@
[quo/list-item {:title (i18n/label :t/reset-database) [quo/list-item {:title (i18n/label :t/reset-database)
:subtitle (i18n/label :t/reset-database-warning) :subtitle (i18n/label :t/reset-database-warning)
:subtitle-max-lines 4 :subtitle-max-lines 4
:disabled true :active reset-db-checked?
:active move-keystore-checked? :on-press #(re-frame/dispatch [::multiaccounts.key-storage/reset-db-checked (not reset-db-checked?)])
:accessory :checkbox}]] :accessory :checkbox}]]
(when (and next-title next-event) (when (and next-title next-event)
[toolbar/toolbar {:show-border? true [toolbar/toolbar {:show-border? true
@ -195,9 +195,37 @@
:right [quo/button :right [quo/button
{:type :secondary {:type :secondary
:disabled (not keycard-storage-selected?) :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)]}]]])) (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 [] (defview seed-key-uid-mismatch-popover []
(letsubs [{:keys [name]} [:multiaccounts/login] (letsubs [{:keys [name]} [:multiaccounts/login]
{logged-in-name :name} [:multiaccount]] {logged-in-name :name} [:multiaccount]]

View File

@ -2,7 +2,7 @@
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' instead", "_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' instead",
"owner": "status-im", "owner": "status-im",
"repo": "status-go", "repo": "status-go",
"version": "v0.82.0", "version": "v0.83.0",
"commit-sha1": "2f6b32b1f54de5802882c0177bb0b16924d04271", "commit-sha1": "6c2e9652d0e77a732af8649a1a75961ac9ac820f",
"src-sha256": "1nlslsbibwnb7s6mbxqsijxrsw3lrs1dqvqy6bhfxsvwi5x97a28" "src-sha256": "14fhj54d46xv3vh5qkbryxq7hdjrp13pabz8012qa592i0lf72fb"
} }

View File

@ -521,6 +521,10 @@
"pair-code-placeholder": "Pair code...", "pair-code-placeholder": "Pair code...",
"enter-pair-code-description": "Pairing code can be set from an already paired Status client", "enter-pair-code-description": "Pairing code can be set from an already paired Status client",
"enter-password": "Enter password", "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...", "password-placeholder":"Password...",
"confirm-password-placeholder": "Confirm your password...", "confirm-password-placeholder": "Confirm your password...",
"enter-pin": "Enter 6-digit passcode", "enter-pin": "Enter 6-digit passcode",