Clean up biometrics and standard-auth and add tests (#18756)

* ref(biometric): events using reg-event-fx & other

* test(biometric-events): show-message

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix(biometrics): removed unnecessary event

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* test(biometrics): tests & schemas for events

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref(standard-auth): refactored authorize

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* test(biometrics): fixed event tests

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref(standard-auth): authorize using events

* feat(standard-auth): schemas and removed old auth

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* test(standard-auth): added integration tests

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* feat(biometric): added missing schema

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref(biometrics): moved schemas to a separate file

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix(biometric): pressing biometric quickly errors

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: removed namespaced keywords for errors

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref: removed the use of vector cb for biometric fx

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: removed :maybe from fx schema

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref: remove event-fx schema

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* feat: added nested biometrics db keys

* fix: using ExceptionInfo for the error schema

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* test: removed clearing fixture and small refactor

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: reset db password inside standard-authentication

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref: use naming convention for effects

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* ref: renamed not-canceled? to success?

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* rev: removed biometrics schemas :(

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: removed set-in & fix on-close bug

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: onboarding biometric not triggered

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: invalid props bug on onboarding welcome

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: moved side effect cb call into effect

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* test: moved integration test to new location

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: password input theme

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

* fix: disabled biometric not triggering fail when checking biometric

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>

---------

Signed-off-by: Cristian Lungu <lungucristian95@gmail.com>
This commit is contained in:
Lungu Cristian 2024-03-12 12:14:56 +02:00 committed by GitHub
parent c57e5cd6db
commit 637efa24cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 519 additions and 232 deletions

View File

@ -37,7 +37,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]
@ -120,7 +119,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
@ -142,7 +141,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]}]

View File

@ -13,15 +13,14 @@
(native-module/logout)))
(rf/defn initialize-app-db
[{{:keys [keycard initials-avatar-font-file]
:biometric/keys [supported-type]
:network/keys [type]}
[{{:keys [keycard initials-avatar-font-file biometrics]
:network/keys [type]}
:db}]
{:db (assoc db/app-db
:network/type type
:initials-avatar-font-file initials-avatar-font-file
:keycard (dissoc keycard :secrets :pin :application-info)
:biometric/supported-type supported-type
:biometrics biometrics
:syncing nil)})
(rf/defn logout-method

View File

@ -39,13 +39,13 @@
[message]
(let [cause (if platform/android?
(condp = message
android-not-enrolled-error-message ::not-enrolled
android-not-available-error-message ::not-available
::unknown)
android-not-enrolled-error-message :biometrics/not-enrolled-error
android-not-available-error-message :biometrics/not-available-error
:biometrics/unknown-error)
(condp #(string/includes? %2 %1) message
ios-not-enrolled-error-message ::not-enrolled
::unknown))]
ios-not-enrolled-error-message :biometrics/not-enrolled-error
:biometrics/unknown-error))]
(ex-info "Failed to authenticate with biometrics"
{:orig-error-message message}
cause)))

View File

@ -29,10 +29,15 @@
[:on-success [:or fn? [:cat keyword? [:* :any]]]]
[:on-error [:or fn? [:cat keyword? [:* :any]]]]]])
(def ^:private ?exception
[:fn {:error/message "schema.common/exception should be of type ExceptionInfo"}
(fn [v] (instance? ExceptionInfo 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 ::exception ?exception))

View File

@ -0,0 +1,72 @@
(ns status-im.common.biometric.effects
(:require
[react-native.biometrics :as biometrics]
[status-im.common.biometric.utils :as utils]
[status-im.common.keychain.events :as keychain]
[status-im.constants :as constant]
[taoensso.timbre :as log]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(rf/reg-fx
:effects.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 utils/android-device-blacklisted?))
(-> (biometrics/get-supported-type)
(.then (fn [type]
(rf/dispatch [:biometric/set-supported-type type])))))))))
(rf/reg-fx
:effects.biometric/authenticate
(fn [{:keys [prompt-message on-success on-fail on-cancel on-done]
:or {on-done identity
on-success identity
on-cancel identity
on-fail identity}}]
(-> (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 [success?]
(on-done)
(if success?
(on-success)
(on-cancel))))
(.catch (fn [err]
(on-done)
(on-fail err))))))
(rf/reg-fx
:effects.biometric/check-if-available
(fn [{:keys [key-uid on-success on-fail]
:or {on-success identity
on-fail identity}}]
(keychain/can-save-user-password?
(fn [can-save?]
(if-not can-save?
(on-fail (ex-info "cannot-save-user-password"
{:effect :effects.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]
(if (= auth-method constant/auth-method-biometric)
(on-success auth-method)
(throw (js/Error. "biometric-not-enabled")))))
(.catch (fn [err]
(let [message (.-message err)]
(on-fail (ex-info message
{:err err
:effect :effects.biometric/check-if-available}))
(when-not (or (= message "biometric-not-available")
(= message "biometric-not-enabled"))
(log/error "Failed to check if biometrics is available"
{:error err
:key-uid key-uid
:effect :effects.biometric/check-biometric})))))))))))

View File

@ -1,120 +1,62 @@
(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]
[status-im.common.keychain.events :as keychain]
status-im.common.biometric.effects
[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 set-supported-type
[{:keys [db]} [supported-type]]
{:db (assoc-in db [:biometrics :supported-type] supported-type)})
(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)))
(rf/reg-event-fx :biometric/set-supported-type set-supported-type)
(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
(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?))
(-> (biometrics/get-supported-type)
(.then (fn [type]
(rf/dispatch [:biometric/get-supported-biometric-type-success type])))))))))
(rf/defn get-supported-biometric-auth-success
{:events [:biometric/get-supported-biometric-type-success]}
[{: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
::biometrics/not-available}
(defn show-message
[_ [code]]
(let [content (if (#{:biometrics/not-enrolled-error
:biometrics/not-available-error}
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}]]}))
(rf/reg-event-fx :biometric/show-message show-message)
(re-frame/reg-fx
:biometric/authenticate
(fn [{:keys [on-success on-fail prompt-message]}]
(-> (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))))
(.catch (fn [err]
(when on-fail
(on-fail err)))))))
(defn on-authentication-done
[{:keys [db]}]
{:db (assoc-in db [:biometrics :auth-pending?] false)})
(rf/defn authenticate
{:events [:biometric/authenticate]}
[_ opts]
{:biometric/authenticate opts})
(rf/reg-event-fx :biometric/on-authentication-done on-authentication-done)
(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}]})))
(defn authenticate
[{:keys [db]} [opts]]
(let [pending? (get-in db [:biometrics :auth-pending?])]
;;NOTE: prompting biometric check while another one is pending triggers error
(when-not pending?
{:db (assoc-in db [:biometrics :auth-pending?] true)
:fx [[:effects.biometric/authenticate
(assoc opts :on-done #(rf/dispatch [:biometric/on-authentication-done]))]]})))
(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 %])}]}))
(rf/reg-event-fx :biometric/authenticate authenticate)
(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})))
(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/enable enable-biometrics)
(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]]}))
(rf/reg-event-fx :biometric/disable disable-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}))))))))))

View File

@ -0,0 +1,68 @@
(ns status-im.common.biometric.events-test
(:require [cljs.test :refer [deftest testing is]]
matcher-combinators.test
[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-in (:db cofx) [:biometrics :supported-type] supported-type)}]
(is (match? expected (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-error])))))
(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 {:biometrics {: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]))))))
(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))))))

View File

@ -0,0 +1,22 @@
(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]))
(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))

View File

@ -1,11 +1,93 @@
(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 [[:effects.biometric/check-if-available
{:key-uid key-uid
:on-success #(rf/dispatch [:standard-auth/authorize-with-biometric args])
:on-fail #(rf/dispatch [: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 #(rf/dispatch [:standard-auth/authorize-with-password
args-with-biometric-btn])
:on-success #(rf/dispatch [:standard-auth/on-biometric-success on-auth-success])
:on-fail (fn [err]
(when on-auth-fail (on-auth-fail err))
(rf/dispatch [:standard-auth/on-biometric-fail err]))}]]]}))
(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]]
[:dispatch [:standard-auth/reset-login-password]]]}))
(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
[_ [error]]
(log/error (ex-message error)
(-> error
ex-data
(assoc :code (ex-cause error)
:event :standard-auth/on-biometric-fail)))
{:fx [[:dispatch [:standard-auth/reset-login-password]]
[: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]
(rf/dispatch [:standard-auth/reset-login-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 (fn []
(rf/dispatch [:standard-auth/reset-login-password])
(when 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

View File

@ -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]]]
:any])
(def ?authorize-with-biometric
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple ?authorize-map]]]
:any])
(def ?on-biometric-success
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple fn?]]]
:any])
(def ?on-biometrics-fail
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple
[:maybe fn?]
[:maybe :schema.common/exception]]]]
:any])
(def ?authorize-with-password
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:tuple ?authorize-map]]]
:any])

View File

@ -56,6 +56,7 @@
error? (boolean (seq error-message))
default-value (rn/use-ref-atom "") ;;bug on Android
;;https://github.com/status-im/status-mobile/issues/19004
theme (quo.theme/use-theme-value)
on-change-password (rn/use-callback
(fn [entered-password]
(reset! default-value entered-password)
@ -69,6 +70,7 @@
[quo/input
{:container-style {:flex 1}
:type :password
:theme theme
:default-value @default-value
:blur? blur?
:disabled? processing

View File

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

View File

@ -3,7 +3,6 @@
[quo.core :as quo]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[status-im.common.standard-authentication.standard-auth.authorize :as authorize]
[status-im.constants :as constants]
[utils.re-frame :as rf]))
@ -16,14 +15,15 @@
biometric-auth? (= auth-method constants/auth-method-biometric)
on-complete (rn/use-callback
(fn [reset]
(authorize/authorize {:on-close #(js/setTimeout reset 200)
: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}))
(rf/dispatch [:standard-auth/authorize
{:on-close #(js/setTimeout reset 200)
: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}]))
[theme])]
[quo/slide-button
{:container-style container-style

View File

@ -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.blacklist :as blacklist]
[status-im.common.parallax.view :as parallax]
[status-im.common.resources :as resources]
@ -24,7 +24,7 @@
(defn enable-biometrics-buttons
[insets]
(let [supported-biometric-type (rf/sub [:biometric/supported-type])
(let [supported-biometric-type (rf/sub [:biometrics/supported-type])
bio-type-label (biometric/get-label-by-type supported-biometric-type)
profile-color (or (:color (rf/sub [:onboarding/profile]))
(rf/sub [:profile/customization-color]))

View File

@ -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]}
@ -85,7 +87,7 @@
(rf/defn password-set
{:events [:onboarding/password-set]}
[{:keys [db]} password]
(let [supported-type (:biometric/supported-type db)]
(let [supported-type (get-in db [:biometrics :supported-type])]
{:db (-> db
(assoc-in [:onboarding/profile :password] password)
(assoc-in [:onboarding/profile :auth-method] constants/auth-method-password))
@ -96,7 +98,7 @@
(rf/defn navigate-to-enable-biometrics
{:events [:onboarding/navigate-to-enable-biometrics]}
[{:keys [db]}]
(let [supported-type (:biometric/supported-type db)]
(let [supported-type (get-in db [:biometrics :supported-type])]
{:dispatch (if supported-type
[:open-modal :enable-biometrics]
[:open-modal :enable-notifications])}))

View File

@ -9,9 +9,7 @@
(defn page-illustration
[width]
{:resize-mode :stretch
:resize-method :scale
:width width
{:width width
:margin-top 12
:margin-bottom 4})

View File

@ -45,8 +45,10 @@
:on-press #(rf/dispatch [:navigate-back-within-stack :enable-notifications])}]
[page-title]
[rn/image
{:style (style/page-illustration (:width window))
:source (resources/get-image :welcome-illustration)}]
{:style (style/page-illustration (:width window))
:resize-mode :stretch
:resize-method :scale
:source (resources/get-image :welcome-illustration)}]
[rn/view {:style (style/buttons insets)}
(when rn/small-screen?
[linear-gradient/linear-gradient

View File

@ -177,12 +177,15 @@
(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])]})
{:effects.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 +221,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]}

View File

@ -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,21 +13,22 @@
(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-auth-success (fn [password]
(rf/dispatch [:hide-bottom-sheet])
(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]
(let [auth-method (rf/sub [:auth-method])
biometric-type (rf/sub [:biometric/supported-type])
biometric-type (rf/sub [:biometrics/supported-type])
label (biometric/get-label-by-type biometric-type)
icon (biometric/get-icon-by-type biometric-type)
supported? (boolean biometric-type)
@ -45,7 +45,7 @@
:action-props {:disabled? (not supported?)
:on-change press-handler
:checked? biometric-on?}
:on-press press-handler}))
:on-press (when supported? press-handler)}))
(defn- get-change-password-item
[]

View File

@ -39,7 +39,7 @@
{:db db/app-db
:theme/init-theme nil
:network/listen-to-network-info nil
:biometric/get-supported-biometric-type nil
:effects.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

View File

@ -0,0 +1,8 @@
(ns status-im.subs.biometrics
(:require [re-frame.core :as rf]))
(rf/reg-sub
:biometrics/supported-type
:<- [:biometrics]
(fn [biometrics]
(get biometrics :supported-type)))

View File

@ -3,6 +3,7 @@
[re-frame.core :as re-frame]
status-im.subs.activity-center
status-im.subs.alert-banner
status-im.subs.biometrics
status-im.subs.chats
status-im.subs.communities
status-im.subs.contact
@ -57,7 +58,6 @@
(reg-root-key-sub :networks/manage :networks/manage)
(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 :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)
@ -159,6 +159,9 @@
;;wallet
(reg-root-key-sub :wallet :wallet)
;;biometrics
(reg-root-key-sub :biometrics :biometrics)
;;debug
(when js/goog.DEBUG
(reg-root-key-sub :dev/previewed-component :dev/previewed-component))

View File

@ -0,0 +1,94 @@
(ns tests.integration-test.standard-auth-test
(:require
[cljs.test :refer [deftest testing is]]
[day8.re-frame.test :as rf-test]
re-frame.core
[test-helpers.integration :as h]
[utils.re-frame :as rf]))
(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})
(defn auth-success-fixtures
[]
(rf/reg-fx :effects.biometric/check-if-available
(fn [{:keys [on-success]}] (on-success)))
(rf/reg-event-fx :biometric/authenticate
(fn [_ [{:keys [on-success]}]] (on-success)))
(rf/reg-fx :keychain/get-user-password
(fn [[_ on-success]] (on-success))))
(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
(auth-success-fixtures)
(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?))))))
(defn auth-cancel-fixtures
[]
(rf/reg-fx :effects.biometric/check-if-available
(fn [{:keys [on-success]}] (on-success)))
(rf/reg-event-fx :biometric/authenticate
(fn [_ [{:keys [on-cancel]}]] (on-cancel)))
(rf/reg-event-fx :show-bottom-sheet identity))
(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
(auth-cancel-fixtures)
(rf/dispatch [:standard-auth/authorize default-args])
(rf-test/wait-for [:show-bottom-sheet]
(is true)))))
(defn auth-fail-fixtures
[expected-error-cause]
(rf/reg-fx :effects.biometric/check-if-available
(fn [{:keys [on-success]}] (on-success)))
(rf/reg-event-fx :biometric/authenticate
(fn [_ [{:keys [on-fail]}]] (on-fail (ex-info "error" {} expected-error-cause))))
(rf/reg-event-fx :biometric/show-message identity))
(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)))]
(auth-fail-fixtures expected-error-cause)
(rf/dispatch [:standard-auth/authorize args])
(rf-test/wait-for [:biometric/show-message]
(is @on-fail-called?)
(is (= expected-error-cause (ex-cause @error))))))))
(defn auth-password-fallback-fixtures
[]
(rf/reg-fx :effects.biometric/check-if-available
(fn [{:keys [on-fail]}] (on-fail)))
(rf/reg-event-fx :show-bottom-sheet identity))
(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
(auth-password-fallback-fixtures)
(rf/dispatch [:standard-auth/authorize default-args])
(rf-test/wait-for [:standard-auth/authorize-with-password]
(is true)))))