Compare commits

...
20 changed files with 615 additions and 244 deletions
+2 -3
View File
@@ -42,7 +42,6 @@
[react-native.core :as rn]
[react-native.permissions :as permissions]
[react-native.platform :as platform]
[status-im.common.biometric.events :as biometric]
status-im.common.serialization
status-im.common.standard-authentication.events
[status-im.common.theme.core :as theme]
@@ -125,7 +124,7 @@
:content (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/authenticate nil {:on-fail on-biometric-auth-fail})
:on-accept #(rf/dispatch [:biometric/authenticate {:on-fail on-biometric-auth-fail}])
:on-cancel #(re-frame/dispatch [:multiaccounts.logout.ui/logout-confirmed])})))
(rf/defn on-return-from-background
@@ -148,7 +147,7 @@
#(when-let [chat-id (:current-chat-id db)]
{:dispatch [:chat/mark-all-as-read chat-id]})
#(when requires-bio-auth
(biometric/authenticate % {:on-fail on-biometric-auth-fail})))))
{:dispatch [:biometric/authenticate {:on-fail on-biometric-auth-fail}]}))))
(rf/defn on-going-in-background
[{:keys [db now]}]
+6 -1
View File
@@ -28,10 +28,15 @@
[:on-success [:or fn? [:cat keyword? [:* :any]]]]
[:on-error [:or fn? [:cat keyword? [:* :any]]]]]])
(def ^:private ?error
[:fn {:error/message "schema.common/error should be of type js/Error"}
(fn [v] (instance? js/Error v))])
(defn register-schemas
[]
(registry/register ::theme ?theme)
(registry/register ::customization-color ?customization-color)
(registry/register ::public-key ?public-key)
(registry/register ::image-source ?image-source)
(registry/register ::rpc-call ?rpc-call))
(registry/register ::rpc-call ?rpc-call)
(registry/register ::error ?error))
+10
View File
@@ -5,6 +5,16 @@
(def ^:private ?cofx
[:map])
(def ^:private ?event-fx
[:maybe
[:map {:closed true}
[:db {:optional true} [:maybe map?]]
[:fx {:optional true}
[:maybe
[:vector {:optional true}
[:vector :any]]]]]])
(defn register-schemas
[]
(registry/register ::event-fx ?event-fx)
(registry/register ::cofx ?cofx))
+87 -83
View File
@@ -1,120 +1,124 @@
(ns status-im.common.biometric.events
(:require
[native-module.core :as native-module]
[re-frame.core :as re-frame]
[react-native.biometrics :as biometrics]
[react-native.platform :as platform]
[schema.core :as schema]
[status-im.common.biometric.events-schema :as events-schema]
[status-im.common.biometric.utils :as utils]
[status-im.common.keychain.events :as keychain]
[status-im.constants :as constants]
[taoensso.timbre :as log]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(def android-device-blacklisted?
(and platform/android? (= (:brand (native-module/get-device-model-info)) "bannedbrand")))
(defn get-label-by-type
[biometric-type]
(condp = biometric-type
constants/biometrics-type-android (i18n/label :t/biometric-fingerprint)
constants/biometrics-type-face-id (i18n/label :t/biometric-faceid)
(i18n/label :t/biometric-touchid)))
(defn get-icon-by-type
[biometric-type]
(condp = biometric-type
constants/biometrics-type-face-id :i/face-id
:i/touch-id))
(re-frame/reg-fx
:biometric/get-supported-biometric-type
(rf/reg-fx
:biometric/get-supported-type
(fn []
;;NOTE: if we can't save user password, we can't use biometric
(keychain/can-save-user-password?
(fn [can-save?]
(when (and can-save? (not android-device-blacklisted?))
(when (and can-save? (not utils/android-device-blacklisted?))
(-> (biometrics/get-supported-type)
(.then (fn [type]
(rf/dispatch [:biometric/get-supported-biometric-type-success type])))))))))
(rf/dispatch [:biometric/set-supported-type type])))))))))
(rf/defn get-supported-biometric-auth-success
{:events [:biometric/get-supported-biometric-type-success]}
[{:keys [db]} supported-type]
(defn set-supported-type
[{:keys [db]} [supported-type]]
{:db (assoc db :biometric/supported-type supported-type)})
(rf/defn show-message
{:events [:biometric/show-message]}
[_ error]
(let [code (ex-cause error)
content (if (#{::biometrics/not-enrolled
(schema/=> set-supported-type events-schema/?set-supported-type)
(rf/reg-event-fx :biometric/set-supported-type set-supported-type)
(defn show-message
[_ [code]]
(let [content (if (#{::biometrics/not-enrolled
::biometrics/not-available}
code)
(i18n/label :t/grant-face-id-permissions)
(i18n/label :t/biometric-auth-error {:code code}))]
{:effects.utils/show-popup
{:title (i18n/label :t/biometric-auth-login-error-title)
:content content}}))
{:fx [[:effects.utils/show-popup
{:title (i18n/label :t/biometric-auth-login-error-title)
:content content}]]}))
(schema/=> show-message events-schema/?show-message)
(rf/reg-event-fx :biometric/show-message show-message)
(re-frame/reg-fx
(rf/reg-fx
:biometric/authenticate
(fn [{:keys [on-success on-fail prompt-message]}]
(fn [{:keys [on-success on-fail on-cancel prompt-message on-done]}]
(-> (biometrics/authenticate
{:prompt-message (or prompt-message (i18n/label :t/biometric-auth-reason-login))
:fallback-prompt-message (i18n/label
:t/biometric-auth-login-ios-fallback-label)
:cancel-button-text (i18n/label :t/cancel)})
(.then (fn [not-canceled?]
(when (and on-success not-canceled?)
(on-success))))
(utils/handle-cb on-done)
(if not-canceled?
(utils/handle-cb on-success)
(utils/handle-cb on-cancel))))
(.catch (fn [err]
(when on-fail
(on-fail err)))))))
(utils/handle-cb on-done)
(utils/handle-cb on-fail err))))))
(rf/defn authenticate
{:events [:biometric/authenticate]}
[_ opts]
{:biometric/authenticate opts})
(defn authenticate
[{:keys [db]} [opts]]
(let [pending? (get db :biometric/auth-pending?)]
;;NOTE: prompting biometric check while another one is pending triggers error
(when-not pending?
{:db (assoc db :biometric/auth-pending? true)
:fx [[:biometric/authenticate
(assoc opts :on-done [:set :biometric/auth-pending? false])]]})))
(rf/reg-event-fx
:biometric/on-enable-success
(fn [{:keys [db]} [password]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:db (assoc db :auth-method constants/auth-method-biometric)
:dispatch [:keychain/save-password-and-auth-method
{:key-uid key-uid
:masked-password password}]})))
(schema/=> authenticate events-schema/?authenticate)
(rf/reg-event-fx :biometric/authenticate authenticate)
(rf/reg-event-fx
:biometric/enable
(fn [_ [password]]
{:dispatch [:biometric/authenticate
{:on-success #(rf/dispatch [:biometric/on-enable-success password])
:on-fail #(rf/dispatch [:biometric/show-message %])}]}))
(defn enable-biometrics
[{:keys [db]} [password]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:db (assoc db :auth-method constants/auth-method-biometric)
:fx [[:dispatch
[:keychain/save-password-and-auth-method
{:key-uid key-uid
:masked-password password}]]]}))
(rf/reg-event-fx
:biometric/disable
(fn [{:keys [db]}]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:db (assoc db :auth-method constants/auth-method-none)
:keychain/clear-user-password key-uid})))
(schema/=> enable-biometrics events-schema/?enable-biometrics)
(rf/reg-event-fx :biometric/enable enable-biometrics)
(rf/reg-fx
:biometric/check-if-available
(fn [[key-uid callback]]
(keychain/can-save-user-password?
(fn [can-save?]
(when can-save?
(-> (biometrics/get-available)
(.then (fn [available?]
(when-not available?
(throw (js/Error. "biometric-not-available")))))
(.then #(keychain/get-auth-method! key-uid))
(.then (fn [auth-method]
(when auth-method (callback auth-method))))
(.catch (fn [err]
(when-not (= (.-message err) "biometric-not-available")
(log/error "Failed to check if biometrics is available"
{:error err
:key-uid key-uid
:event :profile.login/check-biometric}))))))))))
(defn disable-biometrics
[{:keys [db]}]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:db (assoc db :auth-method constants/auth-method-none)
:fx [[:keychain/clear-user-password key-uid]]}))
(schema/=> disable-biometrics events-schema/?disable-biometrics)
(rf/reg-event-fx :biometric/disable disable-biometrics)
(defn check-if-biometrics-available
[{:keys [key-uid on-success on-fail]}]
(keychain/can-save-user-password?
(fn [can-save?]
(if-not can-save?
(utils/handle-cb on-fail
(ex-info "cannot-save-user-password"
{:fx :biometric/check-if-available}))
(-> (biometrics/get-available)
(.then (fn [available?]
(when-not available?
(throw (js/Error. "biometric-not-available")))))
(.then #(keychain/get-auth-method! key-uid))
(.then (fn [auth-method]
(when auth-method
(utils/handle-cb on-success auth-method))))
(.catch (fn [err]
(let [message (.-message err)]
(utils/handle-cb on-fail
(ex-info message
{:err err
:fx :biometric/check-if-available}))
(when-not (= message "biometric-not-available")
(log/error "Failed to check if biometrics is available"
{:error err
:key-uid key-uid
:event :profile.login/check-biometric}))))))))))
(schema/=> check-if-biometrics-available events-schema/?check-if-biometrics-available)
(rf/reg-fx :biometric/check-if-available check-if-biometrics-available)
@@ -0,0 +1,62 @@
(ns status-im.common.biometric.events-schema
(:require
[status-im.constants :as constants]
[utils.security.core :as security]))
(def ?set-supported-type
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple
[:enum
constants/biometrics-type-android
constants/biometrics-type-face-id
constants/biometrics-type-touch-id]]]]
:schema.re-frame/event-fx])
(def ?show-message
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple keyword?]]]
:schema.re-frame/event-fx])
(def ?authenticate
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple
[:map {:closed true}
[:on-success {:optional true} fn?]
[:on-fail {:optional true} fn?]
[:on-cancel {:optional true} fn?]
[:prompt-message {:optional true} :string]]]]]
:schema.re-frame/event-fx])
(def ?enable-biometrics
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple
[:fn {:error/message "Should be an instance of security/MaskedData"}
(fn [pw] (instance? security/MaskedData pw))]]]]
:schema.re-frame/event-fx])
(def ?disable-biometrics
[:=>
[:catn
[:cofx :schema.re-frame/cofx]]
:schema.re-frame/event-fx])
(def ?check-if-biometrics-available
[:=>
[:cat
[:map {:closed true}
[:key-uid string?]
[:on-success {:optional true} [:maybe fn?]]
[:on-fail {:optional true} [:maybe fn?]]]]
:any])
@@ -0,0 +1,80 @@
(ns status-im.common.biometric.events-test
(:require [cljs.test :refer [deftest testing is]]
matcher-combinators.test
[react-native.biometrics :as biometrics]
[status-im.common.biometric.events :as sut]
[status-im.constants :as constants]
[utils.i18n :as i18n]
[utils.security.core :as security]))
(deftest set-supported-biometrics-type-test
(testing "successfully setting supported biometrics type"
(let [cofx {:db {}}
supported-type constants/biometrics-type-face-id
expected {:db (assoc (:db cofx) :biometric/supported-type supported-type)}]
(is (match? expected (sut/set-supported-type cofx [supported-type])))))
(testing "throws error when setting unsupported biometrics type"
(let [cofx {:db {}}
supported-type :unsupported-type]
(is (thrown? js/Error (sut/set-supported-type cofx [supported-type]))))))
(deftest show-message-test
(testing "informs the user to enable biometrics from settings"
(let [cofx {:db {}}
expected {:fx [[:effects.utils/show-popup
{:title (i18n/label :t/biometric-auth-login-error-title)
:content (i18n/label :t/grant-face-id-permissions)}]]}]
(is (match? expected
(sut/show-message cofx
[::biometrics/not-available])))))
(testing "shows a generic error message"
(let [cofx {:db {}}
error-cause :test-error
expected {:fx [[:effects.utils/show-popup
{:title (i18n/label :t/biometric-auth-login-error-title)
:content (i18n/label :t/biometric-auth-error {:code error-cause})}]]}]
(is (match? expected
(sut/show-message cofx
[error-cause]))))))
(deftest authenticate-biometrics-test
(testing "passing the right args to authenticate"
(let [cofx {:db {}}
args {:on-success identity
:on-fail identity
:prompt-message "test"}
result (sut/authenticate cofx [args])]
(is (= (:prompt-message args) (get-in result [:fx 0 1 :prompt-message])))
(is (not (nil? (get-in result [:fx 0 1 :on-success]))))
(is (not (nil? (get-in result [:fx 0 1 :on-fail]))))))
(testing "skips biometric check if another one pending"
(let [cofx {:db {:biometric/auth-pending? true}}
result (sut/authenticate cofx [{}])]
(is (nil? result)))))
(deftest enable-biometrics-test
(testing "successfully enabling biometrics"
(let [key-uid "test-uid"
password (security/mask-data "test-pw")
cofx {:db {:profile/profile {:key-uid key-uid}}}
expected-db (assoc (:db cofx) :auth-method constants/auth-method-biometric)
result (sut/enable-biometrics cofx [password])]
(is (match? expected-db (:db result)))
(is (= password (get-in result [:fx 0 1 1 :masked-password])))))
(testing "throws error if raw password is passed"
(let [key-uid "test-uid"
password "test-password"
cofx {:db {:profile/profile {:key-uid key-uid}}}]
(is (thrown? js/Error (sut/enable-biometrics cofx [password]))))))
(deftest disable-biometrics-test
(testing "successfully disabling biometrics"
(let [key-uid "test-uid"
cofx {:db {:profile/profile {:key-uid key-uid}}}
expected {:db (assoc (:db cofx) :auth-method constants/auth-method-none)
:fx [[:keychain/clear-user-password key-uid]]}]
(is (match? expected (sut/disable-biometrics cofx))))))
+36
View File
@@ -0,0 +1,36 @@
(ns status-im.common.biometric.utils
(:require
[native-module.core :as native-module]
[react-native.platform :as platform]
[status-im.constants :as constants]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(def android-device-blacklisted?
(and platform/android? (= (:brand (native-module/get-device-model-info)) "bannedbrand")))
(defn get-label-by-type
[biometric-type]
(condp = biometric-type
constants/biometrics-type-android (i18n/label :t/biometric-fingerprint)
constants/biometrics-type-face-id (i18n/label :t/biometric-faceid)
(i18n/label :t/biometric-touchid)))
(defn get-icon-by-type
[biometric-type]
(condp = biometric-type
constants/biometrics-type-face-id :i/face-id
:i/touch-id))
;; NOTE: move to utils.re-frame?
(defn handle-cb
"Handles re-frame callbacks that are passed as anonymous fn or as dispatch vectors to effects.
e.g.:\n
* with dispatch vector - `:on-success [:my-ns/my-event]`\n
* with fn - `:on-success #(rf/dispatch [:my-ns/my-event])`"
[cb & args]
(when cb
(cond
(fn? cb) (apply cb args)
(vector? cb) (rf/dispatch (into cb args)))))
@@ -6,47 +6,59 @@
[status-im.common.standard-authentication.password-input.view :as password-input]
[status-im.contexts.profile.utils :as profile.utils]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
[utils.re-frame :as rf]
[reagent.core :as reagent]
[utils.security.core :as security]))
(defn view
[{:keys [on-enter-password on-press-biometrics button-label button-icon-left]}]
(let [{:keys [key-uid customization-color] :as profile} (rf/sub [:profile/profile-with-image])
{:keys [error processing password]} (rf/sub [:profile/login])
sign-in-enabled? (rf/sub [:sign-in-enabled?])]
[:<>
[rn/view {:style style/enter-password-container}
[rn/view
[quo/text
{:accessibility-label :sync-code-generated
:weight :bold
:size :heading-1
:style {:margin-bottom 4}}
(i18n/label :t/enter-password)]
[rn/view
{:style style/context-tag}
[quo/context-tag
{:type :default
:blur? true
:profile-picture (profile.utils/photo profile)
:full-name (profile.utils/displayed-name profile)
:customization-color customization-color
:size 24}]]
[password-input/view
{:on-press-biometrics on-press-biometrics
:processing processing
:error error
:default-password password
:sign-in-enabled? sign-in-enabled?}]
[quo/button
{:size 40
:container-style style/enter-password-button
:type :primary
:customization-color (or customization-color :primary)
:accessibility-label :login-button
:icon-left button-icon-left
:disabled? (or (not sign-in-enabled?) processing)
:on-press (fn []
(rf/dispatch [:set-in [:profile/login :key-uid] key-uid])
(rf/dispatch [:profile.login/verify-database-password password
#(on-enter-password password)]))}
button-label]]]]))
[]
(let [password-value (reagent/atom nil)]
(fn [{:keys [on-enter-password on-press-biometrics button-label button-icon-left]}]
(let [{:keys [key-uid customization-color]
:as profile} (rf/sub [:profile/profile-with-image])
{:keys [error processing password]} (rf/sub [:profile/login])
sign-in-enabled? (rf/sub [:sign-in-enabled?])
on-change-password (fn [value]
(reset! password-value value)
(when (not= value (security/safe-unmask-data password))
(rf/dispatch [:set-in [:profile/login :password]
(security/mask-data value)])
(rf/dispatch [:set-in [:profile/login :error] ""])))]
[:<>
[rn/view {:style style/enter-password-container}
[rn/view
[quo/text
{:accessibility-label :sync-code-generated
:weight :bold
:size :heading-1
:style {:margin-bottom 4}}
(i18n/label :t/enter-password)]
[rn/view
{:style style/context-tag}
[quo/context-tag
{:type :default
:blur? true
:profile-picture (profile.utils/photo profile)
:full-name (profile.utils/displayed-name profile)
:customization-color customization-color
:size 24}]]
[password-input/view
{:on-press-biometrics on-press-biometrics
:processing processing
:error error
:password @password-value
:on-change-password on-change-password
:sign-in-enabled? sign-in-enabled?}]
[quo/button
{:size 40
:container-style style/enter-password-button
:type :primary
:customization-color (or customization-color :primary)
:accessibility-label :login-button
:icon-left button-icon-left
:disabled? (or (not sign-in-enabled?) processing)
:on-press (fn []
(rf/dispatch [:set-in [:profile/login :key-uid] key-uid])
(rf/dispatch [:profile.login/verify-database-password password
#(on-enter-password password)]))}
button-label]]]]))))
@@ -1,11 +1,86 @@
(ns status-im.common.standard-authentication.events
(:require
[utils.re-frame :as rf]))
[schema.core :as schema]
[status-im.common.standard-authentication.enter-password.view :as enter-password]
[status-im.common.standard-authentication.events-schema :as events-schema]
[taoensso.timbre :as log]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[utils.security.core :as security]))
(rf/reg-event-fx :standard-auth/on-biometric-success
(fn [{:keys [db]} [callback]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:fx [[:keychain/get-user-password [key-uid callback]]]})))
(defn authorize
[{:keys [db]} [args]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:fx [[:biometric/check-if-available
{:key-uid key-uid
:on-success [:standard-auth/authorize-with-biometric args]
:on-fail [:standard-auth/authorize-with-password args]}]]}))
(schema/=> authorize events-schema/?authorize)
(rf/reg-event-fx :standard-auth/authorize authorize)
(defn authorize-with-biometric
[_ [{:keys [on-auth-success on-auth-fail] :as args}]]
(let [args-with-biometric-btn
(assoc args
:on-press-biometric
#(rf/dispatch [:standard-auth/authorize-with-biometric args]))]
{:fx [[:dispatch [:dismiss-keyboard]]
[:dispatch
[:biometric/authenticate
{:prompt-message (i18n/label :t/biometric-auth-confirm-message)
:on-cancel [:standard-auth/authorize-with-password args-with-biometric-btn]
:on-success [:standard-auth/on-biometric-success on-auth-success]
:on-fail [:standard-auth/on-biometric-fail on-auth-fail]}]]]}))
(schema/=> authorize-with-biometric events-schema/?authorize-with-biometric)
(rf/reg-event-fx :standard-auth/authorize-with-biometric authorize-with-biometric)
(defn on-biometric-success
[{:keys [db]} [on-auth-success]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:fx [[:keychain/get-user-password [key-uid on-auth-success]]]}))
(schema/=> on-biometric-success events-schema/?on-biometric-success)
(rf/reg-event-fx :standard-auth/on-biometric-success on-biometric-success)
(defn on-biometric-fail
[_ [on-auth-fail error]]
(when on-auth-fail
(on-auth-fail error))
(log/error (ex-message error)
(-> error
ex-data
(assoc :code (ex-cause error)
:event :standard-auth/on-biometric-fail)))
{:fx [[:dispatch [:biometric/show-message (ex-cause error)]]]})
(schema/=> on-biometric-fail events-schema/?on-biometrics-fail)
(rf/reg-event-fx :standard-auth/on-biometric-fail on-biometric-fail)
(defn- bottom-sheet-password-view
[{:keys [on-press-biometric on-auth-success auth-button-icon-left auth-button-label]}]
(fn []
(let [handle-password-success (fn [password]
(-> password security/hash-masked-password on-auth-success))]
[enter-password/view
{:on-enter-password handle-password-success
:on-press-biometrics on-press-biometric
:button-icon-left auth-button-icon-left
:button-label auth-button-label}])))
(defn authorize-with-password
[_ [{:keys [on-close theme blur?] :as args}]]
{:fx [[:dispatch [:standard-auth/reset-login-password]]
[:dispatch
[:show-bottom-sheet
{:on-close on-close
:theme theme
:shell? blur?
:content #(bottom-sheet-password-view args)}]]]})
(schema/=> authorize-with-password events-schema/?authorize-with-password)
(rf/reg-event-fx :standard-auth/authorize-with-password authorize-with-password)
(rf/reg-event-fx
:standard-auth/reset-login-password
@@ -0,0 +1,53 @@
(ns status-im.common.standard-authentication.events-schema)
(def ^:private ?authorize-map
[:map {:closed true}
[:on-auth-success fn?]
[:on-auth-fail {:optional true} [:maybe fn?]]
[:on-close {:optional true} [:maybe fn?]]
[:auth-button-label {:optional true} [:maybe string?]]
[:auth-button-icon-left {:optional true} [:maybe keyword?]]
[:blur? {:optional true} [:maybe boolean?]]
[:theme {:optional true} [:maybe :schema.common/theme]]])
(def ?authorize
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple ?authorize-map]]]
:schema.re-frame/event-fx])
(def ?authorize-with-biometric
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple ?authorize-map]]]
:schema.re-frame/event-fx])
(def ?on-biometric-success
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple fn?]]]
:schema.re-frame/event-fx])
(def ?on-biometrics-fail
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple
[:maybe fn?]
[:maybe :schema.common/error]]]]
:schema.re-frame/event-fx])
(def ?authorize-with-password
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple ?authorize-map]]]
:schema.re-frame/event-fx])
@@ -9,7 +9,8 @@
[status-im.common.standard-authentication.password-input.style :as style]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[utils.security.core :as security]))
[utils.security.core :as security]
[reagent.core :as reagent]))
(defn get-error-message
[error]
@@ -20,14 +21,8 @@
(i18n/label :t/oops-wrong-password)
error))
(defn- on-change-password
[entered-password]
(rf/dispatch [:set-in [:profile/login :password]
(security/mask-data entered-password)])
(rf/dispatch [:set-in [:profile/login :error] ""]))
(defn- view-internal
[{:keys [default-password theme shell? on-press-biometrics blur?]}]
[{:keys [password on-change-password theme shell? on-press-biometrics blur?]}]
(let [{:keys [error processing]} (rf/sub [:profile/login])
error-message (get-error-message error)
error? (boolean (seq error-message))]
@@ -43,7 +38,7 @@
:error? error?
:label (i18n/label :t/profile-password)
:on-change-text on-change-password
:default-value (security/safe-unmask-data default-password)}]
:value password}]
(when on-press-biometrics
[quo/button
{:container-style style/auth-button
@@ -1,67 +0,0 @@
(ns status-im.common.standard-authentication.standard-auth.authorize
(:require
[react-native.biometrics :as biometrics]
[status-im.common.standard-authentication.enter-password.view :as enter-password]
[taoensso.timbre :as log]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[utils.security.core :as security]))
(defn reset-password
[]
(rf/dispatch [:set-in [:profile/login :password] nil])
(rf/dispatch [:set-in [:profile/login :error] ""]))
(defn authorize
[{:keys [biometric-auth? on-auth-success on-auth-fail on-close
auth-button-label theme blur? auth-button-icon-left]}]
(let [handle-auth-success (fn [biometric?]
(fn [entered-password]
(let [sha3-masked-password (if biometric?
entered-password
(security/hash-masked-password
entered-password))]
(on-auth-success sha3-masked-password))))
password-login (fn [{:keys [on-press-biometrics]}]
(rf/dispatch [:show-bottom-sheet
{:on-close on-close
:theme theme
:shell? blur?
:content (fn []
[enter-password/view
{:on-enter-password (handle-auth-success
false)
:on-press-biometrics on-press-biometrics
:button-icon-left auth-button-icon-left
:button-label auth-button-label}])}]))
; biometrics-login recursively passes itself as a parameter because if the user
; fails biometric auth they will be shown the password bottom sheet with an option
; to retrigger biometric auth, so they can endlessly repeat this cycle.
biometrics-login (fn [on-press-biometrics]
(rf/dispatch [:dismiss-keyboard])
(rf/dispatch
[:biometric/authenticate
{:prompt-message (i18n/label :t/biometric-auth-confirm-message)
:on-success (fn []
(on-close)
(rf/dispatch [:standard-auth/on-biometric-success
(handle-auth-success true)]))
:on-fail (fn [error]
(on-close)
(log/error
(ex-message error)
(-> error ex-data (assoc :code (ex-cause error))))
(when on-auth-fail (on-auth-fail error))
(password-login {:on-press-biometrics
#(on-press-biometrics
on-press-biometrics)}))}]))]
(if biometric-auth?
(-> (biometrics/get-supported-type)
(.then (fn [biometric-type]
(if biometric-type
(biometrics-login biometrics-login)
(do
(reset-password)
(password-login {})))))
(.catch #(password-login {})))
(password-login {}))))
@@ -4,7 +4,6 @@
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]
[status-im.common.standard-authentication.standard-auth.authorize :as authorize]
[status-im.constants :as constants]
[utils.re-frame :as rf]))
@@ -33,14 +32,15 @@
{:size size
:customization-color customization-color
:on-reset (when @reset-slider? #(reset! reset-slider? false))
:on-complete #(authorize/authorize {:on-close on-close
:auth-button-icon-left auth-button-icon-left
:theme theme
:blur? blur?
:biometric-auth? biometric-auth?
:on-auth-success on-auth-success
:on-auth-fail on-auth-fail
:auth-button-label auth-button-label})
:on-complete #(rf/dispatch [:standard-auth/authorize
{:on-close on-close
:auth-button-icon-left auth-button-icon-left
:theme theme
:blur? blur?
:biometric-auth? biometric-auth?
:on-auth-success on-auth-success
:on-auth-fail on-auth-fail
:auth-button-label auth-button-label}])
:track-icon (if biometric-auth? :i/face-id :password)
:track-text track-text}]])))
@@ -3,7 +3,7 @@
[quo.core :as quo]
[react-native.core :as rn]
[react-native.safe-area :as safe-area]
[status-im.common.biometric.events :as biometric]
[status-im.common.biometric.utils :as biometric]
[status-im.common.parallax.view :as parallax]
[status-im.common.parallax.whitelist :as whitelist]
[status-im.common.resources :as resources]
@@ -2,7 +2,7 @@
(:require
[native-module.core :as native-module]
[re-frame.core :as re-frame]
[status-im.common.biometric.events :as biometric]
status-im.common.biometric.events
[status-im.constants :as constants]
[status-im.contexts.profile.create.events :as profile.create]
[status-im.contexts.profile.recover.events :as profile.recover]
@@ -32,8 +32,10 @@
(rf/defn enable-biometrics
{:events [:onboarding/enable-biometrics]}
[_]
{:biometric/authenticate {:on-success #(rf/dispatch [:onboarding/biometrics-done])
:on-fail #(rf/dispatch [:onboarding/biometrics-fail %])}})
{:fx [[:dispatch
[:biometric/authenticate
{:on-success #(rf/dispatch [:onboarding/biometrics-done])
:on-fail #(rf/dispatch [:onboarding/biometrics-fail %])}]]]})
(rf/defn navigate-to-enable-notifications
{:events [:onboarding/navigate-to-enable-notifications]}
@@ -51,10 +53,10 @@
[:onboarding/finalize-setup]
[:onboarding/create-account-and-login])}))
(rf/defn biometrics-fail
{:events [:onboarding/biometrics-fail]}
[cofx code]
(biometric/show-message cofx code))
(rf/reg-event-fx
:onboarding/biometrics-fail
(fn [_ [error]]
{:dispatch [:biometric/show-message (ex-cause error)]}))
(rf/defn create-account-and-login
{:events [:onboarding/create-account-and-login]}
@@ -177,12 +177,14 @@
(rf/defn login-with-biometric-if-available
{:events [:profile.login/login-with-biometric-if-available]}
[_ key-uid]
{:biometric/check-if-available [key-uid
#(rf/dispatch [:profile.login/check-biometric-success % key-uid])]})
{:biometric/check-if-available {:key-uid key-uid
:on-success (fn [auth-method]
(rf/dispatch [:profile.login/check-biometric-success
key-uid auth-method]))}})
(rf/defn check-biometric-success
{:events [:profile.login/check-biometric-success]}
[{:keys [db]} auth-method key-uid]
[{:keys [db]} key-uid auth-method]
(merge {:db (assoc db :auth-method auth-method)}
(when (= auth-method keychain/auth-method-biometric)
{:keychain/password-hash-migration
@@ -218,7 +220,7 @@
ex-data
(assoc :code (ex-cause error)
:event :profile.login/biometric-auth-fail)))
{:dispatch [:biometric/show-message error]}))
{:dispatch [:biometric/show-message (ex-cause error)]}))
(rf/defn verify-database-password
{:events [:profile.login/verify-database-password]}
@@ -3,9 +3,8 @@
[quo.theme :as quo.theme]
[react-native.core :as rn]
[react-native.safe-area :as safe-area]
[status-im.common.biometric.events :as biometric]
[status-im.common.biometric.utils :as biometric]
[status-im.common.not-implemented :as not-implemented]
[status-im.common.standard-authentication.standard-auth.authorize :as authorize]
[status-im.constants :as constants]
[status-im.contexts.profile.settings.screens.password.style :as style]
[utils.i18n :as i18n]
@@ -14,16 +13,19 @@
(defn- on-press-biometric-enable
[button-label theme]
(fn []
(authorize/authorize
{:biometric-auth? false
:blur? true
:theme theme
:auth-button-label (i18n/label :t/biometric-enable-button {:bio-type-label button-label})
:on-close (fn [] (rf/dispatch [:standard-auth/reset-login-password]))
:on-auth-success (fn [password]
(rf/dispatch [:hide-bottom-sheet])
(rf/dispatch [:standard-auth/reset-login-password])
(rf/dispatch [:biometric/enable password]))})))
(rf/dispatch
[:standard-auth/authorize-with-password
{:blur? true
:theme theme
:auth-button-label (i18n/label :t/biometric-enable-button {:bio-type-label button-label})
:on-close (fn [] (rf/dispatch [:standard-auth/reset-login-password]))
:on-auth-success (fn [password]
(rf/dispatch [:hide-bottom-sheet])
(rf/dispatch [:standard-auth/reset-login-password])
(rf/dispatch
[:biometric/authenticate
{:on-success #(rf/dispatch [:biometric/enable password])
:on-fail #(rf/dispatch [:biometric/show-message (ex-cause %)])}]))}])))
(defn- get-biometric-item
[theme]
+1 -1
View File
@@ -38,7 +38,7 @@
{:db db/app-db
:theme/init-theme nil
:network/listen-to-network-info nil
:biometric/get-supported-biometric-type nil
:biometric/get-supported-type nil
;;app starting flow continues in get-profiles-overview
:profile/get-profiles-overview #(rf/dispatch [:profile/get-profiles-overview-success %])
:effects.font/get-font-file-for-initials-avatar
@@ -0,0 +1,100 @@
(ns status-im.integration-test.standard-auth-test
(:require
[cljs.test :refer [deftest testing is use-fixtures]]
[day8.re-frame.test :as rf-test]
re-frame.core
[test-helpers.integration :as h]
[utils.re-frame :as rf]))
(defn fixture-re-frame
[]
(let [restore-re-frame (atom nil)]
{:before #(reset! restore-re-frame (re-frame.core/make-restore-fn))
:after #(@restore-re-frame)}))
(use-fixtures :each (fixture-re-frame))
(def default-args
{:on-auth-success identity
:on-auth-fail identity
:on-close identity
:auth-button-label "test"
:auth-button-icon-left :test-icon
:blur? false
:theme :light})
(deftest standard-auth-biometric-authorize-success
(testing "calling success callback when completing biometric authentication"
(h/log-headline :standard-auth-authorize-success)
(rf-test/run-test-async
(do
(rf/reg-fx :biometric/check-if-available
(fn [{:keys [on-success]}]
(rf/dispatch on-success)))
(rf/reg-event-fx :biometric/authenticate
(fn [_ [{:keys [on-success]}]]
(rf/dispatch on-success)))
(rf/reg-fx :keychain/get-user-password
(fn [[_ on-success]]
(on-success))))
(let [on-success-called? (atom false)
args (assoc default-args :on-auth-success #(reset! on-success-called? true))]
(rf/dispatch [:standard-auth/authorize args])
(rf-test/wait-for [:standard-auth/on-biometric-success]
(is @on-success-called?))))))
(deftest standard-auth-biometric-authorize-cancel
(testing "falling back to password authorization when biometrics canceled"
(h/log-headline :standard-auth-authorize-cancel)
(rf-test/run-test-async
(do
(rf/reg-fx :biometric/check-if-available
(fn [{:keys [on-success]}]
(rf/dispatch on-success)))
(rf/reg-event-fx :biometric/authenticate
(fn [_ [{:keys [on-cancel]}]]
(rf/dispatch on-cancel)))
(rf/reg-event-fx :show-bottom-sheet
(fn [_])))
(rf/dispatch [:standard-auth/authorize default-args])
(rf-test/wait-for [:show-bottom-sheet]
(is true)))))
(deftest standard-auth-biometric-authorize-fail
(testing "showing biometric error message when authorization failed"
(h/log-headline :standard-auth-authorize-fail)
(rf-test/run-test-async
(let [on-fail-called? (atom false)
expected-error-cause :bad-error
error (atom nil)
args (assoc default-args
:on-auth-fail
(fn [err]
(reset! on-fail-called? true)
(reset! error err)))]
(rf/reg-fx :biometric/check-if-available
(fn [{:keys [on-success]}]
(rf/dispatch on-success)))
(rf/reg-event-fx :biometric/authenticate
(fn [_ [{:keys [on-fail]}]]
(rf/dispatch (conj on-fail (ex-info "error" {} expected-error-cause)))))
(rf/reg-fx :biometric/show-message
(fn [_]))
(rf/dispatch [:standard-auth/authorize args])
(rf-test/wait-for [:biometric/show-message]
(is @on-fail-called?)
(is (= expected-error-cause (ex-cause @error))))))))
(deftest standard-auth-password-authorize-fallback
(testing "falling back to password when biometrics is not available"
(h/log-headline :standard-auth-password-authorize-fallback)
(rf-test/run-test-async
(do
(rf/reg-fx :biometric/check-if-available
(fn [{:keys [on-fail]}]
(rf/dispatch on-fail)))
(rf/reg-event-fx :show-bottom-sheet
(fn [_])))
(rf/dispatch [:standard-auth/authorize default-args])
(rf-test/wait-for [:standard-auth/authorize-with-password]
(is true)))))
+1
View File
@@ -55,6 +55,7 @@
(reg-root-key-sub :get-pairing-installations :pairing/installations)
(reg-root-key-sub :tooltips :tooltips)
(reg-root-key-sub :biometric/supported-type :biometric/supported-type)
(reg-root-key-sub :biometric/auth-pending? :biometric/auth-pending?)
(reg-root-key-sub :app-state :app-state)
(reg-root-key-sub :home-items-show-number :home-items-show-number)
(reg-root-key-sub :waku/v2-peer-stats :peer-stats)