From 090a4e7c7684d362eb1e429877b74327acafcae9 Mon Sep 17 00:00:00 2001 From: tbenr Date: Thu, 30 May 2019 16:01:20 +0200 Subject: [PATCH] fixes #6573 Signed-off-by: Andrey Shovkoplyas --- android/app/build.gradle | 1 + android/app/src/main/AndroidManifest.xml | 1 + .../im/status/ethereum/MainApplication.java | 4 +- android/settings.gradle | 2 + clj-rn.conf.edn | 1 + ios/Podfile | 1 + ios/Podfile.lock | 8 +- ios/StatusIm/Info.plist | 2 + mobile_files/package.json.orig | 1 + mobile_files/yarn.lock | 5 + .../status/ethereum/module/StatusModule.java | 4 + .../ios/RCTStatus/RCTStatus.h | 1 + .../ios/RCTStatus/RCTStatus.m | 47 ++++++++++ .../react_native/js_dependencies.cljs | 1 + .../react_native/js_dependencies.cljs | 1 + src/status_im/accounts/core.cljs | 9 ++ src/status_im/accounts/login/core.cljs | 28 ++++-- src/status_im/biometric_auth/core.cljs | 93 +++++++++++++++++++ src/status_im/constants.cljs | 2 + src/status_im/events.cljs | 22 ++++- src/status_im/init/core.cljs | 18 +++- src/status_im/native_module/core.cljs | 3 + src/status_im/native_module/impl/module.cljs | 7 ++ src/status_im/subs.cljs | 1 + src/status_im/ui/screens/db.cljs | 7 ++ src/status_im/ui/screens/events.cljs | 48 ++++++++-- .../ui/screens/profile/user/views.cljs | 17 +++- src/status_im/utils/keychain/core.cljs | 10 +- .../react_native/js_dependencies.cljs | 1 + translations/en.json | 13 +++ 30 files changed, 335 insertions(+), 24 deletions(-) create mode 100644 src/status_im/biometric_auth/core.cljs diff --git a/android/app/build.gradle b/android/app/build.gradle index d606b22b89..916d2adaa9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -276,6 +276,7 @@ dependencies { implementation project(':react-native-securerandom') implementation project(':react-native-webview-bridge') implementation project(':react-native-webview') + implementation project(':react-native-touch-id') implementation project(':react-native-config') implementation project(':react-native-firebase') implementation project(':react-native-shake') diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 14e2b18647..bc649f4fa7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ + 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 c6886a07bb..179a52628a 100644 --- a/android/app/src/main/java/im/status/ethereum/MainApplication.java +++ b/android/app/src/main/java/im/status/ethereum/MainApplication.java @@ -17,6 +17,7 @@ import com.ocetnik.timer.BackgroundTimerPackage; import com.reactcommunity.rnlanguages.RNLanguagesPackage; import com.reactnative.ivpusic.imagepicker.PickerPackage; import com.rnfs.RNFSPackage; +import com.rnfingerprint.FingerprintAuthPackage; import net.rhogan.rnsecurerandom.RNSecureRandomPackage; @@ -74,7 +75,8 @@ public class MainApplication extends MultiDexApplication implements ReactApplica new RNCWebViewPackage(), new ReactNativeConfigPackage(), new KeychainPackage(), - new RNShakeEventPackage()); + new RNShakeEventPackage(), + new FingerprintAuthPackage()); } @Override diff --git a/android/settings.gradle b/android/settings.gradle index 41ee4d0294..ca49acc3c5 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -38,6 +38,8 @@ include ':react-native-webview-bridge' project(':react-native-webview-bridge').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview-bridge/android') include ':react-native-webview' project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android') +include ':react-native-touch-id' +project(':react-native-touch-id').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-touch-id/android') include ':react-native-config' project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android') include ':react-native-android' diff --git a/clj-rn.conf.edn b/clj-rn.conf.edn index 55455b90ff..7d1ea20642 100644 --- a/clj-rn.conf.edn +++ b/clj-rn.conf.edn @@ -23,6 +23,7 @@ "react-native-webview-bridge" "react-native-webview" "react-native-firebase" + "react-native-touch-id" "homoglyph-finder" "web3" "web3-utils" diff --git a/ios/Podfile b/ios/Podfile index 5b863508b0..d732cd02de 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -18,6 +18,7 @@ target 'StatusIm' do pod 'RNKeychain', :path => '../node_modules/react-native-keychain' pod 'react-native-camera', path: '../node_modules/react-native-camera' pod 'react-native-webview', path: '../node_modules/react-native-webview' + pod 'TouchID', path: '../node_modules/react-native-touch-id' pod 'SQLCipher', '~>3.0' pod 'SSZipArchive' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 80c027c3cf..5b43971f46 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -78,6 +78,8 @@ PODS: - SQLCipher/standard (3.4.2): - SQLCipher/common - SSZipArchive (2.1.4) + - TouchID (4.4.1): + - React - yoga (0.59.3.React) DEPENDENCIES: @@ -90,6 +92,7 @@ DEPENDENCIES: - RNKeychain (from `../node_modules/react-native-keychain`) - SQLCipher (~> 3.0) - SSZipArchive + - TouchID (from `../node_modules/react-native-touch-id`) - yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -117,6 +120,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-webview" RNKeychain: :path: "../node_modules/react-native-keychain" + TouchID: + :path: "../node_modules/react-native-touch-id" yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -137,8 +142,9 @@ SPEC CHECKSUMS: RNKeychain: 627c6095cef215dd3d9804a9a9cf45ab96aa3997 SQLCipher: f9fcf29b2e59ced7defc2a2bdd0ebe79b40d4990 SSZipArchive: 41455d4b8d2b6ab93990820b50dc697c2554a322 + TouchID: b0640fedb86fa2db2fe1df15b61594ad49e76288 yoga: 128daf064cacaede0c3bb27424b6b4c71052e6cd -PODFILE CHECKSUM: 1b73a7ab29d939e99e86434864c837afaf55851c +PODFILE CHECKSUM: 13006106aa0716d54fffebf8dcb42006f0fb8798 COCOAPODS: 1.5.3 diff --git a/ios/StatusIm/Info.plist b/ios/StatusIm/Info.plist index a6c8f44069..8492a45750 100644 --- a/ios/StatusIm/Info.plist +++ b/ios/StatusIm/Info.plist @@ -4,6 +4,8 @@ CFBundleBuildUrl ???? + NSFaceIDUsageDescription + Enabling Face ID allows you quick and secure access to your account. CFBundleDevelopmentRegion en CFBundleDisplayName diff --git a/mobile_files/package.json.orig b/mobile_files/package.json.orig index a5b6582f43..9ab8299dd8 100644 --- a/mobile_files/package.json.orig +++ b/mobile_files/package.json.orig @@ -60,6 +60,7 @@ "react-native-svg": "^9.2.4", "react-native-svg-transformer": "^0.12.1", "react-native-tcp": "git+https://github.com/status-im/react-native-tcp.git#v3.3.0-1-status", + "react-native-touch-id": "^4.4.1", "react-native-udp": "git+https://github.com/status-im/react-native-udp.git#2.3.1-1", "react-native-webview": "^5.2.1", "react-native-webview-bridge": "git+https://github.com/status-im/react-native-webview-bridge.git#fix/classnames-colision", diff --git a/mobile_files/yarn.lock b/mobile_files/yarn.lock index 876bc1f811..4bb8bd9a12 100644 --- a/mobile_files/yarn.lock +++ b/mobile_files/yarn.lock @@ -5635,6 +5635,11 @@ react-native-tab-view@^1.0.0: process "^0.11.9" util "^0.10.3" +react-native-touch-id@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/react-native-touch-id/-/react-native-touch-id-4.4.1.tgz#8b1bb2d04c30bac36bb9696d2d723e719c4a8b08" + integrity sha512-1jTl8fC+0fxvqegy/XXTyo6vMvPhjzkoDdaqoYZx0OH8AT250NuXnNPyKktvigIcys3+2acciqOeaCall7lrvg== + "react-native-udp@git+https://github.com/status-im/react-native-udp.git#2.3.1-1": version "2.3.1" resolved "git+https://github.com/status-im/react-native-udp.git#6e9a817326208f6ca36fa42b20f5e530c1b39d37" 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 ab49ff5199..d37453e180 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 @@ -1122,6 +1122,10 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL HashMap constants = new HashMap(); constants.put("is24Hour", this.is24Hour()); + constants.put("model", Build.MODEL); + constants.put("brand", Build.BRAND); + constants.put("buildId", Build.ID); + constants.put("deviceId", Build.BOARD); return constants; } diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.h b/modules/react-native-status/ios/RCTStatus/RCTStatus.h index f09d7fe841..3f3873c8d8 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.h +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.h @@ -1,3 +1,4 @@ +#import #import #import #import "Statusgo/Statusgo.h" diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m index 207abc6203..77c3d79317 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m @@ -548,16 +548,63 @@ RCT_EXPORT_METHOD(setBlankPreviewFlag:(BOOL *)newValue) [userDefaults synchronize]; } +//// deviceinfo + - (bool) is24Hour { NSString *format = [NSDateFormatter dateFormatFromTemplate:@"j" options:0 locale:[NSLocale currentLocale]]; return ([format rangeOfString:@"a"].location == NSNotFound); } +- (NSString *)getBuildId { + return @"not available"; +} + +- (NSString*) deviceId +{ + struct utsname systemInfo; + + uname(&systemInfo); + + NSString* deviceId = [NSString stringWithCString:systemInfo.machine + encoding:NSUTF8StringEncoding]; + + if ([deviceId isEqualToString:@"i386"] || [deviceId isEqualToString:@"x86_64"] ) { + deviceId = [NSString stringWithFormat:@"%s", getenv("SIMULATOR_MODEL_IDENTIFIER")]; + } + + return deviceId; +} + +- (NSString*) deviceName +{ + + NSString* deviceName = nil; + + if ([self.deviceId rangeOfString:@"iPod"].location != NSNotFound) { + deviceName = @"iPod Touch"; + } + else if([self.deviceId rangeOfString:@"iPad"].location != NSNotFound) { + deviceName = @"iPad"; + } + else if([self.deviceId rangeOfString:@"iPhone"].location != NSNotFound){ + deviceName = @"iPhone"; + } + else if([self.deviceId rangeOfString:@"AppleTV"].location != NSNotFound){ + deviceName = @"Apple TV"; + } + + return deviceName; +} + - (NSDictionary *)constantsToExport { return @{ @"is24Hour": @(self.is24Hour), + @"model": self.deviceName ?: [NSNull null], + @"brand": @"Apple", + @"buildId": [self getBuildId], + @"deviceId": self.deviceId ?: [NSNull null], }; } diff --git a/react-native/src/desktop/status_im/react_native/js_dependencies.cljs b/react-native/src/desktop/status_im/react_native/js_dependencies.cljs index ae6eb8d308..e0f847d545 100644 --- a/react-native/src/desktop/status_im/react_native/js_dependencies.cljs +++ b/react-native/src/desktop/status_im/react_native/js_dependencies.cljs @@ -21,6 +21,7 @@ (def desktop-config (js/require "react-native-desktop-config")) (def desktop-shortcuts (js/require "react-native-desktop-shortcuts")) (def react-native-firebase (fn [] #js {})) +(def touchid (fn [] #js {})) (def camera (fn [] #js {:default #js {:constants {:Aspect "Portrait"}}})) (def status-keycard (fn [] #js {:default #js {}})) (def dialogs (fn [] #js {})) diff --git a/react-native/src/mobile/status_im/react_native/js_dependencies.cljs b/react-native/src/mobile/status_im/react_native/js_dependencies.cljs index c9f14f99c4..9c71e4c655 100644 --- a/react-native/src/mobile/status_im/react_native/js_dependencies.cljs +++ b/react-native/src/mobile/status_im/react_native/js_dependencies.cljs @@ -10,6 +10,7 @@ (def status-keycard (js-require/js-require "react-native-status-keycard")) (def realm (js/require "realm")) (def webview-bridge (js-require/js-require "react-native-webview-bridge")) +(defn touchid [] (.-default (js/require "react-native-touch-id"))) (def webview (js-require/js-require "react-native-webview")) (def securerandom (js-require/js-require "react-native-securerandom")) (defn secure-random [] (.-generateSecureRandom (securerandom))) diff --git a/src/status_im/accounts/core.cljs b/src/status_im/accounts/core.cljs index c38bf7583d..6bf57e65d0 100644 --- a/src/status_im/accounts/core.cljs +++ b/src/status_im/accounts/core.cljs @@ -80,6 +80,15 @@ (assoc settings :chaos-mode? chaos-mode?) {}))))) +(fx/defn switch-biometric-auth + {:events [:accounts.ui/switch-biometric-auth]} + [{:keys [db] :as cofx} biometric-auth?] + (when (:account/account db) + (let [settings (get-in db [:account/account :settings])] + (accounts.update/update-settings cofx + (assoc settings :biometric-auth? biometric-auth?) + {})))) + (fx/defn enable-notifications [cofx desktop-notifications?] (accounts.update/account-update cofx {:desktop-notifications? desktop-notifications?} diff --git a/src/status_im/accounts/login/core.cljs b/src/status_im/accounts/login/core.cljs index 5d61bcefc9..9b03fdb26f 100644 --- a/src/status_im/accounts/login/core.cljs +++ b/src/status_im/accounts/login/core.cljs @@ -19,6 +19,7 @@ [status-im.utils.keychain.core :as keychain] [status-im.utils.platform :as platform] [status-im.utils.security :as security] + [status-im.biometric-auth.core :as biometric-auth] [status-im.utils.types :as types] [status-im.utils.universal-links.core :as universal-links] [status-im.wallet.core :as wallet] @@ -297,7 +298,7 @@ [_ address] {:keychain/can-save-user-password? nil :keychain/get-user-password [address - #(re-frame/dispatch [:accounts.login.callback/get-user-password-success %])]}) + #(re-frame/dispatch [:accounts.login.callback/get-user-password-success % address])]}) (fx/defn open-login [{:keys [db] :as cofx} address photo-path name] (let [keycard-account? (get-in db [:accounts/accounts address :keycard-instance-uid])] @@ -315,13 +316,26 @@ (get-user-password address))))) (fx/defn open-login-callback + {:events [:accounts.login.callback/biometric-auth-done]} + [{:keys [db] :as cofx} password biometric-auth-result] + (let [{:keys [bioauth-success bioauth-notrequired bioauth-message]} biometric-auth-result] + (if (and password + (or bioauth-success bioauth-notrequired)) + (fx/merge cofx + {:db (assoc-in db [:accounts/login :password] password)} + (navigation/navigate-to-cofx :progress nil) + (user-login false)) + (fx/merge cofx + (when bioauth-message + {:utils/show-popup {:title (i18n/label :t/biometric-auth-login-error-title) :content bioauth-message}}) + (navigation/navigate-to-cofx :login nil))))) + +(fx/defn do-biometric-auth [{:keys [db] :as cofx} password] - (if password - (fx/merge cofx - {:db (assoc-in db [:accounts/login :password] password)} - (navigation/navigate-to-cofx :progress nil) - (user-login false)) - (navigation/navigate-to-cofx cofx :login nil))) + (biometric-auth/authenticate-fx cofx + #(re-frame/dispatch [:accounts.login.callback/biometric-auth-done password %]) + {:reason (i18n/label :t/biometric-auth-reason-login) + :ios-fallback-label (i18n/label :t/biometric-auth-login-ios-fallback-label)})) (re-frame/reg-fx :accounts.login/login diff --git a/src/status_im/biometric_auth/core.cljs b/src/status_im/biometric_auth/core.cljs new file mode 100644 index 0000000000..bb5a4f6635 --- /dev/null +++ b/src/status_im/biometric_auth/core.cljs @@ -0,0 +1,93 @@ +(ns status-im.biometric-auth.core + (:require [re-frame.core :as re-frame] + [status-im.utils.platform :as platform] + [status-im.i18n :as i18n] + [status-im.utils.fx :as fx] + [status-im.ui.components.colors :as colors] + [status-im.native-module.core :as status] + [status-im.react-native.js-dependencies :as rn])) + +;; currently, for android, react-native-touch-id +;; is not returning supported biometric type +;; defaulting to :fingerprint +(def android-default-support :fingerprint) + +;;; android blacklist based on device info: + +(def deviceinfo (status/get-device-model-info)) + +;; {:model ? +;; :brand "Xiaomi" +;; :build-id "13D15" +;; :device-id "goldfish" +;; more info on https://github.com/react-native-community/react-native-device-info + +(def android-device-blacklisted? + (cond + (= (:brand deviceinfo) "bannedbrand") true + :else false)) + +;; biometric auth config +;; https://github.com/naoufal/react-native-touch-id#authenticatereason-config +(defn- authenticate-options [ios-fallback-label] + (clj->js (merge + {:unifiedErrors true} + (when platform/ios? + {:passcodeFallback false + :fallbackLabel (or ios-fallback-label "")}) + (when platform/android? + {:title (i18n/label :t/biometric-auth-android-title) + :imageColor colors/blue + :imageErrorColor colors/red + :sensorDescription (i18n/label :t/biometric-auth-android-sensor-desc) + :sensorErrorDescription (i18n/label :t/biometric-auth-android-sensor-error-desc) + :cancelText (i18n/label :cancel)})))) + +(defn- get-error-message + "must return an error message for the user" + [touchid-error-code] + (cond + ;; no message if user canceled or falled back to password + (= touchid-error-code "USER_CANCELED") nil + (= touchid-error-code "USER_FALLBACK") nil + ;; add here more specific errors if needed + ;; https://github.com/naoufal/react-native-touch-id#unified-errors + :else (i18n/label :t/biometric-auth-error {:code touchid-error-code}))) + +(def success-result + {:bioauth-success true}) + +(defn- generate-error-result [touchid-error-obj] + (let [code (aget touchid-error-obj "code")] + {:bioauth-success false + :bioauth-code code + :bioauth-message (get-error-message code)})) + +(defn- do-get-supported [callback] + (-> (.isSupported (rn/touchid)) + (.then #(callback (or (keyword %) android-default-support))) + (.catch #(callback nil)))) + +(defn get-supported [callback] + (cond platform/ios? (do-get-supported callback) + platform/android? (if android-device-blacklisted? + (callback false) + (do-get-supported callback)) + :else (callback false))) + +(defn authenticate + ([cb] + (authenticate cb nil)) + ([cb {:keys [reason ios-fallback-label]}] + (-> (.authenticate (rn/touchid) reason (authenticate-options ios-fallback-label)) + (.then #(cb success-result)) + (.catch #(cb (generate-error-result %)))))) + +(fx/defn authenticate-fx + [_ cb options] + {:biometric-auth/authenticate [cb options]}) + +(re-frame/reg-fx + :biometric-auth/authenticate + (fn [[cb options]] + (authenticate #(cb %) options))) \ No newline at end of file diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 471bfbd4a8..019c46bdc2 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -5,6 +5,8 @@ (def ethereum-rpc-url "http://localhost:8545") +(def ms-in-bg-for-require-bioauth 5000) + (def content-type-text "text/plain") (def content-type-sticker "sticker") (def content-type-status "status") diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index bbf0c67e76..1a3fa97832 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -60,6 +60,7 @@ [status-im.wallet.core :as wallet] [status-im.wallet.db :as wallet.db] [status-im.web3.core :as web3] + [status-im.biometric-auth.core :as biomentric-auth] [taoensso.timbre :as log] [status-im.wallet.custom-tokens.core :as custom-tokens])) @@ -202,6 +203,20 @@ :on-accept open-chaos-unicorn-day-link}}) (accounts/switch-chaos-mode chaos-mode?))))) +(handlers/register-handler-fx + :accounts.ui/biometric-auth-switched + (fn [cofx [_ biometric-auth?]] + (if biometric-auth? + (biomentric-auth/authenticate-fx + cofx + (fn [{:keys [bioauth-success bioauth-message]}] + (when bioauth-success + (re-frame/dispatch [:accounts.ui/switch-biometric-auth true])) + (when bioauth-message + (utils/show-popup (i18n/label :t/biometric-auth-reason-verify) bioauth-message))) + {:reason (i18n/label :t/biometric-auth-reason-verify)}) + (accounts/switch-biometric-auth cofx false)))) + (handlers/register-handler-fx :accounts.ui/notifications-enabled (fn [cofx [_ desktop-notifications?]] @@ -325,8 +340,11 @@ (handlers/register-handler-fx :accounts.login.callback/get-user-password-success - (fn [cofx [_ password]] - (accounts.login/open-login-callback cofx password))) + (fn [{:keys [db] :as cofx} [_ password address]] + (let [biometric-auth? (get-in db [:accounts/accounts address :settings :biometric-auth?])] + (if (and password biometric-auth?) + (accounts.login/do-biometric-auth cofx password) + (accounts.login/open-login-callback cofx password {:bioauth-notrequired true}))))) ;; accounts logout module diff --git a/src/status_im/init/core.cljs b/src/status_im/init/core.cljs index 9d381809cd..9a0a4a0fa7 100644 --- a/src/status_im/init/core.cljs +++ b/src/status_im/init/core.cljs @@ -22,6 +22,7 @@ [status-im.utils.fx :as fx] [status-im.utils.keychain.core :as keychain] [status-im.utils.platform :as platform] + [status-im.biometric-auth.core :as biometric-auth] [taoensso.timbre :as log])) (defn init-store! @@ -66,6 +67,7 @@ (fx/defn start-app [cofx] (fx/merge cofx {:init/get-device-UUID nil + :init/get-supported-biometric-auth nil :init/restore-native-settings nil :ui/listen-to-window-dimensions-change nil :notifications/init nil @@ -79,7 +81,7 @@ [{{:keys [view-id hardwallet initial-props desktop/desktop network-status network peers-count peers-summary device-UUID - push-notifications/stored network/type] + supported-biometric-auth push-notifications/stored network/type] :node/keys [status] :or {network (get app-db :network)}} :db}] {:db (assoc app-db @@ -94,6 +96,7 @@ :network/type type :hardwallet hardwallet :device-UUID device-UUID + :supported-biometric-auth supported-biometric-auth :view-id view-id :push-notifications/stored stored)}) @@ -109,6 +112,11 @@ [{:keys [db]} device-uuid] {:db (assoc db :device-UUID device-uuid)}) +(fx/defn set-supported-biometric-auth + {:events [:init.callback/get-supported-biometric-auth-success]} + [{:keys [db]} supported-biometric-auth] + {:db (assoc db :supported-biometric-auth supported-biometric-auth)}) + (fx/defn handle-init-store-error [encryption-key cofx] {:ui/show-confirmation @@ -159,7 +167,7 @@ :keys [accounts/accounts accounts/create networks/networks network network-status peers-count peers-summary view-id navigation-stack mailserver/mailservers - desktop/desktop hardwallet custom-fleets + desktop/desktop hardwallet custom-fleets supported-biometric-auth device-UUID semaphores accounts/login] :node/keys [status on-ready] :or {network (get app-db :network)}} db @@ -187,6 +195,7 @@ :peers-summary peers-summary :peers-count peers-count :device-UUID device-UUID + :supported-biometric-auth supported-biometric-auth :semaphores semaphores :hardwallet hardwallet :web3 web3) @@ -254,6 +263,11 @@ (fn [] (status/get-device-UUID #(re-frame/dispatch [:init.callback/get-device-UUID-success %])))) +(re-frame/reg-fx + :init/get-supported-biometric-auth + (fn [] + (biometric-auth/get-supported #(re-frame/dispatch [:init.callback/get-supported-biometric-auth-success %])))) + (re-frame/reg-fx :init/reset-data reset-data!) diff --git a/src/status_im/native_module/core.cljs b/src/status_im/native_module/core.cljs index 81e76c3aad..0f09fa09f1 100644 --- a/src/status_im/native_module/core.cljs +++ b/src/status_im/native_module/core.cljs @@ -94,6 +94,9 @@ (defn is24Hour [] (native-module/is24Hour)) +(defn get-device-model-info [] + (native-module/get-device-model-info)) + (def extract-group-membership-signatures native-module/extract-group-membership-signatures) (def sign-group-membership native-module/sign-group-membership) diff --git a/src/status_im/native_module/impl/module.cljs b/src/status_im/native_module/impl/module.cljs index cf73522fc4..5d505949d1 100644 --- a/src/status_im/native_module/impl/module.cljs +++ b/src/status_im/native_module/impl/module.cljs @@ -156,6 +156,13 @@ (when (status) (.-is24Hour (status)))) +(defn get-device-model-info [] + (when status + {:model (.-model status) + :brand (.-brand status) + :build-id (.-buildId status) + :device-id (.-deviceId status)})) + (defn update-mailservers [enodes on-result] (when (status) (.updateMailservers (status) enodes on-result))) diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 63846087ba..14b0993c82 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -89,6 +89,7 @@ (reg-root-key-sub :get-pairing-installations :pairing/installations) (reg-root-key-sub :network/type :network/type) (reg-root-key-sub :tooltips :tooltips) +(reg-root-key-sub :supported-biometric-auth :supported-biometric-auth) ;;profile (reg-root-key-sub :my-profile/seed :my-profile/seed) diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index c2c0041d5b..be1d02cd5c 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -93,6 +93,7 @@ (spec/def ::network-status (spec/nilable keyword?)) (spec/def ::app-state string?) +(spec/def ::app-in-background-since (spec/nilable number?)) ;;;;NODE @@ -176,6 +177,10 @@ (spec/def ::device-UUID (spec/nilable string?)) +;;;; Supported Biometric authentication types + +(spec/def ::supported-biometric-auth (spec/nilable #{:FaceID :TouchID :fingerprint})) + ;;;;UNIVERSAL LINKS (spec/def :universal-links/url (spec/nilable string?)) @@ -295,6 +300,7 @@ ::network ::chain ::app-state + ::app-in-background-since ::semaphores ::hardwallet :navigation/view-id @@ -332,6 +338,7 @@ :prices/prices-loading? :notifications/notifications ::device-UUID + ::supported-biometric-auth ::collectible ::collectibles ::extensions-store diff --git a/src/status_im/ui/screens/events.cljs b/src/status_im/ui/screens/events.cljs index 4dcc391c3e..4239d3190d 100644 --- a/src/status_im/ui/screens/events.cljs +++ b/src/status_im/ui/screens/events.cljs @@ -32,7 +32,10 @@ [status-im.utils.handlers :as handlers] [status-im.utils.http :as http] [status-im.utils.utils :as utils] - [status-im.wallet.core :as wallet])) + [status-im.wallet.core :as wallet] + [status-im.i18n :as i18n] + [status-im.biometric-auth.core :as biometric-auth] + [status-im.constants :as const])) (defn- http-get [{:keys [url response-validator success-event-creator failure-event-creator timeout-ms]}] (let [on-success #(re-frame/dispatch (success-event-creator %)) @@ -136,18 +139,51 @@ (fn [{:keys [db]} [_ path v]] {:db (assoc-in db path v)})) -(fx/defn on-return-from-background [cofx] +(def authentication-options + {:reason (i18n/label :t/biometric-auth-reason-login)}) + +(defn- on-biometric-auth-result [{:keys [bioauth-success bioauth-code bioauth-message]}] + (when-not bioauth-success + (if (= bioauth-code "USER_FALLBACK") + (re-frame/dispatch [:accounts.logout.ui/logout-confirmed]) + (utils/show-confirmation {:title (i18n/label :t/biometric-auth-confirm-title) + :content (or bioauth-message (i18n/label :t/biometric-auth-confirm-message)) + :confirm-button-text (i18n/label :t/biometric-auth-confirm-try-again) + :cancel-button-text (i18n/label :t/biometric-auth-confirm-logout) + :on-accept #(biometric-auth/authenticate on-biometric-auth-result authentication-options) + :on-cancel #(re-frame/dispatch [:accounts.logout.ui/logout-confirmed])})))) + +(fx/defn on-return-from-background [{:keys [db now] :as cofx}] + (let [app-in-background-since (get db :app-in-background-since) + signed-up? (get-in db [:account/account :signed-up?]) + biometric-auth? (get-in db [:account/account :settings :biometric-auth?]) + requires-bio-auth (and + signed-up? + biometric-auth? + (some? app-in-background-since) + (>= (- now app-in-background-since) + const/ms-in-bg-for-require-bioauth))] + (fx/merge cofx + {:db (assoc db :app-in-background-since nil)} + (mailserver/process-next-messages-request) + (hardwallet/return-back-from-nfc-settings) + #(when requires-bio-auth + (biometric-auth/authenticate-fx % on-biometric-auth-result authentication-options))))) + +(fx/defn on-going-in-background [{:keys [db now] :as cofx}] (fx/merge cofx - (mailserver/process-next-messages-request) - (hardwallet/return-back-from-nfc-settings))) + {:db (assoc db :app-in-background-since now)})) (defn app-state-change [state {:keys [db] :as cofx}] - (let [app-coming-from-background? (= state "active")] + (let [app-coming-from-background? (= state "active") + app-going-in-background? (= state "background")] (fx/merge cofx {::app-state-change-fx state :db (assoc db :app-state state)} #(when app-coming-from-background? - (on-return-from-background %))))) + (on-return-from-background %)) + #(when app-going-in-background? + (on-going-in-background %))))) (handlers/register-handler-fx :app-state-change diff --git a/src/status_im/ui/screens/profile/user/views.cljs b/src/status_im/ui/screens/profile/user/views.cljs index 4b9c07d132..d020aff964 100644 --- a/src/status_im/ui/screens/profile/user/views.cljs +++ b/src/status_im/ui/screens/profile/user/views.cljs @@ -23,6 +23,7 @@ [status-im.utils.identicon :as identicon] [status-im.utils.platform :as platform] [status-im.utils.universal-links.core :as universal-links] + [status-im.biometric-auth.core :as biometric-auth] [status-im.utils.utils :as utils]) (:require-macros [status-im.utils.views :refer [defview letsubs]])) @@ -180,7 +181,7 @@ :active? logged-in? :action-fn #(re-frame/dispatch [:accounts.logout.ui/logout-pressed])}]]]])) -(defview advanced-settings [{:keys [network networks dev-mode? settings]} on-show] +(defview advanced-settings [{:keys [network networks dev-mode? settings]} on-show supported-biometric-auth] {:component-did-mount on-show} [react/view (when (and config/extensions-enabled? dev-mode?) @@ -239,10 +240,18 @@ [profile.components/settings-switch-item {:label-kw :t/chaos-mode :value (:chaos-mode? settings) - :action-fn #(re-frame/dispatch [:accounts.ui/chaos-mode-switched %])}]]) + :action-fn #(re-frame/dispatch [:accounts.ui/chaos-mode-switched %])}] + (when dev-mode? + [profile.components/settings-item-separator] + [profile.components/settings-switch-item + {:label-kw :t/biometric-auth-setting-label + :value (:biometric-auth? settings) + :active? (some? supported-biometric-auth) + :action-fn #(re-frame/dispatch [:accounts.ui/biometric-auth-switched %])}])]) (defview advanced [params on-show] - (letsubs [advanced? [:my-profile/advanced?]] + (letsubs [advanced? [:my-profile/advanced?] + supported-biometric-auth [:supported-biometric-auth]] {:component-will-unmount #(re-frame/dispatch [:set :my-profile/advanced? false])} [react/view {:padding-bottom 16} [react/touchable-highlight {:on-press #(re-frame/dispatch [:set :my-profile/advanced? (not advanced?)]) @@ -254,7 +263,7 @@ (i18n/label :t/wallet-advanced)] [icons/icon (if advanced? :main-icons/dropdown-up :main-icons/dropdown) {:color colors/blue}]]]]] (when advanced? - [advanced-settings params on-show])])) + [advanced-settings params on-show supported-biometric-auth])])) (defn share-profile-item [{:keys [public-key photo-path] :as current-account}] diff --git a/src/status_im/utils/keychain/core.cljs b/src/status_im/utils/keychain/core.cljs index 693ea853e7..3607f9c233 100644 --- a/src/status_im/utils/keychain/core.cljs +++ b/src/status_im/utils/keychain/core.cljs @@ -4,6 +4,7 @@ [status-im.react-native.js-dependencies :as rn] [status-im.utils.platform :as platform] [status-im.utils.security :as security] + [status-im.biometric-auth.core :as biometric-auth] [status-im.native-module.core :as status])) (def key-bytes 64) @@ -84,6 +85,10 @@ (enum-val "ACCESS_CONTROL" "BIOMETRY_ANY_OR_DEVICE_PASSCODE")})) (.then callback))) +;; Android and iOS +(defn- biometric-auth-available? [callback] + (biometric-auth/get-supported #(callback (some? %)))) + ;; Stores the password for the address to the Keychain (defn save-user-password [address password callback] (-> (.setInternetCredentials (rn/keychain) address address password keychain-secure-hardware (clj->js keychain-restricted-availability)) @@ -112,11 +117,14 @@ ;; Resolves to `false` if the device doesn't have neither a passcode nor a biometry auth. (defn can-save-user-password? [callback] (cond - platform/ios? (device-encrypted? callback) + platform/ios? (check-conditions callback + device-encrypted? + biometric-auth-available?) platform/android? (check-conditions callback secure-hardware-available? + biometric-auth-available? device-not-rooted?) :else (callback false))) diff --git a/test/cljs/status_im/react_native/js_dependencies.cljs b/test/cljs/status_im/react_native/js_dependencies.cljs index 3afee1d369..99d5e57eec 100644 --- a/test/cljs/status_im/react_native/js_dependencies.cljs +++ b/test/cljs/status_im/react_native/js_dependencies.cljs @@ -25,6 +25,7 @@ (def vector-icons (fn [] #js {:default #js {}})) (def webview-bridge (fn [] #js {:default #js {}})) (def webview (fn [] #js {:WebView #js {}})) +(def touchid (fn [] #js {})) (def svg (fn [] #js {:default #js {}})) (def status-keycard (fn [] #js {:default #js {}})) diff --git a/translations/en.json b/translations/en.json index 00f136ff70..2ffed80191 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1029,6 +1029,19 @@ "load-messages-before": "before {{date}}", "open-dapp-store": "Discover ÐApps", "browsed-websites": "Browsed websites will appear here.", + "biometric-auth-error": "Unable perform biometric authentication ({{code}})", + "biometric-auth-reason-verify": "Verify authentication", + "biometric-auth-reason-login": "Login in Status", + "biometric-auth-confirm-try-again": "Try again", + "biometric-auth-confirm-logout": "Relogin", + "biometric-auth-confirm-message": "Biometric authentication is required to continue, if not possible please relogin using account password", + "biometric-auth-confirm-title": "You must authenticate!", + "biometric-auth-login-error-title": "Biometric authentication error", + "biometric-auth-login-ios-fallback-label": "Enter Password", + "biometric-auth-setting-label": "Use biometric authentication", + "biometric-auth-android-title": "Authentication Required", + "biometric-auth-android-sensor-desc": "Touch sensor", + "biometric-auth-android-sensor-error-desc": "Failed", "dapps-can-access" : "ÐApps can access my wallet and contact code", "require-my-permission" : "Require my permission", "might-break" : "Might break some ÐApps",