diff --git a/android/app/src/main/java/im/status/ethereum/MainApplication.java b/android/app/src/main/java/im/status/ethereum/MainApplication.java index e8796fcdf2..6f49b79a4b 100644 --- a/android/app/src/main/java/im/status/ethereum/MainApplication.java +++ b/android/app/src/main/java/im/status/ethereum/MainApplication.java @@ -52,7 +52,7 @@ public class MainApplication extends MultiDexApplication implements ReactApplica webViewDebugEnabled = true; } - StatusPackage statusPackage = new StatusPackage(BuildConfig.DEBUG, devCluster); + StatusPackage statusPackage = new StatusPackage(BuildConfig.DEBUG, devCluster, RootUtil.isDeviceRooted()); Function callRPC = statusPackage.getCallRPC(); List packages = new ArrayList(Arrays.asList( new MainReactPackage(), diff --git a/env/dev/env/config.cljs b/env/dev/env/config.cljs index aa09f4dcda..af77efd812 100644 --- a/env/dev/env/config.cljs +++ b/env/dev/env/config.cljs @@ -1,5 +1,6 @@ (ns env.config) -(def figwheel-urls {:android "ws://192.168.10.203:3449/figwheel-ws", - :ios "ws://localhost:3449/figwheel-ws", - :desktop "ws://localhost:3449/figwheel-ws"}) +(def figwheel-urls {:android "ws://localhost:3449/figwheel-ws", + :ios "ws://192.168.0.9:3449/figwheel-ws", + :desktop "ws://localhost:3449/figwheel-ws"} +) \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 033b93def1..9f8ca4ec7c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -68,7 +68,7 @@ PODS: - React - React/Core (0.56.0): - yoga (= 0.56.0.React) - - RNKeychain (3.0.0): + - RNKeychain (3.0.0-rc.3): - React - yoga (0.56.0.React) @@ -123,4 +123,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 7636f960a0dbec2dd55b8b20e244befa3fdb4438 -COCOAPODS: 1.5.2 +COCOAPODS: 1.5.3 diff --git a/mobile_files/package.json.orig b/mobile_files/package.json.orig index 94a3c3225c..88c00af624 100644 --- a/mobile_files/package.json.orig +++ b/mobile_files/package.json.orig @@ -47,7 +47,7 @@ "react-native-image-crop-picker": "0.18.1", "react-native-image-resizer": "https://github.com/status-im/react-native-image-resizer.git#1.0.0-1", "react-native-invertible-scroll-view": "1.1.0", - "react-native-keychain": "3.0.0", + "react-native-keychain": "https://github.com/status-im/react-native-keychain#v.3.0.0-status", "react-native-level-fs": "3.0.1", "react-native-os": "https://github.com/status-im/react-native-os.git#1.1.0-1", "react-native-qrcode": "0.2.7", diff --git a/mobile_files/yarn.lock b/mobile_files/yarn.lock index c4c7d3a47d..e59d518039 100644 --- a/mobile_files/yarn.lock +++ b/mobile_files/yarn.lock @@ -5936,10 +5936,9 @@ react-native-invertible-scroll-view@1.1.0: react-clone-referenced-element "^1.0.1" react-native-scrollable-mixin "^1.0.1" -react-native-keychain@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/react-native-keychain/-/react-native-keychain-3.0.0.tgz#29da1dfa43c2581f76bf9420914fd38a1558cf18" - integrity sha512-0incABt1+aXsZvG34mDV57KKanSB+iMHmxvWv+N6lgpNLaSoqrCxazjbZdeqD4qJ7Z+Etp5CLf/4v1aI+sNLBw== +"react-native-keychain@https://github.com/status-im/react-native-keychain#v.3.0.0-status": + version "3.0.0-rc.3" + resolved "https://github.com/status-im/react-native-keychain#43e5512cabb8ee064fd9e503be943dcf2c7d7669" react-native-level-fs@3.0.1: version "3.0.1" 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 a93e5bc979..f081356d37 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 @@ -61,8 +61,9 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL private boolean debug; private boolean devCluster; private ReactApplicationContext reactContext; + private boolean rootedDevice; - StatusModule(ReactApplicationContext reactContext, boolean debug, boolean devCluster) { + StatusModule(ReactApplicationContext reactContext, boolean debug, boolean devCluster, boolean rootedDevice) { super(reactContext); if (executor == null) { executor = Executors.newCachedThreadPool(); @@ -70,6 +71,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL this.debug = debug; this.devCluster = devCluster; this.reactContext = reactContext; + this.rootedDevice = rootedDevice; reactContext.addLifecycleEventListener(this); } @@ -857,7 +859,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL Log.d(TAG, "AppStateChange: " + type); Statusgo.AppStateChange(type); } - + private static String uniqueID = null; private static final String PREF_UNIQUE_ID = "PREF_UNIQUE_ID"; @@ -989,4 +991,9 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL constants.put("is24Hour", this.is24Hour()); return constants; } + + @ReactMethod + public void isDeviceRooted(final Callback callback) { + callback.invoke(rootedDevice); + } } diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusPackage.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusPackage.java index 88e26593f0..3b9546a779 100644 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusPackage.java +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusPackage.java @@ -16,10 +16,12 @@ public class StatusPackage implements ReactPackage { private boolean debug; private boolean devCluster; + private boolean rootedDevice; - public StatusPackage (boolean debug, boolean devCluster) { + public StatusPackage (boolean debug, boolean devCluster, boolean rootedDevice) { this.debug = debug; this.devCluster = devCluster; + this.rootedDevice = rootedDevice; } @Override @@ -27,7 +29,7 @@ public class StatusPackage implements ReactPackage { List modules = new ArrayList<>(); System.loadLibrary("statusgoraw"); System.loadLibrary("statusgo"); - modules.add(new StatusModule(reactContext, this.debug, this.devCluster)); + modules.add(new StatusModule(reactContext, this.debug, this.devCluster, this.rootedDevice )); return modules; } diff --git a/src/status_im/native_module/core.cljs b/src/status_im/native_module/core.cljs index 8839b6ab64..2545862656 100644 --- a/src/status_im/native_module/core.cljs +++ b/src/status_im/native_module/core.cljs @@ -84,3 +84,5 @@ (def disable-installation native-module/disable-installation) (def update-mailservers native-module/update-mailservers) + +(def rooted-device? native-module/rooted-device?) diff --git a/src/status_im/native_module/impl/module.cljs b/src/status_im/native_module/impl/module.cljs index c81610d59a..29b65e87be 100644 --- a/src/status_im/native_module/impl/module.cljs +++ b/src/status_im/native_module/impl/module.cljs @@ -8,7 +8,8 @@ [status-im.utils.platform :as p] [status-im.utils.async :as async-util] [status-im.react-native.js-dependencies :as rn-dependencies] - [clojure.string :as string])) + [clojure.string :as string] + [status-im.utils.platform :as platform])) ;; if StatusModule is not initialized better to store ;; calls and make them only when StatusModule is ready @@ -171,3 +172,24 @@ (defn update-mailservers [enodes on-result] (when status (call-module #(.updateMailservers status enodes on-result)))) + +(defn rooted-device? [callback] + (cond + ;; we assume that iOS is safe by default + platform/ios? + (callback false) + + ;; we assume that Desktop is unsafe by default + ;; (theoretically, Desktop is always "rooted", by design + platform/desktop? + (callback true) + + ;; we check root on android + platform/android? + (if status + (call-module #(.isDeviceRooted status callback)) + ;; if module isn't initialized we return true to avoid degrading security + (callback true)) + + ;; in unknown scenarios we also consider the device rooted to avoid degrading security + :else (callback true))) diff --git a/src/status_im/ui/screens/accounts/login/styles.cljs b/src/status_im/ui/screens/accounts/login/styles.cljs index 59104c10c6..0bcbdb100c 100644 --- a/src/status_im/ui/screens/accounts/login/styles.cljs +++ b/src/status_im/ui/screens/accounts/login/styles.cljs @@ -52,3 +52,11 @@ :text-align :center :flex-direction :row :align-items :center}) + +(def save-password-unavailable-android + {:margin-top 8 + :width "100%" + :color colors/text-gray + :text-align :center + :flex-direction :row + :align-items :center}) diff --git a/src/status_im/ui/screens/accounts/login/views.cljs b/src/status_im/ui/screens/accounts/login/views.cljs index 403fd1dd9d..9c265bef82 100644 --- a/src/status_im/ui/screens/accounts/login/views.cljs +++ b/src/status_im/ui/screens/accounts/login/views.cljs @@ -18,7 +18,8 @@ [cljs.spec.alpha :as spec] [status-im.utils.platform :as platform] [status-im.accounts.db :as db] - [status-im.utils.security :as security])) + [status-im.utils.security :as security] + [status-im.utils.keychain.core :as keychain])) (defn login-toolbar [can-navigate-back?] [toolbar/toolbar @@ -81,15 +82,22 @@ (re-frame/dispatch [:set-in [:accounts/login :error] ""])) :secure-text-entry true :error (when (not-empty error) (i18n/label (error-key error)))}]] - (when platform/ios? + + (when-not platform/desktop? + ;; saving passwords is unavailable on Desktop [react/view {:style styles/save-password-checkbox-container} - [profile.components/settings-switch-item - {:label-kw (if can-save-password? - :t/save-password - :t/save-password-unavailable) - :active? can-save-password? - :value save-password? - :action-fn #(re-frame/dispatch [:set-in [:accounts/login :save-password?] %])}]])]] + (if (and platform/android? (not can-save-password?)) + ;; on Android, there is much more reasons for the password save to be unavailable, + ;; so we don't show the checkbox whatsoever but put a label explaining why it happenned. + [react/i18n-text {:style styles/save-password-unavailable-android + :key :save-password-unavailable-android}] + [profile.components/settings-switch-item + {:label-kw (if can-save-password? + :t/save-password + :t/save-password-unavailable) + :active? can-save-password? + :value save-password? + :action-fn #(re-frame/dispatch [:set-in [:accounts/login :save-password?] %])}])])]] (when processing [react/view styles/processing-view [components/activity-indicator {:animating true}] diff --git a/src/status_im/utils/keychain/core.cljs b/src/status_im/utils/keychain/core.cljs index 166882d74d..a1d241cbbb 100644 --- a/src/status_im/utils/keychain/core.cljs +++ b/src/status_im/utils/keychain/core.cljs @@ -3,10 +3,12 @@ [taoensso.timbre :as log] [status-im.react-native.js-dependencies :as rn] [status-im.utils.platform :as platform] - [status-im.utils.security :as security])) + [status-im.utils.security :as security] + [status-im.native-module.core :as status])) (def key-bytes 64) (def username "status-im.encryptionkey") +(def android-keystore-min-version 23) (defn- bytes->js-array [b] (.from js/Array b)) @@ -14,6 +16,19 @@ (defn- string->js-array [s] (.parse js/JSON (.-password s))) +(defn- check-conditions [callback & checks] + (if (= (count checks) 0) + (callback true) + (let [current-check-fn (first checks) + process-check-result (fn [callback-success callback-fail] + (fn [current-check-passed?] + (if current-check-passed? + (callback-success) + (callback-fail))))] + (current-check-fn (process-check-result + #(apply (partial check-conditions callback) (rest checks)) + #(callback false)))))) + ;; ******************************************************************************** ;; Storing / Retrieving a user password to/from Keychain ;; ******************************************************************************** @@ -21,7 +36,6 @@ ;; We are using set/get/reset internet credentials there because they are bound ;; to an address (`server`) property. - (defn enum-val [enum-name value-name] (get-in (js->clj rn/keychain) [enum-name value-name])) @@ -43,15 +57,37 @@ ;; > you might choose kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly. ;; That is exactly what we use there. ;; Note that the password won't be stored if the device isn't locked by a passcode. - {:accessible (enum-val "ACCESSIBLE" "WHEN_PASSCODE_SET_THIS_DEVICE_ONLY")}) + {:accessible (enum-val "ACCESSIBLE" "WHEN_PASSCODE_SET_THIS_DEVICE_ONLY")}) + +(def keychain-secure-hardware + ;; (Android) Requires storing the encryption key for the entry in secure hardware + ;; or StrongBox (see https://developer.android.com/training/articles/keystore#ExtractionPrevention) + "SECURE_HARDWARE") + +;; These helpers check if the device is okay to use for password storage +;; They resolve callback with `true` if the check is passed, with `false` otherwise. +;; Android only +(defn- device-not-rooted? [callback] + (status/rooted-device? (fn [rooted?] (callback (not rooted?))))) + +;; Android only +(defn- secure-hardware-available? [callback] + (-> (.getSecurityLevel rn/keychain) + (.then (fn [level] (callback (= level keychain-secure-hardware)))))) + +;; iOS only +(defn- device-encrypted? [callback] + (-> (.canImplyAuthentication + rn/keychain + (clj->js + {:authenticationType + (enum-val "ACCESS_CONTROL" "BIOMETRY_ANY_OR_DEVICE_PASSCODE")})) + (.then callback))) ;; Stores the password for the address to the Keychain (defn save-user-password [address password callback] - (if-not platform/ios? - (callback true) ;; no-op on Androids (for now) - (-> (.setInternetCredentials rn/keychain address address password - (clj->js keychain-restricted-availability)) - (.then callback)))) + (-> (.setInternetCredentials rn/keychain address address password keychain-secure-hardware (clj->js keychain-restricted-availability)) + (.then callback))) (defn handle-callback [callback result] (if result @@ -60,29 +96,30 @@ ;; Gets the password for a specified address from the Keychain (defn get-user-password [address callback] - (if-not platform/ios? - (callback) ;; no-op on Androids (for now) + (if (or platform/ios? platform/android?) (-> (.getInternetCredentials rn/keychain address) - (.then (partial handle-callback callback))))) + (.then (partial handle-callback callback))) + (callback))) ;; no-op for Desktop ;; Clears the password for a specified address from the Keychain ;; (example of usage is logout or signing in w/o "save-password") (defn clear-user-password [address callback] - (if-not platform/ios? - (callback true) + (if (or platform/ios? platform/android?) (-> (.resetInternetCredentials rn/keychain address) - (.then callback)))) + (.then callback)) + (callback true))) ;; no-op for Desktop ;; Resolves to `false` if the device doesn't have neither a passcode nor a biometry auth. (defn can-save-user-password? [callback] - (if-not platform/ios? - (callback false) - (-> (.canImplyAuthentication - rn/keychain - (clj->js - {:authenticationType - (enum-val "ACCESS_CONTROL" "BIOMETRY_ANY_OR_DEVICE_PASSCODE")})) - (.then callback)))) + (cond + platform/ios? (device-encrypted? callback) + + platform/android? (check-conditions + callback + secure-hardware-available? + device-not-rooted?) + + :else (callback false))) ;; ******************************************************************************** ;; Storing / Retrieving the realm encryption key to/from the Keychain diff --git a/src/status_im/utils/platform.cljs b/src/status_im/utils/platform.cljs index 6adf063cd2..5cc3f167d5 100644 --- a/src/status_im/utils/platform.cljs +++ b/src/status_im/utils/platform.cljs @@ -36,3 +36,6 @@ android? (str (.-DocumentDirectoryPath rn-dependencies/fs) "/../no_backup") ios? (.-LibraryDirectoryPath rn-dependencies/fs))) + +(defn android-version>= [v] + (and android? (>= version v))) diff --git a/translations/en.json b/translations/en.json index c45a32eaee..ffe1933696 100644 --- a/translations/en.json +++ b/translations/en.json @@ -136,6 +136,7 @@ "twelve-words-in-correct-order": "12 words in correct order", "permissions": "Permissions", "save-password-unavailable": "Set device passcode to save password", + "save-password-unavailable-android": "Save password is unavailable: your device may be rooted or lacks necessary security features.", "currency-display-name-inr": "India Rupee", "transaction-moved-title": "Transaction moved", "counter-9-plus": "9+",