enable saving password on android
Signed-off-by: Igor Mandrigin <i@mandrigin.ru>
This commit is contained in:
parent
744abb3984
commit
2777809db9
|
@ -52,7 +52,7 @@ public class MainApplication extends MultiDexApplication implements ReactApplica
|
||||||
webViewDebugEnabled = true;
|
webViewDebugEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusPackage statusPackage = new StatusPackage(BuildConfig.DEBUG, devCluster);
|
StatusPackage statusPackage = new StatusPackage(BuildConfig.DEBUG, devCluster, RootUtil.isDeviceRooted());
|
||||||
Function<String, String> callRPC = statusPackage.getCallRPC();
|
Function<String, String> callRPC = statusPackage.getCallRPC();
|
||||||
List<ReactPackage> packages = new ArrayList<ReactPackage>(Arrays.asList(
|
List<ReactPackage> packages = new ArrayList<ReactPackage>(Arrays.asList(
|
||||||
new MainReactPackage(),
|
new MainReactPackage(),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
(ns env.config)
|
(ns env.config)
|
||||||
|
|
||||||
(def figwheel-urls {:android "ws://192.168.10.203:3449/figwheel-ws",
|
(def figwheel-urls {:android "ws://localhost:3449/figwheel-ws",
|
||||||
:ios "ws://localhost:3449/figwheel-ws",
|
:ios "ws://192.168.0.9:3449/figwheel-ws",
|
||||||
:desktop "ws://localhost:3449/figwheel-ws"})
|
:desktop "ws://localhost:3449/figwheel-ws"}
|
||||||
|
)
|
|
@ -68,7 +68,7 @@ PODS:
|
||||||
- React
|
- React
|
||||||
- React/Core (0.56.0):
|
- React/Core (0.56.0):
|
||||||
- yoga (= 0.56.0.React)
|
- yoga (= 0.56.0.React)
|
||||||
- RNKeychain (3.0.0):
|
- RNKeychain (3.0.0-rc.3):
|
||||||
- React
|
- React
|
||||||
- yoga (0.56.0.React)
|
- yoga (0.56.0.React)
|
||||||
|
|
||||||
|
@ -123,4 +123,4 @@ SPEC CHECKSUMS:
|
||||||
|
|
||||||
PODFILE CHECKSUM: 7636f960a0dbec2dd55b8b20e244befa3fdb4438
|
PODFILE CHECKSUM: 7636f960a0dbec2dd55b8b20e244befa3fdb4438
|
||||||
|
|
||||||
COCOAPODS: 1.5.2
|
COCOAPODS: 1.5.3
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
"react-native-image-crop-picker": "0.18.1",
|
"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-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-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-level-fs": "3.0.1",
|
||||||
"react-native-os": "https://github.com/status-im/react-native-os.git#1.1.0-1",
|
"react-native-os": "https://github.com/status-im/react-native-os.git#1.1.0-1",
|
||||||
"react-native-qrcode": "0.2.7",
|
"react-native-qrcode": "0.2.7",
|
||||||
|
|
|
@ -5936,10 +5936,9 @@ react-native-invertible-scroll-view@1.1.0:
|
||||||
react-clone-referenced-element "^1.0.1"
|
react-clone-referenced-element "^1.0.1"
|
||||||
react-native-scrollable-mixin "^1.0.1"
|
react-native-scrollable-mixin "^1.0.1"
|
||||||
|
|
||||||
react-native-keychain@3.0.0:
|
"react-native-keychain@https://github.com/status-im/react-native-keychain#v.3.0.0-status":
|
||||||
version "3.0.0"
|
version "3.0.0-rc.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-keychain/-/react-native-keychain-3.0.0.tgz#29da1dfa43c2581f76bf9420914fd38a1558cf18"
|
resolved "https://github.com/status-im/react-native-keychain#43e5512cabb8ee064fd9e503be943dcf2c7d7669"
|
||||||
integrity sha512-0incABt1+aXsZvG34mDV57KKanSB+iMHmxvWv+N6lgpNLaSoqrCxazjbZdeqD4qJ7Z+Etp5CLf/4v1aI+sNLBw==
|
|
||||||
|
|
||||||
react-native-level-fs@3.0.1:
|
react-native-level-fs@3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
|
|
|
@ -61,8 +61,9 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
||||||
private boolean debug;
|
private boolean debug;
|
||||||
private boolean devCluster;
|
private boolean devCluster;
|
||||||
private ReactApplicationContext reactContext;
|
private ReactApplicationContext reactContext;
|
||||||
|
private boolean rootedDevice;
|
||||||
|
|
||||||
StatusModule(ReactApplicationContext reactContext, boolean debug, boolean devCluster) {
|
StatusModule(ReactApplicationContext reactContext, boolean debug, boolean devCluster, boolean rootedDevice) {
|
||||||
super(reactContext);
|
super(reactContext);
|
||||||
if (executor == null) {
|
if (executor == null) {
|
||||||
executor = Executors.newCachedThreadPool();
|
executor = Executors.newCachedThreadPool();
|
||||||
|
@ -70,6 +71,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
||||||
this.debug = debug;
|
this.debug = debug;
|
||||||
this.devCluster = devCluster;
|
this.devCluster = devCluster;
|
||||||
this.reactContext = reactContext;
|
this.reactContext = reactContext;
|
||||||
|
this.rootedDevice = rootedDevice;
|
||||||
reactContext.addLifecycleEventListener(this);
|
reactContext.addLifecycleEventListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -989,4 +991,9 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
||||||
constants.put("is24Hour", this.is24Hour());
|
constants.put("is24Hour", this.is24Hour());
|
||||||
return constants;
|
return constants;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void isDeviceRooted(final Callback callback) {
|
||||||
|
callback.invoke(rootedDevice);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,12 @@ public class StatusPackage implements ReactPackage {
|
||||||
|
|
||||||
private boolean debug;
|
private boolean debug;
|
||||||
private boolean devCluster;
|
private boolean devCluster;
|
||||||
|
private boolean rootedDevice;
|
||||||
|
|
||||||
public StatusPackage (boolean debug, boolean devCluster) {
|
public StatusPackage (boolean debug, boolean devCluster, boolean rootedDevice) {
|
||||||
this.debug = debug;
|
this.debug = debug;
|
||||||
this.devCluster = devCluster;
|
this.devCluster = devCluster;
|
||||||
|
this.rootedDevice = rootedDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -27,7 +29,7 @@ public class StatusPackage implements ReactPackage {
|
||||||
List<NativeModule> modules = new ArrayList<>();
|
List<NativeModule> modules = new ArrayList<>();
|
||||||
System.loadLibrary("statusgoraw");
|
System.loadLibrary("statusgoraw");
|
||||||
System.loadLibrary("statusgo");
|
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;
|
return modules;
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,3 +84,5 @@
|
||||||
(def disable-installation native-module/disable-installation)
|
(def disable-installation native-module/disable-installation)
|
||||||
|
|
||||||
(def update-mailservers native-module/update-mailservers)
|
(def update-mailservers native-module/update-mailservers)
|
||||||
|
|
||||||
|
(def rooted-device? native-module/rooted-device?)
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
[status-im.utils.platform :as p]
|
[status-im.utils.platform :as p]
|
||||||
[status-im.utils.async :as async-util]
|
[status-im.utils.async :as async-util]
|
||||||
[status-im.react-native.js-dependencies :as rn-dependencies]
|
[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
|
;; if StatusModule is not initialized better to store
|
||||||
;; calls and make them only when StatusModule is ready
|
;; calls and make them only when StatusModule is ready
|
||||||
|
@ -171,3 +172,24 @@
|
||||||
(defn update-mailservers [enodes on-result]
|
(defn update-mailservers [enodes on-result]
|
||||||
(when status
|
(when status
|
||||||
(call-module #(.updateMailservers status enodes on-result))))
|
(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)))
|
||||||
|
|
|
@ -52,3 +52,11 @@
|
||||||
:text-align :center
|
:text-align :center
|
||||||
:flex-direction :row
|
:flex-direction :row
|
||||||
:align-items :center})
|
: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})
|
||||||
|
|
|
@ -18,7 +18,8 @@
|
||||||
[cljs.spec.alpha :as spec]
|
[cljs.spec.alpha :as spec]
|
||||||
[status-im.utils.platform :as platform]
|
[status-im.utils.platform :as platform]
|
||||||
[status-im.accounts.db :as db]
|
[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?]
|
(defn login-toolbar [can-navigate-back?]
|
||||||
[toolbar/toolbar
|
[toolbar/toolbar
|
||||||
|
@ -81,15 +82,22 @@
|
||||||
(re-frame/dispatch [:set-in [:accounts/login :error] ""]))
|
(re-frame/dispatch [:set-in [:accounts/login :error] ""]))
|
||||||
:secure-text-entry true
|
:secure-text-entry true
|
||||||
:error (when (not-empty error) (i18n/label (error-key error)))}]]
|
: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}
|
[react/view {:style styles/save-password-checkbox-container}
|
||||||
|
(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
|
[profile.components/settings-switch-item
|
||||||
{:label-kw (if can-save-password?
|
{:label-kw (if can-save-password?
|
||||||
:t/save-password
|
:t/save-password
|
||||||
:t/save-password-unavailable)
|
:t/save-password-unavailable)
|
||||||
:active? can-save-password?
|
:active? can-save-password?
|
||||||
:value save-password?
|
:value save-password?
|
||||||
:action-fn #(re-frame/dispatch [:set-in [:accounts/login :save-password?] %])}]])]]
|
:action-fn #(re-frame/dispatch [:set-in [:accounts/login :save-password?] %])}])])]]
|
||||||
(when processing
|
(when processing
|
||||||
[react/view styles/processing-view
|
[react/view styles/processing-view
|
||||||
[components/activity-indicator {:animating true}]
|
[components/activity-indicator {:animating true}]
|
||||||
|
|
|
@ -3,10 +3,12 @@
|
||||||
[taoensso.timbre :as log]
|
[taoensso.timbre :as log]
|
||||||
[status-im.react-native.js-dependencies :as rn]
|
[status-im.react-native.js-dependencies :as rn]
|
||||||
[status-im.utils.platform :as platform]
|
[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 key-bytes 64)
|
||||||
(def username "status-im.encryptionkey")
|
(def username "status-im.encryptionkey")
|
||||||
|
(def android-keystore-min-version 23)
|
||||||
|
|
||||||
(defn- bytes->js-array [b]
|
(defn- bytes->js-array [b]
|
||||||
(.from js/Array b))
|
(.from js/Array b))
|
||||||
|
@ -14,6 +16,19 @@
|
||||||
(defn- string->js-array [s]
|
(defn- string->js-array [s]
|
||||||
(.parse js/JSON (.-password 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
|
;; 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
|
;; We are using set/get/reset internet credentials there because they are bound
|
||||||
;; to an address (`server`) property.
|
;; to an address (`server`) property.
|
||||||
|
|
||||||
|
|
||||||
(defn enum-val [enum-name value-name]
|
(defn enum-val [enum-name value-name]
|
||||||
(get-in (js->clj rn/keychain) [enum-name value-name]))
|
(get-in (js->clj rn/keychain) [enum-name value-name]))
|
||||||
|
|
||||||
|
@ -45,13 +59,35 @@
|
||||||
;; Note that the password won't be stored if the device isn't locked by a passcode.
|
;; 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
|
;; Stores the password for the address to the Keychain
|
||||||
(defn save-user-password [address password callback]
|
(defn save-user-password [address password callback]
|
||||||
(if-not platform/ios?
|
(-> (.setInternetCredentials rn/keychain address address password keychain-secure-hardware (clj->js keychain-restricted-availability))
|
||||||
(callback true) ;; no-op on Androids (for now)
|
(.then callback)))
|
||||||
(-> (.setInternetCredentials rn/keychain address address password
|
|
||||||
(clj->js keychain-restricted-availability))
|
|
||||||
(.then callback))))
|
|
||||||
|
|
||||||
(defn handle-callback [callback result]
|
(defn handle-callback [callback result]
|
||||||
(if result
|
(if result
|
||||||
|
@ -60,29 +96,30 @@
|
||||||
|
|
||||||
;; Gets the password for a specified address from the Keychain
|
;; Gets the password for a specified address from the Keychain
|
||||||
(defn get-user-password [address callback]
|
(defn get-user-password [address callback]
|
||||||
(if-not platform/ios?
|
(if (or platform/ios? platform/android?)
|
||||||
(callback) ;; no-op on Androids (for now)
|
|
||||||
(-> (.getInternetCredentials rn/keychain address)
|
(-> (.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
|
;; Clears the password for a specified address from the Keychain
|
||||||
;; (example of usage is logout or signing in w/o "save-password")
|
;; (example of usage is logout or signing in w/o "save-password")
|
||||||
(defn clear-user-password [address callback]
|
(defn clear-user-password [address callback]
|
||||||
(if-not platform/ios?
|
(if (or platform/ios? platform/android?)
|
||||||
(callback true)
|
|
||||||
(-> (.resetInternetCredentials rn/keychain address)
|
(-> (.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.
|
;; Resolves to `false` if the device doesn't have neither a passcode nor a biometry auth.
|
||||||
(defn can-save-user-password? [callback]
|
(defn can-save-user-password? [callback]
|
||||||
(if-not platform/ios?
|
(cond
|
||||||
(callback false)
|
platform/ios? (device-encrypted? callback)
|
||||||
(-> (.canImplyAuthentication
|
|
||||||
rn/keychain
|
platform/android? (check-conditions
|
||||||
(clj->js
|
callback
|
||||||
{:authenticationType
|
secure-hardware-available?
|
||||||
(enum-val "ACCESS_CONTROL" "BIOMETRY_ANY_OR_DEVICE_PASSCODE")}))
|
device-not-rooted?)
|
||||||
(.then callback))))
|
|
||||||
|
:else (callback false)))
|
||||||
|
|
||||||
;; ********************************************************************************
|
;; ********************************************************************************
|
||||||
;; Storing / Retrieving the realm encryption key to/from the Keychain
|
;; Storing / Retrieving the realm encryption key to/from the Keychain
|
||||||
|
|
|
@ -36,3 +36,6 @@
|
||||||
android? (str (.-DocumentDirectoryPath rn-dependencies/fs)
|
android? (str (.-DocumentDirectoryPath rn-dependencies/fs)
|
||||||
"/../no_backup")
|
"/../no_backup")
|
||||||
ios? (.-LibraryDirectoryPath rn-dependencies/fs)))
|
ios? (.-LibraryDirectoryPath rn-dependencies/fs)))
|
||||||
|
|
||||||
|
(defn android-version>= [v]
|
||||||
|
(and android? (>= version v)))
|
||||||
|
|
|
@ -136,6 +136,7 @@
|
||||||
"twelve-words-in-correct-order": "12 words in correct order",
|
"twelve-words-in-correct-order": "12 words in correct order",
|
||||||
"permissions": "Permissions",
|
"permissions": "Permissions",
|
||||||
"save-password-unavailable": "Set device passcode to save password",
|
"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",
|
"currency-display-name-inr": "India Rupee",
|
||||||
"transaction-moved-title": "Transaction moved",
|
"transaction-moved-title": "Transaction moved",
|
||||||
"counter-9-plus": "9+",
|
"counter-9-plus": "9+",
|
||||||
|
|
Loading…
Reference in New Issue