| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/sync/test/integration/webauthn_credentials_helper.h" |
| |
| #include <vector> |
| |
| #include "base/rand_util.h" |
| #include "chrome/browser/sync/test/integration/fake_server_match_status_checker.h" |
| #include "chrome/browser/sync/test/integration/single_client_status_change_checker.h" |
| #include "chrome/browser/sync/test/integration/sync_datatype_helper.h" |
| #include "chrome/browser/sync/test/integration/sync_integration_test_util.h" |
| #include "chrome/browser/sync/test/integration/sync_test.h" |
| #include "chrome/browser/webauthn/passkey_model_factory.h" |
| #include "components/sync/base/data_type.h" |
| #include "components/sync/protocol/sync_entity.pb.h" |
| #include "components/sync/protocol/webauthn_credential_specifics.pb.h" |
| #include "components/webauthn/core/browser/passkey_model.h" |
| #include "components/webauthn/core/browser/passkey_model_change.h" |
| #include "components/webauthn/core/browser/passkey_sync_bridge.h" |
| |
| namespace webauthn_credentials_helper { |
| |
| using sync_datatype_helper::test; |
| |
| namespace { |
| |
| // Passkey creation timestamps are assumed to increase monotonically to get |
| // expected behaviour around shadowed credentials. This global tracks the |
| // "current" timestamp, and is increased on each use. |
| int g_timestamp = 0; |
| |
| class WebAuthnCredentialsSyncIdEqualsChecker |
| : public MultiClientStatusChangeChecker { |
| public: |
| WebAuthnCredentialsSyncIdEqualsChecker() |
| : MultiClientStatusChangeChecker(test()->GetSyncServices()) {} |
| |
| // MultiClientStatusChangeChecker: |
| bool IsExitConditionSatisfied(std::ostream* os) override { |
| for (int i = 1; i < test()->num_clients(); ++i) { |
| if (GetModel(0).GetAllSyncIds() != GetModel(i).GetAllSyncIds()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| }; |
| |
| } // namespace |
| |
| PasskeySyncActiveChecker::PasskeySyncActiveChecker( |
| syncer::SyncServiceImpl* service) |
| : SingleClientStatusChangeChecker(service) {} |
| PasskeySyncActiveChecker::~PasskeySyncActiveChecker() = default; |
| |
| bool PasskeySyncActiveChecker::IsExitConditionSatisfied(std::ostream* os) { |
| return service()->GetActiveDataTypes().Has(syncer::WEBAUTHN_CREDENTIAL); |
| } |
| |
| LocalPasskeysChangedChecker::LocalPasskeysChangedChecker(int profile) |
| : profile_(profile) { |
| observation_.Observe(&GetModel(profile_)); |
| } |
| |
| LocalPasskeysChangedChecker::~LocalPasskeysChangedChecker() = default; |
| |
| bool LocalPasskeysChangedChecker::IsExitConditionSatisfied(std::ostream* os) { |
| return satisfied_; |
| } |
| |
| void LocalPasskeysChangedChecker::OnPasskeysChanged( |
| const std::vector<webauthn::PasskeyModelChange>& changes) { |
| satisfied_ = true; |
| CheckExitCondition(); |
| } |
| |
| void LocalPasskeysChangedChecker::OnPasskeyModelShuttingDown() { |
| observation_.Reset(); |
| } |
| |
| void LocalPasskeysChangedChecker::OnPasskeyModelIsReady(bool is_ready) {} |
| |
| LocalPasskeysMatchChecker::LocalPasskeysMatchChecker(int profile, |
| Matcher matcher) |
| : profile_(profile), matcher_(matcher) { |
| observation_.Observe(&GetModel(profile_)); |
| } |
| |
| LocalPasskeysMatchChecker::~LocalPasskeysMatchChecker() = default; |
| |
| bool LocalPasskeysMatchChecker::IsExitConditionSatisfied(std::ostream* os) { |
| *os << "Waiting for local passkeys to match: "; |
| testing::StringMatchResultListener result_listener; |
| const bool matches = testing::ExplainMatchResult( |
| matcher_, GetModel(profile_).GetAllPasskeys(), &result_listener); |
| *os << result_listener.str(); |
| return matches; |
| } |
| |
| void LocalPasskeysMatchChecker::OnPasskeysChanged( |
| const std::vector<webauthn::PasskeyModelChange>& changes) { |
| CheckExitCondition(); |
| } |
| |
| void LocalPasskeysMatchChecker::OnPasskeyModelShuttingDown() { |
| observation_.Reset(); |
| } |
| |
| void LocalPasskeysMatchChecker::OnPasskeyModelIsReady(bool is_ready) {} |
| |
| ServerPasskeysMatchChecker::ServerPasskeysMatchChecker(Matcher matcher) |
| : matcher_(matcher) {} |
| |
| ServerPasskeysMatchChecker::~ServerPasskeysMatchChecker() = default; |
| |
| bool ServerPasskeysMatchChecker::IsExitConditionSatisfied(std::ostream* os) { |
| *os << "Waiting for server passkeys to match: "; |
| std::vector<sync_pb::SyncEntity> entities = |
| fake_server()->GetSyncEntitiesByDataType(syncer::WEBAUTHN_CREDENTIAL); |
| testing::StringMatchResultListener result_listener; |
| const bool matches = |
| testing::ExplainMatchResult(matcher_, entities, &result_listener); |
| *os << result_listener.str(); |
| return matches; |
| } |
| |
| PasskeyChangeObservationChecker::PasskeyChangeObservationChecker( |
| int profile, |
| ChangeList expected_changes) |
| : profile_(profile), expected_changes_(std::move(expected_changes)) { |
| observation_.Observe(&GetModel(profile_)); |
| } |
| |
| PasskeyChangeObservationChecker::~PasskeyChangeObservationChecker() = default; |
| |
| bool PasskeyChangeObservationChecker::IsExitConditionSatisfied( |
| std::ostream* os) { |
| *os << "Waiting to observe change: "; |
| |
| if (expected_changes_.size() != changes_observed_.size()) { |
| *os << "Size mismatch: " << expected_changes_.size() << " vs " |
| << changes_observed_.size(); |
| return false; |
| } |
| for (const auto& change : changes_observed_) { |
| if (std::ranges::none_of( |
| expected_changes_, [&change](const auto& expected_change) { |
| return expected_change.first == change.type() && |
| expected_change.second == change.passkey().sync_id(); |
| })) { |
| *os << "Unexpected change type " << static_cast<int>(change.type()) |
| << ", id " << change.passkey().sync_id(); |
| return false; |
| } |
| } |
| *os << "Match"; |
| return true; |
| } |
| |
| void PasskeyChangeObservationChecker::OnPasskeysChanged( |
| const std::vector<webauthn::PasskeyModelChange>& changes) { |
| changes_observed_ = changes; |
| CheckExitCondition(); |
| } |
| |
| void PasskeyChangeObservationChecker::OnPasskeyModelShuttingDown() { |
| observation_.Reset(); |
| } |
| |
| void PasskeyChangeObservationChecker::OnPasskeyModelIsReady(bool is_ready) {} |
| |
| MockPasskeyModelObserver::MockPasskeyModelObserver( |
| webauthn::PasskeyModel* model) { |
| observation_.Observe(model); |
| } |
| |
| MockPasskeyModelObserver::~MockPasskeyModelObserver() = default; |
| |
| webauthn::PasskeySyncBridge& GetModel(int profile_idx) { |
| return *static_cast<webauthn::PasskeySyncBridge*>( |
| PasskeyModelFactory::GetForProfile(test()->GetProfile(profile_idx))); |
| } |
| |
| bool AwaitAllModelsMatch() { |
| return WebAuthnCredentialsSyncIdEqualsChecker().Wait(); |
| } |
| |
| sync_pb::WebauthnCredentialSpecifics NewPasskey() { |
| sync_pb::WebauthnCredentialSpecifics specifics; |
| specifics.set_sync_id(base::RandBytesAsString(16)); |
| specifics.set_credential_id(base::RandBytesAsString(16)); |
| specifics.set_rp_id(kTestRpId); |
| // Pick random user IDs so we don't accidentally create shadow chains. Use |
| // `NewShadowingPasskey` to explicitly test shadowing. |
| specifics.set_user_id(base::RandBytesAsString(16)); |
| specifics.set_creation_time(++g_timestamp); |
| // Set some random encrypted_data to ensure the model accepts the specifics as |
| // valid. |
| specifics.set_encrypted("a"); |
| return specifics; |
| } |
| |
| sync_pb::WebauthnCredentialSpecifics NewShadowingPasskey( |
| const sync_pb::WebauthnCredentialSpecifics& shadowed) { |
| sync_pb::WebauthnCredentialSpecifics specifics; |
| specifics.set_sync_id(base::RandBytesAsString(16)); |
| specifics.set_credential_id(base::RandBytesAsString(16)); |
| specifics.set_rp_id(shadowed.rp_id()); |
| specifics.set_user_id(shadowed.user_id()); |
| specifics.set_creation_time(++g_timestamp); |
| specifics.add_newly_shadowed_credential_ids(shadowed.credential_id()); |
| // Set some random encrypted_data to ensure the model accepts the specifics as |
| // valid. |
| specifics.set_encrypted("a"); |
| return specifics; |
| } |
| |
| } // namespace webauthn_credentials_helper |