enable saving password on android

Signed-off-by: Igor Mandrigin <i@mandrigin.ru>
This commit is contained in:
Roman Volosovskyi 2018-11-06 13:19:11 +01:00 committed by Igor Mandrigin
parent 744abb3984
commit 2777809db9
No known key found for this signature in database
GPG Key ID: 4A0EDDE26E66BC8B
14 changed files with 137 additions and 47 deletions

View File

@ -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<String, String> callRPC = statusPackage.getCallRPC();
List<ReactPackage> packages = new ArrayList<ReactPackage>(Arrays.asList(
new MainReactPackage(),

View File

@ -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"}
)

View File

@ -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

View File

@ -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",

View File

@ -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"

View File

@ -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);
}
@ -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);
}
}

View File

@ -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<NativeModule> 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;
}

View File

@ -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?)

View File

@ -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)))

View File

@ -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})

View File

@ -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}]

View File

@ -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

View File

@ -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)))

View File

@ -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+",