From db009a58cbc51d4d87e7cd4b6201554b8d046128 Mon Sep 17 00:00:00 2001 From: Joshua McCloskey Date: Tue, 10 May 2022 05:18:20 +0000 Subject: [PATCH] Add virtual Face HAL Fixes: 230514750 Test: atest VtsHalBiometricsFaceTargetTest Test: atest VtsHalBiometricsFingerprintTargetTest Test: atest android.hardware.biometrics.face.FakeFaceEngineTest Test: atest android.hardware.biometrics.face.FakeFingerprintEngineTest Test: See README.md Test: Verified that face and fingerprint get reset upon authenticating. Change-Id: I57c1a61bec960e3be28736e6050be662ef412d8c --- biometrics/common/util/include/util/Util.h | 69 ++++ biometrics/face/aidl/default/Android.bp | 33 ++ biometrics/face/aidl/default/Face.cpp | 8 +- .../face/aidl/default/FakeFaceEngine.cpp | 318 +++++++++++++++ biometrics/face/aidl/default/FakeFaceEngine.h | 65 +++ biometrics/face/aidl/default/README.md | 77 ++++ biometrics/face/aidl/default/Session.cpp | 126 +++--- biometrics/face/aidl/default/Session.h | 11 +- ...e.biometrics.face.VirtualProps-current.txt | 98 +++++ biometrics/face/aidl/default/face.sysprop | 159 ++++++++ .../aidl/default/tests/FakeFaceEngineTest.cpp | 383 ++++++++++++++++++ .../aidl/default/FakeFingerprintEngine.cpp | 50 +-- 12 files changed, 1282 insertions(+), 115 deletions(-) create mode 100644 biometrics/common/util/include/util/Util.h create mode 100644 biometrics/face/aidl/default/FakeFaceEngine.cpp create mode 100644 biometrics/face/aidl/default/FakeFaceEngine.h create mode 100644 biometrics/face/aidl/default/README.md create mode 100644 biometrics/face/aidl/default/api/android.hardware.biometrics.face.VirtualProps-current.txt create mode 100644 biometrics/face/aidl/default/face.sysprop create mode 100644 biometrics/face/aidl/default/tests/FakeFaceEngineTest.cpp diff --git a/biometrics/common/util/include/util/Util.h b/biometrics/common/util/include/util/Util.h new file mode 100644 index 0000000000..29ec0f865e --- /dev/null +++ b/biometrics/common/util/include/util/Util.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +namespace aidl::android::hardware::biometrics { + +#define SLEEP_MS(x) \ + if (x > 0) std::this_thread::sleep_for(std::chrono::milliseconds(x)) +#define BEGIN_OP(x) \ + do { \ + LOG(INFO) << __func__; \ + SLEEP_MS(x); \ + } while (0) +#define IS_TRUE(x) ((x == "1") || (x == "true")) + +// This is for non-test situations, such as casual cuttlefish users, that don't +// set an explicit value. +// Some operations (i.e. enroll, authenticate) will be executed in tight loops +// by parts of the UI or fail if there is no latency. For example, the +// Face settings page constantly runs auth and the enrollment UI uses a +// cancel/restart cycle that requires some latency while the activities change. +#define DEFAULT_LATENCY 800 + +class Util { + public: + static int64_t getSystemNanoTime() { + timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return now.tv_sec * 1000000000LL + now.tv_nsec; + } + + static bool hasElapsed(int64_t start, int64_t durationMillis) { + auto now = getSystemNanoTime(); + if (now < start) return true; + if (durationMillis <= 0) return true; + return ((now - start) / 1000000LL) > durationMillis; + } + + static std::vector split(const std::string& str, const std::string& sep) { + std::regex regex(sep); + std::vector parts( + std::sregex_token_iterator(str.begin(), str.end(), regex, -1), + std::sregex_token_iterator()); + return parts; + } +}; + +} // namespace aidl::android::hardware::biometrics \ No newline at end of file diff --git a/biometrics/face/aidl/default/Android.bp b/biometrics/face/aidl/default/Android.bp index 7f66ecaf83..48c929bc42 100644 --- a/biometrics/face/aidl/default/Android.bp +++ b/biometrics/face/aidl/default/Android.bp @@ -18,10 +18,43 @@ cc_binary { "libbinder_ndk", "android.hardware.biometrics.face-V2-ndk", "android.hardware.biometrics.common-V2-ndk", + "android.hardware.biometrics.common.thread", + "android.hardware.biometrics.common.util", ], srcs: [ "main.cpp", "Face.cpp", + "FakeFaceEngine.cpp", "Session.cpp", ], + static_libs: ["android.hardware.biometrics.face.VirtualProps"], +} + +sysprop_library { + name: "android.hardware.biometrics.face.VirtualProps", + srcs: ["face.sysprop"], + property_owner: "Vendor", + vendor: true, +} + +cc_test { + name: "android.hardware.biometrics.face.FakeFaceEngineTest", + srcs: [ + "tests/FakeFaceEngineTest.cpp", + "FakeFaceEngine.cpp", + ], + shared_libs: [ + "libbase", + "libbinder_ndk", + ], + static_libs: [ + "android.hardware.biometrics.face.VirtualProps", + "android.hardware.biometrics.face-V2-ndk", + "android.hardware.biometrics.common-V2-ndk", + "android.hardware.keymaster-V3-ndk", + "android.hardware.biometrics.common.util", + ], + vendor: true, + test_suites: ["general-tests"], + require_root: true, } diff --git a/biometrics/face/aidl/default/Face.cpp b/biometrics/face/aidl/default/Face.cpp index aca3e135f6..652a7e1f6c 100644 --- a/biometrics/face/aidl/default/Face.cpp +++ b/biometrics/face/aidl/default/Face.cpp @@ -17,12 +17,14 @@ #include "Face.h" #include "Session.h" +#include "FakeFaceEngine.h" + namespace aidl::android::hardware::biometrics::face { const int kSensorId = 4; -const common::SensorStrength kSensorStrength = common::SensorStrength::STRONG; +const common::SensorStrength kSensorStrength = FakeFaceEngine::GetSensorStrength(); const int kMaxEnrollmentsPerUser = 5; -const FaceSensorType kSensorType = FaceSensorType::RGB; +const FaceSensorType kSensorType = FakeFaceEngine::GetSensorType(); const bool kHalControlsPreview = true; const std::string kHwComponentId = "faceSensor"; const std::string kHardwareVersion = "vendor/model/revision"; @@ -69,7 +71,7 @@ ndk::ScopedAStatus Face::getSensorProps(std::vector* return_val) { ndk::ScopedAStatus Face::createSession(int32_t /*sensorId*/, int32_t /*userId*/, const std::shared_ptr& cb, std::shared_ptr* return_val) { - *return_val = SharedRefBase::make(cb); + *return_val = SharedRefBase::make(std::make_unique(), cb); return ndk::ScopedAStatus::ok(); } diff --git a/biometrics/face/aidl/default/FakeFaceEngine.cpp b/biometrics/face/aidl/default/FakeFaceEngine.cpp new file mode 100644 index 0000000000..0f088f4331 --- /dev/null +++ b/biometrics/face/aidl/default/FakeFaceEngine.cpp @@ -0,0 +1,318 @@ +#include "FakeFaceEngine.h" + +#include + +#include + +#include "util/CancellationSignal.h" +#include "util/Util.h" + +using namespace ::android::face::virt; + +namespace aidl::android::hardware::biometrics::face { + +FaceSensorType FakeFaceEngine::GetSensorType() { + std::string type = FaceHalProperties::type().value_or(""); + if (type == "IR") { + return FaceSensorType::IR; + } else { + FaceHalProperties::type("RGB"); + return FaceSensorType::RGB; + } +} + +common::SensorStrength FakeFaceEngine::GetSensorStrength() { + std::string strength = FaceHalProperties::strength().value_or(""); + if (strength == "convenience") { + return common::SensorStrength::CONVENIENCE; + } else if (strength == "weak") { + return common::SensorStrength::WEAK; + } else { + FaceHalProperties::strength("strong"); + return common::SensorStrength::STRONG; + } +} + +void FakeFaceEngine::generateChallengeImpl(ISessionCallback* cb) { + BEGIN_OP(0); + std::uniform_int_distribution dist; + auto challenge = dist(mRandom); + FaceHalProperties::challenge(challenge); + cb->onChallengeGenerated(challenge); +} + +void FakeFaceEngine::revokeChallengeImpl(ISessionCallback* cb, int64_t challenge) { + BEGIN_OP(0); + FaceHalProperties::challenge({}); + cb->onChallengeRevoked(challenge); +} +void FakeFaceEngine::getEnrollmentConfigImpl(ISessionCallback* /*cb*/, + std::vector* /*return_val*/) {} +void FakeFaceEngine::enrollImpl(ISessionCallback* cb, const keymaster::HardwareAuthToken& hat, + EnrollmentType /*enrollmentType*/, + const std::vector& /*features*/, + const std::future& cancel) { + BEGIN_OP(FaceHalProperties::operation_start_enroll_latency().value_or(0)); + // format is ",::,::... + auto nextEnroll = FaceHalProperties::next_enrollment().value_or(""); + // Erase the next enrollment + FaceHalProperties::next_enrollment({}); + + AuthenticationFrame frame; + frame.data.acquiredInfo = AcquiredInfo::START; + frame.data.vendorCode = 0; + cb->onAuthenticationFrame(frame); + + // Do proper HAT verification in the real implementation. + if (hat.mac.empty()) { + LOG(ERROR) << "Fail: hat"; + cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorError */); + return; + } + + if (FaceHalProperties::operation_enroll_fails().value_or(false)) { + LOG(ERROR) << "Fail: operation_enroll_fails"; + cb->onError(Error::VENDOR, 0 /* vendorError */); + return; + } + + auto parts = Util::split(nextEnroll, ","); + if (parts.size() < 2) { + LOG(ERROR) << "Fail: invalid next_enrollment for : " << nextEnroll; + cb->onError(Error::VENDOR, 0 /* vendorError */); + return; + } + + auto enrollmentId = std::stoi(parts[0]); + const int numBuckets = parts.size() - 1; + for (size_t i = 1; i < parts.size(); i++) { + auto enrollHit = Util::split(parts[i], ":"); + if (enrollHit.size() != 3) { + LOG(ERROR) << "Error when unpacking enrollment hit: " << parts[i]; + cb->onError(Error::VENDOR, 0 /* vendorError */); + } + std::string bucket = enrollHit[0]; + std::string delay = enrollHit[1]; + std::string succeeds = enrollHit[2]; + + SLEEP_MS(std::stoi(delay)); + + if (shouldCancel(cancel)) { + LOG(ERROR) << "Fail: cancel"; + cb->onError(Error::CANCELED, 0 /* vendorCode */); + return; + } + + if (!IS_TRUE(succeeds)) { // end and failed + LOG(ERROR) << "Fail: requested by caller: " << parts[i]; + cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorCode */); + return; + } + + EnrollmentFrame frame; + + frame.data.acquiredInfo = AcquiredInfo::GOOD; + frame.data.vendorCode = 0; + cb->onEnrollmentFrame(frame); + + frame.data.acquiredInfo = AcquiredInfo::VENDOR; + frame.data.vendorCode = std::stoi(bucket); + cb->onEnrollmentFrame(frame); + + int remainingBuckets = numBuckets - i; + if (remainingBuckets > 0) { + cb->onEnrollmentProgress(enrollmentId, remainingBuckets); + } + } + + auto enrollments = FaceHalProperties::enrollments(); + enrollments.push_back(enrollmentId); + FaceHalProperties::enrollments(enrollments); + LOG(INFO) << "enrolled : " << enrollmentId; + cb->onEnrollmentProgress(enrollmentId, 0); +} + +void FakeFaceEngine::authenticateImpl(ISessionCallback* cb, int64_t /*operationId*/, + const std::future& cancel) { + BEGIN_OP(FaceHalProperties::operation_authenticate_latency().value_or(0)); + + // Signal to the framework that we have begun authenticating. + AuthenticationFrame frame; + frame.data.acquiredInfo = AcquiredInfo::START; + frame.data.vendorCode = 0; + cb->onAuthenticationFrame(frame); + + // Also signal that we have opened the camera. + frame = {}; + frame.data.acquiredInfo = AcquiredInfo::FIRST_FRAME_RECEIVED; + frame.data.vendorCode = 0; + cb->onAuthenticationFrame(frame); + + auto now = Util::getSystemNanoTime(); + int64_t duration = FaceHalProperties::operation_authenticate_duration().value_or(0); + if (duration > 0) { + do { + SLEEP_MS(5); + } while (!Util::hasElapsed(now, duration)); + } + + if (FaceHalProperties::operation_authenticate_fails().value_or(false)) { + LOG(ERROR) << "Fail: operation_authenticate_fails"; + cb->onError(Error::VENDOR, 0 /* vendorError */); + return; + } + + if (FaceHalProperties::lockout().value_or(false)) { + LOG(ERROR) << "Fail: lockout"; + cb->onLockoutPermanent(); + cb->onError(Error::HW_UNAVAILABLE, 0 /* vendorError */); + return; + } + + if (shouldCancel(cancel)) { + LOG(ERROR) << "Fail: cancel"; + cb->onError(Error::CANCELED, 0 /* vendorCode */); + return; + } + + auto id = FaceHalProperties::enrollment_hit().value_or(0); + auto enrolls = FaceHalProperties::enrollments(); + auto isEnrolled = std::find(enrolls.begin(), enrolls.end(), id) != enrolls.end(); + if (id < 0 || !isEnrolled) { + LOG(ERROR) << (isEnrolled ? "invalid enrollment hit" : "Fail: not enrolled"); + cb->onAuthenticationFailed(); + cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorError */); + return; + } + + cb->onAuthenticationSucceeded(id, {} /* hat */); +} + +void FakeFaceEngine::detectInteractionImpl(ISessionCallback* cb, const std::future& cancel) { + BEGIN_OP(FaceHalProperties::operation_detect_interaction_latency().value_or(0)); + + if (FaceHalProperties::operation_detect_interaction_fails().value_or(false)) { + LOG(ERROR) << "Fail: operation_detect_interaction_fails"; + cb->onError(Error::VENDOR, 0 /* vendorError */); + return; + } + + if (shouldCancel(cancel)) { + LOG(ERROR) << "Fail: cancel"; + cb->onError(Error::CANCELED, 0 /* vendorCode */); + return; + } + + auto id = FaceHalProperties::enrollment_hit().value_or(0); + auto enrolls = FaceHalProperties::enrollments(); + auto isEnrolled = std::find(enrolls.begin(), enrolls.end(), id) != enrolls.end(); + if (id <= 0 || !isEnrolled) { + LOG(ERROR) << "Fail: not enrolled"; + cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorError */); + return; + } + + cb->onInteractionDetected(); +} + +void FakeFaceEngine::enumerateEnrollmentsImpl(ISessionCallback* cb) { + BEGIN_OP(0); + std::vector enrollments; + for (const auto& enrollmentId : FaceHalProperties::enrollments()) { + if (enrollmentId) { + enrollments.push_back(*enrollmentId); + } + } + cb->onEnrollmentsEnumerated(enrollments); +} + +void FakeFaceEngine::removeEnrollmentsImpl(ISessionCallback* cb, + const std::vector& enrollmentIds) { + BEGIN_OP(0); + + std::vector> newEnrollments; + for (const auto& enrollment : FaceHalProperties::enrollments()) { + auto id = enrollment.value_or(0); + if (std::find(enrollmentIds.begin(), enrollmentIds.end(), id) == enrollmentIds.end()) { + newEnrollments.emplace_back(id); + } + } + FaceHalProperties::enrollments(newEnrollments); + cb->onEnrollmentsRemoved(enrollmentIds); +} + +void FakeFaceEngine::getFeaturesImpl(ISessionCallback* cb) { + BEGIN_OP(0); + + if (FaceHalProperties::enrollments().empty()) { + cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorCode */); + return; + } + + std::vector featuresToReturn = {}; + for (const auto& feature : FaceHalProperties::features()) { + if (feature) { + featuresToReturn.push_back((Feature)(*feature)); + } + } + cb->onFeaturesRetrieved(featuresToReturn); +} + +void FakeFaceEngine::setFeatureImpl(ISessionCallback* cb, const keymaster::HardwareAuthToken& hat, + Feature feature, bool enabled) { + BEGIN_OP(0); + + if (FaceHalProperties::enrollments().empty()) { + LOG(ERROR) << "Unable to set feature, enrollments are empty"; + cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorCode */); + return; + } + + if (hat.mac.empty()) { + LOG(ERROR) << "Unable to set feature, invalid hat"; + cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorCode */); + return; + } + + auto features = FaceHalProperties::features(); + + auto itr = std::find_if(features.begin(), features.end(), [feature](const auto& theFeature) { + return *theFeature == (int)feature; + }); + + if (!enabled && (itr != features.end())) { + features.erase(itr); + } else if (enabled && (itr == features.end())) { + features.push_back((int)feature); + } + + FaceHalProperties::features(features); + cb->onFeatureSet(feature); +} + +void FakeFaceEngine::getAuthenticatorIdImpl(ISessionCallback* cb) { + BEGIN_OP(0); + // If this is a weak HAL return 0 per the spec. + if (GetSensorStrength() != common::SensorStrength::STRONG) { + cb->onAuthenticatorIdRetrieved(0); + } else { + cb->onAuthenticatorIdRetrieved(FaceHalProperties::authenticator_id().value_or(0)); + } +} + +void FakeFaceEngine::invalidateAuthenticatorIdImpl(ISessionCallback* cb) { + BEGIN_OP(0); + int64_t authenticatorId = FaceHalProperties::authenticator_id().value_or(0); + int64_t newId = authenticatorId + 1; + FaceHalProperties::authenticator_id(newId); + cb->onAuthenticatorIdInvalidated(newId); +} + +void FakeFaceEngine::resetLockoutImpl(ISessionCallback* cb, + const keymaster::HardwareAuthToken& /*hat*/) { + BEGIN_OP(0); + FaceHalProperties::lockout(false); + cb->onLockoutCleared(); +} + +} // namespace aidl::android::hardware::biometrics::face \ No newline at end of file diff --git a/biometrics/face/aidl/default/FakeFaceEngine.h b/biometrics/face/aidl/default/FakeFaceEngine.h new file mode 100644 index 0000000000..edb54ce0f8 --- /dev/null +++ b/biometrics/face/aidl/default/FakeFaceEngine.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include +#include + +namespace aidl::android::hardware::biometrics::face { + +namespace face = aidl::android::hardware::biometrics::face; +namespace common = aidl::android::hardware::biometrics::common; +namespace keymaster = aidl::android::hardware::keymaster; + +using aidl::android::hardware::common::NativeHandle; +// A fake engine that is backed by system properties instead of hardware. +class FakeFaceEngine { + public: + FakeFaceEngine() : mRandom(std::mt19937::default_seed) {} + + static face::FaceSensorType GetSensorType(); + static common::SensorStrength GetSensorStrength(); + void generateChallengeImpl(ISessionCallback* cb); + void revokeChallengeImpl(ISessionCallback* cb, int64_t challenge); + void getEnrollmentConfigImpl(ISessionCallback* cb, + std::vector* return_val); + void enrollImpl(ISessionCallback* cb, const keymaster::HardwareAuthToken& hat, + EnrollmentType enrollmentType, const std::vector& features, + const std::future& cancel); + void authenticateImpl(ISessionCallback* cb, int64_t operationId, + const std::future& cancel); + void detectInteractionImpl(ISessionCallback* cb, const std::future& cancel); + void enumerateEnrollmentsImpl(ISessionCallback* cb); + void removeEnrollmentsImpl(ISessionCallback* cb, const std::vector& enrollmentIds); + void getFeaturesImpl(ISessionCallback* cb); + void setFeatureImpl(ISessionCallback* cb, const keymaster::HardwareAuthToken& hat, + Feature feature, bool enabled); + void getAuthenticatorIdImpl(ISessionCallback* cb); + void invalidateAuthenticatorIdImpl(ISessionCallback* cb); + void resetLockoutImpl(ISessionCallback* cb, const keymaster::HardwareAuthToken& /*hat*/); + + std::mt19937 mRandom; +}; + +} // namespace aidl::android::hardware::biometrics::face \ No newline at end of file diff --git a/biometrics/face/aidl/default/README.md b/biometrics/face/aidl/default/README.md new file mode 100644 index 0000000000..16559733c0 --- /dev/null +++ b/biometrics/face/aidl/default/README.md @@ -0,0 +1,77 @@ +# Virtual Face HAL + +This is a virtual HAL implementation that is backed by system properties +instead of actual hardware. It's intended for testing and UI development +on debuggable builds to allow devices to masquerade as alternative device +types and for emulators. + +## Device Selection + +You can either run the FakeFaceEngine on a [real device](#actual-device) or a [virtual device/cuttlefish](#getting-started-on-a-virtual-device-cuttlefish). This document should +help you to get started on either one. + +After setting up a device, go ahead and try out [enrolling](#enrolling) & [authenticating](#authenticating) + +### Getting started on a Virtual Device (cuttlefish) + + +Note, I'm running this via a cloudtop virtual device. + +1. Setup cuttlefish on cloudtop, See [this](https://g3doc.corp.google.com/company/teams/android/teampages/acloud/getting_started.md?cl=head) for more details. +2. acloud create --local-image +3. Enter in the shell command to disable hidl + +```shell +$ adb root +$ adb shell settings put secure com.android.server.biometrics.AuthService.hidlDisabled 1 +$ adb reboot +``` +4. You should now be able to do fake enrollments and authentications (as seen down below) + +### Actual Device + +1. Modify your real devices make file (I.E. vendor/google/products/{YOUR_DEVICE}.mk) +2. Ensure that there is no other face HAL that is being included by the device +3. Add the following +``` +PRODUCT_COPY_FILES += \ + frameworks/native/data/etc/android.hardware.biometrics.face.xml:$(TARGET_COPY_OUT_PRODUCT)/etc/permissions/android.hardware.biometrics.face.xml + +PRODUCT_PACKAGES += \ + android.hardware.biometrics.face-service.example \ + +``` +4. Now build and flash m -j120 && flash +5. Run the following commands + +```shell +# This is a temporary workaround +$ adb root +$ adb shell setprop persist.vendor.face.virtual.type RGB +$ adb shell setprop persist.vendor.face.virtual.strength strong +$ adb shell locksettings set-pin 0000 +$ adb reboot +``` + +## Enrolling + +```shell +# authenticar_id,bucket_id:duration:(true|false).... +$ adb shell setprop vendor.face.virtual.next_enrollment 1,0:500:true,5:250:true,10:150:true,15:500:true +$ adb shell am start -n com.android.settings/.biometrics.face.FaceEnrollIntroduction +# If you would like to get rid of the enrollment, run the follwoing command +$ adb shell setprop persist.vendor.face.virtual.enrollments \"\" +``` + +## Authenticating + +```shell +# If enrollment hasn't been setup +$ adb shell setprop persist.vendor.face.virtual.enrollments 1 +$ adb shell cmd face sync +# After enrollment has been setup +$ adb shell setprop vendor.face.virtual.operation_authenticate_duration 800 +$ adb shell setprop vendor.face.virtual.enrollment_hit 1 +# Power button press to simulate auth +$ adb shell input keyevent 26 +``` diff --git a/biometrics/face/aidl/default/Session.cpp b/biometrics/face/aidl/default/Session.cpp index 984a1a99dc..1188459f3f 100644 --- a/biometrics/face/aidl/default/Session.cpp +++ b/biometrics/face/aidl/default/Session.cpp @@ -14,139 +14,135 @@ * limitations under the License. */ -#include #include #include "Session.h" namespace aidl::android::hardware::biometrics::face { -class CancellationSignal : public common::BnCancellationSignal { - private: - std::shared_ptr cb_; +constexpr size_t MAX_WORKER_QUEUE_SIZE = 5; - public: - explicit CancellationSignal(std::shared_ptr cb) : cb_(std::move(cb)) {} - - ndk::ScopedAStatus cancel() override { - cb_->onError(Error::CANCELED, 0 /* vendorCode */); - return ndk::ScopedAStatus::ok(); - } -}; - -Session::Session(std::shared_ptr cb) - : cb_(std::move(cb)), mRandom(std::mt19937::default_seed) {} +Session::Session(std::unique_ptr engine, std::shared_ptr cb) + : mEngine(std::move(engine)), mCb(std::move(cb)), mRandom(std::mt19937::default_seed) { + mThread = std::make_unique(MAX_WORKER_QUEUE_SIZE); +} ndk::ScopedAStatus Session::generateChallenge() { LOG(INFO) << "generateChallenge"; - if (cb_) { - std::uniform_int_distribution dist; - auto challenge = dist(mRandom); - cb_->onChallengeGenerated(challenge); - } + mThread->schedule(Callable::from([this] { mEngine->generateChallengeImpl(mCb.get()); })); return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus Session::revokeChallenge(int64_t challenge) { LOG(INFO) << "revokeChallenge"; - if (cb_) { - cb_->onChallengeRevoked(challenge); - } + mThread->schedule(Callable::from( + [this, challenge] { mEngine->revokeChallengeImpl(mCb.get(), challenge); })); return ndk::ScopedAStatus::ok(); } -ndk::ScopedAStatus Session::getEnrollmentConfig(EnrollmentType /*enrollmentType*/, - std::vector* return_val) { - *return_val = {}; +ndk::ScopedAStatus Session::getEnrollmentConfig( + EnrollmentType /*enrollmentType*/, std::vector* cancellationSignal) { + *cancellationSignal = {}; return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus Session::enroll( - const keymaster::HardwareAuthToken& /*hat*/, EnrollmentType /*enrollmentType*/, - const std::vector& /*features*/, - const std::optional& /*previewSurface*/, - std::shared_ptr* /*return_val*/) { + const keymaster::HardwareAuthToken& hat, EnrollmentType enrollmentType, + const std::vector& features, const std::optional& /*previewSurface*/, + std::shared_ptr* cancellationSignal) { LOG(INFO) << "enroll"; - if (cb_) { - cb_->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorError */); - } + std::promise cancellationPromise; + auto cancFuture = cancellationPromise.get_future(); + + mThread->schedule(Callable::from( + [this, hat, enrollmentType, features, cancFuture = std::move(cancFuture)] { + mEngine->enrollImpl(mCb.get(), hat, enrollmentType, features, cancFuture); + })); + + *cancellationSignal = SharedRefBase::make(std::move(cancellationPromise)); return ndk::ScopedAStatus::ok(); } -ndk::ScopedAStatus Session::authenticate(int64_t /*keystoreOperationId*/, - std::shared_ptr* return_val) { +ndk::ScopedAStatus Session::authenticate( + int64_t keystoreOperationId, + std::shared_ptr* cancellationSignal) { LOG(INFO) << "authenticate"; - if (cb_) { - cb_->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorCode */); - } - *return_val = SharedRefBase::make(cb_); + std::promise cancellationPromise; + auto cancFuture = cancellationPromise.get_future(); + + mThread->schedule( + Callable::from([this, keystoreOperationId, cancFuture = std::move(cancFuture)] { + mEngine->authenticateImpl(mCb.get(), keystoreOperationId, cancFuture); + })); + + *cancellationSignal = SharedRefBase::make(std::move(cancellationPromise)); return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus Session::detectInteraction( - std::shared_ptr* /*return_val*/) { + std::shared_ptr* cancellationSignal) { LOG(INFO) << "detectInteraction"; + std::promise cancellationPromise; + auto cancFuture = cancellationPromise.get_future(); + + mThread->schedule(Callable::from([this, cancFuture = std::move(cancFuture)] { + mEngine->detectInteractionImpl(mCb.get(), cancFuture); + })); + + *cancellationSignal = SharedRefBase::make(std::move(cancellationPromise)); return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus Session::enumerateEnrollments() { LOG(INFO) << "enumerateEnrollments"; - if (cb_) { - cb_->onEnrollmentsEnumerated(std::vector()); - } + mThread->schedule(Callable::from([this] { mEngine->enumerateEnrollmentsImpl(mCb.get()); })); return ndk::ScopedAStatus::ok(); } -ndk::ScopedAStatus Session::removeEnrollments(const std::vector& /*enrollmentIds*/) { +ndk::ScopedAStatus Session::removeEnrollments(const std::vector& enrollmentIds) { LOG(INFO) << "removeEnrollments"; - if (cb_) { - cb_->onEnrollmentsRemoved(std::vector()); - } + mThread->schedule(Callable::from( + [this, enrollmentIds] { mEngine->removeEnrollmentsImpl(mCb.get(), enrollmentIds); })); return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus Session::getFeatures() { LOG(INFO) << "getFeatures"; - if (cb_) { - // Must error out with UNABLE_TO_PROCESS when no faces are enrolled. - cb_->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorCode */); - } + mThread->schedule(Callable::from([this] { mEngine->getFeaturesImpl(mCb.get()); })); return ndk::ScopedAStatus::ok(); } -ndk::ScopedAStatus Session::setFeature(const keymaster::HardwareAuthToken& /*hat*/, - Feature /*feature*/, bool /*enabled*/) { +ndk::ScopedAStatus Session::setFeature(const keymaster::HardwareAuthToken& hat, Feature feature, + bool enabled) { LOG(INFO) << "setFeature"; + mThread->schedule(Callable::from([this, hat, feature, enabled] { + mEngine->setFeatureImpl(mCb.get(), hat, feature, enabled); + })); return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus Session::getAuthenticatorId() { LOG(INFO) << "getAuthenticatorId"; - if (cb_) { - cb_->onAuthenticatorIdRetrieved(0 /* authenticatorId */); - } + mThread->schedule(Callable::from([this] { mEngine->getAuthenticatorIdImpl(mCb.get()); })); return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus Session::invalidateAuthenticatorId() { LOG(INFO) << "invalidateAuthenticatorId"; - if (cb_) { - cb_->onAuthenticatorIdInvalidated(0); - } + mThread->schedule( + Callable::from([this] { mEngine->invalidateAuthenticatorIdImpl(mCb.get()); })); return ndk::ScopedAStatus::ok(); } -ndk::ScopedAStatus Session::resetLockout(const keymaster::HardwareAuthToken& /*hat*/) { +ndk::ScopedAStatus Session::resetLockout(const keymaster::HardwareAuthToken& hat) { LOG(INFO) << "resetLockout"; - if (cb_) { - cb_->onLockoutCleared(); - } + mThread->schedule(Callable::from([this, hat] { mEngine->resetLockoutImpl(mCb.get(), hat); })); return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus Session::close() { - if (cb_) { - cb_->onSessionClosed(); + if (mCb) { + mCb->onSessionClosed(); } return ndk::ScopedAStatus::ok(); } diff --git a/biometrics/face/aidl/default/Session.h b/biometrics/face/aidl/default/Session.h index 9db17d2df9..7ca6a1f4ff 100644 --- a/biometrics/face/aidl/default/Session.h +++ b/biometrics/face/aidl/default/Session.h @@ -21,6 +21,10 @@ #include #include +#include "FakeFaceEngine.h" +#include "thread/WorkerThread.h" +#include "util/CancellationSignal.h" + namespace aidl::android::hardware::biometrics::face { namespace common = aidl::android::hardware::biometrics::common; @@ -30,7 +34,7 @@ using aidl::android::hardware::common::NativeHandle; class Session : public BnSession { public: - explicit Session(std::shared_ptr cb); + explicit Session(std::unique_ptr engine, std::shared_ptr cb); ndk::ScopedAStatus generateChallenge() override; @@ -85,8 +89,11 @@ class Session : public BnSession { ndk::ScopedAStatus onContextChanged(const common::OperationContext& context) override; private: - std::shared_ptr cb_; + std::unique_ptr mEngine; + std::shared_ptr mCb; std::mt19937 mRandom; + std::unique_ptr mThread; + std::shared_ptr mCancellationSignal; }; } // namespace aidl::android::hardware::biometrics::face diff --git a/biometrics/face/aidl/default/api/android.hardware.biometrics.face.VirtualProps-current.txt b/biometrics/face/aidl/default/api/android.hardware.biometrics.face.VirtualProps-current.txt new file mode 100644 index 0000000000..95489205f6 --- /dev/null +++ b/biometrics/face/aidl/default/api/android.hardware.biometrics.face.VirtualProps-current.txt @@ -0,0 +1,98 @@ +props { + owner: Vendor + module: "android.face.virt.FaceHalProperties" + prop { + api_name: "authenticator_id" + type: Long + access: ReadWrite + prop_name: "vendor.face.virtual.authenticator_id" + } + prop { + api_name: "challenge" + type: Long + access: ReadWrite + prop_name: "vendor.face.virtual.challenge" + } + prop { + api_name: "enrollment_hit" + type: Integer + access: ReadWrite + prop_name: "vendor.face.virtual.enrollment_hit" + } + prop { + api_name: "enrollments" + type: IntegerList + access: ReadWrite + prop_name: "persist.vendor.face.virtual.enrollments" + } + prop { + api_name: "features" + type: IntegerList + access: ReadWrite + prop_name: "persist.vendor.face.virtual.features" + } + prop { + api_name: "lockout" + access: ReadWrite + prop_name: "vendor.face.virtual.lockout" + } + prop { + api_name: "next_enrollment" + type: String + access: ReadWrite + prop_name: "vendor.face.virtual.next_enrollment" + } + prop { + api_name: "operation_authenticate_duration" + type: Integer + access: ReadWrite + prop_name: "vendor.face.virtual.operation_authenticate_duration" + } + prop { + api_name: "operation_authenticate_fails" + access: ReadWrite + prop_name: "vendor.face.virtual.operation_authenticate_fails" + } + prop { + api_name: "operation_authenticate_latency" + type: Integer + access: ReadWrite + prop_name: "vendor.face.virtual.operation_authenticate_latency" + } + prop { + api_name: "operation_detect_interaction_fails" + access: ReadWrite + prop_name: "vendor.face.virtual.operation_detect_interaction_fails" + } + prop { + api_name: "operation_detect_interaction_latency" + type: Integer + access: ReadWrite + prop_name: "vendor.face.virtual.operation_detect_interaction_latency" + } + prop { + api_name: "operation_enroll_fails" + access: ReadWrite + prop_name: "vendor.face.virtual.operation_enroll_fails" + } + prop { + api_name: "operation_start_enroll_latency" + type: Integer + access: ReadWrite + prop_name: "vendor.face.virtual.operation_start_enroll_latency" + } + prop { + api_name: "strength" + type: String + access: ReadWrite + prop_name: "persist.vendor.face.virtual.strength" + enum_values: "convenience|weak|strong" + } + prop { + api_name: "type" + type: String + access: ReadWrite + prop_name: "persist.vendor.face.virtual.type" + enum_values: "IR|RGB" + } +} diff --git a/biometrics/face/aidl/default/face.sysprop b/biometrics/face/aidl/default/face.sysprop new file mode 100644 index 0000000000..6b0f37fec5 --- /dev/null +++ b/biometrics/face/aidl/default/face.sysprop @@ -0,0 +1,159 @@ +# face.sysprop +# module becomes static class (Java) / namespace (C++) for serving API +module: "android.face.virt.FaceHalProperties" +owner: Vendor + +# type of face sensor +prop { + prop_name: "persist.vendor.face.virtual.type" + type: String + scope: Public + access: ReadWrite + enum_values: "IR|RGB" + api_name: "type" +} + +# the strength of the sensor +prop { + prop_name: "persist.vendor.face.virtual.strength" + type: String + scope: Public + access: ReadWrite + enum_values: "convenience|weak|strong" + api_name: "strength" +} + +# ids of current enrollments +prop { + prop_name: "persist.vendor.face.virtual.enrollments" + type: IntegerList + scope: Public + access: ReadWrite + api_name: "enrollments" +} + +# List of features +prop { + prop_name: "persist.vendor.face.virtual.features" + type: IntegerList + scope: Public + access: ReadWrite + api_name: "features" +} + +# authenticate and detectInteraction will succeed with this +# enrollment id, when present, otherwise they will error +prop { + prop_name: "vendor.face.virtual.enrollment_hit" + type: Integer + scope: Public + access: ReadWrite + api_name: "enrollment_hit" +} + +# The initial latency for enrollment +prop { + prop_name: "vendor.face.virtual.operation_start_enroll_latency" + type: Integer + scope: Public + access: ReadWrite + api_name: "operation_start_enroll_latency" +} + +# the next enrollment in the format: +# ",::,..." +# for example: "0:1,0:100:1,1:200:1" indicating that bucket 0 took +# 50 milliseconds, bucket 1 took 100 milliseconds, bucket 2 took 200 milliseconds. +# Note that it is up to the configuration to determine how many buckets are required +# to complete an enrollment +prop { + prop_name: "vendor.face.virtual.next_enrollment" + type: String + scope: Public + access: ReadWrite + api_name: "next_enrollment" +} + +# value for getAuthenticatorId or 0 +prop { + prop_name: "vendor.face.virtual.authenticator_id" + type: Long + scope: Public + access: ReadWrite + api_name: "authenticator_id" +} + +# value for generateChallenge +prop { + prop_name: "vendor.face.virtual.challenge" + type: Long + scope: Public + access: ReadWrite + api_name: "challenge" +} + +# if locked out +prop { + prop_name: "vendor.face.virtual.lockout" + type: Boolean + scope: Public + access: ReadWrite + api_name: "lockout" +} + +# force all authenticate operations to fail +prop { + prop_name: "vendor.face.virtual.operation_authenticate_fails" + type: Boolean + scope: Public + access: ReadWrite + api_name: "operation_authenticate_fails" +} + +# force all detectInteraction operations to fail +prop { + prop_name: "vendor.face.virtual.operation_detect_interaction_fails" + type: Boolean + scope: Public + access: ReadWrite + api_name: "operation_detect_interaction_fails" +} + +# force all enroll operations to fail +prop { + prop_name: "vendor.face.virtual.operation_enroll_fails" + type: Boolean + scope: Public + access: ReadWrite + api_name: "operation_enroll_fails" +} + +# add a latency to authentication operations +# Note that this latency is the initial authentication latency that occurs before +# the HAL will send AcquiredInfo::START and AcquiredInfo::FIRST_FRAME_RECEIVED +prop { + prop_name: "vendor.face.virtual.operation_authenticate_latency" + type: Integer + scope: Public + access: ReadWrite + api_name: "operation_authenticate_latency" +} + +# add a latency to detectInteraction operations +prop { + prop_name: "vendor.face.virtual.operation_detect_interaction_latency" + type: Integer + scope: Public + access: ReadWrite + api_name: "operation_detect_interaction_latency" +} + +# millisecond duration for authenticate operations +# (waits for changes to enrollment_hit) +prop { + prop_name: "vendor.face.virtual.operation_authenticate_duration" + type: Integer + scope: Public + access: ReadWrite + api_name: "operation_authenticate_duration" +} diff --git a/biometrics/face/aidl/default/tests/FakeFaceEngineTest.cpp b/biometrics/face/aidl/default/tests/FakeFaceEngineTest.cpp new file mode 100644 index 0000000000..c8ad6b7dc3 --- /dev/null +++ b/biometrics/face/aidl/default/tests/FakeFaceEngineTest.cpp @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include + +#include "FakeFaceEngine.h" + +using namespace ::android::face::virt; +using namespace ::aidl::android::hardware::biometrics::face; +using namespace ::aidl::android::hardware::keymaster; + +namespace aidl::android::hardware::biometrics::face { + +class TestSessionCallback : public BnSessionCallback { + public: + ndk::ScopedAStatus onChallengeGenerated(int64_t challenge) override { + mLastChallenge = challenge; + return ndk::ScopedAStatus::ok(); + }; + ::ndk::ScopedAStatus onChallengeRevoked(int64_t challenge) override { + mLastChallengeRevoked = challenge; + return ndk::ScopedAStatus::ok(); + }; + ::ndk::ScopedAStatus onError(Error error, int32_t) override { + mError = error; + return ndk::ScopedAStatus::ok(); + }; + ::ndk::ScopedAStatus onEnrollmentProgress(int32_t enrollmentId, int32_t remaining) override { + if (remaining == 0) mLastEnrolled = enrollmentId; + return ndk::ScopedAStatus::ok(); + }; + + ::ndk::ScopedAStatus onAuthenticationSucceeded(int32_t enrollmentId, + const HardwareAuthToken&) override { + mLastAuthenticated = enrollmentId; + mAuthenticateFailed = false; + return ndk::ScopedAStatus::ok(); + }; + ::ndk::ScopedAStatus onAuthenticationFailed() override { + mLastAuthenticated = 0; + mAuthenticateFailed = true; + return ndk::ScopedAStatus::ok(); + }; + ::ndk::ScopedAStatus onInteractionDetected() override { + mInteractionDetectedCount++; + return ndk::ScopedAStatus::ok(); + }; + + ::ndk::ScopedAStatus onEnrollmentFrame(const EnrollmentFrame& frame) override { + mEnrollmentFrames.push_back(frame.data.vendorCode); + return ndk::ScopedAStatus::ok(); + } + + ::ndk::ScopedAStatus onEnrollmentsEnumerated( + const std::vector& enrollmentIds) override { + mLastEnrollmentsEnumerated = enrollmentIds; + return ndk::ScopedAStatus::ok(); + }; + ::ndk::ScopedAStatus onEnrollmentsRemoved(const std::vector& enrollmentIds) override { + mLastEnrollmentRemoved = enrollmentIds; + return ndk::ScopedAStatus::ok(); + }; + ::ndk::ScopedAStatus onAuthenticatorIdRetrieved(int64_t authenticatorId) override { + mLastAuthenticatorId = authenticatorId; + return ndk::ScopedAStatus::ok(); + }; + ::ndk::ScopedAStatus onAuthenticatorIdInvalidated(int64_t authenticatorId) override { + mLastAuthenticatorId = authenticatorId; + mAuthenticatorIdInvalidated = true; + return ndk::ScopedAStatus::ok(); + }; + ::ndk::ScopedAStatus onAuthenticationFrame(const AuthenticationFrame& /*authFrame*/) override { + return ndk::ScopedAStatus::ok(); + } + ::ndk::ScopedAStatus onLockoutPermanent() override { + mLockoutPermanent = true; + return ndk::ScopedAStatus::ok(); + }; + ::ndk::ScopedAStatus onLockoutTimed(int64_t /* timeout */) override { + return ndk::ScopedAStatus::ok(); + } + ::ndk::ScopedAStatus onLockoutCleared() override { + mLockoutPermanent = false; + return ndk::ScopedAStatus::ok(); + } + ::ndk::ScopedAStatus onSessionClosed() override { return ndk::ScopedAStatus::ok(); } + + ::ndk::ScopedAStatus onFeaturesRetrieved(const std::vector& features) override { + mFeatures = features; + return ndk::ScopedAStatus::ok(); + } + + ::ndk::ScopedAStatus onFeatureSet(Feature feature) override { + mLastFeatureSet = feature; + return ndk::ScopedAStatus::ok(); + } + + Error mError = Error::UNKNOWN; + int64_t mLastChallenge = -1; + int64_t mLastChallengeRevoked = -1; + int32_t mLastEnrolled = -1; + int32_t mLastAuthenticated = -1; + int64_t mLastAuthenticatorId = -1; + std::vector mLastEnrollmentsEnumerated; + std::vector mLastEnrollmentRemoved; + std::vector mFeatures; + Feature mLastFeatureSet; + std::vector mEnrollmentFrames; + bool mAuthenticateFailed = false; + bool mAuthenticatorIdInvalidated = false; + bool mLockoutPermanent = false; + int mInteractionDetectedCount = 0; +}; + +class FakeFaceEngineTest : public ::testing::Test { + protected: + void SetUp() override { + LOG(ERROR) << "JRM SETUP"; + mCallback = ndk::SharedRefBase::make(); + FaceHalProperties::enrollments({}); + FaceHalProperties::challenge({}); + FaceHalProperties::features({}); + FaceHalProperties::authenticator_id({}); + FaceHalProperties::strength(""); + } + + FakeFaceEngine mEngine; + std::shared_ptr mCallback; + std::promise mCancel; +}; + +TEST_F(FakeFaceEngineTest, one_eq_one) { + ASSERT_EQ(1, 1); +} + +TEST_F(FakeFaceEngineTest, GenerateChallenge) { + mEngine.generateChallengeImpl(mCallback.get()); + ASSERT_EQ(FaceHalProperties::challenge().value(), mCallback->mLastChallenge); +} + +TEST_F(FakeFaceEngineTest, RevokeChallenge) { + auto challenge = FaceHalProperties::challenge().value_or(10); + mEngine.revokeChallengeImpl(mCallback.get(), challenge); + ASSERT_FALSE(FaceHalProperties::challenge().has_value()); + ASSERT_EQ(challenge, mCallback->mLastChallengeRevoked); +} + +TEST_F(FakeFaceEngineTest, ResetLockout) { + FaceHalProperties::lockout(true); + mEngine.resetLockoutImpl(mCallback.get(), {}); + ASSERT_FALSE(mCallback->mLockoutPermanent); + ASSERT_FALSE(FaceHalProperties::lockout().value_or(true)); +} + +TEST_F(FakeFaceEngineTest, AuthenticatorId) { + FaceHalProperties::authenticator_id(50); + mEngine.getAuthenticatorIdImpl(mCallback.get()); + ASSERT_EQ(50, mCallback->mLastAuthenticatorId); + ASSERT_FALSE(mCallback->mAuthenticatorIdInvalidated); +} + +TEST_F(FakeFaceEngineTest, GetAuthenticatorIdWeakReturnsZero) { + FaceHalProperties::strength("weak"); + FaceHalProperties::authenticator_id(500); + mEngine.getAuthenticatorIdImpl(mCallback.get()); + ASSERT_EQ(0, mCallback->mLastAuthenticatorId); + ASSERT_FALSE(mCallback->mAuthenticatorIdInvalidated); +} + +TEST_F(FakeFaceEngineTest, AuthenticatorIdInvalidate) { + FaceHalProperties::authenticator_id(500); + mEngine.invalidateAuthenticatorIdImpl(mCallback.get()); + ASSERT_NE(500, FaceHalProperties::authenticator_id().value()); + ASSERT_TRUE(mCallback->mAuthenticatorIdInvalidated); +} + +TEST_F(FakeFaceEngineTest, Enroll) { + FaceHalProperties::next_enrollment("1,0:30:true,1:0:true,2:0:true,3:0:true,4:0:true"); + keymaster::HardwareAuthToken hat{.mac = {2, 4}}; + mEngine.enrollImpl(mCallback.get(), hat, {} /*enrollmentType*/, {} /*features*/, + mCancel.get_future()); + ASSERT_FALSE(FaceHalProperties::next_enrollment().has_value()); + ASSERT_EQ(1, FaceHalProperties::enrollments().size()); + ASSERT_EQ(1, FaceHalProperties::enrollments()[0].value()); + ASSERT_EQ(1, mCallback->mLastEnrolled); +} + +TEST_F(FakeFaceEngineTest, EnrollFails) { + FaceHalProperties::next_enrollment("1,0:30:true,1:0:true,2:0:true,3:0:true,4:0:false"); + keymaster::HardwareAuthToken hat{.mac = {2, 4}}; + mEngine.enrollImpl(mCallback.get(), hat, {} /*enrollmentType*/, {} /*features*/, + mCancel.get_future()); + ASSERT_FALSE(FaceHalProperties::next_enrollment().has_value()); + ASSERT_EQ(0, FaceHalProperties::enrollments().size()); +} + +TEST_F(FakeFaceEngineTest, EnrollCancel) { + FaceHalProperties::next_enrollment("1,0:30:true,1:0:true,2:0:true,3:0:true,4:0:false"); + keymaster::HardwareAuthToken hat{.mac = {2, 4}}; + mCancel.set_value(); + mEngine.enrollImpl(mCallback.get(), hat, {} /*enrollmentType*/, {} /*features*/, + mCancel.get_future()); + ASSERT_EQ(Error::CANCELED, mCallback->mError); + ASSERT_EQ(-1, mCallback->mLastEnrolled); + ASSERT_EQ(0, FaceHalProperties::enrollments().size()); + ASSERT_FALSE(FaceHalProperties::next_enrollment().has_value()); +} + +TEST_F(FakeFaceEngineTest, Authenticate) { + FaceHalProperties::enrollments({100}); + FaceHalProperties::enrollment_hit(100); + mEngine.authenticateImpl(mCallback.get(), 0 /* operationId*/, mCancel.get_future()); + + ASSERT_EQ(100, mCallback->mLastAuthenticated); + ASSERT_FALSE(mCallback->mAuthenticateFailed); +} + +TEST_F(FakeFaceEngineTest, AuthenticateCancel) { + FaceHalProperties::enrollments({100}); + FaceHalProperties::enrollment_hit(100); + mCancel.set_value(); + mEngine.authenticateImpl(mCallback.get(), 0 /* operationId*/, mCancel.get_future()); + ASSERT_EQ(Error::CANCELED, mCallback->mError); +} + +TEST_F(FakeFaceEngineTest, AuthenticateFailedForUnEnrolled) { + FaceHalProperties::enrollments({3}); + FaceHalProperties::enrollment_hit(100); + mEngine.authenticateImpl(mCallback.get(), 0 /* operationId*/, mCancel.get_future()); + ASSERT_EQ(Error::UNABLE_TO_PROCESS, mCallback->mError); + ASSERT_TRUE(mCallback->mAuthenticateFailed); +} + +TEST_F(FakeFaceEngineTest, DetectInteraction) { + FaceHalProperties::enrollments({100}); + FaceHalProperties::enrollment_hit(100); + ASSERT_EQ(0, mCallback->mInteractionDetectedCount); + mEngine.detectInteractionImpl(mCallback.get(), mCancel.get_future()); + ASSERT_EQ(1, mCallback->mInteractionDetectedCount); +} + +TEST_F(FakeFaceEngineTest, DetectInteractionCancel) { + FaceHalProperties::enrollments({100}); + FaceHalProperties::enrollment_hit(100); + mCancel.set_value(); + mEngine.detectInteractionImpl(mCallback.get(), mCancel.get_future()); + ASSERT_EQ(Error::CANCELED, mCallback->mError); +} + +TEST_F(FakeFaceEngineTest, GetFeatureEmpty) { + mEngine.getFeaturesImpl(mCallback.get()); + ASSERT_TRUE(mCallback->mFeatures.empty()); +} + +TEST_F(FakeFaceEngineTest, SetFeature) { + FaceHalProperties::enrollments({1}); + keymaster::HardwareAuthToken hat{.mac = {2, 4}}; + mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_ATTENTION, true); + auto features = mCallback->mFeatures; + ASSERT_TRUE(features.empty()); + ASSERT_EQ(Feature::REQUIRE_ATTENTION, mCallback->mLastFeatureSet); + + mEngine.getFeaturesImpl(mCallback.get()); + features = mCallback->mFeatures; + ASSERT_FALSE(features.empty()); + ASSERT_NE(features.end(), + std::find(features.begin(), features.end(), Feature::REQUIRE_ATTENTION)); +} + +TEST_F(FakeFaceEngineTest, ToggleFeature) { + FaceHalProperties::enrollments({1}); + keymaster::HardwareAuthToken hat{.mac = {2, 4}}; + mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_ATTENTION, true); + mEngine.getFeaturesImpl(mCallback.get()); + auto features = mCallback->mFeatures; + ASSERT_FALSE(features.empty()); + ASSERT_NE(features.end(), + std::find(features.begin(), features.end(), Feature::REQUIRE_ATTENTION)); + + mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_ATTENTION, false); + mEngine.getFeaturesImpl(mCallback.get()); + features = mCallback->mFeatures; + ASSERT_TRUE(features.empty()); +} + +TEST_F(FakeFaceEngineTest, TurningOffNonExistentFeatureDoesNothing) { + FaceHalProperties::enrollments({1}); + keymaster::HardwareAuthToken hat{.mac = {2, 4}}; + mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_ATTENTION, false); + mEngine.getFeaturesImpl(mCallback.get()); + auto features = mCallback->mFeatures; + ASSERT_TRUE(features.empty()); +} + +TEST_F(FakeFaceEngineTest, SetMultipleFeatures) { + FaceHalProperties::enrollments({1}); + keymaster::HardwareAuthToken hat{.mac = {2, 4}}; + mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_ATTENTION, true); + mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_DIVERSE_POSES, true); + mEngine.setFeatureImpl(mCallback.get(), hat, Feature::DEBUG, true); + mEngine.getFeaturesImpl(mCallback.get()); + auto features = mCallback->mFeatures; + ASSERT_EQ(3, features.size()); + ASSERT_NE(features.end(), + std::find(features.begin(), features.end(), Feature::REQUIRE_ATTENTION)); + ASSERT_NE(features.end(), + std::find(features.begin(), features.end(), Feature::REQUIRE_DIVERSE_POSES)); + ASSERT_NE(features.end(), std::find(features.begin(), features.end(), Feature::DEBUG)); +} + +TEST_F(FakeFaceEngineTest, SetMultipleFeaturesAndTurnOffSome) { + FaceHalProperties::enrollments({1}); + keymaster::HardwareAuthToken hat{.mac = {2, 4}}; + mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_ATTENTION, true); + mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_DIVERSE_POSES, true); + mEngine.setFeatureImpl(mCallback.get(), hat, Feature::DEBUG, true); + mEngine.setFeatureImpl(mCallback.get(), hat, Feature::DEBUG, false); + mEngine.getFeaturesImpl(mCallback.get()); + auto features = mCallback->mFeatures; + ASSERT_EQ(2, features.size()); + ASSERT_NE(features.end(), + std::find(features.begin(), features.end(), Feature::REQUIRE_ATTENTION)); + ASSERT_NE(features.end(), + std::find(features.begin(), features.end(), Feature::REQUIRE_DIVERSE_POSES)); + ASSERT_EQ(features.end(), std::find(features.begin(), features.end(), Feature::DEBUG)); +} + +TEST_F(FakeFaceEngineTest, Enumerate) { + FaceHalProperties::enrollments({120, 3}); + mEngine.enumerateEnrollmentsImpl(mCallback.get()); + auto enrolls = mCallback->mLastEnrollmentsEnumerated; + ASSERT_FALSE(enrolls.empty()); + ASSERT_NE(enrolls.end(), std::find(enrolls.begin(), enrolls.end(), 120)); + ASSERT_NE(enrolls.end(), std::find(enrolls.begin(), enrolls.end(), 3)); +} + +TEST_F(FakeFaceEngineTest, RemoveEnrollments) { + FaceHalProperties::enrollments({120, 3, 100}); + mEngine.removeEnrollmentsImpl(mCallback.get(), {120, 100}); + mEngine.enumerateEnrollmentsImpl(mCallback.get()); + auto enrolls = mCallback->mLastEnrollmentsEnumerated; + ASSERT_FALSE(enrolls.empty()); + ASSERT_EQ(enrolls.end(), std::find(enrolls.begin(), enrolls.end(), 120)); + ASSERT_NE(enrolls.end(), std::find(enrolls.begin(), enrolls.end(), 3)); + ASSERT_EQ(enrolls.end(), std::find(enrolls.begin(), enrolls.end(), 100)); +} + +TEST_F(FakeFaceEngineTest, ResetLockoutWithAuth) { + FaceHalProperties::lockout(true); + FaceHalProperties::enrollments({33}); + FaceHalProperties::enrollment_hit(33); + auto cancelFuture = mCancel.get_future(); + mEngine.authenticateImpl(mCallback.get(), 0 /* operationId*/, cancelFuture); + + ASSERT_TRUE(mCallback->mLockoutPermanent); + + mEngine.resetLockoutImpl(mCallback.get(), {} /* hat */); + ASSERT_FALSE(mCallback->mLockoutPermanent); + FaceHalProperties::enrollment_hit(33); + mEngine.authenticateImpl(mCallback.get(), 0 /* operationId*/, cancelFuture); + ASSERT_EQ(33, mCallback->mLastAuthenticated); + ASSERT_FALSE(mCallback->mAuthenticateFailed); +} + +} // namespace aidl::android::hardware::biometrics::face \ No newline at end of file diff --git a/biometrics/fingerprint/aidl/default/FakeFingerprintEngine.cpp b/biometrics/fingerprint/aidl/default/FakeFingerprintEngine.cpp index d0250af3bf..138caa0fe2 100644 --- a/biometrics/fingerprint/aidl/default/FakeFingerprintEngine.cpp +++ b/biometrics/fingerprint/aidl/default/FakeFingerprintEngine.cpp @@ -19,51 +19,11 @@ #include #include -#include -#include -#include #include "util/CancellationSignal.h" - -#define SLEEP_MS(x) \ - if (x > 0) std::this_thread::sleep_for(std::chrono::milliseconds(x)) -#define BEGIN_OP(x) \ - do { \ - LOG(INFO) << __func__; \ - SLEEP_MS(x); \ - } while (0) -#define IS_TRUE(x) ((x == "1") || (x == "true")) - -// This is for non-test situations, such as casual cuttlefish users, that don't -// set an explicit value. -// Some operations (i.e. enroll, authenticate) will be executed in tight loops -// by parts of the UI or fail if there is no latency. For example, the -// fingerprint settings page constantly runs auth and the enrollment UI uses a -// cancel/restart cycle that requires some latency while the activities change. -#define DEFAULT_LATENCY 2000 +#include "util/Util.h" using namespace ::android::fingerprint::virt; -using namespace ::aidl::android::hardware::biometrics::fingerprint; - -int64_t getSystemNanoTime() { - timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); - return now.tv_sec * 1000000000LL + now.tv_nsec; -} - -bool hasElapsed(int64_t start, int64_t durationMillis) { - auto now = getSystemNanoTime(); - if (now < start) return true; - if (durationMillis <= 0) return true; - return ((now - start) / 1000000LL) > durationMillis; -} - -std::vector split(const std::string& str, const std::string& sep) { - std::regex regex(sep); - std::vector parts(std::sregex_token_iterator(str.begin(), str.end(), regex, -1), - std::sregex_token_iterator()); - return parts; -} namespace aidl::android::hardware::biometrics::fingerprint { @@ -101,14 +61,14 @@ void FakeFingerprintEngine::enrollImpl(ISessionCallback* cb, // format is ":,,...: auto nextEnroll = FingerprintHalProperties::next_enrollment().value_or(""); - auto parts = split(nextEnroll, ":"); + auto parts = Util::split(nextEnroll, ":"); if (parts.size() != 3) { LOG(ERROR) << "Fail: invalid next_enrollment"; cb->onError(Error::VENDOR, 0 /* vendorError */); return; } auto enrollmentId = std::stoi(parts[0]); - auto progress = split(parts[1], ","); + auto progress = Util::split(parts[1], ","); for (size_t i = 0; i < progress.size(); i++) { auto left = progress.size() - i - 1; SLEEP_MS(std::stoi(progress[i])); @@ -141,7 +101,7 @@ void FakeFingerprintEngine::authenticateImpl(ISessionCallback* cb, int64_t /* op const std::future& cancel) { BEGIN_OP(FingerprintHalProperties::operation_authenticate_latency().value_or(DEFAULT_LATENCY)); - auto now = getSystemNanoTime(); + auto now = Util::getSystemNanoTime(); int64_t duration = FingerprintHalProperties::operation_authenticate_duration().value_or(0); do { if (FingerprintHalProperties::operation_authenticate_fails().value_or(false)) { @@ -172,7 +132,7 @@ void FakeFingerprintEngine::authenticateImpl(ISessionCallback* cb, int64_t /* op } SLEEP_MS(100); - } while (!hasElapsed(now, duration)); + } while (!Util::hasElapsed(now, duration)); LOG(ERROR) << "Fail: not enrolled"; cb->onAuthenticationFailed();